From fbab340e3e4d7a216f2ca4d5bef4e7a51ecb3f69 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:45:41 -0700 Subject: [PATCH 01/34] Define the current VSTD profile ladder Reason: Establish only the full object and Graph numbered-profile semantics in current specifications, schemas, and runtime checks. Evidence: Normative sources, packaged byte copies, receipt schemas, mechanisms, specimens, and adversarial tests change together. Coordinate: VSTD-1 through VSTD-5; VSTD-Graph-1 through VSTD-Graph-5; release 1.2.0. Falsification: A retired partial-profile identifier remains active, packaged specification bytes differ, or a later profile strengthens an unmet prerequisite. Compatibility: Retired developmental specifications and readers are absent; current serialized identifiers are explicit and fail closed. --- examples/flagship_demo/README.md | 6 +- .../specimens/honest-unknown.json | 8 +- examples/flagship_demo/specimens/index.json | 8 +- .../specimens/inflated-tier.json | 8 +- .../specimens/poisoned-ancestor.json | 29 +- .../specimens/wrong-artifact.json | 8 +- examples/logits_constraint_kernel/README.md | 6 +- examples/logits_constraint_kernel/demo.py | 4 +- .../verification_geometry_residual/README.md | 16 +- .../geometry.json | 2 +- .../schema/vstd1_generic_run_receipt.json | 311 +++++++++++++++ receipts/schema/vstd1_receipt.json | 43 ++- receipts/schema/vstd2_receipt.json | 5 +- .../schema/vstd3_accelerator_profile.json | 1 + receipts/schema/vstd3_receipt.json | 1 + receipts/schema/vstd4_certificate.json | 3 +- receipts/schema/vstd4_receipt.json | 11 +- receipts/schema/vstd5_receipt.json | 5 +- receipts/schema/vstd_graph_receipt.json | 82 +++- src/verifier/constraints/kernel.py | 8 +- .../constraints/llguidance_backend.py | 4 +- src/verifier/constraints/postvalidate.py | 8 +- src/verifier/core/certificate.py | 14 +- src/verifier/core/checker.py | 102 ++++- src/verifier/core/depth.py | 107 ++++-- src/verifier/core/geometry.py | 101 +++-- src/verifier/core/grounding.py | 5 +- src/verifier/core/kernel.py | 13 +- src/verifier/core/provenance.py | 4 +- src/verifier/core/receipt.py | 48 ++- src/verifier/core/refutation.py | 11 +- src/verifier/core/reproducibility.py | 27 +- src/verifier/core/translation.py | 11 +- src/verifier/data/__init__.py | 6 +- src/verifier/data/graph_level.py | 134 ++++--- src/verifier/data/models.py | 105 ++++- src/verifier/data/policy.py | 6 +- src/verifier/data/receipt.py | 101 ++++- src/verifier/layer4/__init__.py | 4 +- src/verifier/layer4/availability.py | 18 +- src/verifier/layer4/challenge.py | 13 +- src/verifier/layer4/closure.py | 33 +- src/verifier/layer4/precommit.py | 7 +- src/verifier/layer4/surface.py | 8 +- src/verifier/runtime/demo.py | 17 +- src/verifier/specifications/LADDER.md | 363 ++++++++++++++---- src/verifier/specifications/VSTD-1.md | 192 +++++++++ src/verifier/specifications/VSTD-2.md | 363 ++++++++++++++++++ src/verifier/specifications/VSTD-3.md | 37 +- src/verifier/specifications/VSTD-4.md | 65 ++-- src/verifier/specifications/VSTD-5.md | 98 +++++ src/verifier/specifications/VSTD-Graph-1.md | 172 +++++++++ src/verifier/specifications/VSTD-Graph-2.md | 20 + src/verifier/specifications/VSTD-Graph-3.md | 23 ++ src/verifier/specifications/VSTD-Graph-4.md | 22 ++ src/verifier/specifications/VSTD-Graph-5.md | 21 + .../specifications/WIRE_IDENTIFIERS.md | 146 +++---- standard/LADDER.md | 363 ++++++++++++++---- standard/VSTD-1.md | 69 +++- standard/VSTD-2.md | 68 +++- standard/VSTD-3.md | 37 +- standard/VSTD-4.md | 65 ++-- standard/VSTD-5.md | 19 +- standard/VSTD-Graph-1.md | 38 +- standard/VSTD-Graph-2.md | 21 +- standard/VSTD-Graph-3.md | 18 +- standard/VSTD-Graph-4.md | 21 +- standard/VSTD-Graph-5.md | 15 +- standard/WIRE_IDENTIFIERS.md | 146 +++---- tests/test_assurance_flow_invariants.py | 81 ++++ tests/test_core_receipt_integrity.py | 82 ++++ tests/test_flagship_demo.py | 6 +- tests/test_gdc_certificate.py | 14 +- tests/test_graph_level.py | 107 +++++- tests/test_independent_checker.py | 42 +- tests/test_layer4.py | 28 +- tests/test_logits_constraint_kernel.py | 4 +- tests/test_packaged_specifications.py | 34 +- tests/test_public_data.py | 116 +++++- tests/test_refutation_certificate.py | 7 +- tests/test_verification_geometry.py | 13 +- tests/test_vstd4_depth.py | 56 ++- tests/test_vstd_schemas.py | 95 ++++- 83 files changed, 3731 insertions(+), 828 deletions(-) create mode 100644 receipts/schema/vstd1_generic_run_receipt.json create mode 100644 src/verifier/specifications/VSTD-1.md create mode 100644 src/verifier/specifications/VSTD-2.md create mode 100644 src/verifier/specifications/VSTD-5.md create mode 100644 src/verifier/specifications/VSTD-Graph-1.md create mode 100644 src/verifier/specifications/VSTD-Graph-2.md create mode 100644 src/verifier/specifications/VSTD-Graph-3.md create mode 100644 src/verifier/specifications/VSTD-Graph-4.md create mode 100644 src/verifier/specifications/VSTD-Graph-5.md create mode 100644 tests/test_assurance_flow_invariants.py create mode 100644 tests/test_core_receipt_integrity.py diff --git a/examples/flagship_demo/README.md b/examples/flagship_demo/README.md index 2fa6d93..edf5449 100644 --- a/examples/flagship_demo/README.md +++ b/examples/flagship_demo/README.md @@ -1,4 +1,6 @@ -# VSTD flagship adversarial demo +# Verifier Standard (VSTD) flagship adversarial demo + +> **Acronym:** JavaScript Object Notation (JSON). This is the shortest executable explanation of VSTD's intended behavior. It tests four failure boundaries rather than presenting a happy-path receipt and asking the reader to @@ -19,7 +21,7 @@ Only `--emit-specimens DIR` writes files, and only inside the named directory. | `wrong-artifact` | The clause grounding names a different artifact from the grounded fact. | `REJECTED` | | `honest-unknown` | A deterministic proof bound is exhausted. | `ACCEPTED/UNKNOWN` | | `inflated-tier` | A Horn formula claims the more expensive general-resolution tier. | `REJECTED` | -| `poisoned-ancestor` | Valid descendants conceal a transitive `REVOKED` source. | graph level `0`, named blocker, accepted refutation | +| `poisoned-ancestor` | Valid descendants conceal a transitive `REVOKED` source. | Graph candidate `0`, named blocker, accepted refutation | The poisoned-ancestor fixture declares object and edge ratings as inputs. Its graph refutation checks the collection ceiling; it does not establish or upgrade the separate diff --git a/examples/flagship_demo/specimens/honest-unknown.json b/examples/flagship_demo/specimens/honest-unknown.json index 834134f..a978cd7 100644 --- a/examples/flagship_demo/specimens/honest-unknown.json +++ b/examples/flagship_demo/specimens/honest-unknown.json @@ -28,9 +28,9 @@ ], "deterministic": true, "format_fragment": "UP,WIDTH-K,RES", - "implementation_hash": "sha256:49afe96d327f99b12a518529d47b1b68c16228c4688be14eca279ea3e5b14d5b", - "parser_hash": "sha256:a49fc58d15ec1b925b4e2bd48f5dfb350d9e9fca28044c5a0fa9ff051dc70c01", - "specification_hash": "sha256:f2420f7826531ffdfadb1fcb9c0d3317d330d47a1cc367328ebd1c6921af36eb" + "implementation_hash": "sha256:a7d3c7b3123e9932395eff82f231c53e5b701888abbd203a47f155644cd10a64", + "parser_hash": "sha256:c657e7777a850e584b7c010f8f7ff778b5269507db3eb0b2ac362a7cdac4cd67", + "specification_hash": "sha256:bce4666ddbcf73f460432990045b16e5197c3b03a1db5db096ba3220ae313205" } }, "certificate": { @@ -157,7 +157,7 @@ ] }, "header": { - "binding": "aee206fd7bc450c01bda6c54a4e26ba228ebf752b4085df56f1d250055a7c9cd", + "binding": "7a334822c49b2a8685e47b1dc05dabdead1ba980e3b907632cc9e0546db8b828", "clause_count": 3, "format": "VSTD4-GDC-1", "literal_count": 4, diff --git a/examples/flagship_demo/specimens/index.json b/examples/flagship_demo/specimens/index.json index 0dde150..6587a75 100644 --- a/examples/flagship_demo/specimens/index.json +++ b/examples/flagship_demo/specimens/index.json @@ -37,11 +37,11 @@ "title": "Inflated verification-cost claim" }, { - "details": "collection:demo holds at graph level 0. Level 1 is refuted by: STATUS_ADMISSIBILITY: artifact:source is REVOKED.", - "expected": "GRAPH-LEVEL-0; REVOKED blocker; checked refutation", - "observed": "GRAPH-LEVEL-0; REVOKED", + "details": "collection:demo computes to candidate Graph profile 0 from caller-supplied ratings; conformance is not established. Graph profile 1 is refuted by: STATUS_ADMISSIBILITY: artifact:source is REVOKED.", + "expected": "GRAPH-CANDIDATE-0; REVOKED blocker; checked refutation", + "observed": "GRAPH-CANDIDATE-0; REVOKED", "ok": true, - "question": "Does a poisoned transitive ancestor cap the collection's graph level?", + "question": "Does a poisoned transitive ancestor cap the collection's candidate Graph profile?", "scenario": "poisoned-ancestor", "title": "Revoked ancestor behind valid descendants" } diff --git a/examples/flagship_demo/specimens/inflated-tier.json b/examples/flagship_demo/specimens/inflated-tier.json index 481fe4d..3400606 100644 --- a/examples/flagship_demo/specimens/inflated-tier.json +++ b/examples/flagship_demo/specimens/inflated-tier.json @@ -28,9 +28,9 @@ ], "deterministic": true, "format_fragment": "UP,WIDTH-K,RES", - "implementation_hash": "sha256:49afe96d327f99b12a518529d47b1b68c16228c4688be14eca279ea3e5b14d5b", - "parser_hash": "sha256:a49fc58d15ec1b925b4e2bd48f5dfb350d9e9fca28044c5a0fa9ff051dc70c01", - "specification_hash": "sha256:f2420f7826531ffdfadb1fcb9c0d3317d330d47a1cc367328ebd1c6921af36eb" + "implementation_hash": "sha256:a7d3c7b3123e9932395eff82f231c53e5b701888abbd203a47f155644cd10a64", + "parser_hash": "sha256:c657e7777a850e584b7c010f8f7ff778b5269507db3eb0b2ac362a7cdac4cd67", + "specification_hash": "sha256:bce4666ddbcf73f460432990045b16e5197c3b03a1db5db096ba3220ae313205" } }, "certificate": { @@ -154,7 +154,7 @@ ] }, "header": { - "binding": "aee206fd7bc450c01bda6c54a4e26ba228ebf752b4085df56f1d250055a7c9cd", + "binding": "7a334822c49b2a8685e47b1dc05dabdead1ba980e3b907632cc9e0546db8b828", "clause_count": 3, "format": "VSTD4-GDC-1", "literal_count": 4, diff --git a/examples/flagship_demo/specimens/poisoned-ancestor.json b/examples/flagship_demo/specimens/poisoned-ancestor.json index e0ded0b..258ac64 100644 --- a/examples/flagship_demo/specimens/poisoned-ancestor.json +++ b/examples/flagship_demo/specimens/poisoned-ancestor.json @@ -1,9 +1,9 @@ { - "details": "collection:demo holds at graph level 0. Level 1 is refuted by: STATUS_ADMISSIBILITY: artifact:source is REVOKED.", - "expected": "GRAPH-LEVEL-0; REVOKED blocker; checked refutation", - "observed": "GRAPH-LEVEL-0; REVOKED", + "details": "collection:demo computes to candidate Graph profile 0 from caller-supplied ratings; conformance is not established. Graph profile 1 is refuted by: STATUS_ADMISSIBILITY: artifact:source is REVOKED.", + "expected": "GRAPH-CANDIDATE-0; REVOKED blocker; checked refutation", + "observed": "GRAPH-CANDIDATE-0; REVOKED", "ok": true, - "question": "Does a poisoned transitive ancestor cap the collection's graph level?", + "question": "Does a poisoned transitive ancestor cap the collection's candidate Graph profile?", "scenario": "poisoned-ancestor", "specimen": { "binding": { @@ -12,13 +12,13 @@ "memory_bound": 10000, "verification_cost_bound": 10000 }, - "claim": "compute the bounded graph level for collection:demo", + "claim": "compute the bounded candidate Graph profile for collection:demo", "coordinate": { "parameters": {}, "predicate": "vstd_graph_level", "subject": "collection:demo" }, - "evidence_root": "4cd50c61d6162451488a984b85a9a209e708095186132c33d1529adeeacd6ca5", + "evidence_root": "8d42243ad0124e5946c34fbd3d6175daf5712a3cd75560f47ec950885866e483", "policy_root": "e8e31ddeae93b0e85ec8cb26487489781efeab36ccef7676922a9e18b36155d8", "prior_commitment": "", "verifier": { @@ -28,9 +28,9 @@ ], "deterministic": true, "format_fragment": "UP,WIDTH-K,RES", - "implementation_hash": "sha256:49afe96d327f99b12a518529d47b1b68c16228c4688be14eca279ea3e5b14d5b", - "parser_hash": "sha256:a49fc58d15ec1b925b4e2bd48f5dfb350d9e9fca28044c5a0fa9ff051dc70c01", - "specification_hash": "sha256:f2420f7826531ffdfadb1fcb9c0d3317d330d47a1cc367328ebd1c6921af36eb" + "implementation_hash": "sha256:a7d3c7b3123e9932395eff82f231c53e5b701888abbd203a47f155644cd10a64", + "parser_hash": "sha256:c657e7777a850e584b7c010f8f7ff778b5269507db3eb0b2ac362a7cdac4cd67", + "specification_hash": "sha256:bce4666ddbcf73f460432990045b16e5197c3b03a1db5db096ba3220ae313205" } }, "collection": { @@ -48,7 +48,7 @@ "artifact:source": 5 } }, - "fixture_boundary": "Object and edge levels are declared scenario inputs. This graph-level refutation does not establish or upgrade their separate evidence.", + "fixture_boundary": "Object and edge profile ratings are declared scenario inputs. This Graph-profile refutation does not establish or upgrade their separate evidence.", "graph_result": { "blocking_obligations": [ { @@ -60,10 +60,12 @@ } ], "collection_id": "collection:demo", - "explanation": "collection:demo holds at graph level 0. Level 1 is refuted by: STATUS_ADMISSIBILITY: artifact:source is REVOKED.", + "conformance_status": "NOT_ESTABLISHED", + "explanation": "collection:demo computes to candidate Graph profile 0 from caller-supplied ratings; conformance is not established. Graph profile 1 is refuted by: STATUS_ADMISSIBILITY: artifact:source is REVOKED.", "level": 0, "max_level": 5, - "refutation_digest": "87e9b1889e745c83c4a2dfc3968eadc9a4015146884a99f892e70b2d04d27eed", + "rating_basis": "CALLER_SUPPLIED", + "refutation_digest": "d3f0af5c1aba3e79079cd017f54bea0472f7e08d3b65601084eca220303f3f49", "witness_digest": null }, "hypergraph": { @@ -120,6 +122,7 @@ "storage_uris": [] } ], + "conflicts": [], "contributors": [], "rights": [], "transformations": [ @@ -589,7 +592,7 @@ ] }, "header": { - "binding": "23c087d7ef52a1995c2f54f51940f82faef60dc5262627fcee68dd9c9d78eb8e", + "binding": "2e395dbb80e179a5aa41bf847816635c633615a27e107bce8c67a039c4494c88", "clause_count": 17, "format": "VSTD4-GDC-1", "literal_count": 25, diff --git a/examples/flagship_demo/specimens/wrong-artifact.json b/examples/flagship_demo/specimens/wrong-artifact.json index c419d74..e38c907 100644 --- a/examples/flagship_demo/specimens/wrong-artifact.json +++ b/examples/flagship_demo/specimens/wrong-artifact.json @@ -28,9 +28,9 @@ ], "deterministic": true, "format_fragment": "UP,WIDTH-K,RES", - "implementation_hash": "sha256:49afe96d327f99b12a518529d47b1b68c16228c4688be14eca279ea3e5b14d5b", - "parser_hash": "sha256:a49fc58d15ec1b925b4e2bd48f5dfb350d9e9fca28044c5a0fa9ff051dc70c01", - "specification_hash": "sha256:f2420f7826531ffdfadb1fcb9c0d3317d330d47a1cc367328ebd1c6921af36eb" + "implementation_hash": "sha256:a7d3c7b3123e9932395eff82f231c53e5b701888abbd203a47f155644cd10a64", + "parser_hash": "sha256:c657e7777a850e584b7c010f8f7ff778b5269507db3eb0b2ac362a7cdac4cd67", + "specification_hash": "sha256:bce4666ddbcf73f460432990045b16e5197c3b03a1db5db096ba3220ae313205" } }, "certificate": { @@ -162,7 +162,7 @@ ] }, "header": { - "binding": "aee206fd7bc450c01bda6c54a4e26ba228ebf752b4085df56f1d250055a7c9cd", + "binding": "7a334822c49b2a8685e47b1dc05dabdead1ba980e3b907632cc9e0546db8b828", "clause_count": 3, "format": "VSTD4-GDC-1", "literal_count": 4, diff --git a/examples/logits_constraint_kernel/README.md b/examples/logits_constraint_kernel/README.md index 2df0606..deedb61 100644 --- a/examples/logits_constraint_kernel/README.md +++ b/examples/logits_constraint_kernel/README.md @@ -1,9 +1,11 @@ # Logits Constraint Kernel demo +> **Acronym:** JavaScript Object Notation (JSON). + This example bypasses Outlines and calls `llguidance` 1.8.0 directly. It compiles a strict JSON Schema containing `patternProperties`, computes the packed allowed-token mask before every generated byte token, applies that mask to a real PyTorch logits -tensor, advances the native matcher, and independently post-validates the completed +tensor, advances the native matcher, and separately post-validates the completed JSON with `jsonschema` Draft 2020-12. ```powershell @@ -13,7 +15,7 @@ python examples/logits_constraint_kernel/demo.py `llguidance` is the only grammar engine. `torch` is only the tensor adapter and is normally already supplied by the model runtime; `jsonschema` comes through the test -profile solely for the independently selected post-validation facet. +profile solely for the separate post-validation facet. The generated `trace.json` binds the source constraint, native compiled grammar, tokenizer, every observed mask, every state transition, and the whole-output diff --git a/examples/logits_constraint_kernel/demo.py b/examples/logits_constraint_kernel/demo.py index 85c19df..a80b890 100644 --- a/examples/logits_constraint_kernel/demo.py +++ b/examples/logits_constraint_kernel/demo.py @@ -1,4 +1,6 @@ -"""Emit a real llguidance logits-mask trace for a schema Outlines 0.2.14 dropped.""" +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Emit a real llguidance logits-mask trace for a schema Outlines 0.2.14 dropped.""" from __future__ import annotations diff --git a/examples/verification_geometry_residual/README.md b/examples/verification_geometry_residual/README.md index 2add039..3126e2e 100644 --- a/examples/verification_geometry_residual/README.md +++ b/examples/verification_geometry_residual/README.md @@ -1,7 +1,10 @@ # Reconstruction residual and bounded closure -This example is the smallest VSTD-0.2 verification-geometry vertical slice. Its -machine-readable form is [`geometry.json`](geometry.json). +> **Acronym:** Verifier Standard (VSTD). + +This example is the smallest VSTD-2 verification-geometry vertical slice. Its +machine-readable form is [`geometry.json`](geometry.json) under the current `VSTD-2` +identifier. ## 1. Apparently complete decomposition @@ -53,8 +56,9 @@ the bounded geometry earn self-closure. ## 5. Higher-order verification without infinite workflow abstraction -`layer:v0` verifies the formatter surface. `layer:v1` treats the V0 geometry as a -secondary subject and verifies the immediately preceding layer. The typed validator -requires orders to be finite, contiguous, and adjacent. A skipped or recursively -invented workflow layer is invalid; an inability to continue is represented as a +The serialized compatibility identifiers `layer:v0` and `layer:v1` denote VSTD-2 +**verification orders**, not numbered VSTD profiles. V0 verifies the formatter surface; +V1 treats the V0 geometry as a secondary subject and verifies the immediately preceding +order. The typed validator requires orders to be finite, contiguous, and adjacent. A +skipped or recursively invented verification order is invalid; an inability to continue is represented as a horizon rather than hidden behind a trust assumption. diff --git a/examples/verification_geometry_residual/geometry.json b/examples/verification_geometry_residual/geometry.json index 841be14..53be8f8 100644 --- a/examples/verification_geometry_residual/geometry.json +++ b/examples/verification_geometry_residual/geometry.json @@ -204,7 +204,7 @@ "seam_id": "seam:locale-render" } ], - "schema_version": "VSTD-0.2", + "schema_version": "VSTD-2", "seams": [ { "label": "parsed value to renderer", diff --git a/receipts/schema/vstd1_generic_run_receipt.json b/receipts/schema/vstd1_generic_run_receipt.json new file mode 100644 index 0000000..63fbbfd --- /dev/null +++ b/receipts/schema/vstd1_generic_run_receipt.json @@ -0,0 +1,311 @@ +{ + "$comment": "Terminology: JavaScript Object Notation (JSON); Secure Hash Algorithm 256-bit (SHA-256); Verifier Standard (VSTD).", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://timelordraps.github.io/verifier/schemas/vstd1_generic_run_receipt.json", + "title": "VSTD-1 Generic Computational Run Receipt", + "description": "Strict shape for the VSTD-1 generic_computational_run profile.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "receipt_kind", + "receipt_id", + "canonical_digest", + "claim_title", + "claim_statement", + "claim_scope", + "claim_limitations", + "falsification_condition", + "source_state", + "inputs", + "outputs", + "execution", + "claims", + "provenance_linkage", + "reproducibility", + "assessment_context" + ], + "properties": { + "schema_version": { "const": "VSTD-1" }, + "receipt_kind": { "const": "generic_computational_run" }, + "receipt_id": { "type": "string", "minLength": 1 }, + "canonical_digest": { "$ref": "#/$defs/sha256" }, + "claim_title": { "type": "string" }, + "claim_statement": { "type": "string" }, + "claim_scope": { "type": "string" }, + "claim_limitations": { + "type": "array", + "items": { "type": "string" } + }, + "falsification_condition": { "type": "string" }, + "source_state": { + "type": "object", + "additionalProperties": false, + "required": [ + "target_name", + "portable_repository_id", + "local_repository_path", + "git", + "runtime", + "captured_at_utc", + "command_executed", + "source_file_hashes" + ], + "properties": { + "target_name": { "type": "string" }, + "portable_repository_id": { "type": "string" }, + "local_repository_path": { "type": "string" }, + "captured_at_utc": { "type": "string" }, + "command_executed": { "type": "string" }, + "source_file_hashes": { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/sha256" } + }, + "git": { + "type": "object", + "additionalProperties": false, + "required": ["commit_sha", "branch", "is_dirty"], + "properties": { + "commit_sha": { "type": "string" }, + "branch": { "type": "string" }, + "is_dirty": { "type": "boolean" }, + "dirty_files": { "type": "array", "items": { "type": "string" } }, + "untracked_files": { "type": "array", "items": { "type": "string" } }, + "remote_origin": { "type": "string" } + } + }, + "runtime": { + "type": "object", + "additionalProperties": false, + "required": ["python_version", "platform_system"], + "properties": { + "python_version": { "type": "string" }, + "python_implementation": { "type": "string" }, + "platform_system": { "type": "string" }, + "platform_release": { "type": "string" }, + "platform_machine": { "type": "string" }, + "hostname_masked": { "type": "string" } + } + } + } + }, + "inputs": { + "type": "array", + "items": { "$ref": "#/$defs/artifact" } + }, + "outputs": { + "type": "array", + "items": { "$ref": "#/$defs/artifact" } + }, + "execution": { + "type": "object", + "additionalProperties": false, + "required": [ + "command", + "cwd", + "started_at_utc", + "ended_at_utc", + "elapsed_ms", + "exit_code", + "outcome", + "python_version", + "platform_system", + "determinism_declared", + "seed_declared", + "stdout_sha256", + "stderr_sha256", + "stdout_snippet", + "stderr_snippet" + ], + "properties": { + "command": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "cwd": { "type": "string" }, + "started_at_utc": { "type": "string" }, + "ended_at_utc": { "type": "string" }, + "elapsed_ms": { "type": "number", "minimum": 0 }, + "exit_code": { "type": ["integer", "null"] }, + "outcome": { + "enum": ["COMPLETED", "NONZERO_EXIT", "MISSING_INPUT", "MISSING_OUTPUT", "TIMEOUT", "EXCEPTION"] + }, + "python_version": { "type": "string" }, + "platform_system": { "type": "string" }, + "determinism_declared": { + "enum": ["DETERMINISTIC", "NONDETERMINISTIC", "UNKNOWN"] + }, + "seed_declared": { "type": ["string", "null"] }, + "stdout_sha256": { "$ref": "#/$defs/sha256" }, + "stderr_sha256": { "$ref": "#/$defs/sha256" }, + "stdout_snippet": { "type": "string" }, + "stderr_snippet": { "type": "string" } + } + }, + "claims": { + "type": "object", + "additionalProperties": false, + "required": [ + "execution_completed", + "output_digests_recorded", + "all_declared_artifacts_present", + "evaluator_claims", + "external_evaluation" + ], + "properties": { + "execution_completed": { "type": "boolean" }, + "output_digests_recorded": { "type": "boolean" }, + "all_declared_artifacts_present": { "type": ["boolean", "null"] }, + "evaluator_claims": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["evaluator_name", "metric_name", "value", "computed_by", "verified_independently"], + "properties": { + "evaluator_name": { "type": "string" }, + "metric_name": { "type": "string" }, + "value": {}, + "computed_by": { + "enum": ["bound_output_extraction", "declared_by_manifest_author"] + }, + "verified_independently": { "const": false } + } + } + }, + "external_evaluation": { + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "required": ["source", "description", "reported_value", "evidence_kind", "evidence_ref", "attested"], + "properties": { + "source": { "type": "string" }, + "description": { "type": "string" }, + "reported_value": {}, + "evidence_kind": { "type": "string" }, + "evidence_ref": { "type": ["string", "null"] }, + "attested": { "const": false } + } + } + ] + } + } + }, + "provenance_linkage": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["dataset_receipt_path", "artifact_id", "found_in_hypergraph", "ancestor_count", "ancestor_ids"], + "properties": { + "dataset_receipt_path": { "type": "string" }, + "artifact_id": { "type": "string" }, + "found_in_hypergraph": { "type": "boolean" }, + "ancestor_count": { "type": ["integer", "null"], "minimum": 0 }, + "ancestor_ids": { "type": "array", "items": { "type": "string" } } + } + } + }, + "reproducibility": { + "type": "object", + "additionalProperties": false, + "required": ["highest_demonstrated_level", "declared_ceiling", "supported_levels", "reproduction_command"], + "properties": { + "highest_demonstrated_level": { "type": ["string", "null"] }, + "declared_ceiling": { "type": "string" }, + "supported_levels": { "type": "array", "items": { "type": "string" } }, + "reproduction_command": { "type": "string" } + } + }, + "assessment_context": { + "type": "object", + "description": "Mechanism, declared resource-bound, commitment, and refutation coordinates for this VSTD-1 generic run. The container does not establish VSTD-4 conformance.", + "additionalProperties": false, + "required": ["verifier", "resource_bounds", "prior_commitment", "refutation_surface"], + "properties": { + "verifier": { + "type": "object", + "description": "Identity and implementation coordinates for the generic-run mechanism; these fields do not establish actor or implementation independence.", + "additionalProperties": false, + "required": ["specification_hash", "implementation_hash", "parser_hash", "certificate_format", "format_fragment", "dependencies", "deterministic"], + "properties": { + "specification_hash": { "$ref": "#/$defs/specificationBinding" }, + "implementation_hash": { "$ref": "#/$defs/prefixedSha256" }, + "parser_hash": { "$ref": "#/$defs/prefixedSha256" }, + "certificate_format": { "type": "string" }, + "format_fragment": { "type": "string" }, + "dependencies": { "type": "array", "items": { "type": "string" } }, + "deterministic": { "type": "boolean" } + } + }, + "resource_bounds": { + "type": "object", + "description": "Manifest-declared assessment bounds recorded by the generic writer; presence does not establish enforcement.", + "additionalProperties": false, + "required": ["verification_cost_bound", "memory_bound", "certificate_size_bound"], + "properties": { + "verification_cost_bound": { "type": "integer", "minimum": 0 }, + "memory_bound": { "type": "integer", "minimum": 0 }, + "certificate_size_bound": { "type": "integer", "minimum": 0 } + } + }, + "prior_commitment": { + "type": "string", + "description": "Recorded commitment declaration; receipt inclusion alone does not establish that it preceded execution." + }, + "refutation_surface": { + "type": "object", + "description": "Domain-refutation map whose named fields have fixed meanings; additional fields remain declarations and earn no result without an applicable mechanism.", + "required": ["admissible_refutations", "excluded_claims", "falsification_condition"], + "properties": { + "admissible_refutations": { "type": "array", "items": { "type": "string" } }, + "excluded_claims": { "type": "array", "items": { "type": "string" } }, + "falsification_condition": { "type": "string" } + } + } + } + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "prefixedSha256": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "specificationBinding": { + "oneOf": [ + { "$ref": "#/$defs/prefixedSha256" }, + { "type": "string", "pattern": "^UNAVAILABLE:.+$" } + ] + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": ["path", "role", "present", "sha256", "byte_size"], + "properties": { + "path": { "type": "string" }, + "role": { "type": "string" }, + "present": { "type": "boolean" }, + "sha256": { "oneOf": [{ "$ref": "#/$defs/sha256" }, { "type": "null" }] }, + "byte_size": { "type": ["integer", "null"], "minimum": 0 } + }, + "allOf": [ + { + "if": { "properties": { "present": { "const": true } } }, + "then": { + "properties": { + "sha256": { "$ref": "#/$defs/sha256" }, + "byte_size": { "type": "integer", "minimum": 0 } + } + } + } + ] + } + } +} diff --git a/receipts/schema/vstd1_receipt.json b/receipts/schema/vstd1_receipt.json index ac2bd90..814c39e 100644 --- a/receipts/schema/vstd1_receipt.json +++ b/receipts/schema/vstd1_receipt.json @@ -1,11 +1,13 @@ { + "$comment": "Terminology: Verifier Standard (VSTD).", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/vstd1_receipt.json", "title": "VSTD-1 Claim Mechanics Receipt", - "description": "Schema for VSTD-1 claim-mechanics receipts. The VSTD-0.1 wire identifier is frozen for compatibility.", + "description": "Schema for VSTD-1 claim-mechanics receipts.", "type": "object", "required": [ "schema_version", + "receipt_kind", "receipt_id", "canonical_digest", "claim", @@ -18,7 +20,10 @@ "properties": { "schema_version": { "type": "string", - "enum": ["VSTD-0.1"] + "enum": ["VSTD-1"] + }, + "receipt_kind": { + "const": "claim_mechanics" }, "receipt_id": { "type": "string", @@ -92,7 +97,39 @@ "grounding_result": { "type": "object" }, "structural_integrity_passed": { "type": "boolean" }, "trusted_computing_base": { "type": "object" }, - "audit_notes": { "type": "array", "items": { "type": "string" } } + "audit_notes": { "type": "array", "items": { "type": "string" } }, + "independence_basis": { + "type": "object", + "additionalProperties": false, + "required": ["independently_verified", "actor_independence", "implementation_separation", "runtime_separation", "evidence"], + "properties": { + "independently_verified": { + "type": "boolean", + "description": "A conformance claim, not a structural inference. VSTD 1.2.0's bundled runtime accepts only false because it has no actor/execution evidence-binding validator." + }, + "actor_independence": { "enum": ["EVIDENCED", "DECLARED", "NOT_DEMONSTRATED", "CONFLICTED"] }, + "implementation_separation": { "enum": ["EVIDENCED", "DECLARED", "NOT_DEMONSTRATED", "CONFLICTED"] }, + "runtime_separation": { "enum": ["EVIDENCED", "DECLARED", "NOT_DEMONSTRATED", "CONFLICTED"] }, + "evidence": { + "type": "array", + "description": "References are declarations until an implemented profile resolves and validates their execution bindings.", + "items": { "type": "string", "minLength": 1 } + } + }, + "allOf": [ + { + "if": { "properties": { "independently_verified": { "const": true } } }, + "then": { + "properties": { + "actor_independence": { "const": "EVIDENCED" }, + "implementation_separation": { "const": "EVIDENCED" }, + "runtime_separation": { "const": "EVIDENCED" }, + "evidence": { "minItems": 1 } + } + } + } + ] + } } }, "provenance": { diff --git a/receipts/schema/vstd2_receipt.json b/receipts/schema/vstd2_receipt.json index 3047bdb..6861202 100644 --- a/receipts/schema/vstd2_receipt.json +++ b/receipts/schema/vstd2_receipt.json @@ -1,8 +1,9 @@ { + "$comment": "Terminology: abstract syntax tree (AST); intermediate representation (IR); Verifier Standard (VSTD).", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/vstd2_receipt.json", "title": "VSTD-2 Verification Surface Receipt", - "description": "VSTD-2 verification-surface document. The VSTD-0.2 wire identifier is frozen; semantic closure rules are enforced by verifier.core.geometry.validate_geometry.", + "description": "VSTD-2 verification-surface document. Semantic closure rules are enforced by verifier.core.geometry.validate_geometry.", "type": "object", "additionalProperties": false, "required": [ @@ -16,7 +17,7 @@ "surface" ], "properties": { - "schema_version": {"const": "VSTD-0.2"}, + "schema_version": {"const": "VSTD-2"}, "geometry_id": {"$ref": "#/$defs/nonEmpty"}, "primary_subject_id": {"$ref": "#/$defs/nonEmpty"}, "secondary_subject_id": {"type": ["string", "null"]}, diff --git a/receipts/schema/vstd3_accelerator_profile.json b/receipts/schema/vstd3_accelerator_profile.json index 30e5031..5a96fc1 100644 --- a/receipts/schema/vstd3_accelerator_profile.json +++ b/receipts/schema/vstd3_accelerator_profile.json @@ -1,4 +1,5 @@ { + "$comment": "Terminology: artificial intelligence (AI); application-specific integrated circuit (ASIC); graphics processing unit (GPU); JavaScript Object Notation (JSON); neural processing unit (NPU); tensor processing unit (TPU); Verifier Standard (VSTD).", "$defs": { "AcceleratorProfile": { "additionalProperties": false, diff --git a/receipts/schema/vstd3_receipt.json b/receipts/schema/vstd3_receipt.json index 322b923..252b9bd 100644 --- a/receipts/schema/vstd3_receipt.json +++ b/receipts/schema/vstd3_receipt.json @@ -1,4 +1,5 @@ { + "$comment": "Terminology: artificial intelligence (AI); application-specific integrated circuit (ASIC); graphics processing unit (GPU); JavaScript Object Notation (JSON); neural processing unit (NPU); tensor processing unit (TPU); Verifier Standard (VSTD).", "$defs": { "AcceleratorDescriptor": { "additionalProperties": false, diff --git a/receipts/schema/vstd4_certificate.json b/receipts/schema/vstd4_certificate.json index c939b44..9e94237 100644 --- a/receipts/schema/vstd4_certificate.json +++ b/receipts/schema/vstd4_certificate.json @@ -1,8 +1,9 @@ { + "$comment": "Terminology: grounded decision certificate (GDC); Boolean satisfiability problem (SAT).", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/vstd4_certificate.json", "title": "VSTD4-GDC-1 Decision Certificate", - "description": "Grounded decision certificate for PASS, FAIL, or bounded UNKNOWN. Semantic and proof checks remain mandatory in the independent kernel.", + "description": "Grounded decision certificate for PASS, FAIL, or bounded UNKNOWN. Semantic and proof checks remain mandatory in the separately implemented kernel; this does not establish distinct actors.", "type": "object", "additionalProperties": false, "required": ["header", "formula", "grounding", "decision", "hints"], diff --git a/receipts/schema/vstd4_receipt.json b/receipts/schema/vstd4_receipt.json index 83ebe2e..81f4a64 100644 --- a/receipts/schema/vstd4_receipt.json +++ b/receipts/schema/vstd4_receipt.json @@ -1,7 +1,9 @@ { + "$comment": "Terminology: grounded decision certificate (GDC); Verifier Standard (VSTD).", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/vstd4_receipt.json", - "title": "VSTD-4 Refutability Receipt", + "title": "VSTD-4 Structural Candidate Receipt", + "description": "Legacy-compatible VSTD-4 shape. vstd4_depth is a candidate over caller-supplied rung references; conformance is NOT_ESTABLISHED. Historical receipts without conformance_status have the same non-upgrading interpretation.", "type": "object", "additionalProperties": false, "required": ["schema_version", "receipt_id", "claim_id", "binding", "vstd4_depth", "rung_evidence", "witness", "ceiling_refutation", "blocking_rungs", "status"], @@ -10,7 +12,8 @@ "receipt_id": {"type": "string", "pattern": "^VFY-4-[A-Za-z0-9._:-]+$"}, "claim_id": {"type": "string", "minLength": 1}, "binding": {"$ref": "#/$defs/binding"}, - "vstd4_depth": {"type": "integer", "minimum": 0, "maximum": 14}, + "vstd4_depth": {"type": "integer", "minimum": 0, "maximum": 14, "description": "Structural candidate depth, not normative VSTD-4 conformance."}, + "conformance_status": {"const": "NOT_ESTABLISHED"}, "rung_evidence": { "type": "object", "additionalProperties": false, @@ -27,7 +30,7 @@ "witness": {"anyOf": [{"$ref": "vstd4_certificate.json"}, {"type": "null"}]}, "ceiling_refutation": {"anyOf": [{"$ref": "vstd4_certificate.json"}, {"type": "null"}]}, "blocking_rungs": {"type": "array", "items": {"pattern": "^4\\.(?:[1-9]|1[0-4])$"}, "uniqueItems": true}, - "status": {"enum": ["VALID", "CHALLENGED", "REVOKED", "STALE", "UNKNOWN"]}, + "status": {"enum": ["VALID", "CHALLENGED", "REVOKED", "STALE", "UNKNOWN"], "description": "Current append-only challenge-ledger state. VALID means no admitted challenge currently disqualifies the claim; it does not establish VSTD-4 conformance."}, "refutation_surface": {"type": "object"}, "precommitment_envelope": {"type": "object"}, "availability": {"type": "array", "items": {"type": "object"}}, @@ -43,7 +46,7 @@ ], "$defs": { "digest": {"type": "string", "pattern": "^(?:sha256:)?[0-9a-f]{64}$"}, - "evidenceRef": {"type": "string", "minLength": 1}, + "evidenceRef": {"type": "string", "minLength": 1, "description": "Caller-supplied reference. Schema validity does not establish retrieval, content binding, the rung proposition, or prerequisite-profile conformance."}, "coordinate": { "type": "object", "additionalProperties": false, "required": ["subject", "predicate", "parameters"], diff --git a/receipts/schema/vstd5_receipt.json b/receipts/schema/vstd5_receipt.json index dcbbcd6..368d3e1 100644 --- a/receipts/schema/vstd5_receipt.json +++ b/receipts/schema/vstd5_receipt.json @@ -1,8 +1,9 @@ { + "$comment": "Terminology: Verifier Standard (VSTD). Draft interface only; matching this schema is not VSTD-5 readiness or proof of independent corroboration.", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/vstd5_receipt.json", "title": "VSTD-5 Witness Corroboration Receipt (DRAFT)", - "$comment": "Draft interface only. A document matching this schema is not proof of independent corroboration.", + "description": "Shape for review only. entry_vstd4_depth is a declaration of the future normative precondition; the current VSTD-4 candidate cannot satisfy it, and no reference VSTD-5 acceptance path is implemented.", "type": "object", "additionalProperties": false, "required": ["schema_version", "status", "receipt_id", "claim_id", "claim_binding", "entry_vstd4_depth", "witnesses", "corroborations", "disagreements", "computed_independence"], @@ -12,7 +13,7 @@ "receipt_id": {"type": "string", "pattern": "^VFY-5-[A-Za-z0-9._:-]+$"}, "claim_id": {"type": "string", "minLength": 1}, "claim_binding": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, - "entry_vstd4_depth": {"const": 14}, + "entry_vstd4_depth": {"const": 14, "description": "Future normative VSTD-4 conformance depth. Structural candidate depth 14 is insufficient."}, "witnesses": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/witness"}}, "corroborations": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/corroboration"}}, "disagreements": {"type": "array", "items": {"$ref": "#/$defs/disagreement"}}, diff --git a/receipts/schema/vstd_graph_receipt.json b/receipts/schema/vstd_graph_receipt.json index dad3e63..eac96fb 100644 --- a/receipts/schema/vstd_graph_receipt.json +++ b/receipts/schema/vstd_graph_receipt.json @@ -1,8 +1,9 @@ { + "$comment": "Terminology: Verifier Standard (VSTD).", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/vstd_graph_receipt.json", "title": "VSTD-Graph Provenance Hypergraph Receipt", - "description": "VSTD-Graph receipt. The VSTD-DATA-0.1 wire identifier is frozen for historical Graph-1 receipts; computed_graph_level records the independently computed 1-5 profile when present.", + "description": "VSTD-Graph receipt. The VSTD-DATA-0.1 serialized receipt identifier is frozen for historical Graph-1 receipts; computed_graph_level and its level field are compatibility names for a candidate Graph profile number over caller-supplied ratings and do not establish conformance. Historical blocks without rating_basis or conformance_status have the same unestablished interpretation.", "type": "object", "required": [ "schema_version", @@ -43,10 +44,47 @@ "type": "object", "required": ["artifacts", "transformations", "contributors", "rights"], "properties": { - "artifacts": { "type": "array" }, + "artifacts": { + "type": "array", + "items": { + "type": "object", + "required": ["artifact_id", "status"], + "properties": { + "artifact_id": { "type": "string" }, + "status": { + "enum": ["VALID", "CHALLENGED", "STALE", "SUPERSEDED", "REVOKED", "UNKNOWN"] + } + } + } + }, "transformations": { "type": "array" }, "contributors": { "type": "array" }, - "rights": { "type": "array" } + "rights": { "type": "array" }, + "conflicts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["conflict_id", "subject_id", "predicate", "competing_values", "evidence_refs"], + "properties": { + "conflict_id": { "type": "string", "minLength": 1 }, + "subject_id": { "type": "string", "minLength": 1 }, + "predicate": { "type": "string", "minLength": 1 }, + "competing_values": { + "type": "array", + "minItems": 2, + "uniqueItems": true, + "items": { "type": "string" } + }, + "evidence_refs": { + "type": "array", + "minItems": 2, + "uniqueItems": true, + "items": { "type": "string" } + } + } + } + } } }, "completeness_metrics": { @@ -75,7 +113,41 @@ }, "independent_audit": { "type": "object", - "required": ["overall_verdict", "acyclic_hypergraph", "integrity_passed", "trusted_computing_base"] + "required": ["overall_verdict", "acyclic_hypergraph", "integrity_passed", "trusted_computing_base"], + "properties": { + "independence_basis": { + "type": "object", + "additionalProperties": false, + "required": ["independently_verified", "actor_independence", "implementation_separation", "runtime_separation", "evidence"], + "properties": { + "independently_verified": { + "type": "boolean", + "description": "A conformance claim, not a structural inference. VSTD 1.2.0's bundled runtime accepts only false because it has no actor/execution evidence-binding validator." + }, + "actor_independence": { "enum": ["EVIDENCED", "DECLARED", "NOT_DEMONSTRATED", "CONFLICTED"] }, + "implementation_separation": { "enum": ["EVIDENCED", "DECLARED", "NOT_DEMONSTRATED", "CONFLICTED"] }, + "runtime_separation": { "enum": ["EVIDENCED", "DECLARED", "NOT_DEMONSTRATED", "CONFLICTED"] }, + "evidence": { + "type": "array", + "description": "References are declarations until an implemented profile resolves and validates their execution bindings.", + "items": { "type": "string", "minLength": 1 } + } + }, + "allOf": [ + { + "if": { "properties": { "independently_verified": { "const": true } } }, + "then": { + "properties": { + "actor_independence": { "const": "EVIDENCED" }, + "implementation_separation": { "const": "EVIDENCED" }, + "runtime_separation": { "const": "EVIDENCED" }, + "evidence": { "minItems": 1 } + } + } + } + ] + } + } }, "provenance": { "type": "object", @@ -93,6 +165,8 @@ "collection_id": {"type": "string", "minLength": 1}, "level": {"type": "integer", "minimum": 0, "maximum": 5}, "max_level": {"const": 5}, + "rating_basis": {"const": "CALLER_SUPPLIED"}, + "conformance_status": {"const": "NOT_ESTABLISHED"}, "blocking_obligations": {"type": "array", "items": {"type": "object"}}, "witness_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, "refutation_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, diff --git a/src/verifier/constraints/kernel.py b/src/verifier/constraints/kernel.py index 82842c6..683536c 100644 --- a/src/verifier/constraints/kernel.py +++ b/src/verifier/constraints/kernel.py @@ -1,10 +1,14 @@ -"""Small common contract around native constrained-decoding engines. +"""Terminology: intermediate representation (IR); JavaScript Object Notation (JSON); +Verifier Standard (VSTD). + +Small common contract around native constrained-decoding engines. This is intentionally not a universal grammar IR. The source constraint remains in its native language and the selected engine owns compilation. VSTD standardizes only the adjacent observable seam: source identity, compiled-object identity, tokenizer identity, per-step token masks, state transitions, and optional -independent post-validation. +separately implemented post-validation. This mechanism separation does not establish +distinct actors. """ from __future__ import annotations diff --git a/src/verifier/constraints/llguidance_backend.py b/src/verifier/constraints/llguidance_backend.py index cafe1bc..ae93a5c 100644 --- a/src/verifier/constraints/llguidance_backend.py +++ b/src/verifier/constraints/llguidance_backend.py @@ -1,4 +1,6 @@ -"""Strict llguidance backend for the VSTD logits constraint seam.""" +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Strict llguidance backend for the VSTD logits constraint seam.""" from __future__ import annotations diff --git a/src/verifier/constraints/postvalidate.py b/src/verifier/constraints/postvalidate.py index 17beef3..2d62634 100644 --- a/src/verifier/constraints/postvalidate.py +++ b/src/verifier/constraints/postvalidate.py @@ -1,4 +1,8 @@ -"""Independent whole-output checks adjacent to logits-time constraints.""" +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Whole-output checks implemented separately from logits-time constraints. + +This mechanism separation does not establish distinct actors.""" from __future__ import annotations @@ -17,7 +21,7 @@ def validate_json_schema_output(output_text: str, schema: Mapping[str, Any]) -> import jsonschema # type: ignore[import-untyped] from jsonschema import Draft202012Validator except ImportError as exc: # pragma: no cover - exercised only without test/runtime dependency - raise RuntimeError("jsonschema is required for independent JSON Schema post-validation") from exc + raise RuntimeError("jsonschema is required for separate JSON Schema post-validation") from exc output_digest = hashlib.sha256(output_text.encode("utf-8")).hexdigest() constraint_source_digest = canonical_digest(dict(schema)) diff --git a/src/verifier/core/certificate.py b/src/verifier/core/certificate.py index aadc1a2..dc189bf 100644 --- a/src/verifier/core/certificate.py +++ b/src/verifier/core/certificate.py @@ -1,4 +1,12 @@ -"""``VSTD4-GDC-1`` -- Grounded Decision Certificates for VSTD layer 4. +"""Terminology: American Standard Code for Information Interchange (ASCII); +conjunctive normal form (CNF); deletion resolution asymmetric tautology (DRAT); +Boolean satisfiability problem (SAT); flexible SAT proof format (FRAT); +grounded decision certificate (GDC); GRAT proof format (GRAT); JavaScript Object Notation (JSON); +linear resolution asymmetric tautology (LRAT); resolution asymmetric tautology (RAT); +reverse unit propagation (RUP); Unicode Transformation Format, 8-bit (UTF-8); +Verifier Standard (VSTD). + +``VSTD4-GDC-1`` -- Grounded Decision Certificates for the VSTD-4 Refutability profile. Competition proof formats (DRAT, LRAT, GRAT, FRAT) answer exactly one question: *is this large formula really unsatisfiable?* They are deliberately @@ -176,7 +184,7 @@ class VerifierDescriptor: ``format_fragment`` exists so a checker can be honest about what it does not implement. Silently mis-accepting a construct the checker does not - understand is the format-level form of semantic mismatch. + understand is the serialized-format form of semantic mismatch. """ specification_hash: str @@ -456,6 +464,8 @@ def to_dict(self) -> dict[str, Any]: @dataclass(frozen=True) class DecisionCertificate: + """Canonical grounded decision certificate (GDC) blocks for the bounded checker.""" + header: CertificateHeader formula: tuple[tuple[int, ...], ...] grounding: Grounding diff --git a/src/verifier/core/checker.py b/src/verifier/core/checker.py index ffc2c1d..7a2d4f7 100644 --- a/src/verifier/core/checker.py +++ b/src/verifier/core/checker.py @@ -1,13 +1,19 @@ -"""Independent VSTD Checker for SAT, Derivation Graphs, and Grounding. +"""Terminology: application programming interface (API); +Boolean satisfiability problem (SAT); Davis-Putnam-Logemann-Loveland (DPLL); +grounded decision certificate (GDC); Secure Hash Algorithm 256-bit (SHA-256); +trusted computing base (TCB); Verifier Standard (VSTD). + +Bundled VSTD Checker for SAT, Derivation Graphs, and Grounding. This module provides a minimal, self-contained verification engine with zero dependencies on external solver libraries or the target repository under test. -It serves as an independent auditor in the Trusted Computing Base (TCB). +It is a separate checker implementation in the trusted computing base (TCB), but +calling it does not itself establish actor, implementation, or runtime independence. """ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum import hashlib from pathlib import Path @@ -26,9 +32,10 @@ def _source_digest(*candidates: Any) -> str: """SHA-256 of the first candidate available in source or an installed wheel. - Several candidates are accepted because specification filenames move as the - ladder is renumbered, and a descriptor that breaks on a rename would push - implementers back toward hardcoding. + Each named candidate is tried as an absolute path, a repository-relative path, + and a current-working-directory-relative path. ``standard/`` coordinates also + resolve to the byte-identical installed specification copy. This is location + resolution, not semantic substitution; callers must name only equivalent sources. """ for candidate in candidates: path = Path(candidate) @@ -78,12 +85,67 @@ def unit_propagation_conflict( class VerificationVerdict(str, Enum): + """Outcome vocabulary returned by the VSTD-1 claim-mechanics checker.""" + VERIFIED = "VERIFIED" FALSIFIED = "FALSIFIED" INDETERMINATE = "INDETERMINATE" UNSUPPORTED = "UNSUPPORTED" +class IndependenceStatus(str, Enum): + """Evidence state for separation between producer and checker.""" + + EVIDENCED = "EVIDENCED" + DECLARED = "DECLARED" + NOT_DEMONSTRATED = "NOT_DEMONSTRATED" + CONFLICTED = "CONFLICTED" + + +def independence_is_evidenced(basis: Mapping[str, Any]) -> bool: + """Apply the bundled runtime's current independence capability ceiling. + + Serialized status words and evidence references are declarations. VSTD 1.2.0 + ships no validator that resolves and binds them to distinct actors and execution + seams, so no supplied mapping can establish evidenced independence. + """ + + del basis + return False + + +@dataclass(frozen=True) +class IndependenceBasis: + """Actor and execution separation; artifact agreement proves neither.""" + + actor_independence: IndependenceStatus = IndependenceStatus.NOT_DEMONSTRATED + implementation_separation: IndependenceStatus = IndependenceStatus.NOT_DEMONSTRATED + runtime_separation: IndependenceStatus = IndependenceStatus.NOT_DEMONSTRATED + evidence: tuple[str, ...] = () + + @property + def independently_verified(self) -> bool: + """False until an implemented adapter validates the recorded bindings.""" + + return independence_is_evidenced( + { + "actor_independence": self.actor_independence.value, + "implementation_separation": self.implementation_separation.value, + "runtime_separation": self.runtime_separation.value, + "evidence": self.evidence, + } + ) + + def to_dict(self) -> dict[str, Any]: + return { + "independently_verified": self.independently_verified, + "actor_independence": self.actor_independence.value, + "implementation_separation": self.implementation_separation.value, + "runtime_separation": self.runtime_separation.value, + "evidence": list(self.evidence), + } + + class GroundingVerdict(str, Enum): GROUNDED = "GROUNDED" ASSUMED = "ASSUMED" @@ -118,6 +180,8 @@ class IndependentGroundingResult: @dataclass(frozen=True) class IndependentAuditReport: + """Historical API name for a checker report with explicit separation evidence.""" + claim_id: str sat_result: IndependentSatResult grounding_result: IndependentGroundingResult @@ -125,11 +189,13 @@ class IndependentAuditReport: overall_verdict: VerificationVerdict trusted_computing_base: dict[str, str] audit_notes: list[str] + independence_basis: IndependenceBasis = field(default_factory=IndependenceBasis) def to_dict(self) -> dict[str, Any]: return { "claim_id": self.claim_id, "overall_verdict": self.overall_verdict.value, + "independence_basis": self.independence_basis.to_dict(), "structural_integrity_passed": self.structural_integrity_passed, "sat_result": { "satisfiable": self.sat_result.satisfiable, @@ -160,7 +226,8 @@ def to_dict(self) -> dict[str, Any]: class MinimalIndependentDPLL: """A self-contained DPLL SAT solver in pure standard-library Python. - Independent of target solvers, third-party SAT packages, or external binaries. + It shares no target-solver, third-party SAT package, or external-binary logic. + That implementation separation does not establish actor independence. """ def __init__(self, n_vars: int, clauses: Sequence[Sequence[int]]): @@ -272,7 +339,7 @@ def _dpll( class IndependentGroundingChecker: - """Checks grounding, acyclicity, and derivation validity independently.""" + """Separately implemented grounding, acyclicity, and derivation checks.""" @staticmethod def audit_derivation( @@ -382,7 +449,13 @@ def dfs(node: str) -> bool: class IndependentAuditor: - """Top-level independent auditor that evaluates claims and derivation artifacts.""" + """Historical API name for the bundled SAT and grounding checker. + + Calling this class does not establish that separate actors performed the + producer and checker runs. Matching results cannot establish that fact. The + returned report records actor, implementation, and runtime separation as + ``NOT_DEMONSTRATED`` unless a separate integration supplies bound evidence. + """ @classmethod def verifier_descriptor(cls) -> VerifierDescriptor: @@ -398,13 +471,13 @@ def verifier_descriptor(cls) -> VerifierDescriptor: ``certificate_format`` deliberately does **not** say ``VSTD4-GDC-1``. This auditor emits an ``IndependentAuditReport``, which is a different artifact, and claiming a format one does not implement is the - format-level form of the semantic mismatch rung 4.2 prohibits. + serialized-format form of the semantic mismatch rung 4.2 prohibits. """ return VerifierDescriptor( - specification_hash=_source_digest("standard/VSTD-3.md"), + specification_hash=_source_digest("standard/VSTD-1.md"), implementation_hash=_source_digest(_MODULE_PATH), parser_hash=_source_digest(_MODULE_PATH.with_name("receipt.py")), - certificate_format="VSTD3-INDEPENDENT-AUDIT", + certificate_format="VSTD1-CHECKER-REPORT", format_fragment="SAT,GROUNDING,ACYCLICITY", dependencies=("python-stdlib",), deterministic=True, @@ -476,9 +549,10 @@ def audit_claim_derivation( overall = VerificationVerdict.INDETERMINATE notes = [ - f"SAT formula solved independently: satisfiable={is_sat} (decisions={solver.decisions}, propagations={solver.propagations}).", - f"Grounding audit status: {grounding_result.grounding_status.value} ({grounding_result.details}).", + f"SAT formula solved by the bundled separate implementation: satisfiable={is_sat} (decisions={solver.decisions}, propagations={solver.propagations}).", + f"Grounding checker status: {grounding_result.grounding_status.value} ({grounding_result.details}).", f"Acyclicity verified: cycle_detected={grounding_result.cycle_detected}.", + "This same-process call did not demonstrate separate actors, implementation separation, or runtime separation; matching results cannot establish actor independence.", ] if not is_sat: notes.append( diff --git a/src/verifier/core/depth.py b/src/verifier/core/depth.py index 8de5a10..d5e8d91 100644 --- a/src/verifier/core/depth.py +++ b/src/verifier/core/depth.py @@ -1,27 +1,33 @@ -"""``vstd4_depth`` -- how far up the layer-4 ladder a claim actually got. +"""Terminology: conjunctive normal form (CNF); identifier (ID); unsatisfiable (UNSAT); +Verifier Standard (VSTD). + +``vstd4_depth`` -- candidate depth over caller-supplied rung references. VSTD-4 is fourteen rungs, ordered so that each is unstatable without the one -below it. That ordering is not editorial tidiness. Standing up a genuinely -external verification node -- VSTD-5 -- must be *computationally costly*, -because verification is the new scaling, and layer 4 is where the cost is paid. -The ladder makes the cost curve explicit instead of letting an implementer -declare the top rung and skip the climb. +below it. That ordering is not editorial tidiness. Entry to VSTD-5 requires +separately checkable VSTD-4 obligations rather than a declared top-rung reference. +The rung sequence makes those dependencies explicit. -So the depth is **computed, never declared**:: +The structural candidate is **computed, never copied from a declared depth**:: vstd4_depth(claim) = max { k : CNF_4k(claim) is satisfiable } and the UNSAT certificate at ``k+1`` **is** the explanation of why the claim -cannot climb higher. The layer certifies its own ceiling using its own -mechanism, and the conflict clause of that certificate names the missing rung -outright. +cannot climb higher. The candidate-depth calculation certifies its own ceiling +using its own mechanism, and the conflict clause names the missing rung outright. The encoding is Horn -- assertions ``[j]``, dependencies ``[-k, d]``, absences ``[-j]`` -- so every certificate this module produces is tier ``UP`` and checks in linear time. The dependency clauses look inert while the numbering is a valid -topological order, and that is exactly the point: reorder the ladder so a rung +topological order, and that is exactly the point: reorder the sequence so a rung depends on one above it and the formula goes unsatisfiable at a low depth, -loudly, instead of quietly certifying a ladder that is no longer a ladder. +loudly, instead of quietly certifying a sequence that no longer preserves its +dependencies. + +The current producer checks reference presence and rung dependencies. It does not +resolve those references, validate the propositions they allegedly establish, or +check VSTD-1/2/3 preconditions. Its result is therefore a candidate with +``conformance_status = NOT_ESTABLISHED`` and cannot admit VSTD-5. This module *produces* certificates. It is not part of the trusted computing base; :mod:`verifier.core.kernel` checks what it emits, and the propagation @@ -50,7 +56,10 @@ ) MAX_DEPTH = 14 -"""Entry condition for VSTD-5: ``vstd4_depth(claim) == 14``.""" +"""Highest structural candidate depth; not sufficient for VSTD-5 entry.""" + +DEPTH_KIND = "CANDIDATE" +CONFORMANCE_STATUS = "NOT_ESTABLISHED" class VSTD5EntryError(RuntimeError): @@ -111,27 +120,29 @@ class Rung: RULES = (RULE_ASSERTED, RULE_ABSENT, RULE_REQUIRES) -def _validate_ladder() -> None: +def _validate_rung_sequence() -> None: for rung in RUNGS: for dependency in rung.depends_on: if dependency >= rung.index: raise ValueError( f"rung {rung.id} depends on rung index {dependency}, which is not " - "below it; the ladder numbering is no longer a topological order" + "below it; the rung numbering is no longer a topological order" ) -_validate_ladder() +_validate_rung_sequence() @dataclass(frozen=True) class DepthResult: - """A computed depth, with the evidence for both halves of the answer. + """A computed candidate depth, with certificates for the structural answer. - ``witness`` certifies the rungs that were climbed. ``refutation`` certifies - why the next one was not, and its ``blocking_rungs`` name the reason. A + ``witness`` certifies consistency of the caller-supplied rung references. + ``refutation`` certifies why the next structural rung was not reached, and + its ``blocking_rungs`` name the reason. A depth reported without ``refutation`` at anything below :data:`MAX_DEPTH` - would be a declaration, which is the thing this module exists to avoid. + would be a declaration, which is the thing this module exists to avoid. The + references themselves and prerequisite-profile coordinates are not validated here. """ depth: int @@ -141,11 +152,17 @@ class DepthResult: @property def admits_vstd5(self) -> bool: - return self.depth >= MAX_DEPTH + return False + + @property + def conformance_status(self) -> str: + return CONFORMANCE_STATUS def to_dict(self) -> dict[str, object]: return { "depth": self.depth, + "depth_kind": DEPTH_KIND, + "conformance_status": self.conformance_status, "max_depth": MAX_DEPTH, "admits_vstd5": self.admits_vstd5, "blocking_rungs": list(self.blocking_rungs), @@ -155,12 +172,12 @@ def to_dict(self) -> dict[str, object]: def require_vstd5_entry(result: DepthResult) -> DepthResult: - """Fail closed unless ``result`` carries the complete layer-4 witness. + """Reject the current unbound candidate result at the VSTD-5 boundary. - VSTD-5 is draft, but its entry boundary is not: no future witness transport - may admit a partial layer-4 claim. Returning the checked result makes this - function usable as the first line of any later VSTD-5 procedure without - turning the gate into a second, declarative depth field. + VSTD-5 is draft, but its entry boundary is not: a structural candidate over + caller-supplied references is not normative VSTD-4 conformance. A future + evidence-binding implementation needs a distinct result type and gate; it + must not make this candidate stronger by setting another declaration field. """ if result.depth != MAX_DEPTH or result.witness is None: raise VSTD5EntryError( @@ -173,7 +190,10 @@ def require_vstd5_entry(result: DepthResult) -> DepthResult: raise VSTD5EntryError( "VSTD-5 entry result carries a ceiling refutation or blocking rung" ) - return result + raise VSTD5EntryError( + "VSTD-5 requires established VSTD-4 conformance; this structural " + f"candidate has conformance_status {result.conformance_status}" + ) # -------------------------------------------------------------------------- @@ -182,7 +202,7 @@ def require_vstd5_entry(result: DepthResult) -> DepthResult: def _encode( - level: int, evidence: Mapping[str, str], claim_id: str + candidate_depth: int, evidence: Mapping[str, str], claim_id: str ) -> tuple[tuple[tuple[int, ...], ...], Grounding]: """CNF_4k, together with the grounding that says what its variables mean.""" formula: list[tuple[int, ...]] = [] @@ -192,7 +212,7 @@ def emit(literals: Sequence[int], rule: EncodingRule, bindings, subjects) -> Non clauses.append(ClauseGrounding(len(formula), rule.rule_id, dict(bindings), dict(subjects))) formula.append(tuple(literals)) - for rung in RUNGS[:level]: + for rung in RUNGS[:candidate_depth]: emit([rung.index], RULE_ASSERTED, {"rung": rung.index}, {"rung": claim_id}) for rung in RUNGS: @@ -254,9 +274,9 @@ def _propagate( def _certify( - level: int, evidence: Mapping[str, str], claim_id: str, binding: ClaimBinding + candidate_depth: int, evidence: Mapping[str, str], claim_id: str, binding: ClaimBinding ) -> tuple[DecisionCertificate, tuple[str, ...]]: - formula, grounding = _encode(level, evidence, claim_id) + formula, grounding = _encode(candidate_depth, evidence, claim_id) steps, conflict, assignment = _propagate(formula) literals = sum(len(clause) for clause in formula) @@ -294,29 +314,32 @@ def vstd4_depth( claim_id: str, binding: ClaimBinding, ) -> DepthResult: - """Compute how far up the layer-4 ladder ``evidence`` carries a claim. + """Compute a structural candidate depth from caller-supplied references. ``evidence`` maps a rung id (``"4.1"`` .. ``"4.14"``) to the content address - of the artifact establishing it. An absent or empty entry means the rung is - not established, and the resulting UNSAT certificate at the next level names - it. + claimed for the artifact establishing it. This function checks only whether + each value is nonempty; it does not retrieve the artifact or validate the + rung proposition. An absent or empty entry blocks the candidate, and the + resulting UNSAT certificate at the next candidate depth names it. - Descends from :data:`MAX_DEPTH`, so the first satisfiable level found is the - depth -- the ladder is monotone by construction, but searching downward + Descends from :data:`MAX_DEPTH`, so the first satisfiable candidate depth found is the + depth -- the rung sequence is monotone by construction, but searching downward means a fully-conformant claim costs one solve rather than fourteen. """ unknown = set(evidence) - set(BY_ID) if unknown: raise ValueError(f"evidence names rungs that do not exist: {sorted(unknown)}") - for level in range(MAX_DEPTH, 0, -1): - certificate, blocking = _certify(level, evidence, claim_id, binding) + for candidate_depth in range(MAX_DEPTH, 0, -1): + certificate, blocking = _certify(candidate_depth, evidence, claim_id, binding) if certificate.header.verdict is Verdict.PASS: refutation: Optional[DecisionCertificate] = None blocked: tuple[str, ...] = () - if level < MAX_DEPTH: - refutation, blocked = _certify(level + 1, evidence, claim_id, binding) - return DepthResult(level, certificate, refutation, blocked) + if candidate_depth < MAX_DEPTH: + refutation, blocked = _certify( + candidate_depth + 1, evidence, claim_id, binding + ) + return DepthResult(candidate_depth, certificate, refutation, blocked) refutation, blocked = _certify(1, evidence, claim_id, binding) return DepthResult(0, None, refutation, blocked) diff --git a/src/verifier/core/geometry.py b/src/verifier/core/geometry.py index eddaf85..79b8807 100644 --- a/src/verifier/core/geometry.py +++ b/src/verifier/core/geometry.py @@ -1,6 +1,7 @@ -"""Typed verification geometry for the additive VSTD-0.2 vertical slice. +"""Terminology: abstract syntax tree (AST); intermediate representation (IR); +Verifier Standard (VSTD). -This module does not alter VSTD-0.1 or VSTD-DATA-0.1 receipts. It supplies a +Typed verification geometry for the VSTD-2 vertical slice. This module supplies a small common representation for describing *where* verification attaches, *in what respect*, and why apparent closure must sometimes be refused. @@ -18,7 +19,7 @@ from typing import Any, Iterable, Optional -GEOMETRY_SCHEMA_VERSION = "VSTD-0.2" +GEOMETRY_SCHEMA_VERSION = "VSTD-2" class LocusKind(str, Enum): @@ -259,7 +260,11 @@ class ReconstructionAttempt: @dataclass(frozen=True) class VerificationLayer: - """One bounded order of verification under the adjacent-layer invariant.""" + """One bounded adjacent verification order. + + The class and its ``*_layer*`` fields retain their published compatibility + names. They represent VSTD-2 meta-verification orders, not numbered VSTD profiles. + """ layer_id: str order: int @@ -568,35 +573,54 @@ def validate(self) -> list[str]: f"reconstruction {reconstruction.reconstruction_id!r} references unknown residual {residual_id!r}" ) - layers_by_id = {layer.layer_id: layer for layer in self.verification_layers} - orders = sorted(layer.order for layer in self.verification_layers) + orders_by_id = { + verification_order.layer_id: verification_order + for verification_order in self.verification_layers + } + orders = sorted( + verification_order.order + for verification_order in self.verification_layers + ) if orders and orders != list(range(orders[-1] + 1)): - errors.append("verification layer orders must be contiguous and start at 0") - for layer in self.verification_layers: - if layer.order < 0: - errors.append(f"verification layer {layer.layer_id!r} has negative order") - if layer.subject_id not in subject_ids: - errors.append(f"verification layer {layer.layer_id!r} has unknown subject") - if layer.order == 0 and layer.verifies_layer_id is not None: - errors.append("verification layer order 0 cannot verify another layer") - if layer.order > 0: - target = layers_by_id.get(layer.verifies_layer_id or "") + errors.append("verification orders must be contiguous and start at 0") + for verification_order in self.verification_layers: + if verification_order.order < 0: + errors.append( + f"verification order {verification_order.layer_id!r} has negative order" + ) + if verification_order.subject_id not in subject_ids: + errors.append( + f"verification order {verification_order.layer_id!r} has unknown subject" + ) + if verification_order.order == 0 and verification_order.verifies_layer_id is not None: + errors.append("verification order 0 cannot verify another order") + if verification_order.order > 0: + target = orders_by_id.get(verification_order.verifies_layer_id or "") if target is None: errors.append( - f"verification layer {layer.layer_id!r} does not identify a previous layer" + f"verification order {verification_order.layer_id!r} does not identify a previous order" ) - elif target.order != layer.order - 1: + elif target.order != verification_order.order - 1: errors.append( - f"verification layer {layer.layer_id!r} violates the adjacent-layer invariant" + f"verification order {verification_order.layer_id!r} violates the order-adjacency invariant" ) - for coordinate_id in layer.coordinate_ids: + for coordinate_id in verification_order.coordinate_ids: if coordinate_id not in coordinate_ids: - errors.append(f"verification layer {layer.layer_id!r} has unknown coordinate") - for mechanism_id in layer.mechanism_ids: + errors.append( + f"verification order {verification_order.layer_id!r} has unknown coordinate" + ) + for mechanism_id in verification_order.mechanism_ids: if mechanism_id not in mechanism_ids: - errors.append(f"verification layer {layer.layer_id!r} has unknown mechanism") - if layer.horizon_id and layer.horizon_id not in horizon_ids: - errors.append(f"verification layer {layer.layer_id!r} has unknown horizon") + errors.append( + f"verification order {verification_order.layer_id!r} has unknown mechanism" + ) + if ( + verification_order.horizon_id + and verification_order.horizon_id not in horizon_ids + ): + errors.append( + f"verification order {verification_order.layer_id!r} has unknown horizon" + ) for novelty in self.novelties: if novelty.residual_id not in residual_ids: @@ -611,7 +635,7 @@ def assess_closure(self) -> ClosureAssessment: coordinates must pass, and every material residual must be resolved or honestly terminated at a horizon. Self-closure is stronger and refuses all unresolved horizons, open valence, unverified mechanisms, and - unaccounted verification layers. + unaccounted verification orders. """ ordinary: list[str] = list(self.validate()) @@ -659,29 +683,32 @@ def assess_closure(self) -> ClosureAssessment: self_blockers.append( f"mechanism {mechanism.mechanism_id!r} is not post-verified" ) - for layer in self.verification_layers: - if layer.horizon_id: + for verification_order in self.verification_layers: + if verification_order.horizon_id: self_blockers.append( - f"verification layer {layer.layer_id!r} terminates at a horizon" + f"verification order {verification_order.layer_id!r} terminates at a horizon" ) if self.secondary_subject_id is None: self_blockers.append("self-closure requires a secondary verification subject") if not self.meta_focus_coordinate_ids: self_blockers.append("self-closure requires an explicit meta-focus") - layer_orders = {layer.order for layer in self.verification_layers} - if not {0, 1}.issubset(layer_orders): - self_blockers.append("self-closure requires adjacent V0 and V1 verification layers") - for layer in self.verification_layers: - for coordinate_id in layer.coordinate_ids: + verification_orders = { + verification_order.order + for verification_order in self.verification_layers + } + if not {0, 1}.issubset(verification_orders): + self_blockers.append("self-closure requires adjacent V0 and V1 verification orders") + for verification_order in self.verification_layers: + for coordinate_id in verification_order.coordinate_ids: judgment = judgments.get(coordinate_id) if judgment is None or judgment.status is not CoordinateStatus.VERIFIED: self_blockers.append( - f"verification layer {layer.layer_id!r} coordinate {coordinate_id!r} is not VERIFIED" + f"verification order {verification_order.layer_id!r} coordinate {coordinate_id!r} is not VERIFIED" ) - if layer.order > 0 and not layer.evidence_ids: + if verification_order.order > 0 and not verification_order.evidence_ids: self_blockers.append( - f"higher verification layer {layer.layer_id!r} has no sufficiency evidence" + f"higher verification order {verification_order.layer_id!r} has no sufficiency evidence" ) return ClosureAssessment( diff --git a/src/verifier/core/grounding.py b/src/verifier/core/grounding.py index 357b7ee..46d4a74 100644 --- a/src/verifier/core/grounding.py +++ b/src/verifier/core/grounding.py @@ -1,4 +1,7 @@ -"""Grounding validation for ``VSTD4-GDC-1`` -- rung 4.2, semantic binding. +"""Terminology: grounded decision certificate (GDC); Boolean satisfiability problem (SAT); +Verifier Standard (VSTD). + +Grounding validation for ``VSTD4-GDC-1`` -- rung 4.2, semantic binding. A resolution proof establishes a fact about a *formula*. A VSTD claim is about the *world*. The gap between them is an encoding, and an encoding is exactly diff --git a/src/verifier/core/kernel.py b/src/verifier/core/kernel.py index f970d5a..3b9f616 100644 --- a/src/verifier/core/kernel.py +++ b/src/verifier/core/kernel.py @@ -1,4 +1,7 @@ -"""The refutability kernel -- the whole trusted computing base of VSTD layer 4. +"""Terminology: conjunctive normal form (CNF); grounded decision certificate (GDC); +Boolean satisfiability problem (SAT); Verifier Standard (VSTD). + +The refutability kernel -- the whole trusted computing base of VSTD-4 Refutability. Rung 4.7 is a claim about code, not a slogan: a certificate checker must be radically simpler than the system that produced the claim, and must share no @@ -145,7 +148,7 @@ def _tier_admissible( # -------------------------------------------------------------------------- -# Propagation -- independently re-implemented; see module docstring +# Propagation -- separately reimplemented from the producer path; see module docstring # -------------------------------------------------------------------------- @@ -337,7 +340,7 @@ def _decision_shape(certificate: DecisionCertificate) -> Optional[str]: if not actual and expected == "transcript": return ( "UNKNOWN carries no indeterminacy transcript; a refusal without evidence " - "is not a layer-4 verdict" + "is not a VSTD-4 verdict" ) if ( certificate.header.verdict is Verdict.FAIL @@ -440,7 +443,7 @@ def check( # Rung 4.7 honesty: a tier this kernel does not implement is UNKNOWN, not # FAIL. Silently mis-accepting an unimplemented construct would be exactly - # the semantic mismatch layer 4 prohibits. + # the semantic mismatch VSTD-4 Refutability prohibits. if header.tier is CostTier.SAT_PRESERVING: return _refuse( IndeterminacyReason.VERIFIER_UNAVAILABLE, @@ -583,7 +586,7 @@ def check( if decision.transcript is None: return _reject( "UNKNOWN carries no indeterminacy transcript; a refusal without evidence " - "is not a layer-4 verdict", + "is not a VSTD-4 verdict", literals=literals, hints=hints_present, ) diff --git a/src/verifier/core/provenance.py b/src/verifier/core/provenance.py index c19f001..2d8b66d 100644 --- a/src/verifier/core/provenance.py +++ b/src/verifier/core/provenance.py @@ -1,4 +1,6 @@ -"""Dynamic provenance capture and environment discovery for VSTD.""" +"""Terminology: Verifier Standard (VSTD). + +Dynamic provenance capture and environment discovery for VSTD.""" from __future__ import annotations diff --git a/src/verifier/core/receipt.py b/src/verifier/core/receipt.py index 0347fad..48bab10 100644 --- a/src/verifier/core/receipt.py +++ b/src/verifier/core/receipt.py @@ -1,4 +1,10 @@ -"""Canonical receipt model, canonicalization algorithm, and digest verification for VSTD-0.1.""" +"""Terminology: command-line interface (CLI); identifier (ID); JavaScript Object Notation (JSON); +Boolean satisfiability problem (SAT); Secure Hash Algorithm 256-bit (SHA-256); +trusted computing base (TCB); Unicode Transformation Format, 8-bit (UTF-8); +Verifier Standard (VSTD). + +Canonical receipt model, canonicalization algorithm, and digest verification for +VSTD-1 claim-mechanics receipts.""" from __future__ import annotations @@ -12,6 +18,10 @@ from .provenance import ProvenanceRecord +CLAIM_SCHEMA_VERSION = "VSTD-1" +CLAIM_RECEIPT_KIND = "claim_mechanics" + + def canonical_json_dumps(payload: Any) -> str: """Deterministic JSON serialization. @@ -96,7 +106,10 @@ def to_dict(self) -> dict[str, Any]: @dataclass class VstdReceipt: + """Mutable in-memory model of a canonically digested VSTD-1 claim receipt.""" + schema_version: str + receipt_kind: str receipt_id: str claim: ClaimSpec evidence: EvidencePayload @@ -107,10 +120,17 @@ class VstdReceipt: canonical_digest: str = "" execution_metadata: Optional[ExecutionMetadata] = None + def __post_init__(self) -> None: + if self.schema_version != CLAIM_SCHEMA_VERSION: + raise ValueError(f"schema_version must be {CLAIM_SCHEMA_VERSION}") + if self.receipt_kind != CLAIM_RECEIPT_KIND: + raise ValueError(f"receipt_kind must be {CLAIM_RECEIPT_KIND}") + def get_stable_payload(self) -> dict[str, Any]: """Extract only deterministic, location-independent fields for canonical hashing.""" return { "schema_version": self.schema_version, + "receipt_kind": self.receipt_kind, "receipt_id": self.receipt_id, "claim": self.claim.to_dict(), "evidence": self.evidence.to_dict(), @@ -144,6 +164,7 @@ def to_dict(self) -> dict[str, Any]: self.compute_and_set_digest() return { "schema_version": self.schema_version, + "receipt_kind": self.receipt_kind, "receipt_id": self.receipt_id, "canonical_digest": self.canonical_digest, "claim": self.claim.to_dict(), @@ -172,6 +193,7 @@ def save_to_directory(self, out_dir: Path) -> Path: "receipt_id": self.receipt_id, "canonical_digest": self.canonical_digest, "schema_version": self.schema_version, + "receipt_kind": self.receipt_kind, "files": { "receipt.json": hashlib.sha256(receipt_path.read_bytes()).hexdigest(), "claim.json": hashlib.sha256(claim_path.read_bytes()).hexdigest(), @@ -201,17 +223,21 @@ def save_to_directory(self, out_dir: Path) -> Path: def generate_receipt_markdown_report(receipt: VstdReceipt) -> str: - """Generate human-readable audit report for the receipt.""" + """Generate a human-readable checker report for the receipt.""" audit = receipt.independent_audit prov = receipt.provenance claim = receipt.claim + independence = audit.independence_basis + return f"""# VSTD Receipt Report — {receipt.receipt_id} > **Canonical Digest:** `{receipt.canonical_digest}` > **Schema Version:** `{receipt.schema_version}` +> **Receipt Kind:** `{receipt.receipt_kind}` > **Verification Status:** `{claim.status}` -> **Independent Audit Verdict:** `{audit.overall_verdict.value}` +> **Checker Verdict:** `{audit.overall_verdict.value}` +> **Independent Verification:** `{'EVIDENCED' if independence.independently_verified else 'NOT_DEMONSTRATED'}` --- @@ -228,9 +254,12 @@ def generate_receipt_markdown_report(receipt: VstdReceipt) -> str: --- -## 2. Independent Audit (VSTD Independent Checker) +## 2. Bundled Checker Result -The verification was evaluated by an independent checker with zero shared solver code. +The bundled checker used its recorded implementation and trusted computing base. Running +it twice, or obtaining matching results, does not establish that separate independent +actors performed the runs. Actor, implementation, and runtime separation require their +own bound evidence. - **SAT Status:** `{'Satisfiable' if audit.sat_result.satisfiable else 'Unsatisfiable'}` (decisions={audit.sat_result.decisions_count}, propagations={audit.sat_result.propagations_count}) - **Grounding Status:** `{audit.grounding_result.grounding_status.value}` @@ -246,6 +275,11 @@ def generate_receipt_markdown_report(receipt: VstdReceipt) -> str: {chr(10).join(f"{k}: {v}" for k, v in audit.trusted_computing_base.items())} ``` +### Independence Basis +```yaml +{chr(10).join(f"{k}: {v}" for k, v in independence.to_dict().items())} +``` + --- ## 3. Provenance & Execution Environment @@ -265,7 +299,7 @@ def generate_receipt_markdown_report(receipt: VstdReceipt) -> str: ## 4. Reproducibility Instructions -To reproduce this receipt independently using the VSTD CLI: +To reproduce the stored checks using the VSTD CLI: ```bash vstd reproduce receipts/{receipt.receipt_id} @@ -275,5 +309,5 @@ def generate_receipt_markdown_report(receipt: VstdReceipt) -> str: --- -*Generated by VSTD Runtime v0.1.0* +*Generated by the VSTD-1 claim-mechanics reference runtime.* """ diff --git a/src/verifier/core/refutation.py b/src/verifier/core/refutation.py index d26532b..c59e6e5 100644 --- a/src/verifier/core/refutation.py +++ b/src/verifier/core/refutation.py @@ -1,6 +1,11 @@ -"""Refutation certificates for VSTD layer 4 (refutability). +"""Terminology: conjunctive normal form (CNF); Davis-Putnam-Logemann-Loveland (DPLL); +deletion resolution asymmetric tautology (DRAT); grounded decision certificate (GDC); +nondeterministic polynomial time (NP); reverse unit propagation (RUP); +Boolean satisfiability problem (SAT); unsatisfiable (UNSAT); Verifier Standard (VSTD). -Layer 4 requires that every verdict -- pass **and** fail -- carry an artifact an +Refutation certificates for VSTD-4 Refutability. + +The Refutability coordinate requires that every verdict -- pass **and** fail -- carry an artifact an independent party can check without the declarant's cooperation. A satisfiable result already carries such an artifact: the model. Anyone can @@ -85,7 +90,7 @@ def _is_tautology(clause: Sequence[int]) -> bool: @dataclass(frozen=True) class RefutationCertificate: - """A clausal refutation proof, independently checkable without re-solving.""" + """A clausal refutation proof that a consumer can check without re-solving.""" proof: list[list[int]] n_vars: int diff --git a/src/verifier/core/reproducibility.py b/src/verifier/core/reproducibility.py index ee193d9..06d6d06 100644 --- a/src/verifier/core/reproducibility.py +++ b/src/verifier/core/reproducibility.py @@ -1,4 +1,6 @@ -"""Reproducibility taxonomy and verification comparison levels. +"""Terminology: Boolean satisfiability problem (SAT); Verifier Standard (VSTD). + +Reproduction-fidelity taxonomy and verification comparison states. Defines the formal gradient of reproducibility for computational and formal claims. """ @@ -9,7 +11,7 @@ class ReproducibilityLevel(str, Enum): - """Monotone levels of reproduction fidelity.""" + """Monotone reproduction-fidelity states; class name retained for compatibility.""" BITWISE_IDENTICAL = "BITWISE_IDENTICAL" """Exact byte-for-byte identity of all generated artifacts, receipts, and hashes.""" @@ -23,11 +25,14 @@ class ReproducibilityLevel(str, Enum): truth values and proof certificates, though internal trace order or solver step counts may differ.""" RESULT_EQUIVALENT = "RESULT_EQUIVALENT" - """High-level verification verdict (VERIFIED/FALSIFIED) and primary output metrics agree within + """Summary verification verdict (VERIFIED/FALSIFIED) and primary output metrics agree within declared error tolerance, but internal intermediate proof structures may differ.""" SEMANTIC_REPRODUCTION = "SEMANTIC_REPRODUCTION" - """The underlying formal proposition is sustained under an independent translation or alternate solver.""" + """The proposition is sustained under a separately implemented translation or solver. + + This state does not establish distinct actors. + """ def compare_reproduction_level( @@ -39,8 +44,13 @@ def compare_reproduction_level( reproduced_evidence_hash: str | None = None, original_raw_bytes: bytes | None = None, reproduced_raw_bytes: bytes | None = None, -) -> ReproducibilityLevel: - """Classify the observed reproduction fidelity between two verification runs.""" +) -> ReproducibilityLevel | None: + """Return the strongest reproduction state earned by the supplied comparison evidence. + + ``None`` means that these inputs do not establish a taxonomy state. A + matching verdict without matching primary metrics cannot establish result + equivalence, and a verdict mismatch cannot establish semantic reproduction. + """ if original_raw_bytes is not None and reproduced_raw_bytes is not None: if original_raw_bytes == reproduced_raw_bytes: return ReproducibilityLevel.BITWISE_IDENTICAL @@ -55,7 +65,4 @@ def compare_reproduction_level( ): return ReproducibilityLevel.EVIDENCE_EQUIVALENT - if original_verdict == reproduced_verdict: - return ReproducibilityLevel.RESULT_EQUIVALENT - - return ReproducibilityLevel.SEMANTIC_REPRODUCTION + return None diff --git a/src/verifier/core/translation.py b/src/verifier/core/translation.py index df30967..9e8ff63 100644 --- a/src/verifier/core/translation.py +++ b/src/verifier/core/translation.py @@ -1,4 +1,7 @@ -"""Translation-boundary assurance: the missing dimension between "a formal +"""Terminology: finite-state machine (FSM); JavaScript Object Notation (JSON); +Boolean satisfiability problem (SAT); Verifier Standard (VSTD). + +Translation-boundary assurance: the missing dimension between "a formal system said yes" and "the formal system was fed an honest encoding of the real thing." @@ -182,8 +185,8 @@ def from_dict(cls, d: dict[str, Any]) -> "TranslationRecord": return rec def canonical_digest(self) -> str: - """Reuses the same canonicalization/digest machinery as VSTD-0.1 - receipts (``verifier.core.receipt.compute_canonical_digest``) + """Reuse the VSTD-1 receipt canonicalization and digest machinery + (``verifier.core.receipt.compute_canonical_digest``) rather than inventing a second canonical-JSON scheme.""" return compute_canonical_digest(self.to_dict()) @@ -297,7 +300,7 @@ def canonical_json_digest(obj: Any) -> str: """Small helper for hashing an arbitrary source document (e.g. a JSON Schema) into ``source_digest``, using plain sorted-key JSON -- not the receipt payload schema, since the source document is not itself a - receipt. Kept separate from ``compute_canonical_digest`` (VSTD-0.1 + receipt. Kept separate from ``compute_canonical_digest`` (VSTD-1 receipt stable-payload canonicalization) to avoid implying the source document conforms to that schema.""" import hashlib diff --git a/src/verifier/data/__init__.py b/src/verifier/data/__init__.py index 602b9ea..cc51803 100644 --- a/src/verifier/data/__init__.py +++ b/src/verifier/data/__init__.py @@ -1,4 +1,6 @@ -"""Target-neutral VSTD-Graph reference types and receipt mechanisms.""" +"""Terminology: Verifier Standard (VSTD). + +Target-neutral VSTD-Graph reference types and receipt mechanisms.""" from verifier.data.graph_level import ( GraphCollection, @@ -11,6 +13,7 @@ ArtifactStatus, ArtifactType, CompletenessMetrics, + ConflictRecord, ContributorSpec, HyperedgePort, ProvenanceHypergraph, @@ -32,6 +35,7 @@ "ArtifactStatus", "ArtifactType", "CompletenessMetrics", + "ConflictRecord", "ContributorSpec", "HyperedgePort", "ProvenanceHypergraph", diff --git a/src/verifier/data/graph_level.py b/src/verifier/data/graph_level.py index bb523d3..5530b35 100644 --- a/src/verifier/data/graph_level.py +++ b/src/verifier/data/graph_level.py @@ -1,19 +1,22 @@ -"""``graph_level`` -- how far up the VSTD-Graph ladder a collection actually got. +"""Terminology: application programming interface (API); conjunctive normal form (CNF); grounded decision certificate (GDC); +Boolean satisfiability problem (SAT); unsatisfiable (UNSAT); Verifier Standard (VSTD). + +``graph_level`` -- compatibility API for the candidate Graph profile satisfied by supplied collection ratings. VSTD is verification *mechanics* over one object. VSTD-Graph is verification -*dynamics* over a collection. The axes remain distinct: a collection holds at -Graph level ``N`` only when four separately checked conditions over the supplied -ratings and graph records hold at once. +*dynamics* over a collection. The axes remain distinct. This module computes a +candidate Graph profile from caller-supplied ratings; that computation is not +conformance unless a separate profile validates and binds those ratings. -1. **Membership floor** -- every member object is at object level >= N. +1. **Membership floor** -- every member object has an object-profile rating >= N. 2. **Provenance closure** -- every ancestor reachable from any member is also >= N. A plain minimum-over-members misses this, which is the whole reason a corpus of well-rated repositories can still be badly rated as a corpus. 3. **Status admissibility** -- no artifact in the closure is ``REVOKED``, ``CHALLENGED``, ``STALE`` or ``UNKNOWN``. Fail-closed, per the ``UNKNOWN`` - principle the data layer already applies elsewhere. -4. **Edge evidence** -- the transformation hyperedges themselves carry level-N - evidence. A graph is only as verified as its edges, and this is the condition + principle the data package already applies elsewhere. +4. **Edge evidence** -- the transformation hyperedges themselves carry profile-N + ratings. A graph is only as verified as its edges, and this is the condition that makes the axis dynamics rather than aggregation. Then, exactly as on the object axis:: @@ -21,12 +24,12 @@ graph_level(C) = max { N : CNF_N(C) is satisfiable } computed by iterated SAT descending 5 -> 1, and **the UNSAT certificate at N+1 -is the explanation of why the collection cannot rate higher**. That certificate -is a VSTD4-GDC-1 refutation. That certificate is evidence for the graph-level -ceiling only. It does not supply, imply, upgrade, or repair evidence for any -object or graph layer. +is the explanation of why those supplied ratings do not support a higher candidate**. +That certificate is a VSTD4-GDC-1 refutation of the encoded candidate only. It is not +evidence that any object or Graph profile was satisfied, and does not supply, imply, +upgrade, or repair evidence for one. -Three independent opinions must agree before this module reports a level: the +Three separately implemented checks must agree before this module reports a candidate: the certified Horn encoding, :class:`MinimalIndependentDPLL`, and a direct Python evaluation of the four conditions. Divergence raises rather than silently preferring one, because an encoding bug is precisely the failure that makes two @@ -68,17 +71,18 @@ INADMISSIBLE_STATUSES = frozenset( { - ArtifactStatus.REVOKED, - ArtifactStatus.CHALLENGED, - ArtifactStatus.STALE, - ArtifactStatus.UNKNOWN, + ArtifactStatus.REVOKED.value, + ArtifactStatus.CHALLENGED.value, + ArtifactStatus.STALE.value, + ArtifactStatus.UNKNOWN.value, + "CONFLICTED", } ) -"""Statuses that disqualify an artifact from any graph level. +"""Statuses that disqualify an artifact from any candidate Graph profile. ``SUPERSEDED`` is deliberately absent: a superseded artifact was replaced going forward, but its historical role in a lineage is unchanged and re-rating the -past every time something is superseded would make levels unstable for reasons +past every time something is superseded would make candidate profiles unstable for reasons having nothing to do with evidence. A caller wanting the stricter reading has :meth:`~verifier.data.policy.ProvenancePolicyVerifier.verify_all_ancestors_valid`, which admits ``VALID`` and nothing else. @@ -108,7 +112,7 @@ def __init__( # -------------------------------------------------------------------------- -# Obligations -- what a level actually asks of a collection +# Obligations -- what a candidate Graph profile asks of a collection # -------------------------------------------------------------------------- @@ -131,10 +135,10 @@ class ObligationKind(str, Enum): @dataclass(frozen=True) class Obligation: - """One thing a level requires, and what the graph actually says about it. + """One thing a candidate Graph profile requires and what the graph records. - ``observed`` is level-independent -- it is the ground fact. Whether the - obligation is *met* is a question asked of that fact once per level, which + ``observed`` is profile-independent -- it is the ground fact. Whether the + obligation is *met* is a question asked of that fact once per profile, which is why the variable numbering below is stable across all five encodings and only the unit clauses move. """ @@ -143,7 +147,7 @@ class Obligation: subject: str observed: str level: int = 0 - """The rated level behind ``observed``; unused for status obligations.""" + """Compatibility field carrying the profile rating; unused for status obligations.""" @property def predicate(self) -> str: @@ -151,15 +155,15 @@ def predicate(self) -> str: def met_at(self, level: int) -> bool: if self.kind is ObligationKind.STATUS_ADMISSIBILITY: - return self.observed not in {status.value for status in INADMISSIBLE_STATUSES} + return self.observed not in INADMISSIBLE_STATUSES return self.level >= level def describe(self, level: int) -> str: if self.kind is ObligationKind.STATUS_ADMISSIBILITY: return f"{self.kind.value}: {self.subject} is {self.observed}" return ( - f"{self.kind.value}: {self.subject} is rated {self.level}, " - f"which is below {level}" + f"{self.kind.value}: {self.subject} has profile rating {self.level}, " + f"below required profile {level}" ) def to_dict(self) -> dict[str, Any]: @@ -174,12 +178,12 @@ def to_dict(self) -> dict[str, Any]: @dataclass(frozen=True) class GraphCollection: - """A collection under test, with the ratings its level will be computed from. + """A collection under test, with ratings for its candidate Graph profile. - ``object_levels`` and ``edge_levels`` are read as ratings someone else - established. An artifact or edge with no entry is rated ``0``: unrated is - not a passing grade, and reading it as one is how a collection of unknowns - becomes a level-5 corpus. + ``object_levels`` and ``edge_levels`` retain compatibility field names and are + read as profile ratings someone else established. An artifact or edge with + no entry is rated ``0``: unrated is not passing, and reading it as one is how + a collection of unknowns becomes a candidate Graph-5 collection. """ collection_id: str @@ -213,7 +217,10 @@ def obligations(graph: ProvenanceHypergraph, collection: GraphCollection) -> tup ) for artifact_id in sorted(closure): node = graph.artifacts.get(artifact_id) - status = ArtifactStatus.UNKNOWN.value if node is None else node.status.value + if graph.has_conflict(artifact_id): + status = "CONFLICTED" + else: + status = ArtifactStatus.UNKNOWN.value if node is None else node.status.value found.append(Obligation(ObligationKind.STATUS_ADMISSIBILITY, artifact_id, status)) edges = sorted( @@ -224,7 +231,9 @@ def obligations(graph: ProvenanceHypergraph, collection: GraphCollection) -> tup } ) for transformation_id in edges: - level = collection.edge_level(transformation_id) + level = 0 if graph.has_conflict(transformation_id) else collection.edge_level( + transformation_id + ) found.append( Obligation(ObligationKind.EDGE_EVIDENCE, transformation_id, str(level), level) ) @@ -268,9 +277,9 @@ def encode( ) -> tuple[tuple[tuple[int, ...], ...], Grounding]: """CNF_N, together with the grounding that says what its variables mean. - Variable 1 is the collection holding at ``level``; variable ``1 + i`` is - obligation ``i``. The numbering does not move between levels -- only the - unit clauses do -- so two certificates for adjacent levels are directly + Variable 1 is the collection satisfying the profile number stored in ``level``; + variable ``1 + i`` is obligation ``i``. The numbering does not move between + profiles -- only the unit clauses do -- so two certificates for adjacent profiles are directly comparable rather than being two unrelated formulas that happen to share a subject. """ @@ -333,7 +342,7 @@ def certify_graph_cnf( verdict = kernel_check(certificate, binding=binding) if not verdict.accepted: raise GraphEncodingError( - f"{collection_id} at level {level}: the kernel refused this " + f"{collection_id} at candidate Graph profile {level}: the kernel refused this " f"collection's own certificate: {verdict.details}", certificate=certificate, ) @@ -346,8 +355,8 @@ def certify_graph_cnf( satisfiable, _model = solver.solve() if encoded != satisfiable: raise GraphEncodingError( - f"{collection_id} at level {level}: the certified encoding says " - f"{encoded} but the independent solver said {satisfiable}", + f"{collection_id} at candidate Graph profile {level}: the certified encoding says " + f"{encoded} but the separately implemented solver said {satisfiable}", certificate=certificate, cnf_satisfiable=satisfiable, direct_result=holds_at(items, level), @@ -356,7 +365,7 @@ def certify_graph_cnf( direct = holds_at(items, level) if encoded != direct: raise GraphEncodingError( - f"{collection_id} at level {level}: CNF encoding and direct " + f"{collection_id} at candidate Graph profile {level}: CNF encoding and direct " f"computation disagree -- encoding says {encoded}, direct " f"computation says {direct}. One of them is wrong and the " "certificate attached shows what the encoding actually proves.", @@ -379,17 +388,17 @@ def certify_graph_cnf( # -------------------------------------------------------------------------- -# The computed level +# The computed candidate Graph profile # -------------------------------------------------------------------------- @dataclass(frozen=True) class GraphLevelResult: - """A computed level, with the evidence for both halves of the answer. + """A candidate Graph profile computed from declared ratings, with its SAT evidence. - ``witness`` certifies the level reached. ``refutation`` certifies why the - next one was not, and ``blocking_obligations`` names what stopped it. A - level reported without a refutation at anything below + ``witness`` certifies the profile formula satisfied. ``refutation`` certifies + why the next one was not, and ``blocking_obligations`` names what stopped it. + A candidate reported without a refutation at anything below :data:`GRAPH_MAX_LEVEL` would be a declaration, which is the thing this module exists to avoid. """ @@ -399,17 +408,23 @@ class GraphLevelResult: witness: Optional[DecisionCertificate] refutation: Optional[DecisionCertificate] blocking_obligations: tuple[Obligation, ...] + rating_basis: str = field(default="CALLER_SUPPLIED", init=False) + conformance_status: str = field(default="NOT_ESTABLISHED", init=False) @property def explanation(self) -> str: if self.level >= GRAPH_MAX_LEVEL: - return f"{self.collection_id} holds at graph level {GRAPH_MAX_LEVEL}." + return ( + f"{self.collection_id} computes to candidate Graph profile " + f"{GRAPH_MAX_LEVEL} from caller-supplied ratings; conformance is not established." + ) blocked = "; ".join( item.describe(self.level + 1) for item in self.blocking_obligations ) return ( - f"{self.collection_id} holds at graph level {self.level}. " - f"Level {self.level + 1} is refuted by: {blocked or 'no obligation'}." + f"{self.collection_id} computes to candidate Graph profile {self.level} " + "from caller-supplied ratings; conformance is not established. " + f"Graph profile {self.level + 1} is refuted by: {blocked or 'no obligation'}." ) def to_dict(self) -> dict[str, Any]: @@ -417,6 +432,8 @@ def to_dict(self) -> dict[str, Any]: "collection_id": self.collection_id, "level": self.level, "max_level": GRAPH_MAX_LEVEL, + "rating_basis": self.rating_basis, + "conformance_status": self.conformance_status, "blocking_obligations": [item.to_dict() for item in self.blocking_obligations], "witness_digest": None if self.witness is None else self.witness.digest(), "refutation_digest": ( @@ -432,19 +449,26 @@ def graph_level( *, binding: ClaimBinding, ) -> GraphLevelResult: - """Compute the graph level of ``collection``, with the proof of its ceiling. + """Compute the candidate Graph profile, retaining the compatibility API name. - Descends from :data:`GRAPH_MAX_LEVEL`, so the first satisfiable level found - is the answer. The conditions are monotone in the level by construction -- + Descends from :data:`GRAPH_MAX_LEVEL`, so the first satisfiable profile formula + is the answer. The conditions are monotone in the profile number by construction -- an obligation met at ``N`` is met at every ``N' <= N`` -- so descending - means a fully-conformant collection costs one solve rather than five. + means a collection meeting its supplied ratings costs one solve rather than five. """ if not collection.members: raise GraphEncodingError( f"{collection.collection_id} has no members, so every obligation is " - "vacuously met and the encoding would hand out level " + "vacuously met and the encoding would hand out candidate Graph profile " f"{GRAPH_MAX_LEVEL} for a collection nobody can refute. An empty " - "collection has no level." + "collection satisfies no Graph profile." + ) + + closure = graph.ancestors(collection.members) + if not graph.verify_acyclicity(closure): + raise GraphEncodingError( + f"{collection.collection_id} has cyclic recorded ancestry, so recursive " + "reachability cannot establish a candidate Graph profile." ) items = obligations(graph, collection) diff --git a/src/verifier/data/models.py b/src/verifier/data/models.py index 279b5a6..5bec45c 100644 --- a/src/verifier/data/models.py +++ b/src/verifier/data/models.py @@ -1,6 +1,8 @@ -"""VSTD-Graph provenance models and algorithms. +"""Terminology: Secure Hash Algorithm 256-bit (SHA-256); Verifier Standard (VSTD). -Graph-1 receipts retain the frozen ``VSTD-DATA-0.1`` wire identifier. +VSTD-Graph provenance models and algorithms. + +Graph-1 receipts retain the frozen ``VSTD-DATA-0.1`` serialized receipt identifier. """ from __future__ import annotations @@ -162,6 +164,26 @@ def to_dict(self) -> dict[str, Any]: } +@dataclass(frozen=True) +class ConflictRecord: + """Retained incompatible evidence about one artifact or transformation field.""" + + conflict_id: str + subject_id: str + predicate: str + competing_values: tuple[str, ...] + evidence_refs: tuple[str, ...] + + def to_dict(self) -> dict[str, Any]: + return { + "conflict_id": self.conflict_id, + "subject_id": self.subject_id, + "predicate": self.predicate, + "competing_values": list(self.competing_values), + "evidence_refs": list(self.evidence_refs), + } + + @dataclass(frozen=True) class HyperedgePort: artifact_id: str @@ -236,22 +258,36 @@ def __init__(self) -> None: self.transformations: dict[str, TransformationHyperedge] = {} self.contributors: dict[str, ContributorSpec] = {} self.rights: dict[str, RightsSpec] = {} + self.conflicts: dict[str, ConflictRecord] = {} + + @staticmethod + def _add_unique(collection: dict[str, Any], identifier: str, value: Any) -> str: + if identifier in collection: + raise ValueError(f"duplicate graph identifier: {identifier}") + collection[identifier] = value + return identifier def add_artifact(self, artifact: ArtifactNode) -> str: - self.artifacts[artifact.artifact_id] = artifact - return artifact.artifact_id + return self._add_unique(self.artifacts, artifact.artifact_id, artifact) def add_transformation(self, transform: TransformationHyperedge) -> str: - self.transformations[transform.transformation_id] = transform - return transform.transformation_id + return self._add_unique( + self.transformations, transform.transformation_id, transform + ) def add_contributor(self, contributor: ContributorSpec) -> str: - self.contributors[contributor.contributor_id] = contributor - return contributor.contributor_id + return self._add_unique( + self.contributors, contributor.contributor_id, contributor + ) def add_rights(self, rights: RightsSpec) -> str: - self.rights[rights.rights_id] = rights - return rights.rights_id + return self._add_unique(self.rights, rights.rights_id, rights) + + def add_conflict(self, conflict: ConflictRecord) -> str: + return self._add_unique(self.conflicts, conflict.conflict_id, conflict) + + def has_conflict(self, subject_id: str) -> bool: + return any(record.subject_id == subject_id for record in self.conflicts.values()) def incoming_hyperedges(self, artifact_id: str) -> list[TransformationHyperedge]: """Hyperedges that produce artifact_id as an output.""" @@ -352,15 +388,41 @@ def validate_structure(self) -> list[str]: errors.append( f"transformation {transformation_id} has an empty role for {port.artifact_id}" ) + subjects = set(self.artifacts) | set(self.transformations) + for conflict_id, conflict in sorted(self.conflicts.items()): + if not conflict_id or conflict.conflict_id != conflict_id: + errors.append(f"conflict map key does not match conflict_id: {conflict_id}") + if conflict.subject_id not in subjects: + errors.append( + f"conflict {conflict_id} references missing subject {conflict.subject_id}" + ) + if not conflict.predicate: + errors.append(f"conflict {conflict_id} has an empty predicate") + if len(set(conflict.competing_values)) < 2: + errors.append(f"conflict {conflict_id} must retain at least two competing values") + if len(set(conflict.evidence_refs)) < 2: + errors.append(f"conflict {conflict_id} must retain at least two evidence references") return errors - def verify_acyclicity(self) -> bool: - """Check whether the bipartite artifact-hyperedge graph contains cycles.""" - adj: dict[str, set[str]] = {a: set() for a in self.artifacts} + def verify_acyclicity(self, artifact_ids: Optional[Iterable[str]] = None) -> bool: + """Check whether all or a selected artifact-induced subgraph contains cycles. + + Structural reference validation remains the responsibility of + :meth:`validate_structure`; missing referenced artifacts are retained as + vertices here so the cycle check itself remains total. + """ + if artifact_ids is None: + selected = set(self.artifacts) + for transform in self.transformations.values(): + selected.update(port.artifact_id for port in (*transform.inputs, *transform.outputs)) + else: + selected = set(artifact_ids) + adj: dict[str, set[str]] = {artifact_id: set() for artifact_id in selected} for t in self.transformations.values(): for inp in t.inputs: for out in t.outputs: - adj[inp.artifact_id].add(out.artifact_id) + if inp.artifact_id in selected and out.artifact_id in selected: + adj[inp.artifact_id].add(out.artifact_id) visited: set[str] = set() rec_stack: set[str] = set() @@ -377,7 +439,7 @@ def dfs(node: str) -> bool: rec_stack.remove(node) return False - for a in self.artifacts: + for a in selected: if a not in visited: if dfs(a): return False @@ -407,7 +469,7 @@ def compute_completeness(self) -> CompletenessMetrics: trans_cov = trans_covered / max(total_trans, 1) # 3. Content-digest declaration coverage. This is syntax coverage, not a - # physical-byte rehash; see VSTD-DATA-0.1 section 3. + # physical-byte rehash; see VSTD-Graph-1 section 3. digest_pattern = re.compile(r"^[0-9a-fA-F]{64}$") integ_covered = sum( 1 for art in self.artifacts.values() @@ -464,6 +526,7 @@ def to_dict(self) -> dict[str, Any]: "transformations": [t.to_dict() for t in self.transformations.values()], "contributors": [c.to_dict() for c in self.contributors.values()], "rights": [r.to_dict() for r in self.rights.values()], + "conflicts": [c.to_dict() for c in self.conflicts.values()], } @classmethod @@ -483,6 +546,16 @@ def from_dict(cls, data: Mapping[str, Any]) -> "ProvenanceHypergraph": rights_evidence_level=RightsEvidenceLevel(r_data.get("rights_evidence_level", "RIGHTS_DECLARED")), ) ) + for conflict_data in data.get("conflicts", []): + g.add_conflict( + ConflictRecord( + conflict_id=conflict_data["conflict_id"], + subject_id=conflict_data["subject_id"], + predicate=conflict_data["predicate"], + competing_values=tuple(conflict_data.get("competing_values", ())), + evidence_refs=tuple(conflict_data.get("evidence_refs", ())), + ) + ) for a_data in data.get("artifacts", []): g.add_artifact( ArtifactNode( diff --git a/src/verifier/data/policy.py b/src/verifier/data/policy.py index 2ba406e..c502cec 100644 --- a/src/verifier/data/policy.py +++ b/src/verifier/data/policy.py @@ -1,4 +1,8 @@ -"""Formal Policy Verification Engine for Dataset & Computational Provenance.""" +"""Terminology: conjunctive normal form (CNF); grounded decision certificate (GDC); +Boolean satisfiability problem (SAT); Software Package Data Exchange (SPDX); +Verifier Standard (VSTD). + +Formal Policy Verification Engine for Dataset & Computational Provenance.""" from __future__ import annotations diff --git a/src/verifier/data/receipt.py b/src/verifier/data/receipt.py index bb95ef6..1c460bd 100644 --- a/src/verifier/data/receipt.py +++ b/src/verifier/data/receipt.py @@ -1,6 +1,9 @@ -"""VSTD-Graph receipt model and canonical serialization. +"""Terminology: identifier (ID); JavaScript Object Notation (JSON); operating system (OS); +Boolean satisfiability problem (SAT); trusted computing base (TCB); Verifier Standard (VSTD). -Graph-1 receipts retain the frozen ``VSTD-DATA-0.1`` wire identifier. +VSTD-Graph receipt model and canonical serialization. + +Graph-1 receipts retain the frozen ``VSTD-DATA-0.1`` serialized receipt identifier. """ from __future__ import annotations @@ -9,11 +12,11 @@ import json import sys import time -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import Any, Optional -from verifier.core.checker import VerificationVerdict +from verifier.core.checker import IndependenceBasis, VerificationVerdict from verifier.core.provenance import ProvenanceRecord from verifier.core.receipt import compute_canonical_digest from verifier.data.models import CompletenessMetrics, ProvenanceHypergraph @@ -44,6 +47,8 @@ def to_dict(self) -> dict[str, Any]: @dataclass(frozen=True) class DataIndependentAudit: + """Historical wire field for a checker result plus explicit separation basis.""" + overall_verdict: VerificationVerdict acyclic_hypergraph: bool integrity_passed: bool @@ -52,6 +57,7 @@ class DataIndependentAudit: transformations_count: int trusted_computing_base: dict[str, str] audit_notes: list[str] + independence_basis: IndependenceBasis = field(default_factory=IndependenceBasis) def to_dict(self) -> dict[str, Any]: return { @@ -63,6 +69,7 @@ def to_dict(self) -> dict[str, Any]: "transformations_count": self.transformations_count, "trusted_computing_base": self.trusted_computing_base, "audit_notes": self.audit_notes, + "independence_basis": self.independence_basis.to_dict(), } @@ -189,7 +196,7 @@ def _duplicate_ids(items: Any, key: str) -> list[str]: def validate_data_receipt(receipt_path_or_dir: Path) -> int: - """Validate a VSTD-DATA-0.1 receipt without a target-specific adapter.""" + """Validate a VSTD-Graph-1 receipt without a target-specific adapter.""" receipt_file = _receipt_file(receipt_path_or_dir) if not receipt_file.exists(): print(f"[FAIL] Receipt not found at {receipt_file}", file=sys.stderr) @@ -263,6 +270,7 @@ def validate_data_receipt(receipt_path_or_dir: Path) -> int: ("transformations", "transformation_id"), ("contributors", "contributor_id"), ("rights", "rights_id"), + ("conflicts", "conflict_id"), ): graph_errors.extend(_duplicate_ids(graph_payload.get(collection), key)) try: @@ -290,6 +298,63 @@ def validate_data_receipt(receipt_path_or_dir: Path) -> int: if not isinstance(audit, dict): graph_errors.append("independent_audit must be an object") audit = {} + basis = audit.get("independence_basis") + if basis is not None: + if not isinstance(basis, dict): + graph_errors.append("independent_audit.independence_basis must be an object") + else: + basis_fields = { + "independently_verified", + "actor_independence", + "implementation_separation", + "runtime_separation", + "evidence", + } + missing_basis_fields = sorted(basis_fields - basis.keys()) + unknown_basis_fields = sorted(basis.keys() - basis_fields) + if missing_basis_fields: + graph_errors.append( + "independent_audit.independence_basis is missing fields: " + + ", ".join(missing_basis_fields) + ) + if unknown_basis_fields: + graph_errors.append( + "independent_audit.independence_basis has unknown fields: " + + ", ".join(unknown_basis_fields) + ) + statuses = { + "EVIDENCED", + "DECLARED", + "NOT_DEMONSTRATED", + "CONFLICTED", + } + separation_fields = ( + "actor_independence", + "implementation_separation", + "runtime_separation", + ) + for field_name in separation_fields: + if basis.get(field_name) not in statuses: + graph_errors.append( + f"independent_audit.independence_basis.{field_name} is not recognized" + ) + evidence = basis.get("evidence") + if not isinstance(evidence, list) or not all( + isinstance(item, str) and item for item in evidence + ): + graph_errors.append( + "independent_audit.independence_basis.evidence must be an array of nonempty strings" + ) + if basis.get("independently_verified") is not False: + graph_errors.append( + "independent_audit.independence_basis cannot be independently verified: " + "VSTD 1.2.0 has no actor/execution evidence-binding validator" + ) + if any(basis.get(field_name) == "EVIDENCED" for field_name in separation_fields): + graph_errors.append( + "independent_audit.independence_basis EVIDENCED assertions are unvalidated; " + "the bundled runtime treats externally supplied assertions as no stronger than DECLARED" + ) expected_audit_fields = { "acyclic_hypergraph": acyclic, "integrity_passed": completeness.content_integrity == 1.0, @@ -363,10 +428,13 @@ def validate_data_receipt(receipt_path_or_dir: Path) -> int: print(f"[FAIL] {error}", file=sys.stderr) return 1 - print(f"[PASS] Dataset Receipt {data.get('receipt_id')} is valid.") + print( + f"[VALIDATION OK] Dataset Receipt {data.get('receipt_id')} passed " + "the implemented stored-receipt checks." + ) print(f" Schema: {data.get('schema_version')}") print(f" Digest: {recorded_digest}") - print(f" Verdict: {data.get('independent_audit', {}).get('overall_verdict')}") + print(f" Stored checker verdict: {data.get('independent_audit', {}).get('overall_verdict')}") print(" Scope: stored receipt + recorded hypergraph; upstream bytes not rehashed") return 0 @@ -389,7 +457,7 @@ def reproduce_data_receipt(receipt_path_or_dir: Path) -> int: completeness = hypergraph.compute_completeness() elapsed_ms = (time.perf_counter() - start_time) * 1000.0 - print("[REPRODUCTION RESULT] Level: CONTENT_IDENTICAL") + print("[REPRODUCTION RESULT] Fidelity state: CONTENT_IDENTICAL") print(" Replay scope: stored receipt digest + hypergraph mechanisms") print(" Upstream execution: NOT_RECONSTRUCTED") print(f" Receipt ID: {data.get('receipt_id')}") @@ -473,17 +541,23 @@ def generate_data_receipt_markdown(receipt: VstdDataReceipt) -> str: --- -## 5. Independent Auditor & Trusted Computing Base (TCB) +## 5. Stored Checker Result & Trusted Computing Base (TCB) - **Acyclicity Verified:** {'PASSED (No cycles)' if audit.acyclic_hypergraph else 'FAILED (Cycle detected)'} - **Content-Digest Declaration Check:** {'PASSED' if audit.integrity_passed else 'FAILED'} -- **Overall Independent Verdict:** {audit.overall_verdict.value} +- **Overall Checker Verdict:** {audit.overall_verdict.value} +- **Independent Verification:** {'EVIDENCED' if audit.independence_basis.independently_verified else 'NOT_DEMONSTRATED'} ### TCB Declaration ```yaml {chr(10).join(f"{k}: {v}" for k, v in audit.trusted_computing_base.items())} ``` +### Independence Basis +```yaml +{chr(10).join(f"{k}: {v}" for k, v in audit.independence_basis.to_dict().items())} +``` + --- ## 6. Upstream Source & Environment Provenance @@ -496,14 +570,15 @@ def generate_data_receipt_markdown(receipt: VstdDataReceipt) -> str: --- -## 7. Independent Reproduction +## 7. Reproduction of Stored Checks -To independently inspect and reproduce this dataset hypergraph receipt: +To inspect and reproduce the stored dataset-hypergraph checks: ```bash vstd data verify receipts/{receipt.receipt_id} vstd data trace {spec.target_artifact_id} --receipt receipts/{receipt.receipt_id} ``` -*Generated by VSTD Data Runtime v0.1.0 (VSTD-DATA-0.1)* +*Generated by the VSTD-Graph-1 reference runtime +(serialized receipt identifier `VSTD-DATA-0.1`).* """ diff --git a/src/verifier/layer4/__init__.py b/src/verifier/layer4/__init__.py index 7dc5779..66d3a51 100644 --- a/src/verifier/layer4/__init__.py +++ b/src/verifier/layer4/__init__.py @@ -1,4 +1,6 @@ -"""VSTD-4 refutability records outside the trusted decision kernel.""" +"""Terminology: Verifier Standard (VSTD). + +VSTD-4 refutability records outside the trusted decision kernel.""" from .availability import ( ArtifactAvailability, diff --git a/src/verifier/layer4/availability.py b/src/verifier/layer4/availability.py index 65c050f..c577941 100644 --- a/src/verifier/layer4/availability.py +++ b/src/verifier/layer4/availability.py @@ -1,4 +1,7 @@ -"""Rung 4.8 -- the availability ladder. +"""Terminology: identifier (ID); International Organization for Standardization (ISO); +Verifier Standard (VSTD). + +Rung 4.8 -- the availability-state sequence. A hash is not availability. ``proof_sha256 = abc123…`` that nobody can obtain is cryptographically bound and completely uncheckable, and a verdict resting on it @@ -6,9 +9,11 @@ IDENTIFIED -> AVAILABLE -> PORTABLE -> SELF_CONTAINED -The levels are monotone in the same sense as +The states are monotone in the same sense as :class:`verifier.core.reproducibility.ReproducibilityLevel`, whose shape this -mirrors deliberately: an artifact at a level satisfies every level below it. +mirrors deliberately: an artifact in one state satisfies every prerequisite state. +The public ``AvailabilityLevel`` name and serialized ``*_level`` fields are retained +compatibility identifiers; they do not denote numbered VSTD profiles. > All verdict-critical artifacts MUST either accompany the certificate or be > retrievable through content-addressed references satisfying a declared @@ -29,7 +34,7 @@ class AvailabilityLevel(str, Enum): - """Monotone levels of artifact obtainability.""" + """Monotone artifact-obtainability states; name retained for compatibility.""" IDENTIFIED = "IDENTIFIED" """A content address exists. Nothing asserts that the bytes can be fetched.""" @@ -139,8 +144,9 @@ def to_dict(self) -> dict[str, Any]: class ArtifactAvailability: """One verdict-critical artifact and how obtainable it actually is. - The level is **derived**, never taken on the declarant's word -- see - :meth:`assess`. A record may state a level, and if the stated level exceeds + The availability state is **derived**, never taken on the declarant's word -- see + :meth:`assess`. A record may state a value in its compatibility ``declared_level`` + field, and if the stated value exceeds the derived one the record is refused rather than believed. """ diff --git a/src/verifier/layer4/challenge.py b/src/verifier/layer4/challenge.py index 593efa7..6ff158d 100644 --- a/src/verifier/layer4/challenge.py +++ b/src/verifier/layer4/challenge.py @@ -1,11 +1,14 @@ -"""Rung 4.12 -- the challenge protocol. +"""Terminology: Verifier Standard (VSTD). -Layer 4 must define what happens when someone says *this verdict is wrong*, +Rung 4.12 -- the challenge protocol. + +The Refutability coordinate must define what happens when someone says *this verdict is wrong*, even though nobody has yet. A challenge mechanism that exists but does not move verdict state is item 7 on the challenge-theater list, and until now this repository was on that list: ``ArtifactStatus.CHALLENGED`` has existed in -``verifier.data.models`` with **no producer anywhere in the tree**. This -module is its producer. +``verifier.data.models`` with **no producer anywhere in the tree**. This module +produces challenge-ledger claim state only; it is not an adapter that mutates or +binds that state into a VSTD-Graph artifact. The state machine:: @@ -25,7 +28,7 @@ :class:`ChallengeLedger` follows it, and :meth:`ChallengeLedger.status` recomputes from the records every time. -The split with layer 5 is clean and worth stating, because it is the whole +The split with profile 5 is clean and worth stating, because it is the whole reason this rung sits at 4 and not at 5: * **VSTD-4:** is the claim structurally challengeable? Testable alone, with a diff --git a/src/verifier/layer4/closure.py b/src/verifier/layer4/closure.py index 05c8dc9..08fcf40 100644 --- a/src/verifier/layer4/closure.py +++ b/src/verifier/layer4/closure.py @@ -1,4 +1,6 @@ -"""Rung 4.14 -- refutability closure, and the handoff out of layer 4. +"""Terminology: Verifier Standard (VSTD). + +Candidate rung 4.14 -- structural refutability closure. ``A`` is VSTD-4 and ``B`` is VSTD-4 does **not** make ``C = f(A, B)`` VSTD-4. Refutability is not preserved by arbitrary transformation, and assuming it is @@ -12,21 +14,23 @@ This rung is simultaneously three things, which is why it sits at the top: -* the top of layer 4; -* the precondition for VSTD-Graph condition 4 -- edges carry evidence, not just - nodes, because a graph is only as verified as its edges; -* the entry gate to VSTD-5. An external witness can only corroborate a claim - whose refutability composes, so ``vstd4_depth(claim) == 14`` is the gate. +* the structural top of the current VSTD-4 candidate; +* a candidate input to VSTD-Graph condition 4 -- edges need evidence, not just + nodes, because a graph is only as verified as its edges. + +The depths and certificate references accepted here are caller-supplied and are not +resolved by this module. Its accepted result is therefore a candidate with conformance +``NOT_ESTABLISHED``; it is not a VSTD-5 entry gate. :meth:`RefutabilityClosure.closed_depth` is the load-bearing computation: the output is capped at the *minimum* depth across its inputs and its transformation. Not the average, and emphatically not the maximum -- an unevidenced edge between -two layer-5 artifacts does not yield a layer-5 collection. +two profile-5 artifacts does not yield a Graph-5 collection. """ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from typing import Any, Optional @@ -53,7 +57,7 @@ class InputBinding: input_id: str certificate_digest: str depth: int - """``vstd4_depth`` of this input, computed by :mod:`verifier.core.depth`.""" + """Caller-supplied VSTD-4 candidate depth of this input.""" def to_dict(self) -> dict[str, Any]: return { @@ -87,6 +91,7 @@ class ClosureCheck: closed_depth: int details: str unmapped: tuple[str, ...] = () + conformance_status: str = field(default="NOT_ESTABLISHED", init=False) def to_dict(self) -> dict[str, Any]: return { @@ -94,6 +99,7 @@ def to_dict(self) -> dict[str, Any]: "closed_depth": self.closed_depth, "details": self.details, "unmapped": list(self.unmapped), + "conformance_status": self.conformance_status, } @@ -209,12 +215,12 @@ def validate(self) -> ClosureCheck: True, depth, f"closure over {len(self.inputs)} input(s) is complete; output is capped " - f"at vstd4_depth {depth}", + f"at VSTD-4 candidate depth {depth}; conformance is not established", ) def cap_output_depth(closure: RefutabilityClosure, claimed_depth: int) -> ClosureCheck: - """Refuse an output claiming more layer-4 depth than its closure supports. + """Refuse an output claiming more candidate depth than its closure supports. This is rung 4.13 acting across a transformation rather than across time, and it is the specific check VSTD-Graph condition 4 calls into. @@ -226,11 +232,12 @@ def cap_output_depth(closure: RefutabilityClosure, claimed_depth: int) -> Closur return ClosureCheck( False, check.closed_depth, - f"output claims vstd4_depth {claimed_depth} but its closure supports only " + f"output claims VSTD-4 candidate depth {claimed_depth} but its closure supports only " f"{check.closed_depth}; refutability does not increase under composition", ) return ClosureCheck( True, check.closed_depth, - f"output depth {claimed_depth} is within the closure's ceiling of {check.closed_depth}", + f"output candidate depth {claimed_depth} is within the closure's ceiling of " + f"{check.closed_depth}; conformance is not established", ) diff --git a/src/verifier/layer4/precommit.py b/src/verifier/layer4/precommit.py index 6392159..e17c723 100644 --- a/src/verifier/layer4/precommit.py +++ b/src/verifier/layer4/precommit.py @@ -1,4 +1,7 @@ -"""Rung 4.11 -- the precommitment envelope. +"""Terminology: International Organization for Standardization (ISO); +Coordinated Universal Time (UTC); Verifier Standard (VSTD). + +Rung 4.11 -- the precommitment envelope. Committing only the claim is not enough, and the gap is not subtle. A declarant can honestly precommit *"my system achieves X"* and then, after looking at the @@ -12,7 +15,7 @@ > A declarant MUST NOT select any verdict-material degree of freedom after > observing the evidence produced by that degree of freedom. -Two independent checks enforce it, and they catch different cheats. +Two separate checks enforce it, and they catch different cheats. :func:`audit_selections` compares what was *used* against what was *committed* -- that catches substitution. The temporal comparison catches the subtler case where the committed value was left open, or committed late: a choice timestamped diff --git a/src/verifier/layer4/surface.py b/src/verifier/layer4/surface.py index b92c90c..f9e80f0 100644 --- a/src/verifier/layer4/surface.py +++ b/src/verifier/layer4/surface.py @@ -1,7 +1,9 @@ -"""Rung 4.10 -- the explicit refutation surface. +"""Terminology: Verifier Standard (VSTD). + +Rung 4.10 -- the explicit refutation surface. VSTD-2 defines the *claim* surface. VSTD-4 defines the *refutation* surface of -that claim surface. This is where the two layers compose, and it is the rung +that claim surface. This is where the two profile coordinates compose, and it is the rung that turns "someone could theoretically challenge this" into a list: here are the predicates they may challenge, the coordinates on which each applies, and the evidence that would overturn the verdict. @@ -16,7 +18,7 @@ which is enforced literally: a surface with an empty ``admissible`` list is refused. A claim nobody is permitted to refute is not a strong claim, it is an -unfalsifiable one, and layer 4 exists to say so out loud. +unfalsifiable one, and the Refutability coordinate exists to say so out loud. ``excluded_claims`` is the other half, and it is not a disclaimer. It gives ``PHYSICAL_WORLD_COMPLETENESS`` a permanent machine-readable home: ordinary diff --git a/src/verifier/runtime/demo.py b/src/verifier/runtime/demo.py index 663b929..cda5288 100644 --- a/src/verifier/runtime/demo.py +++ b/src/verifier/runtime/demo.py @@ -1,4 +1,7 @@ -"""Deterministic adversarial demonstration of VSTD's refutation boundaries. +"""Terminology: command-line interface (CLI); JavaScript Object Notation (JSON); +Verifier Standard (VSTD). + +Deterministic adversarial demonstration of VSTD's refutation boundaries. The demo is intentionally self-contained and side-effect free unless a caller explicitly asks to emit its JSON specimens. It does not execute manifests, @@ -328,7 +331,7 @@ def _poisoned_ancestor() -> DemoResult: {"transform:extract": 5, "transform:collect": 5}, ) binding = ClaimBinding( - claim="compute the bounded graph level for collection:demo", + claim="compute the bounded candidate Graph profile for collection:demo", coordinate=ClaimCoordinate("collection:demo", "vstd_graph_level"), policy_root=canonical_digest("flagship-demo-graph-policy"), evidence_root=canonical_digest(graph.to_dict()), @@ -353,14 +356,14 @@ def _poisoned_ancestor() -> DemoResult: and refutation_check.verdict is Verdict.FAIL ) observed = ( - f"GRAPH-LEVEL-{result.level}; " + f"GRAPH-CANDIDATE-{result.level}; " f"{blockers[0].observed if blockers else 'NO-BLOCKER'}" ) return DemoResult( scenario="poisoned-ancestor", title="Revoked ancestor behind valid descendants", - question="Does a poisoned transitive ancestor cap the collection's graph level?", - expected="GRAPH-LEVEL-0; REVOKED blocker; checked refutation", + question="Does a poisoned transitive ancestor cap the collection's candidate Graph profile?", + expected="GRAPH-CANDIDATE-0; REVOKED blocker; checked refutation", observed=observed, ok=ok, details=result.explanation, @@ -373,8 +376,8 @@ def _poisoned_ancestor() -> DemoResult: "edge_levels": dict(sorted(collection.edge_levels.items())), }, "fixture_boundary": ( - "Object and edge levels are declared scenario inputs. This graph-level " - "refutation does not establish or upgrade their separate evidence." + "Object and edge profile ratings are declared scenario inputs. This " + "Graph-profile refutation does not establish or upgrade their separate evidence." ), "hypergraph": graph.to_dict(), "graph_result": result.to_dict(), diff --git a/src/verifier/specifications/LADDER.md b/src/verifier/specifications/LADDER.md index be23b3d..08fccf2 100644 --- a/src/verifier/specifications/LADDER.md +++ b/src/verifier/specifications/LADDER.md @@ -1,41 +1,219 @@ -# The VSTD Ladder — what the numbers mean +# The Verifier Standard (VSTD) verification complex — what the numbers mean + +> **Acronyms:** application programming interface (API); conjunctive normal form (CNF); Certificate Transparency (CT); +> deletion resolution asymmetric tautology (DRAT); grounded decision certificate (GDC); +> JavaScript Object Notation (JSON); National Institute of Standards and Technology (NIST); +> nondeterministic polynomial time (NP); proof-carrying code (PCC); +> World Wide Web Consortium provenance vocabulary (PROV); PROV data model (PROV-DM); Protect the Software (PS); +> Request for Comments (RFC); reverse unit propagation (RUP); Boolean satisfiability problem (SAT); +> Supply-chain Levels for Software Artifacts (SLSA); satisfiability modulo theories (SMT); +> SMT library standard (SMT-LIB); Secure Software Development Framework (SSDF); The Update Framework (TUF); +> unsatisfiable (UNSAT); World Wide Web Consortium (W3C). **Status:** project specification (normative for numbering and composition) **Editor:** TimeLordRaps **License:** Apache-2.0 -VSTD specification numbers are **layers of verification depth**, not revisions of a single -document. VSTD-3 does not supersede VSTD-1 any more than a floor supersedes its -foundation. +**Normative language:** The uppercase key words in this series are interpreted as +described by [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and +[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) only when they appear in all capitals; +lowercase uses are ordinary prose. + +**Reader context:** [`Concept guide and intellectual precedents`](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md) + +VSTD records separate answers to separate verification questions about an identified +claim, its evidence, the mechanism that checked it, and the bounds of that check. It does +not collapse those answers into one universal “verified†label or confidence score. The +questions, their evidence-bearing relations, and cumulative profiles over them form the +VSTD **verification complex**. + +### Read this first: the number is a checklist position, not a strength score + +A numbered profile is a cumulative checklist on one axis. `VSTD-3`, for example, means +that the required questions for `VSTD-1`, `VSTD-2`, and `VSTD-3` are each established by +their own applicable evidence. It does **not** mean “assurance strength 3,†software +version 3, or evidence that is three times stronger than `VSTD-1`. + +Here, **established** means that a named mechanism checked the exact required proposition +against bound evidence under declared limits. A field, document, or actor merely saying +that the proposition passed does not establish it. + +Consider one verification object with these separately recorded results: + +| Object-axis question | Current result | +|---|---| +| `VSTD-1` Claim Mechanics | established | +| `VSTD-2` Verification Surface | `UNKNOWN` | +| `VSTD-3` Substrate Accountability | established | + +Its **object profile depth is 1**. The cumulative checklist cannot skip the missing +`VSTD-2` result. The `VSTD-3` coordinate evidence remains recorded and useful, but it does +not fill the `VSTD-2` gap or make profile 3 satisfied. + +The Graph axis applies a different cumulative checklist to a collection of artifacts and +their recorded relations. `VSTD-3` and `VSTD-Graph-3` therefore ask different questions; +the shared number does not make them equivalent. + +In one sentence: a **closure coordinate** is one verification question, a **numbered +profile** is a cumulative checklist of those questions on one axis, and **profile depth** +is the largest uninterrupted prefix of that checklist that is established. + +### Terminology contract + +The rest of the Standard uses the following terms precisely: + +| Term | Plain meaning | Important boundary | +|---|---|---| +| **Closure coordinate** | One named verification question and its failure class, such as Claim Mechanics or Refutability. | Closure is scoped to that question. VSTD-2 surface closure, Graph provenance closure, refutability closure, and artifact-seal structural closure are different results. | +| **Numbered profile** | A cumulative checklist selected by `VSTD-N` or `VSTD-Graph-N`. Profile `N` requires its named coordinate and every earlier coordinate on the same axis. | A profile number is not a software revision, spatial layer, confidence score, or substitute for the underlying results. | +| **Profile axis** | One ordered family of cumulative checklists. VSTD has an object axis and a Graph axis. | Equal numbers on different axes do not identify equivalent or interchangeable results. | +| **Object profile depth** | For one verification object, start at `VSTD-1` and count upward only while every required coordinate remains established. The last uninterrupted number is its depth. | Depth is a compact summary of separately established results, not a new verdict, evidence-strength rating, or permission to ignore a later established coordinate after an earlier gap. | +| **Candidate Graph profile** | The greatest Graph checklist position satisfied by the current caller-supplied ratings. | The current calculation is `NOT_ESTABLISHED` because those ratings are not evidence-bound. It is not a verified Graph profile. | +| **VSTD-4 rung** | One of the fourteen ordered refutability obligations `4.1` through `4.14`. | “Rung†names only this internal sequence, never a top-level VSTD profile. | +| **Verification order** | One adjacent meta-verification order in the VSTD-2 geometry model. | The compatibility names `VerificationLayer` and `verification_layers` do not denote numbered VSTD profiles. | +| **Level** | A retained word in an explicitly named external taxonomy or compatibility identifier, including `ReproducibilityLevel`, `AvailabilityLevel`, `graph_level`, and serialized Graph `level` fields. | In Graph compatibility identifiers, the value is the candidate Graph profile number; “level†is not the governing name for a VSTD profile. | +| **Layer** | An implementation, protocol, or physical stack whose parts are ordered by containment. | It does not name `VSTD-N` or `VSTD-Graph-N`; historical paths such as `verifier.layer4` remain compatibility identifiers only. | +| **Tier** | A declared checker-cost class in `VSTD4-GDC-1`. | It is not a VSTD profile, evidence-strength rating, or actor rating. | + +“Profile†must also be qualified when confusion is possible: **numbered profile**, **receipt +profile**, **application profile**, or **geometry profile**. Likewise, “depth†must be +qualified as object profile depth, VSTD-4 normative or candidate depth, or lineage +topological depth. The retained `LADDER.md` filename is a stable document path, not the +governing topology; this document defines a verification complex. +Current public serialized identifiers, fields, class names, functions, and module paths +retain their exact compatibility spelling; adjacent prose supplies the precise meaning. +Profiles are therefore **requirement-set coordinates**, not spatial layers. A profile is +satisfied only when a named mechanism has bound evidence for every required fact; Boolean +SAT over caller-supplied assertions establishes only a candidate formula result. --- ## 1. The governing idea -Each layer names a distinct verification question and a distinct failure class. The -ordering is a composition rule, not logical entailment between layers. +Each closure coordinate names a distinct verification question and failure class. Profile +ordering is a composition rule, not logical entailment between coordinates. + +The nearest familiar security analogy is +[defense in depth](https://en.wikipedia.org/wiki/Defense_in_depth_%28computing%29 "Wikipedia orientation; primary references are mapped below"), +but the analogy is limited: VSTD closure coordinates are separately evidenced questions, not +interchangeable controls whose mere quantity establishes assurance. Decomposing assurance +into named components also has precedent in the Common Criteria, while VSTD deliberately +uses different coordinates, evidence rules, and conformance semantics. -**Evidence for one layer never supplies evidence for another layer.** In particular, -layer-4 evidence does not supply, imply, upgrade, or repair layer 3, 2, or 1. A reported -depth of `N` is only shorthand for `N` separately checked results, one for each layer -from 1 through `N`. +**Evidence for one closure coordinate never supplies evidence for another.** In particular, +Refutability evidence does not supply, imply, upgrade, or repair Substrate Accountability, +Verification Surface, or Claim Mechanics evidence. A reported object profile depth of `N` +is only shorthand for the separately checked results required by profiles 1 through `N`. -Reflection and metalanguage are useful design analogies for asking what a given +Reflection and [metalanguage](https://en.wikipedia.org/wiki/Metalogic "Wikipedia orientation; not a proof of the VSTD verification complex") +are useful design analogies for asking what a given verification surface leaves unexamined. VSTD does not claim that Tarski's -undefinability theorem proves this ladder, that adjacent layers form formal -metalanguages, or that a lower-layer implementation is logically incapable of -describing another layer's failure. The normative requirement is narrower: an +[undefinability theorem](https://en.wikipedia.org/wiki/Tarski%27s_undefinability_theorem "Wikipedia orientation; the theorem does not derive this verification complex") +proves these profile coordinates, that adjacent profiles form formal +metalanguages, or that an earlier-profile implementation is logically incapable of +describing another coordinate's failure. The normative requirement is narrower: an implementation MUST NOT treat success on one question as evidence for a different question. +### 1.1 Artifact-first causal provenance orientation + +VSTD evaluates bounded propositions about computational processes represented by +identified software, executions, evidence, and resulting artifacts. It does not evaluate +whether an actor is good, bad, reputable, or worthy of trust. Standing alone, an actor's +identity, popularity, repetition, or reputation MUST NOT strengthen an artifact-bound +result. A named mechanism MAY establish an exact attribution, authorization, or separation +proposition by checking the required identity evidence; that result remains an adjacent +proposition and MUST NOT promote an unrelated computational claim. + +**Zero identity** means zero identity-derived verdict weight, not anonymity or absence of +identifiers. **Zero knowledge** means zero unevidenced knowledge is presumed: without a +mechanism-earned result for the exact proposition, its state remains `UNKNOWN`. This +architectural zero-knowledge rule MAY be enclosed by cryptographic zero knowledge when a +witness must remain confidential. That enclosure MUST bind the exact software or program +coordinate, predicate, public commitments, output, proof parameters, and verification +mechanism while revealing no more witness information than its declared proof statement. +Cryptographic zero knowledge MUST be claimed only when a named proof system establishes +that property under explicit assumptions; a digest or undisclosed input alone is not such +a proof. The resulting support is bearer- and artifact-bound, never prover-identity-bound. +**Actor** and **artifact** remain contextual roles, not permanent entity classes: software +can be an artifact when created, versioned, or evaluated and an actor when it executes a +transformation. + +The capitalized terms **TRUST**, **RUST**, and **ROT** are formal VSTD semantic names, not +acronyms, serialized receipt values, actor ratings, scalar scores, or references to the Rust programming +language. The same bound development graph and its time-indexed lifecycle carry three +distinct relations: + +```text +development: ancestor artifact --TRUST through a checked transformation--> descendant +lifecycle: recorded TRUST --ROT under typed current-state evidence--> reassessment +diagnosis: descendant deviation --RUST memetic causal backtrace--> ancestor candidates +``` + +**Memetic propagation** is the transmission of claim and evidence state through recorded +developmental provenance. The genetic or viral language names this inheritance mechanic: +TRUST moves forward into descendant claim space; RUST moves backward toward recorded +ancestor states; ROT changes the current admissibility of previously recorded support. It +does not claim biological transmission or make identity and reputation sources of +assurance. + +**TRUST** is positive support earned when a named mechanism checks an exact +artifact-bound process obligation under declared evidence, specification, bounds, and +trust roots. It moves parent-to-child only across a declared creation or dependency edge +whose relevant transformation obligations pass. Applicable support composes by +intersection and is capped by the weakest required parent or edge; it is never added, +averaged, voted, or converted into actor standing. Every child MUST still discharge its +new predicates, transformations, boundaries, and evidence obligations. A declared trust +root is an explicit dependency and stopping boundary, not actor TRUST. + +**ROT** is typed, time-indexed degradation of the current admissibility of recorded TRUST. +It requires exact lifecycle or dependency evidence, such as expiry under a declared +freshness bound, `STALE`, `CHALLENGED`, `REVOKED`, `SUPERSEDED`, or an invalidated required +coordinate. Wall-clock passage, age, or popularity alone MUST NOT create ROT. ROT MUST NOT +rewrite an immutable historical receipt or imply that its historical result was false. It +may require reassessment of dependent descendants, but any resulting status change still +requires its named policy or mechanism. + +**RUST** is the inverse-TRUST diagnostic mechanic: a typed trace created by an observed +descendant deviation from a declared expectation. It moves child-to-parent only through +recorded admissible creation, input, or transformation paths. The inverse is directional +and diagnostic, not arithmetic: TRUST and RUST never cancel. Distinct comparable +backtraces may concentrate on a shared ancestor and prioritize it for falsification or +diagnostic examination. Transferred RUST establishes ancestral reachability, not direct +observation, falsehood, or causal responsibility; localization requires additional +intervention, ablation, independently bound execution evidence, or an equivalent declared +mechanism. + +The word *causal* is required here for recorded developmental and provenance causality: +the graph states which artifacts and transformations produced later claim architecture. +Propagation across those causal-provenance edges does not by itself establish +intervention-level physical causality, causal localization, responsibility, or guilt. + +TRUST, ROT, and RUST MUST remain separate. They do not cancel, form one scalar score, or +flow in the opposite direction as inherited truth, decay, or guilt. `UNKNOWN` and +`CONFLICTED` support or lineage MUST remain visible and MUST NOT become a clean signal. +This section fixes their semantic orientation and prohibited inferences; event formats, +TRUST-transfer algebra, ROT derivation and propagation, RUST concentration-independence, +and localization protocols each require their own specification and evidence. Until those +exist, the three terms are causal-provenance and lifecycle constraints, not serialized receipt values or +computable conformance results; no current VSTD runtime emits or validates a TRUST, ROT, or +RUST transfer. + +Artifact freezing and sealing are bounded mechanisms under this orientation, specified +separately in [`ARTIFACT_CONTROL.md`](ARTIFACT_CONTROL.md). A verified freeze preserves +and recomputes exact bytes; a verified seal earns structural closure for those bytes. It +does not earn semantic correctness, prevent ROT, localize RUST, create actor TRUST, or +supply any numbered profile. A sealed realm or temporal descriptor remains a bound input +until its own mapping, continuity, or transition verifier checks the exact proposition. + --- -## 2. The object ladder +## 2. The object profile axis VSTD proper governs the verification of **one object**. Call this verification *mechanics*. -| Layer | Name | Closes | Does not establish | +| Numbered profile | Required closure coordinate | Closes | Does not establish | |---|---|---|---| | **1** | Claim mechanics | A malformed or tampered statement | Whether the claim applies where it is being applied | | **2** | Verification surface | A verdict leaking beyond the coordinate actually verified | Whether the evidence behind it is real | @@ -43,18 +221,23 @@ VSTD proper governs the verification of **one object**. Call this verification | **4** | Refutability | A claim unfalsifiable in principle by any outside party | Whether the parties who could check are independent | | **5** | Witness corroboration | Pseudo-independence — witnesses sharing the declarant's trust root | — | -### 2.1 The self-discernability boundary +### 2.1 The single-declarant boundary + +**A single declarant can in principle produce the evidence required by profiles 1 through +4.** No second party is required merely to create those bounded inputs and mechanisms. -**Layers 1 through 4 are self-discernable.** A declarant can establish them alone, with -no second party in existence. +**Profile 5 is not.** It requires another party to exist, to act, and to be independent. -**Layer 5 is not.** It requires another party to exist, to act, and to be independent. +VSTD-1 records the claim-mechanics status of actor independence but cannot infer it from +two runs or matching artifacts. VSTD-5 requires the corroborating witness procedure that +uses such separately evidenced actor participation; recording a field is not witnessing. -That transition between 4 and 5 is the most important boundary in the ladder. Layer 4 -asks *could a stranger check this?* Layer 5 asks *did one, and were they actually a -stranger?* The first is a property of the claim. The second is a property of the world. +That transition between profiles 4 and 5 is the most important object-axis boundary. +Refutability asks *is a bounded outside check possible?* Witness Corroboration asks *was +one performed, and are the required separation seams evidence-bound?* The first is a +property of the claim surface. The second requires additional evidence about an execution. -An implementation MUST NOT report a layer-5 property on the basis of layer-4 evidence. +An implementation MUST NOT report a profile-5 property on the basis of Refutability evidence. Preparing to be checked is not being checked. --- @@ -65,9 +248,12 @@ VSTD-Graph governs the verification of a **collection** of objects. Call this verification *dynamics*. The two axes are parallel but coupled: a collection's dynamics are constrained by its -members' mechanics, and by the provenance edges between them. +members' mechanics, and by the +[provenance](https://en.wikipedia.org/wiki/Data_provenance "Wikipedia orientation; see W3C PROV-DM and supply-chain references below") +edges between them. The implemented N-ary representation is a +[hypergraph](https://en.wikipedia.org/wiki/Hypergraph "Wikipedia orientation; not a claim of complete real-world lineage"). -| Layer | Name | Collection-level closure | +| Numbered profile | Required closure coordinate | Collection proposition | |---|---|---| | **Graph-1** | Recorded lineage | members and transformations are represented | | **Graph-2** | Bounded collection surface | scope does not leak across the collection | @@ -75,27 +261,34 @@ members' mechanics, and by the provenance edges between them. | **Graph-4** | Refutable transformation closure | challenges compose across hyperedges | | **Graph-5** | Corroborated verification network | member and edge witnesses are independently corroborated | -A collection `C` holds at Graph layer `N` only if all four conditions hold: +A collection `C` satisfies candidate Graph profile `N` only if all four conditions hold: -1. **Membership floor** — every member is at object layer ≥ N. -2. **Provenance closure** — every ancestor reachable from any member is at layer ≥ N. -3. **Status admissibility** — no ancestor is `REVOKED`, `CHALLENGED`, `STALE`, or - `UNKNOWN`. -4. **Edge evidence** — the transformation hyperedges themselves carry layer-N evidence. +1. **Membership floor** — every member rating is at object profile ≥ N. +2. **Provenance closure** — every ancestor reachable from any member is rated at object + profile ≥ N. +3. **Status admissibility** — no ancestor is `REVOKED`, `CHALLENGED`, `STALE`, + `UNKNOWN`, or subject to an unresolved `CONFLICTED` record. +4. **Edge evidence** — the transformation hyperedges themselves carry profile-N ratings. Condition 2 is what a plain minimum over members misses. Condition 4 is what makes this dynamics rather than aggregation: **a graph is only as verified as its edges**, and an -unevidenced edge between two layer-5 artifacts does not yield a layer-5 collection. +unevidenced edge between two profile-5 artifacts does not yield a Graph-5 collection. -The level is **computed, never declared**: +The candidate Graph profile number is **computed from object and edge ratings, never +declared**: ``` -graph_level(C) = max { N : CNF_N(C) is satisfiable } +candidate_graph_profile(C) = max { N : CNF_N(C) is satisfiable } ``` -The reference implementation searches 5→1. At a result below 5, the grounded -`FAIL` certificate for `N+1` is the explanation of the ceiling. A level without -that certificate is a declaration and is non-conforming. +The compatibility API `graph_level` implements that function and the frozen Graph receipt +stores its number in a `level` field. The reference implementation searches 5→1 and +certifies its Boolean encoding. Its current rating inputs are caller-supplied, so it reports +a **candidate Graph profile** with +`conformance_status = NOT_ESTABLISHED`; the certificate proves the computation over +those inputs, not the validity of the ratings. At a result below 5, the grounded `FAIL` +certificate for profile `N+1` explains that candidate ceiling. Graph conformance additionally +requires evidence-bound ratings under the applicable object and edge profiles. --- @@ -108,17 +301,22 @@ the third is the load-bearing one. VSTD does not classify every receipt as an NP certificate. Specific bounded formats, including `VSTD4-GDC-1`, define a finite decision problem, a certificate language, and -an independent checker. Complexity claims apply only to such a defined formal problem. +a checker implemented separately from the producer path. Complexity claims apply only +to such a defined formal problem; checker separation alone does not establish distinct +actors. Other receipt fields may be signed declarations, hashes, measurements, or references whose meaning depends on explicitly named trust roots. The useful engineering asymmetry is concrete rather than universal: when a result can -carry a smaller independently checkable artifact instead of requiring the original +carry a smaller consumer-checkable artifact instead of requiring the original computation, VSTD preserves that artifact and its verification bounds. ### 4.2 Bounded admission uses CNF -The reference admission procedures encode finite, bounded policy questions as CNF. +The reference admission procedures encode finite, bounded policy questions as +[conjunctive normal form](https://en.wikipedia.org/wiki/Conjunctive_normal_form "Wikipedia orientation; the implemented format is finite CNF") +(CNF) for the +[Boolean satisfiability problem](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem "Wikipedia orientation; SAT success establishes only the encoded formula"). CNF is not identical to 3-SAT. A finite CNF satisfiability instance can be transformed in polynomial time into an equisatisfiable 3-CNF instance, using auxiliary variables where required. VSTD does not need that transformation for every checker and does not @@ -136,7 +334,7 @@ A future claim may cover a finite enumerated world if its observation boundary a completeness mechanism are declared and checked. It still MUST NOT be widened into a claim about unobserved physical activity. -This is why the ladder tops out at corroboration rather than proof of global absence. Layer 5 +This is why the object profile axis tops out at corroboration rather than proof of global absence. Profile 5 does not detect hidden work. It makes the *independence status* of declared work legible, and leaves the undeclared remainder named and quantified rather than silent. @@ -145,7 +343,7 @@ and leaves the undeclared remainder named and quantified rather than silent. ## 5. Certificates for refusals This section applies to the finite propositional decision procedures used by the -reference layer-4 implementation. +reference Refutability implementation. A satisfiable result already carries its certificate: the model. Anyone can evaluate it against the clause set without a solver. @@ -154,8 +352,12 @@ An unsatisfiable result, by default, carries nothing but the solver's word. For a fail-closed standard, **refusals are the most consequential output**. A standard whose passes are checkable and whose refusals are not has its assurance backwards. -Layer 4 therefore requires a refutation certificate — a clausal proof, verifiable by -reverse unit propagation, checkable without re-solving. +The Refutability coordinate therefore requires a refutation certificate — a clausal proof, verifiable by +[reverse unit propagation](https://en.wikipedia.org/wiki/Unit_propagation "Wikipedia orientation; VSTD implements a bounded RUP checker"), +checkable without re-solving. This follows the same producer-certificate/consumer-checker +engineering asymmetry as +[proof-carrying code](https://en.wikipedia.org/wiki/Proof-carrying_code "Wikipedia orientation; VSTD does not inherit PCC's safety theorem"), +while using a narrower certificate language. Resolution proofs have exponential lower bounds for some formula families. A conforming implementation therefore MUST declare a bound and MUST answer `UNKNOWN` @@ -165,46 +367,77 @@ An `UNKNOWN` is never a pass and never an unsatisfiability claim. Reference implementation: `verifier.core.refutation`. -### 5.1 The internal VSTD-4 ladder +### 5.1 The internal VSTD-4 rung sequence VSTD-4 contains fourteen ordered rungs, from decision certification through semantic binding, anti-equivocation, bounded portable checking, availability, -precommitment, challenge handling, degradation, and compositionality. Its depth +precommitment, challenge handling, degradation, and compositionality. Its normative depth is computed: ``` vstd4_depth(claim) = max { k : CNF_4k(claim) is satisfiable } ``` -The certificate for rung `k+1` explains a partial depth. Only depth 14 admits a -claim to any VSTD-5 procedure. See `VSTD-4.md` for the normative rung graph and -`VSTD4-GDC-1` format. +The certificate for rung `k+1` explains a partial VSTD-4 normative depth. Only established +VSTD-4 conformance at normative depth 14 admits a claim to any VSTD-5 procedure. The current +reference `vstd4_depth` function instead computes a structural candidate from +caller-supplied rung references, labels conformance `NOT_ESTABLISHED`, and never admits +VSTD-5. See `VSTD-4.md` for the normative rung graph and `VSTD4-GDC-1` format. --- -## 6. Composition — layers do not supply or substitute +## 6. Composition — closure coordinates do not supply or substitute -Layer results may be composed into a depth report. They do not replace, entail, or -supply one another. +Closure-coordinate results may be composed into an object profile-depth report. They do +not replace, entail, or supply one another. -- Layer 4 without layer 3 certifies a claim whose evidence source is unaccountable. -- Layer 5 without layer 4 solicits witnesses for a claim no witness could check. -- Layer 2 without layer 1 scopes a statement whose integrity is unestablished. +- Refutability without Substrate Accountability certifies a claim whose evidence source is unaccountable. +- Witness Corroboration without Refutability solicits witnesses for a claim no witness could check. +- Verification Surface without Claim Mechanics scopes a statement whose integrity is unestablished. -An implementation reporting aggregate depth *N* MUST present separately checkable -evidence for every layer from 1 through *N*. Conformance may also be reported for an -individual layer without claiming aggregate depth. Conformance profiles are declared -per layer, following VSTD-3 §7. +An implementation reporting object profile depth *N* MUST present separately checkable +evidence for every required coordinate in profiles 1 through *N*. Conformance may also be +reported for one coordinate without claiming cumulative profile depth. Incremental +Substrate Accountability profiles are declared in VSTD-3 §7. "Higher is more protected" is true only in the sense that more classes of failure are -closed. It never means the lower layers became unnecessary. +closed. It never means the prerequisite coordinates became unnecessary. --- ## 7. Numbering -- **Specification layers are integers**: VSTD-1 … VSTD-5, VSTD-Graph-1 … VSTD-Graph-5. -- **Repository releases use semantic versioning** and are independent of layer numbers. +- **Numbered profiles use integers**: VSTD-1 … VSTD-5, VSTD-Graph-1 … VSTD-Graph-5. +- **Repository releases use [semantic versioning](https://semver.org/)** and are independent + of profile numbers. -A release version never implies a layer, and a layer never implies a release. See -`WIRE_IDENTIFIERS.md` for frozen wire identifiers and the historical public filenames. +A release version never implies a numbered profile, and a numbered profile never implies a +release. See +`WIRE_IDENTIFIERS.md` for serialized receipt identifiers and historical public filenames. + +--- + +## 8. Intellectual lineage and adjacent precedents + +The verification complex is VSTD project architecture; no cited work proves that these five +coordinates on either axis are +necessary, sufficient, complete, or uniquely ordered. The references below show that its +individual design pressures have established precedents in security engineering, +provenance, reproducible systems, and proof checking. The +[`concept guide`](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md) provides definitions, additional +sources, and explicit non-equivalences. + +| VSTD pressure | Adjacent precedent | What the precedent contributes—and does not | +|---|---|---| +| Separate failure surfaces and fail-closed defaults | Saltzer and Schroeder, [*The Protection of Information in Computer Systems*](https://web.mit.edu/Saltzer/www/publications/pubs.html) | Classic principles include fail-safe defaults, complete mediation, separation of privilege, and least common mechanism. They motivate separation; they do not derive VSTD's coordinate count. | +| Named assurance components | Common Criteria, [Part 3: Security assurance components](https://www.commoncriteriaportal.org/files/ccfiles/CC2022PART3R1.pdf) | Demonstrates established componentized assurance and assurance packages. VSTD is not a Common Criteria evaluation or an Evaluation Assurance Level. | +| Stable cryptographic representations | [RFC 8785: JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html) | Shows why JSON used as cryptographic input needs invariant representation. VSTD formats retain their own declared canonicalization rules. | +| Recorded entities, activities, and agents | W3C [PROV-DM](https://www.w3.org/TR/prov-dm/) | Supplies an interoperable provenance model adjacent to the Graph axis. VSTD-Graph is not a PROV implementation and does not infer complete history. | +| Software materials, builders, steps, and products | [in-toto specification v1.0](https://in-toto.io/docs/specs/) and [SLSA v1.2](https://slsa.dev/spec/v1.2/) | Establish supply-chain provenance and attestation precedents. VSTD may bind their evidence but cannot manufacture their authorization or assurance level. | +| Preserved release and provenance evidence | NIST [Special Publication (SP) 800-218 SSDF 1.1](https://doi.org/10.6028/NIST.SP.800-218) | Protect the Software practices PS.3.1 and PS.3.2 call for preserving releases and provenance and enabling integrity verification. They do not certify a VSTD receipt. | +| Independent recreation | Reproducible Builds, [formal definition](https://reproducible-builds.org/docs/definition/) | Grounds the special case where another party recreates specified artifacts from declared inputs and instructions. Reproducibility does not establish every semantic claim. | +| Producer-supplied portable certificates | Necula, [*Proof-Carrying Code*](https://doi.org/10.1145/263699.263712) | Establishes the pattern of an untrusted producer supplying a proof checked under a declared policy. VSTD uses the pattern beyond code safety without inheriting PCC's theorem. | +| Consumer-checked UNSAT results | Wetzler, Heule, and Hunt, [*DRAT-trim*](https://www.cs.cmu.edu/~mheule/publications/drat-trim.pdf) | Establishes practical checking of clausal unsatisfiability proofs rather than trusting solver output. VSTD's implemented RUP format is narrower than DRAT. | +| A first-class refusal to fabricate a Boolean answer | [SMT-LIB Standard 2.7](https://smt-lib.org/papers/smt-lib-reference-v2.7-r2025-04-09.pdf) | Its response grammar includes `sat`, `unsat`, and `unknown`. VSTD independently defines a richer status system with the same fail-closed pressure. | +| Append-only public evidence and detectable equivocation | [RFC 9162: Certificate Transparency Version 2.0](https://www.rfc-editor.org/rfc/rfc9162.html) | Merkle proofs make log inclusion and consistency auditable while preserving explicit split-view limitations. VSTD additive receipts are analogous, not a CT implementation. | +| Freshness, rollback, freeze, and compromise recovery | [The Update Framework specification](https://theupdateframework.github.io/specification/latest/) | Demonstrates that authentic old data is not automatically current data. VSTD does not implement TUF, but likewise keeps freshness and revocation distinct from byte identity. | diff --git a/src/verifier/specifications/VSTD-1.md b/src/verifier/specifications/VSTD-1.md new file mode 100644 index 0000000..bb928a7 --- /dev/null +++ b/src/verifier/specifications/VSTD-1.md @@ -0,0 +1,192 @@ +# Verifier Standard (VSTD)-1 — Claim Mechanics + +> **Acronyms:** artificial intelligence (AI); conjunctive normal form (CNF); directed acyclic graph (DAG); +> Davis-Putnam-Logemann-Loveland (DPLL); International Organization for Standardization (ISO); +> JavaScript Object Notation (JSON); Request for Comments (RFC); Boolean satisfiability problem (SAT); +> Secure Hash Algorithm 256-bit (SHA-256); satisfiability modulo theories (SMT); trusted computing base (TCB); +> Coordinated Universal Time (UTC); Unicode Transformation Format, 8-bit (UTF-8). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-1 on the object axis; required closure coordinate: Claim Mechanics (see `LADDER.md`) +**Receipt serialization:** `schema_version = "VSTD-1"`; see `WIRE_IDENTIFIERS.md` +**Status:** Project Specification with Implemented Reference Subset +**Maintainer:** TimeLordRaps +**Date:** 2026-08-21 + +--- + +## 1. Purpose & Thesis + +VSTD specifies infrastructure for consequential computational claims to carry +evidence checkable outside its producer. Conformance is defined by this document, not by +the identity of its maintainer. + +Modern AI systems, scientific simulators, and autonomous code generators routinely +produce complex assertions without an attached, machine-checkable audit trail showing +what evidence is offered for those claims. **VSTD-1** is a project +specification for representing claims, capturing runtime provenance, structuring +machine-readable verification receipts, defining reproduction-fidelity states, and +separating trusted computing bases from untrusted outputs. It is not a consensus or +accredited standard. + +--- + +## 2. Scope & Boundaries + +### 2.1 What VSTD-1 Covers +- **Software Artifacts**: Deterministic test execution, static invariant validation, schema conformance. +- **Formal & Logic Artifacts**: Bounded propositional entailment, derivation graphs, + acyclicity checks, and grounding invariants. The current reference subset implements + a minimal propositional DPLL path; it does not implement general SMT verification. +- **AI & Autonomous Agents**: Bounded input/output constraints, zero-trust admission policies, and execution traces. +- **Scientific Simulation**: Invariant checking, exactness bounds, and deterministic reproduction traces. + +### 2.2 What a VSTD Verification Claim Does NOT Imply +1. **Universal Truth**: Verification is strictly relative to the declared formal system, input formula, and explicit scope. +2. **Unbounded Safety**: A verified component does not guarantee overall system safety if surrounding orchestration or unmodeled environmental dynamics fail. +3. **Unchecked Prose**: Non-extracted, unverified natural language outside the formal translation grammar is not certified. + +--- + +## 3. Epistemic Ontology & Claim Statuses + +Claims conforming to this specification must carry one of the following explicit status +labels. Producers and validators MUST downgrade or challenge a claim when applicable +evidence is missing or falsified. A historical receipt is immutable: correction is an +additive record rather than an in-place rewrite. + +| Status | Definition | +| :--- | :--- | +| `DEMONSTRATED` | The claim is backed by executable tests or formal proofs that pass in a reproducible environment with recorded execution coordinates. Actor independence is a separate claim. | +| `BENCHMARKED` | Quantitative performance or accuracy metrics have been empirically measured against a defined reference baseline. | +| `SUPPORTED` | Theoretical derivation or empirical evidence is established, but automated end-to-end continuous verification is partial. | +| `IMPLEMENTED_UNVALIDATED` | Code or logic exists on disk, but automated end-to-end verification has not yet run or passed. | +| `INDETERMINATE` | Evidence is ambiguous, supporting leaves are unspecified, or solver execution timed out. | +| `UNSUPPORTED` | No valid empirical or formal evidence is attached to the proposition. | +| `FALSIFIED` | An executable check, counterexample, or evidence-bound audit refuted the claim. | +| `HYPOTHESIS` | A stated conjecture intended for experimental falsification. | +| `LONG_RANGE_OBJECTIVE` | A strategic or architectural aspiration requiring substantial future R&D. | + +--- + +## 4. Claim Representation Schema + +A canonical claim record contains: +- `id`: Unique identifier (e.g., `VFY-000001`). +- `title`: Short human-readable summary. +- `statement`: Precise, bounded technical claim. +- `status`: Verification status from the ontology above. +- `scope`: Bounded operational domain. +- `limitations`: Explicit list of assumptions, bounds, and exclusions. +- `falsification_condition`: Explicit condition under which the claim is considered refuted. +- `last_verified`: ISO-8601 UTC timestamp of the most recent passing verification. + +--- + +## 5. Independent Verification & Trusted Computing Base (TCB) + +To prevent self-referential confirmation bias (systems verifying their own uninspected +outputs), VSTD-1 defines **independent-verification role separation** as a conformance +requirement for claims labeled independent: + +```text +Target System (Producer) + ↓ (Generates derivation / CNF / artifacts) +Independent VSTD-Conformant Auditor + ↓ (Runs separately implemented DPLL solver + DAG grounding checker in isolated TCB) +Structured VFY Receipt +``` + +Independence in this profile is a claim about distinct actors occupying the producer and +checker roles. Two executions that return the same result do not prove that separate +actors performed them; nor do two processes or machines. Those are artifact and runtime +observations. Actor independence requires separately bound evidence, and it never +strengthens the checked result merely because an actor is identified or trusted. + +### Trusted Computing Base Invariant +An auditor described as independent must: +1. Share zero solver state or runtime logic with the producer. +2. Rely exclusively on a minimal, inspectable codebase (e.g. Python standard library). +3. Explicitly declare its TCB components in every generated receipt. + +Running the bundled reference implementation does not by itself establish +actor, implementation, or runtime independence. A receipt MUST state the actual +separation achieved. If distinct actors are not evidenced, actor independence is +`NOT_DEMONSTRATED` even when two results match. If producer and auditor share relevant +logic or state, the result is still inspectable but MUST NOT be labeled independent on +that seam. + +Serialized `EVIDENCED` status words and evidence-reference strings are declarations, not +validated bindings. A runtime MUST derive independent verification only after an +implemented validator resolves the referenced evidence, binds it to the producer and +checker executions, and establishes distinct actors plus the claimed implementation and +runtime seams. The VSTD 1.2.0 reference runtime implements no such adapter; it therefore +treats externally supplied assertions as no stronger than `DECLARED`, rejects receipts +that serialize them as `EVIDENCED`, and never emits `EVIDENCED`. + +--- + +## 6. Reproducibility Taxonomy + +VSTD-1 defines a five-state reproduction-fidelity taxonomy. The public +`ReproducibilityLevel` name is a compatibility identifier; it does not denote a numbered +VSTD profile or assurance strength: + +1. `BITWISE_IDENTICAL`: Byte-for-byte exact match across all generated files, logs, and artifacts. +2. `CONTENT_IDENTICAL`: Canonical JSON representation of stable verification payload matches exactly, ignoring volatile execution fields (timestamps, elapsed wall-clock ms, hostnames). +3. `EVIDENCE_EQUIVALENT`: All checks, proofs, SAT assignments, and invariant bounds evaluate to the same truth values and proof certificates, though internal trace order or solver step counts may differ. +4. `RESULT_EQUIVALENT`: Summary verification verdict (`VERIFIED`/`FALSIFIED`) and primary metrics agree within declared tolerance bounds. +5. `SEMANTIC_REPRODUCTION`: The underlying formal proposition is sustained under a separately implemented translation or alternate solver. This does not establish distinct actors. + +--- + +## 7. Canonical Receipt Specification & Hashing + +A VSTD-1 receipt separates **stable verification content** from **volatile execution metadata**. +The receipt kind is explicit: + +``` +receipt.json +├── schema_version: "VSTD-1" +├── receipt_kind: "claim_mechanics" +├── receipt_id: "VFY-XXXXXX" +├── canonical_digest: SHA256(canonical_json(stable_payload)) +├── claim: {...} +├── evidence: {...} +├── target_result: {...} +├── independent_audit: {...} +├── provenance: {...} +├── reproducibility: {...} +└── execution_metadata: (volatile: timestamps, elapsed_ms, logs) +``` + +### Canonicalization Algorithm +1. Extract stable fields (`schema_version`, `receipt_kind`, `receipt_id`, `claim`, `evidence`, `target_result`, `independent_audit`, `provenance_stable`, `reproducibility`). +2. Serialize the VSTD-1 JSON subset with alphabetically sorted object keys, compact + separators `","` and `":"`, UTF-8 encoding, and no non-finite numbers. This + project-specific canonicalization is deterministic for the supported value subset; + VSTD-1 does not claim full RFC 8785 conformance. +3. Compute `SHA-256` digest over the serialized bytes. +4. The digest remains invariant across directory moves, path changes, and reformatting of human-readable reports. + +--- + +## 8. Challenge & Correction Model + +1. Any party may submit a counterexample, failing test, or ungrounded leaf finding. +2. A validator or reproducer returns failure when the bound content or declared rerun + does not match. It does not silently mutate a historical receipt. +3. The maintainer or integrating system must publish an additive `FALSIFIED`, + `INDETERMINATE`, or challenged record, preserving the affected receipt's provenance. + +--- + +## 9. Implementation Roadmap & Extensibility + +- **Currently Implemented Reference Subset**: Minimal propositional DPLL entailment, + derivation-graph acyclicity and grounding checks, Git/runtime provenance capture, + stable-payload digest validation, generic command receipts, and bounded + reproducibility comparison. +- **VSTD-2 — Verification Surface**: verification geometry, residual-driven deconstruction, horizons, valences, and bounded self-closure. Its results remain separate from VSTD-1 claim-mechanics results. +- **Unassigned Future Work**: Additional proof mechanisms, execution-environment binding, and cross-institutional proof-carrying software gates require separate scoped proposals and evidence. No future version number is reserved here. diff --git a/src/verifier/specifications/VSTD-2.md b/src/verifier/specifications/VSTD-2.md new file mode 100644 index 0000000..ebb0fb0 --- /dev/null +++ b/src/verifier/specifications/VSTD-2.md @@ -0,0 +1,363 @@ +# Verifier Standard (VSTD)-2 — Verification Surface + +> **Acronyms:** abstract syntax tree (AST); continuous delivery or deployment (CD); continuous integration (CI); +> intermediate representation (IR); trusted computing base (TCB). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-2 on the object axis; required closure coordinate: Verification Surface (see `LADDER.md`) +**Receipt serialization:** `schema_version = "VSTD-2"`; see `WIRE_IDENTIFIERS.md` +**Status:** experimental project specification with an implemented vertical slice +**Maintainer:** TimeLordRaps +**Date:** 2026-08-20 + +--- + +## 1. Relationship to adjacent profiles + +VSTD-2 adds a verification-geometry ontology beside VSTD-1 claim mechanics and +VSTD-Graph collection dynamics. A document conforms to this extension only when it +declares `schema_version = "VSTD-2"`; a result on one coordinate does not supply a +result on either adjacent surface. + +VSTD-1 answers how a bounded claim carries evidence, provenance, a checker judgment, +an explicitly evidenced independence basis, and reproducibility information. VSTD-Graph-1 answers how artifacts and +transformations compose into a provenance hypergraph. VSTD-2 answers a different +question: **what geometry was selected for verification, what did reconstruction +expose that the geometry missed, and has the sufficiency of the declared closure +itself been verified?** + +The normative typed slice is implemented by: + +- `verifier.core.geometry`; +- `receipts/schema/vstd2_receipt.json`; and +- `tests/test_verification_geometry.py`. + +--- + +## 2. Epistemic law + +VSTD MUST NOT claim more than the declared verification surface and actual evidence +establish. + +Assumptions MUST NOT manufacture closure. Unknownness, unsupported structure, +missing evidence, unresolved translation, an unverified mechanism, and an unverified +root are information. They MUST remain explicit states, residuals, valences, or +horizons. + +A declared trust root is a boundary, not evidence that the root is true. When a +derivation stops at such a boundary, the geometry MUST record a `TRUST_ROOT` horizon +and MUST NOT claim self-closure. + +--- + +## 3. Verification geometry + +### 3.1 Subject and locus + +A **subject** is the overall entity under consideration. A subject may itself become +an addressable entity inside a larger subject. + +A **locus** is a scale-independent, addressable place or entity to which verification +can attach. A locus may recursively contain other loci. Repositories, functions, AST +nodes, instructions, dataset rows, models, processes, interfaces, and dependency +relations are all possible loci. + +`LOCUS` answers **where or what**. + +### 3.2 Facet + +A **facet** is a dimension of assurance applicable to a locus, such as functional or +semantic correctness, termination, determinism, integrity, provenance, +reproducibility, translation fidelity, performance, or security. + +`FACET` answers **in what respect**. + +A facet is not a constituent part of a subject. New facets remain expressible through +stable identifiers rather than a permanently closed enumeration. + +### 3.3 Region, grain, and stratum + +A **region** is a meaningful collection of loci considered together, whether or not +they are syntactically contiguous. The implemented slice represents a region through +a named surface selection; a separate region object is deferred until distinct region +semantics are demonstrated. + +**Grain** is the resolution at which a subject is decomposed: repository, module, +function, statement, instruction, row, checkpoint, or another declared resolution. + +**Stratum** is the representation layer: requirement, source, AST, IR, assembly, +execution, output, or verification. + +Grain and stratum are orthogonal. Two loci may have function grain while one belongs +to source stratum and another to execution stratum. + +### 3.4 Seam + +A **seam** is an interface, transition, dependency, or translation boundary between +loci. A seam records its source locus, target locus, and relation. A seam can be made +a locus when assurance must attach to the seam itself. + +### 3.5 Coordinate and surface + +A **coordinate** is a locus-facet pair: + +`coordinate = locus x facet` + +A verification claim attaches to a coordinate or an explicitly represented relation +among coordinates. + +A **verification surface** is the declared set of coordinates and relevant seams for +which verification status is claimed. For subject `S`, loci `L`, and facets `F`: + +`surface(S) = (C_selected, E_selected)` + +where `C_selected` is a finite subset of `L x F` and `E_selected` is the finite set of +relevant seams. Coordinates not selected by the surface do not inherit its verdict. + +### 3.6 Horizon + +A **horizon** is a localized point at which the current verification derivation cannot +proceed because evidence, representation, mechanism, grain, ontology, or a root ends. +A horizon proves nothing beyond itself. It records the limit without converting the +limit into an assumption. + +--- + +## 4. Decomposition, reconstruction, and deconstruction + +**Decomposition** resolves or partitions a subject into loci at a declared grain. It +asks: *what parts can be exposed?* + +**Reconstruction** generates, reproduces, simulates, or predicts a subject or its +relevant behavior from the represented geometry. It asks: *is this representation +sufficient to regenerate what mattered?* + +**Deconstruction** is the iterative inference of a reconstructible verification +geometry. It combines decomposition, reverse engineering, reconstruction pressure, +residual analysis, and ontology refinement: + +```text +SUBJECT --deconstruct--> GEOMETRY + ^ | + | | + +----reconstruct--------+ +``` + +Deconstruction may recurse over the subject by exposing finer loci. It may also +recurse over the ontology when a residual cannot be expressed by the current +verification language. Neither recursion licenses invented structure. + +Zero residual is not itself a valid objective. A residual eliminated by enlarging an +unverified TCB, deleting unsupported semantics, overfitting a reconstruction, or +adding an assumption remains epistemically unresolved. Every material residual MUST +instead be resolved, localized, represented, or terminated at a horizon. + +--- + +## 5. Residuals and novelty + +### 5.1 Residual taxonomy + +A **residual** is an evidenced difference between observation and the current +verification geometry or reconstruction. + +- `STRUCTURAL`: observed structure absent from the locus/dependency geometry. +- `BEHAVIORAL`: observed behavior differs from reconstructed or predicted behavior. +- `SEMANTIC`: source meaning differs from meaning established by its formalization. +- `ONTOLOGICAL`: the current verification ontology cannot adequately classify the + observed phenomenon. + +A residual has a disposition: + +- `OPEN`: discovered but not yet adequately localized; +- `LOCALIZED`: bound to a locus, coordinate, or seam but not discharged; +- `RESOLVED`: discharged by represented evidence and refinement; or +- `HORIZON`: localized at an explicit boundary beyond which derivation cannot proceed. + +An assumption is not a residual disposition. + +### 5.2 Novelty + +**Novelty** is residual structure that cannot be discharged using the currently +declared geometry or mechanism vocabulary. A novelty claim MUST cite its grounding +residual and classify the insufficiency as grain, locus, facet, seam, stratum, +mechanism, or ontological novelty. Surprise alone is not novelty. + +--- + +## 6. Closure, valence, and self-closure + +### 6.1 Ordinary bounded closure + +Ordinary closure asks whether all obligations selected by the declared surface have +been discharged. The implemented vertical slice permits **bounded closure up to an +explicit horizon** when: + +1. every selected coordinate has a `VERIFIED` judgment backed by evidence and an + identified mechanism; and +2. every material residual is `RESOLVED` or explicitly terminated at a `HORIZON`. + +This form of closure is never evidence about what lies beyond a horizon. + +### 6.2 Verification valence + +**Verification valence** is an open relational or evidentiary capacity licensed by +the existing geometry. A valence identifies its source, the relation or evidence slot +that the geometry implies, and whether that slot is `OPEN`, `DISCHARGED`, or terminated +at a `HORIZON`. + +Valence describes the shape of an unresolved obligation. It does not invent the +entity or evidence that would satisfy it. + +### 6.3 Self-closure + +**Self-closure is closure that recursively verifies the sufficiency of its own +declared closure conditions and exposes remaining verification valence rather than +assuming it away.** + +Self-closure requires: + +1. structurally valid verification geometry; +2. ordinary bounded closure; +3. every material residual `RESOLVED`, not merely stopped at a horizon; +4. every verification valence `DISCHARGED` by evidence; +5. every material verification mechanism post-verified by identified evidence; +6. no unresolved evidence, mechanism, ontology, grain, representation, or trust-root + horizon; and +7. a finite, contiguous sequence of adjacent verification orders. + +If any condition fails, the geometry MUST refuse self-closure and enumerate the +blockers. + +### 6.4 Higher verification orders + +Higher-order verification is represented as a finite sequence: + +- `V0`: verification of the primary subject; +- `V1`: verification of V0's geometry, evidence, mechanisms, and selected surface; +- `V2`: verification of V1's sufficiency criteria; and so on only when evidenced. + +Each order greater than zero MUST verify exactly the preceding order. Skipped orders +violate the verification-order adjacency invariant. A finite document never claims that simply +adding one more self-description would close the sequence; inability to justify the +next order is a horizon or open valence. + +--- + +## 7. Lifecycle vocabulary + +- `PRE_VERIFIED`: the coordinate or surface exists before an applicable verification + has had the opportunity to establish a result. It is not a passing status. +- `VERIFIED`: a bounded coordinate passed an applicable mechanism with bound evidence, + declared limitations, freshness, and non-expansion. +- `POST_VERIFIED`: a passing result is bound to a frozen, content-identified snapshot + of the subject, evidence, mechanism state, and relevant environment. +- `GEOMETRY_INSPECTABLE`: the declared situation has an inspectable geometry that + represents covered, unsupported, indeterminate, and horizon-bounded coordinates + honestly. This vocabulary is prose-only: it is not a serialized receipt value, and it is not a + member of the `CoordinateStatus` enumeration serialized in a VSTD-2 receipt. +- `COMPLETELY_VERIFIED`: the declared closed surface satisfies self-closure. It never + means universal truth, unbounded safety, or permanent validity. + +Systems SHOULD minimize pre-verified surface area and dwell time. Post-verified +snapshots are useful compositional checkpoints, but continuous verification is +preferred: material changes invalidate dependent judgments and create new +pre-verified coordinates until checks pass again. + +--- + +## 8. Verifying processes and the common verification language + +A **verifying process** has an attached self-verification pipeline that observes its +operation, translates relevant facts into the common verification geometry, applies +mechanisms, and emits evidence about both the process and the pipeline. + +Self-observation is not self-certification. A pipeline that does not represent its own +mechanisms, dependencies, translation limits, and horizons is only +verification-instrumented. + +The common **verification language** is the typed graph of subjects, loci, facets, +coordinates, seams, surfaces, judgments, mechanisms, residuals, horizons, valences, +and adjacent verification orders. It is not an intermediate programming language for +every CI/CD system. Native workflows translate observable verification events through +thin adapters into this graph: + +```text +native process -> adjacent adapter -> verification geometry -> verifier +``` + +The adapter and verifier become loci in the next adjacent verification order. This +keeps verification orders adjacent and finite instead of recursing into infinite +workflow abstraction. + +The language is self-describing only in the bounded sense that its schema, adapter, +validator, and closure criteria can themselves become subjects. Their description is +not evidence of their correctness. + +### 8.1 Profiles and profiler adapters + +A **geometry profile** is a named, reusable constraint on how this geometry is applied; +it is not a new verdict, numbered VSTD profile, assurance score, or substitute for a +verification mechanism. A geometry profile +may identify its subject and grain, expected loci and facets, selected surface and +exclusions, native observation sources, adapter and mapping identities, applicable +mechanisms, evidence requirements, bounds, trust roots, horizons, and falsification or +conformance conditions. + +A native profiler or domain tool remains an observation source. Its output enters a +VSTD-2 surface only through an adjacent adapter that attributes the translated values to +exact coordinates and exposes omissions, transformations, and information loss. A native +status word does not transfer into a VSTD judgment without the identified assessment that +earns that judgment. + +Geometry profiles are linked only through explicit shared coordinates, seams, mappings, and +evidence-bearing transformations. Naming two profiles together, applying them to the same +subject, or repeating their observations does not compose their verdicts. A composite +geometry profile must declare and assess the cross-profile seams; unresolved mappings and conflicts +remain horizons or open valences. + +The `VSTD-2` receipt does not currently carry a geometry-profile identifier or a +geometry-profile-composition object. This section defines the conceptual relationship only. +A geometry-profile document can bind +an exact VSTD-2 surface and receipt externally; a new wire representation requires an +explicit versioned profile boundary. + +--- + +## 9. Reprogramming compatibility + +VSTD-2 reserves no universal transformation engine. It remains compatible with the +following future pattern: + +```text +SUBJECT S0 + -> deconstruct to GEOMETRY G0 + -> transform selected verified coordinates into G1 + -> reconstruct SUBJECT S1 + -> verify the transformation and resulting behavior +``` + +**Reprogramming** is a verified transformation of selected coordinates in a +deconstructed representation followed by reconstruction into a modified subject. +Any future implementation MUST receipt the selection, transformation, reconstruction, +residuals, and resulting verification without silently transferring S0 judgments to +S1. + +--- + +## 10. Conformance and present limits + +A VSTD-2 geometry document conforms to the implemented vertical slice when: + +1. it validates against `vstd2_receipt.json`; +2. `validate_geometry` returns no errors; +3. every `VERIFIED` judgment cites evidence and a known mechanism; +4. references and containment are internally consistent; +5. reconstruction residuals are typed and localized; +6. verification orders obey the verification-order adjacency invariant; and +7. closure is reported by `assess_closure` without suppressing its blockers. + +The current slice does not infer loci automatically, prove ontology completeness, +translate arbitrary CI/CD workflow languages, or certify its own Python runtime. Those +are explicit present limits, not assumed capabilities. diff --git a/src/verifier/specifications/VSTD-3.md b/src/verifier/specifications/VSTD-3.md index cd15da2..ecbe454 100644 --- a/src/verifier/specifications/VSTD-3.md +++ b/src/verifier/specifications/VSTD-3.md @@ -1,7 +1,20 @@ -# VSTD-3 — Substrate Accountability - -**Layer:** 3 of 5 on the object axis (see `LADDER.md`) -**Receipt wire format:** `schema_version = "VSTD-3.0"` — frozen; see `WIRE_IDENTIFIERS.md` +# Verifier Standard (VSTD)-3 — Substrate Accountability + +> **Acronyms:** Advanced Micro Devices (AMD); Amazon Web Services (AWS); Compute Unified Device Architecture (CUDA); +> Device Identifier Composition Engine (DICE); DMTF standards organization (DMTF); DICE Protection Environment (DPE); +> Entity Attestation Token (EAT); floating-point operation (FLOP); hash-based message authentication code (HMAC); +> integrated development environment (IDE); Internet Engineering Task Force (IETF); +> International Organization for Standardization (ISO); JavaScript Object Notation (JSON); +> NVIDIA Management Library (NVML); Peripheral Component Interconnect (PCI); PCI Special Interest Group (PCI-SIG); +> Remote Attestation Procedures (RATS); Reference Integrity Manifest (RIM); software development kit (SDK); +> Secure Hash Algorithm 256-bit (SHA-256); system management interface (SMI); Security Protocol and Data Model (SPDM); +> Trusted Device Interface Security Protocol (TDISP); tensor processing unit (TPU); Coordinated Universal Time (UTC); +> Unicode Transformation Format, 8-bit (UTF-8); World Wide Web Consortium (W3C). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-3 on the object axis; required closure coordinate: Substrate Accountability (see `LADDER.md`) +**Receipt serialization:** `schema_version = "VSTD-3.0"` — frozen; see `WIRE_IDENTIFIERS.md` **Status:** implemented project specification **Editor:** TimeLordRaps **License:** Apache-2.0 @@ -126,7 +139,7 @@ The reference implication graph is explicit. In particular: - fleet-boundary attestation does not imply physical-world completeness. `VERIFIED` flags inside a receipt are not self-authenticating. A verifier MUST -independently reproduce signature and continuity checks before using those flags to +recompute signature and continuity checks from the bound evidence before using those flags to accept a strong `PASS`. ## 7. Incremental conformance profiles @@ -350,7 +363,7 @@ hardware or firmware evidence therefore reaches downstream artifacts through the existing blast-radius algorithm. VSTD-3 does not create a second lineage graph. Composition is transactional and refuses receipts whose recorded `PASS` claims cannot -be independently reproduced. +be recomputed from the bound evidence. ## 20. Verification algorithm @@ -361,12 +374,12 @@ A verifier MUST, in order: 3. verify identifiers and all references; 4. verify raw evidence byte digests; 5. validate challenge freshness, nonce uniqueness, subject, and certificate binding; -6. independently verify implemented attestation and provider signatures; +6. verify implemented attestation and provider signatures against configured trust material; 7. validate topology and partition lineage; 8. bind starts, observations, accounting, ends, and workload identity to events; 9. verify event continuity, resets, and anchors; 10. verify the exact fleet boundary when present; -11. recompute every recorded passing claim from independently accepted evidence; +11. recompute every recorded passing claim from mechanism-verified evidence; 12. reject any stronger recorded `PASS`. Receipt digest integrity alone completes only steps 1–2. @@ -388,8 +401,8 @@ trust anchors. Merely labeling bytes `SPDM`, `EAT`, or `DICE` is not verificatio ## 23. Compatibility VSTD-3 adds record and enum values. It does not reinterpret VSTD-1, VSTD-Graph-1, -VSTD-2, or their historical wire identifiers. Existing readers remain valid -for their versioned surfaces. VSTD-3 hardware nodes use additive artifact and +VSTD-2, or their current serialized receipt identifiers. Each reader remains bounded to its +versioned surface. VSTD-3 hardware nodes use additive artifact and transformation enum values in the existing hypergraph. ## 24. Falsification conditions @@ -408,5 +421,5 @@ VSTD-3 conformance is falsified for a claimed surface if any of these occurs: - global absence of undeclared compute is derived from an ordinary receipt. Implementation limitations and the complete threat model are in -`../docs/layers/vstd-3/threat-model.md`; vendor requirements are in -`../docs/layers/vstd-3/vendor-integration.md`. +`../docs/profiles/vstd-3/threat-model.md`; vendor requirements are in +`../docs/profiles/vstd-3/vendor-integration.md`. diff --git a/src/verifier/specifications/VSTD-4.md b/src/verifier/specifications/VSTD-4.md index dd4def4..bdec9af 100644 --- a/src/verifier/specifications/VSTD-4.md +++ b/src/verifier/specifications/VSTD-4.md @@ -1,52 +1,63 @@ -# VSTD-4 — Refutability +# Verifier Standard (VSTD)-4 — Refutability -**Layer:** 4 of 5 on the object axis (see `LADDER.md`) +> **Acronyms:** application programming interface (API); conjunctive normal form (CNF); grounded decision certificate (GDC); JavaScript Object Notation (JSON); +> resolution asymmetric tautology (RAT); Boolean satisfiability problem (SAT); +> Unicode Transformation Format, 8-bit (UTF-8). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-4 on the object axis; required closure coordinate: Refutability (see `LADDER.md`) **Certificate format:** `VSTD4-GDC-1` -**Status:** implemented project specification +**Status:** project specification; candidate computation implemented; evidence binding and conformance not implemented **Editor:** TimeLordRaps **License:** Apache-2.0 **Date:** 2026-08-22 -VSTD-4 defines **adversarially portable checkability**. A verdict reaches this -layer only when its exact meaning, evidence, failure conditions, and checking -procedure can leave the declarant and survive hostile independent inspection. +VSTD-4 defines **adversarially portable checkability**. A verdict satisfies this +profile only when its exact meaning, evidence, failure conditions, and checking +procedure can leave the declarant and survive hostile inspection outside the declarant. -VSTD-4 establishes that independent checking is possible. It does not establish -that an independent party exists or has checked anything; that is VSTD-5. +VSTD-4 establishes that checking by an outside party is possible. It does not establish +that such a party exists or has checked anything; that is VSTD-5. > **No verdict without a portable certificate.** > **No portable certificate without an explicit falsifier.** --- -## 1. Conformance and lower-layer preconditions +## 1. Conformance and prerequisite-profile coordinates VSTD-4 conformance is incremental. A claim MUST conform to VSTD-1, VSTD-2, and VSTD-3 before it can conform to VSTD-4. A VSTD-4 certificate over an unaccountable substrate does not repair the missing VSTD-3 evidence. -The normative depth is computed: +The VSTD-4 normative depth is computed: ``` vstd4_depth(claim) = max { k : CNF_4k(claim) is satisfiable } ``` -An implementation MUST NOT accept a declarant-supplied depth as authoritative. -For a depth below 14, the `FAIL` certificate for rung `k+1` is the normative -explanation of the ceiling. Entry to any VSTD-5 procedure requires: +An implementation MUST NOT accept a declarant-supplied VSTD-4 normative depth as authoritative. +For a normative depth below 14, the `FAIL` certificate for rung `k+1` is the normative +explanation of the ceiling. Entry to any VSTD-5 procedure requires established +VSTD-4 conformance and: ``` vstd4_depth(claim) == 14 ``` -The reference implementation is `verifier.core.depth`. +The historical `verifier.core.depth.vstd4_depth` API computes only a structural +candidate over caller-supplied, nonempty rung references. It does not resolve those +references, validate their rung propositions, or check VSTD-1/2/3 preconditions. Its +result is therefore `CANDIDATE` with `conformance_status = NOT_ESTABLISHED`, including +at candidate depth 14, and the reference VSTD-5 entry gate rejects it. --- -## 2. The fourteen-rung ladder +## 2. The fourteen-rung sequence -Each rung depends on the evidence named below and on every lower-layer -precondition. Rung 4.14 depends on the complete ladder. +Each rung depends on the evidence named below and on every prerequisite-profile +precondition. Rung 4.14 depends on the complete sequence. | Rung | Requirement | Direct dependencies | |---|---|---| @@ -102,7 +113,7 @@ C = H(claim || coordinate || policy_root || evidence_root || verifier Canonical serialization MUST use sorted object keys, integer-valued numeric fields, no floating-point values, UTF-8, and no insignificant whitespace. A -checker MUST reject a certificate whose binding does not match the independently +checker MUST reject a certificate whose binding does not match the externally supplied `ClaimBinding`. ### 2.4 Portable verification @@ -160,7 +171,7 @@ IDENTIFIED < AVAILABLE < PORTABLE < SELF_CONTAINED A digest alone establishes only `IDENTIFIED`. VSTD-4 requires at least `AVAILABLE`, and the claim's bundle is capped by its weakest verdict-critical -artifact. A declared level that its retrieval and retention evidence cannot +artifact. A declared availability state that its retrieval and retention evidence cannot support MUST be rejected. A locator and retention declaration alone are not retrieval evidence. `AVAILABLE` @@ -168,7 +179,7 @@ requires a successful retrieval observation bound to the artifact identifier, de locator, observed bytes, observation time, and observer. The observed bytes MUST match the content address. `PORTABLE` additionally requires anonymous access and a declared retrieval procedure. A retrieval observation is scoped to its named trust root; it does -not by itself establish independent retrieval. +not by itself establish retrieval by a distinct actor. ### 2.9 Disclosure-safe checkability @@ -219,7 +230,7 @@ VALID -> CHALLENGED -> REVOKED A valid challenge mechanism that cannot change claim status is non-conforming. Synthetic challenges test structural challengeability at VSTD-4. Actual -independent action belongs to VSTD-5. +action by a distinct actor belongs to VSTD-5. ### 2.13 Monotonic degradation @@ -233,7 +244,8 @@ A `RefutabilityClosure` MUST bind input certificates, the transformation certificate, the output claim, and a total output-refutation mapping. A challenge to an output must localize to an input, the transformation, or the composition. -Output depth MUST NOT exceed the weakest required input or transformation depth. +Output VSTD-4 normative depth MUST NOT exceed the weakest required input or transformation +VSTD-4 normative depth. This closure is both the handoff to VSTD-Graph edge evidence and the entry gate to VSTD-5. @@ -300,7 +312,7 @@ accepted. ## 4. Normative invariants > A verdict MUST NOT be recorded at a strength exceeding the strength of the -> certificate an independent party could check without the declarant's +> certificate an outside party could check without the declarant's > cooperation. > Loss of certificate validity, accessibility, dependency validity, or @@ -330,7 +342,7 @@ bounded checking. ## 6. Reference implementation boundary -The reference producer and data structures are in: +The reference certificate producer, candidate-depth computation, and data structures are in: * `src/verifier/core/certificate.py` * `src/verifier/core/grounding.py` @@ -341,6 +353,11 @@ The reference producer and data structures are in: The trusted checker is `src/verifier/core/kernel.py`. Producer modules are not part of its trusted import boundary. +The kernel checks the supplied certificate, grounding, and `ClaimBinding` for internal +consistency. It does not retrieve rung references or establish the required prerequisite-profile +results. Kernel acceptance of a candidate certificate is therefore not VSTD-4 +conformance. + No external implementation, interoperability profile, or third-party attack has yet been demonstrated for `VSTD4-GDC-1`. This implementation status MUST remain visible in claims about the format. diff --git a/src/verifier/specifications/VSTD-5.md b/src/verifier/specifications/VSTD-5.md new file mode 100644 index 0000000..ddbbfcb --- /dev/null +++ b/src/verifier/specifications/VSTD-5.md @@ -0,0 +1,98 @@ +# Verifier Standard (VSTD)-5 — Witness Corroboration + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-5 on the object axis; required closure coordinate: Witness Corroboration (see `LADDER.md`) +**Status:** DRAFT — not implemented +**Editor:** TimeLordRaps +**License:** Apache-2.0 +**Date:** 2026-08-22 + +VSTD-5 binds a fully refutable claim to witnesses that do not share the +declarant's trust root. It is the first numbered profile that cannot be established by a +declarant acting alone. + +This document is a draft interface, not an implementation or a claim that any +independent witness exists. + +--- + +## 1. Entry gate + +Every VSTD-5 procedure MUST reject a claim unless VSTD-1/2/3 preconditions and all +VSTD-4 rung propositions have been evidence-bound and checked, establishing normative +VSTD-4 conformance at normative depth 14: + +``` +vstd4_depth(claim) == 14 +``` + +The gate is not satisfied by a structural candidate over caller-supplied references. +The current reference candidate reports `conformance_status = NOT_ESTABLISHED`, and +`require_vstd5_entry` rejects it even at candidate depth 14. A witness cannot +corroborate a claim whose refutability does not compose. + +--- + +## 2. Required record families + +A future conforming receipt will contain: + +* `WitnessIdentity` — the witness and the method used to bind the record to it; +* `IndependenceAssertion` — shared control, vendor, jurisdiction, funding, + infrastructure, and trust-root relationships; +* `CorroborationRecord` — what the witness independently checked, the VSTD-4 + certificate checked, observable results, time, and bounds; +* `CorroborationClass` — procurement, power/thermal envelope, network egress, + vendor telemetry, financial attestation, or physical inspection; and +* `DisagreementRecord` — conflicting observations and their effect on the claim. + +Independence fields MUST be evidence-bearing. A declarant's statement that a +witness is independent is not independence evidence. + +--- + +## 3. Independence + +At minimum, an independence assertion MUST name whether declarant and witness +share: + +1. ownership or operational control; +2. a verdict-producing codebase; +3. a verifier trust root; +4. an evidence source or telemetry provider; +5. infrastructure capable of changing the observed result; +6. financial dependence material to the corroboration; and +7. a jurisdiction or contractual relationship material to compulsion. + +`UNKNOWN` in any required independence dimension MUST cap the independence claim. + +> Claim independence MUST NOT exceed the independence of its weakest binding +> witness. + +Independence is not manufacturable from self-report at any cryptographic +strength. + +--- + +## 4. Corroboration and disagreement + +A corroboration record MUST bind the exact VSTD-4 commitment `C`, certificate +digest, checker descriptor, observable evidence, result, and observation time. +Checking a neighbouring claim or a different commitment is not corroboration. + +Witnesses are not votes. Conflicting witnesses MUST degrade the claim and create +an additive `DisagreementRecord`; their conclusions MUST NOT be averaged into an +apparently clean result. + +--- + +## 5. Draft boundary + +The schema `receipts/schema/vstd5_receipt.json` records the intended shape for +review. No reference witness transport, identity scheme, independence scoring +algorithm, or second-party implementation is shipped in release v1.0.0. + +The document remains `DRAFT` until VSTD-4 operating experience supplies evidence +for the final protocol. A draft schema MUST NOT be presented as VSTD-5 +conformance. diff --git a/src/verifier/specifications/VSTD-Graph-1.md b/src/verifier/specifications/VSTD-Graph-1.md new file mode 100644 index 0000000..06396d9 --- /dev/null +++ b/src/verifier/specifications/VSTD-Graph-1.md @@ -0,0 +1,172 @@ +# Verifier Standard (VSTD)-Graph-1 — Recorded Lineage + +> **Acronyms:** application programming interface (API); conjunctive normal form (CNF); Davis-Putnam-Logemann-Loveland (DPLL); operating system (OS); +> Boolean satisfiability problem (SAT); Secure Hash Algorithm 256-bit (SHA-256); satisfiability modulo theories (SMT); +> Software Package Data Exchange (SPDX); uniform resource identifier (URI). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-1; required closure coordinate: Recorded Lineage (see `LADDER.md`) +**Receipt serialization:** `schema_version = "VSTD-DATA-0.1"` — frozen; see `WIRE_IDENTIFIERS.md` +**Status:** Project Specification with Implemented Reference Subset +**Maintainer:** TimeLordRaps +**Date:** 2026-08-21 + +--- + +## 1. Purpose & Core Thesis + +> **Dataset and training provenance is the foundational substrate of computational verifiability: data sits directly upstream of training runs, checkpoints, fine-tuned adapters, evaluations, model behavior, downstream software products, licensing, and attribution.** + +`VSTD-Graph-1` establishes a content-addressed **Hypergraph Specification** for +capturing recorded and evidenced lineage of datasets, neural weights, and computational +outputs within a declared observation boundary. It does not infer unobserved history or +prove that the recorded graph is complete in the real world. Transformations are +first-class **N-ary Hyperedges**, which represent many-to-many merges, sharding, and +multi-input processing without flattening those relationships into ambiguous binary +links. + +This document defines the first numbered profile of the Graph axis. `VSTD-Graph-2.md` through +`VSTD-Graph-5.md` apply progressively stronger object and transformation-edge +requirements to the same closed collection. `LADDER.md` defines the computed candidate +Graph profile and its ceiling certificate; the compatibility API +`verifier.data.graph_level.graph_level` +implements that computation. + +--- + +## 2. The Provenance Hypergraph Abstraction + +A Dataset Provenance Hypergraph is a 6-tuple: +$$\mathcal{H} = (\mathcal{A}, \mathcal{T}, \mathcal{C}, \mathcal{R}, \mathcal{P}, \mathcal{X})$$ + +### 2.1 Artifact Nodes ($\mathcal{A}$) +Represents any discrete, inspectable data object or model state: +- `artifact_id`: Unique identifier (e.g. `art:sha256:...`). +- `artifact_type`: `RAW_SOURCE_FILE`, `CORPUS`, `SHARD`, `DATASET_SPLIT`, `TOKENIZED_CORPUS`, `CHECKPOINT`, `ADAPTER`, `MODEL`, `EVALUATION_REPORT`, `SUBMISSION_ARTIFACT`. +- **Content-Addressable Cryptographic Digests**: + - `content_digest`: a declared `SHA-256` over raw payload bytes. It becomes a verified + byte-identity statement only when a named mechanism actually hashes accessible bytes + and binds the observation as evidence. + - `metadata_digest`: a declared `SHA-256` over explicitly normalized metadata. + - `provenance_digest`: a declared `SHA-256` over an explicitly canonicalized ancestor + subgraph. +- `byte_size`, `record_count`, `mime_type`, `storage_uris`. +- `status`: `VALID`, `CHALLENGED`, `STALE`, `SUPERSEDED`, `REVOKED`, `UNKNOWN`. + +### 2.2 Transformation Hyperedges ($\mathcal{T}$) +Represents a declared N-ary transformation relationship consuming inputs and producing +outputs. The edge records ancestry; it does not by itself establish causal influence: +- `transformation_id`: Unique process identifier. +- `transformation_type`: `COLLECTION`, `EXTRACTION`, `FILTERING`, `DEDUPLICATION`, `NORMALIZATION`, `AUGMENTATION`, `SYNTHETIC_GENERATION`, `TOKENIZATION`, `TRAINING`, `FINE_TUNING`, `DISTILLATION`, `QUANTIZATION`, `EVALUATION`. +- `inputs`: List of input artifact references with role bindings (e.g. `TRAINING_SPLIT`, `BASE_WEIGHTS`, `CONFIG`). +- `outputs`: List of produced artifact references with role bindings (e.g. `CHECKPOINT_WEIGHTS`, `METRICS_LOG`). +- `software_provenance`: Git repository, commit SHA, branch, clean/dirty state, script path, execution command. +- `parameters`: Exact hyperparameter dictionary, filter criteria, or random seeds. +- `execution_environment`: Python runtime, host OS, hardware acceleration class, timestamp. + +### 2.3 Contributor Nodes ($\mathcal{C}$) +- `contributor_id`, `name`, `contributor_type` (`INDIVIDUAL`, `ORGANIZATION`, `MODEL_GENERATOR`, `AUTOMATED_SYSTEM`), `uri`. + +### 2.4 Rights & Licensing Nodes ($\mathcal{R}$) +- `rights_id`, `license_spdx` (e.g. `CC-BY-NC-4.0`, `MIT`, `Apache-2.0`), `commercial_allowed`, `attribution_required`. + +### 2.5 Policy & Formal Constraints ($\mathcal{P}$) +- Machine-checkable Boolean admission rules. The current reference subset evaluates + bounded CNF with its minimal DPLL implementation; general SMT is not implemented. + +### 2.6 Conflict Records ($\mathcal{X}$) +- `conflict_id`, `subject_id`, and `predicate` identify the disputed coordinate. +- `competing_values` retains at least two incompatible values. +- `evidence_refs` retains at least two evidence records rather than selecting a winner. + +A conflict record does not mutate the frozen artifact-status vocabulary. It makes the +subject inadmissible to a clean candidate Graph profile. The current reference implementation +has no conflict-resolution transition; later resolution must be additive and must retain the +competing evidence. + +--- + +## 3. Provenance Completeness Dimensions + +`VSTD-Graph-1` rejects treating a monolithic score as proof. The reference subset +reports six descriptive dimensions plus a disclosed weighted summary: + +$$\mathbf{C} = \langle C_{\text{src}}, C_{\text{trans}}, C_{\text{integ}}, C_{\text{lic}}, C_{\text{contrib}}, C_{\text{lineage}} \rangle$$ + +1. **Source-declaration coverage ($C_{\text{src}}$)**: Share of root artifacts with a + non-empty storage URI or `source_repository` declaration $[0.0, 1.0]$. +2. **Transformation-declaration coverage ($C_{\text{trans}}$)**: Share of hyperedges + with a recorded commit identifier or script path $[0.0, 1.0]$. +3. **Content-digest declaration coverage ($C_{\text{integ}}$)**: Share of artifacts with + a syntactically valid 64-hex-character digest $[0.0, 1.0]$. This metric does not by + itself show that the referenced physical bytes were rehashed. +4. **License-metadata coverage ($C_{\text{lic}}$)**: Share of root artifacts linked to + an explicit rights record $[0.0, 1.0]$. It is not a legal-validity score. +5. **Contributor Coverage ($C_{\text{contrib}}$)**: Share of artifacts attributed to identified agents $[0.0, 1.0]$. +6. **Downstream Lineage Depth ($C_{\text{lineage}}$)**: Integer topological depth from + root sources to reachable outputs. + +The current weighted summary is +`0.25*C_src + 0.25*C_trans + 0.25*C_integ + 0.15*C_lic + 0.10*C_contrib`. +It is a coverage summary, not a probability, trust score, or verification verdict. + +--- + +## 4. Epistemic Incompleteness & Fail-Closed Law + +* **The `UNKNOWN` Principle**: If an artifact's status is omitted, or its upstream + origin or transformation is not evidenced, the applicable state remains `UNKNOWN` or + the applicable coverage dimension remains incomplete. It never silently becomes + observed real-world truth. +* **The `CONFLICTED` Principle**: Incompatible retained evidence remains an explicit + conflict record. It is neither averaged nor collapsed into `UNKNOWN`, `VALID`, or a + scalar confidence value. +* **Fail-Closed Policy Admission**: A policy passes only the Boolean condition it + actually encodes. For example, "no ancestor is marked `REVOKED`" does not establish + that every ancestor is `VALID`; a clean-ancestor policy must explicitly require + `VALID` and reject `UNKNOWN`, `CHALLENGED`, `STALE`, and `SUPERSEDED`. + +--- + +## 5. Challenge & Revocation Blast Radius + +When an upstream source $S$ is marked `REVOKED` (e.g. due to copyright claim, data poisoning, or corruption): +1. The hypergraph query engine computes the forward reachability closure: + $$\text{BlastRadius}(S) = \{ a \in \mathcal{A} \mid S \rightsquigarrow a \}$$ +2. An integrating lifecycle controller can use that returned set to create additive + `CHALLENGED` or `REVOKED` records. The reference query does not silently mutate + historical artifact nodes. + +--- + +## 6. Threat Model & Explicit Non-Guarantees + +### What the implemented reference subset can establish +- **Receipt integrity**: Detects changes to stable fields bound by the receipt's + canonical digest. +- **Recorded graph structure**: Checks references, acyclicity, reachability, and the + declared coverage metrics of the stored hypergraph. +- **Declared lineage queries**: Computes ancestors, descendants, and forward blast + radius over recorded edges. +- **Bounded policy evaluation**: Evaluates the recorded CNF condition over its declared + graph-to-variable mapping. This does not prove that the mapping captured every + real-world fact. +- **Byte identity when separately observed**: A named adapter that rehashes accessible + bytes can establish whether those bytes match a recorded digest at that observation + time. Receipt validation alone does not access unbundled upstream files. + +### What `VSTD-Graph-1` Does NOT Guarantee +- **Real-World Ground Truth**: A hash proves byte identity; it does not prove the data is empirically accurate. +- **Legal Copyright Validity**: A declared SPDX license string records claimed provenance; it is not a judicial copyright ruling. +- **Authenticity of declarations**: A digest binds bytes or fields; it does not prove + that a claimed origin, contributor, execution, or license declaration is authentic. +- **Complete real-world lineage**: Missing instrumentation, hidden inputs, pre-observation + contamination, and out-of-band transformations remain outside the graph unless + separately evidenced. +- **Automatic physical-file checking**: A stored VSTD-Graph receipt validates its own + stable content. It flags a physical-file mismatch only when an adapter supplies and + rehashes that file. +- **Translation completeness**: SAT success establishes the encoded formula, not the + completeness or correctness of the translation from policy prose or the external + world into that formula. diff --git a/src/verifier/specifications/VSTD-Graph-2.md b/src/verifier/specifications/VSTD-Graph-2.md new file mode 100644 index 0000000..7778283 --- /dev/null +++ b/src/verifier/specifications/VSTD-Graph-2.md @@ -0,0 +1,20 @@ +# Verifier Standard (VSTD)-Graph-2 — Bounded Collection Surface + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-2; required closure coordinate: Bounded Collection Surface (see `LADDER.md`) +**Status:** implemented candidate computation; rating-evidence binding not implemented +**License:** Apache-2.0 + +VSTD-Graph-2 closes collection-scope leakage. A collection satisfies this candidate +profile only when every member and provenance ancestor is rated at object profile 2 or +higher, every reachable status is admissible, and every transformation hyperedge +carries profile-2 edge ratings. + +`verifier.data.graph_level` computes a candidate from caller-supplied ratings and marks +conformance `NOT_ESTABLISHED`. The `FAIL` certificate for Graph profile 2 names the member, +ancestor, status, or edge obligation that prevents admission under those inputs. It does +not validate the ratings themselves. + +VSTD-Graph-2 does not establish that the evidence sources behind the collection +are accountable. That is the blind spot closed by VSTD-Graph-3. diff --git a/src/verifier/specifications/VSTD-Graph-3.md b/src/verifier/specifications/VSTD-Graph-3.md new file mode 100644 index 0000000..4f8bc70 --- /dev/null +++ b/src/verifier/specifications/VSTD-Graph-3.md @@ -0,0 +1,23 @@ +# Verifier Standard (VSTD)-Graph-3 — Accountable Provenance Closure + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-3; required closure coordinate: Accountable Provenance Closure (see `LADDER.md`) +**Status:** implemented candidate computation; rating-evidence binding not implemented +**License:** Apache-2.0 + +VSTD-Graph-3 closes unaccountable substrate across a collection. A collection +satisfies this candidate profile only when every member and reachable ancestor is rated at +object profile 3 or higher, every reachable status is admissible, and every transformation +hyperedge carries profile-3 edge ratings. + +The provenance closure condition is normative: rating only the selected members +is insufficient. The weakest reachable ancestor or transformation caps the +collection. + +The reference computation consumes caller-supplied ratings and therefore reports a +candidate with conformance `NOT_ESTABLISHED`. Its certificate does not establish that a +VSTD-3 mechanism produced any supplied rating. + +VSTD-Graph-3 cannot establish that an outside party could refute the composed +collection. That blind spot is closed by VSTD-Graph-4. diff --git a/src/verifier/specifications/VSTD-Graph-4.md b/src/verifier/specifications/VSTD-Graph-4.md new file mode 100644 index 0000000..230e3f6 --- /dev/null +++ b/src/verifier/specifications/VSTD-Graph-4.md @@ -0,0 +1,22 @@ +# Verifier Standard (VSTD)-Graph-4 — Refutable Transformation Closure + +> **Acronym:** unsatisfiable (UNSAT). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-4; required closure coordinate: Refutable Transformation Closure (see `LADDER.md`) +**Status:** implemented candidate computation; rating-evidence binding not implemented +**License:** Apache-2.0 + +VSTD-Graph-4 closes non-compositional refutability. A collection satisfies this candidate +profile only when every member and reachable ancestor is rated at object profile 4 or +higher, statuses are admissible, and every transformation hyperedge carries +profile-4 ratings including a valid `RefutabilityClosure`. + +Two VSTD-4 nodes connected by an unevidenced edge do not make a VSTD-Graph-4 +collection. A challenge to the collection output must localize to a member, +ancestor, transformation, or the composition itself. + +The UNSAT certificate at the next profile is the computed explanation of the candidate +ceiling over caller-supplied ratings. It does not establish Graph-4 conformance or +validate the claimed `RefutabilityClosure` records. diff --git a/src/verifier/specifications/VSTD-Graph-5.md b/src/verifier/specifications/VSTD-Graph-5.md new file mode 100644 index 0000000..8552586 --- /dev/null +++ b/src/verifier/specifications/VSTD-Graph-5.md @@ -0,0 +1,21 @@ +# Verifier Standard (VSTD)-Graph-5 — Corroborated Verification Network + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-5; required closure coordinate: Corroborated Verification Network (see `LADDER.md`) +**Status:** DRAFT profile; computation is implemented, witness protocol is not +**License:** Apache-2.0 + +VSTD-Graph-5 is the collection profile for independently corroborated members, +ancestors, and transformations. The candidate-profile computation requires +object and edge ratings of at least 5, provenance closure, and admissible status +throughout. + +Because VSTD-5 is draft, the reference implementation can compute this profile +only over externally supplied profile-5 ratings; it does not manufacture or verify +their independence. A result based on self-declared ratings is not VSTD-Graph-5 +conformance. + +Conflicting witness records are retained as conflict records and make the relevant +subject inadmissible to a clean candidate Graph profile. They are never averaged into +a passing collection. diff --git a/src/verifier/specifications/WIRE_IDENTIFIERS.md b/src/verifier/specifications/WIRE_IDENTIFIERS.md index 736365f..da77128 100644 --- a/src/verifier/specifications/WIRE_IDENTIFIERS.md +++ b/src/verifier/specifications/WIRE_IDENTIFIERS.md @@ -1,106 +1,88 @@ -# VSTD frozen wire identifiers and historical filenames +# Verifier Standard (VSTD) serialized receipt identifiers -**Status:** normative for wire-identifier dispatch; filename history is informative -**Date:** 2026-08-22 +> **Acronyms:** command-line interface (CLI). -VSTD has no demonstrated external adoption or independent implementation as of this -release. This document therefore does not prescribe an adopter migration. It records -identifiers and filenames that appeared in the project's own public releases so that -those artifacts are not silently reinterpreted. +**Status:** normative for current serialized-receipt dispatch +**Date:** 2026-08-27 -Specification numbers now identify verification depth. Repository releases use -semantic versions independently. +A **serialized receipt identifier** is the value written into a receipt to select its exact reader and schema, principally `schema_version` plus any required profile discriminator. Standards literature often calls this a *wire identifier* or part of a *wire format*; here it means the stored JavaScript Object Notation (JSON) contract, not a network protocol. -## 1. Frozen receipt wire identifiers +Specification numbers identify numbered profiles and their cumulative closure coordinates. +Repository releases use semantic +versions independently. Retired partial-profile object identifiers and specification files are +not current profiles and are absent from this source tree; published tags and Git history +preserve those earlier project artifacts without making the current reader accept or +reinterpret them. -A filename or current layer label does not change the meaning of an issued receipt. -Readers MUST dispatch a receipt by its wire identifier: +## 1. Current serialized receipt dispatch -| Current layer document | Frozen wire identifier | +Readers MUST dispatch by the exact `schema_version` and any required profile +discriminator. Unknown identifiers, missing discriminators, and mismatched shapes fail +closed: + +| Numbered-profile document | Current serialized receipt identifier | |---|---| -| `VSTD-1.md` | `schema_version = "VSTD-0.1"` | -| `VSTD-2.md` | `schema_version = "VSTD-0.2"` | +| `VSTD-1.md` | `schema_version = "VSTD-1"` | +| `VSTD-2.md` | `schema_version = "VSTD-2"` | | `VSTD-3.md` | `schema_version = "VSTD-3.0"` | +| `VSTD-4.md` | `schema_version = "VSTD-4"` | +| `VSTD-5.md` | `schema_version = "VSTD-5-DRAFT"` | | `VSTD-Graph-1.md` | `schema_version = "VSTD-DATA-0.1"` | -New layer-4 and layer-5 documents use their own schemas without changing historical -canonical digests. +VSTD-1 has two current receipt profiles: + +| `receipt_kind` | Schema | Meaning | +|---|---|---| +| `claim_mechanics` | `vstd1_receipt.json` | bounded claim, evidence, checker, provenance, and reproducibility | +| `generic_computational_run` | `vstd1_generic_run_receipt.json` | planned execution, captured outputs, assessment context, and reproduction surface | + +Both discriminators are required. A reader MUST NOT guess the profile from incidental +field similarity. + +The generic-run `assessment_context` is a VSTD-1 container for mechanism identity, +declared resource bounds, prior commitment, and the refutation surface. It is not a +VSTD-4 object and carries no VSTD-4 conformance field. The container and its selected +fields participate in the canonical digest. ### 1.1 Non-wire vocabulary -`VSTD-2.md` section 7 defines a prose lifecycle vocabulary. Only the +`VSTD-2.md` section 7 defines prose lifecycle vocabulary. Only the `CoordinateStatus` members serialized in `receipts/schema/vstd2_receipt.json` (`PRE_VERIFIED`, `VERIFIED`, `FALSIFIED`, `INDETERMINATE`, `UNSUPPORTED`, `STALE`) -are wire values. `POST_VERIFIED`, `GEOMETRY_INSPECTABLE`, and `COMPLETELY_VERIFIED` -are descriptive terms only and have never appeared in an issued receipt; renaming -them does not affect any canonical digest. `GEOMETRY_INSPECTABLE` was named -`VERIFIABLE` in unreleased drafts before `v1.1.2`; a status token MUST NOT reuse the -maintainer's name. +are serialized receipt values. `POST_VERIFIED`, `GEOMETRY_INSPECTABLE`, and `COMPLETELY_VERIFIED` +are descriptive terms rather than receipt values. -## 2. Historical names in project releases +## 2. Stored artifact-control identifiers -| Historical public name | Current layer label | Meaning | -|---|---|---| -| `VSTD-0.1` | `VSTD-1` | claim mechanics | -| `VSTD-0.2` | `VSTD-2` | verification surface | -| `VSTD-3.0` | `VSTD-3` | substrate accountability | -| — | `VSTD-4` | refutability | -| — | `VSTD-5` | witness corroboration, draft | -| `VSTD-DATA-0.1` | `VSTD-Graph-1` | recorded lineage over collections | - -`VSTD-Graph-2` through `VSTD-Graph-5` first appeared under their current labels. - -The current repository does not duplicate old specification paths. Historical tags -remain the resolver for the bytes published under those paths: - -```text -standard/VSTD-0.1.md -> standard/VSTD-1.md -standard/VSTD-0.2.md -> standard/VSTD-2.md -standard/VSTD-3.0.md -> standard/VSTD-3.md -standard/VSTD-DATA-0.1.md -> standard/VSTD-Graph-1.md -VSTD3_THREAT_MODEL.md -> docs/layers/vstd-3/threat-model.md -VSTD3_VENDOR_INTEGRATION.md -> docs/layers/vstd-3/vendor-integration.md -VSTD3_REFERENCES.md -> docs/layers/vstd-3/references.md -VSTD3_MIGRATION.md -> docs/layers/vstd-3/compatibility.md -COMPETITION_EVALUATION_PROFILE.md -> docs/profiles/competition-evaluation.md -CLAIMS_AND_LIMITS.md -> docs/CLAIMS_AND_LIMITS.md -``` - -## 2.1 Import package and distribution rename - -From `v1.1.2` the import package is `verifier` and the distribution is -`verifier-standard`. Both -were previously `verifiable` / `verifiable-standard`. The rename removes a name that -collided with the ordinary-English adjective, with a former VSTD-2 status token, and -with the maintainer's former project name. - -| Historical name | Current name | Kind | -|---|---|---| -| `verifiable` | `verifier` | import package | -| `verifiable-standard` | `verifier-standard` | distribution | -| `verifiable-standard-.zip` | `verifier-standard-.zip` | release source archive | +Artifact-control mechanism objects are stored JSON contracts, not network traffic, VSTD +receipts, or new numbered profiles. They dispatch independently by: + +| Object | `schema_version` | +|---|---| +| Freeze manifest | `VSTD-ARTIFACT-FREEZE-1` | +| Self-closing seal envelope | `VSTD-ARTIFACT-SEAL-1` | +| Seal closure payload | `VSTD-ARTIFACT-SEAL-CLOSURE-1` | +| Thaw lineage sidecar | `VSTD-ARTIFACT-THAW-1` | -No receipt wire identifier, schema `$id`, or canonical digest changes. Specification -text that cites a reference module (for example `verifier.core.kernel`) is a pointer -into the reference implementation, not a wire value. +Their normative behavior is [`ARTIFACT_CONTROL.md`](ARTIFACT_CONTROL.md); their strict +combined schema is published as +[`artifact-control-1.schema.json`](https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json). +These identifiers do not imply a network protocol or VSTD conformance result. -Release manifests published up to and including `v1.1.1` bind -`verifiable-standard-.zip` in their `source.archive_prefix`. -`scripts/release_artifacts.py verify` derives the archive name from the manifest, so -those releases stay verifiable without republishing. +## 3. Import package and distribution -## 3. CLI compatibility +The distribution is `verifier-standard`, the import package is `verifier`, and +`vstd` is the canonical cross-platform CLI name. `verifier` may resolve to Windows Driver +Verifier on common Windows `PATH` configurations. The `verifiable` command remains a +compatibility alias for already-published execution instructions; it is not an import +package or a standard identifier. -`vstd` is the canonical cross-platform CLI name. The `verifier` alias remains -available, but Windows resolves the unqualified name to its built-in Driver Verifier -utility on common `PATH` configurations. `verifiable` also remains an alias because -project release materials and receipt instructions may bind that executable name. It is -a command name only: since `v1.1.2` it no longer corresponds to any import package. -Retaining either alias preserves project compatibility; it is not evidence of -external use. +Release verification derives archive names and console-script expectations from the +release manifest being checked. This preserves issued release evidence without carrying +obsolete standard identifiers into current receipt dispatch. ## 4. Release versioning -The first repository release using integer layer names is `v1.0.0`. The release -number does not claim VSTD-5 implementation: VSTD-5 is explicitly draft. Existing -`v0.1.0` and `v0.2.0` tags and GitHub releases remain untouched. +A repository release number does not claim conformance to a same-numbered VSTD profile. +VSTD-5 remains draft until its required witness mechanism and evidence binding are +implemented. diff --git a/standard/LADDER.md b/standard/LADDER.md index be23b3d..08fccf2 100644 --- a/standard/LADDER.md +++ b/standard/LADDER.md @@ -1,41 +1,219 @@ -# The VSTD Ladder — what the numbers mean +# The Verifier Standard (VSTD) verification complex — what the numbers mean + +> **Acronyms:** application programming interface (API); conjunctive normal form (CNF); Certificate Transparency (CT); +> deletion resolution asymmetric tautology (DRAT); grounded decision certificate (GDC); +> JavaScript Object Notation (JSON); National Institute of Standards and Technology (NIST); +> nondeterministic polynomial time (NP); proof-carrying code (PCC); +> World Wide Web Consortium provenance vocabulary (PROV); PROV data model (PROV-DM); Protect the Software (PS); +> Request for Comments (RFC); reverse unit propagation (RUP); Boolean satisfiability problem (SAT); +> Supply-chain Levels for Software Artifacts (SLSA); satisfiability modulo theories (SMT); +> SMT library standard (SMT-LIB); Secure Software Development Framework (SSDF); The Update Framework (TUF); +> unsatisfiable (UNSAT); World Wide Web Consortium (W3C). **Status:** project specification (normative for numbering and composition) **Editor:** TimeLordRaps **License:** Apache-2.0 -VSTD specification numbers are **layers of verification depth**, not revisions of a single -document. VSTD-3 does not supersede VSTD-1 any more than a floor supersedes its -foundation. +**Normative language:** The uppercase key words in this series are interpreted as +described by [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and +[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) only when they appear in all capitals; +lowercase uses are ordinary prose. + +**Reader context:** [`Concept guide and intellectual precedents`](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md) + +VSTD records separate answers to separate verification questions about an identified +claim, its evidence, the mechanism that checked it, and the bounds of that check. It does +not collapse those answers into one universal “verified†label or confidence score. The +questions, their evidence-bearing relations, and cumulative profiles over them form the +VSTD **verification complex**. + +### Read this first: the number is a checklist position, not a strength score + +A numbered profile is a cumulative checklist on one axis. `VSTD-3`, for example, means +that the required questions for `VSTD-1`, `VSTD-2`, and `VSTD-3` are each established by +their own applicable evidence. It does **not** mean “assurance strength 3,†software +version 3, or evidence that is three times stronger than `VSTD-1`. + +Here, **established** means that a named mechanism checked the exact required proposition +against bound evidence under declared limits. A field, document, or actor merely saying +that the proposition passed does not establish it. + +Consider one verification object with these separately recorded results: + +| Object-axis question | Current result | +|---|---| +| `VSTD-1` Claim Mechanics | established | +| `VSTD-2` Verification Surface | `UNKNOWN` | +| `VSTD-3` Substrate Accountability | established | + +Its **object profile depth is 1**. The cumulative checklist cannot skip the missing +`VSTD-2` result. The `VSTD-3` coordinate evidence remains recorded and useful, but it does +not fill the `VSTD-2` gap or make profile 3 satisfied. + +The Graph axis applies a different cumulative checklist to a collection of artifacts and +their recorded relations. `VSTD-3` and `VSTD-Graph-3` therefore ask different questions; +the shared number does not make them equivalent. + +In one sentence: a **closure coordinate** is one verification question, a **numbered +profile** is a cumulative checklist of those questions on one axis, and **profile depth** +is the largest uninterrupted prefix of that checklist that is established. + +### Terminology contract + +The rest of the Standard uses the following terms precisely: + +| Term | Plain meaning | Important boundary | +|---|---|---| +| **Closure coordinate** | One named verification question and its failure class, such as Claim Mechanics or Refutability. | Closure is scoped to that question. VSTD-2 surface closure, Graph provenance closure, refutability closure, and artifact-seal structural closure are different results. | +| **Numbered profile** | A cumulative checklist selected by `VSTD-N` or `VSTD-Graph-N`. Profile `N` requires its named coordinate and every earlier coordinate on the same axis. | A profile number is not a software revision, spatial layer, confidence score, or substitute for the underlying results. | +| **Profile axis** | One ordered family of cumulative checklists. VSTD has an object axis and a Graph axis. | Equal numbers on different axes do not identify equivalent or interchangeable results. | +| **Object profile depth** | For one verification object, start at `VSTD-1` and count upward only while every required coordinate remains established. The last uninterrupted number is its depth. | Depth is a compact summary of separately established results, not a new verdict, evidence-strength rating, or permission to ignore a later established coordinate after an earlier gap. | +| **Candidate Graph profile** | The greatest Graph checklist position satisfied by the current caller-supplied ratings. | The current calculation is `NOT_ESTABLISHED` because those ratings are not evidence-bound. It is not a verified Graph profile. | +| **VSTD-4 rung** | One of the fourteen ordered refutability obligations `4.1` through `4.14`. | “Rung†names only this internal sequence, never a top-level VSTD profile. | +| **Verification order** | One adjacent meta-verification order in the VSTD-2 geometry model. | The compatibility names `VerificationLayer` and `verification_layers` do not denote numbered VSTD profiles. | +| **Level** | A retained word in an explicitly named external taxonomy or compatibility identifier, including `ReproducibilityLevel`, `AvailabilityLevel`, `graph_level`, and serialized Graph `level` fields. | In Graph compatibility identifiers, the value is the candidate Graph profile number; “level†is not the governing name for a VSTD profile. | +| **Layer** | An implementation, protocol, or physical stack whose parts are ordered by containment. | It does not name `VSTD-N` or `VSTD-Graph-N`; historical paths such as `verifier.layer4` remain compatibility identifiers only. | +| **Tier** | A declared checker-cost class in `VSTD4-GDC-1`. | It is not a VSTD profile, evidence-strength rating, or actor rating. | + +“Profile†must also be qualified when confusion is possible: **numbered profile**, **receipt +profile**, **application profile**, or **geometry profile**. Likewise, “depth†must be +qualified as object profile depth, VSTD-4 normative or candidate depth, or lineage +topological depth. The retained `LADDER.md` filename is a stable document path, not the +governing topology; this document defines a verification complex. +Current public serialized identifiers, fields, class names, functions, and module paths +retain their exact compatibility spelling; adjacent prose supplies the precise meaning. +Profiles are therefore **requirement-set coordinates**, not spatial layers. A profile is +satisfied only when a named mechanism has bound evidence for every required fact; Boolean +SAT over caller-supplied assertions establishes only a candidate formula result. --- ## 1. The governing idea -Each layer names a distinct verification question and a distinct failure class. The -ordering is a composition rule, not logical entailment between layers. +Each closure coordinate names a distinct verification question and failure class. Profile +ordering is a composition rule, not logical entailment between coordinates. + +The nearest familiar security analogy is +[defense in depth](https://en.wikipedia.org/wiki/Defense_in_depth_%28computing%29 "Wikipedia orientation; primary references are mapped below"), +but the analogy is limited: VSTD closure coordinates are separately evidenced questions, not +interchangeable controls whose mere quantity establishes assurance. Decomposing assurance +into named components also has precedent in the Common Criteria, while VSTD deliberately +uses different coordinates, evidence rules, and conformance semantics. -**Evidence for one layer never supplies evidence for another layer.** In particular, -layer-4 evidence does not supply, imply, upgrade, or repair layer 3, 2, or 1. A reported -depth of `N` is only shorthand for `N` separately checked results, one for each layer -from 1 through `N`. +**Evidence for one closure coordinate never supplies evidence for another.** In particular, +Refutability evidence does not supply, imply, upgrade, or repair Substrate Accountability, +Verification Surface, or Claim Mechanics evidence. A reported object profile depth of `N` +is only shorthand for the separately checked results required by profiles 1 through `N`. -Reflection and metalanguage are useful design analogies for asking what a given +Reflection and [metalanguage](https://en.wikipedia.org/wiki/Metalogic "Wikipedia orientation; not a proof of the VSTD verification complex") +are useful design analogies for asking what a given verification surface leaves unexamined. VSTD does not claim that Tarski's -undefinability theorem proves this ladder, that adjacent layers form formal -metalanguages, or that a lower-layer implementation is logically incapable of -describing another layer's failure. The normative requirement is narrower: an +[undefinability theorem](https://en.wikipedia.org/wiki/Tarski%27s_undefinability_theorem "Wikipedia orientation; the theorem does not derive this verification complex") +proves these profile coordinates, that adjacent profiles form formal +metalanguages, or that an earlier-profile implementation is logically incapable of +describing another coordinate's failure. The normative requirement is narrower: an implementation MUST NOT treat success on one question as evidence for a different question. +### 1.1 Artifact-first causal provenance orientation + +VSTD evaluates bounded propositions about computational processes represented by +identified software, executions, evidence, and resulting artifacts. It does not evaluate +whether an actor is good, bad, reputable, or worthy of trust. Standing alone, an actor's +identity, popularity, repetition, or reputation MUST NOT strengthen an artifact-bound +result. A named mechanism MAY establish an exact attribution, authorization, or separation +proposition by checking the required identity evidence; that result remains an adjacent +proposition and MUST NOT promote an unrelated computational claim. + +**Zero identity** means zero identity-derived verdict weight, not anonymity or absence of +identifiers. **Zero knowledge** means zero unevidenced knowledge is presumed: without a +mechanism-earned result for the exact proposition, its state remains `UNKNOWN`. This +architectural zero-knowledge rule MAY be enclosed by cryptographic zero knowledge when a +witness must remain confidential. That enclosure MUST bind the exact software or program +coordinate, predicate, public commitments, output, proof parameters, and verification +mechanism while revealing no more witness information than its declared proof statement. +Cryptographic zero knowledge MUST be claimed only when a named proof system establishes +that property under explicit assumptions; a digest or undisclosed input alone is not such +a proof. The resulting support is bearer- and artifact-bound, never prover-identity-bound. +**Actor** and **artifact** remain contextual roles, not permanent entity classes: software +can be an artifact when created, versioned, or evaluated and an actor when it executes a +transformation. + +The capitalized terms **TRUST**, **RUST**, and **ROT** are formal VSTD semantic names, not +acronyms, serialized receipt values, actor ratings, scalar scores, or references to the Rust programming +language. The same bound development graph and its time-indexed lifecycle carry three +distinct relations: + +```text +development: ancestor artifact --TRUST through a checked transformation--> descendant +lifecycle: recorded TRUST --ROT under typed current-state evidence--> reassessment +diagnosis: descendant deviation --RUST memetic causal backtrace--> ancestor candidates +``` + +**Memetic propagation** is the transmission of claim and evidence state through recorded +developmental provenance. The genetic or viral language names this inheritance mechanic: +TRUST moves forward into descendant claim space; RUST moves backward toward recorded +ancestor states; ROT changes the current admissibility of previously recorded support. It +does not claim biological transmission or make identity and reputation sources of +assurance. + +**TRUST** is positive support earned when a named mechanism checks an exact +artifact-bound process obligation under declared evidence, specification, bounds, and +trust roots. It moves parent-to-child only across a declared creation or dependency edge +whose relevant transformation obligations pass. Applicable support composes by +intersection and is capped by the weakest required parent or edge; it is never added, +averaged, voted, or converted into actor standing. Every child MUST still discharge its +new predicates, transformations, boundaries, and evidence obligations. A declared trust +root is an explicit dependency and stopping boundary, not actor TRUST. + +**ROT** is typed, time-indexed degradation of the current admissibility of recorded TRUST. +It requires exact lifecycle or dependency evidence, such as expiry under a declared +freshness bound, `STALE`, `CHALLENGED`, `REVOKED`, `SUPERSEDED`, or an invalidated required +coordinate. Wall-clock passage, age, or popularity alone MUST NOT create ROT. ROT MUST NOT +rewrite an immutable historical receipt or imply that its historical result was false. It +may require reassessment of dependent descendants, but any resulting status change still +requires its named policy or mechanism. + +**RUST** is the inverse-TRUST diagnostic mechanic: a typed trace created by an observed +descendant deviation from a declared expectation. It moves child-to-parent only through +recorded admissible creation, input, or transformation paths. The inverse is directional +and diagnostic, not arithmetic: TRUST and RUST never cancel. Distinct comparable +backtraces may concentrate on a shared ancestor and prioritize it for falsification or +diagnostic examination. Transferred RUST establishes ancestral reachability, not direct +observation, falsehood, or causal responsibility; localization requires additional +intervention, ablation, independently bound execution evidence, or an equivalent declared +mechanism. + +The word *causal* is required here for recorded developmental and provenance causality: +the graph states which artifacts and transformations produced later claim architecture. +Propagation across those causal-provenance edges does not by itself establish +intervention-level physical causality, causal localization, responsibility, or guilt. + +TRUST, ROT, and RUST MUST remain separate. They do not cancel, form one scalar score, or +flow in the opposite direction as inherited truth, decay, or guilt. `UNKNOWN` and +`CONFLICTED` support or lineage MUST remain visible and MUST NOT become a clean signal. +This section fixes their semantic orientation and prohibited inferences; event formats, +TRUST-transfer algebra, ROT derivation and propagation, RUST concentration-independence, +and localization protocols each require their own specification and evidence. Until those +exist, the three terms are causal-provenance and lifecycle constraints, not serialized receipt values or +computable conformance results; no current VSTD runtime emits or validates a TRUST, ROT, or +RUST transfer. + +Artifact freezing and sealing are bounded mechanisms under this orientation, specified +separately in [`ARTIFACT_CONTROL.md`](ARTIFACT_CONTROL.md). A verified freeze preserves +and recomputes exact bytes; a verified seal earns structural closure for those bytes. It +does not earn semantic correctness, prevent ROT, localize RUST, create actor TRUST, or +supply any numbered profile. A sealed realm or temporal descriptor remains a bound input +until its own mapping, continuity, or transition verifier checks the exact proposition. + --- -## 2. The object ladder +## 2. The object profile axis VSTD proper governs the verification of **one object**. Call this verification *mechanics*. -| Layer | Name | Closes | Does not establish | +| Numbered profile | Required closure coordinate | Closes | Does not establish | |---|---|---|---| | **1** | Claim mechanics | A malformed or tampered statement | Whether the claim applies where it is being applied | | **2** | Verification surface | A verdict leaking beyond the coordinate actually verified | Whether the evidence behind it is real | @@ -43,18 +221,23 @@ VSTD proper governs the verification of **one object**. Call this verification | **4** | Refutability | A claim unfalsifiable in principle by any outside party | Whether the parties who could check are independent | | **5** | Witness corroboration | Pseudo-independence — witnesses sharing the declarant's trust root | — | -### 2.1 The self-discernability boundary +### 2.1 The single-declarant boundary + +**A single declarant can in principle produce the evidence required by profiles 1 through +4.** No second party is required merely to create those bounded inputs and mechanisms. -**Layers 1 through 4 are self-discernable.** A declarant can establish them alone, with -no second party in existence. +**Profile 5 is not.** It requires another party to exist, to act, and to be independent. -**Layer 5 is not.** It requires another party to exist, to act, and to be independent. +VSTD-1 records the claim-mechanics status of actor independence but cannot infer it from +two runs or matching artifacts. VSTD-5 requires the corroborating witness procedure that +uses such separately evidenced actor participation; recording a field is not witnessing. -That transition between 4 and 5 is the most important boundary in the ladder. Layer 4 -asks *could a stranger check this?* Layer 5 asks *did one, and were they actually a -stranger?* The first is a property of the claim. The second is a property of the world. +That transition between profiles 4 and 5 is the most important object-axis boundary. +Refutability asks *is a bounded outside check possible?* Witness Corroboration asks *was +one performed, and are the required separation seams evidence-bound?* The first is a +property of the claim surface. The second requires additional evidence about an execution. -An implementation MUST NOT report a layer-5 property on the basis of layer-4 evidence. +An implementation MUST NOT report a profile-5 property on the basis of Refutability evidence. Preparing to be checked is not being checked. --- @@ -65,9 +248,12 @@ VSTD-Graph governs the verification of a **collection** of objects. Call this verification *dynamics*. The two axes are parallel but coupled: a collection's dynamics are constrained by its -members' mechanics, and by the provenance edges between them. +members' mechanics, and by the +[provenance](https://en.wikipedia.org/wiki/Data_provenance "Wikipedia orientation; see W3C PROV-DM and supply-chain references below") +edges between them. The implemented N-ary representation is a +[hypergraph](https://en.wikipedia.org/wiki/Hypergraph "Wikipedia orientation; not a claim of complete real-world lineage"). -| Layer | Name | Collection-level closure | +| Numbered profile | Required closure coordinate | Collection proposition | |---|---|---| | **Graph-1** | Recorded lineage | members and transformations are represented | | **Graph-2** | Bounded collection surface | scope does not leak across the collection | @@ -75,27 +261,34 @@ members' mechanics, and by the provenance edges between them. | **Graph-4** | Refutable transformation closure | challenges compose across hyperedges | | **Graph-5** | Corroborated verification network | member and edge witnesses are independently corroborated | -A collection `C` holds at Graph layer `N` only if all four conditions hold: +A collection `C` satisfies candidate Graph profile `N` only if all four conditions hold: -1. **Membership floor** — every member is at object layer ≥ N. -2. **Provenance closure** — every ancestor reachable from any member is at layer ≥ N. -3. **Status admissibility** — no ancestor is `REVOKED`, `CHALLENGED`, `STALE`, or - `UNKNOWN`. -4. **Edge evidence** — the transformation hyperedges themselves carry layer-N evidence. +1. **Membership floor** — every member rating is at object profile ≥ N. +2. **Provenance closure** — every ancestor reachable from any member is rated at object + profile ≥ N. +3. **Status admissibility** — no ancestor is `REVOKED`, `CHALLENGED`, `STALE`, + `UNKNOWN`, or subject to an unresolved `CONFLICTED` record. +4. **Edge evidence** — the transformation hyperedges themselves carry profile-N ratings. Condition 2 is what a plain minimum over members misses. Condition 4 is what makes this dynamics rather than aggregation: **a graph is only as verified as its edges**, and an -unevidenced edge between two layer-5 artifacts does not yield a layer-5 collection. +unevidenced edge between two profile-5 artifacts does not yield a Graph-5 collection. -The level is **computed, never declared**: +The candidate Graph profile number is **computed from object and edge ratings, never +declared**: ``` -graph_level(C) = max { N : CNF_N(C) is satisfiable } +candidate_graph_profile(C) = max { N : CNF_N(C) is satisfiable } ``` -The reference implementation searches 5→1. At a result below 5, the grounded -`FAIL` certificate for `N+1` is the explanation of the ceiling. A level without -that certificate is a declaration and is non-conforming. +The compatibility API `graph_level` implements that function and the frozen Graph receipt +stores its number in a `level` field. The reference implementation searches 5→1 and +certifies its Boolean encoding. Its current rating inputs are caller-supplied, so it reports +a **candidate Graph profile** with +`conformance_status = NOT_ESTABLISHED`; the certificate proves the computation over +those inputs, not the validity of the ratings. At a result below 5, the grounded `FAIL` +certificate for profile `N+1` explains that candidate ceiling. Graph conformance additionally +requires evidence-bound ratings under the applicable object and edge profiles. --- @@ -108,17 +301,22 @@ the third is the load-bearing one. VSTD does not classify every receipt as an NP certificate. Specific bounded formats, including `VSTD4-GDC-1`, define a finite decision problem, a certificate language, and -an independent checker. Complexity claims apply only to such a defined formal problem. +a checker implemented separately from the producer path. Complexity claims apply only +to such a defined formal problem; checker separation alone does not establish distinct +actors. Other receipt fields may be signed declarations, hashes, measurements, or references whose meaning depends on explicitly named trust roots. The useful engineering asymmetry is concrete rather than universal: when a result can -carry a smaller independently checkable artifact instead of requiring the original +carry a smaller consumer-checkable artifact instead of requiring the original computation, VSTD preserves that artifact and its verification bounds. ### 4.2 Bounded admission uses CNF -The reference admission procedures encode finite, bounded policy questions as CNF. +The reference admission procedures encode finite, bounded policy questions as +[conjunctive normal form](https://en.wikipedia.org/wiki/Conjunctive_normal_form "Wikipedia orientation; the implemented format is finite CNF") +(CNF) for the +[Boolean satisfiability problem](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem "Wikipedia orientation; SAT success establishes only the encoded formula"). CNF is not identical to 3-SAT. A finite CNF satisfiability instance can be transformed in polynomial time into an equisatisfiable 3-CNF instance, using auxiliary variables where required. VSTD does not need that transformation for every checker and does not @@ -136,7 +334,7 @@ A future claim may cover a finite enumerated world if its observation boundary a completeness mechanism are declared and checked. It still MUST NOT be widened into a claim about unobserved physical activity. -This is why the ladder tops out at corroboration rather than proof of global absence. Layer 5 +This is why the object profile axis tops out at corroboration rather than proof of global absence. Profile 5 does not detect hidden work. It makes the *independence status* of declared work legible, and leaves the undeclared remainder named and quantified rather than silent. @@ -145,7 +343,7 @@ and leaves the undeclared remainder named and quantified rather than silent. ## 5. Certificates for refusals This section applies to the finite propositional decision procedures used by the -reference layer-4 implementation. +reference Refutability implementation. A satisfiable result already carries its certificate: the model. Anyone can evaluate it against the clause set without a solver. @@ -154,8 +352,12 @@ An unsatisfiable result, by default, carries nothing but the solver's word. For a fail-closed standard, **refusals are the most consequential output**. A standard whose passes are checkable and whose refusals are not has its assurance backwards. -Layer 4 therefore requires a refutation certificate — a clausal proof, verifiable by -reverse unit propagation, checkable without re-solving. +The Refutability coordinate therefore requires a refutation certificate — a clausal proof, verifiable by +[reverse unit propagation](https://en.wikipedia.org/wiki/Unit_propagation "Wikipedia orientation; VSTD implements a bounded RUP checker"), +checkable without re-solving. This follows the same producer-certificate/consumer-checker +engineering asymmetry as +[proof-carrying code](https://en.wikipedia.org/wiki/Proof-carrying_code "Wikipedia orientation; VSTD does not inherit PCC's safety theorem"), +while using a narrower certificate language. Resolution proofs have exponential lower bounds for some formula families. A conforming implementation therefore MUST declare a bound and MUST answer `UNKNOWN` @@ -165,46 +367,77 @@ An `UNKNOWN` is never a pass and never an unsatisfiability claim. Reference implementation: `verifier.core.refutation`. -### 5.1 The internal VSTD-4 ladder +### 5.1 The internal VSTD-4 rung sequence VSTD-4 contains fourteen ordered rungs, from decision certification through semantic binding, anti-equivocation, bounded portable checking, availability, -precommitment, challenge handling, degradation, and compositionality. Its depth +precommitment, challenge handling, degradation, and compositionality. Its normative depth is computed: ``` vstd4_depth(claim) = max { k : CNF_4k(claim) is satisfiable } ``` -The certificate for rung `k+1` explains a partial depth. Only depth 14 admits a -claim to any VSTD-5 procedure. See `VSTD-4.md` for the normative rung graph and -`VSTD4-GDC-1` format. +The certificate for rung `k+1` explains a partial VSTD-4 normative depth. Only established +VSTD-4 conformance at normative depth 14 admits a claim to any VSTD-5 procedure. The current +reference `vstd4_depth` function instead computes a structural candidate from +caller-supplied rung references, labels conformance `NOT_ESTABLISHED`, and never admits +VSTD-5. See `VSTD-4.md` for the normative rung graph and `VSTD4-GDC-1` format. --- -## 6. Composition — layers do not supply or substitute +## 6. Composition — closure coordinates do not supply or substitute -Layer results may be composed into a depth report. They do not replace, entail, or -supply one another. +Closure-coordinate results may be composed into an object profile-depth report. They do +not replace, entail, or supply one another. -- Layer 4 without layer 3 certifies a claim whose evidence source is unaccountable. -- Layer 5 without layer 4 solicits witnesses for a claim no witness could check. -- Layer 2 without layer 1 scopes a statement whose integrity is unestablished. +- Refutability without Substrate Accountability certifies a claim whose evidence source is unaccountable. +- Witness Corroboration without Refutability solicits witnesses for a claim no witness could check. +- Verification Surface without Claim Mechanics scopes a statement whose integrity is unestablished. -An implementation reporting aggregate depth *N* MUST present separately checkable -evidence for every layer from 1 through *N*. Conformance may also be reported for an -individual layer without claiming aggregate depth. Conformance profiles are declared -per layer, following VSTD-3 §7. +An implementation reporting object profile depth *N* MUST present separately checkable +evidence for every required coordinate in profiles 1 through *N*. Conformance may also be +reported for one coordinate without claiming cumulative profile depth. Incremental +Substrate Accountability profiles are declared in VSTD-3 §7. "Higher is more protected" is true only in the sense that more classes of failure are -closed. It never means the lower layers became unnecessary. +closed. It never means the prerequisite coordinates became unnecessary. --- ## 7. Numbering -- **Specification layers are integers**: VSTD-1 … VSTD-5, VSTD-Graph-1 … VSTD-Graph-5. -- **Repository releases use semantic versioning** and are independent of layer numbers. +- **Numbered profiles use integers**: VSTD-1 … VSTD-5, VSTD-Graph-1 … VSTD-Graph-5. +- **Repository releases use [semantic versioning](https://semver.org/)** and are independent + of profile numbers. -A release version never implies a layer, and a layer never implies a release. See -`WIRE_IDENTIFIERS.md` for frozen wire identifiers and the historical public filenames. +A release version never implies a numbered profile, and a numbered profile never implies a +release. See +`WIRE_IDENTIFIERS.md` for serialized receipt identifiers and historical public filenames. + +--- + +## 8. Intellectual lineage and adjacent precedents + +The verification complex is VSTD project architecture; no cited work proves that these five +coordinates on either axis are +necessary, sufficient, complete, or uniquely ordered. The references below show that its +individual design pressures have established precedents in security engineering, +provenance, reproducible systems, and proof checking. The +[`concept guide`](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md) provides definitions, additional +sources, and explicit non-equivalences. + +| VSTD pressure | Adjacent precedent | What the precedent contributes—and does not | +|---|---|---| +| Separate failure surfaces and fail-closed defaults | Saltzer and Schroeder, [*The Protection of Information in Computer Systems*](https://web.mit.edu/Saltzer/www/publications/pubs.html) | Classic principles include fail-safe defaults, complete mediation, separation of privilege, and least common mechanism. They motivate separation; they do not derive VSTD's coordinate count. | +| Named assurance components | Common Criteria, [Part 3: Security assurance components](https://www.commoncriteriaportal.org/files/ccfiles/CC2022PART3R1.pdf) | Demonstrates established componentized assurance and assurance packages. VSTD is not a Common Criteria evaluation or an Evaluation Assurance Level. | +| Stable cryptographic representations | [RFC 8785: JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html) | Shows why JSON used as cryptographic input needs invariant representation. VSTD formats retain their own declared canonicalization rules. | +| Recorded entities, activities, and agents | W3C [PROV-DM](https://www.w3.org/TR/prov-dm/) | Supplies an interoperable provenance model adjacent to the Graph axis. VSTD-Graph is not a PROV implementation and does not infer complete history. | +| Software materials, builders, steps, and products | [in-toto specification v1.0](https://in-toto.io/docs/specs/) and [SLSA v1.2](https://slsa.dev/spec/v1.2/) | Establish supply-chain provenance and attestation precedents. VSTD may bind their evidence but cannot manufacture their authorization or assurance level. | +| Preserved release and provenance evidence | NIST [Special Publication (SP) 800-218 SSDF 1.1](https://doi.org/10.6028/NIST.SP.800-218) | Protect the Software practices PS.3.1 and PS.3.2 call for preserving releases and provenance and enabling integrity verification. They do not certify a VSTD receipt. | +| Independent recreation | Reproducible Builds, [formal definition](https://reproducible-builds.org/docs/definition/) | Grounds the special case where another party recreates specified artifacts from declared inputs and instructions. Reproducibility does not establish every semantic claim. | +| Producer-supplied portable certificates | Necula, [*Proof-Carrying Code*](https://doi.org/10.1145/263699.263712) | Establishes the pattern of an untrusted producer supplying a proof checked under a declared policy. VSTD uses the pattern beyond code safety without inheriting PCC's theorem. | +| Consumer-checked UNSAT results | Wetzler, Heule, and Hunt, [*DRAT-trim*](https://www.cs.cmu.edu/~mheule/publications/drat-trim.pdf) | Establishes practical checking of clausal unsatisfiability proofs rather than trusting solver output. VSTD's implemented RUP format is narrower than DRAT. | +| A first-class refusal to fabricate a Boolean answer | [SMT-LIB Standard 2.7](https://smt-lib.org/papers/smt-lib-reference-v2.7-r2025-04-09.pdf) | Its response grammar includes `sat`, `unsat`, and `unknown`. VSTD independently defines a richer status system with the same fail-closed pressure. | +| Append-only public evidence and detectable equivocation | [RFC 9162: Certificate Transparency Version 2.0](https://www.rfc-editor.org/rfc/rfc9162.html) | Merkle proofs make log inclusion and consistency auditable while preserving explicit split-view limitations. VSTD additive receipts are analogous, not a CT implementation. | +| Freshness, rollback, freeze, and compromise recovery | [The Update Framework specification](https://theupdateframework.github.io/specification/latest/) | Demonstrates that authentic old data is not automatically current data. VSTD does not implement TUF, but likewise keeps freshness and revocation distinct from byte identity. | diff --git a/standard/VSTD-1.md b/standard/VSTD-1.md index 86ed48f..bb928a7 100644 --- a/standard/VSTD-1.md +++ b/standard/VSTD-1.md @@ -1,7 +1,15 @@ -# VSTD-1 — Claim Mechanics +# Verifier Standard (VSTD)-1 — Claim Mechanics -**Layer:** 1 of 5 on the object axis (see `LADDER.md`) -**Receipt wire format:** `schema_version = "VSTD-0.1"` — frozen; see `WIRE_IDENTIFIERS.md` +> **Acronyms:** artificial intelligence (AI); conjunctive normal form (CNF); directed acyclic graph (DAG); +> Davis-Putnam-Logemann-Loveland (DPLL); International Organization for Standardization (ISO); +> JavaScript Object Notation (JSON); Request for Comments (RFC); Boolean satisfiability problem (SAT); +> Secure Hash Algorithm 256-bit (SHA-256); satisfiability modulo theories (SMT); trusted computing base (TCB); +> Coordinated Universal Time (UTC); Unicode Transformation Format, 8-bit (UTF-8). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-1 on the object axis; required closure coordinate: Claim Mechanics (see `LADDER.md`) +**Receipt serialization:** `schema_version = "VSTD-1"`; see `WIRE_IDENTIFIERS.md` **Status:** Project Specification with Implemented Reference Subset **Maintainer:** TimeLordRaps **Date:** 2026-08-21 @@ -11,14 +19,14 @@ ## 1. Purpose & Thesis VSTD specifies infrastructure for consequential computational claims to carry -independently checkable evidence. Conformance is defined by this document, not by +evidence checkable outside its producer. Conformance is defined by this document, not by the identity of its maintainer. Modern AI systems, scientific simulators, and autonomous code generators routinely produce complex assertions without an attached, machine-checkable audit trail showing what evidence is offered for those claims. **VSTD-1** is a project specification for representing claims, capturing runtime provenance, structuring -machine-readable verification receipts, defining reproducibility levels, and +machine-readable verification receipts, defining reproduction-fidelity states, and separating trusted computing bases from untrusted outputs. It is not a consensus or accredited standard. @@ -37,7 +45,7 @@ accredited standard. ### 2.2 What a VSTD Verification Claim Does NOT Imply 1. **Universal Truth**: Verification is strictly relative to the declared formal system, input formula, and explicit scope. 2. **Unbounded Safety**: A verified component does not guarantee overall system safety if surrounding orchestration or unmodeled environmental dynamics fail. -3. **Semantic Infallibility of Unchecked Layers**: Non-extracted, unverified natural language outside the formal translation grammar is not certified. +3. **Unchecked Prose**: Non-extracted, unverified natural language outside the formal translation grammar is not certified. --- @@ -50,13 +58,13 @@ additive record rather than an in-place rewrite. | Status | Definition | | :--- | :--- | -| `DEMONSTRATED` | The claim is backed by executable tests or formal proofs that pass in an independently reproducible environment. | +| `DEMONSTRATED` | The claim is backed by executable tests or formal proofs that pass in a reproducible environment with recorded execution coordinates. Actor independence is a separate claim. | | `BENCHMARKED` | Quantitative performance or accuracy metrics have been empirically measured against a defined reference baseline. | | `SUPPORTED` | Theoretical derivation or empirical evidence is established, but automated end-to-end continuous verification is partial. | -| `IMPLEMENTED_UNVALIDATED` | Code or logic exists on disk, but automated independent verification has not yet run or passed. | +| `IMPLEMENTED_UNVALIDATED` | Code or logic exists on disk, but automated end-to-end verification has not yet run or passed. | | `INDETERMINATE` | Evidence is ambiguous, supporting leaves are unspecified, or solver execution timed out. | | `UNSUPPORTED` | No valid empirical or formal evidence is attached to the proposition. | -| `FALSIFIED` | An executable check, counterexample, or independent audit refuted the claim. | +| `FALSIFIED` | An executable check, counterexample, or evidence-bound audit refuted the claim. | | `HYPOTHESIS` | A stated conjecture intended for experimental falsification. | | `LONG_RANGE_OBJECTIVE` | A strategic or architectural aspiration requiring substantial future R&D. | @@ -79,17 +87,23 @@ A canonical claim record contains: ## 5. Independent Verification & Trusted Computing Base (TCB) To prevent self-referential confirmation bias (systems verifying their own uninspected -outputs), VSTD-1 defines an **Independent Verification Layer** as a conformance +outputs), VSTD-1 defines **independent-verification role separation** as a conformance requirement for claims labeled independent: ```text Target System (Producer) ↓ (Generates derivation / CNF / artifacts) Independent VSTD-Conformant Auditor - ↓ (Runs independent DPLL solver + DAG grounding checker in isolated TCB) + ↓ (Runs separately implemented DPLL solver + DAG grounding checker in isolated TCB) Structured VFY Receipt ``` +Independence in this profile is a claim about distinct actors occupying the producer and +checker roles. Two executions that return the same result do not prove that separate +actors performed them; nor do two processes or machines. Those are artifact and runtime +observations. Actor independence requires separately bound evidence, and it never +strengthens the checked result merely because an actor is identified or trusted. + ### Trusted Computing Base Invariant An auditor described as independent must: 1. Share zero solver state or runtime logic with the producer. @@ -97,32 +111,45 @@ An auditor described as independent must: 3. Explicitly declare its TCB components in every generated receipt. Running the bundled reference implementation does not by itself establish -organizational, implementation, or runtime independence. A receipt MUST state the -actual separation achieved. If producer and auditor share relevant logic or state, the -result is still inspectable but MUST NOT be labeled independent on that seam. +actor, implementation, or runtime independence. A receipt MUST state the actual +separation achieved. If distinct actors are not evidenced, actor independence is +`NOT_DEMONSTRATED` even when two results match. If producer and auditor share relevant +logic or state, the result is still inspectable but MUST NOT be labeled independent on +that seam. + +Serialized `EVIDENCED` status words and evidence-reference strings are declarations, not +validated bindings. A runtime MUST derive independent verification only after an +implemented validator resolves the referenced evidence, binds it to the producer and +checker executions, and establishes distinct actors plus the claimed implementation and +runtime seams. The VSTD 1.2.0 reference runtime implements no such adapter; it therefore +treats externally supplied assertions as no stronger than `DECLARED`, rejects receipts +that serialize them as `EVIDENCED`, and never emits `EVIDENCED`. --- ## 6. Reproducibility Taxonomy -VSTD-1 defines a five-tier reproducibility taxonomy: +VSTD-1 defines a five-state reproduction-fidelity taxonomy. The public +`ReproducibilityLevel` name is a compatibility identifier; it does not denote a numbered +VSTD profile or assurance strength: 1. `BITWISE_IDENTICAL`: Byte-for-byte exact match across all generated files, logs, and artifacts. 2. `CONTENT_IDENTICAL`: Canonical JSON representation of stable verification payload matches exactly, ignoring volatile execution fields (timestamps, elapsed wall-clock ms, hostnames). 3. `EVIDENCE_EQUIVALENT`: All checks, proofs, SAT assignments, and invariant bounds evaluate to the same truth values and proof certificates, though internal trace order or solver step counts may differ. -4. `RESULT_EQUIVALENT`: High-level verification verdict (`VERIFIED`/`FALSIFIED`) and primary metrics agree within declared tolerance bounds. -5. `SEMANTIC_REPRODUCTION`: The underlying formal proposition is sustained under an independent translation or alternate solver. +4. `RESULT_EQUIVALENT`: Summary verification verdict (`VERIFIED`/`FALSIFIED`) and primary metrics agree within declared tolerance bounds. +5. `SEMANTIC_REPRODUCTION`: The underlying formal proposition is sustained under a separately implemented translation or alternate solver. This does not establish distinct actors. --- ## 7. Canonical Receipt Specification & Hashing A VSTD-1 receipt separates **stable verification content** from **volatile execution metadata**. -Its historical wire identifier remains frozen: +The receipt kind is explicit: ``` receipt.json -├── schema_version: "VSTD-0.1" +├── schema_version: "VSTD-1" +├── receipt_kind: "claim_mechanics" ├── receipt_id: "VFY-XXXXXX" ├── canonical_digest: SHA256(canonical_json(stable_payload)) ├── claim: {...} @@ -135,7 +162,7 @@ receipt.json ``` ### Canonicalization Algorithm -1. Extract stable fields (`schema_version`, `receipt_id`, `claim`, `evidence`, `target_result`, `independent_audit`, `provenance_stable`, `reproducibility`). +1. Extract stable fields (`schema_version`, `receipt_kind`, `receipt_id`, `claim`, `evidence`, `target_result`, `independent_audit`, `provenance_stable`, `reproducibility`). 2. Serialize the VSTD-1 JSON subset with alphabetically sorted object keys, compact separators `","` and `":"`, UTF-8 encoding, and no non-finite numbers. This project-specific canonicalization is deterministic for the supported value subset; @@ -161,5 +188,5 @@ receipt.json derivation-graph acyclicity and grounding checks, Git/runtime provenance capture, stable-payload digest validation, generic command receipts, and bounded reproducibility comparison. -- **VSTD-2 — Verification Surface**: verification geometry, residual-driven deconstruction, horizons, valences, and bounded self-closure. VSTD-2 does not reinterpret existing receipts whose wire identifier is `VSTD-0.1`. +- **VSTD-2 — Verification Surface**: verification geometry, residual-driven deconstruction, horizons, valences, and bounded self-closure. Its results remain separate from VSTD-1 claim-mechanics results. - **Unassigned Future Work**: Additional proof mechanisms, execution-environment binding, and cross-institutional proof-carrying software gates require separate scoped proposals and evidence. No future version number is reserved here. diff --git a/standard/VSTD-2.md b/standard/VSTD-2.md index 32b94ea..ebb0fb0 100644 --- a/standard/VSTD-2.md +++ b/standard/VSTD-2.md @@ -1,23 +1,27 @@ -# VSTD-2 — Verification Surface +# Verifier Standard (VSTD)-2 — Verification Surface -**Layer:** 2 of 5 on the object axis (see `LADDER.md`) -**Receipt wire format:** `schema_version = "VSTD-0.2"` — frozen; see `WIRE_IDENTIFIERS.md` -**Status:** Additive experimental standard with an implemented vertical slice +> **Acronyms:** abstract syntax tree (AST); continuous delivery or deployment (CD); continuous integration (CI); +> intermediate representation (IR); trusted computing base (TCB). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-2 on the object axis; required closure coordinate: Verification Surface (see `LADDER.md`) +**Receipt serialization:** `schema_version = "VSTD-2"`; see `WIRE_IDENTIFIERS.md` +**Status:** experimental project specification with an implemented vertical slice **Maintainer:** TimeLordRaps **Date:** 2026-08-20 --- -## 1. Relationship to earlier standards +## 1. Relationship to adjacent profiles -VSTD-2 adds a verification-geometry ontology to VSTD-1. It does not replace or -reinterpret historical receipts whose wire identifiers are `VSTD-0.1` or -`VSTD-DATA-0.1`. A document conforms to this -extension only when it declares `schema_version = "VSTD-0.2"`; older validators may -continue to process their existing receipt kinds unchanged. +VSTD-2 adds a verification-geometry ontology beside VSTD-1 claim mechanics and +VSTD-Graph collection dynamics. A document conforms to this extension only when it +declares `schema_version = "VSTD-2"`; a result on one coordinate does not supply a +result on either adjacent surface. -VSTD-1 answers how a bounded claim carries evidence, provenance, an independent -judgment, and reproducibility information. VSTD-Graph-1 answers how artifacts and +VSTD-1 answers how a bounded claim carries evidence, provenance, a checker judgment, +an explicitly evidenced independence basis, and reproducibility information. VSTD-Graph-1 answers how artifacts and transformations compose into a provenance hypergraph. VSTD-2 answers a different question: **what geometry was selected for verification, what did reconstruction expose that the geometry missed, and has the sufficiency of the declared closure @@ -234,8 +238,8 @@ Higher-order verification is represented as a finite sequence: - `V1`: verification of V0's geometry, evidence, mechanisms, and selected surface; - `V2`: verification of V1's sufficiency criteria; and so on only when evidenced. -Each order greater than zero MUST verify exactly the preceding order. Skipped layers -violate the adjacent-layer invariant. A finite document never claims that simply +Each order greater than zero MUST verify exactly the preceding order. Skipped orders +violate the verification-order adjacency invariant. A finite document never claims that simply adding one more self-description would close the sequence; inability to justify the next order is a horizon or open valence. @@ -251,7 +255,7 @@ next order is a horizon or open valence. of the subject, evidence, mechanism state, and relevant environment. - `GEOMETRY_INSPECTABLE`: the declared situation has an inspectable geometry that represents covered, unsupported, indeterminate, and horizon-bounded coordinates - honestly. This vocabulary is prose-only: it is not a wire value, and it is not a + honestly. This vocabulary is prose-only: it is not a serialized receipt value, and it is not a member of the `CoordinateStatus` enumeration serialized in a VSTD-2 receipt. - `COMPLETELY_VERIFIED`: the declared closed surface satisfies self-closure. It never means universal truth, unbounded safety, or permanent validity. @@ -275,7 +279,7 @@ verification-instrumented. The common **verification language** is the typed graph of subjects, loci, facets, coordinates, seams, surfaces, judgments, mechanisms, residuals, horizons, valences, -and adjacent verification layers. It is not an intermediate programming language for +and adjacent verification orders. It is not an intermediate programming language for every CI/CD system. Native workflows translate observable verification events through thin adapters into this graph: @@ -283,7 +287,7 @@ thin adapters into this graph: native process -> adjacent adapter -> verification geometry -> verifier ``` -The adapter and verifier become loci in the next adjacent verification layer. This +The adapter and verifier become loci in the next adjacent verification order. This keeps verification orders adjacent and finite instead of recursing into infinite workflow abstraction. @@ -291,6 +295,34 @@ The language is self-describing only in the bounded sense that its schema, adapt validator, and closure criteria can themselves become subjects. Their description is not evidence of their correctness. +### 8.1 Profiles and profiler adapters + +A **geometry profile** is a named, reusable constraint on how this geometry is applied; +it is not a new verdict, numbered VSTD profile, assurance score, or substitute for a +verification mechanism. A geometry profile +may identify its subject and grain, expected loci and facets, selected surface and +exclusions, native observation sources, adapter and mapping identities, applicable +mechanisms, evidence requirements, bounds, trust roots, horizons, and falsification or +conformance conditions. + +A native profiler or domain tool remains an observation source. Its output enters a +VSTD-2 surface only through an adjacent adapter that attributes the translated values to +exact coordinates and exposes omissions, transformations, and information loss. A native +status word does not transfer into a VSTD judgment without the identified assessment that +earns that judgment. + +Geometry profiles are linked only through explicit shared coordinates, seams, mappings, and +evidence-bearing transformations. Naming two profiles together, applying them to the same +subject, or repeating their observations does not compose their verdicts. A composite +geometry profile must declare and assess the cross-profile seams; unresolved mappings and conflicts +remain horizons or open valences. + +The `VSTD-2` receipt does not currently carry a geometry-profile identifier or a +geometry-profile-composition object. This section defines the conceptual relationship only. +A geometry-profile document can bind +an exact VSTD-2 surface and receipt externally; a new wire representation requires an +explicit versioned profile boundary. + --- ## 9. Reprogramming compatibility @@ -323,7 +355,7 @@ A VSTD-2 geometry document conforms to the implemented vertical slice when: 3. every `VERIFIED` judgment cites evidence and a known mechanism; 4. references and containment are internally consistent; 5. reconstruction residuals are typed and localized; -6. verification orders obey the adjacent-layer invariant; and +6. verification orders obey the verification-order adjacency invariant; and 7. closure is reported by `assess_closure` without suppressing its blockers. The current slice does not infer loci automatically, prove ontology completeness, diff --git a/standard/VSTD-3.md b/standard/VSTD-3.md index cd15da2..ecbe454 100644 --- a/standard/VSTD-3.md +++ b/standard/VSTD-3.md @@ -1,7 +1,20 @@ -# VSTD-3 — Substrate Accountability - -**Layer:** 3 of 5 on the object axis (see `LADDER.md`) -**Receipt wire format:** `schema_version = "VSTD-3.0"` — frozen; see `WIRE_IDENTIFIERS.md` +# Verifier Standard (VSTD)-3 — Substrate Accountability + +> **Acronyms:** Advanced Micro Devices (AMD); Amazon Web Services (AWS); Compute Unified Device Architecture (CUDA); +> Device Identifier Composition Engine (DICE); DMTF standards organization (DMTF); DICE Protection Environment (DPE); +> Entity Attestation Token (EAT); floating-point operation (FLOP); hash-based message authentication code (HMAC); +> integrated development environment (IDE); Internet Engineering Task Force (IETF); +> International Organization for Standardization (ISO); JavaScript Object Notation (JSON); +> NVIDIA Management Library (NVML); Peripheral Component Interconnect (PCI); PCI Special Interest Group (PCI-SIG); +> Remote Attestation Procedures (RATS); Reference Integrity Manifest (RIM); software development kit (SDK); +> Secure Hash Algorithm 256-bit (SHA-256); system management interface (SMI); Security Protocol and Data Model (SPDM); +> Trusted Device Interface Security Protocol (TDISP); tensor processing unit (TPU); Coordinated Universal Time (UTC); +> Unicode Transformation Format, 8-bit (UTF-8); World Wide Web Consortium (W3C). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-3 on the object axis; required closure coordinate: Substrate Accountability (see `LADDER.md`) +**Receipt serialization:** `schema_version = "VSTD-3.0"` — frozen; see `WIRE_IDENTIFIERS.md` **Status:** implemented project specification **Editor:** TimeLordRaps **License:** Apache-2.0 @@ -126,7 +139,7 @@ The reference implication graph is explicit. In particular: - fleet-boundary attestation does not imply physical-world completeness. `VERIFIED` flags inside a receipt are not self-authenticating. A verifier MUST -independently reproduce signature and continuity checks before using those flags to +recompute signature and continuity checks from the bound evidence before using those flags to accept a strong `PASS`. ## 7. Incremental conformance profiles @@ -350,7 +363,7 @@ hardware or firmware evidence therefore reaches downstream artifacts through the existing blast-radius algorithm. VSTD-3 does not create a second lineage graph. Composition is transactional and refuses receipts whose recorded `PASS` claims cannot -be independently reproduced. +be recomputed from the bound evidence. ## 20. Verification algorithm @@ -361,12 +374,12 @@ A verifier MUST, in order: 3. verify identifiers and all references; 4. verify raw evidence byte digests; 5. validate challenge freshness, nonce uniqueness, subject, and certificate binding; -6. independently verify implemented attestation and provider signatures; +6. verify implemented attestation and provider signatures against configured trust material; 7. validate topology and partition lineage; 8. bind starts, observations, accounting, ends, and workload identity to events; 9. verify event continuity, resets, and anchors; 10. verify the exact fleet boundary when present; -11. recompute every recorded passing claim from independently accepted evidence; +11. recompute every recorded passing claim from mechanism-verified evidence; 12. reject any stronger recorded `PASS`. Receipt digest integrity alone completes only steps 1–2. @@ -388,8 +401,8 @@ trust anchors. Merely labeling bytes `SPDM`, `EAT`, or `DICE` is not verificatio ## 23. Compatibility VSTD-3 adds record and enum values. It does not reinterpret VSTD-1, VSTD-Graph-1, -VSTD-2, or their historical wire identifiers. Existing readers remain valid -for their versioned surfaces. VSTD-3 hardware nodes use additive artifact and +VSTD-2, or their current serialized receipt identifiers. Each reader remains bounded to its +versioned surface. VSTD-3 hardware nodes use additive artifact and transformation enum values in the existing hypergraph. ## 24. Falsification conditions @@ -408,5 +421,5 @@ VSTD-3 conformance is falsified for a claimed surface if any of these occurs: - global absence of undeclared compute is derived from an ordinary receipt. Implementation limitations and the complete threat model are in -`../docs/layers/vstd-3/threat-model.md`; vendor requirements are in -`../docs/layers/vstd-3/vendor-integration.md`. +`../docs/profiles/vstd-3/threat-model.md`; vendor requirements are in +`../docs/profiles/vstd-3/vendor-integration.md`. diff --git a/standard/VSTD-4.md b/standard/VSTD-4.md index dd4def4..bdec9af 100644 --- a/standard/VSTD-4.md +++ b/standard/VSTD-4.md @@ -1,52 +1,63 @@ -# VSTD-4 — Refutability +# Verifier Standard (VSTD)-4 — Refutability -**Layer:** 4 of 5 on the object axis (see `LADDER.md`) +> **Acronyms:** application programming interface (API); conjunctive normal form (CNF); grounded decision certificate (GDC); JavaScript Object Notation (JSON); +> resolution asymmetric tautology (RAT); Boolean satisfiability problem (SAT); +> Unicode Transformation Format, 8-bit (UTF-8). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-4 on the object axis; required closure coordinate: Refutability (see `LADDER.md`) **Certificate format:** `VSTD4-GDC-1` -**Status:** implemented project specification +**Status:** project specification; candidate computation implemented; evidence binding and conformance not implemented **Editor:** TimeLordRaps **License:** Apache-2.0 **Date:** 2026-08-22 -VSTD-4 defines **adversarially portable checkability**. A verdict reaches this -layer only when its exact meaning, evidence, failure conditions, and checking -procedure can leave the declarant and survive hostile independent inspection. +VSTD-4 defines **adversarially portable checkability**. A verdict satisfies this +profile only when its exact meaning, evidence, failure conditions, and checking +procedure can leave the declarant and survive hostile inspection outside the declarant. -VSTD-4 establishes that independent checking is possible. It does not establish -that an independent party exists or has checked anything; that is VSTD-5. +VSTD-4 establishes that checking by an outside party is possible. It does not establish +that such a party exists or has checked anything; that is VSTD-5. > **No verdict without a portable certificate.** > **No portable certificate without an explicit falsifier.** --- -## 1. Conformance and lower-layer preconditions +## 1. Conformance and prerequisite-profile coordinates VSTD-4 conformance is incremental. A claim MUST conform to VSTD-1, VSTD-2, and VSTD-3 before it can conform to VSTD-4. A VSTD-4 certificate over an unaccountable substrate does not repair the missing VSTD-3 evidence. -The normative depth is computed: +The VSTD-4 normative depth is computed: ``` vstd4_depth(claim) = max { k : CNF_4k(claim) is satisfiable } ``` -An implementation MUST NOT accept a declarant-supplied depth as authoritative. -For a depth below 14, the `FAIL` certificate for rung `k+1` is the normative -explanation of the ceiling. Entry to any VSTD-5 procedure requires: +An implementation MUST NOT accept a declarant-supplied VSTD-4 normative depth as authoritative. +For a normative depth below 14, the `FAIL` certificate for rung `k+1` is the normative +explanation of the ceiling. Entry to any VSTD-5 procedure requires established +VSTD-4 conformance and: ``` vstd4_depth(claim) == 14 ``` -The reference implementation is `verifier.core.depth`. +The historical `verifier.core.depth.vstd4_depth` API computes only a structural +candidate over caller-supplied, nonempty rung references. It does not resolve those +references, validate their rung propositions, or check VSTD-1/2/3 preconditions. Its +result is therefore `CANDIDATE` with `conformance_status = NOT_ESTABLISHED`, including +at candidate depth 14, and the reference VSTD-5 entry gate rejects it. --- -## 2. The fourteen-rung ladder +## 2. The fourteen-rung sequence -Each rung depends on the evidence named below and on every lower-layer -precondition. Rung 4.14 depends on the complete ladder. +Each rung depends on the evidence named below and on every prerequisite-profile +precondition. Rung 4.14 depends on the complete sequence. | Rung | Requirement | Direct dependencies | |---|---|---| @@ -102,7 +113,7 @@ C = H(claim || coordinate || policy_root || evidence_root || verifier Canonical serialization MUST use sorted object keys, integer-valued numeric fields, no floating-point values, UTF-8, and no insignificant whitespace. A -checker MUST reject a certificate whose binding does not match the independently +checker MUST reject a certificate whose binding does not match the externally supplied `ClaimBinding`. ### 2.4 Portable verification @@ -160,7 +171,7 @@ IDENTIFIED < AVAILABLE < PORTABLE < SELF_CONTAINED A digest alone establishes only `IDENTIFIED`. VSTD-4 requires at least `AVAILABLE`, and the claim's bundle is capped by its weakest verdict-critical -artifact. A declared level that its retrieval and retention evidence cannot +artifact. A declared availability state that its retrieval and retention evidence cannot support MUST be rejected. A locator and retention declaration alone are not retrieval evidence. `AVAILABLE` @@ -168,7 +179,7 @@ requires a successful retrieval observation bound to the artifact identifier, de locator, observed bytes, observation time, and observer. The observed bytes MUST match the content address. `PORTABLE` additionally requires anonymous access and a declared retrieval procedure. A retrieval observation is scoped to its named trust root; it does -not by itself establish independent retrieval. +not by itself establish retrieval by a distinct actor. ### 2.9 Disclosure-safe checkability @@ -219,7 +230,7 @@ VALID -> CHALLENGED -> REVOKED A valid challenge mechanism that cannot change claim status is non-conforming. Synthetic challenges test structural challengeability at VSTD-4. Actual -independent action belongs to VSTD-5. +action by a distinct actor belongs to VSTD-5. ### 2.13 Monotonic degradation @@ -233,7 +244,8 @@ A `RefutabilityClosure` MUST bind input certificates, the transformation certificate, the output claim, and a total output-refutation mapping. A challenge to an output must localize to an input, the transformation, or the composition. -Output depth MUST NOT exceed the weakest required input or transformation depth. +Output VSTD-4 normative depth MUST NOT exceed the weakest required input or transformation +VSTD-4 normative depth. This closure is both the handoff to VSTD-Graph edge evidence and the entry gate to VSTD-5. @@ -300,7 +312,7 @@ accepted. ## 4. Normative invariants > A verdict MUST NOT be recorded at a strength exceeding the strength of the -> certificate an independent party could check without the declarant's +> certificate an outside party could check without the declarant's > cooperation. > Loss of certificate validity, accessibility, dependency validity, or @@ -330,7 +342,7 @@ bounded checking. ## 6. Reference implementation boundary -The reference producer and data structures are in: +The reference certificate producer, candidate-depth computation, and data structures are in: * `src/verifier/core/certificate.py` * `src/verifier/core/grounding.py` @@ -341,6 +353,11 @@ The reference producer and data structures are in: The trusted checker is `src/verifier/core/kernel.py`. Producer modules are not part of its trusted import boundary. +The kernel checks the supplied certificate, grounding, and `ClaimBinding` for internal +consistency. It does not retrieve rung references or establish the required prerequisite-profile +results. Kernel acceptance of a candidate certificate is therefore not VSTD-4 +conformance. + No external implementation, interoperability profile, or third-party attack has yet been demonstrated for `VSTD4-GDC-1`. This implementation status MUST remain visible in claims about the format. diff --git a/standard/VSTD-5.md b/standard/VSTD-5.md index 889b93c..ddbbfcb 100644 --- a/standard/VSTD-5.md +++ b/standard/VSTD-5.md @@ -1,13 +1,15 @@ -# VSTD-5 — Witness Corroboration +# Verifier Standard (VSTD)-5 — Witness Corroboration -**Layer:** 5 of 5 on the object axis (see `LADDER.md`) +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-5 on the object axis; required closure coordinate: Witness Corroboration (see `LADDER.md`) **Status:** DRAFT — not implemented **Editor:** TimeLordRaps **License:** Apache-2.0 **Date:** 2026-08-22 VSTD-5 binds a fully refutable claim to witnesses that do not share the -declarant's trust root. It is the first layer that cannot be established by a +declarant's trust root. It is the first numbered profile that cannot be established by a declarant acting alone. This document is a draft interface, not an implementation or a claim that any @@ -17,15 +19,18 @@ independent witness exists. ## 1. Entry gate -Every VSTD-5 procedure MUST reject a claim unless its computed VSTD-4 depth is -exactly 14: +Every VSTD-5 procedure MUST reject a claim unless VSTD-1/2/3 preconditions and all +VSTD-4 rung propositions have been evidence-bound and checked, establishing normative +VSTD-4 conformance at normative depth 14: ``` vstd4_depth(claim) == 14 ``` -The gate is structural. A witness cannot corroborate a claim whose refutability -does not compose. +The gate is not satisfied by a structural candidate over caller-supplied references. +The current reference candidate reports `conformance_status = NOT_ESTABLISHED`, and +`require_vstd5_entry` rejects it even at candidate depth 14. A witness cannot +corroborate a claim whose refutability does not compose. --- diff --git a/standard/VSTD-Graph-1.md b/standard/VSTD-Graph-1.md index a06b9cb..06396d9 100644 --- a/standard/VSTD-Graph-1.md +++ b/standard/VSTD-Graph-1.md @@ -1,7 +1,13 @@ -# VSTD-Graph-1 — Recorded Lineage +# Verifier Standard (VSTD)-Graph-1 — Recorded Lineage -**Layer:** 1 of 5 on the graph axis (see `LADDER.md`) -**Receipt wire format:** `schema_version = "VSTD-DATA-0.1"` — frozen; see `WIRE_IDENTIFIERS.md` +> **Acronyms:** application programming interface (API); conjunctive normal form (CNF); Davis-Putnam-Logemann-Loveland (DPLL); operating system (OS); +> Boolean satisfiability problem (SAT); Secure Hash Algorithm 256-bit (SHA-256); satisfiability modulo theories (SMT); +> Software Package Data Exchange (SPDX); uniform resource identifier (URI). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-1; required closure coordinate: Recorded Lineage (see `LADDER.md`) +**Receipt serialization:** `schema_version = "VSTD-DATA-0.1"` — frozen; see `WIRE_IDENTIFIERS.md` **Status:** Project Specification with Implemented Reference Subset **Maintainer:** TimeLordRaps **Date:** 2026-08-21 @@ -20,18 +26,19 @@ first-class **N-ary Hyperedges**, which represent many-to-many merges, sharding, multi-input processing without flattening those relationships into ambiguous binary links. -This document is the first rung of the Graph axis. `VSTD-Graph-2.md` through +This document defines the first numbered profile of the Graph axis. `VSTD-Graph-2.md` through `VSTD-Graph-5.md` apply progressively stronger object and transformation-edge -requirements to the same closed collection. `LADDER.md` defines the computed -level and its ceiling certificate; `verifier.data.graph_level.graph_level` +requirements to the same closed collection. `LADDER.md` defines the computed candidate +Graph profile and its ceiling certificate; the compatibility API +`verifier.data.graph_level.graph_level` implements that computation. --- ## 2. The Provenance Hypergraph Abstraction -A Dataset Provenance Hypergraph is a 5-tuple: -$$\mathcal{H} = (\mathcal{A}, \mathcal{T}, \mathcal{C}, \mathcal{R}, \mathcal{P})$$ +A Dataset Provenance Hypergraph is a 6-tuple: +$$\mathcal{H} = (\mathcal{A}, \mathcal{T}, \mathcal{C}, \mathcal{R}, \mathcal{P}, \mathcal{X})$$ ### 2.1 Artifact Nodes ($\mathcal{A}$) Represents any discrete, inspectable data object or model state: @@ -68,6 +75,16 @@ outputs. The edge records ancestry; it does not by itself establish causal influ - Machine-checkable Boolean admission rules. The current reference subset evaluates bounded CNF with its minimal DPLL implementation; general SMT is not implemented. +### 2.6 Conflict Records ($\mathcal{X}$) +- `conflict_id`, `subject_id`, and `predicate` identify the disputed coordinate. +- `competing_values` retains at least two incompatible values. +- `evidence_refs` retains at least two evidence records rather than selecting a winner. + +A conflict record does not mutate the frozen artifact-status vocabulary. It makes the +subject inadmissible to a clean candidate Graph profile. The current reference implementation +has no conflict-resolution transition; later resolution must be additive and must retain the +competing evidence. + --- ## 3. Provenance Completeness Dimensions @@ -102,6 +119,9 @@ It is a coverage summary, not a probability, trust score, or verification verdic origin or transformation is not evidenced, the applicable state remains `UNKNOWN` or the applicable coverage dimension remains incomplete. It never silently becomes observed real-world truth. +* **The `CONFLICTED` Principle**: Incompatible retained evidence remains an explicit + conflict record. It is neither averaged nor collapsed into `UNKNOWN`, `VALID`, or a + scalar confidence value. * **Fail-Closed Policy Admission**: A policy passes only the Boolean condition it actually encodes. For example, "no ancestor is marked `REVOKED`" does not establish that every ancestor is `VALID`; a clean-ancestor policy must explicitly require @@ -143,7 +163,7 @@ When an upstream source $S$ is marked `REVOKED` (e.g. due to copyright claim, da that a claimed origin, contributor, execution, or license declaration is authentic. - **Complete real-world lineage**: Missing instrumentation, hidden inputs, pre-observation contamination, and out-of-band transformations remain outside the graph unless - independently evidenced. + separately evidenced. - **Automatic physical-file checking**: A stored VSTD-Graph receipt validates its own stable content. It flags a physical-file mismatch only when an adapter supplies and rehashes that file. diff --git a/standard/VSTD-Graph-2.md b/standard/VSTD-Graph-2.md index 9a714cc..7778283 100644 --- a/standard/VSTD-Graph-2.md +++ b/standard/VSTD-Graph-2.md @@ -1,17 +1,20 @@ -# VSTD-Graph-2 — Bounded Collection Surface +# Verifier Standard (VSTD)-Graph-2 — Bounded Collection Surface -**Layer:** 2 of 5 on the graph axis (see `LADDER.md`) -**Status:** implemented computed profile +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-2; required closure coordinate: Bounded Collection Surface (see `LADDER.md`) +**Status:** implemented candidate computation; rating-evidence binding not implemented **License:** Apache-2.0 -VSTD-Graph-2 closes collection-level scope leakage. A collection reaches this -layer only when every member and provenance ancestor is at object layer 2 or +VSTD-Graph-2 closes collection-scope leakage. A collection satisfies this candidate +profile only when every member and provenance ancestor is rated at object profile 2 or higher, every reachable status is admissible, and every transformation hyperedge -carries layer-2 edge evidence. +carries profile-2 edge ratings. -The level is computed by `verifier.data.graph_level`; it is never declared. -The `FAIL` certificate for Graph layer 2 names the member, ancestor, status, or -edge obligation that prevents admission. +`verifier.data.graph_level` computes a candidate from caller-supplied ratings and marks +conformance `NOT_ESTABLISHED`. The `FAIL` certificate for Graph profile 2 names the member, +ancestor, status, or edge obligation that prevents admission under those inputs. It does +not validate the ratings themselves. VSTD-Graph-2 does not establish that the evidence sources behind the collection are accountable. That is the blind spot closed by VSTD-Graph-3. diff --git a/standard/VSTD-Graph-3.md b/standard/VSTD-Graph-3.md index 710a34f..4f8bc70 100644 --- a/standard/VSTD-Graph-3.md +++ b/standard/VSTD-Graph-3.md @@ -1,17 +1,23 @@ -# VSTD-Graph-3 — Accountable Provenance Closure +# Verifier Standard (VSTD)-Graph-3 — Accountable Provenance Closure -**Layer:** 3 of 5 on the graph axis (see `LADDER.md`) -**Status:** implemented computed profile +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-3; required closure coordinate: Accountable Provenance Closure (see `LADDER.md`) +**Status:** implemented candidate computation; rating-evidence binding not implemented **License:** Apache-2.0 VSTD-Graph-3 closes unaccountable substrate across a collection. A collection -reaches this layer only when every member and reachable ancestor is at object -layer 3 or higher, every reachable status is admissible, and every transformation -hyperedge carries layer-3 edge evidence. +satisfies this candidate profile only when every member and reachable ancestor is rated at +object profile 3 or higher, every reachable status is admissible, and every transformation +hyperedge carries profile-3 edge ratings. The provenance closure condition is normative: rating only the selected members is insufficient. The weakest reachable ancestor or transformation caps the collection. +The reference computation consumes caller-supplied ratings and therefore reports a +candidate with conformance `NOT_ESTABLISHED`. Its certificate does not establish that a +VSTD-3 mechanism produced any supplied rating. + VSTD-Graph-3 cannot establish that an outside party could refute the composed collection. That blind spot is closed by VSTD-Graph-4. diff --git a/standard/VSTD-Graph-4.md b/standard/VSTD-Graph-4.md index 7fe4e93..230e3f6 100644 --- a/standard/VSTD-Graph-4.md +++ b/standard/VSTD-Graph-4.md @@ -1,17 +1,22 @@ -# VSTD-Graph-4 — Refutable Transformation Closure +# Verifier Standard (VSTD)-Graph-4 — Refutable Transformation Closure -**Layer:** 4 of 5 on the graph axis (see `LADDER.md`) -**Status:** implemented computed profile +> **Acronym:** unsatisfiable (UNSAT). + +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-4; required closure coordinate: Refutable Transformation Closure (see `LADDER.md`) +**Status:** implemented candidate computation; rating-evidence binding not implemented **License:** Apache-2.0 -VSTD-Graph-4 closes non-compositional refutability. A collection reaches this -layer only when every member and reachable ancestor is at object layer 4 or +VSTD-Graph-4 closes non-compositional refutability. A collection satisfies this candidate +profile only when every member and reachable ancestor is rated at object profile 4 or higher, statuses are admissible, and every transformation hyperedge carries -layer-4 evidence including a valid `RefutabilityClosure`. +profile-4 ratings including a valid `RefutabilityClosure`. Two VSTD-4 nodes connected by an unevidenced edge do not make a VSTD-Graph-4 collection. A challenge to the collection output must localize to a member, ancestor, transformation, or the composition itself. -The UNSAT certificate at the next level is the computed explanation of the -collection's ceiling. +The UNSAT certificate at the next profile is the computed explanation of the candidate +ceiling over caller-supplied ratings. It does not establish Graph-4 conformance or +validate the claimed `RefutabilityClosure` records. diff --git a/standard/VSTD-Graph-5.md b/standard/VSTD-Graph-5.md index bdcf96b..8552586 100644 --- a/standard/VSTD-Graph-5.md +++ b/standard/VSTD-Graph-5.md @@ -1,18 +1,21 @@ -# VSTD-Graph-5 — Corroborated Verification Network +# Verifier Standard (VSTD)-Graph-5 — Corroborated Verification Network -**Layer:** 5 of 5 on the graph axis (see `LADDER.md`) +> Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-Graph-5; required closure coordinate: Corroborated Verification Network (see `LADDER.md`) **Status:** DRAFT profile; computation is implemented, witness protocol is not **License:** Apache-2.0 VSTD-Graph-5 is the collection profile for independently corroborated members, -ancestors, and transformations. The computed graph-level mechanism requires +ancestors, and transformations. The candidate-profile computation requires object and edge ratings of at least 5, provenance closure, and admissible status throughout. Because VSTD-5 is draft, the reference implementation can compute this profile -only over externally supplied level-5 ratings; it does not manufacture or verify +only over externally supplied profile-5 ratings; it does not manufacture or verify their independence. A result based on self-declared ratings is not VSTD-Graph-5 conformance. -Conflicting witness records degrade the relevant object status and therefore the -computed collection level. They are never averaged into a passing collection. +Conflicting witness records are retained as conflict records and make the relevant +subject inadmissible to a clean candidate Graph profile. They are never averaged into +a passing collection. diff --git a/standard/WIRE_IDENTIFIERS.md b/standard/WIRE_IDENTIFIERS.md index 736365f..da77128 100644 --- a/standard/WIRE_IDENTIFIERS.md +++ b/standard/WIRE_IDENTIFIERS.md @@ -1,106 +1,88 @@ -# VSTD frozen wire identifiers and historical filenames +# Verifier Standard (VSTD) serialized receipt identifiers -**Status:** normative for wire-identifier dispatch; filename history is informative -**Date:** 2026-08-22 +> **Acronyms:** command-line interface (CLI). -VSTD has no demonstrated external adoption or independent implementation as of this -release. This document therefore does not prescribe an adopter migration. It records -identifiers and filenames that appeared in the project's own public releases so that -those artifacts are not silently reinterpreted. +**Status:** normative for current serialized-receipt dispatch +**Date:** 2026-08-27 -Specification numbers now identify verification depth. Repository releases use -semantic versions independently. +A **serialized receipt identifier** is the value written into a receipt to select its exact reader and schema, principally `schema_version` plus any required profile discriminator. Standards literature often calls this a *wire identifier* or part of a *wire format*; here it means the stored JavaScript Object Notation (JSON) contract, not a network protocol. -## 1. Frozen receipt wire identifiers +Specification numbers identify numbered profiles and their cumulative closure coordinates. +Repository releases use semantic +versions independently. Retired partial-profile object identifiers and specification files are +not current profiles and are absent from this source tree; published tags and Git history +preserve those earlier project artifacts without making the current reader accept or +reinterpret them. -A filename or current layer label does not change the meaning of an issued receipt. -Readers MUST dispatch a receipt by its wire identifier: +## 1. Current serialized receipt dispatch -| Current layer document | Frozen wire identifier | +Readers MUST dispatch by the exact `schema_version` and any required profile +discriminator. Unknown identifiers, missing discriminators, and mismatched shapes fail +closed: + +| Numbered-profile document | Current serialized receipt identifier | |---|---| -| `VSTD-1.md` | `schema_version = "VSTD-0.1"` | -| `VSTD-2.md` | `schema_version = "VSTD-0.2"` | +| `VSTD-1.md` | `schema_version = "VSTD-1"` | +| `VSTD-2.md` | `schema_version = "VSTD-2"` | | `VSTD-3.md` | `schema_version = "VSTD-3.0"` | +| `VSTD-4.md` | `schema_version = "VSTD-4"` | +| `VSTD-5.md` | `schema_version = "VSTD-5-DRAFT"` | | `VSTD-Graph-1.md` | `schema_version = "VSTD-DATA-0.1"` | -New layer-4 and layer-5 documents use their own schemas without changing historical -canonical digests. +VSTD-1 has two current receipt profiles: + +| `receipt_kind` | Schema | Meaning | +|---|---|---| +| `claim_mechanics` | `vstd1_receipt.json` | bounded claim, evidence, checker, provenance, and reproducibility | +| `generic_computational_run` | `vstd1_generic_run_receipt.json` | planned execution, captured outputs, assessment context, and reproduction surface | + +Both discriminators are required. A reader MUST NOT guess the profile from incidental +field similarity. + +The generic-run `assessment_context` is a VSTD-1 container for mechanism identity, +declared resource bounds, prior commitment, and the refutation surface. It is not a +VSTD-4 object and carries no VSTD-4 conformance field. The container and its selected +fields participate in the canonical digest. ### 1.1 Non-wire vocabulary -`VSTD-2.md` section 7 defines a prose lifecycle vocabulary. Only the +`VSTD-2.md` section 7 defines prose lifecycle vocabulary. Only the `CoordinateStatus` members serialized in `receipts/schema/vstd2_receipt.json` (`PRE_VERIFIED`, `VERIFIED`, `FALSIFIED`, `INDETERMINATE`, `UNSUPPORTED`, `STALE`) -are wire values. `POST_VERIFIED`, `GEOMETRY_INSPECTABLE`, and `COMPLETELY_VERIFIED` -are descriptive terms only and have never appeared in an issued receipt; renaming -them does not affect any canonical digest. `GEOMETRY_INSPECTABLE` was named -`VERIFIABLE` in unreleased drafts before `v1.1.2`; a status token MUST NOT reuse the -maintainer's name. +are serialized receipt values. `POST_VERIFIED`, `GEOMETRY_INSPECTABLE`, and `COMPLETELY_VERIFIED` +are descriptive terms rather than receipt values. -## 2. Historical names in project releases +## 2. Stored artifact-control identifiers -| Historical public name | Current layer label | Meaning | -|---|---|---| -| `VSTD-0.1` | `VSTD-1` | claim mechanics | -| `VSTD-0.2` | `VSTD-2` | verification surface | -| `VSTD-3.0` | `VSTD-3` | substrate accountability | -| — | `VSTD-4` | refutability | -| — | `VSTD-5` | witness corroboration, draft | -| `VSTD-DATA-0.1` | `VSTD-Graph-1` | recorded lineage over collections | - -`VSTD-Graph-2` through `VSTD-Graph-5` first appeared under their current labels. - -The current repository does not duplicate old specification paths. Historical tags -remain the resolver for the bytes published under those paths: - -```text -standard/VSTD-0.1.md -> standard/VSTD-1.md -standard/VSTD-0.2.md -> standard/VSTD-2.md -standard/VSTD-3.0.md -> standard/VSTD-3.md -standard/VSTD-DATA-0.1.md -> standard/VSTD-Graph-1.md -VSTD3_THREAT_MODEL.md -> docs/layers/vstd-3/threat-model.md -VSTD3_VENDOR_INTEGRATION.md -> docs/layers/vstd-3/vendor-integration.md -VSTD3_REFERENCES.md -> docs/layers/vstd-3/references.md -VSTD3_MIGRATION.md -> docs/layers/vstd-3/compatibility.md -COMPETITION_EVALUATION_PROFILE.md -> docs/profiles/competition-evaluation.md -CLAIMS_AND_LIMITS.md -> docs/CLAIMS_AND_LIMITS.md -``` - -## 2.1 Import package and distribution rename - -From `v1.1.2` the import package is `verifier` and the distribution is -`verifier-standard`. Both -were previously `verifiable` / `verifiable-standard`. The rename removes a name that -collided with the ordinary-English adjective, with a former VSTD-2 status token, and -with the maintainer's former project name. - -| Historical name | Current name | Kind | -|---|---|---| -| `verifiable` | `verifier` | import package | -| `verifiable-standard` | `verifier-standard` | distribution | -| `verifiable-standard-.zip` | `verifier-standard-.zip` | release source archive | +Artifact-control mechanism objects are stored JSON contracts, not network traffic, VSTD +receipts, or new numbered profiles. They dispatch independently by: + +| Object | `schema_version` | +|---|---| +| Freeze manifest | `VSTD-ARTIFACT-FREEZE-1` | +| Self-closing seal envelope | `VSTD-ARTIFACT-SEAL-1` | +| Seal closure payload | `VSTD-ARTIFACT-SEAL-CLOSURE-1` | +| Thaw lineage sidecar | `VSTD-ARTIFACT-THAW-1` | -No receipt wire identifier, schema `$id`, or canonical digest changes. Specification -text that cites a reference module (for example `verifier.core.kernel`) is a pointer -into the reference implementation, not a wire value. +Their normative behavior is [`ARTIFACT_CONTROL.md`](ARTIFACT_CONTROL.md); their strict +combined schema is published as +[`artifact-control-1.schema.json`](https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json). +These identifiers do not imply a network protocol or VSTD conformance result. -Release manifests published up to and including `v1.1.1` bind -`verifiable-standard-.zip` in their `source.archive_prefix`. -`scripts/release_artifacts.py verify` derives the archive name from the manifest, so -those releases stay verifiable without republishing. +## 3. Import package and distribution -## 3. CLI compatibility +The distribution is `verifier-standard`, the import package is `verifier`, and +`vstd` is the canonical cross-platform CLI name. `verifier` may resolve to Windows Driver +Verifier on common Windows `PATH` configurations. The `verifiable` command remains a +compatibility alias for already-published execution instructions; it is not an import +package or a standard identifier. -`vstd` is the canonical cross-platform CLI name. The `verifier` alias remains -available, but Windows resolves the unqualified name to its built-in Driver Verifier -utility on common `PATH` configurations. `verifiable` also remains an alias because -project release materials and receipt instructions may bind that executable name. It is -a command name only: since `v1.1.2` it no longer corresponds to any import package. -Retaining either alias preserves project compatibility; it is not evidence of -external use. +Release verification derives archive names and console-script expectations from the +release manifest being checked. This preserves issued release evidence without carrying +obsolete standard identifiers into current receipt dispatch. ## 4. Release versioning -The first repository release using integer layer names is `v1.0.0`. The release -number does not claim VSTD-5 implementation: VSTD-5 is explicitly draft. Existing -`v0.1.0` and `v0.2.0` tags and GitHub releases remain untouched. +A repository release number does not claim conformance to a same-numbered VSTD profile. +VSTD-5 remains draft until its required witness mechanism and evidence binding are +implemented. diff --git a/tests/test_assurance_flow_invariants.py b/tests/test_assurance_flow_invariants.py new file mode 100644 index 0000000..76e6c81 --- /dev/null +++ b/tests/test_assurance_flow_invariants.py @@ -0,0 +1,81 @@ +"""Terminology: Verifier Standard (VSTD). + +Falsification probes for evidence-strength invariants shared by the five-As +human traversal and existing VSTD machinery. +""" + +from __future__ import annotations + +from verifier.core.reproducibility import ( + ReproducibilityLevel, + compare_reproduction_level, +) +from verifier.data.models import ( + ArtifactNode, + ArtifactType, + HyperedgePort, + ProvenanceHypergraph, + TransformationHyperedge, + TransformationType, +) + + +def _artifact(artifact_id: str) -> ArtifactNode: + return ArtifactNode(artifact_id, artifact_id, ArtifactType.MODEL, "a" * 64) + + +def _edge(edge_id: str, source: str, target: str) -> TransformationHyperedge: + return TransformationHyperedge( + edge_id, + edge_id, + TransformationType.EVALUATION, + (HyperedgePort(source, "INPUT"),), + (HyperedgePort(target, "OUTPUT"),), + {}, + {}, + {}, + ) + + +def test_matching_field_or_mismatching_verdict_earns_no_reproduction_level() -> None: + assert compare_reproduction_level("a", "b", "PASS", "PASS") is None + assert compare_reproduction_level("a", "b", "PASS", "FAIL") is None + + +def test_matching_bound_evidence_can_earn_only_its_checked_level() -> None: + assert ( + compare_reproduction_level( + "a", + "b", + "PASS", + "PASS", + original_evidence_hash="evidence", + reproduced_evidence_hash="evidence", + ) + is ReproducibilityLevel.EVIDENCE_EQUIVALENT + ) + + +def test_duplicate_paths_do_not_multiply_ancestral_support() -> None: + graph = ProvenanceHypergraph() + for artifact_id in ("source", "result"): + graph.add_artifact(_artifact(artifact_id)) + graph.add_transformation(_edge("path:one", "source", "result")) + graph.add_transformation(_edge("path:two", "source", "result")) + + assert graph.ancestors(["result"]) == {"source", "result"} + assert graph.descendants(["source"]) == {"source", "result"} + + +def test_self_consumption_and_two_node_feedback_are_cycles() -> None: + self_graph = ProvenanceHypergraph() + self_graph.add_artifact(_artifact("a")) + self_graph.add_transformation(_edge("self", "a", "a")) + assert self_graph.verify_acyclicity() is False + + feedback = ProvenanceHypergraph() + feedback.add_artifact(_artifact("a")) + feedback.add_artifact(_artifact("b")) + feedback.add_transformation(_edge("a-to-b", "a", "b")) + feedback.add_transformation(_edge("b-to-a", "b", "a")) + assert feedback.verify_acyclicity() is False diff --git a/tests/test_core_receipt_integrity.py b/tests/test_core_receipt_integrity.py new file mode 100644 index 0000000..362589a --- /dev/null +++ b/tests/test_core_receipt_integrity.py @@ -0,0 +1,82 @@ +"""Adversarial digest tests for the Verifier Standard (VSTD)-1 receipt model.""" + +from __future__ import annotations + +from dataclasses import replace + +import pytest + +from verifier.core.checker import IndependentAuditor +from verifier.core.provenance import GitProvenance, ProvenanceRecord, RuntimeEnvironment +from verifier.core.receipt import ClaimSpec, EvidencePayload, VstdReceipt + + +def _receipt() -> VstdReceipt: + return VstdReceipt( + schema_version="VSTD-1", + receipt_kind="claim_mechanics", + receipt_id="receipt-integrity-test", + claim=ClaimSpec( + id="claim-1", + title="Bounded claim", + statement="The recorded formula is satisfiable.", + status="PASS", + scope="This fixture only.", + limitations=("No actor independence is established.",), + falsification_condition="The formula is unsatisfiable.", + last_verified="2026-08-26", + ), + evidence=EvidencePayload( + domain="Boolean satisfiability problem", + input_text_or_formula="x1", + n_vars=1, + clauses=((1,),), + atomic_reasons=(), + assumptions=(), + source_artifacts={"fixture": "sha256:" + "0" * 64}, + ), + target_result={"satisfiable": True}, + independent_audit=IndependentAuditor.audit_claim_derivation( + claim_id="claim-1", + n_vars=1, + clauses=((1,),), + atomic_reasons=(), + ), + provenance=ProvenanceRecord( + target_name="fixture", + portable_repository_id="example/fixture", + local_repository_path="excluded-from-stable-payload", + git=GitProvenance("a" * 40, "main", False), + runtime=RuntimeEnvironment("3.12", "CPython", "test", "test", "test", "masked"), + captured_at_utc="2026-08-26T00:00:00Z", + command_executed="fixture", + ), + reproducibility={"highest_demonstrated_level": "CONTENT_IDENTICAL"}, + ) + + +def test_retired_claim_identifier_is_rejected() -> None: + retired = "VSTD-" + "0.1" + with pytest.raises(ValueError, match="schema_version must be VSTD-1"): + replace(_receipt(), schema_version=retired) + + +def test_stable_payload_tampering_invalidates_receipt_digest() -> None: + receipt = _receipt() + receipt.compute_and_set_digest() + assert receipt.verify_digest_integrity() + + receipt.target_result["satisfiable"] = False + + assert not receipt.verify_digest_integrity() + + +def test_recorded_digest_cannot_self_validate_after_claim_replacement() -> None: + receipt = _receipt() + original_digest = receipt.compute_and_set_digest() + receipt.claim = ClaimSpec( + **{**receipt.claim.__dict__, "statement": "A substituted proposition."} + ) + + assert receipt.canonical_digest == original_digest + assert not receipt.verify_digest_integrity() diff --git a/tests/test_flagship_demo.py b/tests/test_flagship_demo.py index 7453e93..04b5ab7 100644 --- a/tests/test_flagship_demo.py +++ b/tests/test_flagship_demo.py @@ -1,4 +1,6 @@ -"""Conformance tests for the public, adversarial VSTD flagship demo.""" +"""Terminology: Verifier Standard (VSTD). + +Conformance tests for the public, adversarial VSTD flagship demo.""" from __future__ import annotations @@ -12,7 +14,7 @@ "wrong-artifact": "REJECTED", "honest-unknown": "ACCEPTED/UNKNOWN", "inflated-tier": "REJECTED", - "poisoned-ancestor": "GRAPH-LEVEL-0; REVOKED", + "poisoned-ancestor": "GRAPH-CANDIDATE-0; REVOKED", } diff --git a/tests/test_gdc_certificate.py b/tests/test_gdc_certificate.py index 4c6945d..f09bef3 100644 --- a/tests/test_gdc_certificate.py +++ b/tests/test_gdc_certificate.py @@ -1,4 +1,8 @@ -"""``VSTD4-GDC-1`` conformance, and regressions pinning the three retrofits. +"""Terminology: conjunctive normal form (CNF); grounded decision certificate (GDC); +Boolean satisfiability problem (SAT); trusted computing base (TCB); unsatisfiable (UNSAT); +Verifier Standard (VSTD). + +``VSTD4-GDC-1`` conformance, and regressions pinning the three retrofits. The tests that matter most here are the ones no competition proof format could express: the keystone test, where a decision block is perfectly valid and the @@ -6,7 +10,7 @@ linear-time check is dressed in general-resolution machinery to look rigorous. The retrofit regressions at the bottom assert that the *old* behaviour is gone, -not merely that the new behaviour works. Three things shipped that layer 4 +not merely that the new behaviour works. Three things shipped that VSTD-4 prohibits, and a test that only exercises the fix would pass again the moment someone reintroduced the shortcut beside it. """ @@ -580,6 +584,7 @@ def test_trusted_computing_base_is_hashes_not_a_literal_dict(): descriptor = IndependentAuditor.verifier_descriptor() assert descriptor.implementation_hash.startswith("sha256:") assert descriptor.specification_hash.startswith("sha256:") + assert descriptor.certificate_format == "VSTD1-CHECKER-REPORT" # Computed from the file on disk, not from a string constant. import hashlib @@ -589,6 +594,11 @@ def test_trusted_computing_base_is_hashes_not_a_literal_dict(): ).hexdigest() assert descriptor.implementation_hash == expected + expected_specification = "sha256:" + hashlib.sha256( + (Path(__file__).resolve().parents[1] / "standard" / "VSTD-1.md").read_bytes() + ).hexdigest() + assert descriptor.specification_hash == expected_specification + # And it declares what it actually implements, not VSTD4-GDC-1. assert descriptor.certificate_format != FORMAT assert "isolation" not in IndependentAuditor.tcb() diff --git a/tests/test_graph_level.py b/tests/test_graph_level.py index 30e96b9..fb7e06d 100644 --- a/tests/test_graph_level.py +++ b/tests/test_graph_level.py @@ -1,15 +1,18 @@ -"""The VSTD-Graph axis: a computed level, and the proof of its ceiling. +"""Terminology: Verifier Standard (VSTD). -The level is never declared. Each test below pins one of the four conditions -- +The VSTD-Graph axis: a candidate profile over supplied ratings, and the proof of its ceiling. + +The candidate profile is never declared. Each test below pins one of the four conditions -- membership floor, provenance closure, status admissibility, edge evidence -- -and checks not just that the level dropped but that the certificate at ``N+1`` -*says why*. A level without that certificate would be an assertion, and the +and checks not just that the profile number dropped but that the certificate at ``N+1`` +*says why*. A candidate profile without that certificate would be an assertion, and the whole point of this axis is that a collection of well-rated members can still be badly rated as a collection. """ from __future__ import annotations +from dataclasses import replace import importlib import pytest @@ -26,6 +29,7 @@ GRAPH_MAX_LEVEL, GraphCollection, GraphEncodingError, + GraphLevelResult, INADMISSIBLE_STATUSES, ObligationKind, certify_graph_cnf, @@ -38,11 +42,13 @@ ArtifactNode, ArtifactStatus, ArtifactType, + ConflictRecord, HyperedgePort, ProvenanceHypergraph, TransformationHyperedge, TransformationType, ) +from verifier.data.policy import ProvenancePolicyVerifier graph_module = importlib.import_module("verifier.data.graph_level") @@ -51,7 +57,7 @@ def _binding() -> ClaimBinding: return ClaimBinding( - claim="corpus graph level", + claim="corpus candidate Graph profile", coordinate=ClaimCoordinate("collection:C", "vstd_graph_level"), policy_root="sha256:policy", evidence_root="sha256:evidence", @@ -170,15 +176,34 @@ def test_a_revoked_ancestor_disqualifies_the_collection_entirely(): _assert_certificates_check(result) -@pytest.mark.parametrize("status", sorted(INADMISSIBLE_STATUSES, key=lambda s: s.value)) +@pytest.mark.parametrize("status", sorted(INADMISSIBLE_STATUSES - {"CONFLICTED"})) def test_every_inadmissible_status_fails_closed(status): - assert _level(_graph(mid=status), _collection()).level == 0 + assert _level(_graph(mid=ArtifactStatus(status)), _collection()).level == 0 def test_superseded_is_admissible_and_documented_as_such(): """A superseded ancestor was replaced going forward; its history is unchanged.""" - assert ArtifactStatus.SUPERSEDED not in INADMISSIBLE_STATUSES - assert _level(_graph(src=ArtifactStatus.SUPERSEDED), _collection()).level == GRAPH_MAX_LEVEL + graph = _graph(src=ArtifactStatus.SUPERSEDED) + assert ArtifactStatus.SUPERSEDED.value not in INADMISSIBLE_STATUSES + assert _level(graph, _collection()).level == GRAPH_MAX_LEVEL + assert ProvenancePolicyVerifier.verify_all_ancestors_valid(graph, "corpus").passed is False + + +@pytest.mark.parametrize( + "current_status", + (ArtifactStatus.CHALLENGED, ArtifactStatus.REVOKED, ArtifactStatus.STALE), +) +def test_current_admissibility_changes_without_rewriting_historical_graph(current_status): + historical = _graph() + historical_bytes = historical.to_dict() + assert _level(historical, _collection()).level == GRAPH_MAX_LEVEL + + current = ProvenanceHypergraph.from_dict(historical_bytes) + current.artifacts["src"] = replace(current.artifacts["src"], status=current_status) + + assert _level(current, _collection()).level == 0 + assert historical.artifacts["src"].status is ArtifactStatus.VALID + assert historical.to_dict() == historical_bytes def test_an_artifact_missing_from_the_graph_is_unknown_not_absent(): @@ -187,6 +212,19 @@ def test_an_artifact_missing_from_the_graph_is_unknown_not_absent(): assert _level(graph, _collection()).level == 0 +def test_cyclic_ancestry_cannot_receive_a_clean_candidate_level(): + graph = _graph() + graph.add_transformation( + TransformationHyperedge( + "t3", "feedback", TransformationType.AUGMENTATION, + (HyperedgePort("corpus", "IN"),), (HyperedgePort("src", "OUT"),), {}, {}, {}, + ) + ) + + with pytest.raises(GraphEncodingError, match="cyclic recorded ancestry"): + _level(graph, _collection(edges={"t1": 5, "t2": 5, "t3": 5})) + + # -------------------------------------------------------------------------- # The certificate at N+1 is the explanation # -------------------------------------------------------------------------- @@ -211,10 +249,47 @@ def test_the_witness_and_the_refutation_are_different_certificates(): assert summary["witness_digest"] is not None assert summary["refutation_digest"] is not None assert summary["witness_digest"] != summary["refutation_digest"] + assert summary["rating_basis"] == "CALLER_SUPPLIED" + assert summary["conformance_status"] == "NOT_ESTABLISHED" + + +def test_caller_cannot_promote_a_graph_candidate_to_conformance(): + with pytest.raises(TypeError, match="conformance_status"): + GraphLevelResult( + "collection:x", + 5, + None, + None, + (), + conformance_status="ESTABLISHED", # type: ignore[call-arg] + ) + + +def test_conflicting_lineage_is_retained_and_blocks_a_clean_level(): + graph = _graph() + graph.add_conflict( + ConflictRecord( + conflict_id="conflict:src-digest", + subject_id="src", + predicate="content_digest", + competing_values=("sha256:a", "sha256:b"), + evidence_refs=("receipt:a", "receipt:b"), + ) + ) + + restored = ProvenanceHypergraph.from_dict(graph.to_dict()) + assert restored.conflicts["conflict:src-digest"].competing_values == ( + "sha256:a", + "sha256:b", + ) + result = _level(restored, _collection()) + assert result.level == 0 + assert "caller-supplied ratings" in result.explanation + assert "conformance is not established" in result.explanation def test_variable_numbering_is_stable_across_adjacent_levels(): - """Two levels of the same collection are comparable, not unrelated formulas.""" + """Two candidate profiles of the same collection are comparable formulas.""" items = obligations(_graph(), _collection({"src": 5, "mid": 3, "corpus": 5})) low, low_grounding = encode("collection:C", items, 3) high, high_grounding = encode("collection:C", items, 4) @@ -226,7 +301,7 @@ def test_variable_numbering_is_stable_across_adjacent_levels(): # -------------------------------------------------------------------------- -# Monotonicity, and the things the level must refuse to say +# Monotonicity and the claims the candidate-profile computation must refuse # -------------------------------------------------------------------------- @@ -251,7 +326,7 @@ def test_lowering_any_single_rating_can_only_lower_the_level(): def test_an_empty_collection_has_no_level(): - """Vacuous truth would hand out level 5 for a collection nobody can refute.""" + """Vacuous truth would hand out candidate Graph-5 for an empty collection.""" with pytest.raises(GraphEncodingError, match="no members"): _level(_graph(), GraphCollection("collection:empty", ())) @@ -289,7 +364,7 @@ def test_encoding_divergence_raises_with_a_certificate_attached(monkeypatch): def test_solver_divergence_is_caught_before_the_direct_check(monkeypatch): - """The encoding and the independent solver must agree first, or nothing else counts.""" + """The encoding and separately implemented solver must agree first.""" class ContrarySolver: def __init__(self, **_kwargs): @@ -301,7 +376,7 @@ def solve(self): monkeypatch.setattr(graph_module, "MinimalIndependentDPLL", ContrarySolver) items = obligations(_graph(), _collection()) - with pytest.raises(GraphEncodingError, match="independent solver said False"): + with pytest.raises(GraphEncodingError, match="separately implemented solver said False"): certify_graph_cnf( collection_id="collection:C", items=items, level=GRAPH_MAX_LEVEL, binding=_binding(), @@ -323,6 +398,6 @@ def test_every_graph_formula_is_horn_and_therefore_tier_up(): graph = _graph() for rating in range(0, GRAPH_MAX_LEVEL + 1): items = obligations(graph, _collection({"src": rating, "mid": rating, "corpus": rating})) - for level in range(1, GRAPH_MAX_LEVEL + 1): - formula, _grounding = encode("collection:C", items, level) + for candidate_profile in range(1, GRAPH_MAX_LEVEL + 1): + formula, _grounding = encode("collection:C", items, candidate_profile) assert is_horn(formula) diff --git a/tests/test_independent_checker.py b/tests/test_independent_checker.py index 638ddab..e70de01 100644 --- a/tests/test_independent_checker.py +++ b/tests/test_independent_checker.py @@ -1,13 +1,19 @@ -"""Unit tests for the independent VSTD SAT solver and Grounding checker.""" +"""Terminology: Boolean satisfiability problem (SAT); unsatisfiable (UNSAT); +Verifier Standard (VSTD). + +Unit tests for the bundled VSTD SAT solver and grounding checker.""" from __future__ import annotations from verifier.core.checker import ( GroundingVerdict, + IndependenceBasis, + IndependenceStatus, IndependentGroundingChecker, IndependentAuditor, MinimalIndependentDPLL, VerificationVerdict, + independence_is_evidenced, ) @@ -117,6 +123,40 @@ def test_independent_auditor_end_to_end() -> None: expected_satisfiable=True, ) assert audit.overall_verdict == VerificationVerdict.VERIFIED + assert audit.independence_basis.independently_verified is False + assert audit.to_dict()["independence_basis"]["actor_independence"] == ( + "NOT_DEMONSTRATED" + ) + assert audit.to_dict()["independence_basis"]["runtime_separation"] == ( + "NOT_DEMONSTRATED" + ) assert audit.sat_result.satisfiable is True assert audit.grounding_result.grounding_status == GroundingVerdict.GROUNDED assert "MinimalIndependentDPLL" in audit.trusted_computing_base["solver"] + + +def test_matching_checker_runs_do_not_establish_actor_independence() -> None: + arguments = { + "claim_id": "TEST-REPEAT", + "n_vars": 1, + "clauses": [[1]], + "atomic_reasons": [], + "expected_satisfiable": True, + } + first = IndependentAuditor.audit_claim_derivation(**arguments) + second = IndependentAuditor.audit_claim_derivation(**arguments) + assert first.overall_verdict == second.overall_verdict + assert not first.independence_basis.independently_verified + assert not second.independence_basis.independently_verified + + +def test_serialized_evidence_references_cannot_self_promote_independence() -> None: + basis = IndependenceBasis( + actor_independence=IndependenceStatus.EVIDENCED, + implementation_separation=IndependenceStatus.EVIDENCED, + runtime_separation=IndependenceStatus.EVIDENCED, + evidence=("receipt:producer", "receipt:checker"), + ) + assert not basis.independently_verified + raw = basis.to_dict() + assert not independence_is_evidenced(raw) diff --git a/tests/test_layer4.py b/tests/test_layer4.py index f4963ad..9e632d1 100644 --- a/tests/test_layer4.py +++ b/tests/test_layer4.py @@ -1,4 +1,6 @@ -"""Rungs 4.8 through 4.14 -- the parts of layer 4 that are not the kernel. +"""Terminology: Verifier Standard (VSTD). + +Rungs 4.8 through 4.14 -- the parts of VSTD-4 outside the kernel. Each test here pins one of the challenge-theater prohibitions: @@ -18,7 +20,7 @@ import pytest from verifier.core.certificate import ClaimCoordinate -from verifier.data.models import ArtifactStatus +from verifier.data.models import ArtifactNode, ArtifactStatus, ArtifactType, ProvenanceHypergraph from verifier.hardware.anchors import AnchorError, LocalAnchorProvider from verifier.layer4.availability import ( ArtifactAvailability, @@ -426,6 +428,27 @@ def test_a_credible_challenge_actually_moves_verdict_state(): assert ledger.status("claim:1").status is ArtifactStatus.REVOKED +def test_challenge_records_do_not_silently_mutate_graph_state(): + graph = ProvenanceHypergraph() + graph.add_artifact( + ArtifactNode( + "claim:1", + "claim", + ArtifactType.MODEL, + "a" * 64, + status=ArtifactStatus.VALID, + ) + ) + ledger = ChallengeLedger() + ledger.file(_challenge(), _surface()) + ledger.adjudicate( + Adjudication("ch:1", ChallengeOutcome.ACCEPTED, "confirmed", "2026-02-02T00:00:00Z") + ) + + assert ledger.status("claim:1").status is ArtifactStatus.REVOKED + assert graph.artifacts["claim:1"].status is ArtifactStatus.VALID + + def test_a_disproven_challenge_returns_the_claim_to_valid(): ledger = ChallengeLedger() ledger.file(_challenge(), _surface()) @@ -567,6 +590,7 @@ def test_the_output_is_capped_by_its_weakest_link(): check = closure.validate() assert check.accepted is True assert check.closed_depth == 9 # not 14, not the average, not the transformation + assert check.conformance_status == "NOT_ESTABLISHED" def test_refutability_does_not_increase_under_composition(): diff --git a/tests/test_logits_constraint_kernel.py b/tests/test_logits_constraint_kernel.py index 706f821..33a9372 100644 --- a/tests/test_logits_constraint_kernel.py +++ b/tests/test_logits_constraint_kernel.py @@ -1,4 +1,6 @@ -"""Real logits-level constraint tests against llguidance, not an engine simulation.""" +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Real logits-level constraint tests against llguidance, not an engine simulation.""" from __future__ import annotations diff --git a/tests/test_packaged_specifications.py b/tests/test_packaged_specifications.py index b3e03ac..7089342 100644 --- a/tests/test_packaged_specifications.py +++ b/tests/test_packaged_specifications.py @@ -1,4 +1,6 @@ -"""Installed specification resources must match the public normative files exactly.""" +"""Terminology: Request for Comments (RFC); Verifier Standard (VSTD). + +Installed specification resources must match the public normative files exactly.""" from __future__ import annotations @@ -9,7 +11,29 @@ def test_packaged_specification_bytes_match_normative_sources() -> None: - for name in ("LADDER.md", "VSTD-3.md", "VSTD-4.md", "WIRE_IDENTIFIERS.md"): - normative = REPO_ROOT / "standard" / name - packaged = REPO_ROOT / "src" / "verifier" / "specifications" / name - assert packaged.read_bytes() == normative.read_bytes(), name + normative_files = sorted((REPO_ROOT / "standard").glob("*.md")) + packaged_dir = REPO_ROOT / "src" / "verifier" / "specifications" + assert {path.name for path in packaged_dir.glob("*.md")} == { + path.name for path in normative_files + } + for normative in normative_files: + assert (packaged_dir / normative.name).read_bytes() == normative.read_bytes(), ( + normative.name + ) + + +def test_ladder_fixes_causal_provenance_directions_without_actor_trust() -> None: + ladder = (REPO_ROOT / "standard" / "LADDER.md").read_text(encoding="utf-8") + assert "ancestor artifact --TRUST through a checked transformation--> descendant" in ladder + assert "recorded TRUST --ROT under typed current-state evidence--> reassessment" in ladder + assert "descendant deviation --RUST memetic causal backtrace--> ancestor candidates" in ladder + assert "Memetic propagation" in ladder + assert "RFC 2119" in ladder + assert "RFC 8174" in ladder + assert "not serialized receipt values or\ncomputable conformance results" in ladder + assert "current VSTD runtime emits or validates a TRUST, ROT, or\nRUST transfer" in ladder + assert "MUST NOT strengthen an artifact-bound\nresult" in ladder + assert "TRUST and RUST never cancel" in ladder + assert "whether an actor is good, bad, reputable, or worthy of trust" in ladder + assert "zero unevidenced knowledge is presumed" in ladder + assert "cryptographic zero knowledge" in ladder diff --git a/tests/test_public_data.py b/tests/test_public_data.py index be8e56e..15e918a 100644 --- a/tests/test_public_data.py +++ b/tests/test_public_data.py @@ -1,11 +1,17 @@ -"""Target-neutral VSTD-DATA receipt validation and mechanism replay.""" +"""Terminology: Verifier Standard (VSTD). + +Target-neutral VSTD-DATA receipt validation and mechanism replay.""" from __future__ import annotations +import json from pathlib import Path +import pytest + from verifier.core.checker import VerificationVerdict from verifier.core.provenance import GitProvenance, ProvenanceRecord, RuntimeEnvironment +from verifier.core.receipt import compute_canonical_digest from verifier.data.models import ( ArtifactNode, ArtifactStatus, @@ -24,6 +30,7 @@ reproduce_data_receipt, validate_data_receipt, ) +from verifier.runtime.public_cli import _inspect_data_receipt, main def _receipt() -> VstdDataReceipt: @@ -105,12 +112,94 @@ def _receipt() -> VstdDataReceipt: ) -def test_public_data_receipt_round_trip(tmp_path: Path) -> None: +def _rehash(payload: dict) -> None: + provenance = payload["provenance"] + payload["canonical_digest"] = compute_canonical_digest( + { + "schema_version": payload["schema_version"], + "receipt_id": payload["receipt_id"], + "dataset_spec": payload["dataset_spec"], + "hypergraph": payload["hypergraph"], + "completeness_metrics": payload["completeness_metrics"], + "policy_evaluations": payload["policy_evaluations"], + "independent_audit": payload["independent_audit"], + "provenance_stable": { + "target_name": provenance["target_name"], + "portable_repository_id": provenance["portable_repository_id"], + "git_commit_sha": provenance["git"]["commit_sha"], + "git_branch": provenance["git"]["branch"], + "git_is_dirty": provenance["git"]["is_dirty"], + "runtime_python_version": provenance["runtime"]["python_version"], + }, + "reproducibility": payload["reproducibility"], + } + ) + + +def test_public_data_receipt_round_trip(tmp_path: Path, capsys) -> None: _receipt().save_to_directory(tmp_path) assert validate_data_receipt(tmp_path) == 0 + assert "[VALIDATION OK]" in capsys.readouterr().out assert reproduce_data_receipt(tmp_path) == 0 +def test_graph_validate_and_inspect_honor_json(tmp_path: Path, capsys) -> None: + _receipt().save_to_directory(tmp_path) + + for command in ("validate", "inspect"): + assert main([command, str(tmp_path), "--json"]) == 0 + result = json.loads(capsys.readouterr().out) + assert result["command"] == command + assert result["receipt_kind"] == "vstd_graph" + assert result["result"] == "COMPLETED" + assert result["exit_code"] == 0 + + +def test_actorless_independence_upgrade_is_rejected_and_never_displayed( + tmp_path: Path, capsys +) -> None: + receipt_path = _receipt().save_to_directory(tmp_path) + payload = json.loads(receipt_path.read_text(encoding="utf-8")) + payload["independent_audit"]["independence_basis"][ + "independently_verified" + ] = True + _rehash(payload) + receipt_path.write_text(json.dumps(payload), encoding="utf-8") + + assert _inspect_data_receipt(tmp_path) == 0 + assert "Independence: NOT_DEMONSTRATED" in capsys.readouterr().out + assert main(["validate", str(tmp_path)]) == 1 + assert "no actor/execution evidence-binding validator" in capsys.readouterr().err + + +def test_self_promoted_independence_with_arbitrary_references_is_rejected( + tmp_path: Path, capsys +) -> None: + receipt_path = _receipt().save_to_directory(tmp_path) + payload = json.loads(receipt_path.read_text(encoding="utf-8")) + basis = payload["independent_audit"]["independence_basis"] + basis.update( + { + "actor_independence": "EVIDENCED", + "implementation_separation": "EVIDENCED", + "runtime_separation": "EVIDENCED", + "evidence": ["receipt:producer", "receipt:checker"], + "independently_verified": True, + } + ) + _rehash(payload) + receipt_path.write_text(json.dumps(payload), encoding="utf-8") + + assert main(["validate", str(tmp_path)]) == 1 + errors = capsys.readouterr().err + assert "no actor/execution evidence-binding validator" in errors + assert "no stronger than DECLARED" in errors + assert main(["inspect", str(tmp_path)]) == 0 + inspection = capsys.readouterr().out + assert "Independence: NOT_DEMONSTRATED" in inspection + assert "Independence: EVIDENCED" not in inspection + + def test_public_data_receipt_tamper_fails(tmp_path: Path) -> None: receipt_path = _receipt().save_to_directory(tmp_path) receipt_path.write_text(receipt_path.read_text(encoding="utf-8") + " ", encoding="utf-8") @@ -130,6 +219,29 @@ def test_missing_artifact_status_defaults_to_unknown() -> None: assert artifact.status == ArtifactStatus.UNKNOWN +def test_duplicate_graph_identifier_cannot_replace_recorded_evidence() -> None: + graph = ProvenanceHypergraph() + original = ArtifactNode( + artifact_id="artifact:duplicate", + label="original", + artifact_type=ArtifactType.RAW_SOURCE_FILE, + content_digest="a" * 64, + ) + graph.add_artifact(original) + + with pytest.raises(ValueError, match="duplicate graph identifier"): + graph.add_artifact( + ArtifactNode( + artifact_id="artifact:duplicate", + label="replacement", + artifact_type=ArtifactType.RAW_SOURCE_FILE, + content_digest="b" * 64, + ) + ) + + assert graph.artifacts["artifact:duplicate"] is original + + def test_completeness_rejects_non_hex_digest() -> None: graph = ProvenanceHypergraph() graph.add_artifact( diff --git a/tests/test_refutation_certificate.py b/tests/test_refutation_certificate.py index 21ce6d3..b4fe77e 100644 --- a/tests/test_refutation_certificate.py +++ b/tests/test_refutation_certificate.py @@ -1,4 +1,7 @@ -"""VSTD layer 4: refusals must carry certificates a stranger can check. +"""Terminology: conjunctive normal form (CNF); Boolean satisfiability problem (SAT); +unsatisfiable (UNSAT); Verifier Standard (VSTD). + +VSTD-4 Refutability: refusals must carry certificates a stranger can check. The property under test is not merely that the solver is correct. It is that an UNSAT verdict ships an artifact an independent party validates *without* @@ -153,7 +156,7 @@ def _random_cnf(rng: random.Random, n_vars: int, n_clauses: int) -> list[list[in def test_agrees_with_the_existing_solver_and_every_refusal_is_certified(): """Cross-check against MinimalIndependentDPLL over many random 3-CNF instances. - Fixed seed: this must be reproducible, per VSTD layer 1. + Fixed seed: this must be reproducible under VSTD-1 Claim Mechanics. """ rng = random.Random(20260822) unsat_seen = 0 diff --git a/tests/test_verification_geometry.py b/tests/test_verification_geometry.py index 920f228..4a8fc93 100644 --- a/tests/test_verification_geometry.py +++ b/tests/test_verification_geometry.py @@ -1,4 +1,6 @@ -"""Semantic tests for the additive VSTD-0.2 verification geometry slice.""" +"""Terminology: Verifier Standard (VSTD). + +Semantic tests for the additive VSTD-2 verification geometry slice.""" import json from dataclasses import replace @@ -254,6 +256,13 @@ def test_locus_and_facet_are_distinct_and_grain_is_orthogonal_to_stratum() -> No assert geometry.validate() == [] +def test_retired_geometry_identifier_is_rejected() -> None: + geometry = replace( + geometry_with_reconstruction_horizon(), schema_version="VSTD-" + "0.2" + ) + assert any("schema_version must be 'VSTD-2'" in error for error in geometry.validate()) + + def test_assumptions_cannot_manufacture_a_verified_judgment() -> None: geometry = geometry_with_reconstruction_horizon() geometry.judgments[0] = CoordinateJudgment( @@ -335,7 +344,7 @@ def test_verification_orders_must_be_adjacent_not_infinitely_abstracted() -> Non errors = geometry.validate() assert any("contiguous and start at 0" in error for error in errors) - assert any("adjacent-layer invariant" in error for error in errors) + assert any("order-adjacency invariant" in error for error in errors) def test_geometry_digest_is_deterministic() -> None: diff --git a/tests/test_vstd4_depth.py b/tests/test_vstd4_depth.py index 208d4a6..65dd245 100644 --- a/tests/test_vstd4_depth.py +++ b/tests/test_vstd4_depth.py @@ -1,14 +1,15 @@ -"""The ladder internal to VSTD-4, and the gate it guards. +"""Terminology: identifier (ID); unsatisfiable (UNSAT); Verifier Standard (VSTD). -``vstd4_depth`` is computed, never declared. That is the whole point: standing -up an external verification node is VSTD-5, and reaching it must be -*computationally costly*, because verification is the new scaling. A rung that -could be declared would let an implementer skip the climb. +The structural candidate rung sequence internal to VSTD-4, and the gate it cannot cross. + +``vstd4_depth`` computes consistency over caller-supplied rung references. The +references are not resolved and prerequisite-profile coordinates are not checked, so +the result remains ``NOT_ESTABLISHED`` even when its candidate depth is 14. The tests below check the two halves of an honest answer. The witness certifies the rungs that were climbed; the refutation certifies why the next one was not, and its conflict clause names the missing rung. Both are checked by the same -kernel that checks any other VSTD-4 claim -- the layer certifies its own ceiling +kernel that checks any other VSTD-4 claim -- the candidate-depth computation certifies its own ceiling using its own mechanism. """ @@ -74,7 +75,7 @@ def _assert_certificates_check(result: DepthResult) -> None: # -------------------------------------------------------------------------- -# The ladder itself +# The rung sequence itself # -------------------------------------------------------------------------- @@ -97,10 +98,11 @@ def test_the_top_rung_depends_on_every_other(): # -------------------------------------------------------------------------- -def test_full_evidence_reaches_the_top_and_admits_vstd5(): +def test_full_reference_set_reaches_only_the_candidate_top(): result = _depth(_evidence()) assert result.depth == MAX_DEPTH - assert result.admits_vstd5 is True + assert result.conformance_status == "NOT_ESTABLISHED" + assert result.admits_vstd5 is False assert result.refutation is None assert result.blocking_rungs == () assert result.witness is not None @@ -132,7 +134,7 @@ def test_the_refutation_is_the_explanation_not_a_separate_report(): def test_removing_evidence_can_only_lower_the_depth(): - """Rung 4.13, applied to the ladder itself: weakening never strengthens.""" + """Rung 4.13, applied to the sequence itself: weakening never strengthens.""" baseline = _depth(_evidence()).depth previous = baseline for rung in reversed(RUNGS): @@ -144,9 +146,9 @@ def test_removing_evidence_can_only_lower_the_depth(): def test_depth_is_monotone_in_the_prefix(): - for level in range(0, MAX_DEPTH + 1): - result = _depth(_evidence(only=level)) - assert result.depth == level + for candidate_depth in range(0, MAX_DEPTH + 1): + result = _depth(_evidence(only=candidate_depth)) + assert result.depth == candidate_depth _assert_certificates_check(result) @@ -159,16 +161,26 @@ def test_no_evidence_is_depth_zero_with_a_refutation_and_no_witness(): _assert_certificates_check(result) -def test_the_vstd5_gate_refuses_anything_below_fourteen(): - """Layer 4 asks *could a stranger check this?*; layer 5 asks *did one?*""" - for level in range(0, MAX_DEPTH): - result = _depth(_evidence(only=level)) +def test_the_vstd5_gate_refuses_every_unbound_candidate(): + for candidate_depth in range(0, MAX_DEPTH + 1): + result = _depth(_evidence(only=candidate_depth)) assert result.admits_vstd5 is False - with pytest.raises(VSTD5EntryError, match="requires computed vstd4_depth"): + expected = ( + "requires computed vstd4_depth" + if candidate_depth < MAX_DEPTH + else "requires established VSTD-4 conformance" + ) + with pytest.raises(VSTD5EntryError, match=expected): require_vstd5_entry(result) - complete = _depth(_evidence()) - assert complete.admits_vstd5 is True - assert require_vstd5_entry(complete) is complete + + +def test_fourteen_arbitrary_strings_cannot_establish_vstd4_or_vstd5_readiness(): + result = _depth({rung.id: "arbitrary-nonempty-text" for rung in RUNGS}) + assert result.depth == MAX_DEPTH + assert result.conformance_status == "NOT_ESTABLISHED" + assert result.admits_vstd5 is False + with pytest.raises(VSTD5EntryError, match="requires established VSTD-4 conformance"): + require_vstd5_entry(result) def test_unknown_rung_ids_are_refused(): @@ -179,6 +191,8 @@ def test_unknown_rung_ids_are_refused(): def test_depth_summary_carries_both_certificate_digests(): summary = _depth(_evidence(without=("4.5",))).to_dict() assert summary["depth"] == 4 + assert summary["depth_kind"] == "CANDIDATE" + assert summary["conformance_status"] == "NOT_ESTABLISHED" assert summary["admits_vstd5"] is False assert summary["blocking_rungs"] == ["4.5"] assert summary["witness_digest"] is not None diff --git a/tests/test_vstd_schemas.py b/tests/test_vstd_schemas.py index 114a267..4ef9117 100644 --- a/tests/test_vstd_schemas.py +++ b/tests/test_vstd_schemas.py @@ -1,6 +1,8 @@ -"""Published JSON Schema coverage for the integer-layer release. +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). -JSON Schema checks document shape. The independent kernel remains authoritative +Published JavaScript Object Notation (JSON) Schema coverage for the numbered-profile release. + +JSON Schema checks document shape. The separately implemented kernel remains authoritative for grounding, tier, count, binding, and proof semantics. """ @@ -25,11 +27,19 @@ ) from verifier.core.kernel import check, reference_descriptor from verifier.core.refutation import build_horn_certificate +from verifier.data.models import ( + ArtifactNode, + ArtifactStatus, + ArtifactType, + ConflictRecord, + ProvenanceHypergraph, +) SCHEMA_DIR = Path(__file__).resolve().parents[1] / "receipts" / "schema" PUBLISHED_SCHEMAS = ( "vstd1_receipt.json", + "vstd1_generic_run_receipt.json", "vstd2_receipt.json", "vstd3_receipt.json", "vstd4_receipt.json", @@ -93,6 +103,57 @@ def test_every_published_schema_is_valid_draft_2020_12() -> None: Draft202012Validator.check_schema(_load(name)) +def test_graph_schema_and_runtime_share_status_and_conflict_shapes() -> None: + schema = _load("vstd_graph_receipt.json")["properties"]["hypergraph"] + graph = ProvenanceHypergraph() + graph.add_artifact( + ArtifactNode("artifact:a", "a", ArtifactType.CORPUS, "a" * 64, status=ArtifactStatus.VALID) + ) + graph.add_conflict( + ConflictRecord( + "conflict:a", + "artifact:a", + "content_digest", + ("sha256:a", "sha256:b"), + ("receipt:a", "receipt:b"), + ) + ) + payload = graph.to_dict() + Draft202012Validator(schema).validate(payload) + assert ProvenanceHypergraph.from_dict(payload).to_dict() == payload + + +def test_graph_schema_keeps_legacy_candidate_blocks_additively_valid() -> None: + candidate_schema = _load("vstd_graph_receipt.json")["properties"][ + "computed_graph_level" + ] + legacy = { + "collection_id": "collection:legacy", + "level": 2, + "max_level": 5, + "blocking_obligations": [], + "witness_digest": HEX, + "refutation_digest": HEX, + } + Draft202012Validator(candidate_schema).validate(legacy) + assert "rating_basis" not in candidate_schema["required"] + assert "conformance_status" not in candidate_schema["required"] + + +def test_independence_schema_rejects_actorless_independence_claim() -> None: + basis_schema = _load("vstd1_receipt.json")["properties"]["independent_audit"][ + "properties" + ]["independence_basis"] + basis = { + "independently_verified": True, + "actor_independence": "NOT_DEMONSTRATED", + "implementation_separation": "EVIDENCED", + "runtime_separation": "EVIDENCED", + "evidence": ["receipt:checker"], + } + assert list(Draft202012Validator(basis_schema).iter_errors(basis)) + + def test_vstd4_gdc_certificate_matches_its_published_schema() -> None: certificate, _binding = _certificate() Draft202012Validator(_load("vstd4_certificate.json")).validate( @@ -100,7 +161,7 @@ def test_vstd4_gdc_certificate_matches_its_published_schema() -> None: ) -def test_vstd4_receipt_requires_the_computed_ceiling_certificate() -> None: +def test_vstd4_candidate_receipt_is_explicit_and_keeps_legacy_shape_valid() -> None: certificate, binding = _certificate() schema = _load("vstd4_receipt.json") validator = Draft202012Validator(schema, registry=_registry()) @@ -110,6 +171,7 @@ def test_vstd4_receipt_requires_the_computed_ceiling_certificate() -> None: "claim_id": "claim:schema-test", "binding": binding.to_dict(), "vstd4_depth": 13, + "conformance_status": "NOT_ESTABLISHED", "rung_evidence": {f"4.{index}": f"sha256:{HEX}" for index in range(1, 14)}, "witness": certificate.to_dict(), "ceiling_refutation": certificate.to_dict(), @@ -117,6 +179,15 @@ def test_vstd4_receipt_requires_the_computed_ceiling_certificate() -> None: "status": "VALID", } validator.validate(receipt) + assert "does not establish VSTD-4 conformance" in schema["properties"]["status"]["description"] + + legacy = dict(receipt) + del legacy["conformance_status"] + validator.validate(legacy) + + receipt["conformance_status"] = "ESTABLISHED" + assert list(validator.iter_errors(receipt)) + receipt["conformance_status"] = "NOT_ESTABLISHED" receipt["ceiling_refutation"] = None errors = list(validator.iter_errors(receipt)) @@ -124,9 +195,10 @@ def test_vstd4_receipt_requires_the_computed_ceiling_certificate() -> None: assert any("not of type 'object'" in error.message for error in errors) -def test_vstd5_draft_schema_enforces_the_vstd4_entry_gate() -> None: +def test_vstd5_draft_schema_records_shape_without_establishing_entry() -> None: schema = _load("vstd5_receipt.json") validator = Draft202012Validator(schema, format_checker=FormatChecker()) + assert "current VSTD-4 candidate cannot satisfy" in schema["description"] receipt = { "schema_version": "VSTD-5-DRAFT", "status": "DRAFT", @@ -173,13 +245,14 @@ def test_vstd5_draft_schema_enforces_the_vstd4_entry_gate() -> None: assert any("14 was expected" in error.message for error in errors) -def test_layer_filenames_do_not_change_historical_wire_identifiers() -> None: - assert _load("vstd1_receipt.json")["properties"]["schema_version"]["enum"] == [ - "VSTD-0.1" - ] - assert _load("vstd2_receipt.json")["properties"]["schema_version"]["const"] == ( - "VSTD-0.2" - ) +def test_current_wire_identifiers_and_profile_discriminators() -> None: + claim = _load("vstd1_receipt.json")["properties"] + generic = _load("vstd1_generic_run_receipt.json")["properties"] + assert claim["schema_version"]["enum"] == ["VSTD-1"] + assert claim["receipt_kind"]["const"] == "claim_mechanics" + assert generic["schema_version"]["const"] == "VSTD-1" + assert generic["receipt_kind"]["const"] == "generic_computational_run" + assert _load("vstd2_receipt.json")["properties"]["schema_version"]["const"] == "VSTD-2" assert _load("vstd_graph_receipt.json")["properties"]["schema_version"][ "enum" ] == ["VSTD-DATA-0.1"] From 54e5798fa336818f9156a4df95c0d637e890a8a8 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:46:14 -0700 Subject: [PATCH 02/34] Decompose the generic run runtime Reason: Separate capture, planning, validation, inspection, reproduction, and impact responsibilities while preserving one public run-receipt mechanism. Evidence: The runtime modules, generic-run specimen, stable-payload reconstruction, and adversarial lifecycle tests change together. Coordinate: VSTD-1 generic computational run assessment; release 1.2.0. Falsification: A stable field is omitted from digest reconstruction, an inadmissible receipt validates, or reproduction exceeds its declared ceiling. Compatibility: New receipts use the current VSTD-1 assessment context; no retired developmental container is emitted or read. --- examples/generic_run/compute.py | 10 +- examples/generic_run/input.txt | 2 +- examples/generic_run/manifest.json | 18 +- src/verifier/core/run.py | 515 +++++--------------- src/verifier/core/run_impact.py | 68 +++ src/verifier/core/run_inspection.py | 57 +++ src/verifier/core/run_planning.py | 90 ++++ src/verifier/core/run_reproduction.py | 120 +++++ src/verifier/core/run_support.py | 28 ++ src/verifier/core/run_validation.py | 650 ++++++++++++++++++++++++++ tests/test_generic_run.py | 418 ++++++++++++++--- 11 files changed, 1501 insertions(+), 475 deletions(-) create mode 100644 src/verifier/core/run_impact.py create mode 100644 src/verifier/core/run_inspection.py create mode 100644 src/verifier/core/run_planning.py create mode 100644 src/verifier/core/run_reproduction.py create mode 100644 src/verifier/core/run_support.py create mode 100644 src/verifier/core/run_validation.py diff --git a/examples/generic_run/compute.py b/examples/generic_run/compute.py index 209710d..accd052 100644 --- a/examples/generic_run/compute.py +++ b/examples/generic_run/compute.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 -"""Deterministic word-frequency computation used by the VSTD generic-run example. +"""Terminology: Verifier Standard (VSTD). + +Word-frequency computation used by the VSTD generic-run example. Pure standard library, no randomness, no floating point, and no wall-clock dependence in its *output* — timing is recorded separately by the VSTD -receipt as execution metadata, not baked into these artifacts. That is what -lets this example legitimately declare ``determinism_declared: DETERMINISTIC`` -in manifest.json. +receipt as execution metadata, not baked into these artifacts. The generic capture +path does not independently establish determinism, so the manifest leaves that +classification unknown and relies on explicit rerun comparison instead. """ from __future__ import annotations diff --git a/examples/generic_run/input.txt b/examples/generic_run/input.txt index f5a31eb..c2a3488 100644 --- a/examples/generic_run/input.txt +++ b/examples/generic_run/input.txt @@ -2,5 +2,5 @@ a claim is not merely logged a claim is packaged with evidence a claim is packaged with scope a claim is packaged with provenance -a claim is checked by an independent auditor +a claim is checked by a separately implemented auditor a claim becomes a challengeable receipt diff --git a/examples/generic_run/manifest.json b/examples/generic_run/manifest.json index a9bcd1e..484f47a 100644 --- a/examples/generic_run/manifest.json +++ b/examples/generic_run/manifest.json @@ -1,21 +1,21 @@ { "claim": { "id": "RUN-000001", - "title": "Deterministic word-frequency computation over a fixed input corpus", - "statement": "Running compute.py against the declared input.txt deterministically produces output.json (a sorted word-frequency table) and metrics.json (a total-token-count metric), reproducing byte-identically on rerun.", - "scope": "A single, self-contained, dependency-free Python computation used to demonstrate the VERIFIABLE generic proof-carrying run primitive end-to-end: source -> inputs -> execution -> outputs -> claim -> receipt -> independent validation -> reproduction.", + "title": "Word-frequency computation over a fixed input corpus", + "statement": "The recorded command produced output.json (a sorted word-frequency table) and metrics.json (token-count metrics) from the declared input.txt; an explicit rerun can compare those output bytes.", + "scope": "A single, self-contained, dependency-free Python computation used to demonstrate the VSTD generic receipt-carrying run workflow end-to-end: source -> inputs -> execution -> outputs -> claim -> receipt -> separate validation -> reproduction.", "limitations": [ - "This is a deliberately small worked example chosen for zero external dependencies and full determinism, not a claim about any production model, dataset, or benchmark.", - "Determinism is declared only for this exact recorded Python version and platform; the computation performs no floating point and no hash-order-dependent operations, so cross-run determinism is expected but not independently proven for other environments.", + "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." ], - "falsification_condition": "If `verifiable reproduce --rerun` regenerates output.json/metrics.json with a different SHA-256 digest than recorded, or `verifiable validate` finds the recomputed canonical_digest does not match receipt.json's recorded canonical_digest, this claim is falsified." + "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." }, "command": ["python", "compute.py", "input.txt", "output.json", "metrics.json"], "cwd": ".", "repo_dir": "../..", - "target_name": "verifiable-generic-run-example", - "portable_repository_id": "github.com/TimeLordRaps/Verifiable", + "target_name": "vstd-generic-run-example", + "portable_repository_id": "github.com/TimeLordRaps/verifier", "inputs": [ {"path": "input.txt", "role": "primary_input"}, {"path": "compute.py", "role": "entrypoint_source"} @@ -24,7 +24,7 @@ {"path": "output.json", "role": "primary_output"}, {"path": "metrics.json", "role": "metrics"} ], - "determinism_declared": "DETERMINISTIC", + "determinism_declared": "UNKNOWN", "seed": null, "evaluator_claims": [ { diff --git a/src/verifier/core/run.py b/src/verifier/core/run.py index 9aee4cb..3e40e30 100644 --- a/src/verifier/core/run.py +++ b/src/verifier/core/run.py @@ -1,22 +1,22 @@ -"""Generic proof-carrying computational run capture for VSTD. +"""Terminology: JavaScript Object Notation (JSON); Boolean satisfiability problem (SAT); +Secure Hash Algorithm 256-bit (SHA-256); Verifier Standard (VSTD); +YAML Ain't Markup Language (YAML). + +Generic computational run receipt capture for VSTD-1. This module implements the smallest working version of the "wrap any consequential computation and get a receipt" primitive described in the VSTD program graph. -It deliberately reuses the existing VSTD-0.1 canonicalization/digest machinery -(``verifier.core.receipt``), provenance discovery (``verifier.core.provenance``), -and reproducibility taxonomy (``verifier.core.reproducibility``) rather than -introducing a parallel schema. No new standard version is declared here — this is -an implementation living under ``schema_version = "VSTD-0.1"`` with a distinct -``receipt_kind`` discriminator (``generic_computational_run``) so existing -``VstdReceipt`` (SAT/derivation-shaped, ``receipt_kind = "claim_verification"``) -and ``VstdDataReceipt`` (dataset-provenance-shaped) documents are untouched. +It reuses the VSTD-1 receipt canonicalization, provenance discovery, and +reproducibility taxonomy rather than introducing a parallel schema. The required +``receipt_kind`` discriminator ``generic_computational_run`` distinguishes this +profile from VSTD-1 claim-mechanics receipts. Design commitments (do not weaken without updating tests + docs): 1. **Claims are not flattened.** "The command exited 0", "the declared output files exist with these digests", "an evaluator computed this metric", "the run's inputs trace to a provenance root", and "an external party reported a score" are five - different, independently falsifiable statements. They are recorded as five + different, separately falsifiable statements. They are recorded as five distinct fields under :class:`RunClaims`, never collapsed into one boolean. 2. **Fail closed.** A missing declared input aborts the run *before* executing the command (no fabricated "it probably would have worked"). A missing declared @@ -25,13 +25,15 @@ commands are accepted, closing off the shell-indirection attack class. 3. **External evaluation is never auto-promoted.** If a manifest declares that an organizer/leaderboard reported a score, that is stored as an - :class:`ExternalEvaluationEvidence` record with ``attested=False`` unless the - manifest itself supplies a checkable evidence reference. Its presence never - flips ``execution_completed`` or any other locally-checked claim to true. + :class:`ExternalEvaluationEvidence` record with ``attested=False``. A supplied + evidence reference is recorded but not dereferenced or verified by this runtime. + Its presence never flips any locally checked claim to true. 4. **Reproduction fidelity is classified, not asserted.** Rehashing on-disk output artifacts (always available, side-effect free) is distinguished from re-running - the recorded command (only performed when explicitly requested via ``rerun``), - and nondeterministic runs are never permitted to claim ``BITWISE_IDENTICAL``. + the recorded command (only performed when explicitly requested via ``rerun``). + The generic rerun compares the declared output bytes and execution outcome, so + it can establish only scoped ``CONTENT_IDENTICAL``; a determinism declaration + earns no reproduction-fidelity state. """ from __future__ import annotations @@ -43,7 +45,6 @@ import time from dataclasses import dataclass from datetime import datetime, timezone -from enum import Enum from pathlib import Path from typing import Any, Mapping, Optional @@ -54,42 +55,62 @@ ) from verifier.core.receipt import compute_canonical_digest from verifier.core.reproducibility import ReproducibilityLevel - -RUN_SCHEMA_VERSION = "VSTD-0.1" -RUN_RECEIPT_KIND = "generic_computational_run" +from verifier.core.run_support import ( + RUN_RECEIPT_KIND, + RUN_SCHEMA_VERSION, + DeterminismDeclaration, + RunError, + RunOutcome, +) +from verifier.core.run_planning import _validated_command, describe_run_plan, load_manifest _SNIPPET_LIMIT = 4000 _DEFAULT_TIMEOUT_SECONDS = 300 -def _digest_if_available(path: Path, label: str) -> str: +def _digest_if_available(path: Path, label: str, *alternatives: Path) -> str: + for candidate in (path, *alternatives): + try: + return "sha256:" + hashlib.sha256(candidate.read_bytes()).hexdigest() + except OSError: + continue + return f"UNAVAILABLE:{label}" + + +def _implementation_inventory_digest(paths: tuple[Path, ...]) -> str: + """Hash the named module bytes without collapsing producer/checker boundaries.""" + + digest = hashlib.sha256() try: - return "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest() + for path in paths: + payload = path.read_bytes() + digest.update(path.name.encode("utf-8") + b"\0") + digest.update(len(payload).to_bytes(8, "big")) + digest.update(payload) except OSError: - return f"UNAVAILABLE:{label}" + return "UNAVAILABLE:generic-run-module-inventory" + return "sha256:" + digest.hexdigest() -def _run_layer4_binding( +def _assessment_context( manifest: Mapping[str, Any], *, falsification_condition: str, ) -> dict[str, Any]: - """Bind the verifier, bounds, precommitment, and refutation surface. - - Historical generic-run receipts omit this block and retain their canonical - digests. New captures always include it, including explicit empty or - undeclared values; absence is evidence of a missing rung, not permission to - infer that a bound or precommitment existed. - """ + """Serialize bounded generic-run mechanism and refutation context.""" raw_bounds = manifest.get("resource_bounds", {}) if not isinstance(raw_bounds, Mapping): raise RunError("resource_bounds must be an object") - bounds: dict[str, int] = {} - for name in ( + bound_fields = ( "verification_cost_bound", "memory_bound", "certificate_size_bound", - ): + ) + unknown_bounds = sorted(set(raw_bounds) - set(bound_fields)) + if unknown_bounds: + raise RunError(f"resource_bounds has unknown fields: {', '.join(unknown_bounds)}") + bounds: dict[str, int] = {} + for name in bound_fields: value = raw_bounds.get(name, 0) if type(value) is not int or value < 0: raise RunError(f"resource_bounds.{name} must be a non-negative integer") @@ -101,16 +122,38 @@ def _run_layer4_binding( surface = dict(raw_surface or {}) surface.setdefault("admissible_refutations", []) surface.setdefault("excluded_claims", ["PHYSICAL_WORLD_COMPLETENESS"]) - surface.setdefault("legacy_falsification_condition", falsification_condition) + surface.setdefault("falsification_condition", falsification_condition) + for name in ("admissible_refutations", "excluded_claims"): + if not isinstance(surface[name], list) or not all( + isinstance(item, str) for item in surface[name] + ): + raise RunError(f"refutation_surface.{name} must be an array of strings") + if not isinstance(surface["falsification_condition"], str): + raise RunError("refutation_surface.falsification_condition must be a string") here = Path(__file__).resolve() + implementation_modules = tuple( + here.with_name(name) + for name in ( + "run.py", + "run_support.py", + "run_planning.py", + "run_validation.py", + "run_inspection.py", + "run_reproduction.py", + "run_impact.py", + ) + ) specification = here.parents[3] / "standard" / "VSTD-1.md" + packaged_specification = here.parents[1] / "specifications" / "VSTD-1.md" verifier = { "specification_hash": _digest_if_available( - specification, "standard/VSTD-1.md" + specification, "standard/VSTD-1.md", packaged_specification + ), + "implementation_hash": _implementation_inventory_digest(implementation_modules), + "parser_hash": _digest_if_available( + here.with_name("run_validation.py"), "core/run_validation.py" ), - "implementation_hash": _digest_if_available(here, "core/run.py"), - "parser_hash": _digest_if_available(here, "core/run.py"), "certificate_format": "VSTD1-GENERIC-RUN", "format_fragment": "CAPTURE,VALIDATE,REPRODUCE", "dependencies": ["python-stdlib"], @@ -124,25 +167,6 @@ def _run_layer4_binding( } -class RunError(RuntimeError): - """Raised for manifest or capture errors that must fail closed.""" - - -class RunOutcome(str, Enum): - COMPLETED = "COMPLETED" - NONZERO_EXIT = "NONZERO_EXIT" - MISSING_INPUT = "MISSING_INPUT" - MISSING_OUTPUT = "MISSING_OUTPUT" - TIMEOUT = "TIMEOUT" - EXCEPTION = "EXCEPTION" - - -class DeterminismDeclaration(str, Enum): - DETERMINISTIC = "DETERMINISTIC" - NONDETERMINISTIC = "NONDETERMINISTIC" - UNKNOWN = "UNKNOWN" - - def _now_utc() -> str: return datetime.now(timezone.utc).isoformat() @@ -237,7 +261,7 @@ class EvaluatorClaim: evaluator_name: str metric_name: str value: Any - computed_by: str # "local_reference_evaluator" | "declared_by_manifest_author" + computed_by: str # "bound_output_extraction" | "declared_by_manifest_author" verified_independently: bool def to_dict(self) -> dict[str, Any]: @@ -255,10 +279,10 @@ class ExternalEvaluationEvidence: """Explicit, bounded slot for organizer/third-party reported results. Presence of this record NEVER means the runtime cryptographically or - independently verified the external event described. ``attested`` - distinguishes a claim carrying real checkable evidence (a signature, a - linked artifact digest) from a bare unverifiable assertion. Default is - the least trusting classification. + verified the external event described through a separate mechanism or actor. + ``evidence_kind`` and + ``evidence_ref`` preserve what the manifest supplied; ``attested`` remains + false because this capture path does not dereference or verify that evidence. """ source: str @@ -281,17 +305,13 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_manifest(cls, d: Mapping[str, Any]) -> "ExternalEvaluationEvidence": evidence_kind = str(d.get("evidence_kind", "UNVERIFIED_ASSERTION")).upper() - # Fail closed: only LINKED_ARTIFACT/SIGNED_ATTESTATION with a concrete - # evidence_ref may claim attested=True. A bare assertion never can, - # regardless of what the manifest author writes in "attested". - attested = bool(d.get("attested", False)) and evidence_kind != "UNVERIFIED_ASSERTION" and bool(d.get("evidence_ref")) return cls( source=str(d.get("source", "unspecified")), description=str(d.get("description", "")), reported_value=d.get("reported_value"), evidence_kind=evidence_kind, evidence_ref=d.get("evidence_ref"), - attested=attested, + attested=False, ) @@ -355,7 +375,7 @@ def _resolve_provenance_linkage(base_dir: Path, root: Mapping[str, Any]) -> Prov class RunClaims: """Distinct, non-flattened claims a run receipt may make. - Each field is an independently falsifiable statement. They must never be + Each field is a separately falsifiable statement. They must never be collapsed into a single pass/fail boolean — see module docstring. """ @@ -392,7 +412,7 @@ class GenericRunReceipt: claims: RunClaims provenance_linkage: tuple[ProvenanceLinkage, ...] reproducibility: dict[str, Any] - layer4_binding: Optional[dict[str, Any]] = None + assessment_context: dict[str, Any] canonical_digest: str = "" def get_stable_payload(self) -> dict[str, Any]: @@ -440,9 +460,8 @@ def get_stable_payload(self) -> dict[str, Any]: "claims": self.claims.to_dict(), "provenance_linkage": [p.to_dict() for p in self.provenance_linkage], "reproducibility": self.reproducibility, + "assessment_context": self.assessment_context, } - if self.layer4_binding is not None: - payload["layer4_binding"] = self.layer4_binding return payload def compute_and_set_digest(self) -> str: @@ -472,9 +491,8 @@ def to_dict(self) -> dict[str, Any]: "claims": self.claims.to_dict(), "provenance_linkage": [p.to_dict() for p in self.provenance_linkage], "reproducibility": self.reproducibility, + "assessment_context": self.assessment_context, } - if self.layer4_binding is not None: - payload["layer4_binding"] = self.layer4_binding return payload def save_to_directory(self, out_dir: Path) -> Path: @@ -549,8 +567,8 @@ def generate_run_receipt_markdown(receipt: GenericRunReceipt) -> str: f"- **Reported value:** `{ext.reported_value}`\n" f"- **Evidence kind:** `{ext.evidence_kind}`\n" f"- **Evidence reference:** `{ext.evidence_ref}`\n" - f"- **Attested by the runtime:** `{ext.attested}` " - f"({'a checkable evidence reference backs this value' if ext.attested else 'this is an UNVERIFIED external assertion — recorded for bookkeeping only, NOT independently checked'})\n" + f"- **Verified by this runtime:** `{ext.attested}` " + "(the reference is recorded but not checked by a separate mechanism or actor)\n" ) else: external_md = "_(no external evaluation evidence declared — this run makes no claim about any external score, leaderboard, or organizer report)_" @@ -637,94 +655,19 @@ def generate_run_receipt_markdown(receipt: GenericRunReceipt) -> str: ## 8. Reproduction ```bash -vstd reproduce {receipt.receipt_id if False else ''} +vstd reproduce ``` Highest demonstrated reproduction fidelity: `{receipt.reproducibility.get("highest_demonstrated_level") or "NOT YET REPRODUCED"}`. -Declared supported ceiling (determinism-bounded): `{receipt.reproducibility.get("declared_ceiling")}`. +Declared supported ceiling (bundled mechanism): `{receipt.reproducibility.get("declared_ceiling")}`. --- -*Generated by VSTD Generic Run Runtime (VSTD-0.1, receipt_kind=generic_computational_run).* +*Generated by the VSTD-1 generic-run reference runtime +(`receipt_kind = generic_computational_run`).* """ -def load_manifest(manifest_path: Path) -> dict[str, Any]: - text = manifest_path.read_text(encoding="utf-8") - if manifest_path.suffix.lower() in (".yaml", ".yml"): - try: - import yaml # type: ignore[import-untyped] - except ImportError as exc: - raise RunError( - "YAML manifest support is optional; install verifier-standard[yaml] or use JSON" - ) from exc - data = yaml.safe_load(text) - else: - data = json.loads(text) - if not isinstance(data, dict): - raise RunError(f"Manifest at {manifest_path} must decode to a JSON/YAML object.") - return data - - -def _validated_command(manifest: Mapping[str, Any]) -> tuple[str, ...]: - command = manifest.get("command") - if not isinstance(command, list) or not command or not all(isinstance(c, str) for c in command): - raise RunError( - "manifest 'command' must be a non-empty list of strings (argv form). " - "String/shell commands are rejected to close off shell-indirection attacks." - ) - return tuple(command) - - -def describe_run_plan(manifest: Mapping[str, Any], manifest_dir: Path) -> dict[str, Any]: - """Return the observable execution and capture paths without executing them. - - This is a review aid, not a sandbox analysis. A subprocess may access resources - that are not named in a manifest, so the result deliberately says that the - command's effective access remains outside VSTD's observation boundary. - """ - - command = _validated_command(manifest) - root = manifest_dir.resolve() - - def path_record(path_value: Any) -> dict[str, Any]: - declared = str(path_value) - resolved = (root / declared).resolve() - try: - resolved.relative_to(root) - outside = False - except ValueError: - outside = True - return { - "declared": declared, - "resolved": str(resolved), - "outside_manifest_directory": outside, - } - - def artifacts(key: str) -> list[dict[str, Any]]: - result: list[dict[str, Any]] = [] - for entry in manifest.get(key, []): - if not isinstance(entry, Mapping) or "path" not in entry: - raise RunError(f"manifest '{key}' entries must be objects with a path") - record = path_record(entry["path"]) - record["role"] = str(entry.get("role", key[:-1])) - record["present_before_execution"] = Path(record["resolved"]).is_file() - result.append(record) - return result - - return { - "executes_without_sandbox": True, - "manifest_directory": str(root), - "command": list(command), - "cwd": path_record(manifest.get("cwd", ".")), - "repo_dir": path_record(manifest.get("repo_dir", ".")), - "inputs": artifacts("inputs"), - "outputs": artifacts("outputs"), - "observation_limit": ( - "Declared paths describe receipt capture only; they do not confine the " - "subprocess or enumerate everything it may access." - ), - } def capture_run( @@ -732,7 +675,7 @@ def capture_run( manifest_dir: Path, receipt_id: Optional[str] = None, ) -> GenericRunReceipt: - """Execute the manifest-declared command and capture a proof-carrying receipt. + """Execute the manifest-declared command and capture a computational run receipt. Fails closed (raises :class:`RunError`) on manifest shape errors that would otherwise silently under-specify the claim (non-list command, absent claim @@ -896,11 +839,11 @@ def capture_run( for key in [k for k in pointer.split(".") if k]: node = node[key] value = node - computed_by = "local_reference_evaluator" - verified_independently = True + computed_by = "bound_output_extraction" + verified_independently = False except Exception: value = None - computed_by = "local_reference_evaluator" + computed_by = "bound_output_extraction" verified_independently = False evaluator_claims.append( EvaluatorClaim( @@ -929,17 +872,8 @@ def capture_run( key_files=key_files, ) - supported_levels = [ - ReproducibilityLevel.CONTENT_IDENTICAL.value, - ReproducibilityLevel.EVIDENCE_EQUIVALENT.value, - ReproducibilityLevel.RESULT_EQUIVALENT.value, - ReproducibilityLevel.SEMANTIC_REPRODUCTION.value, - ] - if determinism == DeterminismDeclaration.DETERMINISTIC.value: - supported_levels.insert(0, ReproducibilityLevel.BITWISE_IDENTICAL.value) - ceiling = ReproducibilityLevel.BITWISE_IDENTICAL.value - else: - ceiling = ReproducibilityLevel.CONTENT_IDENTICAL.value + supported_levels = [ReproducibilityLevel.CONTENT_IDENTICAL.value] + ceiling = ReproducibilityLevel.CONTENT_IDENTICAL.value receipt = GenericRunReceipt( schema_version=RUN_SCHEMA_VERSION, @@ -968,7 +902,7 @@ def capture_run( "supported_levels": supported_levels, "reproduction_command": "vstd reproduce ", }, - layer4_binding=_run_layer4_binding( + assessment_context=_assessment_context( manifest, falsification_condition=str( claim_block.get("falsification_condition", "") @@ -979,234 +913,15 @@ def capture_run( return receipt -def _rebuild_stable_payload_from_dict(data: Mapping[str, Any]) -> dict[str, Any]: - src = data.get("source_state", {}) - payload = { - "schema_version": data.get("schema_version"), - "receipt_kind": data.get("receipt_kind"), - "receipt_id": data.get("receipt_id"), - "claim_title": data.get("claim_title"), - "claim_statement": data.get("claim_statement"), - "claim_scope": data.get("claim_scope"), - "claim_limitations": data.get("claim_limitations"), - "falsification_condition": data.get("falsification_condition"), - "source_state_stable": { - "target_name": src.get("target_name"), - "portable_repository_id": src.get("portable_repository_id"), - "git_commit_sha": src.get("git", {}).get("commit_sha"), - "git_branch": src.get("git", {}).get("branch"), - "git_is_dirty": src.get("git", {}).get("is_dirty"), - "git_dirty_files": src.get("git", {}).get("dirty_files", []), - "source_file_hashes": src.get("source_file_hashes", {}), - "runtime_python_version": src.get("runtime", {}).get("python_version"), - }, - "inputs": data.get("inputs", []), - "outputs": data.get("outputs", []), - "execution_stable": { - "command": data.get("execution", {}).get("command"), - "cwd": data.get("execution", {}).get("cwd"), - "exit_code": data.get("execution", {}).get("exit_code"), - "outcome": data.get("execution", {}).get("outcome"), - "python_version": data.get("execution", {}).get("python_version"), - "platform_system": data.get("execution", {}).get("platform_system"), - "determinism_declared": data.get("execution", {}).get("determinism_declared"), - "seed_declared": data.get("execution", {}).get("seed_declared"), - "stdout_sha256": data.get("execution", {}).get("stdout_sha256"), - "stderr_sha256": data.get("execution", {}).get("stderr_sha256"), - }, - "claims": data.get("claims", {}), - "provenance_linkage": data.get("provenance_linkage", []), - "reproducibility": data.get("reproducibility", {}), - } - if "layer4_binding" in data: - payload["layer4_binding"] = data.get("layer4_binding") - return payload - - -def is_generic_run_receipt(data: Mapping[str, Any]) -> bool: - return data.get("receipt_kind") == RUN_RECEIPT_KIND - - -def validate_run_receipt(receipt_path_or_dir: Path) -> int: - receipt_file = receipt_path_or_dir / "receipt.json" if receipt_path_or_dir.is_dir() else receipt_path_or_dir - if not receipt_file.exists(): - print(f"[FAIL] Receipt file not found: {receipt_file}") - return 1 - data = json.loads(receipt_file.read_text(encoding="utf-8")) - recorded_digest = data.get("canonical_digest", "") - recomputed = compute_canonical_digest(_rebuild_stable_payload_from_dict(data)) - if recomputed != recorded_digest: - print(f"[FAIL] Canonical digest mismatch:\n Recorded: {recorded_digest}\n Recomputed: {recomputed}") - return 1 - print(f"[PASS] Run receipt {data.get('receipt_id')} is valid.") - print(f" Digest: {recorded_digest}") - print(f" Outcome: {data.get('execution', {}).get('outcome')}") - return 0 - - -def inspect_run_receipt(receipt_path_or_dir: Path) -> int: - receipt_file = receipt_path_or_dir / "receipt.json" if receipt_path_or_dir.is_dir() else receipt_path_or_dir - if not receipt_file.exists(): - print(f"Error: receipt not found at {receipt_file}") - return 1 - data = json.loads(receipt_file.read_text(encoding="utf-8")) - print("=" * 70) - print(f"GENERIC RUN RECEIPT: {data.get('receipt_id')} ({data.get('schema_version')}/{data.get('receipt_kind')})") - print("=" * 70) - print(f"Canonical Digest: {data.get('canonical_digest')}") - print(f"Claim: {data.get('claim_statement')}") - ex = data.get("execution", {}) - print(f"Command: {' '.join(ex.get('command', []))}") - print(f"Outcome: {ex.get('outcome')} (exit={ex.get('exit_code')})") - c = data.get("claims", {}) - print("-" * 70) - print("CLAIMS (distinct, not flattened):") - print(f" execution_completed: {c.get('execution_completed')}") - print(f" output_digests_recorded: {c.get('output_digests_recorded')}") - print(f" all_declared_artifacts_present: {c.get('all_declared_artifacts_present')}") - ext = c.get("external_evaluation") - if ext: - print(f" external_evaluation: reported={ext.get('reported_value')} attested={ext.get('attested')}") - else: - print(" external_evaluation: (none declared)") - print("=" * 70) - return 0 - - -def reproduce_run_receipt(receipt_path_or_dir: Path, rerun: bool = False) -> int: - """Assess reproduction fidelity. - - By default this rehashes the declared output artifacts as they currently - exist on disk relative to the receipt directory's manifest base (safe, - side-effect free, always available). Pass ``rerun=True`` to additionally - re-execute the recorded command and compare freshly produced outputs — - this mutates on-disk state at the declared output paths and is therefore - opt-in only. - """ - receipt_dir = receipt_path_or_dir if receipt_path_or_dir.is_dir() else receipt_path_or_dir.parent - receipt_file = receipt_dir / "receipt.json" - if not receipt_file.exists(): - print(f"Error: receipt not found at {receipt_file}") - return 1 - data = json.loads(receipt_file.read_text(encoding="utf-8")) - # Inputs/outputs in the receipt are recorded as paths relative to the manifest's - # own directory. The convention this runtime uses (see `vstd run`) is that - # a receipt directory colocates receipt.json with a copy of the originating - # manifest (manifest.source.json), so that directory is also the correct base - # for resolving those relative paths during reproduction. - base_dir = receipt_dir - - determinism = data.get("execution", {}).get("determinism_declared") - - if rerun: - manifest_path = base_dir / "manifest.source.json" - if not manifest_path.exists(): - manifest_path = base_dir / "manifest.json" - if not manifest_path.exists(): - print(f"[WARN] No source manifest found under {base_dir}; cannot rerun. Falling back to artifact rehash.") - rerun = False - else: - manifest = load_manifest(manifest_path) - reproduced = capture_run(manifest, manifest_dir=base_dir, receipt_id=data.get("receipt_id")) - original_outcome = data.get("execution", {}).get("outcome") - reproduced_outcome = reproduced.execution.outcome - outputs_match = all( - o.sha256 == next((x.get("sha256") for x in data.get("outputs", []) if x.get("path") == o.path), None) - for o in reproduced.outputs - ) - if determinism == DeterminismDeclaration.DETERMINISTIC.value and outputs_match and original_outcome == reproduced_outcome: - level = ReproducibilityLevel.BITWISE_IDENTICAL - elif outputs_match and original_outcome == reproduced_outcome: - level = ReproducibilityLevel.CONTENT_IDENTICAL - elif original_outcome == reproduced_outcome: - level = ReproducibilityLevel.RESULT_EQUIVALENT - else: - level = ReproducibilityLevel.SEMANTIC_REPRODUCTION - print(f"[REPRODUCTION RESULT - RERUN] Level: {level.value}") - print(f" Original outcome: {original_outcome}") - print(f" Reproduced outcome: {reproduced_outcome}") - print(f" Outputs match: {outputs_match}") - return 0 if outputs_match and original_outcome == reproduced_outcome else 1 - - # Default path: rehash on-disk artifacts only (no execution). - mismatches: list[tuple[Any, Any, Optional[str]]] = [] - checked = 0 - for out in data.get("outputs", []): - recorded_hash = out.get("sha256") - path = base_dir / out["path"] - if not path.exists(): - mismatches.append((out["path"], recorded_hash, None)) - continue - checked += 1 - current_hash = sha256_file(path) - if current_hash != recorded_hash: - mismatches.append((out["path"], recorded_hash, current_hash)) - - if not data.get("outputs"): - print("[REPRODUCTION RESULT - ARTIFACT REHASH] No outputs were declared; nothing to compare.") - return 0 - - if mismatches: - print(f"[REPRODUCTION RESULT - ARTIFACT REHASH] MISMATCH ({len(mismatches)} of {len(data.get('outputs', []))} outputs)") - for path, recorded, current in mismatches: - print(f" {path}: recorded={recorded} current={current}") - return 1 - - print(f"[REPRODUCTION RESULT - ARTIFACT REHASH] All {checked} on-disk output artifact(s) match recorded digests.") - print(f" Reproduction level: {ReproducibilityLevel.CONTENT_IDENTICAL.value} (artifact-level; command was not re-executed - pass --rerun for a full rerun comparison)") - return 0 - - -def compute_blast_radius_impacted_artifacts(dataset_receipt_file: Path, revoked_artifact_id: str) -> set[str]: - """Forward blast radius of a revoked/invalidated artifact, plus the artifact itself. - - Reuses ``ProvenanceHypergraph.blast_radius`` from the existing VSTD-Graph-1 - runtime rather than reimplementing graph traversal here. - """ - from verifier.data.models import ProvenanceHypergraph - - data = json.loads(dataset_receipt_file.read_text(encoding="utf-8")) - hg = ProvenanceHypergraph.from_dict(data["hypergraph"]) - if revoked_artifact_id not in hg.artifacts: - raise RunError(f"Artifact '{revoked_artifact_id}' not found in {dataset_receipt_file}.") - affected = set(hg.blast_radius(revoked_artifact_id)) - affected.add(revoked_artifact_id) - return affected - - -def find_run_receipts_impacted_by_revocation( - search_root: Path, - dataset_receipt_file: Path, - revoked_artifact_id: str, -) -> list[dict[str, Any]]: - """Answer: "which recorded runs need to be reconsidered because this upstream - dataset-provenance artifact changed or became invalid?" - - Scans ``search_root`` recursively for ``receipt.json`` files that are generic - run receipts (``receipt_kind == generic_computational_run``) and whose - ``provenance_linkage`` references an artifact inside the forward blast radius - of ``revoked_artifact_id`` (or the artifact itself). This composes the - dataset-provenance hypergraph directly into run-receipt impact analysis - instead of introducing a parallel lineage system. - """ - impacted_artifacts = compute_blast_radius_impacted_artifacts(dataset_receipt_file, revoked_artifact_id) - results: list[dict[str, Any]] = [] - for receipt_file in search_root.rglob("receipt.json"): - try: - data = json.loads(receipt_file.read_text(encoding="utf-8")) - except Exception: - continue - if not is_generic_run_receipt(data): - continue - for link in data.get("provenance_linkage", []): - if link.get("artifact_id") in impacted_artifacts: - results.append( - { - "receipt_path": str(receipt_file), - "receipt_id": data.get("receipt_id"), - "matched_artifact_id": link.get("artifact_id"), - "claim_statement": data.get("claim_statement"), - } - ) - break - return results +# Compatibility facade: historical imports continue to resolve from verifier.core.run. +from verifier.core.run_impact import ( + compute_blast_radius_impacted_artifacts, + find_run_receipts_impacted_by_revocation, +) +from verifier.core.run_inspection import inspect_run_receipt +from verifier.core.run_reproduction import reproduce_run_receipt +from verifier.core.run_validation import ( + _rebuild_stable_payload_from_dict, + is_generic_run_receipt, + validate_run_receipt, +) diff --git a/src/verifier/core/run_impact.py b/src/verifier/core/run_impact.py new file mode 100644 index 0000000..4ec1260 --- /dev/null +++ b/src/verifier/core/run_impact.py @@ -0,0 +1,68 @@ +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Recorded-ancestry impact analysis for generic-run receipts. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from verifier.core.run_support import RunError +from verifier.core.run_validation import is_generic_run_receipt + + +def compute_blast_radius_impacted_artifacts(dataset_receipt_file: Path, revoked_artifact_id: str) -> set[str]: + """Forward blast radius of a revoked/invalidated artifact, plus the artifact itself. + + Reuses ``ProvenanceHypergraph.blast_radius`` from the existing VSTD-Graph-1 + runtime rather than reimplementing graph traversal here. + """ + from verifier.data.models import ProvenanceHypergraph + + data = json.loads(dataset_receipt_file.read_text(encoding="utf-8")) + hg = ProvenanceHypergraph.from_dict(data["hypergraph"]) + if revoked_artifact_id not in hg.artifacts: + raise RunError(f"Artifact '{revoked_artifact_id}' not found in {dataset_receipt_file}.") + affected = set(hg.blast_radius(revoked_artifact_id)) + affected.add(revoked_artifact_id) + return affected + + +def find_run_receipts_impacted_by_revocation( + search_root: Path, + dataset_receipt_file: Path, + revoked_artifact_id: str, +) -> list[dict[str, Any]]: + """Answer: "which recorded runs need to be reconsidered because this upstream + dataset-provenance artifact changed or became invalid?" + + Scans ``search_root`` recursively for ``receipt.json`` files that are generic + run receipts (``receipt_kind == generic_computational_run``) and whose + ``provenance_linkage`` references an artifact inside the forward blast radius + of ``revoked_artifact_id`` (or the artifact itself). This composes the + dataset-provenance hypergraph directly into run-receipt impact analysis + instead of introducing a parallel lineage system. + """ + impacted_artifacts = compute_blast_radius_impacted_artifacts(dataset_receipt_file, revoked_artifact_id) + results: list[dict[str, Any]] = [] + for receipt_file in search_root.rglob("receipt.json"): + try: + data = json.loads(receipt_file.read_text(encoding="utf-8")) + except Exception: + continue + if not is_generic_run_receipt(data): + continue + for link in data.get("provenance_linkage", []): + if link.get("artifact_id") in impacted_artifacts: + results.append( + { + "receipt_path": str(receipt_file), + "receipt_id": data.get("receipt_id"), + "matched_artifact_id": link.get("artifact_id"), + "claim_statement": data.get("claim_statement"), + } + ) + break + return results diff --git a/src/verifier/core/run_inspection.py b/src/verifier/core/run_inspection.py new file mode 100644 index 0000000..110bb74 --- /dev/null +++ b/src/verifier/core/run_inspection.py @@ -0,0 +1,57 @@ +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Human-readable inspection of structurally valid generic-run receipts. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Mapping + +from verifier.core.run_validation import _run_payload_errors + + +def inspect_run_receipt(receipt_path_or_dir: Path) -> int: + receipt_file = receipt_path_or_dir / "receipt.json" if receipt_path_or_dir.is_dir() else receipt_path_or_dir + if not receipt_file.exists(): + print(f"Error: receipt not found at {receipt_file}") + return 1 + try: + data = json.loads(receipt_file.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + print(f"[FAIL] Receipt is not readable JSON: {exc}") + return 1 + if not isinstance(data, Mapping): + print("[FAIL] Receipt root must be an object") + return 1 + errors = _run_payload_errors(data) + if errors: + for error in errors: + print(f"[FAIL] {error}") + return 1 + print("=" * 70) + print(f"GENERIC RUN RECEIPT: {data.get('receipt_id')} ({data.get('schema_version')}/{data.get('receipt_kind')})") + print("=" * 70) + print(f"Canonical Digest: {data.get('canonical_digest')}") + print(f"Claim: {data.get('claim_statement')}") + ex = data.get("execution", {}) + print(f"Command: {' '.join(ex.get('command', []))}") + print(f"Outcome: {ex.get('outcome')} (exit={ex.get('exit_code')})") + c = data.get("claims", {}) + print("-" * 70) + print("CLAIMS (distinct, not flattened):") + print(f" execution_completed: {c.get('execution_completed')}") + print(f" output_digests_recorded: {c.get('output_digests_recorded')}") + print(f" all_declared_artifacts_present: {c.get('all_declared_artifacts_present')}") + ext = c.get("external_evaluation") + if ext: + print( + " external_evaluation: " + f"reported={ext.get('reported_value')} recorded_attested={ext.get('attested')} " + "(not verified by inspect)" + ) + else: + print(" external_evaluation: (none declared)") + print("=" * 70) + return 0 diff --git a/src/verifier/core/run_planning.py b/src/verifier/core/run_planning.py new file mode 100644 index 0000000..44b40c8 --- /dev/null +++ b/src/verifier/core/run_planning.py @@ -0,0 +1,90 @@ +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD); YAML Ain't Markup Language (YAML). + +Side-effect-free generic-run manifest loading and execution-plan description. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Mapping + +from verifier.core.run_support import RunError + + +def load_manifest(manifest_path: Path) -> dict[str, Any]: + text = manifest_path.read_text(encoding="utf-8") + if manifest_path.suffix.lower() in (".yaml", ".yml"): + try: + import yaml # type: ignore[import-untyped] + except ImportError as exc: + raise RunError( + "YAML manifest support is optional; install verifier-standard[yaml] or use JSON" + ) from exc + data = yaml.safe_load(text) + else: + data = json.loads(text) + if not isinstance(data, dict): + raise RunError(f"Manifest at {manifest_path} must decode to a JSON/YAML object.") + return data + + +def _validated_command(manifest: Mapping[str, Any]) -> tuple[str, ...]: + command = manifest.get("command") + if not isinstance(command, list) or not command or not all(isinstance(c, str) for c in command): + raise RunError( + "manifest 'command' must be a non-empty list of strings (argv form). " + "String/shell commands are rejected to close off shell-indirection attacks." + ) + return tuple(command) + + +def describe_run_plan(manifest: Mapping[str, Any], manifest_dir: Path) -> dict[str, Any]: + """Return the observable execution and capture paths without executing them. + + This is a review aid, not a sandbox analysis. A subprocess may access resources + that are not named in a manifest, so the result deliberately says that the + command's effective access remains outside VSTD's observation boundary. + """ + + command = _validated_command(manifest) + root = manifest_dir.resolve() + + def path_record(path_value: Any) -> dict[str, Any]: + declared = str(path_value) + resolved = (root / declared).resolve() + try: + resolved.relative_to(root) + outside = False + except ValueError: + outside = True + return { + "declared": declared, + "resolved": str(resolved), + "outside_manifest_directory": outside, + } + + def artifacts(key: str) -> list[dict[str, Any]]: + result: list[dict[str, Any]] = [] + for entry in manifest.get(key, []): + if not isinstance(entry, Mapping) or "path" not in entry: + raise RunError(f"manifest '{key}' entries must be objects with a path") + record = path_record(entry["path"]) + record["role"] = str(entry.get("role", key[:-1])) + record["present_before_execution"] = Path(record["resolved"]).is_file() + result.append(record) + return result + + return { + "executes_without_sandbox": True, + "manifest_directory": str(root), + "command": list(command), + "cwd": path_record(manifest.get("cwd", ".")), + "repo_dir": path_record(manifest.get("repo_dir", ".")), + "inputs": artifacts("inputs"), + "outputs": artifacts("outputs"), + "observation_limit": ( + "Declared paths describe receipt capture only; they do not confine the " + "subprocess or enumerate everything it may access." + ), + } diff --git a/src/verifier/core/run_reproduction.py b/src/verifier/core/run_reproduction.py new file mode 100644 index 0000000..b3ffccd --- /dev/null +++ b/src/verifier/core/run_reproduction.py @@ -0,0 +1,120 @@ +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Opt-in rerun and side-effect-free artifact-rehash reproduction assessment. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Mapping, Optional + +from verifier.core.provenance import sha256_file +from verifier.core.reproducibility import ReproducibilityLevel +from verifier.core.run_planning import load_manifest +from verifier.core.run_validation import _run_payload_errors + + +def reproduce_run_receipt(receipt_path_or_dir: Path, rerun: bool = False) -> int: + """Assess reproduction fidelity. + + By default this rehashes the declared output artifacts as they currently + exist on disk relative to the receipt directory's manifest base (safe, + side-effect free, always available). Pass ``rerun=True`` to additionally + re-execute the recorded command and compare freshly produced outputs — + this mutates on-disk state at the declared output paths and is therefore + opt-in only. + """ + receipt_file = ( + receipt_path_or_dir / "receipt.json" + if receipt_path_or_dir.is_dir() + else receipt_path_or_dir + ) + receipt_dir = receipt_file.parent + if not receipt_file.exists(): + print(f"Error: receipt not found at {receipt_file}") + return 1 + try: + data = json.loads(receipt_file.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + print(f"[FAIL] Receipt is not readable JSON: {exc}") + return 1 + if not isinstance(data, Mapping): + print("[FAIL] Receipt root must be an object") + return 1 + errors = _run_payload_errors(data) + if errors: + for error in errors: + print(f"[FAIL] {error}") + return 1 + # Inputs/outputs in the receipt are recorded as paths relative to the manifest's + # own directory. The convention this runtime uses (see `vstd run`) is that + # a receipt directory colocates receipt.json with a copy of the originating + # manifest (manifest.source.json), so that directory is also the correct base + # for resolving those relative paths during reproduction. + base_dir = receipt_dir + + if rerun: + manifest_path = base_dir / "manifest.source.json" + if not manifest_path.exists(): + manifest_path = base_dir / "manifest.json" + if not manifest_path.exists(): + print(f"[WARN] No source manifest found under {base_dir}; cannot rerun. Falling back to artifact rehash.") + rerun = False + else: + from verifier.core.run import capture_run + + manifest = load_manifest(manifest_path) + reproduced = capture_run(manifest, manifest_dir=base_dir, receipt_id=data.get("receipt_id")) + original_outcome = data.get("execution", {}).get("outcome") + reproduced_outcome = reproduced.execution.outcome + original_outputs = { + str(item.get("path")): item.get("sha256") + for item in data.get("outputs", []) + } + reproduced_outputs = {item.path: item.sha256 for item in reproduced.outputs} + outputs_match = bool(original_outputs) and original_outputs == reproduced_outputs + outcomes_match = original_outcome == reproduced_outcome + fidelity_state = ( + ReproducibilityLevel.CONTENT_IDENTICAL.value + if outputs_match and outcomes_match + else "NOT_DEMONSTRATED" + ) + print( + "[REPRODUCTION RESULT - RERUN] " + f"Fidelity state: {fidelity_state} (declared-output scope)" + ) + print(f" Original outcome: {original_outcome}") + print(f" Reproduced outcome: {reproduced_outcome}") + print(f" Outputs match: {outputs_match}") + print(" Scope: declared output artifacts and execution outcome") + return 0 if outputs_match and outcomes_match else 1 + + # Default path: rehash on-disk artifacts only (no execution). + mismatches: list[tuple[Any, Any, Optional[str]]] = [] + checked = 0 + for out in data.get("outputs", []): + recorded_hash = out.get("sha256") + path = base_dir / out["path"] + if not path.exists(): + mismatches.append((out["path"], recorded_hash, None)) + continue + checked += 1 + current_hash = sha256_file(path) + if current_hash != recorded_hash: + mismatches.append((out["path"], recorded_hash, current_hash)) + + if not data.get("outputs"): + print("[REPRODUCTION RESULT - ARTIFACT REHASH] NOT_DEMONSTRATED: no outputs were declared.") + return 1 + + if mismatches: + print(f"[REPRODUCTION RESULT - ARTIFACT REHASH] MISMATCH ({len(mismatches)} of {len(data.get('outputs', []))} outputs)") + for path, recorded, current in mismatches: + print(f" {path}: recorded={recorded} current={current}") + return 1 + + print(f"[REPRODUCTION RESULT - ARTIFACT REHASH] All {checked} on-disk output artifact(s) match recorded digests.") + print(" Declared-output bytes: MATCH") + print(" Full-run reproduction: NOT_DEMONSTRATED (command was not re-executed; pass --rerun to assess it)") + return 0 diff --git a/src/verifier/core/run_support.py b/src/verifier/core/run_support.py new file mode 100644 index 0000000..fb6253e --- /dev/null +++ b/src/verifier/core/run_support.py @@ -0,0 +1,28 @@ +"""Shared internal coordinates for the Verifier Standard (VSTD) generic-run profile.""" + +from __future__ import annotations + +from enum import Enum + + +RUN_SCHEMA_VERSION = "VSTD-1" +RUN_RECEIPT_KIND = "generic_computational_run" + + +class RunError(RuntimeError): + """Manifest, capture, or impact error that must fail closed.""" + + +class RunOutcome(str, Enum): + COMPLETED = "COMPLETED" + NONZERO_EXIT = "NONZERO_EXIT" + MISSING_INPUT = "MISSING_INPUT" + MISSING_OUTPUT = "MISSING_OUTPUT" + TIMEOUT = "TIMEOUT" + EXCEPTION = "EXCEPTION" + + +class DeterminismDeclaration(str, Enum): + DETERMINISTIC = "DETERMINISTIC" + NONDETERMINISTIC = "NONDETERMINISTIC" + UNKNOWN = "UNKNOWN" diff --git a/src/verifier/core/run_validation.py b/src/verifier/core/run_validation.py new file mode 100644 index 0000000..8cb2303 --- /dev/null +++ b/src/verifier/core/run_validation.py @@ -0,0 +1,650 @@ +"""Terminology: JavaScript Object Notation (JSON); Secure Hash Algorithm 256-bit (SHA-256); Verifier Standard (VSTD). + +Fail-closed structural and canonical-digest validation for generic-run receipts. +""" + +from __future__ import annotations + +import json +from pathlib import Path +import re +from typing import Any, Mapping + +from verifier.core.receipt import compute_canonical_digest +from verifier.core.run_support import ( + RUN_RECEIPT_KIND, + RUN_SCHEMA_VERSION, + DeterminismDeclaration, + RunOutcome, +) + + +_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") + + +def _rebuild_stable_payload_from_dict(data: Mapping[str, Any]) -> dict[str, Any]: + src = data.get("source_state", {}) + payload = { + "schema_version": data.get("schema_version"), + "receipt_kind": data.get("receipt_kind"), + "receipt_id": data.get("receipt_id"), + "claim_title": data.get("claim_title"), + "claim_statement": data.get("claim_statement"), + "claim_scope": data.get("claim_scope"), + "claim_limitations": data.get("claim_limitations"), + "falsification_condition": data.get("falsification_condition"), + "source_state_stable": { + "target_name": src.get("target_name"), + "portable_repository_id": src.get("portable_repository_id"), + "git_commit_sha": src.get("git", {}).get("commit_sha"), + "git_branch": src.get("git", {}).get("branch"), + "git_is_dirty": src.get("git", {}).get("is_dirty"), + "git_dirty_files": src.get("git", {}).get("dirty_files", []), + "source_file_hashes": src.get("source_file_hashes", {}), + "runtime_python_version": src.get("runtime", {}).get("python_version"), + }, + "inputs": data.get("inputs", []), + "outputs": data.get("outputs", []), + "execution_stable": { + "command": data.get("execution", {}).get("command"), + "cwd": data.get("execution", {}).get("cwd"), + "exit_code": data.get("execution", {}).get("exit_code"), + "outcome": data.get("execution", {}).get("outcome"), + "python_version": data.get("execution", {}).get("python_version"), + "platform_system": data.get("execution", {}).get("platform_system"), + "determinism_declared": data.get("execution", {}).get("determinism_declared"), + "seed_declared": data.get("execution", {}).get("seed_declared"), + "stdout_sha256": data.get("execution", {}).get("stdout_sha256"), + "stderr_sha256": data.get("execution", {}).get("stderr_sha256"), + }, + "claims": data.get("claims", {}), + "provenance_linkage": data.get("provenance_linkage", []), + "reproducibility": data.get("reproducibility", {}), + "assessment_context": data.get("assessment_context", {}), + } + return payload + + +def _missing_fields( + value: object, + label: str, + required: tuple[str, ...], + errors: list[str], +) -> Mapping[str, Any] | None: + if not isinstance(value, Mapping): + errors.append(f"{label} must be an object") + return None + missing = [name for name in required if name not in value] + if missing: + errors.append(f"{label} missing required fields: {', '.join(missing)}") + return value + + +def _unexpected_fields( + value: Mapping[str, Any], label: str, allowed: tuple[str, ...], errors: list[str] +) -> None: + unexpected = sorted(set(value) - set(allowed)) + if unexpected: + errors.append(f"{label} has unexpected fields: {', '.join(unexpected)}") + + +def _run_payload_errors(data: Mapping[str, Any]) -> list[str]: + """Fail-closed structural checks for the generic-run wire profile.""" + + errors: list[str] = [] + required = ( + "schema_version", + "receipt_kind", + "receipt_id", + "canonical_digest", + "claim_title", + "claim_statement", + "claim_scope", + "claim_limitations", + "falsification_condition", + "source_state", + "inputs", + "outputs", + "execution", + "claims", + "provenance_linkage", + "reproducibility", + "assessment_context", + ) + _missing_fields(data, "receipt", required, errors) + _unexpected_fields(data, "receipt", required, errors) + if data.get("schema_version") != RUN_SCHEMA_VERSION: + errors.append(f"schema_version must be {RUN_SCHEMA_VERSION}") + if data.get("receipt_kind") != RUN_RECEIPT_KIND: + errors.append(f"receipt_kind must be {RUN_RECEIPT_KIND}") + for name in ( + "receipt_id", + "claim_title", + "claim_statement", + "claim_scope", + "falsification_condition", + ): + if not isinstance(data.get(name), str): + errors.append(f"{name} must be a string") + digest = data.get("canonical_digest") + if not isinstance(digest, str) or not _SHA256_PATTERN.fullmatch(digest): + errors.append("canonical_digest must be 64 lowercase hexadecimal characters") + if not isinstance(data.get("claim_limitations"), list) or not all( + isinstance(item, str) for item in data.get("claim_limitations", []) + ): + errors.append("claim_limitations must be an array of strings") + + source = _missing_fields( + data.get("source_state"), + "source_state", + ( + "target_name", + "portable_repository_id", + "local_repository_path", + "git", + "runtime", + "captured_at_utc", + "command_executed", + "source_file_hashes", + ), + errors, + ) + if source is not None: + source_fields = ( + "target_name", + "portable_repository_id", + "local_repository_path", + "git", + "runtime", + "captured_at_utc", + "command_executed", + "source_file_hashes", + ) + _unexpected_fields(source, "source_state", source_fields, errors) + for name in ( + "target_name", + "portable_repository_id", + "local_repository_path", + "captured_at_utc", + "command_executed", + ): + if not isinstance(source.get(name), str): + errors.append(f"source_state.{name} must be a string") + source_hashes = source.get("source_file_hashes") + if not isinstance(source_hashes, Mapping) or not all( + isinstance(path, str) + and isinstance(digest, str) + and bool(_SHA256_PATTERN.fullmatch(digest)) + for path, digest in ( + source_hashes.items() if isinstance(source_hashes, Mapping) else () + ) + ): + errors.append("source_state.source_file_hashes must map paths to SHA-256 digests") + git = _missing_fields( + source.get("git"), + "source_state.git", + ("commit_sha", "branch", "is_dirty"), + errors, + ) + if git is not None: + git_fields = ( + "commit_sha", + "branch", + "is_dirty", + "dirty_files", + "untracked_files", + "remote_origin", + ) + _unexpected_fields(git, "source_state.git", git_fields, errors) + if not isinstance(git.get("commit_sha"), str) or not isinstance( + git.get("branch"), str + ): + errors.append("source_state.git commit_sha and branch must be strings") + if type(git.get("is_dirty")) is not bool: + errors.append("source_state.git.is_dirty must be a boolean") + for name in ("dirty_files", "untracked_files"): + if name in git and ( + not isinstance(git.get(name), list) + or not all(isinstance(item, str) for item in git.get(name, [])) + ): + errors.append(f"source_state.git.{name} must be an array of strings") + if "remote_origin" in git and not isinstance(git.get("remote_origin"), str): + errors.append("source_state.git.remote_origin must be a string") + runtime = _missing_fields( + source.get("runtime"), + "source_state.runtime", + ("python_version", "platform_system"), + errors, + ) + if runtime is not None and any( + not isinstance(runtime.get(name), str) + for name in ("python_version", "platform_system") + ): + errors.append("source_state.runtime required fields must be strings") + if runtime is not None: + runtime_fields = ( + "python_version", + "python_implementation", + "platform_system", + "platform_release", + "platform_machine", + "hostname_masked", + ) + _unexpected_fields(runtime, "source_state.runtime", runtime_fields, errors) + for name in runtime_fields: + if name in runtime and not isinstance(runtime.get(name), str): + errors.append(f"source_state.runtime.{name} must be a string") + + for collection_name in ("inputs", "outputs"): + collection = data.get(collection_name) + if not isinstance(collection, list): + errors.append(f"{collection_name} must be an array") + continue + for index, raw in enumerate(collection): + label = f"{collection_name}[{index}]" + item = _missing_fields(raw, label, ("path", "role", "present", "sha256", "byte_size"), errors) + if item is None: + continue + _unexpected_fields( + item, label, ("path", "role", "present", "sha256", "byte_size"), errors + ) + if not isinstance(item.get("path"), str) or not isinstance(item.get("role"), str): + errors.append(f"{label}.path and .role must be strings") + if type(item.get("present")) is not bool: + errors.append(f"{label}.present must be a boolean") + artifact_digest = item.get("sha256") + if artifact_digest is not None and ( + not isinstance(artifact_digest, str) + or not _SHA256_PATTERN.fullmatch(artifact_digest) + ): + errors.append(f"{label}.sha256 must be null or 64 lowercase hexadecimal characters") + byte_size = item.get("byte_size") + if byte_size is not None and (type(byte_size) is not int or byte_size < 0): + errors.append(f"{label}.byte_size must be null or a non-negative integer") + if item.get("present") is True and (artifact_digest is None or byte_size is None): + errors.append(f"{label} is present but lacks a digest or byte size") + + execution = _missing_fields( + data.get("execution"), + "execution", + ( + "command", + "cwd", + "started_at_utc", + "ended_at_utc", + "elapsed_ms", + "exit_code", + "outcome", + "python_version", + "platform_system", + "determinism_declared", + "seed_declared", + "stdout_sha256", + "stderr_sha256", + "stdout_snippet", + "stderr_snippet", + ), + errors, + ) + if execution is not None: + _unexpected_fields( + execution, + "execution", + ( + "command", + "cwd", + "started_at_utc", + "ended_at_utc", + "elapsed_ms", + "exit_code", + "outcome", + "python_version", + "platform_system", + "determinism_declared", + "seed_declared", + "stdout_sha256", + "stderr_sha256", + "stdout_snippet", + "stderr_snippet", + ), + errors, + ) + command = execution.get("command") + if not isinstance(command, list) or not command or not all(isinstance(arg, str) for arg in command): + errors.append("execution.command must be a non-empty array of strings") + if execution.get("outcome") not in {member.value for member in RunOutcome}: + errors.append("execution.outcome is not a recognized run outcome") + if execution.get("determinism_declared") not in { + member.value for member in DeterminismDeclaration + }: + errors.append("execution.determinism_declared is not recognized") + for name in ( + "cwd", + "started_at_utc", + "ended_at_utc", + "python_version", + "platform_system", + "stdout_snippet", + "stderr_snippet", + ): + if not isinstance(execution.get(name), str): + errors.append(f"execution.{name} must be a string") + elapsed = execution.get("elapsed_ms") + if isinstance(elapsed, bool) or not isinstance(elapsed, (int, float)) or elapsed < 0: + errors.append("execution.elapsed_ms must be a non-negative number") + exit_code = execution.get("exit_code") + if exit_code is not None and (type(exit_code) is not int): + errors.append("execution.exit_code must be an integer or null") + seed = execution.get("seed_declared") + if seed is not None and not isinstance(seed, str): + errors.append("execution.seed_declared must be a string or null") + for name in ("stdout_sha256", "stderr_sha256"): + value = execution.get(name) + if not isinstance(value, str) or not _SHA256_PATTERN.fullmatch(value): + errors.append(f"execution.{name} must be 64 lowercase hexadecimal characters") + + claims = _missing_fields( + data.get("claims"), + "claims", + ( + "execution_completed", + "output_digests_recorded", + "all_declared_artifacts_present", + "evaluator_claims", + "external_evaluation", + ), + errors, + ) + if claims is not None: + _unexpected_fields( + claims, + "claims", + ( + "execution_completed", + "output_digests_recorded", + "all_declared_artifacts_present", + "evaluator_claims", + "external_evaluation", + ), + errors, + ) + for name in ("execution_completed", "output_digests_recorded"): + if type(claims.get(name)) is not bool: + errors.append(f"claims.{name} must be a boolean") + if claims.get("all_declared_artifacts_present") is not None and type( + claims.get("all_declared_artifacts_present") + ) is not bool: + errors.append("claims.all_declared_artifacts_present must be a boolean or null") + evaluator_claims = claims.get("evaluator_claims") + if not isinstance(evaluator_claims, list): + errors.append("claims.evaluator_claims must be an array") + else: + evaluator_fields = ( + "evaluator_name", + "metric_name", + "value", + "computed_by", + "verified_independently", + ) + for index, raw in enumerate(evaluator_claims): + label = f"claims.evaluator_claims[{index}]" + evaluator = _missing_fields(raw, label, evaluator_fields, errors) + if evaluator is None: + continue + _unexpected_fields(evaluator, label, evaluator_fields, errors) + if not isinstance(evaluator.get("evaluator_name"), str) or not isinstance( + evaluator.get("metric_name"), str + ): + errors.append(f"{label} names must be strings") + if evaluator.get("computed_by") not in { + "bound_output_extraction", + "declared_by_manifest_author", + }: + errors.append(f"{label}.computed_by is not recognized") + if evaluator.get("verified_independently") is not False: + errors.append( + f"{label}.verified_independently must be false for this runtime" + ) + external = claims.get("external_evaluation") + if external is not None and not isinstance(external, Mapping): + errors.append("claims.external_evaluation must be an object or null") + elif isinstance(external, Mapping): + external_fields = ( + "source", + "description", + "reported_value", + "evidence_kind", + "evidence_ref", + "attested", + ) + _missing_fields(external, "claims.external_evaluation", external_fields, errors) + _unexpected_fields( + external, "claims.external_evaluation", external_fields, errors + ) + for name in ("source", "description", "evidence_kind"): + if not isinstance(external.get(name), str): + errors.append(f"claims.external_evaluation.{name} must be a string") + if external.get("evidence_ref") is not None and not isinstance( + external.get("evidence_ref"), str + ): + errors.append("claims.external_evaluation.evidence_ref must be a string or null") + if external.get("attested") is not False: + errors.append("claims.external_evaluation.attested must be false for this runtime") + + linkage = data.get("provenance_linkage") + if not isinstance(linkage, list): + errors.append("provenance_linkage must be an array") + else: + linkage_fields = ( + "dataset_receipt_path", + "artifact_id", + "found_in_hypergraph", + "ancestor_count", + "ancestor_ids", + ) + for index, raw in enumerate(linkage): + label = f"provenance_linkage[{index}]" + item = _missing_fields(raw, label, linkage_fields, errors) + if item is None: + continue + _unexpected_fields(item, label, linkage_fields, errors) + if not isinstance(item.get("dataset_receipt_path"), str) or not isinstance( + item.get("artifact_id"), str + ): + errors.append(f"{label} paths and identifiers must be strings") + if type(item.get("found_in_hypergraph")) is not bool: + errors.append(f"{label}.found_in_hypergraph must be a boolean") + count = item.get("ancestor_count") + if count is not None and (type(count) is not int or count < 0): + errors.append(f"{label}.ancestor_count must be a non-negative integer or null") + if not isinstance(item.get("ancestor_ids"), list) or not all( + isinstance(ancestor, str) for ancestor in item.get("ancestor_ids", []) + ): + errors.append(f"{label}.ancestor_ids must be an array of strings") + reproduction = _missing_fields( + data.get("reproducibility"), + "reproducibility", + ("highest_demonstrated_level", "declared_ceiling", "supported_levels", "reproduction_command"), + errors, + ) + if reproduction is not None: + reproduction_fields = ( + "highest_demonstrated_level", + "declared_ceiling", + "supported_levels", + "reproduction_command", + ) + _unexpected_fields(reproduction, "reproducibility", reproduction_fields, errors) + if reproduction.get("highest_demonstrated_level") is not None and not isinstance( + reproduction.get("highest_demonstrated_level"), str + ): + errors.append("reproducibility.highest_demonstrated_level must be a string or null") + if not isinstance(reproduction.get("declared_ceiling"), str) or not isinstance( + reproduction.get("reproduction_command"), str + ): + errors.append("reproducibility ceiling and command must be strings") + if not isinstance(reproduction.get("supported_levels"), list) or not all( + isinstance(level, str) for level in reproduction.get("supported_levels", []) + ): + errors.append("reproducibility.supported_levels must be an array of strings") + context = _missing_fields( + data.get("assessment_context"), + "assessment_context", + ("verifier", "resource_bounds", "prior_commitment", "refutation_surface"), + errors, + ) + if context is not None: + context_fields = ( + "verifier", + "resource_bounds", + "prior_commitment", + "refutation_surface", + ) + _unexpected_fields(context, "assessment_context", context_fields, errors) + verifier = _missing_fields( + context.get("verifier"), + "assessment_context.verifier", + ( + "specification_hash", + "implementation_hash", + "parser_hash", + "certificate_format", + "format_fragment", + "dependencies", + "deterministic", + ), + errors, + ) + if verifier is not None: + verifier_fields = ( + "specification_hash", + "implementation_hash", + "parser_hash", + "certificate_format", + "format_fragment", + "dependencies", + "deterministic", + ) + _unexpected_fields( + verifier, "assessment_context.verifier", verifier_fields, errors + ) + for name in ( + "specification_hash", + "implementation_hash", + "parser_hash", + ): + value = verifier.get(name) + unavailable_specification = ( + name == "specification_hash" + and isinstance(value, str) + and value.startswith("UNAVAILABLE:") + ) + if ( + not unavailable_specification + and ( + not isinstance(value, str) + or not re.fullmatch(r"sha256:[0-9a-f]{64}", value) + ) + ): + errors.append( + f"assessment_context.verifier.{name} must be a prefixed SHA-256 digest" + ) + for name in ("certificate_format", "format_fragment"): + if not isinstance(verifier.get(name), str): + errors.append(f"assessment_context.verifier.{name} must be a string") + if not isinstance(verifier.get("dependencies"), list) or not all( + isinstance(item, str) for item in verifier.get("dependencies", []) + ): + errors.append( + "assessment_context.verifier.dependencies must be an array of strings" + ) + if type(verifier.get("deterministic")) is not bool: + errors.append("assessment_context.verifier.deterministic must be a boolean") + bounds = _missing_fields( + context.get("resource_bounds"), + "assessment_context.resource_bounds", + ( + "verification_cost_bound", + "memory_bound", + "certificate_size_bound", + ), + errors, + ) + if bounds is not None: + bound_fields = ( + "verification_cost_bound", + "memory_bound", + "certificate_size_bound", + ) + _unexpected_fields( + bounds, "assessment_context.resource_bounds", bound_fields, errors + ) + for name in bound_fields: + value = bounds.get(name) + if type(value) is not int or value < 0: + errors.append( + f"assessment_context.resource_bounds.{name} must be a non-negative integer" + ) + if not isinstance(context.get("prior_commitment"), str): + errors.append("assessment_context.prior_commitment must be a string") + surface = _missing_fields( + context.get("refutation_surface"), + "assessment_context.refutation_surface", + ( + "admissible_refutations", + "excluded_claims", + "falsification_condition", + ), + errors, + ) + if surface is not None: + for name in ("admissible_refutations", "excluded_claims"): + if not isinstance(surface.get(name), list) or not all( + isinstance(item, str) for item in surface.get(name, []) + ): + errors.append( + f"assessment_context.refutation_surface.{name} must be an array of strings" + ) + if not isinstance(surface.get("falsification_condition"), str): + errors.append( + "assessment_context.refutation_surface.falsification_condition must be a string" + ) + return errors + + +def is_generic_run_receipt(data: Mapping[str, Any]) -> bool: + return ( + data.get("schema_version") == RUN_SCHEMA_VERSION + and data.get("receipt_kind") == RUN_RECEIPT_KIND + ) + + +def validate_run_receipt(receipt_path_or_dir: Path) -> int: + """Validate one generic-run receipt's required fields and stable canonical digest.""" + + receipt_file = receipt_path_or_dir / "receipt.json" if receipt_path_or_dir.is_dir() else receipt_path_or_dir + if not receipt_file.exists(): + 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: + print(f"[FAIL] Receipt is not readable JSON: {exc}") + return 1 + if not isinstance(data, Mapping): + print("[FAIL] Receipt root must be an object") + return 1 + errors = _run_payload_errors(data) + if errors: + for error in errors: + print(f"[FAIL] {error}") + return 1 + recorded_digest = data.get("canonical_digest", "") + recomputed = compute_canonical_digest(_rebuild_stable_payload_from_dict(data)) + if recomputed != recorded_digest: + print(f"[FAIL] Canonical digest mismatch:\n Recorded: {recorded_digest}\n Recomputed: {recomputed}") + return 1 + print(f"[INTEGRITY OK] Run receipt {data.get('receipt_id')} stable digest matches.") + print(f" Digest: {recorded_digest}") + print(f" Outcome: {data.get('execution', {}).get('outcome')}") + return 0 diff --git a/tests/test_generic_run.py b/tests/test_generic_run.py index 92bd0bb..6a69f2e 100644 --- a/tests/test_generic_run.py +++ b/tests/test_generic_run.py @@ -1,22 +1,29 @@ -"""Adversarial and lifecycle tests for the generic proof-carrying computational run +"""Terminology: Verifier Standard (VSTD). + +Adversarial and lifecycle tests for the generic computational run receipt primitive (`verifier.core.run`). Covers the acceptance-test flow (capture -> validate -> inspect -> reproduce) plus a hostile-scrutiny mini-corpus: tampered receipts, tampered outputs, missing declared inputs/outputs, shell-indirection rejection, non-promotable external evaluation -claims, and determinism-bounded reproduction ceilings. +claims, and mechanism-bounded reproduction ceilings. """ from __future__ import annotations +import copy +import hashlib import json +import subprocess import sys from pathlib import Path import pytest +from jsonschema import Draft202012Validator from verifier.core.run import ( RunError, + _rebuild_stable_payload_from_dict, capture_run, find_run_receipts_impacted_by_revocation, inspect_run_receipt, @@ -24,9 +31,17 @@ reproduce_run_receipt, validate_run_receipt, ) - -REPO_ROOT = Path(__file__).resolve().parents[1] - +from verifier.core.receipt import compute_canonical_digest +from verifier.data.models import ( + ArtifactNode, + ArtifactStatus, + ArtifactType, + HyperedgePort, + ProvenanceHypergraph, + TransformationHyperedge, + TransformationType, +) +from verifier.runtime.public_cli import _write_reproduction_bundle def _write_tiny_project(tmp_path: Path) -> Path: """A minimal deterministic project: script reads input.txt, writes output.json.""" @@ -43,6 +58,20 @@ def _write_tiny_project(tmp_path: Path) -> Path: return tmp_path +def test_digest_consistent_empty_generic_receipt_is_rejected(tmp_path, capsys): + receipt = {"receipt_kind": "generic_computational_run"} + receipt["canonical_digest"] = compute_canonical_digest( + _rebuild_stable_payload_from_dict(receipt) + ) + path = tmp_path / "receipt.json" + path.write_text(json.dumps(receipt), encoding="utf-8") + + assert validate_run_receipt(path) == 1 + output = capsys.readouterr().out + assert "[INTEGRITY OK]" not in output + assert "missing required fields" in output + + def _base_manifest() -> dict: return { "claim": { @@ -61,6 +90,41 @@ def _base_manifest() -> dict: } +def _write_data_receipt(tmp_path: Path) -> tuple[Path, str]: + """Write the smallest public graph fixture needed by linkage/blast-radius tests.""" + + graph = ProvenanceHypergraph() + for artifact_id in ("artifact:source", "artifact:derived"): + graph.add_artifact( + ArtifactNode( + artifact_id, + artifact_id, + ArtifactType.CORPUS, + "a" * 64, + status=ArtifactStatus.VALID, + ) + ) + graph.add_transformation( + TransformationHyperedge( + "transform:derive", + "derive", + TransformationType.EXTRACTION, + (HyperedgePort("artifact:source", "INPUT"),), + (HyperedgePort("artifact:derived", "OUTPUT"),), + {}, + {}, + {}, + ) + ) + receipt_file = tmp_path / "dataset-receipt" / "receipt.json" + receipt_file.parent.mkdir() + receipt_file.write_text( + json.dumps({"hypergraph": graph.to_dict()}), + encoding="utf-8", + ) + return receipt_file, "artifact:source" + + def test_full_lifecycle_capture_validate_inspect_reproduce(tmp_path, capsys): proj = _write_tiny_project(tmp_path) manifest = _base_manifest() @@ -81,29 +145,48 @@ def test_full_lifecycle_capture_validate_inspect_reproduce(tmp_path, capsys): assert (out_dir / "manifest.json").exists() is False # test manifest was never written to disk data = json.loads(receipt_file.read_text(encoding="utf-8")) + schema = json.loads( + (Path(__file__).resolve().parents[1] / "receipts" / "schema" / "vstd1_generic_run_receipt.json").read_text( + encoding="utf-8" + ) + ) + Draft202012Validator(schema).validate(data) assert is_generic_run_receipt(data) assert data["canonical_digest"] == receipt.canonical_digest - layer4 = data["layer4_binding"] - assert layer4["verifier"]["implementation_hash"].startswith("sha256:") - assert layer4["verifier"]["parser_hash"].startswith("sha256:") - assert layer4["resource_bounds"] == { + context = data["assessment_context"] + assert context["verifier"]["implementation_hash"].startswith("sha256:") + assert context["verifier"]["parser_hash"].startswith("sha256:") + assert context["resource_bounds"] == { "verification_cost_bound": 0, "memory_bound": 0, "certificate_size_bound": 0, } - assert layer4["prior_commitment"] == "" - assert layer4["refutation_surface"]["admissible_refutations"] == [] - assert "PHYSICAL_WORLD_COMPLETENESS" in layer4["refutation_surface"][ + assert context["prior_commitment"] == "" + assert context["refutation_surface"]["admissible_refutations"] == [] + assert "PHYSICAL_WORLD_COMPLETENESS" in context["refutation_surface"][ "excluded_claims" ] assert validate_run_receipt(out_dir) == 0 + assert "[INTEGRITY OK]" in capsys.readouterr().out assert inspect_run_receipt(out_dir) == 0 # Default reproduce: artifact rehash only, no side effects. assert reproduce_run_receipt(out_dir) == 0 +def test_reproduce_honors_an_explicit_receipt_filename(tmp_path): + proj = _write_tiny_project(tmp_path) + receipt = capture_run(_base_manifest(), manifest_dir=proj) + receipt_file = receipt.save_to_directory(proj) + renamed_receipt = proj / "renamed-receipt.json" + receipt_file.rename(renamed_receipt) + + assert validate_run_receipt(renamed_receipt) == 0 + assert inspect_run_receipt(renamed_receipt) == 0 + assert reproduce_run_receipt(renamed_receipt) == 0 + + def test_new_run_receipt_binds_precommitment_bounds_and_refutation_surface(tmp_path): proj = _write_tiny_project(tmp_path) manifest = _base_manifest() @@ -119,25 +202,61 @@ def test_new_run_receipt_binds_precommitment_bounds_and_refutation_surface(tmp_p } receipt = capture_run(manifest, manifest_dir=proj) before = receipt.canonical_digest - layer4 = receipt.get_stable_payload()["layer4_binding"] - assert layer4["prior_commitment"] == manifest["prior_commitment"] - assert layer4["resource_bounds"] == manifest["resource_bounds"] - assert layer4["refutation_surface"]["admissible_refutations"] == [ + context = receipt.get_stable_payload()["assessment_context"] + assert context["prior_commitment"] == manifest["prior_commitment"] + assert context["resource_bounds"] == manifest["resource_bounds"] + assert context["refutation_surface"]["admissible_refutations"] == [ "evidence_hash_mismatch" ] - layer4["prior_commitment"] = "sha256:" + "b" * 64 - receipt.layer4_binding = layer4 + context["prior_commitment"] = "sha256:" + "b" * 64 + receipt.assessment_context = context assert receipt.compute_and_set_digest() != before -def test_historical_generic_run_digest_is_unchanged_by_optional_layer4_block(): - receipt_path = REPO_ROOT / "examples" / "generic_run" / "receipt.json" - if not receipt_path.exists(): - pytest.skip("historical private-path receipt is intentionally excluded publicly") - data = json.loads(receipt_path.read_text(encoding="utf-8")) - assert "layer4_binding" not in data - assert validate_run_receipt(receipt_path) == 0 +def test_generic_run_requires_assessment_context(tmp_path, capsys): + proj = _write_tiny_project(tmp_path) + data = capture_run(_base_manifest(), manifest_dir=proj).to_dict() + data.pop("assessment_context") + data["canonical_digest"] = compute_canonical_digest( + _rebuild_stable_payload_from_dict(data) + ) + path = tmp_path / "missing-assessment-context.json" + path.write_text(json.dumps(data), encoding="utf-8") + + assert is_generic_run_receipt(data) + assert validate_run_receipt(path) == 1 + assert "missing required fields: assessment_context" in capsys.readouterr().out + + +def test_retired_generic_run_identifier_is_rejected(tmp_path, capsys): + proj = _write_tiny_project(tmp_path) + data = capture_run(_base_manifest(), manifest_dir=proj).to_dict() + data["schema_version"] = "VSTD-" + "0.1" + data["canonical_digest"] = compute_canonical_digest( + _rebuild_stable_payload_from_dict(data) + ) + path = tmp_path / "retired-identifier.json" + path.write_text(json.dumps(data), encoding="utf-8") + + assert not is_generic_run_receipt(data) + assert validate_run_receipt(path) == 1 + assert "schema_version must be VSTD-1" in capsys.readouterr().out + + +def test_assessment_context_rejects_layer_specific_fields(tmp_path, capsys): + proj = _write_tiny_project(tmp_path) + data = capture_run(_base_manifest(), manifest_dir=proj).to_dict() + prohibited_field = "vstd4_" + "conformance" + data["assessment_context"][prohibited_field] = "PASS" + data["canonical_digest"] = compute_canonical_digest( + _rebuild_stable_payload_from_dict(data) + ) + path = tmp_path / "hostile-vstd4-claim.json" + path.write_text(json.dumps(data), encoding="utf-8") + + assert validate_run_receipt(path) == 1 + assert f"unexpected fields: {prohibited_field}" in capsys.readouterr().out def test_missing_input_fails_closed_without_executing(tmp_path): @@ -248,7 +367,84 @@ def test_external_evaluation_never_auto_promoted_to_attested(tmp_path): assert ext.attested is False, "an unverified assertion must never be silently promoted to attested" -def test_external_evaluation_with_linked_artifact_and_ref_can_be_attested(tmp_path): +def test_validator_rejects_digest_consistent_independence_and_attestation_upgrades( + tmp_path, +): + proj = _write_tiny_project(tmp_path) + manifest = _base_manifest() + manifest["evaluator_claims"] = [ + {"evaluator_name": "declared", "metric_name": "score", "value": 1} + ] + manifest["external_evaluation"] = { + "source": "declared", + "description": "unverified", + "reported_value": 1, + } + receipt = capture_run(manifest, manifest_dir=proj) + original = receipt.to_dict() + + for mutate in ( + lambda data: data["claims"]["evaluator_claims"][0].update( + verified_independently=True + ), + lambda data: data["claims"]["external_evaluation"].update(attested=True), + lambda data: data.update(unbound_claim_upgrade=True), + ): + data = copy.deepcopy(original) + mutate(data) + data["canonical_digest"] = compute_canonical_digest( + _rebuild_stable_payload_from_dict(data) + ) + path = tmp_path / "hostile-receipt.json" + path.write_text(json.dumps(data), encoding="utf-8") + assert validate_run_receipt(path) == 1 + + +@pytest.mark.parametrize( + ("container_path", "field_name"), + ( + (("source_state",), "unknown_source_field"), + (("source_state", "git"), "unknown_git_field"), + (("source_state", "runtime"), "unknown_runtime_field"), + (("assessment_context",), "unknown_binding_field"), + (("assessment_context", "verifier"), "unknown_verifier_field"), + (("assessment_context", "resource_bounds"), "unknown_bound_field"), + ), +) +def test_validator_rejects_digest_consistent_unknown_nested_fields( + tmp_path, container_path, field_name +): + proj = _write_tiny_project(tmp_path) + receipt = capture_run(_base_manifest(), manifest_dir=proj) + data = receipt.to_dict() + container = data + for segment in container_path: + container = container[segment] + container[field_name] = "attacker-controlled" + data["canonical_digest"] = compute_canonical_digest( + _rebuild_stable_payload_from_dict(data) + ) + path = tmp_path / "hostile-nested-receipt.json" + path.write_text(json.dumps(data), encoding="utf-8") + + assert validate_run_receipt(path) == 1 + + +def test_refutation_surface_is_the_explicit_extension_map(tmp_path): + proj = _write_tiny_project(tmp_path) + manifest = _base_manifest() + manifest["refutation_surface"] = {"domain_refutation": "declared extension"} + receipt = capture_run(manifest, manifest_dir=proj) + path = receipt.save_to_directory(proj) + + assert ( + receipt.assessment_context["refutation_surface"]["domain_refutation"] + == "declared extension" + ) + assert validate_run_receipt(path) == 0 + + +def test_external_evaluation_reference_remains_unverified_by_capture_runtime(tmp_path): proj = _write_tiny_project(tmp_path) manifest = _base_manifest() manifest["external_evaluation"] = { @@ -261,7 +457,7 @@ def test_external_evaluation_with_linked_artifact_and_ref_can_be_attested(tmp_pa } receipt = capture_run(manifest, manifest_dir=proj) ext = receipt.claims.external_evaluation - assert ext.attested is True + assert ext.attested is False assert ext.evidence_ref == "sha256:deadbeef" @@ -279,11 +475,11 @@ def test_evaluator_claim_reads_true_value_from_output_not_manifest_assertion(tmp receipt = capture_run(manifest, manifest_dir=proj) claim = receipt.claims.evaluator_claims[0] assert claim.value == 42 # actual value read from the produced artifact, not the bogus 999999 - assert claim.computed_by == "local_reference_evaluator" - assert claim.verified_independently is True + assert claim.computed_by == "bound_output_extraction" + assert claim.verified_independently is False -def test_nondeterministic_run_cannot_declare_bitwise_ceiling(tmp_path): +def test_determinism_declaration_cannot_raise_reproduction_ceiling(tmp_path): proj = _write_tiny_project(tmp_path) script = proj / "rand.py" script.write_text( @@ -295,14 +491,15 @@ def test_nondeterministic_run_cannot_declare_bitwise_ceiling(tmp_path): manifest = _base_manifest() manifest["command"] = [sys.executable, "rand.py", "output.json"] manifest["inputs"] = [{"path": "rand.py", "role": "entrypoint_source"}] - manifest["determinism_declared"] = "NONDETERMINISTIC" + manifest["determinism_declared"] = "DETERMINISTIC" receipt = capture_run(manifest, manifest_dir=proj) - assert receipt.reproducibility["declared_ceiling"] != "BITWISE_IDENTICAL" - assert "BITWISE_IDENTICAL" not in receipt.reproducibility["supported_levels"] + assert receipt.reproducibility["declared_ceiling"] == "CONTENT_IDENTICAL" + assert receipt.reproducibility["supported_levels"] == ["CONTENT_IDENTICAL"] + assert receipt.reproducibility["highest_demonstrated_level"] is None -def test_rerun_reproduction_achieves_bitwise_identical_for_deterministic_example(tmp_path): +def test_rerun_demonstrates_only_declared_output_content_identity(tmp_path, capsys): proj = _write_tiny_project(tmp_path) manifest = _base_manifest() receipt = capture_run(manifest, manifest_dir=proj) @@ -310,37 +507,80 @@ def test_rerun_reproduction_achieves_bitwise_identical_for_deterministic_example (proj / "manifest.source.json").write_text(json.dumps(manifest), encoding="utf-8") assert reproduce_run_receipt(proj, rerun=True) == 0 + output = capsys.readouterr().out + assert "Fidelity state: CONTENT_IDENTICAL (declared-output scope)" in output + assert "BITWISE_IDENTICAL" not in output + + +def test_relocated_bundle_rerun_keeps_declared_output_scope(tmp_path, capsys): + source = tmp_path / "source" + source.mkdir() + _write_tiny_project(source) + subprocess.run(["git", "init", "-q"], cwd=source, check=True) + subprocess.run( + ["git", "config", "user.email", "test" + "@" + "example.invalid"], + cwd=source, + check=True, + ) + subprocess.run(["git", "config", "user.name", "VSTD Test"], cwd=source, check=True) + subprocess.run(["git", "add", "double.py", "input.txt"], cwd=source, check=True) + subprocess.run(["git", "commit", "-q", "-m", "fixture"], cwd=source, check=True) + + manifest = _base_manifest() + receipt = capture_run(manifest, manifest_dir=source) + bundle = tmp_path / "bundle" + _write_reproduction_bundle(manifest, source, bundle) + receipt.save_to_directory(bundle) + + assert reproduce_run_receipt(bundle, rerun=True) == 0 + output = capsys.readouterr().out + assert "Fidelity state: CONTENT_IDENTICAL (declared-output scope)" in output + assert "Scope: declared output artifacts and execution outcome" in output + + +def test_same_outcome_with_changed_output_earns_no_reproduction_level(tmp_path, capsys): + proj = _write_tiny_project(tmp_path) + manifest = _base_manifest() + receipt = capture_run(manifest, manifest_dir=proj) + receipt.save_to_directory(proj) + (proj / "manifest.source.json").write_text(json.dumps(manifest), encoding="utf-8") + (proj / "input.txt").write_text("22", encoding="utf-8") + + assert reproduce_run_receipt(proj, rerun=True) == 1 + output = capsys.readouterr().out + assert "Fidelity state: NOT_DEMONSTRATED" in output + assert "RESULT_EQUIVALENT" not in output + assert "SEMANTIC_REPRODUCTION" not in output -def test_provenance_linkage_against_real_vfy_data_receipt(): - """Dogfood check: link a run to the real VFY-DATA-000001 hypergraph in this repo.""" - data_receipt_dir = REPO_ROOT / "receipts" / "VFY-DATA-000001" - if not (data_receipt_dir / "receipt.json").exists(): - pytest.skip("VFY-DATA-000001 receipt not present in this checkout") +def test_no_declared_outputs_cannot_vacuously_reproduce(tmp_path, capsys): + proj = _write_tiny_project(tmp_path) + manifest = _base_manifest() + manifest["outputs"] = [] + receipt = capture_run(manifest, manifest_dir=proj) + receipt.save_to_directory(proj) + (proj / "manifest.source.json").write_text(json.dumps(manifest), encoding="utf-8") - data = json.loads((data_receipt_dir / "receipt.json").read_text(encoding="utf-8")) - arts = data.get("hypergraph", {}).get("artifacts", []) - # Artifacts are serialized as a list of dicts on disk (see VstdDataReceipt.to_dict - # -> ProvenanceHypergraph.to_dict); normalize defensively in case that ever changes to a - # dict keyed by artifact_id. - if isinstance(arts, dict): - artifact_ids = list(arts.keys()) - else: - artifact_ids = [a["artifact_id"] for a in arts] - assert artifact_ids, "expected at least one artifact in VFY-DATA-000001's hypergraph" + assert reproduce_run_receipt(proj, rerun=True) == 1 + assert "Fidelity state: NOT_DEMONSTRATED" in capsys.readouterr().out + + +def test_provenance_linkage_uses_public_graph_fixture(tmp_path): + """Resolve linkage without depending on a receipt absent from the public tree.""" + data_receipt_file, artifact_id = _write_data_receipt(tmp_path) from verifier.core.run import _resolve_provenance_linkage linkage = _resolve_provenance_linkage( - REPO_ROOT, - {"dataset_receipt_path": "receipts/VFY-DATA-000001", "artifact_id": artifact_ids[0]}, + tmp_path, + {"dataset_receipt_path": str(data_receipt_file.parent.name), "artifact_id": artifact_id}, ) assert linkage.found_in_hypergraph is True assert linkage.ancestor_count is not None missing = _resolve_provenance_linkage( - REPO_ROOT, - {"dataset_receipt_path": "receipts/VFY-DATA-000001", "artifact_id": "art:does_not_exist_12345"}, + tmp_path, + {"dataset_receipt_path": str(data_receipt_file.parent.name), "artifact_id": "artifact:missing"}, ) assert missing.found_in_hypergraph is False assert missing.ancestor_count is None @@ -351,19 +591,13 @@ def test_blast_radius_revocation_flags_dependent_run_receipts(tmp_path): consumed it (directly or via a downstream derivative) — composing dataset provenance into run-receipt impact analysis rather than a parallel system. """ - data_receipt_dir = REPO_ROOT / "receipts" / "VFY-DATA-000001" - data_receipt_file = data_receipt_dir / "receipt.json" - if not data_receipt_file.exists(): - pytest.skip("VFY-DATA-000001 receipt not present in this checkout") - - data = json.loads(data_receipt_file.read_text(encoding="utf-8")) - artifact_id = data["hypergraph"]["artifacts"][0]["artifact_id"] + data_receipt_file, artifact_id = _write_data_receipt(tmp_path) proj = _write_tiny_project(tmp_path) manifest = _base_manifest() manifest["provenance_roots"] = [ { - "dataset_receipt_path": str(data_receipt_dir), + "dataset_receipt_path": str(data_receipt_file.parent), "artifact_id": artifact_id, } ] @@ -399,3 +633,65 @@ def test_blast_radius_revocation_flags_dependent_run_receipts(tmp_path): matched_ids = {e["receipt_id"] for e in impacted_again} assert "RUN-TEST-000" in matched_ids assert "RUN-UNRELATED-000" not in matched_ids + + +def test_repeated_provenance_reference_does_not_duplicate_impact(tmp_path): + data_receipt_file, artifact_id = _write_data_receipt(tmp_path) + proj = _write_tiny_project(tmp_path) + manifest = _base_manifest() + repeated = { + "dataset_receipt_path": str(data_receipt_file.parent), + "artifact_id": artifact_id, + } + manifest["provenance_roots"] = [repeated, dict(repeated)] + receipt = capture_run(manifest, manifest_dir=proj) + assert len(receipt.provenance_linkage) == 2 + receipt.save_to_directory(tmp_path / "receipts_tree" / "RUN-TEST-000") + + impacted = find_run_receipts_impacted_by_revocation( + search_root=tmp_path / "receipts_tree", + dataset_receipt_file=data_receipt_file, + revoked_artifact_id=artifact_id, + ) + + assert [item["receipt_id"] for item in impacted] == ["RUN-TEST-000"] + + +def test_generic_run_facade_preserves_imports_across_bounded_modules() -> None: + from verifier.core import run + + expected_modules = { + "load_manifest": "verifier.core.run_planning", + "describe_run_plan": "verifier.core.run_planning", + "validate_run_receipt": "verifier.core.run_validation", + "inspect_run_receipt": "verifier.core.run_inspection", + "reproduce_run_receipt": "verifier.core.run_reproduction", + "find_run_receipts_impacted_by_revocation": "verifier.core.run_impact", + } + assert run.capture_run.__module__ == "verifier.core.run" + for name, module_name in expected_modules.items(): + assert getattr(run, name).__module__ == module_name + + +def test_generic_run_mechanism_hash_binds_every_decomposed_module() -> None: + from verifier.core import run + + module_names = ( + "run.py", + "run_support.py", + "run_planning.py", + "run_validation.py", + "run_inspection.py", + "run_reproduction.py", + "run_impact.py", + ) + directory = Path(run.__file__).resolve().parent + expected = run._implementation_inventory_digest( + tuple(directory / name for name in module_names) + ) + binding = run._assessment_context({}, falsification_condition="fixture") + + assert binding["verifier"]["implementation_hash"] == expected + assert binding["verifier"]["parser_hash"] == "sha256:" + hashlib.sha256( + (directory / "run_validation.py").read_bytes() + ).hexdigest() From e86a4b4772b3721add1431c60f8edd12bfb8b8e2 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:46:27 -0700 Subject: [PATCH 03/34] Add artifact freeze and self-closing seals Reason: Bind preserved artifact bytes, declared closure, signatures, and additive thaw lineage without conflating freeze, seal, encryption, or temporal continuity. Evidence: Normative text, strict schemas, implementation, and tamper-focused tests change together. Coordinate: Artifact control mechanism 1; VSTD 1.2.0 artifact-first control surface. Falsification: Missing bytes are treated as preserved, a modified closure validates, or thaw mutates the sealed parent. Compatibility: Additive mechanism only; it does not redefine numbered-profile receipt semantics or restore retired specifications. --- docs/REALMS_AND_TIME_CAPSULES.md | 139 +++ src/verifier/artifact_control/__init__.py | 920 ++++++++++++++++++ .../artifact-control-1.schema.json | 122 +++ .../specifications/ARTIFACT_CONTROL.md | 150 +++ standard/ARTIFACT_CONTROL.md | 150 +++ .../schemas/artifact-control-1.schema.json | 122 +++ tests/test_artifact_control.py | 348 +++++++ 7 files changed, 1951 insertions(+) create mode 100644 docs/REALMS_AND_TIME_CAPSULES.md create mode 100644 src/verifier/artifact_control/__init__.py create mode 100644 src/verifier/artifact_control/artifact-control-1.schema.json create mode 100644 src/verifier/specifications/ARTIFACT_CONTROL.md create mode 100644 standard/ARTIFACT_CONTROL.md create mode 100644 standard/schemas/artifact-control-1.schema.json create mode 100644 tests/test_artifact_control.py diff --git a/docs/REALMS_AND_TIME_CAPSULES.md b/docs/REALMS_AND_TIME_CAPSULES.md new file mode 100644 index 0000000..d567200 --- /dev/null +++ b/docs/REALMS_AND_TIME_CAPSULES.md @@ -0,0 +1,139 @@ +# Realms, temporal structures, and time capsules + +> **Acronyms:** directed acyclic graph (DAG); Verifier Standard (VSTD). + +**Status:** architectural model for VSTD 1.2; no public realm or time-capsule receipt +format and no complete inference-law verifier are defined here. + +This model separates structural artifact closure from propositions about time, execution, +or physical law. Multiple temporal structures may coexist in one declared reality, called +a **realm**. A seal can bind the realm description and its evidence; it does not make the +description true. + +## 1. A realm carries temporal domains, not one overloaded time field + +A realm may declare any combination of: + +| Domain | Carrier and relation | Optional structure | +|---|---|---| +| Continuous time | instants or intervals ordered over a metric domain | duration, topology, continuity | +| Discrete step time | states or steps with an order | successor, ticks, bounded gaps | +| Event or causal order | events under a partial order | concurrency, branching | +| Problem-space order | clauses, obligations, or solutions under dependency | valid linearizations, equivalence | +| Branching possibilities | histories or states under reachability | forks, joins, alternatives | +| Cyclic transition time | states under a transition relation | loops, backtracking, recurrence | +| Atemporal structure | no internal temporal carrier | structural closure only | + +Each declared temporal domain identifies its carrier, ordering relation, optional successor, +optional duration or metric, branching/cyclic/partial-order behavior, observation mechanism, +bounds, and unresolved coordinates. Cross-domain mappings are explicit and may be partial, +many-to-many, or information-losing. + +For example, one token step may map to a wall-clock interval, several hardware-kernel +events, one decoder-state transition, and several proof dependencies. Discrete observations +at the endpoints do not establish what occurred continuously between them. That proposition +requires a continuity mechanism covering the gap. + +## 2. Trace order is not dependency order + +If two clauses independently support a third, more than one total execution sequence may +respect the same dependency partial order. VSTD should distinguish: + +- **trace identity:** the exact same recorded sequence; +- **topological equivalence:** different sequences respecting the same dependencies; +- **solution equivalence:** different valid derivations reaching an equivalent solution; +- **evidence equivalence:** different executions producing certificates equivalent under a + named checker. + +A solver trace is one linearization; it is not the governing dependency structure. A +search with loops or backtracking is not globally a DAG. Its transition system retains +the internal cycles. A verifier may collapse strongly connected regions and topologically +order the resulting condensation graph without pretending the cycles disappeared. + +## 3. Structural seals and temporal capsules + +The artifact-control seal in [`standard/ARTIFACT_CONTROL.md`](../standard/ARTIFACT_CONTROL.md) +establishes finite structural closure. It makes no internal time proposition. A +**time capsule** is the composition: + +```text +preserved artifact + + verified self-closing seal + + sealed realm descriptor + + temporal-closure policy + + transition, checkpoint, or continuity evidence +``` + +An atemporal capsule can establish that an artifact was structurally closed under the +seal mechanism. Atemporal does not mean eternal: a verifier in another realm may later +apply time-indexed ROT because a key was revoked, evidence became stale, or a dependency +changed. + +A temporal capsule adds one exact proposition, for example: + +> Closure of artifact A was continuously mediated over interval I in temporal domain T by +> mechanism M. + +A topological capsule can instead establish that every recorded transition respects a +declared dependency relation independent of wall-clock order. Both propositions may coexist. +Neither follows from a signature at two endpoints. + +Cross-realm interoperability is earned only when a named verifier checks the declared +mapping between realm structures. Missing mapping evidence remains `UNKNOWN` or +`UNSUPPORTED`; a seal cannot fill it. + +## 4. Autoregressive language-model generation + +One generation can occupy several domains simultaneously: + +| Surface | Temporal structure | +|---|---| +| Token emission | discrete total order within one accepted sequence | +| Prefix dependency | each accepted next token depends on the accepted prefix | +| Decoder state | discrete state transitions | +| Attention and cache dependencies | directed dependency graph | +| Batched hardware execution | partially ordered events | +| Physical execution | continuous wall-clock intervals | +| Tool calls and revisions | branching event history | +| Reasoning or problem dependencies | partial order that may differ from emitted-token order | + +A future transition verifier could bind the prior state, model and weight identity, +tokenizer, prefix commitment, attention/cache commitment, constraint state, logits +commitment, sampler, random state, selected token, and next state: + +```text +VerifyTransition(state_n, token_n+1, state_n+1) + -> PASS | FAIL | UNKNOWN +``` + +A complete generation would be a checked chain or graph of such transitions. The law +families are distinct: + +- **model-realm law:** the transition follows the declared model, tokenizer, cache, + decoding, and constraints; +- **problem-realm law:** the derivation respects declared proof rules, schemas, clause + dependencies, or domain invariants; +- **substrate-realm law:** evidence binds the transition to the declared runtime and + machine substrate; and +- **cross-realm law:** a checked mapping connects the logical transition, problem + derivation, and substrate execution. + +Passing any such law establishes only its bounded execution proposition. It does not +establish that generated text is true. Textual truth still requires proposition-specific +evidence and verifiers. + +## 5. Placement on the VSTD axes + +This is an architectural allocation, not a new serialized profile: + +- **VSTD-1** records individual operations, transitions, and executions. +- **VSTD-2** describes the selected temporal/problem geometry and cross-domain mappings. +- **VSTD-3** anchors observations to runtime and physical substrate. +- **VSTD-4** exposes violations of transition, continuity, or mapping laws. +- **VSTD-5** may corroborate those bounded results through evidenced independent witnesses. +- **VSTD-Graph** represents the complete multi-temporal topology and retained conflicts. + +Current VSTD 1.2 artifact control can seal an independently serialized realm descriptor +as a generic `bound_contexts` artifact. It does not define the descriptor's schema, check +cross-domain mappings, establish continuous closure, or verify language-model transitions. +Those remain explicit future mechanism work rather than inferred capability. diff --git a/src/verifier/artifact_control/__init__.py b/src/verifier/artifact_control/__init__.py new file mode 100644 index 0000000..c500005 --- /dev/null +++ b/src/verifier/artifact_control/__init__.py @@ -0,0 +1,920 @@ +"""Artifact preservation, self-closing seals, and copy-on-write thawing. + +Terminology: American Standard Code for Information Interchange (ASCII); +identifier (ID); JavaScript Object Notation (JSON); Privacy-Enhanced Mail (PEM); Secure Hash +Algorithm 256-bit (SHA-256); Secure Hash Algorithm 3 256-bit (SHA3-256); +Verifier Standard (VSTD). + +Freezing preserves exact bytes. Sealing is a separate action that closes a +verified freeze manifest with a finite Ed25519 construction; it is not +encryption. Thawing creates a mutable descendant and never changes the frozen +parent. A valid seal establishes only artifact identity and closure under the +declared mechanisms. It does not establish correctness, freshness, ownership, +authorization, or actor reputation. +""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import re +import shutil +import stat +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable, Mapping + + +FREEZE_SCHEMA = "VSTD-ARTIFACT-FREEZE-1" +SEAL_SCHEMA = "VSTD-ARTIFACT-SEAL-1" +SEAL_PAYLOAD_SCHEMA = "VSTD-ARTIFACT-SEAL-CLOSURE-1" +THAW_SCHEMA = "VSTD-ARTIFACT-THAW-1" +CANONICALIZATION = "VSTD-ARTIFACT-CANONICAL-1" +SIGNATURE_ALGORITHM = "Ed25519" +_DIGEST_NAMES = ("sha256", "sha3-256") +_HEX_256 = re.compile(r"[0-9a-f]{64}\Z") +_DUAL_ID = re.compile( + r"vstd-(?:artifact|content|freeze|seal|thaw)-1:sha256:[0-9a-f]{64}:" + r"sha3-256:[0-9a-f]{64}\Z" +) +_MECHANISM = { + "name": "vstd-reference-freezer", + "version": "1", + "canonicalization": CANONICALIZATION, + "write_guard": "PORTABLE_READ_ONLY_TREE", +} + + +class ArtifactControlError(ValueError): + """Raised when an artifact-control action cannot fail closed.""" + + +@dataclass(frozen=True) +class ArtifactVerification: + """Result of independently recomputing a frozen artifact and its seals.""" + + state: str + artifact_id: str | None + content_id: str | None + freeze_id: str | None + freeze_valid: bool + guard_valid: bool + valid_seal_ids: tuple[str, ...] + key_ids: tuple[str, ...] + external_anchor: str + errors: tuple[str, ...] + warnings: tuple[str, ...] + + @property + def sealed(self) -> bool: + return self.state == "SEALED" + + def to_dict(self) -> dict[str, Any]: + return { + "state": self.state, + "artifact_id": self.artifact_id, + "content_id": self.content_id, + "freeze_id": self.freeze_id, + "freeze_valid": self.freeze_valid, + "guard_valid": self.guard_valid, + "valid_seal_ids": list(self.valid_seal_ids), + "key_ids": list(self.key_ids), + "external_anchor": self.external_anchor, + "errors": list(self.errors), + "warnings": list(self.warnings), + } + + +def _canonical_bytes(value: Any) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + allow_nan=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def _digests_bytes(value: bytes) -> dict[str, str]: + return { + "sha256": hashlib.sha256(value).hexdigest(), + "sha3-256": hashlib.sha3_256(value).hexdigest(), + } + + +def _digests_file(path: Path) -> tuple[dict[str, str], int]: + sha256 = hashlib.sha256() + sha3 = hashlib.sha3_256() + size = 0 + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + size += len(block) + sha256.update(block) + sha3.update(block) + return {"sha256": sha256.hexdigest(), "sha3-256": sha3.hexdigest()}, size + + +def _identity(prefix: str, value: bytes) -> str: + digests = _digests_bytes(value) + return ( + f"{prefix}:sha256:{digests['sha256']}:" + f"sha3-256:{digests['sha3-256']}" + ) + + +def _strict_object( + value: Any, expected: set[str], label: str +) -> Mapping[str, Any]: + if not isinstance(value, Mapping): + raise ArtifactControlError(f"{label} must be an object") + actual = set(value) + if actual != expected: + missing = sorted(expected - actual) + extra = sorted(actual - expected) + detail = [] + if missing: + detail.append("missing " + ", ".join(missing)) + if extra: + detail.append("unknown " + ", ".join(extra)) + raise ArtifactControlError(f"{label} has invalid fields: {'; '.join(detail)}") + return value + + +def _read_json_object(path: Path, label: str) -> dict[str, Any]: + def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ArtifactControlError(f"{label} contains duplicate key {key!r}") + result[key] = value + return result + + def reject_constant(value: str) -> None: + raise ArtifactControlError(f"{label} contains non-finite number {value}") + + try: + value = json.loads( + path.read_text(encoding="utf-8"), + object_pairs_hook=reject_duplicates, + parse_constant=reject_constant, + ) + except OSError as exc: + raise ArtifactControlError(f"cannot read {label}: {path}") from exc + except json.JSONDecodeError as exc: + raise ArtifactControlError(f"{label} is not readable JSON: {path}") from exc + if not isinstance(value, dict): + raise ArtifactControlError(f"{label} must contain one JSON object") + return value + + +def _write_json(path: Path, value: Mapping[str, Any]) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, allow_nan=False, indent=2, sort_keys=True) + + "\n", + encoding="utf-8", + newline="\n", + ) + + +def _relative_posix(path: Path, root: Path) -> str: + return path.relative_to(root).as_posix() + + +def _source_entries(source: Path) -> list[dict[str, Any]]: + if source.is_symlink(): + raise ArtifactControlError("symbolic links are not accepted as frozen artifacts") + if source.is_file(): + digests, size = _digests_file(source) + return [{"kind": "file", "path": ".", "byte_size": size, "digests": digests}] + if not source.is_dir(): + raise ArtifactControlError("artifact source must be a regular file or directory") + + entries: list[dict[str, Any]] = [] + for path in sorted(source.rglob("*"), key=lambda item: item.relative_to(source).as_posix()): + if path.is_symlink(): + raise ArtifactControlError( + f"symbolic links are not accepted as frozen artifacts: {_relative_posix(path, source)}" + ) + relative = _relative_posix(path, source) + if path.is_dir(): + entries.append({"kind": "directory", "path": relative}) + elif path.is_file(): + digests, size = _digests_file(path) + entries.append( + { + "kind": "file", + "path": relative, + "byte_size": size, + "digests": digests, + } + ) + else: + raise ArtifactControlError(f"special filesystem object is not supported: {relative}") + return entries + + +def _descriptor(kind: str, media_type: str, entries: list[dict[str, Any]]) -> dict[str, Any]: + return { + "canonicalization": CANONICALIZATION, + "artifact_kind": kind, + "media_type": media_type, + "entries": entries, + } + + +def _content_descriptor(kind: str, entries: list[dict[str, Any]]) -> dict[str, Any]: + return { + "canonicalization": CANONICALIZATION, + "artifact_kind": kind, + "entries": entries, + } + + +def _make_read_only(path: Path) -> None: + path.chmod(path.stat().st_mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)) + + +def _make_writable(path: Path) -> None: + path.chmod(path.stat().st_mode | stat.S_IWUSR) + + +def _is_read_only(path: Path) -> bool: + return not bool(path.stat().st_mode & (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)) + + +def _set_bundle_guard(bundle: Path) -> None: + payload = bundle / "payload" + if payload.is_file(): + _make_read_only(payload) + else: + for path in payload.rglob("*"): + if path.is_file(): + _make_read_only(path) + for path in sorted( + (item for item in payload.rglob("*") if item.is_dir()), + key=lambda item: len(item.parts), + reverse=True, + ): + _make_read_only(path) + _make_read_only(payload) + _make_read_only(bundle / "freeze.json") + + +def _guard_errors(bundle: Path) -> list[str]: + errors: list[str] = [] + guarded = [bundle / "freeze.json"] + payload = bundle / "payload" + if payload.is_file(): + guarded.append(payload) + elif payload.is_dir(): + guarded.append(payload) + guarded.extend( + path for path in payload.rglob("*") if path.is_file() or path.is_dir() + ) + for path in guarded: + if path.exists() and not _is_read_only(path): + errors.append(f"write guard is not active: {path.relative_to(bundle).as_posix()}") + return errors + + +def _validate_entries(entries: Any) -> list[dict[str, Any]]: + if not isinstance(entries, list): + raise ArtifactControlError("freeze.entries must be an array") + validated: list[dict[str, Any]] = [] + seen: set[str] = set() + for index, entry in enumerate(entries): + if not isinstance(entry, dict): + raise ArtifactControlError(f"freeze.entries[{index}] must be an object") + kind = entry.get("kind") + expected = {"kind", "path"} if kind == "directory" else { + "kind", + "path", + "byte_size", + "digests", + } + _strict_object(entry, expected, f"freeze.entries[{index}]") + path = entry["path"] + if kind not in {"file", "directory"} or not isinstance(path, str): + raise ArtifactControlError(f"freeze.entries[{index}] has invalid kind or path") + if path in seen: + raise ArtifactControlError(f"freeze.entries contains duplicate path {path!r}") + seen.add(path) + if path != ".": + candidate = Path(path) + if candidate.is_absolute() or ".." in candidate.parts or candidate.as_posix() != path: + raise ArtifactControlError(f"freeze.entries[{index}] path is not portable") + if kind == "file": + if type(entry["byte_size"]) is not int or entry["byte_size"] < 0: + raise ArtifactControlError(f"freeze.entries[{index}].byte_size is invalid") + digests = _strict_object( + entry["digests"], set(_DIGEST_NAMES), f"freeze.entries[{index}].digests" + ) + for name in _DIGEST_NAMES: + value = digests[name] + if not isinstance(value, str) or _HEX_256.fullmatch(value) is None: + raise ArtifactControlError( + f"freeze.entries[{index}].digests.{name} is invalid" + ) + validated.append(dict(entry)) + order = sorted(validated, key=lambda item: item["path"]) + if order != validated: + raise ArtifactControlError("freeze.entries must be sorted by portable path") + return validated + + +def _load_freeze(bundle: Path) -> dict[str, Any]: + freeze = _read_json_object(bundle / "freeze.json", "freeze manifest") + _strict_object( + freeze, + { + "schema_version", + "artifact_id", + "content_id", + "artifact_kind", + "media_type", + "entries", + "lineage", + "bound_contexts", + "mechanism", + "freeze_id", + }, + "freeze manifest", + ) + if freeze["schema_version"] != FREEZE_SCHEMA: + raise ArtifactControlError(f"unsupported freeze schema {freeze['schema_version']!r}") + if freeze["artifact_kind"] not in {"file", "directory"}: + raise ArtifactControlError("freeze.artifact_kind is invalid") + if not isinstance(freeze["media_type"], str) or not freeze["media_type"]: + raise ArtifactControlError("freeze.media_type must be a nonempty string") + freeze["entries"] = _validate_entries(freeze["entries"]) + if freeze["artifact_kind"] == "file" and not ( + len(freeze["entries"]) == 1 + and freeze["entries"][0]["kind"] == "file" + and freeze["entries"][0]["path"] == "." + ): + raise ArtifactControlError("a frozen file must have exactly one '.' file entry") + if freeze["artifact_kind"] == "directory" and any( + entry["path"] == "." for entry in freeze["entries"] + ): + raise ArtifactControlError("a frozen directory must not contain a '.' entry") + for name in ("artifact_id", "content_id", "freeze_id"): + if not isinstance(freeze[name], str) or _DUAL_ID.fullmatch(freeze[name]) is None: + raise ArtifactControlError(f"freeze.{name} is invalid") + if not isinstance(freeze["lineage"], list) or not all( + isinstance(item, str) for item in freeze["lineage"] + ): + raise ArtifactControlError("freeze.lineage must be an array of artifact identifiers") + if len(set(freeze["lineage"])) != len(freeze["lineage"]): + raise ArtifactControlError("freeze.lineage must not contain duplicates") + if any( + not item.startswith("vstd-artifact-1:") or _DUAL_ID.fullmatch(item) is None + for item in freeze["lineage"] + ): + raise ArtifactControlError("freeze.lineage contains a non-artifact identifier") + contexts = freeze["bound_contexts"] + if not isinstance(contexts, list) or not all(isinstance(item, str) for item in contexts): + raise ArtifactControlError("freeze.bound_contexts must be an array of artifact identifiers") + if len(set(contexts)) != len(contexts): + raise ArtifactControlError("freeze.bound_contexts must not contain duplicates") + if any( + not item.startswith("vstd-artifact-1:") or _DUAL_ID.fullmatch(item) is None + for item in contexts + ): + raise ArtifactControlError("freeze.bound_contexts contains a non-artifact identifier") + mechanism = _strict_object( + freeze["mechanism"], + {"name", "version", "canonicalization", "write_guard"}, + "freeze.mechanism", + ) + if mechanism != _MECHANISM: + raise ArtifactControlError("freeze.mechanism is unsupported") + return freeze + + +def _stable_freeze(freeze: Mapping[str, Any]) -> dict[str, Any]: + return {key: freeze[key] for key in freeze if key != "freeze_id"} + + +def _observed_bundle_entries(bundle: Path, kind: str) -> list[dict[str, Any]]: + payload = bundle / "payload" + if kind == "file": + if not payload.is_file() or payload.is_symlink(): + raise ArtifactControlError("frozen file payload is missing or not a regular file") + else: + if not payload.is_dir() or payload.is_symlink(): + raise ArtifactControlError("frozen directory payload is missing or not a directory") + return _source_entries(payload) + + +def _seal_dependencies() -> tuple[Any, Any, Any, Any]: + try: + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, + ) + except ImportError as exc: + raise ArtifactControlError( + "artifact sealing requires verifier-standard[seal]; no substitute was used" + ) from exc + return InvalidSignature, serialization, Ed25519PrivateKey, Ed25519PublicKey + + +def _seal_projection(envelope: Mapping[str, Any]) -> dict[str, Any]: + """Return the finite self-closing projection with both closure holes empty.""" + + projected = dict(envelope) + projected["signature_base64"] = None + projected["seal_id"] = None + return projected + + +def _seal_identity_projection(envelope: Mapping[str, Any]) -> dict[str, Any]: + projected = dict(envelope) + projected["seal_id"] = None + return projected + + +def _seal_payload( + freeze: Mapping[str, Any], freeze_bytes: bytes, key_id: str +) -> dict[str, Any]: + return { + "schema_version": SEAL_PAYLOAD_SCHEMA, + "artifact_id": freeze["artifact_id"], + "content_id": freeze["content_id"], + "freeze_id": freeze["freeze_id"], + "freeze_manifest_digests": _digests_bytes(freeze_bytes), + "key_id": key_id, + "algorithm": SIGNATURE_ALGORITHM, + "closure_rule": ( + "canonicalize the entire envelope with signature_base64 and seal_id set to null; " + "verify the signature; then canonicalize with only seal_id null and recompute seal_id" + ), + } + + +def _seal_file_paths(bundle: Path) -> list[Path]: + seals = bundle / "seals" + if not seals.exists(): + return [] + if not seals.is_dir() or seals.is_symlink(): + raise ArtifactControlError("seals must be a regular directory") + unexpected = [path for path in seals.iterdir() if not path.is_file() or path.suffix != ".json"] + if unexpected: + raise ArtifactControlError("seals directory contains an unsupported entry") + return sorted(seals.glob("*.json"), key=lambda path: path.name) + + +def freeze_artifact( + source: str | Path, + bundle: str | Path, + *, + media_type: str = "application/octet-stream", + parent_bundles: Iterable[str | Path] = (), + context_bundles: Iterable[str | Path] = (), +) -> dict[str, Any]: + """Preserve exact bytes in a new guarded bundle without creating a seal.""" + + source_path = Path(source).resolve() + bundle_path = Path(bundle).resolve() + if not isinstance(media_type, str) or not media_type: + raise ArtifactControlError("media_type must be a nonempty string") + if bundle_path.exists(): + raise ArtifactControlError(f"freeze bundle already exists: {bundle_path}") + if source_path.is_dir(): + try: + bundle_path.relative_to(source_path) + except ValueError: + pass + else: + raise ArtifactControlError("freeze bundle cannot be created inside its source directory") + + lineage: list[str] = [] + for parent in parent_bundles: + result = verify_frozen_artifact(parent, require_seal=True) + if not result.sealed or result.artifact_id is None: + raise ArtifactControlError(f"parent bundle is not cleanly sealed: {parent}") + lineage.append(result.artifact_id) + contexts: list[str] = [] + for context in context_bundles: + result = verify_frozen_artifact(context, require_seal=True) + if not result.sealed or result.artifact_id is None: + raise ArtifactControlError(f"context bundle is not cleanly sealed: {context}") + contexts.append(result.artifact_id) + if len(set(lineage)) != len(lineage) or len(set(contexts)) != len(contexts): + raise ArtifactControlError("duplicate parent or context bundles do not add assurance") + + kind = "file" if source_path.is_file() else "directory" if source_path.is_dir() else "" + entries = _source_entries(source_path) + descriptor = _descriptor(kind, media_type, entries) + artifact_id = _identity("vstd-artifact-1", _canonical_bytes(descriptor)) + content_id = _identity( + "vstd-content-1", _canonical_bytes(_content_descriptor(kind, entries)) + ) + freeze: dict[str, Any] = { + "schema_version": FREEZE_SCHEMA, + "artifact_id": artifact_id, + "content_id": content_id, + "artifact_kind": kind, + "media_type": media_type, + "entries": entries, + "lineage": sorted(lineage), + "bound_contexts": sorted(contexts), + "mechanism": dict(_MECHANISM), + } + freeze["freeze_id"] = _identity("vstd-freeze-1", _canonical_bytes(freeze)) + + bundle_path.parent.mkdir(parents=True, exist_ok=True) + staging = Path( + tempfile.mkdtemp(prefix=f".{bundle_path.name}.freeze-", dir=bundle_path.parent) + ) + try: + payload = staging / "payload" + if kind == "file": + shutil.copyfile(source_path, payload) + else: + shutil.copytree(source_path, payload) + _write_json(staging / "freeze.json", freeze) + _set_bundle_guard(staging) + observed = _observed_bundle_entries(staging, kind) + if observed != entries: + raise ArtifactControlError("preserved payload differs from the source inventory") + staging.replace(bundle_path) + except Exception: + if staging.exists(): + for path in sorted(staging.rglob("*"), key=lambda item: len(item.parts), reverse=True): + try: + _make_writable(path) + except OSError: + pass + shutil.rmtree(staging, ignore_errors=True) + raise + return freeze + + +def seal_artifact(bundle: str | Path, private_key: str | Path) -> dict[str, Any]: + """Add one deterministic, readable, self-closing Ed25519 seal.""" + + bundle_path = Path(bundle).resolve() + verification = verify_frozen_artifact(bundle_path, require_seal=False) + if verification.state not in {"FROZEN_UNSEALED", "SEALED"}: + raise ArtifactControlError("artifact must be cleanly frozen before it can be sealed") + freeze = _load_freeze(bundle_path) + freeze_bytes = (bundle_path / "freeze.json").read_bytes() + + _, serialization, Ed25519PrivateKey, _ = _seal_dependencies() + key_bytes = Path(private_key).read_bytes() + try: + key = serialization.load_pem_private_key(key_bytes, password=None) + except (TypeError, ValueError) as exc: + raise ArtifactControlError("seal private key is not a readable unencrypted PEM key") from exc + if not isinstance(key, Ed25519PrivateKey): + raise ArtifactControlError("seal private key must use Ed25519") + public_raw = key.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + key_id = "vstd-seal-key-1:sha256:" + hashlib.sha256(public_raw).hexdigest() + payload = _seal_payload(freeze, freeze_bytes, key_id) + envelope: dict[str, Any] = { + "schema_version": SEAL_SCHEMA, + "seal_payload": payload, + "public_key_base64": base64.b64encode(public_raw).decode("ascii"), + "signature_base64": None, + "seal_id": None, + } + signature = key.sign(_canonical_bytes(_seal_projection(envelope))) + envelope["signature_base64"] = base64.b64encode(signature).decode("ascii") + envelope["seal_id"] = _identity( + "vstd-seal-1", _canonical_bytes(_seal_identity_projection(envelope)) + ) + + seals = bundle_path / "seals" + seals.mkdir(exist_ok=True) + filename = hashlib.sha256(_canonical_bytes(envelope)).hexdigest() + ".json" + target = seals / filename + if target.exists(): + if _read_json_object(target, "seal") != envelope: + raise ArtifactControlError("existing seal filename contains different bytes") + return envelope + _write_json(target, envelope) + _make_read_only(target) + final = verify_frozen_artifact(bundle_path, require_seal=True) + if not final.sealed or envelope["seal_id"] not in final.valid_seal_ids: + raise ArtifactControlError( + f"seal did not produce a cleanly sealed artifact; observed {final.state}" + ) + return envelope + + +def _verify_seal( + seal: Mapping[str, Any], freeze: Mapping[str, Any], freeze_bytes: bytes +) -> tuple[str, str]: + InvalidSignature, serialization, _, Ed25519PublicKey = _seal_dependencies() + _strict_object( + seal, + {"schema_version", "seal_payload", "public_key_base64", "signature_base64", "seal_id"}, + "seal", + ) + if seal["schema_version"] != SEAL_SCHEMA: + raise ArtifactControlError(f"unsupported seal schema {seal['schema_version']!r}") + public_encoded = seal["public_key_base64"] + signature_encoded = seal["signature_base64"] + if not isinstance(public_encoded, str) or not isinstance(signature_encoded, str): + raise ArtifactControlError("seal public key and signature must be base64 strings") + try: + public_raw = base64.b64decode(public_encoded, validate=True) + signature = base64.b64decode(signature_encoded, validate=True) + except ValueError as exc: + raise ArtifactControlError("seal public key or signature is not canonical base64") from exc + if len(public_raw) != 32 or len(signature) != 64: + raise ArtifactControlError("seal public key or signature has the wrong length") + if ( + base64.b64encode(public_raw).decode("ascii") != public_encoded + or base64.b64encode(signature).decode("ascii") != signature_encoded + ): + raise ArtifactControlError("seal public key or signature is not canonical base64") + key_id = "vstd-seal-key-1:sha256:" + hashlib.sha256(public_raw).hexdigest() + expected_payload = _seal_payload(freeze, freeze_bytes, key_id) + if seal["seal_payload"] != expected_payload: + raise ArtifactControlError("seal payload does not close the current freeze manifest") + seal_id = seal["seal_id"] + if not isinstance(seal_id, str) or seal_id != _identity( + "vstd-seal-1", _canonical_bytes(_seal_identity_projection(seal)) + ): + raise ArtifactControlError("seal identity does not close the signature-bearing envelope") + try: + Ed25519PublicKey.from_public_bytes(public_raw).verify( + signature, _canonical_bytes(_seal_projection(seal)) + ) + except InvalidSignature as exc: + raise ArtifactControlError("self-closing seal signature did not verify") from exc + return seal_id, key_id + + +def verify_frozen_artifact( + bundle: str | Path, + *, + expected_artifact_id: str | None = None, + expected_key_id: str | None = None, + require_seal: bool = True, +) -> ArtifactVerification: + """Recompute preserved bytes, write guards, closure, and optional external anchors.""" + + bundle_path = Path(bundle).resolve() + errors: list[str] = [] + warnings: list[str] = [] + artifact_id: str | None = None + content_id: str | None = None + freeze_id: str | None = None + valid_seals: list[str] = [] + key_ids: list[str] = [] + invalid_seals = 0 + freeze_valid = False + guard_valid = False + freeze_errors: list[str] = [] + guard_errors: list[str] = [] + seal_errors: list[str] = [] + anchor_errors: list[str] = [] + try: + if not bundle_path.is_dir() or bundle_path.is_symlink(): + raise ArtifactControlError("artifact bundle must be a regular directory") + allowed = {"payload", "freeze.json", "seals"} + unexpected = sorted(path.name for path in bundle_path.iterdir() if path.name not in allowed) + if unexpected: + raise ArtifactControlError( + "artifact bundle contains unsupported top-level entries: " + + ", ".join(unexpected) + ) + freeze = _load_freeze(bundle_path) + artifact_id = freeze["artifact_id"] + content_id = freeze["content_id"] + freeze_id = freeze["freeze_id"] + entries = _observed_bundle_entries(bundle_path, freeze["artifact_kind"]) + descriptor = _descriptor(freeze["artifact_kind"], freeze["media_type"], entries) + observed_artifact_id = _identity("vstd-artifact-1", _canonical_bytes(descriptor)) + observed_content_id = _identity( + "vstd-content-1", + _canonical_bytes(_content_descriptor(freeze["artifact_kind"], entries)), + ) + if entries != freeze["entries"]: + freeze_errors.append("preserved payload inventory differs from freeze.entries") + if observed_artifact_id != artifact_id: + freeze_errors.append("preserved payload does not match artifact_id") + if observed_content_id != content_id: + freeze_errors.append("preserved payload does not match content_id") + expected_freeze_id = _identity( + "vstd-freeze-1", _canonical_bytes(_stable_freeze(freeze)) + ) + if freeze_id != expected_freeze_id: + freeze_errors.append("freeze_id does not close the freeze manifest") + freeze_valid = not freeze_errors + guard_errors.extend(_guard_errors(bundle_path)) + guard_valid = not guard_errors + freeze_bytes = (bundle_path / "freeze.json").read_bytes() + for seal_path in _seal_file_paths(bundle_path): + try: + seal = _read_json_object(seal_path, "seal") + seal_id, key_id = _verify_seal(seal, freeze, freeze_bytes) + valid_seals.append(seal_id) + key_ids.append(key_id) + if not _is_read_only(seal_path): + guard_errors.append( + f"write guard is not active: {seal_path.relative_to(bundle_path).as_posix()}" + ) + except ArtifactControlError as exc: + invalid_seals += 1 + seal_errors.append(f"{seal_path.name}: {exc}") + guard_valid = not guard_errors + except ArtifactControlError as exc: + freeze_errors.append(str(exc)) + + errors.extend(freeze_errors) + errors.extend(guard_errors) + errors.extend(seal_errors) + + artifact_anchor = "NOT_CHECKED" + if expected_artifact_id is not None: + if artifact_id == expected_artifact_id: + artifact_anchor = "MATCHED" + else: + anchor_errors.append("artifact_id does not match the expected external coordinate") + artifact_anchor = "MISMATCH" + key_anchor = "NOT_CHECKED" + if expected_key_id is not None: + if expected_key_id in key_ids: + key_anchor = "MATCHED" + else: + anchor_errors.append("no valid seal matches the expected external key coordinate") + key_anchor = "MISMATCH" + + if "MISMATCH" in {artifact_anchor, key_anchor}: + external_anchor = "MISMATCH" + elif artifact_anchor == key_anchor == "MATCHED": + external_anchor = "ARTIFACT_AND_KEY_MATCHED" + elif artifact_anchor == "MATCHED": + external_anchor = "ARTIFACT_ID_MATCHED" + elif key_anchor == "MATCHED": + external_anchor = "KEY_MATCHED" + else: + external_anchor = "NOT_CHECKED" + errors.extend(anchor_errors) + + if freeze_errors or guard_errors or anchor_errors: + state = "FAIL" + elif invalid_seals and valid_seals: + state = "CONFLICTED" + elif errors: + state = "FAIL" + elif valid_seals: + state = "SEALED" + else: + state = "FROZEN_UNSEALED" + warnings.append("artifact identity is not seal-backed") + if require_seal: + state = "NOT_ESTABLISHED" + return ArtifactVerification( + state=state, + artifact_id=artifact_id, + content_id=content_id, + freeze_id=freeze_id, + freeze_valid=freeze_valid, + guard_valid=guard_valid, + valid_seal_ids=tuple(sorted(set(valid_seals))), + key_ids=tuple(sorted(set(key_ids))), + external_anchor=external_anchor, + errors=tuple(errors), + warnings=tuple(warnings), + ) + + +def thaw_artifact( + bundle: str | Path, + destination: str | Path, + *, + expected_artifact_id: str | None = None, + expected_key_id: str | None = None, +) -> dict[str, Any]: + """Create a mutable descendant from a cleanly sealed frozen artifact.""" + + bundle_path = Path(bundle).resolve() + destination_path = Path(destination).resolve() + result = verify_frozen_artifact( + bundle_path, + expected_artifact_id=expected_artifact_id, + expected_key_id=expected_key_id, + require_seal=True, + ) + if not result.sealed: + raise ArtifactControlError( + f"thaw requires a cleanly sealed artifact; observed {result.state}" + ) + if destination_path.exists(): + raise ArtifactControlError(f"thaw destination already exists: {destination_path}") + record_path = destination_path.with_name(destination_path.name + ".vstd-thaw.json") + if record_path.exists(): + raise ArtifactControlError(f"thaw record already exists: {record_path}") + freeze = _load_freeze(bundle_path) + payload = bundle_path / "payload" + destination_path.parent.mkdir(parents=True, exist_ok=True) + if freeze["artifact_kind"] == "file": + shutil.copyfile(payload, destination_path) + _make_writable(destination_path) + else: + shutil.copytree(payload, destination_path) + for path in destination_path.rglob("*"): + if path.is_file() or path.is_dir(): + _make_writable(path) + _make_writable(destination_path) + record: dict[str, Any] = { + "schema_version": THAW_SCHEMA, + "parent_artifact_id": result.artifact_id, + "parent_content_id": result.content_id, + "parent_freeze_id": result.freeze_id, + "parent_seal_ids": list(result.valid_seal_ids), + "artifact_kind": freeze["artifact_kind"], + "media_type": freeze["media_type"], + } + record["thaw_id"] = _identity("vstd-thaw-1", _canonical_bytes(record)) + try: + _write_json(record_path, record) + status = thawed_artifact_status(destination_path, record_path) + if status["state"] != "THAWED_CLEAN": + raise ArtifactControlError("thawed descendant did not match the sealed parent") + except Exception: + if record_path.exists(): + record_path.unlink() + if destination_path.is_dir(): + shutil.rmtree(destination_path, ignore_errors=True) + elif destination_path.exists(): + destination_path.unlink() + raise + return {**record, "record_path": str(record_path)} + + +def thawed_artifact_status( + artifact: str | Path, thaw_record: str | Path | None = None +) -> dict[str, Any]: + """Compare a mutable descendant with its sealed parent's initial identity.""" + + artifact_path = Path(artifact).resolve() + record_path = ( + Path(thaw_record).resolve() + if thaw_record is not None + else artifact_path.with_name(artifact_path.name + ".vstd-thaw.json") + ) + record = _read_json_object(record_path, "thaw record") + _strict_object( + record, + { + "schema_version", + "parent_artifact_id", + "parent_content_id", + "parent_freeze_id", + "parent_seal_ids", + "artifact_kind", + "media_type", + "thaw_id", + }, + "thaw record", + ) + if record["schema_version"] != THAW_SCHEMA: + raise ArtifactControlError(f"unsupported thaw schema {record['schema_version']!r}") + stable = {key: record[key] for key in record if key != "thaw_id"} + if record["thaw_id"] != _identity("vstd-thaw-1", _canonical_bytes(stable)): + raise ArtifactControlError("thaw_id does not close the thaw record") + observed_kind = "file" if artifact_path.is_file() else "directory" if artifact_path.is_dir() else "" + if observed_kind != record["artifact_kind"]: + state = "THAWED_DIRTY" + observed_id = None + else: + entries = _source_entries(artifact_path) + observed_id = _identity( + "vstd-artifact-1", + _canonical_bytes(_descriptor(observed_kind, record["media_type"], entries)), + ) + state = ( + "THAWED_CLEAN" + if observed_id == record["parent_artifact_id"] + else "THAWED_DIRTY" + ) + return { + "state": state, + "parent_artifact_id": record["parent_artifact_id"], + "observed_artifact_id": observed_id, + "thaw_id": record["thaw_id"], + } + + +__all__ = [ + "ArtifactControlError", + "ArtifactVerification", + "freeze_artifact", + "seal_artifact", + "thaw_artifact", + "thawed_artifact_status", + "verify_frozen_artifact", +] diff --git a/src/verifier/artifact_control/artifact-control-1.schema.json b/src/verifier/artifact_control/artifact-control-1.schema.json new file mode 100644 index 0000000..fa1b943 --- /dev/null +++ b/src/verifier/artifact_control/artifact-control-1.schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json", + "title": "Verifier Standard (VSTD) artifact-control mechanism formats", + "description": "Strict shapes for freeze manifests, finite self-closing seal envelopes, and copy-on-write thaw lineage records. These objects are mechanism formats, not VSTD receipts or numbered-profile conformance claims.", + "oneOf": [ + {"$ref": "#/$defs/freeze"}, + {"$ref": "#/$defs/seal"}, + {"$ref": "#/$defs/thaw"} + ], + "$defs": { + "dualIdentifier": { + "type": "string", + "pattern": "^vstd-(artifact|content|freeze|seal|thaw)-1:sha256:[0-9a-f]{64}:sha3-256:[0-9a-f]{64}$" + }, + "artifactIdentifier": { + "type": "string", + "pattern": "^vstd-artifact-1:sha256:[0-9a-f]{64}:sha3-256:[0-9a-f]{64}$" + }, + "digests": { + "type": "object", + "additionalProperties": false, + "required": ["sha256", "sha3-256"], + "properties": { + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "sha3-256": {"type": "string", "pattern": "^[0-9a-f]{64}$"} + } + }, + "fileEntry": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "path", "byte_size", "digests"], + "properties": { + "kind": {"const": "file"}, + "path": {"type": "string", "minLength": 1}, + "byte_size": {"type": "integer", "minimum": 0}, + "digests": {"$ref": "#/$defs/digests"} + } + }, + "directoryEntry": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "path"], + "properties": { + "kind": {"const": "directory"}, + "path": {"type": "string", "minLength": 1, "not": {"const": "."}} + } + }, + "freeze": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "artifact_id", "content_id", "artifact_kind", "media_type", "entries", "lineage", "bound_contexts", "mechanism", "freeze_id"], + "properties": { + "schema_version": {"const": "VSTD-ARTIFACT-FREEZE-1"}, + "artifact_id": {"$ref": "#/$defs/artifactIdentifier"}, + "content_id": {"$ref": "#/$defs/dualIdentifier"}, + "artifact_kind": {"enum": ["file", "directory"]}, + "media_type": {"type": "string", "minLength": 1}, + "entries": { + "type": "array", + "items": {"oneOf": [{"$ref": "#/$defs/fileEntry"}, {"$ref": "#/$defs/directoryEntry"}]} + }, + "lineage": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/artifactIdentifier"}}, + "bound_contexts": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/artifactIdentifier"}}, + "mechanism": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version", "canonicalization", "write_guard"], + "properties": { + "name": {"const": "vstd-reference-freezer"}, + "version": {"const": "1"}, + "canonicalization": {"const": "VSTD-ARTIFACT-CANONICAL-1"}, + "write_guard": {"const": "PORTABLE_READ_ONLY_TREE"} + } + }, + "freeze_id": {"$ref": "#/$defs/dualIdentifier"} + } + }, + "sealPayload": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "artifact_id", "content_id", "freeze_id", "freeze_manifest_digests", "key_id", "algorithm", "closure_rule"], + "properties": { + "schema_version": {"const": "VSTD-ARTIFACT-SEAL-CLOSURE-1"}, + "artifact_id": {"$ref": "#/$defs/artifactIdentifier"}, + "content_id": {"$ref": "#/$defs/dualIdentifier"}, + "freeze_id": {"$ref": "#/$defs/dualIdentifier"}, + "freeze_manifest_digests": {"$ref": "#/$defs/digests"}, + "key_id": {"type": "string", "pattern": "^vstd-seal-key-1:sha256:[0-9a-f]{64}$"}, + "algorithm": {"const": "Ed25519"}, + "closure_rule": {"type": "string", "minLength": 1} + } + }, + "seal": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "seal_payload", "public_key_base64", "signature_base64", "seal_id"], + "properties": { + "schema_version": {"const": "VSTD-ARTIFACT-SEAL-1"}, + "seal_payload": {"$ref": "#/$defs/sealPayload"}, + "public_key_base64": {"type": "string", "contentEncoding": "base64"}, + "signature_base64": {"type": "string", "contentEncoding": "base64"}, + "seal_id": {"$ref": "#/$defs/dualIdentifier"} + } + }, + "thaw": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "parent_artifact_id", "parent_content_id", "parent_freeze_id", "parent_seal_ids", "artifact_kind", "media_type", "thaw_id"], + "properties": { + "schema_version": {"const": "VSTD-ARTIFACT-THAW-1"}, + "parent_artifact_id": {"$ref": "#/$defs/artifactIdentifier"}, + "parent_content_id": {"$ref": "#/$defs/dualIdentifier"}, + "parent_freeze_id": {"$ref": "#/$defs/dualIdentifier"}, + "parent_seal_ids": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/dualIdentifier"}}, + "artifact_kind": {"enum": ["file", "directory"]}, + "media_type": {"type": "string", "minLength": 1}, + "thaw_id": {"$ref": "#/$defs/dualIdentifier"} + } + } + } +} diff --git a/src/verifier/specifications/ARTIFACT_CONTROL.md b/src/verifier/specifications/ARTIFACT_CONTROL.md new file mode 100644 index 0000000..9bdd333 --- /dev/null +++ b/src/verifier/specifications/ARTIFACT_CONTROL.md @@ -0,0 +1,150 @@ +# Verifier Standard (VSTD) artifact freeze, seal, and thaw mechanism + +> **Acronyms:** American Standard Code for Information Interchange (ASCII); +> JavaScript Object Notation (JSON); Privacy-Enhanced Mail (PEM); +> Secure Hash Algorithm 256-bit (SHA-256); Secure Hash Algorithm 3 256-bit (SHA3-256); +> Unicode Transformation Format, 8-bit (UTF-8); Verifier Standard (VSTD). + +**Status:** normative for artifact-control mechanism version 1 + +This mechanism preserves exact regular-file bytes, binds them to artifact-derived +identifiers, optionally closes the freeze with a readable Ed25519 seal, and creates +mutable descendants by copy-on-write thaw. It is not a numbered VSTD profile, receipt profile, +encryption format, archival service, correctness proof, or actor reputation system. + +## 1. Distinct operations + +| Operation | What it establishes when verified | What it does not establish | +|---|---|---| +| **Freeze** | The bundle's current regular-file bytes and portable paths match its manifest, and its guarded payload tree is read-only. | Durable external preservation, privileged-write prevention, correctness, freshness, or a cryptographic signer. | +| **Seal** | A carried public key verifies a signature over the exact freeze closure, and the seal identifier closes the signature-bearing envelope. | Encryption, secrecy, ownership, authorization, trusted time, signer reputation, or protection against whole-bundle substitution. | +| **Thaw** | A new mutable descendant initially matched a clean sealed parent and retained a lineage sidecar. | Mutation of the parent, continued equality after thaw, or a sealed descendant. | + +Sealing and encryption are independent. Version 1 seals are readable and authenticated; +they do not encrypt any byte. A future encrypted container MUST still identify a separate +encryption mechanism and MUST NOT treat confidentiality as closure or correctness. + +## 2. Bundle and preservation boundary + +A bundle contains: + +```text +bundle/ + payload exact file, or directory of exact files and paths + freeze.json VSTD-ARTIFACT-FREEZE-1 manifest + seals/*.json zero or more VSTD-ARTIFACT-SEAL-1 envelopes +``` + +The mechanism accepts regular files, directories, and empty directories. Symbolic links +and special filesystem objects fail closed. It preserves file bytes and portable relative +paths. Permissions, owners, access-control lists, timestamps, extended attributes, sparse +allocation, and filesystem-specific metadata are outside version 1. The portable +read-only guard is an observable tripwire, not an access-control boundary against a +privileged writer. + +“Portable†means slash-normalized relative path representation. Case sensitivity, +Unicode normalization, reserved names, and path-length limits remain properties of the +host filesystem; version 1 does not claim that every valid source tree can be materialized +unchanged on every filesystem. + +The manifest inventories every file with its byte size, SHA-256, and SHA3-256 digest. A +directory entry preserves an empty directory or parent path. Identifier computation uses +canonical JSON with UTF-8, sorted object keys, no insignificant whitespace, no non-finite +numbers, and no duplicate keys. Stored objects remain readable; unknown fields fail closed. + +## 3. Artifact-derived identifiers + +Every version 1 identifier carries independent SHA-256 and SHA3-256 commitments: + +```text +vstd--1:sha256:<64 lowercase hexadecimal characters>: + sha3-256:<64 lowercase hexadecimal characters> +``` + +`content_id` closes artifact kind, paths, byte sizes, and file digests. `artifact_id` +closes the same content plus the declared media type. `freeze_id` closes the complete +freeze manifest except its own field. The artifact therefore carries a self-consistent +identity while frozen and sealed; the identity is derived from artifact state, not from +an actor's name or standing. + +Hash commitments are indexes and mutation detectors, not preservation. The bundle keeps +the exact bytes so a verifier can recompute both algorithms. If an algorithm weakens, +later evidence may add a new external commitment to the preserved historical bytes. It +MUST NOT rewrite the old manifest or claim that a digest alone retained the bytes. + +## 4. Finite self-closing seal + +A `VSTD-ARTIFACT-SEAL-1` envelope contains the complete seal payload, raw public key, +signature, and seal identifier. The seal payload closes the artifact, content, freeze, +exact freeze-manifest digests, key identifier, signature algorithm, and closure rule. + +Let `C(x)` be version 1 canonical JSON, `E` the complete envelope, and `Sign` Ed25519: + +```text +E0 = E with signature_base64 = null and seal_id = null +signature = Sign(private_key, C(E0)) +E1 = E with signature_base64 = signature and seal_id = null +seal_id = dual_digest("vstd-seal-1", C(E1)) +``` + +Verification reconstructs `E0`, verifies the signature with the carried public key, +reconstructs `E1`, recomputes `seal_id`, and independently recomputes the freeze and +payload bytes. The two explicit holes terminate the construction: no seal-of-seal chain +is required, while a change to any closed field, signature, or identifier fails. + +The carried key proves only internal signature consistency. An attacker can substitute a +whole self-consistent bundle and key. A relying party that needs continuity with an +earlier coordinate MUST supply an expected `artifact_id`, expected key identifier, or +separately verified external log/manifest entry. External anchoring is not part of +self-closure and actor identity contributes no verdict weight. + +Duplicate copies of one seal deduplicate by `seal_id` and add no strength. A valid and an +invalid seal remain `CONFLICTED`; placement or multiplicity cannot erase the invalid +evidence. + +## 5. Thaw and lineage + +Thaw requires a cleanly verified seal. It copies the parent payload to a new writable +path and emits a `VSTD-ARTIFACT-THAW-1` sidecar beside the descendant. The sidecar records +the parent artifact, content, freeze, and seal identifiers. It is lineage metadata, not a +seal. The parent remains unchanged. + +`THAWED_CLEAN` means the descendant still has the parent's initial artifact identity. +`THAWED_DIRTY` means it no longer does. To produce a new frozen artifact, freeze the +descendant into a new bundle and bind the sealed parent through `lineage`. This is an +additive state transition; no operation edits or erases the parent. + +`bound_contexts` similarly binds the artifact identifiers of clean sealed context +bundles. It does not interpret or validate their subject matter. A sealed realm descriptor, +for example, remains only a bound declaration until a named realm or mapping verifier +checks it. + +## 6. Results and artifact-first semantics + +| State | Meaning | +|---|---| +| `FROZEN_UNSEALED` | Exact bytes, manifest, identifiers, and guards recomputed; no valid seal was required or established. | +| `NOT_ESTABLISHED` | A seal was required but none was established. | +| `SEALED` | Freeze, guards, and at least one seal verified with no contradictory seal. | +| `CONFLICTED` | Valid and invalid seal evidence coexist. | +| `FAIL` | A checked structural, byte, guard, seal, or external-anchor condition failed. | +| `THAWED_CLEAN` / `THAWED_DIRTY` | A mutable descendant currently matches or differs from its recorded parent identity. | + +A clean freeze or seal can earn bounded **TRUST** in integrity and closure. It earns no +support for semantic correctness. Freezing does not stop **ROT** caused by staleness, +revocation, supersession, broken dependencies, or changed admissibility. A clean preserved +ancestor may lower mutation-related diagnostic priority, but **RUST** remains reverse +diagnostic reachability rather than innocence, guilt, or causal localization. + +Realm and temporal claims follow the +[realm and time-capsule architecture](https://github.com/TimeLordRaps/verifier/blob/main/docs/REALMS_AND_TIME_CAPSULES.md). A structural seal +is atemporal at its core. It binds a realm descriptor only as context and does not prove +that realm, its clocks, mappings, physical laws, or continuous closure. + +## 7. Public format and implementation + +The strict combined schema is published at +[`artifact-control-1.schema.json`](https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json). +The Python application programming interface and `vstd artifact` commands are generated +in the public reference. Ed25519 operations require the optional `seal` dependency extra; +the base package retains zero required third-party runtime dependencies. diff --git a/standard/ARTIFACT_CONTROL.md b/standard/ARTIFACT_CONTROL.md new file mode 100644 index 0000000..9bdd333 --- /dev/null +++ b/standard/ARTIFACT_CONTROL.md @@ -0,0 +1,150 @@ +# Verifier Standard (VSTD) artifact freeze, seal, and thaw mechanism + +> **Acronyms:** American Standard Code for Information Interchange (ASCII); +> JavaScript Object Notation (JSON); Privacy-Enhanced Mail (PEM); +> Secure Hash Algorithm 256-bit (SHA-256); Secure Hash Algorithm 3 256-bit (SHA3-256); +> Unicode Transformation Format, 8-bit (UTF-8); Verifier Standard (VSTD). + +**Status:** normative for artifact-control mechanism version 1 + +This mechanism preserves exact regular-file bytes, binds them to artifact-derived +identifiers, optionally closes the freeze with a readable Ed25519 seal, and creates +mutable descendants by copy-on-write thaw. It is not a numbered VSTD profile, receipt profile, +encryption format, archival service, correctness proof, or actor reputation system. + +## 1. Distinct operations + +| Operation | What it establishes when verified | What it does not establish | +|---|---|---| +| **Freeze** | The bundle's current regular-file bytes and portable paths match its manifest, and its guarded payload tree is read-only. | Durable external preservation, privileged-write prevention, correctness, freshness, or a cryptographic signer. | +| **Seal** | A carried public key verifies a signature over the exact freeze closure, and the seal identifier closes the signature-bearing envelope. | Encryption, secrecy, ownership, authorization, trusted time, signer reputation, or protection against whole-bundle substitution. | +| **Thaw** | A new mutable descendant initially matched a clean sealed parent and retained a lineage sidecar. | Mutation of the parent, continued equality after thaw, or a sealed descendant. | + +Sealing and encryption are independent. Version 1 seals are readable and authenticated; +they do not encrypt any byte. A future encrypted container MUST still identify a separate +encryption mechanism and MUST NOT treat confidentiality as closure or correctness. + +## 2. Bundle and preservation boundary + +A bundle contains: + +```text +bundle/ + payload exact file, or directory of exact files and paths + freeze.json VSTD-ARTIFACT-FREEZE-1 manifest + seals/*.json zero or more VSTD-ARTIFACT-SEAL-1 envelopes +``` + +The mechanism accepts regular files, directories, and empty directories. Symbolic links +and special filesystem objects fail closed. It preserves file bytes and portable relative +paths. Permissions, owners, access-control lists, timestamps, extended attributes, sparse +allocation, and filesystem-specific metadata are outside version 1. The portable +read-only guard is an observable tripwire, not an access-control boundary against a +privileged writer. + +“Portable†means slash-normalized relative path representation. Case sensitivity, +Unicode normalization, reserved names, and path-length limits remain properties of the +host filesystem; version 1 does not claim that every valid source tree can be materialized +unchanged on every filesystem. + +The manifest inventories every file with its byte size, SHA-256, and SHA3-256 digest. A +directory entry preserves an empty directory or parent path. Identifier computation uses +canonical JSON with UTF-8, sorted object keys, no insignificant whitespace, no non-finite +numbers, and no duplicate keys. Stored objects remain readable; unknown fields fail closed. + +## 3. Artifact-derived identifiers + +Every version 1 identifier carries independent SHA-256 and SHA3-256 commitments: + +```text +vstd--1:sha256:<64 lowercase hexadecimal characters>: + sha3-256:<64 lowercase hexadecimal characters> +``` + +`content_id` closes artifact kind, paths, byte sizes, and file digests. `artifact_id` +closes the same content plus the declared media type. `freeze_id` closes the complete +freeze manifest except its own field. The artifact therefore carries a self-consistent +identity while frozen and sealed; the identity is derived from artifact state, not from +an actor's name or standing. + +Hash commitments are indexes and mutation detectors, not preservation. The bundle keeps +the exact bytes so a verifier can recompute both algorithms. If an algorithm weakens, +later evidence may add a new external commitment to the preserved historical bytes. It +MUST NOT rewrite the old manifest or claim that a digest alone retained the bytes. + +## 4. Finite self-closing seal + +A `VSTD-ARTIFACT-SEAL-1` envelope contains the complete seal payload, raw public key, +signature, and seal identifier. The seal payload closes the artifact, content, freeze, +exact freeze-manifest digests, key identifier, signature algorithm, and closure rule. + +Let `C(x)` be version 1 canonical JSON, `E` the complete envelope, and `Sign` Ed25519: + +```text +E0 = E with signature_base64 = null and seal_id = null +signature = Sign(private_key, C(E0)) +E1 = E with signature_base64 = signature and seal_id = null +seal_id = dual_digest("vstd-seal-1", C(E1)) +``` + +Verification reconstructs `E0`, verifies the signature with the carried public key, +reconstructs `E1`, recomputes `seal_id`, and independently recomputes the freeze and +payload bytes. The two explicit holes terminate the construction: no seal-of-seal chain +is required, while a change to any closed field, signature, or identifier fails. + +The carried key proves only internal signature consistency. An attacker can substitute a +whole self-consistent bundle and key. A relying party that needs continuity with an +earlier coordinate MUST supply an expected `artifact_id`, expected key identifier, or +separately verified external log/manifest entry. External anchoring is not part of +self-closure and actor identity contributes no verdict weight. + +Duplicate copies of one seal deduplicate by `seal_id` and add no strength. A valid and an +invalid seal remain `CONFLICTED`; placement or multiplicity cannot erase the invalid +evidence. + +## 5. Thaw and lineage + +Thaw requires a cleanly verified seal. It copies the parent payload to a new writable +path and emits a `VSTD-ARTIFACT-THAW-1` sidecar beside the descendant. The sidecar records +the parent artifact, content, freeze, and seal identifiers. It is lineage metadata, not a +seal. The parent remains unchanged. + +`THAWED_CLEAN` means the descendant still has the parent's initial artifact identity. +`THAWED_DIRTY` means it no longer does. To produce a new frozen artifact, freeze the +descendant into a new bundle and bind the sealed parent through `lineage`. This is an +additive state transition; no operation edits or erases the parent. + +`bound_contexts` similarly binds the artifact identifiers of clean sealed context +bundles. It does not interpret or validate their subject matter. A sealed realm descriptor, +for example, remains only a bound declaration until a named realm or mapping verifier +checks it. + +## 6. Results and artifact-first semantics + +| State | Meaning | +|---|---| +| `FROZEN_UNSEALED` | Exact bytes, manifest, identifiers, and guards recomputed; no valid seal was required or established. | +| `NOT_ESTABLISHED` | A seal was required but none was established. | +| `SEALED` | Freeze, guards, and at least one seal verified with no contradictory seal. | +| `CONFLICTED` | Valid and invalid seal evidence coexist. | +| `FAIL` | A checked structural, byte, guard, seal, or external-anchor condition failed. | +| `THAWED_CLEAN` / `THAWED_DIRTY` | A mutable descendant currently matches or differs from its recorded parent identity. | + +A clean freeze or seal can earn bounded **TRUST** in integrity and closure. It earns no +support for semantic correctness. Freezing does not stop **ROT** caused by staleness, +revocation, supersession, broken dependencies, or changed admissibility. A clean preserved +ancestor may lower mutation-related diagnostic priority, but **RUST** remains reverse +diagnostic reachability rather than innocence, guilt, or causal localization. + +Realm and temporal claims follow the +[realm and time-capsule architecture](https://github.com/TimeLordRaps/verifier/blob/main/docs/REALMS_AND_TIME_CAPSULES.md). A structural seal +is atemporal at its core. It binds a realm descriptor only as context and does not prove +that realm, its clocks, mappings, physical laws, or continuous closure. + +## 7. Public format and implementation + +The strict combined schema is published at +[`artifact-control-1.schema.json`](https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json). +The Python application programming interface and `vstd artifact` commands are generated +in the public reference. Ed25519 operations require the optional `seal` dependency extra; +the base package retains zero required third-party runtime dependencies. diff --git a/standard/schemas/artifact-control-1.schema.json b/standard/schemas/artifact-control-1.schema.json new file mode 100644 index 0000000..fa1b943 --- /dev/null +++ b/standard/schemas/artifact-control-1.schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json", + "title": "Verifier Standard (VSTD) artifact-control mechanism formats", + "description": "Strict shapes for freeze manifests, finite self-closing seal envelopes, and copy-on-write thaw lineage records. These objects are mechanism formats, not VSTD receipts or numbered-profile conformance claims.", + "oneOf": [ + {"$ref": "#/$defs/freeze"}, + {"$ref": "#/$defs/seal"}, + {"$ref": "#/$defs/thaw"} + ], + "$defs": { + "dualIdentifier": { + "type": "string", + "pattern": "^vstd-(artifact|content|freeze|seal|thaw)-1:sha256:[0-9a-f]{64}:sha3-256:[0-9a-f]{64}$" + }, + "artifactIdentifier": { + "type": "string", + "pattern": "^vstd-artifact-1:sha256:[0-9a-f]{64}:sha3-256:[0-9a-f]{64}$" + }, + "digests": { + "type": "object", + "additionalProperties": false, + "required": ["sha256", "sha3-256"], + "properties": { + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "sha3-256": {"type": "string", "pattern": "^[0-9a-f]{64}$"} + } + }, + "fileEntry": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "path", "byte_size", "digests"], + "properties": { + "kind": {"const": "file"}, + "path": {"type": "string", "minLength": 1}, + "byte_size": {"type": "integer", "minimum": 0}, + "digests": {"$ref": "#/$defs/digests"} + } + }, + "directoryEntry": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "path"], + "properties": { + "kind": {"const": "directory"}, + "path": {"type": "string", "minLength": 1, "not": {"const": "."}} + } + }, + "freeze": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "artifact_id", "content_id", "artifact_kind", "media_type", "entries", "lineage", "bound_contexts", "mechanism", "freeze_id"], + "properties": { + "schema_version": {"const": "VSTD-ARTIFACT-FREEZE-1"}, + "artifact_id": {"$ref": "#/$defs/artifactIdentifier"}, + "content_id": {"$ref": "#/$defs/dualIdentifier"}, + "artifact_kind": {"enum": ["file", "directory"]}, + "media_type": {"type": "string", "minLength": 1}, + "entries": { + "type": "array", + "items": {"oneOf": [{"$ref": "#/$defs/fileEntry"}, {"$ref": "#/$defs/directoryEntry"}]} + }, + "lineage": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/artifactIdentifier"}}, + "bound_contexts": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/artifactIdentifier"}}, + "mechanism": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version", "canonicalization", "write_guard"], + "properties": { + "name": {"const": "vstd-reference-freezer"}, + "version": {"const": "1"}, + "canonicalization": {"const": "VSTD-ARTIFACT-CANONICAL-1"}, + "write_guard": {"const": "PORTABLE_READ_ONLY_TREE"} + } + }, + "freeze_id": {"$ref": "#/$defs/dualIdentifier"} + } + }, + "sealPayload": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "artifact_id", "content_id", "freeze_id", "freeze_manifest_digests", "key_id", "algorithm", "closure_rule"], + "properties": { + "schema_version": {"const": "VSTD-ARTIFACT-SEAL-CLOSURE-1"}, + "artifact_id": {"$ref": "#/$defs/artifactIdentifier"}, + "content_id": {"$ref": "#/$defs/dualIdentifier"}, + "freeze_id": {"$ref": "#/$defs/dualIdentifier"}, + "freeze_manifest_digests": {"$ref": "#/$defs/digests"}, + "key_id": {"type": "string", "pattern": "^vstd-seal-key-1:sha256:[0-9a-f]{64}$"}, + "algorithm": {"const": "Ed25519"}, + "closure_rule": {"type": "string", "minLength": 1} + } + }, + "seal": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "seal_payload", "public_key_base64", "signature_base64", "seal_id"], + "properties": { + "schema_version": {"const": "VSTD-ARTIFACT-SEAL-1"}, + "seal_payload": {"$ref": "#/$defs/sealPayload"}, + "public_key_base64": {"type": "string", "contentEncoding": "base64"}, + "signature_base64": {"type": "string", "contentEncoding": "base64"}, + "seal_id": {"$ref": "#/$defs/dualIdentifier"} + } + }, + "thaw": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "parent_artifact_id", "parent_content_id", "parent_freeze_id", "parent_seal_ids", "artifact_kind", "media_type", "thaw_id"], + "properties": { + "schema_version": {"const": "VSTD-ARTIFACT-THAW-1"}, + "parent_artifact_id": {"$ref": "#/$defs/artifactIdentifier"}, + "parent_content_id": {"$ref": "#/$defs/dualIdentifier"}, + "parent_freeze_id": {"$ref": "#/$defs/dualIdentifier"}, + "parent_seal_ids": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/dualIdentifier"}}, + "artifact_kind": {"enum": ["file", "directory"]}, + "media_type": {"type": "string", "minLength": 1}, + "thaw_id": {"$ref": "#/$defs/dualIdentifier"} + } + } + } +} diff --git a/tests/test_artifact_control.py b/tests/test_artifact_control.py new file mode 100644 index 0000000..d884924 --- /dev/null +++ b/tests/test_artifact_control.py @@ -0,0 +1,348 @@ +"""Adversarial tests for exact-byte freezing and finite self-closing seals. + +Terminology: Privacy-Enhanced Mail (PEM); Verifier Standard (VSTD). +""" + +from __future__ import annotations + +import base64 +import json +import stat +from pathlib import Path + +import pytest + +from verifier.artifact_control import ( + ArtifactControlError, + freeze_artifact, + seal_artifact, + thaw_artifact, + thawed_artifact_status, + verify_frozen_artifact, +) +from verifier.runtime.public_cli import main + + +ROOT = Path(__file__).resolve().parents[1] + + +def _private_key(path: Path) -> Path: + cryptography = pytest.importorskip("cryptography.hazmat.primitives.serialization") + ed25519 = pytest.importorskip( + "cryptography.hazmat.primitives.asymmetric.ed25519" + ) + key = ed25519.Ed25519PrivateKey.generate() + path.write_bytes( + key.private_bytes( + encoding=cryptography.Encoding.PEM, + format=cryptography.PrivateFormat.PKCS8, + encryption_algorithm=cryptography.NoEncryption(), + ) + ) + return path + + +def _writable(path: Path) -> None: + path.chmod(path.stat().st_mode | stat.S_IWUSR) + + +def _sealed_file(tmp_path: Path, name: str = "case") -> tuple[Path, dict[str, object]]: + source = tmp_path / f"{name}.bin" + source.write_bytes(b"\x00exact\r\nbytes\xff") + bundle = tmp_path / f"{name}.vstd-artifact" + freeze_artifact(source, bundle, media_type="application/x-test") + seal = seal_artifact(bundle, _private_key(tmp_path / f"{name}.pem")) + return bundle, seal + + +def _seal_path(bundle: Path) -> Path: + return next((bundle / "seals").glob("*.json")) + + +def test_freeze_preserves_exact_file_bytes_without_claiming_a_seal(tmp_path: Path) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"\x00\r\n\xff") + bundle = tmp_path / "bundle" + + manifest = freeze_artifact(source, bundle, media_type="application/x-bytes") + + assert (bundle / "payload").read_bytes() == source.read_bytes() + assert manifest["artifact_id"].startswith("vstd-artifact-1:sha256:") + assert manifest["content_id"].startswith("vstd-content-1:sha256:") + assert verify_frozen_artifact(bundle).state == "NOT_ESTABLISHED" + result = verify_frozen_artifact(bundle, require_seal=False) + assert result.state == "FROZEN_UNSEALED" + assert result.freeze_valid and result.guard_valid and not result.sealed + + +def test_freeze_preserves_directory_paths_files_and_empty_directories(tmp_path: Path) -> None: + source = tmp_path / "tree" + (source / "empty").mkdir(parents=True) + (source / "nested").mkdir() + (source / "nested" / "value.txt").write_bytes(b"value\n") + + bundle = tmp_path / "tree.vstd-artifact" + manifest = freeze_artifact(source, bundle) + + assert (bundle / "payload" / "empty").is_dir() + assert (bundle / "payload" / "nested" / "value.txt").read_bytes() == b"value\n" + assert [entry["path"] for entry in manifest["entries"]] == [ + "empty", + "nested", + "nested/value.txt", + ] + assert verify_frozen_artifact(bundle, require_seal=False).freeze_valid + + +def test_payload_mutation_and_guard_removal_fail_closed(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path) + payload = bundle / "payload" + _writable(payload) + payload.write_bytes(b"forged") + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert not result.freeze_valid + assert not result.guard_valid + assert any("payload" in error or "inventory" in error for error in result.errors) + + +def test_directory_addition_requires_breaking_the_tree_guard_and_fails(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "original").write_bytes(b"original") + bundle = tmp_path / "bundle" + freeze_artifact(source, bundle) + payload = bundle / "payload" + _writable(payload) + (payload / "added").write_bytes(b"added") + + result = verify_frozen_artifact(bundle, require_seal=False) + + assert result.state == "FAIL" + assert not result.freeze_valid + assert not result.guard_valid + + +def test_seal_is_finite_self_closing_and_artifact_carried(tmp_path: Path) -> None: + bundle, seal = _sealed_file(tmp_path) + + result = verify_frozen_artifact( + bundle, + expected_artifact_id=seal["seal_payload"]["artifact_id"], + expected_key_id=seal["seal_payload"]["key_id"], + ) + + assert result.state == "SEALED" + assert result.sealed and result.freeze_valid and result.guard_valid + assert result.external_anchor == "ARTIFACT_AND_KEY_MATCHED" + assert result.valid_seal_ids == (seal["seal_id"],) + assert seal["signature_base64"] is not None + assert seal["seal_id"] is not None + + +@pytest.mark.parametrize( + "field", + ( + "artifact_id", + "closure_rule", + "public_key_base64", + "signature_base64", + "seal_id", + ), +) +def test_each_closed_seal_surface_rejects_substitution(tmp_path: Path, field: str) -> None: + bundle, _ = _sealed_file(tmp_path, field) + path = _seal_path(bundle) + envelope = json.loads(path.read_text(encoding="utf-8")) + if field in {"artifact_id", "closure_rule"}: + envelope["seal_payload"][field] += "-tampered" + elif field == "public_key_base64": + envelope[field] = base64.b64encode(b"x" * 32).decode("ascii") + elif field == "signature_base64": + envelope[field] = base64.b64encode(b"x" * 64).decode("ascii") + else: + envelope[field] += "-tampered" + _writable(path) + path.write_text(json.dumps(envelope), encoding="utf-8") + path.chmod(path.stat().st_mode & ~stat.S_IWUSR) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert not result.valid_seal_ids + + +def test_bundle_substitution_requires_an_external_coordinate(tmp_path: Path) -> None: + first, first_seal = _sealed_file(tmp_path, "first") + second, _ = _sealed_file(tmp_path, "second") + + self_result = verify_frozen_artifact(second) + anchored_result = verify_frozen_artifact( + second, + expected_artifact_id=first_seal["seal_payload"]["artifact_id"], + expected_key_id=first_seal["seal_payload"]["key_id"], + ) + + assert self_result.state == "SEALED" + assert anchored_result.state == "FAIL" + assert anchored_result.external_anchor == "MISMATCH" + assert first != second + + +def test_one_external_anchor_match_cannot_hide_the_other_mismatch(tmp_path: Path) -> None: + first, _ = _sealed_file(tmp_path, "first") + second, second_seal = _sealed_file(tmp_path, "second") + expected = str(second_seal["seal_payload"]["artifact_id"]) + mismatched_artifact_id = expected[:-1] + ("0" if expected[-1] != "0" else "1") + + result = verify_frozen_artifact( + second, + expected_artifact_id=mismatched_artifact_id, + expected_key_id=second_seal["seal_payload"]["key_id"], + ) + + assert result.state == "FAIL" + assert result.external_anchor == "MISMATCH" + assert first != second + + +def test_duplicate_seal_does_not_multiply_assurance(tmp_path: Path) -> None: + bundle, seal = _sealed_file(tmp_path) + original = _seal_path(bundle) + duplicate = original.with_name("duplicate.json") + duplicate.write_bytes(original.read_bytes()) + duplicate.chmod(duplicate.stat().st_mode & ~stat.S_IWUSR) + + result = verify_frozen_artifact(bundle) + + assert result.state == "SEALED" + assert result.valid_seal_ids == (seal["seal_id"],) + assert len(result.key_ids) == 1 + + +def test_valid_and_invalid_seals_remain_conflicted(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path) + invalid = bundle / "seals" / "invalid.json" + invalid.write_text("{}\n", encoding="utf-8") + invalid.chmod(invalid.stat().st_mode & ~stat.S_IWUSR) + + result = verify_frozen_artifact(bundle) + + assert result.state == "CONFLICTED" + assert result.valid_seal_ids + assert result.errors + with pytest.raises(ArtifactControlError, match="cleanly frozen"): + seal_artifact(bundle, _private_key(tmp_path / "other.pem")) + + +def test_thaw_is_copy_on_write_and_dirtying_is_observable(tmp_path: Path) -> None: + unsealed_source = tmp_path / "unsealed.bin" + unsealed_source.write_bytes(b"unsealed") + unsealed = tmp_path / "unsealed" + freeze_artifact(unsealed_source, unsealed) + with pytest.raises(ArtifactControlError, match="cleanly sealed"): + thaw_artifact(unsealed, tmp_path / "blocked") + + bundle, _ = _sealed_file(tmp_path, "sealed") + parent_before = (bundle / "payload").read_bytes() + descendant = tmp_path / "descendant.bin" + record = thaw_artifact(bundle, descendant) + + assert descendant.read_bytes() == parent_before + assert thawed_artifact_status(descendant)["state"] == "THAWED_CLEAN" + descendant.write_bytes(b"changed") + assert thawed_artifact_status(descendant)["state"] == "THAWED_DIRTY" + assert (bundle / "payload").read_bytes() == parent_before + assert record["parent_artifact_id"] == verify_frozen_artifact(bundle).artifact_id + + +def test_sealed_parent_and_context_bindings_are_explicit_and_deduplicated( + tmp_path: Path, +) -> None: + parent, _ = _sealed_file(tmp_path, "parent") + context, _ = _sealed_file(tmp_path, "realm") + child_source = tmp_path / "child.bin" + child_source.write_bytes(b"child") + child = tmp_path / "child" + + manifest = freeze_artifact( + child_source, + child, + parent_bundles=[parent], + context_bundles=[context], + ) + + assert manifest["lineage"] == [verify_frozen_artifact(parent).artifact_id] + assert manifest["bound_contexts"] == [verify_frozen_artifact(context).artifact_id] + with pytest.raises(ArtifactControlError, match="duplicate"): + freeze_artifact( + child_source, + tmp_path / "duplicate", + context_bundles=[context, context], + ) + + +def test_unknown_bundle_or_manifest_fields_fail_closed(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path) + (bundle / "surprise.txt").write_text("not part of the format", encoding="utf-8") + assert verify_frozen_artifact(bundle).state == "FAIL" + + (bundle / "surprise.txt").unlink() + freeze_path = bundle / "freeze.json" + _writable(freeze_path) + text = freeze_path.read_text(encoding="utf-8") + freeze_path.write_text(text.replace("{", '{"schema_version":"duplicate",', 1)) + assert verify_frozen_artifact(bundle).state == "FAIL" + + +def test_published_and_packaged_schemas_are_identical_and_accept_emitted_objects( + tmp_path: Path, +) -> None: + jsonschema = pytest.importorskip("jsonschema") + public_path = ROOT / "standard/schemas/artifact-control-1.schema.json" + packaged_path = ROOT / "src/verifier/artifact_control/artifact-control-1.schema.json" + assert public_path.read_bytes() == packaged_path.read_bytes() + schema = json.loads(public_path.read_text(encoding="utf-8")) + jsonschema.Draft202012Validator.check_schema(schema) + validator = jsonschema.Draft202012Validator(schema) + + bundle, seal = _sealed_file(tmp_path) + freeze = json.loads((bundle / "freeze.json").read_text(encoding="utf-8")) + descendant = tmp_path / "descendant" + record = thaw_artifact(bundle, descendant) + record.pop("record_path") + + validator.validate(freeze) + validator.validate(seal) + validator.validate(record) + + +def test_public_cli_exposes_the_complete_artifact_lifecycle( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"cli") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + + assert main(["artifact", "freeze", str(source), str(bundle), "--json"]) == 0 + assert json.loads(capsys.readouterr().out)["state"] == "FROZEN_UNSEALED" + assert main(["artifact", "verify", str(bundle), "--json"]) == 2 + assert json.loads(capsys.readouterr().out)["state"] == "NOT_ESTABLISHED" + assert main( + ["artifact", "seal", str(bundle), "--private-key", str(key), "--json"] + ) == 0 + assert json.loads(capsys.readouterr().out)["state"] == "SEALED" + assert main(["artifact", "verify", str(bundle), "--json"]) == 0 + assert json.loads(capsys.readouterr().out)["state"] == "SEALED" + + descendant = tmp_path / "descendant.bin" + assert main( + ["artifact", "thaw", str(bundle), str(descendant), "--json"] + ) == 0 + assert json.loads(capsys.readouterr().out)["state"] == "THAWED_CLEAN" + descendant.write_bytes(b"dirty") + assert main(["artifact", "status", str(descendant), "--json"]) == 1 + assert json.loads(capsys.readouterr().out)["state"] == "THAWED_DIRTY" From 798e99c2f5361dc06685deada6df1ceb40f9569c Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:46:37 -0700 Subject: [PATCH 04/34] Add bounded SCITT interoperability Reason: Translate exact VSTD receipt claims into Supply Chain Integrity, Transparency, and Trust envelopes without inheriting external guarantees by citation or placement. Evidence: Adapter code, recorded cryptographic specimens, semantic boundary documents, and adversarial interoperability tests change together. Coordinate: VSTD to SCITT translation seam; release 1.2.0. Falsification: Translation widens a proposition, ignores a digest mismatch, or treats registration or transparency as VSTD conformance. Compatibility: Optional additive adapter; base runtime remains dependency-free and current receipt identifiers remain unchanged. --- docs/standards/SCITT_SEMANTIC_BOUNDARY.md | 209 +++++ docs/standards/VSTD_SCITT_CROSSWALK.md | 188 ++++ examples/scitt_interop/README.md | 119 +++ examples/scitt_interop/artifact.txt | 1 + examples/scitt_interop/demo.py | 496 +++++++++++ .../scitt_interop/generated/issuer_public.pem | 3 + .../scitt_interop/generated/log_public.pem | 3 + examples/scitt_interop/generated/receipt.cose | Bin 0 -> 250 bytes .../generated/registration_template.json | 175 ++++ .../generated/signed_statement.cose | 2 + .../generated/transparent_statement.cose | Bin 0 -> 5361 bytes .../generated/verification_result.json | 70 ++ .../scitt_interop/generated/vstd_receipt.json | 132 +++ .../generated/vstd_scitt_payload.json | 1 + src/verifier/interoperability/__init__.py | 1 + .../interoperability/scitt/__init__.py | 49 ++ .../interoperability/scitt/adapter.py | 828 ++++++++++++++++++ tests/test_scitt_crypto_example.py | 121 +++ tests/test_scitt_interop.py | 437 +++++++++ 19 files changed, 2835 insertions(+) create mode 100644 docs/standards/SCITT_SEMANTIC_BOUNDARY.md create mode 100644 docs/standards/VSTD_SCITT_CROSSWALK.md create mode 100644 examples/scitt_interop/README.md create mode 100644 examples/scitt_interop/artifact.txt create mode 100644 examples/scitt_interop/demo.py create mode 100644 examples/scitt_interop/generated/issuer_public.pem create mode 100644 examples/scitt_interop/generated/log_public.pem create mode 100644 examples/scitt_interop/generated/receipt.cose create mode 100644 examples/scitt_interop/generated/registration_template.json create mode 100644 examples/scitt_interop/generated/signed_statement.cose create mode 100644 examples/scitt_interop/generated/transparent_statement.cose create mode 100644 examples/scitt_interop/generated/verification_result.json create mode 100644 examples/scitt_interop/generated/vstd_receipt.json create mode 100644 examples/scitt_interop/generated/vstd_scitt_payload.json create mode 100644 src/verifier/interoperability/__init__.py create mode 100644 src/verifier/interoperability/scitt/__init__.py create mode 100644 src/verifier/interoperability/scitt/adapter.py create mode 100644 tests/test_scitt_crypto_example.py create mode 100644 tests/test_scitt_interop.py diff --git a/docs/standards/SCITT_SEMANTIC_BOUNDARY.md b/docs/standards/SCITT_SEMANTIC_BOUNDARY.md new file mode 100644 index 0000000..6f44d07 --- /dev/null +++ b/docs/standards/SCITT_SEMANTIC_BOUNDARY.md @@ -0,0 +1,209 @@ +# Supply Chain Integrity, Transparency, and Trust (SCITT) semantic boundary for Verifier Standard (VSTD) interoperability + +> **Acronyms:** Concise Binary Object Representation (CBOR); CBOR Object Signing and Encryption (COSE); grounded decision certificate (GDC); +> JavaScript Object Notation (JSON); Request for Comments (RFC); Secure Hash Algorithm 256-bit (SHA-256); +> Transparency Service (TS); verifiable data structure proof (VDP); verifiable data structure (VDS); +> working group (WG). + +> **Status:** experimental, non-normative. This boundary follows [RFC 9943](https://datatracker.ietf.org/doc/html/rfc9943), [RFC 9942](https://datatracker.ietf.org/doc/html/rfc9942), and the current repository's implemented VSTD specifications. It does not claim SCITT WG review. + +## SCITT can establish + +Subject to the named trust anchors, keys, algorithms, VDS profile, registration +policy, receipt validity period, and relying-party checks, SCITT can establish: + +- which exact Signed Statement bytes an issuer signed; +- the authenticated `iss` and `sub` protected claims and payload media type; +- that a TS applied its then-current registration policy before registration; +- that the Signed Statement was included in the TS's VDS state represented by a + valid COSE Receipt; +- the VDS proof properties implemented by the Receipt profile, such as inclusion + and, where supported, consistency; +- append-only/non-equivocation evidence and auditable registration history; +- enough registration collateral for authorized auditors to reproduce the + registration checks required by RFC 9943; +- historical relationships such as later same-issuer/same-subject statements that + a relying-party policy may treat as superseding earlier statements. + +RFC 9943 is explicit that an issuer can make a false statement and that registration +only proves the statement was produced by the issuer. A SCITT Receipt is therefore +not a generic certificate of payload truth. + +## VSTD can establish + +VSTD is not the domain verifier or proof engine. It is the standard domain language +and operator/result layer through which those orchestrated substrates expose +portable claim boundaries and results. Only for its declared claim, coordinate, +evidence, policy, native verifier fragment, and resource bounds, the implemented +VSTD numbered profiles can establish: + +- claim-mechanics and declared falsification conditions; +- an explicit verification surface and claim coordinate; +- substrate/accountability evidence within VSTD-3's implemented capability model; +- an accepted VSTD4-GDC-1 certificate result of PASS, FAIL, or UNKNOWN without + upgrading it to VSTD-4 profile conformance; +- grounding between a bounded logical encoding and named artifact facts; +- checker-side recomputation of the VSTD-4 certificate without sharing verdict-producing + code; +- a bounded cost/memory/certificate-size ceiling and honest refusal when exceeded; +- Graph lineage and blast-radius queries plus candidate degradation from statuses already + recorded in VSTD-Graph. Rating evidence and challenge-to-Graph propagation remain + `NOT_ESTABLISHED`. + +The native solver, proof engine, signature checker, identity service, transparency +log, or provenance system retains its own semantics and result. A loss-declared +adapter maps that result into VSTD's verification interlingua and records the +boundary around its portable composition; VSTD does not absorb or reimplement the +substrate. + +VSTD-5 and VSTD-Graph-5 remain draft. A later VSTD profile does not supply a missing +prerequisite coordinate. + +## Identity, disclosure, trust, and reputation + +VSTD verification is claim-first. Deciding a bounded claim does not, merely by +being a VSTD check, require a natural-person identity, creator identity, or +persistent actor identity. Some VSTD numbered and application profiles name devices, verifier +implementations, evidence sources, or witnesses where those coordinates are part +of the claim. Such identifiers do not automatically establish authorship, +authority, independence, reputation, or real-world identity. + +SCITT composition is therefore **optional**, not a prerequisite for VSTD. An RFC +9943 Signed Statement introduces an authenticated issuer coordinate, and public +registration may expose stable identifiers, subjects, payload bytes or digests, +timing, and relationship metadata. A key or pseudonym need not identify a natural +person, but it can still be linkable. Wrapping a VSTD receipt in SCITT adds an +accountability/transparency proposition; it does not strengthen the native VSTD +computational proposition and can weaken an identity-minimizing privacy posture. + +The implemented VSTD core is disclosure-neutral, not itself a zero-knowledge proof +protocol. Under the governing architecture, zero identity means zero identity-derived +verdict weight and zero knowledge means zero unevidenced knowledge is presumed. Neither +alone is a privacy claim. When a witness must remain confidential, a cryptographic +zero-knowledge proof can enclose that architectural rule by binding the exact program, +predicate, public commitments, output, parameters, and verifier. Full-disclosure receipts +remain valid, and no receipt may claim the cryptographic zero-knowledge property without a +real proof-system guarantee. “Trustless†+must mean trust-minimized and assumption-explicit: a relying party still depends on +selected algorithms, checker code, canonicalization, policy, input availability, and, +when used, proof-system parameters or trust roots. + +VSTD-Graph can preserve artifact history, challenges, lifecycle changes, and +refutations, but the current standard does not define a scalar artifact-reputation +score. The governing semantics instead distinguish TRUST, mechanism-earned artifact +support; ROT, typed time-indexed degradation of current admissibility; and RUST, +inverse-TRUST diagnostic traversal toward recorded ancestors. Their unfinished transfer +mechanisms must never overwrite a native verdict or turn actor identity, repeated +registrations, signatures, observations, age, or reputation into process validity. + +## Neither establishes automatically + +Neither a valid SCITT Receipt nor a valid VSTD receipt automatically establishes: + +- truth of arbitrary physical-world or historical propositions; +- completeness of evidence that was never disclosed or discoverable; +- causal correctness or causal influence merely from recorded lineage; +- safety, harmlessness, fitness for purpose, or regulatory compliance; +- authorization, rights, ownership, or permission merely from identity or + provenance; +- provenance merely from integrity or a matching digest; +- computational correctness merely from signature validity or registration; +- issuer independence, uniqueness, Sybil resistance, or lack of collusion; +- current validity merely from historical inclusion; +- correct policy selection merely because a policy identifier is present; +- privacy, anonymity, confidentiality, or unlinkability. + +## Two receipts, two propositions + +| Artifact | Native proposition | +|---|---| +| VSTD receipt | The declared bounded computational result and its evidence/refutation boundary. | +| SCITT COSE Receipt | A VDS property, normally inclusion of the exact Signed Statement under a TS identity and proof profile. | + +The experimental profile places the first inside the payload of a SCITT Signed +Statement and attaches the second to that statement. Implementations must name the +receipt type whenever “receipt†would be ambiguous. + +The unwrapped VSTD receipt remains checkable outside its producer. Selecting the SCITT +profile deliberately adds issuer and transparency coordinates; it is not the +default wire path for an identity-independent or witness-private VSTD profile. + +## Trust coordinates that must remain visible + +### SCITT + +- issuer key/certificate and identity interpretation; +- TS receipt-verification key and TS identity; +- VDS/VDP profile and algorithm; +- registration policy and policy version/state; +- statement subject and content type; +- registration/receipt time and freshness policy; +- key-compromise, supersession, revocation, and discovery policy; +- external native verifier implementation/version. + +### VSTD + +- claim, subject, predicate, and parameters; +- policy root and evidence root; +- artifact identities and content digests; +- verifier specification, implementation, parser, and supported fragment; +- resource bounds and prior commitment; +- certificate format, verdict, reason, and native lifecycle status; +- evidence availability, challenges, and graph ancestors when applicable. + +## Composition rule + +A composed PASS is permitted only when all of the following hold: + +1. the native VSTD checker accepts a VSTD PASS without sharing verdict-producing code; +2. the full VSTD payload digest matches the payload signed in the SCITT statement; +3. the SCITT statement signature is valid under an accepted issuer policy; +4. the SCITT Receipt is valid for that exact statement under an accepted TS/VDS + policy; +5. the SCITT subject equals the VSTD claim-coordinate subject; +6. the observed artifact digests equal the VSTD-bound artifact digests; +7. the required evidence is current and neither revoked, superseded, conflicted, + missing, nor unavailable under the declared relying-party policy. + +Any single failed condition prevents PASS. Registration never repairs a failed VSTD +claim. A VSTD PASS never fabricates missing SCITT transparency. + +## UNKNOWN and lifecycle behavior + +SCITT core does not define one application-level UNKNOWN verdict. The individual +[Composite Evidence Verification draft](https://datatracker.ietf.org/doc/draft-nobuo-scitt-composite-evidence-verification/) +proposes `unknown`, `missing`, `stale`, `conflict`, and `warning`, but it is not an +adopted WG standard and its result precedence remains draft work. + +VSTD UNKNOWN is bounded and reason-bearing. In VSTD-4, resource exhaustion, +unavailable dependencies, unavailable verifiers, and unretrievable artifacts have +distinct indeterminacy reasons. Therefore adapters must preserve both the native +SCITT condition and native VSTD reason. Label equality alone is not semantic +equivalence. + +Historical SCITT inclusion may remain valid while current VSTD usability falls. For +example, a receipt can still prove that a statement was registered in the past even +after a relying party considers its evidence stale or an ancestor revoked. The +adapter records both facts rather than deleting history or treating inclusion as +current computational validity. + +## Implementation boundary + +The module in `src/verifier/interoperability/scitt/`: + +- emits deterministic application payload bytes and a normalized registration + template; +- does **not** claim that JSON is the SCITT serialized transport format; +- requires a native RFC 9943/COSE producer to create a real Signed Statement; +- requires a native RFC 9942 verifier to validate a COSE Receipt; +- consumes the native verifier's output only under explicit issuer, subject, payload, + policy, TS, and VDS coordinates; +- requires a separately bound native VSTD checker result for the exact embedded + receipt; a receipt's declared `PASS` is not evidence that it was checked; +- returns `computational_verdict = NOT_EVALUATED` when adapting SCITT evidence alone; +- rejects unknown mappings instead of guessing. + +The example uses pinned optional libraries to create and verify real COSE bytes and +an RFC 9162 SHA-256 inclusion receipt in a local one-entry test log. That demonstrates +the cryptographic boundary but does not represent a production TS, public witness, +or public anchoring. diff --git a/docs/standards/VSTD_SCITT_CROSSWALK.md b/docs/standards/VSTD_SCITT_CROSSWALK.md new file mode 100644 index 0000000..89c85aa --- /dev/null +++ b/docs/standards/VSTD_SCITT_CROSSWALK.md @@ -0,0 +1,188 @@ +# Verifier Standard (VSTD) and Internet Engineering Task Force (IETF) Supply Chain Integrity, Transparency, and Trust (SCITT): experimental interoperability crosswalk + +> **Acronyms:** artificial intelligence (AI); application programming interface (API); +> Concise Binary Object Representation (CBOR); Confidential Consortium Framework (CCF); +> CBOR Object Signing and Encryption (COSE); CBOR Web Token (CWT); European Union (EU); +> grounded decision certificate (GDC); Hypertext Transfer Protocol (HTTP); Request for Comments (RFC); +> Supply Chain Integrity, Transparency, and Trust (SCITT); SCITT Reference APIs (SCRAPI); Transparency Service (TS); +> verifiable data structure proof (VDP); verifiable data structure (VDS); working group (WG); zero-knowledge (ZK). + +> **Status:** experimental, non-normative, reviewed against public specifications on +> 2026-08-25. This document does not alter VSTD semantics and does not imply IETF, +> SCITT Working Group, or implementation-provider endorsement. + +## Result + +The working thesis survives with one important correction: + +> **SCITT authenticates statements and makes their policy-governed registration in a +> verifiable data structure transparent and portable. VSTD is a standard domain +> language for verification: an interlingua that standardizes the claim boundary and +> portable result semantics by which a domain verifier or proof engine's bounded +> result is represented, binding-checked, refuted, mapped, and composed with adjacent +> evidence.** + +SCITT is not merely transport. It already specifies issuer/subject binding, signed +statements, registration-policy evaluation, append-only and non-equivocating +transparency, portable COSE receipts, and replayable registration audits. VSTD must +not rename those mechanisms as VSTD inventions. Conversely, SCITT explicitly allows +false statements to be registered and leaves payload truth to application-domain +semantics. VSTD does not replace those application-domain semantics or engines. It +is the general operator-language class; native verifiers, proof engines, and other +evidence substrata are the orchestrated implementations whose own outputs and limits +remain authoritative and visible. Explicit adapters make the mapping and any loss +reviewable. That is the clean VSTD-shaped boundary. + +## Sources and exact status + +| Document | Status on 2026-08-25 | Relevance | +|---|---|---| +| [RFC 9943: SCITT Architecture](https://datatracker.ietf.org/doc/html/rfc9943) | IETF Standards Track RFC, **Proposed Standard**, June 2026 | Normative SCITT architecture, Signed Statements, Registration, Receipts, Transparent Statements, and security boundary. | +| [RFC 9942: COSE Receipts](https://datatracker.ietf.org/doc/html/rfc9942) | IETF Standards Track RFC, **Proposed Standard**, June 2026 | COSE Receipt wrapper, VDS/VDP registries, RFC 9162 inclusion and consistency proof encodings. | +| [draft-ietf-scitt-scrapi-11](https://datatracker.ietf.org/doc/html/draft-ietf-scitt-scrapi-11) | **Active SCITT WG Internet-Draft**, intended Proposed Standard, in the RFC Editor Queue; not yet an RFC | HTTP registration, asynchronous completion, receipt resolution, and TS key discovery. | +| [draft-ietf-scitt-receipts-ccf-profile-04](https://datatracker.ietf.org/doc/html/draft-ietf-scitt-receipts-ccf-profile-04) | **Active SCITT WG Internet-Draft**, intended Proposed Standard, in IETF Last Call through 2026-09-07; not an RFC | CCF ledger VDS and inclusion-proof profile for COSE Receipts. | +| [draft-nobuo-scitt-composite-evidence-verification-00](https://datatracker.ietf.org/doc/draft-nobuo-scitt-composite-evidence-verification/) | **Active individual Internet-Draft**, no WG adoption or formal standing | Closest work: composite verification of statements, receipts, bindings, relationships, freshness, conflicts, and bundles under a named profile. | +| [draft-nobuo-scitt-protected-object-binding-00](https://datatracker.ietf.org/doc/draft-nobuo-scitt-protected-object-binding/) | **Active individual Internet-Draft**, no WG adoption or formal standing | Proposed object bindings and statement-graph relationships; explicitly does not establish payload truth. | +| [draft-emirdag-scitt-ai-agent-execution-00](https://datatracker.ietf.org/doc/html/draft-emirdag-scitt-ai-agent-execution-00) | **Active individual Internet-Draft**, no stream or WG adoption; its draft header says intended Informational | Agent-execution records, sequence completeness, evidence custody, and redaction receipts. | +| [draft-noa-scitt-ai-agent-receipt-01](https://datatracker.ietf.org/doc/html/draft-noa-scitt-ai-agent-receipt-01) | **Active individual Internet-Draft**, no stream or WG adoption; its draft header says Standards Track | Per-action receipt profile with narrow claims, validity/sufficiency separation, absence/indeterminacy semantics, and explicit external-world limits. | +| [draft-dawkins-scitt-ai-article50-00](https://datatracker.ietf.org/doc/html/draft-dawkins-scitt-ai-article50-00) | **Active individual Internet-Draft**, no stream or WG adoption | AI-transparency receipt profile for selected EU AI Act Article 50 disclosure claims. | +| [draft-mih-scitt-agent-action-capsule-02](https://datatracker.ietf.org/doc/html/draft-mih-scitt-agent-action-capsule-02) | **Active individual Internet-Draft**, no stream or WG adoption | Agent Action Capsule payload profile separating dispatched attempts, observed results, and human-in-the-loop records. | +| [draft-mih-scitt-agent-action-capsule-sel-disc-00](https://datatracker.ietf.org/doc/html/draft-mih-scitt-agent-action-capsule-sel-disc-00) | **Active individual Internet-Draft**, no stream or WG adoption | Selective-disclosure construction and missing-required-field behavior for Agent Action Capsules. | +| [draft-hillier-scitt-arp-03](https://datatracker.ietf.org/doc/html/draft-hillier-scitt-arp-03) | **Active individual Internet-Draft**, no stream or WG adoption | Attestation reconciliation, query binding, divergence axes, policy coordinates, and budget-exhaustion concerns. | +| [draft-dogru-scitt-disclosure-evidence-07](https://datatracker.ietf.org/doc/html/draft-dogru-scitt-disclosure-evidence-07) | **Active individual Internet-Draft**, no stream or WG adoption | Transformation evidence and coverage reconciliation, including excluded and indeterminate coverage outcomes. | +| [draft-le-scitt-derived-subjects-00](https://datatracker.ietf.org/doc/html/draft-le-scitt-derived-subjects-00) | **Active individual Internet-Draft**, no stream or WG adoption | Deterministic subject derivation across independently governed identifier schemes. | +| [draft-mih-sokolov-scitt-payload-binding-01](https://datatracker.ietf.org/doc/html/draft-mih-sokolov-scitt-payload-binding-01) | **Active individual Internet-Draft**, no stream or WG adoption | Canonical payload binding and cross-profile digest references; appraisal remains in consuming profiles. | + +Internet-Drafts are work in progress. The individual drafts above are proposals by +their authors, not IETF or SCITT WG positions. Earlier draft revisions that have been +replaced or expired were not used as current authority. None of the documents relied +on in this table is expired as of the review date. + +## Architecture decision + +The cleanest arrangement is **optional bidirectional composition with separate +verdicts**. SCITT is not a prerequisite for VSTD and is not the default publication +path for an identity-independent or witness-private VSTD profile: + +1. **VSTD inside SCITT:** a complete VSTD receipt is the application payload of an + RFC 9943 Signed Statement. The SCITT protected headers bind issuer, subject, + content type, and signature. A COSE Receipt proves registration/inclusion under + the selected TS, VDS, registration policy, key, and time assumptions. +2. **SCITT evidence inside VSTD:** output from a native SCITT verifier may be VSTD + evidence for a narrowly stated transparency proposition, such as “this exact + statement was signed by an accepted issuer and included in this TS VDS under this + policy.†It is not evidence that silently settles the statement's computational + payload. SCITT is one orchestrated substrate, not a privileged source of truth. +3. **Graph composition:** a SCITT statement-graph profile may identify registered + statements, object bindings, edges, supersession, and conflicts. VSTD-Graph can + evaluate bounded predicates over selected nodes and edges, but each graph's + native identifiers, status semantics, and policy remain visible. + +This is not recursive self-certification. SCITT and VSTD remain adjacent systems with +different trust roots and different questions. Selecting SCITT deliberately adds +issuer authentication, registration policy, transparency, and possible correlation; +omitting SCITT leaves those properties unclaimed rather than making them UNKNOWN +VSTD computational evidence. + +## Rigorous crosswalk + +| Concern | VSTD | SCITT | Overlap | Difference | Composition | +|---|---|---|---|---|---| +| Claim identity | Receipt and claim identifiers; VSTD-4 binds a claim string and coordinate. | Signed Statement bytes plus issuer/subject and payload media type identify a statement context. | Both bind an assertion to named coordinates. | SCITT identity is signed-statement identity; VSTD identity includes bounded computational semantics. | Carry the native VSTD receipt intact and bind its full payload digest in the SCITT statement. | +| Actor identity | A bounded artifact claim need not identify a natural person, creator, or persistent actor; profile-specific device/verifier/witness identifiers do not imply authorship or authority. | A Signed Statement authenticates a declared issuer under a relying-party trust policy; the issuer can be a key or pseudonym but may be linkable. | Both may bind identifiers when the declared proposition needs them. | SCITT issuer authentication is central to accountability; actor identity is not required for every VSTD computation. | Make SCITT wrapping optional and never copy issuer reputation into the native VSTD verdict. | +| Disclosure / zero knowledge | Core VSTD is disclosure-neutral; current receipts may disclose evidence, and experimental ZK profiles must supply real proof-system guarantees. | Registration makes signed statement material or commitments available under TS policy and can expose timing, subjects, and relationships. | Either can carry commitments or proofs defined by an application profile. | Neither RFC 9943 nor current VSTD core automatically provides witness confidentiality, anonymity, or unlinkability. | Treat privacy effects as an explicit profile property; do not label this full-disclosure example ZK or zero identity. | +| Artifact TRUST, ROT, and RUST | Graph history can record mechanism-earned support, challenges, staleness, supersession, revocation, refutation, and recorded ancestry; no normative scalar reputation score exists. | Logs provide durable registration history and issuer accountability, not payload reputation or truth. | Both can contribute time-indexed observations about one artifact. | Repetition, age, issuer identity, and reputation do not increase epistemic strength. ROT is current-admissibility degradation; RUST is reverse diagnostic reachability, not guilt or causal localization. | Keep any TRUST transfer, ROT derivation, or RUST traversal separately mechanism-bound and unable to upgrade a native result. | +| Subject identity | VSTD-2/VSTD-4 coordinate `subject`. | Protected CWT `sub` claim; issuer-defined and usable to correlate statements. | Both name what a claim is about. | Equal spelling does not prove equal interpretation. | Require exact subject equality under the experimental profile; reject mismatch. | +| Predicates | Explicit VSTD predicate and parameters. | Payload/application profile defines predicate semantics; SCITT core is content-agnostic. | A VSTD predicate can be a SCITT payload predicate. | SCITT core does not define the VSTD predicate. | Preserve predicate and parameters in the payload projection and full receipt. | +| Parameters | Bound into VSTD claim coordinates and canonical receipt. | May appear in opaque payload or profile-defined protected fields. | Both can integrity-bind parameters. | SCITT has no generic computational-parameter semantics. | Keep parameters in VSTD payload; only promote selected values to protected headers after profile review. | +| Explicit limits | VSTD claim limitations, excluded claims, and refutation surface. | RFC 9943 states architectural/security limits; application payload profiles may add limits. | Both can document scope. | VSTD makes per-result bounds part of verification semantics. | Carry VSTD limits without translating them into SCITT registration-policy claims. | +| Issuer identity | May occur in provenance, but VSTD core does not replace signing identity infrastructure. | Protected `iss`; signature and trust-anchor validation are mandatory registration concerns. | Both may record a producer. | SCITT owns signed issuer authentication; VSTD ownership/authorship is not inferred from integrity. | Reuse SCITT issuer authentication and keep it separate from VSTD computational outcome. | +| Signatures | VSTD can consume signature evidence; it does not define a universal signing system. | COSE_Sign1 is normative for Signed Statements and Receipts. | VSTD can reference verified signature evidence. | SCITT already standardizes the envelope and signature placement. | When the SCITT profile is selected, use SCITT/COSE rather than inventing a competing envelope. | +| Artifact binding | VSTD binds content-addressed subjects/evidence roots and checks wrong-artifact cases. | `sub`, payload hashes/detached payloads, and signed envelope bind statements to declared artifacts. | Both defend substitution through different mechanisms. | SCITT proves what bytes/subject the issuer signed, not that VSTD evaluated the intended artifact correctly. | Require exact VSTD artifact digests and SCITT payload digest; either mismatch fails composition. | +| Statement registration | Not a VSTD core function. | TS applies registration policy, inserts the statement, and issues a receipt. | None needed. | SCITT already owns this function. | VSTD should consume the result, not recreate registration. | +| Transparency | VSTD can record published artifacts but defines no generic transparency service. | Core objective: auditable, accountable signed-content transparency. | VSTD receipts are suitable transparent payloads. | SCITT provides the standardized transparency machinery. | Register through SCITT when public accountability is desired; do not require it for identity-independent/private verification. | +| Append-only logs | VSTD-Graph records additive challenge history but is not a general public log protocol. | SCITT VDS must be append-only, non-equivocating, and replayable. | Both avoid rewriting history. | SCITT defines the log/VDS guarantees and receipts. | Use SCITT VDS rather than a VSTD-specific transparency log. | +| Portable receipts | VSTD receipts carry computational evidence and bounds. | COSE Receipts carry signed VDS proofs and attach to Transparent Statements. | Both produce portable evidence artifacts. | “Receipt†names different proof targets. | Name both explicitly: VSTD computational receipt inside a SCITT Signed Statement; SCITT COSE Receipt outside it. | +| Evidence bundles | VSTD receipts and graph collections may contain evidence references. | Core permits payloads; composite-evidence draft proposes bundles under profiles. | Both can package evidence sets. | The SCITT bundle model is currently an individual proposal, not a WG standard. | Use a VSTD payload now; discuss bundle alignment before standardizing graph exchange. | +| Provenance graphs | VSTD-Graph records typed artifact/transformation lineage and computes candidate degradation from statuses already recorded in the Graph. | RFC 9943 correlates statements by subject; individual drafts propose object bindings and statement graphs. | Both can connect evidence about shared subjects. | SCITT core does not standardize the proposed statement-graph vocabulary; VSTD lineage is not causal proof. | Reference native SCITT statement IDs from VSTD-Graph without rewriting either graph. | +| Statement graphs | VSTD-Graph has implemented graph structures, policy queries, and candidate-profile computation over caller-supplied ratings; conformance is `NOT_ESTABLISHED`. | Proposed by individual object-binding/composite drafts. | Both need explicit edge semantics and policy. | Maturity and graph objects differ. | Experimental bridge only; no claim of SCITT WG alignment. | +| Dependencies | VSTD-4 can return `UNKNOWN/DEPENDENCY_UNAVAILABLE`; Graph evaluates transitive ancestors. | Composite draft proposes required statements and dependency edges. | Both surface unavailable dependencies. | SCITT core receipt validity does not settle application dependency completeness. | Preserve the native missing reason and let VSTD issue its own bounded indeterminacy certificate. | +| Revocation | The challenge ledger can derive claim state. Graph candidate computation degrades when an ancestor already records `REVOKED`. No adapter binds the first result into the second, so challenge-to-Graph propagation is `NOT_ESTABLISHED`. | RFC 9943 discusses compromised-key handling but leaves revocation strategies out of scope; individual composite draft proposes revocation statements/checks. | Both can react to invalidated evidence. | Neither SCITT core nor current VSTD supplies the missing cross-surface propagation mechanism. | Preserve each native state. A future adapter must bind the exact claim, artifact, event, and policy before a relying party changes Graph state. | +| Supersession | VSTD-Graph records `SUPERSEDED` without automatically making the older node inadmissible. | RFC 9943 permits later same-issuer/same-subject statements to supersede earlier ones; selection is relying-party policy. | Both preserve history. | Neither makes “newer†automatically “truerâ€; policy consequences differ. | Normalize `SUPERSEDED` without upgrading; require explicit current-evidence policy. | +| Conflicts | VSTD preserves `CONFLICTED` where defined and graph blockers. | RFC 9943 allows conflicting issuers; individual composite draft proposes `conflict`. | Both refuse silent reconciliation. | SCITT core delegates issuer selection; VSTD may express a bounded conflict result. | Preserve `CONFLICTED` as distinct from UNKNOWN and FAIL. | +| Freshness | VSTD bounds and evidence can include time/freshness; stale Graph artifacts are inadmissible to candidate Graph profiles. | Receipt state is true when issued; keys/policies can change; application policies determine freshness. SCRAPI can issue fresh receipts. | Both require time-indexed trust coordinates. | Inclusion is historical; it does not establish current payload validity. | Carry registration time, policy, key/VDS, and freshness decision separately. | +| Verification profiles | VSTD numbered profiles and verifier descriptors define supported fragments. | RFC 9942 defines VDS profiles; RFC 9943 permits application profiles; composite draft proposes named verification profiles. | Both use explicit capability/profile identifiers. | VDS proof profile is not computational predicate profile. | Bind both profile identifiers; never collapse them. | +| Resource bounds | VSTD-4 preflights verification cost, memory, and certificate size. | SCITT core has operational limits but no payload-domain computational-verdict resource model. | Both can reject over-limit inputs operationally. | SCRAPI 429/204 is protocol state, not epistemic UNKNOWN. | Keep VSTD bounds in payload and preserve resource exhaustion as VSTD UNKNOWN. | +| Computational grounding | VSTD-4 binds variables/clauses to facts, subjects, rules, policy/evidence roots, and verifier code. | SCITT can register such a payload but does not define those semantics. | SCITT can integrity-protect grounding artifacts. | Grounding correctness is distinctively VSTD here. | SCITT carries and makes the grounded certificate transparent; VSTD kernel checks it. | +| Reproduction | VSTD declares reproduction-fidelity states and executable falsification paths. | SCITT auditors reproduce registration checks from retained statements, collateral, policy, and trust anchors. | Both support independent replay. | They replay different decisions. | Report `VSTD_CHECK_REPLAY` and `SCITT_REGISTRATION_REPLAY` separately. | +| Checker separation | VSTD has a small checker isolated from verdict-producing code. | SCITT relying parties verify issuer signatures and COSE Receipts offline; auditors check VDS consistency. | Both support separately executable checks. | The checked proposition differs, and neither mechanism alone establishes distinct producer/checker actors. | Demonstrate both checks in sequence, retain both native results, and reserve “independently verified†for evidence-bound actor and execution separation. | +| Counterexamples | VSTD FAIL can carry a counterexample or refutation certificate. | SCITT receipt invalidity can carry verification failure, but core does not define domain counterexamples. | Both can expose detected failure. | A bad inclusion proof is not a counterexample to payload truth. | Keep SCITT integrity failure and VSTD predicate refutation as typed failures. | +| PASS | Bounded proposition accepted with its required certificate/evidence. | Core SCITT has verified signature/receipt/registration, not a generic application `PASS`; the individual composite draft proposes profile `pass`. | Both can have successful checks. | The success domains are not equivalent. | Composed PASS requires native VSTD PASS and exact current SCITT verification; SCITT alone never creates it. | +| FAIL | Evidenced predicate violation or rejected certificate, depending on the VSTD result surface. | Signature, receipt, inclusion, policy, or profile verification can fail. | Both can detect concrete failures. | Failure reasons apply to different mechanisms and coordinates. | Preserve native reason codes and identify which mechanism and coordinate failed. | +| UNKNOWN | Bounded inability to decide, with VSTD-4 indeterminacy evidence. | No core RFC application verdict; individual composite draft uses `unknown` for unavailable evidence or unrecognized profile and separates missing/stale/conflict. | Both reject guessing. | They are not semantically equivalent. | See the taxonomy below; map by reason, never by label alone. | +| Warnings | VSTD warnings cannot silently supply a missing coordinate or verdict. | Individual composite draft proposes `warning` when mandatory checks pass but a condition is surfaced. | Both can retain nonfatal findings. | A warning's acceptability is profile-specific. | Preserve warnings; do not map warning to VSTD PASS without full native VSTD verification. | +| Cost/work claims | VSTD binds/checks verification work and receipt size at VSTD-4. | SCITT proves VDS properties; its protocol latency/status does not prove application checking cost. | Receipts can carry cost claims as payload data. | SCITT has no generic proof of VSTD work. | Carry the VSTD bound and checker result as payload semantics. | +| Graph degradation | VSTD-Graph recomputes candidate profiles and blast radius without mutating history. | SCITT core preserves log history; individual graph draft proposes revocation/supersession/conflict checks. | Both favor additive history. | SCITT inclusion remains true even if a payload becomes disfavored; VSTD evidence ceiling may fall. | Keep historical inclusion true while lowering the current VSTD composition result. | +| Real-world truth vs evidence validity | VSTD explicitly limits arbitrary truth claims to its declared evidence and predicate. | RFC 9943 states registration only proves the statement was produced by an issuer; issuers may be false. | Strong agreement on non-upgrade. | VSTD additionally specifies a checkable bounded computational proposition. | This is the central composition boundary. | + +## UNKNOWN is not one shared enum + +| Condition | SCITT core / draft treatment | VSTD treatment | Composition | +|---|---|---|---| +| Evidence unavailable | Core receipt may remain historically valid; individual composite draft: `unknown` or `missing`. | `UNKNOWN/DEPENDENCY_UNAVAILABLE` or `ARTIFACT_UNRETRIEVABLE` when relevant. | UNKNOWN with both native reasons. | +| Incomplete bundle | Not a core RFC verdict; composite draft: `missing`. | UNKNOWN if required VSTD evidence is absent. | UNKNOWN, never PASS from registration alone. | +| Resource budget exhausted | SCRAPI 204/429 are protocol/operation states, not application truth. | `UNKNOWN/PROOF_BOUND_EXCEEDED` or `DEPTH_BOUND_EXCEEDED`. | Preserve VSTD UNKNOWN even if the statement is registered. | +| Predicate not established within the declared bound | SCITT core has no payload-domain undecidability result; the individual composite draft's `unknown` is profile/evidence-oriented. | A bounded VSTD check remains UNKNOWN with the native verifier reason; it is not proof that the predicate is globally undecidable. | Preserve the bounded inability to establish, without widening it into global undecidability or narrowing it into FAIL. | +| Unsupported verification method/profile | A relying party cannot verify; composite draft: `unknown` for unrecognized profile. | `UNSUPPORTED` or `UNKNOWN/VERIFIER_UNAVAILABLE`, depending on mechanism and coordinate. | UNKNOWN or explicit UNSUPPORTED; no guess. | +| Conflicting evidence | RFC 9943 permits conflicting statements; relying-party selection is external. Composite draft: `conflict`. | `CONFLICTED` where applicable. | CONFLICTED, not generic UNKNOWN. | +| Stale evidence | Application policy; composite draft: `stale`. | `STALE` graph status or a bounded freshness failure. | Retain STALE and cap current composition. | +| Revoked ancestor/key | Key-compromise response is discussed; universal revocation strategy is out of scope. | A recorded `REVOKED` ancestor lowers the Graph candidate and exposes blast radius; no challenge-to-Graph mutation is implemented. | Preserve historical inclusion and both native states; change current VSTD admissibility only through an explicit binding policy. | +| Failed proof | Invalid SCITT signature/receipt/inclusion is concrete integrity failure. | Invalid decision certificate or evidenced counterexample is FAIL/rejection. | FAIL at the failing mechanism and coordinate, not UNKNOWN. | + +## What SCITT already does well + +- COSE Signed Statements and Receipt attachment. +- Protected issuer and subject coordinates. +- Registration policies and auditable policy history. +- Append-only, non-equivocating VDS requirements. +- Portable, offline-verifiable inclusion receipts. +- Registration/receipt APIs through the active SCRAPI WG draft. +- Multiple issuers, multiple TSs, and historical supersession without claiming + arbitrary payload truth. + +VSTD should reuse these mechanisms rather than define another signature envelope, +transparency log, receipt-attachment convention, or registration API. + +## Current overlap and the narrower VSTD contribution + +Several active **individual** SCITT drafts now address concerns that must not be +marketed as uniquely VSTD: narrow claim boundaries, validity versus sufficiency, +missing/stale/conflicted evidence, statement graphs, evidence bundles, selective +disclosure, canonical payload binding, coverage reconciliation, and typed +application-profile outcomes. They remain work in progress without WG adoption, but +their technical overlap is real. + +The narrower contribution demonstrated by the current VSTD implementation is not a +new domain prover. It is a standard domain language and operator/result layer over +orchestrated native verifier instances: + +- one domain-general claim coordinate for a computational predicate and parameters; +- an implemented `VSTD4-GDC-1` grounded decision certificate binding proof variables + and clauses to named facts, subjects, policy/evidence roots, verifier code, and + resource ceilings; +- a small checker isolated from verdict-producing code that returns evidence-bearing PASS, FAIL, or bounded + UNKNOWN and refuses over-budget work before proof replay; +- separate refutation/challenge and Graph candidate-degradation mechanisms, with + cross-axis propagation explicitly `NOT_ESTABLISHED`; and +- an adapter that requires separately bound native VSTD and native SCITT verifier + results, so neither declared payload success nor registration can create PASS. + +These are implementation and composition distinctions, not a claim that nobody else +has proposed related semantics. + +## Positioning sentence + +> **SCITT can authenticate and make a VSTD receipt's registration transparently +> auditable; VSTD supplies the verification interlingua that preserves the bounded +> claim boundary and portable result semantics of the native verifier or proof engine +> that produced the result.** diff --git a/examples/scitt_interop/README.md b/examples/scitt_interop/README.md new file mode 100644 index 0000000..0662336 --- /dev/null +++ b/examples/scitt_interop/README.md @@ -0,0 +1,119 @@ +# Verifier Standard (VSTD)/Supply Chain Integrity, Transparency, and Trust (SCITT) cryptographic interoperability example + +> **Acronyms:** Concise Binary Object Representation (CBOR); CBOR Object Signing and Encryption (COSE); grounded decision certificate (GDC); +> Internet Engineering Task Force (IETF); Request for Comments (RFC); Secure Hash Algorithm 256-bit (SHA-256); +> verifiable data structure (VDS). + +> **Experimental and non-normative.** This example creates real COSE signatures and +> an RFC 9162 SHA-256 inclusion receipt in a local one-entry test log. +> It does not operate a production SCITT Transparency Service, publish to a public +> log, or demonstrate third-party monitoring. + +## What it proves + +The example executes this chain: + +```text +artifact bytes + -> grounded VSTD4-GDC-1 digest predicate + -> separately implemented kernel check returns PASS + -> deterministic experimental VSTD/SCITT payload + -> RFC 9943-style EdDSA COSE Signed Statement + -> RFC 9942 / RFC9162_SHA256 signed inclusion receipt + -> offline statement-signature and receipt verification + -> composed result preserving both native verdicts +``` + +It proves, under the emitted public keys and local test-log policy, that the exact +Signed Statement is authentic and included in the one-entry VDS, and that the exact +embedded VSTD certificate passes the separately implemented kernel check for the artifact digest +predicate. The enclosing VSTD-4 candidate depth is a structural result with conformance +`NOT_ESTABLISHED`; this example does not establish VSTD-4 conformance or VSTD-5 +readiness or distinct producer/checker actors. It also does not prove artifact safety, +production-service registration, +public witnessing, issuer authority outside the test, or arbitrary payload truth. + +## Identity and privacy boundary + +The VSTD receipt is produced and checkable before SCITT is applied. This example +then deliberately adds a fixed issuer, signature, subject, registration time, and +transparency-service coordinate because those are part of the selected SCITT +profile. It is therefore **not** a zero-identity or zero-knowledge example: the +payload is disclosed, and the issuer and statement can be correlated. SCITT is an +optional accountability wrapper here, not a prerequisite for VSTD verification. + +Before issuing the local receipt, the example policy verifies the statement +signature and requires the exact test issuer, VSTD subject, payload content type, +and experimental profile identifier. The policy identifier is retained in the +normalized SCITT observation. + +## Setup + +From the repository root: + +```bash +python -m pip install -e ".[scitt]" +``` + +The optional extra is pinned in `pyproject.toml`: + +- `scitt-cose==0.2.2` +- `cbor2==6.1.4` +- `cryptography==50.0.0` + +`scitt-cose` is a separately maintained implementation, not an IETF publication or +endorsement. The normative wire references are [RFC 9943](https://datatracker.ietf.org/doc/html/rfc9943), [RFC 9942](https://datatracker.ietf.org/doc/html/rfc9942), RFC 9052/9053, and RFC 9162. + +## Produce and verify + +```bash +python examples/scitt_interop/demo.py produce +python examples/scitt_interop/demo.py verify +``` + +The producer writes a deterministic canonical VSTD payload plus real COSE artifacts +under `generated/`. Fresh ephemeral signing keys are generated on each production +run, so the public keys, signatures, and their hashes intentionally change. The +checked-in specimen remains deterministically verifiable, but producing a new +specimen is not byte-reproducible without externally managed fixed keys. The verifier reads +only those artifacts, the two public keys, the local artifact, and the documented +trust coordinates. No private key is written or committed. The ephemeral keys have +no authority outside this example. + +## Generated artifacts + +| File | Meaning | +|---|---| +| `vstd_receipt.json` | VSTD-4 structural candidate receipt and grounded decision certificate; conformance is `NOT_ESTABLISHED`. | +| `vstd_scitt_payload.json` | Canonical application payload bytes carried by SCITT. | +| `registration_template.json` | Human-readable normalized input; explicitly **not** COSE. | +| `signed_statement.cose` | Real COSE_Sign1 Signed Statement. | +| `receipt.cose` | Real signed RFC9162_SHA256 inclusion receipt. | +| `transparent_statement.cose` | Signed Statement with receipt attached at COSE header label 394. | +| `issuer_public.pem` | Public key for offline statement-signature verification. | +| `log_public.pem` | Public key for offline receipt verification. | +| `verification_result.json` | Native VSTD candidate-check result, explicit VSTD conformance `NOT_ESTABLISHED`, native SCITT observation, scoped composition, and hashes. | + +## Adversarial coverage + +`tests/test_scitt_interop.py` and `tests/test_scitt_crypto_example.py` cover: + +- deterministic serialization and round trips; +- identity, claim-coordinate, artifact, and payload binding; +- valid SCITT registration with VSTD FAIL or UNKNOWN; +- missing, stale, revoked, superseded, conflicted, and unsupported evidence; +- wrong issuer/subject and unaccepted policy coordinates; +- malformed payloads and version mismatches; +- corrupted COSE statement and receipt bytes; +- the invariant that SCITT-only evidence returns + `computational_verdict = NOT_EVALUATED`. +- the invariant that a composed PASS requires a native VSTD checker result bound to + the exact embedded receipt; +- the invariant that the native VSTD payload contains no SCITT issuer, transparency + service, registration policy, or registration time. + +Run: + +```bash +python -m pytest -q tests/test_scitt_interop.py tests/test_scitt_crypto_example.py +``` diff --git a/examples/scitt_interop/artifact.txt b/examples/scitt_interop/artifact.txt new file mode 100644 index 0000000..3518a08 --- /dev/null +++ b/examples/scitt_interop/artifact.txt @@ -0,0 +1 @@ +VSTD and SCITT compose without semantic upgrading. diff --git a/examples/scitt_interop/demo.py b/examples/scitt_interop/demo.py new file mode 100644 index 0000000..ea77bbf --- /dev/null +++ b/examples/scitt_interop/demo.py @@ -0,0 +1,496 @@ +"""Terminology: Concise Binary Object Representation (CBOR); +CBOR Object Signing and Encryption (COSE); CBOR Web Token (CWT); +grounded decision certificate (GDC); Request for Comments (RFC); +Supply Chain Integrity, Transparency, and Trust (SCITT); Secure Hash Algorithm 256-bit (SHA-256); +verifiable data structure (VDS); Verifier Standard (VSTD). + +Cryptographic VSTD/SCITT interoperability specimen with a deterministic application +payload and ephemeral-key COSE artifacts. + +The optional ``scitt`` extra supplies COSE and RFC 9162 receipt primitives. A +one-entry local test log is used so the example is self-contained. This is a +real signed statement, signed inclusion receipt, and offline native verification; +it is not distinct-actor verification, a production Transparency Service, public +anchoring, or endorsement. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from dataclasses import replace +from pathlib import Path +from typing import Any + +from verifier.core.certificate import ( + CertificateHeader, + ClaimBinding, + ClaimCoordinate, + ClauseGrounding, + CostTier, + DecisionBlock, + DecisionCertificate, + EncodingRule, + GroundedFact, + Grounding, + ResourceBounds, + VariableGrounding, + Verdict, + VerifierDescriptor, + canonical_bytes, + canonical_digest, + certificate_from_dict, +) +from verifier.core.kernel import KernelOutcome, check, reference_descriptor +from verifier.interoperability.scitt import ( + EXPERIMENTAL_CONTENT_TYPE, + EXPERIMENTAL_PROFILE, + ScittEvidenceState, + ScittVerificationEvidence, + VstdCoordinates, + VstdScittPayload, + VstdVerificationEvidence, + VstdVerificationState, + compose_results, + consume_scitt_evidence, + create_scitt_registration_template, +) + + +HERE = Path(__file__).resolve().parent +ARTIFACT = HERE / "artifact.txt" +ISSUER = "https://issuer.example/vstd-scitt-demo" +LOCAL_LOG = "urn:example:vstd-scitt-local-test-log" +POLICY = "urn:example:vstd-scitt-registration-policy:v1" + + +def _crypto(): + try: + import cbor2 + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import ed25519 + from scitt_cose import ( + attach_receipts, + build_receipt, + build_signed_statement, + extract_receipts, + merkle_root, + parse_signed_statement, + sign_sign1, + verify_receipt, + ) + except ImportError as exc: # pragma: no cover - exercised in base environment + raise SystemExit( + "Install the pinned optional dependencies with: " + "python -m pip install -e '.[scitt]'" + ) from exc + return { + "cbor2": cbor2, + "serialization": serialization, + "ed25519": ed25519, + "attach_receipts": attach_receipts, + "build_receipt": build_receipt, + "build_signed_statement": build_signed_statement, + "extract_receipts": extract_receipts, + "merkle_root": merkle_root, + "parse_signed_statement": parse_signed_statement, + "sign_sign1": sign_sign1, + "verify_receipt": verify_receipt, + } + + +def _sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _public_key_pair(): + crypto = _crypto() + serialization = crypto["serialization"] + key = crypto["ed25519"].Ed25519PrivateKey.generate() + private_pem = key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + public_pem = key.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + return private_pem, public_pem + + +def _claim_binding_from_dict(value: dict[str, Any]) -> ClaimBinding: + """Reconstruct the exact receipt binding for separate kernel checking.""" + + coordinate = value["coordinate"] + bounds = value["bounds"] + verifier = value["verifier"] + return ClaimBinding( + claim=value["claim"], + coordinate=ClaimCoordinate( + coordinate["subject"], + coordinate["predicate"], + dict(coordinate["parameters"]), + ), + policy_root=value["policy_root"], + evidence_root=value["evidence_root"], + verifier=VerifierDescriptor( + specification_hash=verifier["specification_hash"], + implementation_hash=verifier["implementation_hash"], + parser_hash=verifier["parser_hash"], + certificate_format=verifier["certificate_format"], + format_fragment=verifier["format_fragment"], + dependencies=tuple(verifier["dependencies"]), + deterministic=verifier["deterministic"], + ), + bounds=ResourceBounds( + bounds["verification_cost_bound"], + bounds["memory_bound"], + bounds["certificate_size_bound"], + ), + prior_commitment=value["prior_commitment"], + ) + + +def _apply_local_registration_policy( + parsed: dict[str, Any], coordinates: VstdCoordinates +) -> None: + """Minimal explicit policy applied before the local log issues a receipt.""" + + if parsed.get("signature_verified") is not True: + raise RuntimeError("registration policy rejected an unverified statement") + if parsed.get("issuer") != ISSUER: + raise RuntimeError("registration policy rejected the issuer") + if parsed.get("subject") != coordinates.subject: + raise RuntimeError("registration policy rejected the subject") + if parsed.get("content_type") != EXPERIMENTAL_CONTENT_TYPE: + raise RuntimeError("registration policy rejected the payload content type") + if parsed.get("claims", {}).get("vstd_profile") != EXPERIMENTAL_PROFILE: + raise RuntimeError("registration policy rejected the VSTD profile") + + +def build_vstd_receipt() -> tuple[dict[str, Any], VstdCoordinates]: + artifact_digest = _sha256(ARTIFACT.read_bytes()) + subject = f"artifact:sha256:{artifact_digest}" + predicate = "content_digest_matches" + formula = ((1,),) + rule = EncodingRule("RULE:ASSERT_DIGEST_MATCH", ("artifact",), ((1, "artifact"),)) + grounding = Grounding( + variables=( + VariableGrounding( + 1, GroundedFact(subject, predicate, "MATCH") + ), + ), + clauses=( + ClauseGrounding(0, rule.rule_id, {"artifact": 1}, {"artifact": subject}), + ), + rules=(rule,), + ) + binding = ClaimBinding( + claim="the named artifact bytes have the declared SHA-256 digest", + coordinate=ClaimCoordinate( + subject, predicate, {"algorithm": "sha-256", "digest": artifact_digest} + ), + policy_root=canonical_digest( + {"algorithm": "sha-256", "predicate": predicate} + ), + evidence_root=artifact_digest, + verifier=reference_descriptor(), + bounds=ResourceBounds(100, 10, 20000), + ) + certificate = DecisionCertificate( + CertificateHeader( + Verdict.PASS, + CostTier.UP, + n_vars=1, + clause_count=1, + literal_count=1, + step_count=0, + binding=binding.digest(), + ), + formula, + grounding, + DecisionBlock(model={1: True}), + ) + result = check(certificate, budget=100, binding=binding) + if result.outcome is not KernelOutcome.ACCEPTED or result.verdict is not Verdict.PASS: + raise RuntimeError(f"VSTD kernel did not accept demo certificate: {result}") + + receipt = { + "schema_version": "VSTD-4", + "receipt_id": "VFY-4-scitt-interop-demo", + "claim_id": "SCITT-INTEROP-DEMO-DIGEST", + "binding": binding.to_dict(), + "vstd4_depth": 14, + "conformance_status": "NOT_ESTABLISHED", + "rung_evidence": { + f"4.{index}": f"decision_certificate:{certificate.digest()}#4.{index}" + for index in range(1, 15) + }, + "witness": certificate.to_dict(), + "ceiling_refutation": None, + "blocking_rungs": [], + "status": "VALID", + "refutation_surface": { + "admissible_refutations": [ + "artifact bytes hash to a value other than the bound digest", + "the VSTD decision certificate fails separate kernel checking", + ], + "excluded_claims": [ + "artifact safety", + "issuer authorization", + "truth outside the bounded digest predicate", + ], + }, + } + receipt_digest = _sha256(canonical_bytes(receipt)) + coordinates = VstdCoordinates( + receipt_id=receipt["receipt_id"], + schema_version=receipt["schema_version"], + claim_id=receipt["claim_id"], + subject=subject, + predicate=predicate, + parameters={"algorithm": "sha-256", "digest": artifact_digest}, + native_result=result.verdict.value, + native_canonical_digest=receipt_digest, + evidence_bounds=binding.bounds.to_dict(), + artifact_digests={"primary": artifact_digest}, + provenance_references=("urn:example:vstd-scitt-demo:artifact",), + ) + return receipt, coordinates + + +def produce( + output: Path, *, vstd_binding_tamper: bool = False +) -> dict[str, Any]: + crypto = _crypto() + receipt, coordinates = build_vstd_receipt() + if vstd_binding_tamper: + receipt["witness"]["header"]["binding"] = "0" * 64 + coordinates = replace( + coordinates, + native_canonical_digest=_sha256(canonical_bytes(receipt)), + ) + template = create_scitt_registration_template( + receipt, coordinates, issuer=ISSUER, subject=coordinates.subject + ) + payload_bytes = template.payload.to_bytes() + + # Generate fresh, memory-only private keys. The public keys are emitted + # as explicit trust coordinates; private key material is never committed + # or written to the output directory. + issuer_private, issuer_public = _public_key_pair() + log_private, log_public = _public_key_pair() + issuer_kid = hashlib.sha256(issuer_public).digest() + log_kid = hashlib.sha256(log_public).digest() + statement = crypto["build_signed_statement"]( + payload_bytes, + alg="EdDSA", + private_key_pem=issuer_private, + issuer=ISSUER, + subject=coordinates.subject, + content_type=EXPERIMENTAL_CONTENT_TYPE, + extra_cwt_claims={"vstd_profile": EXPERIMENTAL_PROFILE}, + kid=issuer_kid, + ) + _apply_local_registration_policy( + crypto["parse_signed_statement"]( + statement, public_key_pem=issuer_public + ), + coordinates, + ) + tree_entries = [statement.hex()] + base_receipt = crypto["build_receipt"]( + leaf_entry_hex=statement.hex(), + leaf_index=0, + tree_entries_hex=tree_entries, + alg="EdDSA", + log_private_key_pem=log_private, + ) + # The generic RFC 9942 builder supplies the VDS proof. Re-sign the same + # detached root with RFC 9943's mandatory protected CWT issuer/subject + # claims so this specimen is also a SCITT Receipt, not only a COSE Receipt. + decoded_base = crypto["cbor2"].loads(base_receipt) + root = bytes.fromhex(crypto["merkle_root"](tree_entries)) + scitt_receipt = crypto["sign_sign1"]( + root, + alg="EdDSA", + private_key_pem=log_private, + protected={ + 4: log_kid, + 15: {1: LOCAL_LOG, 2: coordinates.subject}, + 395: 1, + }, + unprotected=decoded_base.value[1], + detached=True, + ) + transparent = crypto["attach_receipts"](statement, [scitt_receipt]) + + output.mkdir(parents=True, exist_ok=True) + (output / "vstd_receipt.json").write_text( + json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + (output / "vstd_scitt_payload.json").write_bytes(payload_bytes + b"\n") + (output / "registration_template.json").write_text( + json.dumps(template.to_dict(), indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + (output / "signed_statement.cose").write_bytes(statement) + (output / "receipt.cose").write_bytes(scitt_receipt) + (output / "transparent_statement.cose").write_bytes(transparent) + (output / "issuer_public.pem").write_bytes(issuer_public) + (output / "log_public.pem").write_bytes(log_public) + return verify(output) + + +def verify(output: Path, *, vstd_budget: int = 100) -> dict[str, Any]: + crypto = _crypto() + payload_bytes = (output / "vstd_scitt_payload.json").read_bytes().rstrip(b"\n") + payload = VstdScittPayload.from_bytes(payload_bytes) + statement = (output / "signed_statement.cose").read_bytes() + scitt_receipt = (output / "receipt.cose").read_bytes() + transparent = (output / "transparent_statement.cose").read_bytes() + issuer_public = (output / "issuer_public.pem").read_bytes() + log_public = (output / "log_public.pem").read_bytes() + + try: + parsed = crypto["parse_signed_statement"]( + statement, public_key_pem=issuer_public + ) + statement_structure = crypto["cbor2"].loads(statement) + statement_protected = crypto["cbor2"].loads(statement_structure.value[0]) + except Exception as exc: + raise RuntimeError("malformed SCITT Signed Statement") from exc + receipt_result = crypto["verify_receipt"]( + scitt_receipt, + leaf_entry_hex=statement.hex(), + log_public_key_pem=log_public, + ) + attached = crypto["extract_receipts"](transparent) + receipt_structure = crypto["cbor2"].loads(scitt_receipt) + receipt_protected = crypto["cbor2"].loads(receipt_structure.value[0]) + if parsed["signature_verified"] is not True: + raise RuntimeError("SCITT Signed Statement signature did not verify") + _apply_local_registration_policy(parsed, payload.coordinates) + if parsed["payload"] != payload_bytes: + raise RuntimeError("SCITT Signed Statement payload changed") + if parsed["issuer"] != ISSUER or parsed["subject"] != payload.coordinates.subject: + raise RuntimeError("SCITT Signed Statement identity coordinates changed") + if parsed["content_type"] != EXPERIMENTAL_CONTENT_TYPE: + raise RuntimeError("SCITT Signed Statement content type changed") + if statement_protected.get(4) != hashlib.sha256(issuer_public).digest(): + raise RuntimeError("SCITT Signed Statement key identifier changed") + if not receipt_result.ok: + raise RuntimeError(f"COSE Receipt failed: {receipt_result.errors}") + if receipt_protected.get(15) != { + 1: LOCAL_LOG, + 2: payload.coordinates.subject, + }: + raise RuntimeError("SCITT Receipt issuer/subject claims changed") + if receipt_protected.get(4) != hashlib.sha256(log_public).digest(): + raise RuntimeError("SCITT Receipt key identifier changed") + if attached != [scitt_receipt]: + raise RuntimeError("Transparent Statement did not preserve its receipt") + + native_receipt = json.loads((output / "vstd_receipt.json").read_text()) + certificate = certificate_from_dict(native_receipt["witness"]) + binding = _claim_binding_from_dict(native_receipt["binding"]) + vstd_result = check(certificate, budget=vstd_budget, binding=binding) + if vstd_result.outcome is KernelOutcome.ACCEPTED: + vstd_state = VstdVerificationState.VERIFIED + if vstd_result.verdict is None: + raise RuntimeError("VSTD checker returned no native verdict") + native_vstd_result = vstd_result.verdict.value + elif vstd_result.outcome is KernelOutcome.REFUSED: + vstd_state = VstdVerificationState.INDETERMINATE + native_vstd_result = "UNKNOWN" + else: + vstd_state = VstdVerificationState.REJECTED + native_vstd_result = "REJECTED" + + vstd_observation = VstdVerificationEvidence( + state=vstd_state, + receipt_sha256=_sha256(canonical_bytes(native_receipt)), + native_result=native_vstd_result, + checker="verifier.core.kernel.check", + verification_profile="VSTD4-GDC-1/reference-kernel", + reason=vstd_result.details, + ) + + observation = ScittVerificationEvidence( + state=ScittEvidenceState.REGISTERED, + statement_sha256=_sha256(statement), + payload_sha256=_sha256(payload_bytes), + issuer=parsed["issuer"], + subject=parsed["subject"], + signed_statement_verified=True, + receipt_verified=True, + verification_profile="RFC9943+RFC9942/RFC9162_SHA256", + registration_policy=POLICY, + transparency_service=LOCAL_LOG, + vds="RFC9162_SHA256", + native_result="SIGNED_STATEMENT_AND_INCLUSION_RECEIPT_VERIFIED", + reason=( + "local one-entry test log; cryptographic inclusion verified, " + "without public anchoring or production-service claims" + ), + registered_at="2026-08-23T00:00:00Z", + ) + composition = compose_results( + payload, + vstd_observation, + observation, + artifact_digests={"primary": _sha256(ARTIFACT.read_bytes())}, + accepted_issuers=[ISSUER], + ) + expected_composition = { + KernelOutcome.ACCEPTED: "PASS", + KernelOutcome.REFUSED: "UNKNOWN", + KernelOutcome.REJECTED: "FAIL", + }[vstd_result.outcome] + if composition.status.value != expected_composition: + raise RuntimeError(f"composition failed: {composition}") + + scitt_as_vstd_evidence = consume_scitt_evidence( + observation, + expected_payload_sha256=payload.payload_sha256(), + expected_subject=payload.coordinates.subject, + accepted_issuers=[ISSUER], + ) + + result = { + "vstd_kernel": vstd_result.to_dict(), + "vstd_observation": vstd_observation.to_dict(), + "scitt_observation": observation.to_dict(), + "scitt_as_vstd_evidence": scitt_as_vstd_evidence, + "composition": composition.to_dict(), + "artifact_sha256": _sha256(ARTIFACT.read_bytes()), + "payload_sha256": _sha256(payload_bytes), + "statement_sha256": _sha256(statement), + "receipt_sha256": _sha256(scitt_receipt), + "transparent_statement_sha256": _sha256(transparent), + } + (output / "verification_result.json").write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + return result + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("command", choices=("produce", "verify")) + parser.add_argument("--output", type=Path, default=HERE / "generated") + parser.add_argument("--vstd-budget", type=int, default=100) + args = parser.parse_args() + result = ( + produce(args.output) + if args.command == "produce" + else verify(args.output, vstd_budget=args.vstd_budget) + ) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/scitt_interop/generated/issuer_public.pem b/examples/scitt_interop/generated/issuer_public.pem new file mode 100644 index 0000000..e078f47 --- /dev/null +++ b/examples/scitt_interop/generated/issuer_public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAb26PMIwi29Ow1WGsGT/TfzRwSDRDwDHh2WPUR6VECmA= +-----END PUBLIC KEY----- diff --git a/examples/scitt_interop/generated/log_public.pem b/examples/scitt_interop/generated/log_public.pem new file mode 100644 index 0000000..5b0b2a9 --- /dev/null +++ b/examples/scitt_interop/generated/log_public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAoV97j2vB5HoFd7rxXDgAd/PEeel0IgquRuQ3nFLs6rI= +-----END PUBLIC KEY----- diff --git a/examples/scitt_interop/generated/receipt.cose b/examples/scitt_interop/generated/receipt.cose new file mode 100644 index 0000000000000000000000000000000000000000..1ab727295d31a5d3160218ebc904b1be3c08b323 GIT binary patch literal 250 zcmVEp%mbbS-ReX99RoVRCe7W?^G=I&)}Y zGBq|jGdW{4G%`6jIASz1HDNJjG-G2oV>V)9G%;i}W@0lnFf=w~WnnfkVKri9Gh<|C zGdW>3I5J`|H8f>mWivD}83Bs{0Vkmu0gRy_fkcA=0D$&bK&0TX7!+RhpWYBx>N&$^ zz*oeNhoJ0~(Q4DQ|8_p54r9Dv AdH?_b literal 0 HcmV?d00001 diff --git a/examples/scitt_interop/generated/registration_template.json b/examples/scitt_interop/generated/registration_template.json new file mode 100644 index 0000000..5014d66 --- /dev/null +++ b/examples/scitt_interop/generated/registration_template.json @@ -0,0 +1,175 @@ +{ + "payload": { + "mapping_version": "0.1", + "profile": "vstd-scitt-interop-experimental-0.1", + "receipt_media_type": "application/vnd.verifier.vstd-receipt+json", + "receipt_sha256": "f8117247e97f12834206f5b024085c54b5d899e6880659d8afcebfeee2ddece1", + "vstd_coordinates": { + "artifact_digests": { + "primary": "39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341" + }, + "claim_coordinate": { + "parameters": { + "algorithm": "sha-256", + "digest": "39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341" + }, + "predicate": "content_digest_matches", + "subject": "artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341" + }, + "claim_id": "SCITT-INTEROP-DEMO-DIGEST", + "evidence_bounds": { + "certificate_size_bound": 20000, + "memory_bound": 10, + "verification_cost_bound": 100 + }, + "native_canonical_digest": "f8117247e97f12834206f5b024085c54b5d899e6880659d8afcebfeee2ddece1", + "native_result": "PASS", + "provenance_references": [ + "urn:example:vstd-scitt-demo:artifact" + ], + "receipt_id": "VFY-4-scitt-interop-demo", + "schema_version": "VSTD-4" + }, + "vstd_receipt": { + "binding": { + "bounds": { + "certificate_size_bound": 20000, + "memory_bound": 10, + "verification_cost_bound": 100 + }, + "claim": "the named artifact bytes have the declared SHA-256 digest", + "coordinate": { + "parameters": { + "algorithm": "sha-256", + "digest": "39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341" + }, + "predicate": "content_digest_matches", + "subject": "artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341" + }, + "evidence_root": "39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341", + "policy_root": "418c69bf2c7e119d75936d599903f860acaf5d3689817ae4ab9881d4659e6b2a", + "prior_commitment": "", + "verifier": { + "certificate_format": "VSTD4-GDC-1", + "dependencies": [ + "python-stdlib" + ], + "deterministic": true, + "format_fragment": "UP,WIDTH-K,RES", + "implementation_hash": "sha256:94e4f7d4cb771f76d3e856ad93f0e7c3d151d47d862abef00fbcda23d7975e1f", + "parser_hash": "sha256:7d05d3810219b1ef8400accd5735fbde494c4fb310c4d86f08381d4979dcde5c", + "specification_hash": "sha256:9648fee5c94a8c41a581ec003226dd87eca59bc6e2356ed0383fcabbf02a1d5f" + } + }, + "blocking_rungs": [], + "ceiling_refutation": null, + "claim_id": "SCITT-INTEROP-DEMO-DIGEST", + "conformance_status": "NOT_ESTABLISHED", + "receipt_id": "VFY-4-scitt-interop-demo", + "refutation_surface": { + "admissible_refutations": [ + "artifact bytes hash to a value other than the bound digest", + "the VSTD decision certificate fails separate kernel checking" + ], + "excluded_claims": [ + "artifact safety", + "issuer authorization", + "truth outside the bounded digest predicate" + ] + }, + "rung_evidence": { + "4.1": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.1", + "4.10": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.10", + "4.11": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.11", + "4.12": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.12", + "4.13": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.13", + "4.14": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.14", + "4.2": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.2", + "4.3": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.3", + "4.4": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.4", + "4.5": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.5", + "4.6": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.6", + "4.7": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.7", + "4.8": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.8", + "4.9": "decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.9" + }, + "schema_version": "VSTD-4", + "status": "VALID", + "vstd4_depth": 14, + "witness": { + "decision": { + "model": { + "1": true + }, + "propagation": null, + "resolution": null, + "transcript": null + }, + "formula": [ + [ + 1 + ] + ], + "grounding": { + "clauses": [ + { + "bindings": { + "artifact": 1 + }, + "clause_index": 0, + "rule_id": "RULE:ASSERT_DIGEST_MATCH", + "subjects": { + "artifact": "artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341" + } + } + ], + "rules": [ + { + "roles": [ + "artifact" + ], + "rule_id": "RULE:ASSERT_DIGEST_MATCH", + "template": [ + [ + 1, + "artifact" + ] + ] + } + ], + "variables": [ + { + "fact": { + "predicate": "content_digest_matches", + "subject": "artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341", + "value": "MATCH" + }, + "var": 1 + } + ] + }, + "header": { + "binding": "6e7b912f47311920b5f0310c4b869e64b0985a309d75f5285360080ca605f6cd", + "clause_count": 1, + "format": "VSTD4-GDC-1", + "literal_count": 1, + "n_vars": 1, + "step_count": 0, + "tier": "UP", + "verdict": "PASS", + "width": 0 + }, + "hints": {} + } + } + }, + "payload_sha256": "a0fc13840915e31f4d4787c7503f86f789be24e4a18fa76181991fa9aaecca63", + "representation": "normalized-registration-input-not-cose", + "required_protected_header_projection": { + "content_type": "application/vnd.verifier.vstd-receipt+json", + "issuer": "https://issuer.example/vstd-scitt-demo", + "payload_hash_algorithm": "sha-256", + "subject": "artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341", + "type": "vstd-scitt-interop-experimental-0.1" + } +} diff --git a/examples/scitt_interop/generated/signed_statement.cose b/examples/scitt_interop/generated/signed_statement.cose new file mode 100644 index 0000000..20be3e0 --- /dev/null +++ b/examples/scitt_interop/generated/signed_statement.cose @@ -0,0 +1,2 @@ +Ò„Y¤x*application/vnd.verifier.vstd-receipt+json£x&https://issuer.example/vstd-scitt-demoxPartifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341lvstd_profilex#vstd-scitt-interop-experimental-0.1X E¸.5(ø4}°hhoV[­Ç9®îº†H”wŠ|Ôm·' Y£{"mapping_version":"0.1","profile":"vstd-scitt-interop-experimental-0.1","receipt_media_type":"application/vnd.verifier.vstd-receipt+json","receipt_sha256":"f8117247e97f12834206f5b024085c54b5d899e6880659d8afcebfeee2ddece1","vstd_coordinates":{"artifact_digests":{"primary":"39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341"},"claim_coordinate":{"parameters":{"algorithm":"sha-256","digest":"39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341"},"predicate":"content_digest_matches","subject":"artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341"},"claim_id":"SCITT-INTEROP-DEMO-DIGEST","evidence_bounds":{"certificate_size_bound":20000,"memory_bound":10,"verification_cost_bound":100},"native_canonical_digest":"f8117247e97f12834206f5b024085c54b5d899e6880659d8afcebfeee2ddece1","native_result":"PASS","provenance_references":["urn:example:vstd-scitt-demo:artifact"],"receipt_id":"VFY-4-scitt-interop-demo","schema_version":"VSTD-4"},"vstd_receipt":{"binding":{"bounds":{"certificate_size_bound":20000,"memory_bound":10,"verification_cost_bound":100},"claim":"the named artifact bytes have the declared SHA-256 digest","coordinate":{"parameters":{"algorithm":"sha-256","digest":"39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341"},"predicate":"content_digest_matches","subject":"artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341"},"evidence_root":"39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341","policy_root":"418c69bf2c7e119d75936d599903f860acaf5d3689817ae4ab9881d4659e6b2a","prior_commitment":"","verifier":{"certificate_format":"VSTD4-GDC-1","dependencies":["python-stdlib"],"deterministic":true,"format_fragment":"UP,WIDTH-K,RES","implementation_hash":"sha256:94e4f7d4cb771f76d3e856ad93f0e7c3d151d47d862abef00fbcda23d7975e1f","parser_hash":"sha256:7d05d3810219b1ef8400accd5735fbde494c4fb310c4d86f08381d4979dcde5c","specification_hash":"sha256:9648fee5c94a8c41a581ec003226dd87eca59bc6e2356ed0383fcabbf02a1d5f"}},"blocking_rungs":[],"ceiling_refutation":null,"claim_id":"SCITT-INTEROP-DEMO-DIGEST","conformance_status":"NOT_ESTABLISHED","receipt_id":"VFY-4-scitt-interop-demo","refutation_surface":{"admissible_refutations":["artifact bytes hash to a value other than the bound digest","the VSTD decision certificate fails separate kernel checking"],"excluded_claims":["artifact safety","issuer authorization","truth outside the bounded digest predicate"]},"rung_evidence":{"4.1":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.1","4.10":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.10","4.11":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.11","4.12":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.12","4.13":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.13","4.14":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.14","4.2":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.2","4.3":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.3","4.4":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.4","4.5":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.5","4.6":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.6","4.7":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.7","4.8":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.8","4.9":"decision_certificate:5f7e75463c755cab27d65ac363b86164e8f3d74233983d0120e4ad1157a13295#4.9"},"schema_version":"VSTD-4","status":"VALID","vstd4_depth":14,"witness":{"decision":{"model":{"1":true},"propagation":null,"resolution":null,"transcript":null},"formula":[[1]],"grounding":{"clauses":[{"bindings":{"artifact":1},"clause_index":0,"rule_id":"RULE:ASSERT_DIGEST_MATCH","subjects":{"artifact":"artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341"}}],"rules":[{"roles":["artifact"],"rule_id":"RULE:ASSERT_DIGEST_MATCH","template":[[1,"artifact"]]}],"variables":[{"fact":{"predicate":"content_digest_matches","subject":"artifact:sha256:39c442988b425a1e4cc7c6bb41d4fb35046dea61a5be3cdf39a582b054eae341","value":"MATCH"},"var":1}]},"header":{"binding":"6e7b912f47311920b5f0310c4b869e64b0985a309d75f5285360080ca605f6cd","clause_count":1,"format":"VSTD4-GDC-1","literal_count":1,"n_vars":1,"step_count":0,"tier":"UP","verdict":"PASS","width":0},"hints":{}}}}X@«ý±kóºPúX¿?²ñÅ€DNmÇ#é( +íLÒö9äÁw,Ô™!á}ÑaªG’ÈÉ h|@“÷ \ No newline at end of file diff --git a/examples/scitt_interop/generated/transparent_statement.cose b/examples/scitt_interop/generated/transparent_statement.cose new file mode 100644 index 0000000000000000000000000000000000000000..875acce6691b6fb7a19445c996df81be584f73fa GIT binary patch literal 5361 zcmd^DTWlOx8LnwTT%-h5t<=EFFcCCrcb&PMGnW-AF|mmaX=2%qn~luc^m8Z6UAAnvh?sE6_W$Vl~j;V4vJE=bKs!* z!CS#Ks@)X|KwK>y?Wismp2Z)_~EA( z&VK%`+taORZEOEy6Pur{^0YGyytBuTB;#nfgraOG+4>k#aA4x0iQ40DK5AT;{@TQK z_r0}pcYJ@}nHwKJZ}t!cDqWAT_yq#N?3kE8vtib97o*f$0;M+2=ZQmh2gub+$z$K35Ng7)Yu>#5| ziKz&}5WFC;JQ|9CNe;0D07nP}ua2Td4$rb&sFY!S%+7gp1kXsQb*xx@uaD?q`4$@e zpkOt(TP9DK>P5R|wbee$Sq~7djStCsmaDScLyH)18`}iEZ5+BrPV{rE1a`Ns1JAHg z(sCGa)MF*@VlrB0QN?G0m$+nvdRL)N=E@D{NK?6vc4lD-=xegJ7TQ#pj zBAPurqcWIlyDe2GaQRDT+N9m^?5GX(8@h;C%pTj*!OM$F(`}+FP~(5tni_?%O0g5x z>&vkvyM{mJq3nW@BIt!Nipq$$unCPW8-SsoVw)xmacV5iPU#*nh8=({w9~Wyug$a5 z6?v9vRDED#LPTab@3*4ihQtoI7sk@zKG=3Bd>Xo*pkWwVt_(bjaVDv7y&w#1p8;Vp zj&K~=$hF|b4yz%qvK-s3*HfjQzUU_29q9;NxxI}nvmBWR*Jyl__Oa=sZOzdF`jBb@ zP)&REx5{powsGbsD%O1-608(ro)Ht%Vug zfJ*C=S`=uB6Lne9MdL$J5)vTN7lg;YZ%f}3E(FwLB6Ot%K6iypF;-s$p2K30mL+2@ znBxjR^eNabq3N0!T=>Rz0@G32rW}02d1q z`k~+gD93$^KJbS8{)^il32?+w9ugLCf|@C?fm@dAIGzxJ51i33<{mgM^*~r|;7ZQo zSXvIV1(oJDvO@d{^KO z6>e;vIJp!d$xnUp_}t>`%rpi!(h2WHYDk$wKcb?_QBL9B%Ql*eG$=J8DJ_GHJ-9U_Q%PYIphtKK#yQAS zNDL(1I*A&gu*s83fQV}9d#67IlTdDHIM%w^U=_kUSNAkIZkY(@vTI~jS>P1hbq-0f zAwy$#kgs9wbjhL-H|UfQlxrRAk;d_8kK-LGeefyqT<%kfC~$n?QN~@*jRVj22m})G zOdQt@16NqKV_}yI+onFVT_>am_1-OQ!5XIttFbZGFotnB;;&8`l!)FH*C@}H&mHIfn(wP4o~arUM3*X7sz~C)SwkSGxXW|JH{^_ zE3%~03T8Br%bcYJ&++3-a|hqklebD3j`CG|ZLMXl=UUwll|Bxos;ITq&HUlHInyVc{^?B+2I9vj%!unK?^rRFRxLSk@S}Psu@wfCxt3P_ zk~#r(J{~ zqIaj}b(a8sl&L>G4A09OO6Z2s$Mr6ra5bd1@wakh;)#DgbMEFhU- bytes: + """Serialize experimental mapping objects deterministically. + + This deliberately matches VSTD's existing sorted, compact, ASCII JSON + rules, while remaining a mapping-level serializer rather than a claim that + JSON is SCITT's COSE serialized transport format. + """ + + try: + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + except (TypeError, ValueError) as exc: + raise InteropError(f"value is not canonical-JSON serializable: {exc}") from exc + + +def _sha256(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def _digest(value: str, label: str) -> str: + if not isinstance(value, str): + raise InteropError(f"{label} must be a lowercase SHA-256 digest") + normalized = value.removeprefix("sha256:") + if not _SHA256.fullmatch(normalized): + raise InteropError(f"{label} must be a lowercase SHA-256 digest") + return normalized + + +def _nonempty(value: Any, label: str) -> str: + if not isinstance(value, str) or not value: + raise InteropError(f"{label} must be a non-empty string") + return value + + +def _exact_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None: + actual = set(value) + if actual != expected: + raise InteropError( + f"{label} keys mismatch; missing={sorted(expected - actual)}, " + f"extra={sorted(actual - expected)}" + ) + + +def _string_map(value: Mapping[str, Any], label: str) -> dict[str, str]: + result: dict[str, str] = {} + for key, item in value.items(): + result[_nonempty(key, f"{label} key")] = _nonempty( + item, f"{label}[{key!r}]" + ) + return dict(sorted(result.items())) + + +@dataclass(frozen=True) +class VstdCoordinates: + """Loss-sensitive projection of the VSTD semantics carried in SCITT. + + The full native receipt is embedded as the payload. This projection makes + the coordinates a SCITT registration policy or relying-party tool is most + likely to inspect explicit without pretending one generic adapter can infer + every VSTD receipt family's semantics. + """ + + receipt_id: str + schema_version: str + claim_id: str + subject: str + predicate: str + parameters: Mapping[str, str] + native_result: str + native_canonical_digest: str + evidence_bounds: Mapping[str, int] + artifact_digests: Mapping[str, str] + provenance_references: tuple[str, ...] = () + + def __post_init__(self) -> None: + for name in ( + "receipt_id", + "schema_version", + "claim_id", + "subject", + "predicate", + "native_result", + ): + _nonempty(getattr(self, name), name) + object.__setattr__( + self, + "native_canonical_digest", + _digest(self.native_canonical_digest, "native_canonical_digest"), + ) + params = _string_map(self.parameters, "parameters") + object.__setattr__(self, "parameters", MappingProxyType(params)) + + bounds: dict[str, int] = {} + for key, value in self.evidence_bounds.items(): + key = _nonempty(key, "evidence_bounds key") + if type(value) is not int or value < 0: + raise InteropError( + f"evidence_bounds[{key!r}] must be a non-negative integer" + ) + bounds[key] = value + object.__setattr__( + self, "evidence_bounds", MappingProxyType(dict(sorted(bounds.items()))) + ) + + artifacts = { + _nonempty(key, "artifact_digests key"): _digest( + value, f"artifact_digests[{key!r}]" + ) + for key, value in self.artifact_digests.items() + } + if not artifacts: + raise InteropError("at least one artifact digest is required") + object.__setattr__( + self, "artifact_digests", MappingProxyType(dict(sorted(artifacts.items()))) + ) + refs = tuple(_nonempty(item, "provenance reference") for item in self.provenance_references) + if len(set(refs)) != len(refs): + raise InteropError("provenance_references must be unique") + object.__setattr__(self, "provenance_references", refs) + + def to_dict(self) -> dict[str, Any]: + return { + "receipt_id": self.receipt_id, + "schema_version": self.schema_version, + "claim_id": self.claim_id, + "claim_coordinate": { + "subject": self.subject, + "predicate": self.predicate, + "parameters": dict(self.parameters), + }, + "native_result": self.native_result, + "native_canonical_digest": self.native_canonical_digest, + "evidence_bounds": dict(self.evidence_bounds), + "artifact_digests": dict(self.artifact_digests), + "provenance_references": list(self.provenance_references), + } + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "VstdCoordinates": + _exact_keys( + value, + { + "receipt_id", + "schema_version", + "claim_id", + "claim_coordinate", + "native_result", + "native_canonical_digest", + "evidence_bounds", + "artifact_digests", + "provenance_references", + }, + "vstd_coordinates", + ) + coordinate = value["claim_coordinate"] + if not isinstance(coordinate, Mapping): + raise InteropError("claim_coordinate must be an object") + _exact_keys( + coordinate, {"subject", "predicate", "parameters"}, "claim_coordinate" + ) + parameters = coordinate["parameters"] + bounds = value["evidence_bounds"] + artifacts = value["artifact_digests"] + refs = value["provenance_references"] + if not isinstance(parameters, Mapping): + raise InteropError("claim_coordinate.parameters must be an object") + if not isinstance(bounds, Mapping): + raise InteropError("evidence_bounds must be an object") + if not isinstance(artifacts, Mapping): + raise InteropError("artifact_digests must be an object") + if not isinstance(refs, list) or not all(isinstance(item, str) for item in refs): + raise InteropError("provenance_references must be an array of strings") + return cls( + receipt_id=value["receipt_id"], + schema_version=value["schema_version"], + claim_id=value["claim_id"], + subject=coordinate["subject"], + predicate=coordinate["predicate"], + parameters=parameters, + native_result=value["native_result"], + native_canonical_digest=value["native_canonical_digest"], + evidence_bounds=bounds, + artifact_digests=artifacts, + provenance_references=tuple(refs), + ) + + +@dataclass(frozen=True) +class VstdScittPayload: + """Experimental application payload for carriage in a SCITT statement.""" + + receipt: Mapping[str, Any] + coordinates: VstdCoordinates + receipt_sha256: str + mapping_version: str = MAPPING_VERSION + profile: str = EXPERIMENTAL_PROFILE + receipt_media_type: str = EXPERIMENTAL_CONTENT_TYPE + + def __post_init__(self) -> None: + if self.mapping_version != MAPPING_VERSION: + raise InteropError(f"unsupported mapping version {self.mapping_version!r}") + if self.profile != EXPERIMENTAL_PROFILE: + raise InteropError(f"unsupported profile {self.profile!r}") + if self.receipt_media_type != EXPERIMENTAL_CONTENT_TYPE: + raise InteropError( + f"unsupported receipt media type {self.receipt_media_type!r}" + ) + if not isinstance(self.receipt, Mapping): + raise InteropError("receipt must be an object") + _digest(self.receipt_sha256, "receipt_sha256") + # Break aliases to caller-owned nested dictionaries. ``to_dict`` also + # rechecks the digest, so even deliberate mutation through the exposed + # nested projection fails closed rather than changing signed bytes. + copied = json.loads(canonical_json_bytes(dict(self.receipt)).decode("utf-8")) + object.__setattr__(self, "receipt", MappingProxyType(copied)) + self.verify_integrity() + + @classmethod + def create( + cls, receipt: Mapping[str, Any], coordinates: VstdCoordinates + ) -> "VstdScittPayload": + copied = dict(receipt) + return cls( + receipt=copied, + coordinates=coordinates, + receipt_sha256=_sha256(canonical_json_bytes(copied)), + ) + + def verify_integrity(self) -> None: + observed = _sha256(canonical_json_bytes(dict(self.receipt))) + if observed != self.receipt_sha256: + raise InteropError("embedded VSTD receipt does not match receipt_sha256") + for field in ("receipt_id", "schema_version"): + native = self.receipt.get(field) + declared = getattr(self.coordinates, field) + if native != declared: + raise InteropError( + f"embedded receipt {field} {native!r} does not match " + f"declared coordinate {declared!r}" + ) + native_digest = self.receipt.get("canonical_digest") + if native_digest is not None: + if _digest(native_digest, "receipt.canonical_digest") != ( + self.coordinates.native_canonical_digest + ): + raise InteropError( + "embedded receipt canonical_digest does not match VSTD coordinates" + ) + elif observed != self.coordinates.native_canonical_digest: + raise InteropError( + "embedded receipt full canonical digest does not match VSTD coordinates" + ) + + native_claim_id = self.receipt.get("claim_id") + if native_claim_id is not None and native_claim_id != self.coordinates.claim_id: + raise InteropError( + "embedded receipt claim_id does not match VSTD coordinates" + ) + + binding = self.receipt.get("binding") + if isinstance(binding, Mapping): + coordinate = binding.get("coordinate") + if isinstance(coordinate, Mapping): + expected = { + "subject": self.coordinates.subject, + "predicate": self.coordinates.predicate, + "parameters": dict(self.coordinates.parameters), + } + if dict(coordinate) != expected: + raise InteropError( + "embedded VSTD binding coordinate does not match mapping coordinate" + ) + bounds = binding.get("bounds") + if isinstance(bounds, Mapping) and dict(bounds) != dict( + self.coordinates.evidence_bounds + ): + raise InteropError( + "embedded VSTD evidence bounds do not match mapping coordinates" + ) + + native_result = None + witness = self.receipt.get("witness") + if isinstance(witness, Mapping): + header = witness.get("header") + if isinstance(header, Mapping): + native_result = header.get("verdict") + decision = self.receipt.get("decision") + if native_result is None and isinstance(decision, Mapping): + native_result = decision.get("verdict") + if native_result is not None and native_result != self.coordinates.native_result: + raise InteropError( + "embedded VSTD native result does not match mapping coordinates" + ) + + def to_dict(self) -> dict[str, Any]: + self.verify_integrity() + return { + "mapping_version": self.mapping_version, + "profile": self.profile, + "receipt_media_type": self.receipt_media_type, + "receipt_sha256": self.receipt_sha256, + "vstd_coordinates": self.coordinates.to_dict(), + "vstd_receipt": dict(self.receipt), + } + + def to_bytes(self) -> bytes: + return canonical_json_bytes(self.to_dict()) + + def payload_sha256(self) -> str: + return _sha256(self.to_bytes()) + + @classmethod + def from_bytes(cls, value: bytes) -> "VstdScittPayload": + try: + decoded = json.loads(value.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise InteropError(f"SCITT payload is not canonical JSON: {exc}") from exc + if not isinstance(decoded, Mapping): + raise InteropError("SCITT payload must be an object") + if canonical_json_bytes(decoded) != value: + raise InteropError("SCITT payload bytes are not in canonical form") + _exact_keys( + decoded, + { + "mapping_version", + "profile", + "receipt_media_type", + "receipt_sha256", + "vstd_coordinates", + "vstd_receipt", + }, + "SCITT payload", + ) + coordinates = decoded["vstd_coordinates"] + receipt = decoded["vstd_receipt"] + if not isinstance(coordinates, Mapping) or not isinstance(receipt, Mapping): + raise InteropError("vstd_coordinates and vstd_receipt must be objects") + return cls( + mapping_version=decoded["mapping_version"], + profile=decoded["profile"], + receipt_media_type=decoded["receipt_media_type"], + receipt_sha256=decoded["receipt_sha256"], + coordinates=VstdCoordinates.from_dict(coordinates), + receipt=receipt, + ) + + +@dataclass(frozen=True) +class ScittRegistrationTemplate: + """Normalized input for a native RFC 9943/COSE statement producer.""" + + issuer: str + subject: str + payload: VstdScittPayload + + def __post_init__(self) -> None: + _nonempty(self.issuer, "issuer") + _nonempty(self.subject, "subject") + if self.subject != self.payload.coordinates.subject: + raise InteropError( + "SCITT subject must equal the VSTD claim-coordinate subject" + ) + + def to_dict(self) -> dict[str, Any]: + return { + "representation": "normalized-registration-input-not-cose", + "required_protected_header_projection": { + "content_type": EXPERIMENTAL_CONTENT_TYPE, + "issuer": self.issuer, + "payload_hash_algorithm": "sha-256", + "subject": self.subject, + "type": EXPERIMENTAL_PROFILE, + }, + "payload_sha256": self.payload.payload_sha256(), + "payload": self.payload.to_dict(), + } + + def to_bytes(self) -> bytes: + return canonical_json_bytes(self.to_dict()) + + +class ScittEvidenceState(str, Enum): + """Normalized relying-party state; not an IETF registry.""" + + REGISTERED = "REGISTERED" + MISSING = "MISSING" + STALE = "STALE" + CONFLICTED = "CONFLICTED" + REVOKED = "REVOKED" + SUPERSEDED = "SUPERSEDED" + UNKNOWN = "UNKNOWN" + INVALID = "INVALID" + + +class VstdVerificationState(str, Enum): + """Normalized state from a native VSTD checker, not a wire registry.""" + + VERIFIED = "VERIFIED" + REJECTED = "REJECTED" + INDETERMINATE = "INDETERMINATE" + NOT_EVALUATED = "NOT_EVALUATED" + + +@dataclass(frozen=True) +class VstdVerificationEvidence: + """Bound output from a native VSTD checker. + + The adapter cannot infer that an embedded receipt was checked merely + because the receipt declares ``PASS``. A caller must provide the native + check state for the exact embedded receipt and retain the checker trust + coordinates. This is deliberately symmetric with + :class:`ScittVerificationEvidence`, which is normalized output from a + native SCITT verifier rather than a replacement for one. ``state`` says + whether that bounded native check ran; it is not numbered VSTD profile conformance. + The current adapter therefore emits ``conformance_status`` explicitly and + accepts only ``NOT_ESTABLISHED``. + """ + + state: VstdVerificationState + receipt_sha256: str + native_result: str + checker: str + verification_profile: str + reason: str + conformance_status: str = "NOT_ESTABLISHED" + + def __post_init__(self) -> None: + try: + state = VstdVerificationState(self.state) + except (TypeError, ValueError) as exc: + raise InteropError( + f"unsupported VSTD verification state {self.state!r}" + ) from exc + object.__setattr__(self, "state", state) + object.__setattr__( + self, "receipt_sha256", _digest(self.receipt_sha256, "receipt_sha256") + ) + for name in ("native_result", "checker", "verification_profile", "reason"): + _nonempty(getattr(self, name), name) + if self.conformance_status != "NOT_ESTABLISHED": + raise InteropError( + "this experimental adapter cannot establish VSTD conformance; " + "conformance_status must be NOT_ESTABLISHED" + ) + + def to_dict(self) -> dict[str, str]: + return { + "state": self.state.value, + "receipt_sha256": self.receipt_sha256, + "native_result": self.native_result, + "checker": self.checker, + "verification_profile": self.verification_profile, + "reason": self.reason, + "conformance_status": self.conformance_status, + } + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "VstdVerificationEvidence": + expected = { + "state", + "receipt_sha256", + "native_result", + "checker", + "verification_profile", + "reason", + "conformance_status", + } + # Read the pre-1.2 experimental shape fail-closed: the old object did + # not serialize conformance, and absence never established it. New + # writers always emit the explicit status. + legacy_expected = expected - {"conformance_status"} + if set(value) == legacy_expected: + value = dict(value) + value["conformance_status"] = "NOT_ESTABLISHED" + _exact_keys(value, expected, "VSTD verification evidence") + try: + state = VstdVerificationState(value["state"]) + except ValueError as exc: + raise InteropError( + f"unsupported VSTD verification state {value['state']!r}" + ) from exc + return cls(state=state, **{key: value[key] for key in expected - {"state"}}) + + +@dataclass(frozen=True) +class ScittVerificationEvidence: + """Output supplied by a native SCITT verifier under an explicit policy. + + ``state`` is a local normalized policy result. RFC 9943 does not define + this enum, and callers must retain ``native_result`` and ``reason`` so that + the source verifier's semantics are not erased. + """ + + state: ScittEvidenceState + statement_sha256: str + payload_sha256: str + issuer: str + subject: str + signed_statement_verified: bool + receipt_verified: bool + verification_profile: str + registration_policy: str + transparency_service: str + vds: str + native_result: str + reason: str + registered_at: str | None = None + + def __post_init__(self) -> None: + try: + state = ScittEvidenceState(self.state) + except (TypeError, ValueError) as exc: + raise InteropError(f"unsupported SCITT evidence state {self.state!r}") from exc + object.__setattr__(self, "state", state) + object.__setattr__( + self, "statement_sha256", _digest(self.statement_sha256, "statement_sha256") + ) + object.__setattr__( + self, "payload_sha256", _digest(self.payload_sha256, "payload_sha256") + ) + for name in ( + "issuer", + "subject", + "verification_profile", + "registration_policy", + "transparency_service", + "vds", + "native_result", + "reason", + ): + _nonempty(getattr(self, name), name) + if type(self.signed_statement_verified) is not bool: + raise InteropError("signed_statement_verified must be boolean") + if type(self.receipt_verified) is not bool: + raise InteropError("receipt_verified must be boolean") + if self.registered_at is not None: + _nonempty(self.registered_at, "registered_at") + if self.state is ScittEvidenceState.REGISTERED and not ( + self.signed_statement_verified and self.receipt_verified + ): + raise InteropError( + "REGISTERED requires native verification of both statement and receipt" + ) + + def to_dict(self) -> dict[str, Any]: + return { + "state": self.state.value, + "statement_sha256": self.statement_sha256, + "payload_sha256": self.payload_sha256, + "issuer": self.issuer, + "subject": self.subject, + "signed_statement_verified": self.signed_statement_verified, + "receipt_verified": self.receipt_verified, + "verification_profile": self.verification_profile, + "registration_policy": self.registration_policy, + "transparency_service": self.transparency_service, + "vds": self.vds, + "native_result": self.native_result, + "reason": self.reason, + "registered_at": self.registered_at, + } + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "ScittVerificationEvidence": + expected = { + "state", + "statement_sha256", + "payload_sha256", + "issuer", + "subject", + "signed_statement_verified", + "receipt_verified", + "verification_profile", + "registration_policy", + "transparency_service", + "vds", + "native_result", + "reason", + "registered_at", + } + _exact_keys(value, expected, "SCITT verification evidence") + try: + state = ScittEvidenceState(value["state"]) + except ValueError as exc: + raise InteropError(f"unsupported SCITT evidence state {value['state']!r}") from exc + return cls(state=state, **{key: value[key] for key in expected - {"state"}}) + + +class CompositionStatus(str, Enum): + PASS = "PASS" + FAIL = "FAIL" + UNKNOWN = "UNKNOWN" + CONFLICTED = "CONFLICTED" + + +@dataclass(frozen=True) +class CompositionResult: + """Scoped conjunction of native results, never a conformance certificate.""" + + status: CompositionStatus + status_scope: str + vstd_conformance_status: str + native_vstd_result: str + native_scitt_result: str + reason: str + vstd_receipt_sha256: str + scitt_statement_sha256: str + + def to_dict(self) -> dict[str, str]: + return { + "status": self.status.value, + "status_scope": self.status_scope, + "vstd_conformance_status": self.vstd_conformance_status, + "native_vstd_result": self.native_vstd_result, + "native_scitt_result": self.native_scitt_result, + "reason": self.reason, + "vstd_receipt_sha256": self.vstd_receipt_sha256, + "scitt_statement_sha256": self.scitt_statement_sha256, + } + + +def create_scitt_registration_template( + receipt: Mapping[str, Any], + coordinates: VstdCoordinates, + *, + issuer: str, + subject: str, +) -> ScittRegistrationTemplate: + """Create deterministic inputs for an external SCITT/COSE producer.""" + + return ScittRegistrationTemplate( + issuer=issuer, + subject=subject, + payload=VstdScittPayload.create(receipt, coordinates), + ) + + +def consume_scitt_evidence( + evidence: ScittVerificationEvidence, + *, + expected_payload_sha256: str, + expected_subject: str, + accepted_issuers: Sequence[str], +) -> dict[str, Any]: + """Convert a native SCITT verifier result into bounded VSTD evidence. + + The returned object describes transparency evidence only. Its + ``computational_verdict`` is always ``NOT_EVALUATED``. + """ + + expected_digest = _digest(expected_payload_sha256, "expected_payload_sha256") + accepted = tuple(_nonempty(item, "accepted issuer") for item in accepted_issuers) + if not accepted: + raise InteropError("accepted_issuers cannot be empty") + + state = evidence.state + reason = evidence.reason + if evidence.payload_sha256 != expected_digest: + state = ScittEvidenceState.INVALID + reason = "SCITT statement payload does not bind the expected VSTD payload" + elif evidence.subject != expected_subject: + state = ScittEvidenceState.INVALID + reason = "SCITT subject does not match the VSTD claim subject" + elif evidence.issuer not in accepted: + state = ScittEvidenceState.INVALID + reason = "SCITT issuer is not accepted by the relying-party policy" + elif not evidence.signed_statement_verified or not evidence.receipt_verified: + state = ScittEvidenceState.INVALID + reason = "native SCITT statement or receipt verification did not succeed" + + return { + "evidence_kind": "SCITT_TRANSPARENCY", + "normalized_state": state.value, + "native_scitt_result": evidence.native_result, + "reason": reason, + "computational_verdict": "NOT_EVALUATED", + "trust_coordinates": { + "accepted_issuers": list(accepted), + "registration_policy": evidence.registration_policy, + "transparency_service": evidence.transparency_service, + "verification_profile": evidence.verification_profile, + "vds": evidence.vds, + }, + "statement_sha256": evidence.statement_sha256, + "payload_sha256": evidence.payload_sha256, + "registered_at": evidence.registered_at, + } + + +def compose_results( + payload: VstdScittPayload, + vstd: VstdVerificationEvidence, + scitt: ScittVerificationEvidence, + *, + artifact_digests: Mapping[str, str], + accepted_issuers: Sequence[str], +) -> CompositionResult: + """Compose exact VSTD and SCITT results without semantic upgrading.""" + + observed_artifacts = { + _nonempty(key, "artifact_digests key"): _digest( + value, f"artifact_digests[{key!r}]" + ) + for key, value in artifact_digests.items() + } + transparency = consume_scitt_evidence( + scitt, + expected_payload_sha256=payload.payload_sha256(), + expected_subject=payload.coordinates.subject, + accepted_issuers=accepted_issuers, + ) + scitt_state = ScittEvidenceState(transparency["normalized_state"]) + native_vstd = vstd.native_result + + if observed_artifacts != dict(payload.coordinates.artifact_digests): + status = CompositionStatus.FAIL + reason = "artifact binding mismatch" + elif vstd.receipt_sha256 != payload.receipt_sha256: + status = CompositionStatus.FAIL + reason = "native VSTD checker result does not bind the embedded receipt" + elif ( + vstd.state is VstdVerificationState.VERIFIED + and vstd.native_result != payload.coordinates.native_result + ): + status = CompositionStatus.FAIL + reason = "native VSTD checker result does not match the payload result" + elif vstd.state is VstdVerificationState.REJECTED: + status = CompositionStatus.FAIL + reason = f"native VSTD checker rejected the receipt: {vstd.reason}" + elif vstd.state is VstdVerificationState.NOT_EVALUATED: + status = CompositionStatus.UNKNOWN + reason = "native VSTD receipt was not evaluated" + elif vstd.state is VstdVerificationState.INDETERMINATE: + status = CompositionStatus.UNKNOWN + reason = f"native VSTD checker was unable to decide: {vstd.reason}" + elif native_vstd in _VSTD_FAIL: + status = CompositionStatus.FAIL + reason = "native VSTD verification failed" + elif scitt_state is ScittEvidenceState.INVALID: + status = CompositionStatus.FAIL + reason = transparency["reason"] + elif native_vstd == CompositionStatus.CONFLICTED.value: + status = CompositionStatus.CONFLICTED + reason = "native VSTD evidence is conflicted" + elif scitt_state is ScittEvidenceState.CONFLICTED: + status = CompositionStatus.CONFLICTED + reason = "SCITT evidence graph or relying-party policy reports a conflict" + elif native_vstd in _VSTD_UNKNOWN: + status = CompositionStatus.UNKNOWN + reason = "native VSTD verification is indeterminate or unsupported" + elif scitt_state is not ScittEvidenceState.REGISTERED: + status = CompositionStatus.UNKNOWN + reason = f"SCITT evidence state {scitt_state.value} does not establish a current registration" + elif native_vstd in _VSTD_PASS: + status = CompositionStatus.PASS + reason = ( + "native candidate-check result PASS (VSTD conformance " + "NOT_ESTABLISHED) and exact current SCITT registration both verified" + ) + else: + raise InteropError( + f"unsupported native VSTD result {native_vstd!r}; refusing to guess" + ) + + return CompositionResult( + status=status, + status_scope="NATIVE_VSTD_RESULT_AND_SCITT_REGISTRATION", + vstd_conformance_status=vstd.conformance_status, + native_vstd_result=native_vstd, + native_scitt_result=scitt.native_result, + reason=reason, + vstd_receipt_sha256=payload.receipt_sha256, + scitt_statement_sha256=scitt.statement_sha256, + ) diff --git a/tests/test_scitt_crypto_example.py b/tests/test_scitt_crypto_example.py new file mode 100644 index 0000000..a367b42 --- /dev/null +++ b/tests/test_scitt_crypto_example.py @@ -0,0 +1,121 @@ +"""Terminology: Concise Binary Object Representation (CBOR); +CBOR Object Signing and Encryption (COSE); Supply Chain Integrity, Transparency, and Trust (SCITT); +Verifier Standard (VSTD). + +Optional real-COSE integration test for the self-contained example.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator +from referencing import Registry, Resource + + +pytest.importorskip("scitt_cose") +pytest.importorskip("cryptography") + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEMO = REPO_ROOT / "examples" / "scitt_interop" / "demo.py" + + +def _load_demo(): + spec = importlib.util.spec_from_file_location("vstd_scitt_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_real_signed_statement_receipt_and_independent_consumption(tmp_path): + result = _load_demo().produce(tmp_path) + assert result["vstd_kernel"]["outcome"] == "ACCEPTED" + assert result["vstd_kernel"]["verdict"] == "PASS" + assert result["scitt_observation"]["signed_statement_verified"] is True + assert result["scitt_observation"]["receipt_verified"] is True + assert result["composition"]["status"] == "PASS" + assert result["vstd_observation"]["conformance_status"] == "NOT_ESTABLISHED" + assert result["composition"]["vstd_conformance_status"] == "NOT_ESTABLISHED" + assert result["composition"]["status_scope"] == ( + "NATIVE_VSTD_RESULT_AND_SCITT_REGISTRATION" + ) + assert "conformance NOT_ESTABLISHED" in result["composition"]["reason"] + + schema_dir = REPO_ROOT / "receipts" / "schema" + receipt_schema = json.loads((schema_dir / "vstd4_receipt.json").read_text()) + certificate_schema = json.loads( + (schema_dir / "vstd4_certificate.json").read_text() + ) + registry = Registry().with_resource( + certificate_schema["$id"], Resource.from_contents(certificate_schema) + ) + receipt = json.loads((tmp_path / "vstd_receipt.json").read_text()) + Draft202012Validator(receipt_schema, registry=registry).validate(receipt) + assert receipt["conformance_status"] == "NOT_ESTABLISHED" + + +def test_application_payload_is_deterministic_but_ephemeral_cose_keys_are_not( + tmp_path, +): + demo = _load_demo() + first = tmp_path / "first" + second = tmp_path / "second" + demo.produce(first) + demo.produce(second) + + assert (first / "vstd_scitt_payload.json").read_bytes() == ( + second / "vstd_scitt_payload.json" + ).read_bytes() + assert (first / "signed_statement.cose").read_bytes() != ( + second / "signed_statement.cose" + ).read_bytes() + + +def test_real_statement_and_receipt_tampering_are_rejected(tmp_path): + demo = _load_demo() + demo.produce(tmp_path) + + statement = tmp_path / "signed_statement.cose" + statement_bytes = statement.read_bytes() + statement.write_bytes(statement_bytes[:-1] + bytes([statement_bytes[-1] ^ 1])) + with pytest.raises(RuntimeError, match="signature did not verify"): + demo.verify(tmp_path) + + demo.produce(tmp_path) + receipt = tmp_path / "receipt.cose" + receipt_bytes = receipt.read_bytes() + receipt.write_bytes(receipt_bytes[:-1] + bytes([receipt_bytes[-1] ^ 1])) + with pytest.raises(RuntimeError, match="COSE Receipt failed"): + demo.verify(tmp_path) + + +def test_real_malformed_scitt_statement_is_rejected_before_composition(tmp_path): + demo = _load_demo() + demo.produce(tmp_path) + (tmp_path / "signed_statement.cose").write_bytes(b"\x80") + + with pytest.raises(RuntimeError, match="malformed SCITT Signed Statement"): + demo.verify(tmp_path) + + +def test_real_scitt_registration_does_not_upgrade_vstd_budget_exhaustion(tmp_path): + demo = _load_demo() + demo.produce(tmp_path) + result = demo.verify(tmp_path, vstd_budget=0) + assert result["scitt_observation"]["signed_statement_verified"] is True + assert result["scitt_observation"]["receipt_verified"] is True + assert result["vstd_kernel"]["outcome"] == "REFUSED" + assert result["vstd_kernel"]["verdict"] == "UNKNOWN" + assert result["composition"]["status"] == "UNKNOWN" + + +def test_real_valid_scitt_registration_does_not_repair_rejected_vstd_claim(tmp_path): + result = _load_demo().produce(tmp_path, vstd_binding_tamper=True) + assert result["scitt_observation"]["signed_statement_verified"] is True + assert result["scitt_observation"]["receipt_verified"] is True + assert result["vstd_kernel"]["outcome"] == "REJECTED" + assert result["vstd_observation"]["state"] == "REJECTED" + assert result["composition"]["status"] == "FAIL" diff --git a/tests/test_scitt_interop.py b/tests/test_scitt_interop.py new file mode 100644 index 0000000..6f73df4 --- /dev/null +++ b/tests/test_scitt_interop.py @@ -0,0 +1,437 @@ +"""Terminology: grounded decision certificate (GDC); +Supply Chain Integrity, Transparency, and Trust (SCITT); Verifier Standard (VSTD). + +Adversarial tests for the experimental VSTD/SCITT composition boundary.""" + +from __future__ import annotations + +import json + +import pytest + +from verifier.interoperability.scitt import ( + CompositionStatus, + InteropError, + ScittEvidenceState, + ScittVerificationEvidence, + VstdCoordinates, + VstdScittPayload, + VstdVerificationEvidence, + VstdVerificationState, + compose_results, + consume_scitt_evidence, + create_scitt_registration_template, +) + + +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +ISSUER = "https://issuer.example" +SUBJECT = "artifact:sha256:" + DIGEST_A + + +def _receipt(*, result: str = "PASS") -> dict: + return { + "schema_version": "VSTD-4", + "receipt_id": "VFY-4-scitt-interop-test", + "canonical_digest": DIGEST_B, + "claim_id": "SCITT-INTEROP-TEST", + "binding": { + "claim": "the bounded predicate holds for the named artifact", + "coordinate": { + "subject": SUBJECT, + "predicate": "bounded_predicate", + "parameters": {"policy": "test-policy-v1"}, + }, + "bounds": { + "verification_cost_bound": 100, + "memory_bound": 10, + "certificate_size_bound": 10000, + }, + }, + "decision": {"verdict": result, "certificate": "fixture-only"}, + } + + +def _coordinates(*, result: str = "PASS") -> VstdCoordinates: + return VstdCoordinates( + receipt_id="VFY-4-scitt-interop-test", + schema_version="VSTD-4", + claim_id="SCITT-INTEROP-TEST", + subject=SUBJECT, + predicate="bounded_predicate", + parameters={"policy": "test-policy-v1"}, + native_result=result, + native_canonical_digest=DIGEST_B, + evidence_bounds={ + "verification_cost_bound": 100, + "memory_bound": 10, + "certificate_size_bound": 10000, + }, + artifact_digests={"primary": DIGEST_A}, + provenance_references=("urn:example:provenance:1",), + ) + + +def _payload(*, result: str = "PASS") -> VstdScittPayload: + return VstdScittPayload.create(_receipt(result=result), _coordinates(result=result)) + + +def _scitt( + payload: VstdScittPayload, + *, + state: ScittEvidenceState = ScittEvidenceState.REGISTERED, + signed: bool = True, + receipt: bool = True, + payload_digest: str | None = None, + issuer: str = ISSUER, + subject: str = SUBJECT, +) -> ScittVerificationEvidence: + return ScittVerificationEvidence( + state=state, + statement_sha256=DIGEST_C, + payload_sha256=payload_digest or payload.payload_sha256(), + issuer=issuer, + subject=subject, + signed_statement_verified=signed, + receipt_verified=receipt, + verification_profile="RFC9943+RFC9942", + registration_policy="urn:example:registration-policy:v1", + transparency_service="https://transparency.example", + vds="RFC9162_SHA256", + native_result=state.value.lower(), + reason="native verifier fixture result", + registered_at="2026-08-23T00:00:00Z", + ) + + +def _vstd( + payload: VstdScittPayload, + *, + state: VstdVerificationState = VstdVerificationState.VERIFIED, + result: str | None = None, + receipt_digest: str | None = None, +) -> VstdVerificationEvidence: + return VstdVerificationEvidence( + state=state, + receipt_sha256=receipt_digest or payload.receipt_sha256, + native_result=result or payload.coordinates.native_result, + checker="verifier.core.kernel.check", + verification_profile="VSTD4-GDC-1/reference-kernel", + reason="native checker fixture result", + ) + + +def _compose( + payload: VstdScittPayload, + scitt: ScittVerificationEvidence, + *, + artifacts: dict[str, str] | None = None, +): + return compose_results( + payload, + _vstd(payload), + scitt, + artifact_digests=artifacts or {"primary": DIGEST_A}, + accepted_issuers=[ISSUER], + ) + + +def test_deterministic_serialization_and_round_trip_preserve_coordinates(): + payload = _payload() + encoded = payload.to_bytes() + assert encoded == payload.to_bytes() + assert b'": ' not in encoded + assert b", " not in encoded + + decoded = VstdScittPayload.from_bytes(encoded) + assert decoded.to_bytes() == encoded + assert decoded.coordinates.to_dict() == payload.coordinates.to_dict() + assert decoded.receipt_sha256 == payload.receipt_sha256 + assert decoded.coordinates.evidence_bounds["memory_bound"] == 10 + assert decoded.coordinates.provenance_references == ( + "urn:example:provenance:1", + ) + + +def test_native_vstd_payload_does_not_require_scitt_identity_or_log_coordinates(): + payload = _payload().to_dict() + serialized = json.dumps(payload, sort_keys=True) + for scitt_coordinate in ( + "issuer", + "transparency_service", + "registration_policy", + "registered_at", + ): + assert scitt_coordinate not in payload + assert f'"{scitt_coordinate}"' not in serialized + + template = create_scitt_registration_template( + _receipt(), _coordinates(), issuer=ISSUER, subject=SUBJECT + ).to_dict() + assert template["required_protected_header_projection"]["issuer"] == ISSUER + + +def test_noncanonical_or_extra_payload_fields_are_rejected(): + payload = _payload().to_dict() + payload["unexpected"] = True + with pytest.raises(InteropError, match="not in canonical form"): + VstdScittPayload.from_bytes(json.dumps(payload).encode()) + + canonical_with_extra = json.dumps( + payload, sort_keys=True, separators=(",", ":") + ).encode() + with pytest.raises(InteropError, match="keys mismatch"): + VstdScittPayload.from_bytes(canonical_with_extra) + + +def test_version_mismatch_and_unsupported_profile_fail_closed(): + payload = _payload().to_dict() + payload["mapping_version"] = "9.9" + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + with pytest.raises(InteropError, match="unsupported mapping version"): + VstdScittPayload.from_bytes(encoded) + + payload["mapping_version"] = "0.1" + payload["profile"] = "unknown-profile" + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + with pytest.raises(InteropError, match="unsupported profile"): + VstdScittPayload.from_bytes(encoded) + + +def test_receipt_identity_and_claim_coordinate_mismatch_are_rejected(): + receipt = _receipt() + receipt["receipt_id"] = "VFY-4-other" + with pytest.raises(InteropError, match="receipt_id"): + VstdScittPayload.create(receipt, _coordinates()) + + receipt = _receipt() + receipt["binding"]["coordinate"]["predicate"] = "other_predicate" + with pytest.raises(InteropError, match="binding coordinate"): + VstdScittPayload.create(receipt, _coordinates()) + + +def test_mutating_nested_receipt_after_creation_does_not_change_payload(): + receipt = _receipt() + payload = VstdScittPayload.create(receipt, _coordinates()) + before = payload.to_bytes() + receipt["binding"]["claim"] = "mutated by caller" + assert payload.to_bytes() == before + + +def test_registration_template_is_explicitly_not_cose_and_binds_subject(): + template = create_scitt_registration_template( + _receipt(), _coordinates(), issuer=ISSUER, subject=SUBJECT + ) + data = template.to_dict() + assert data["representation"] == "normalized-registration-input-not-cose" + assert data["payload_sha256"] == template.payload.payload_sha256() + assert data["required_protected_header_projection"]["issuer"] == ISSUER + assert data["required_protected_header_projection"]["subject"] == SUBJECT + + with pytest.raises(InteropError, match="subject must equal"): + create_scitt_registration_template( + _receipt(), _coordinates(), issuer=ISSUER, subject="artifact:other" + ) + + +def test_registered_vstd_pass_composes_to_pass_only_for_exact_artifact(): + payload = _payload() + result = _compose(payload, _scitt(payload)) + assert result.status is CompositionStatus.PASS + assert result.status_scope == "NATIVE_VSTD_RESULT_AND_SCITT_REGISTRATION" + assert result.vstd_conformance_status == "NOT_ESTABLISHED" + assert result.native_vstd_result == "PASS" + assert result.native_scitt_result == "registered" + assert "conformance NOT_ESTABLISHED" in result.reason + + +def test_registered_scitt_cannot_create_pass_without_bound_vstd_verification(): + payload = _payload() + result = compose_results( + payload, + _vstd(payload, state=VstdVerificationState.NOT_EVALUATED), + _scitt(payload), + artifact_digests={"primary": DIGEST_A}, + accepted_issuers=[ISSUER], + ) + assert result.status is CompositionStatus.UNKNOWN + assert result.reason == "native VSTD receipt was not evaluated" + + +def test_vstd_checker_result_must_bind_exact_receipt_and_native_result(): + payload = _payload() + wrong_receipt = compose_results( + payload, + _vstd(payload, receipt_digest=DIGEST_C), + _scitt(payload), + artifact_digests={"primary": DIGEST_A}, + accepted_issuers=[ISSUER], + ) + assert wrong_receipt.status is CompositionStatus.FAIL + assert "embedded receipt" in wrong_receipt.reason + + wrong_result = compose_results( + payload, + _vstd(payload, result="UNKNOWN"), + _scitt(payload), + artifact_digests={"primary": DIGEST_A}, + accepted_issuers=[ISSUER], + ) + assert wrong_result.status is CompositionStatus.FAIL + assert "payload result" in wrong_result.reason + + +def test_rejected_vstd_receipt_cannot_be_repaired_by_scitt_registration(): + payload = _payload() + result = compose_results( + payload, + _vstd(payload, state=VstdVerificationState.REJECTED), + _scitt(payload), + artifact_digests={"primary": DIGEST_A}, + accepted_issuers=[ISSUER], + ) + assert result.status is CompositionStatus.FAIL + assert "checker rejected" in result.reason + + +def test_registered_scitt_preserves_vstd_resource_indeterminacy(): + payload = _payload() + result = compose_results( + payload, + _vstd( + payload, + state=VstdVerificationState.INDETERMINATE, + result="UNKNOWN", + ), + _scitt(payload), + artifact_digests={"primary": DIGEST_A}, + accepted_issuers=[ISSUER], + ) + assert result.status is CompositionStatus.UNKNOWN + assert result.native_vstd_result == "UNKNOWN" + assert "unable to decide" in result.reason + + +def test_artifact_substitution_fails_even_when_scitt_registration_is_valid(): + payload = _payload() + result = _compose(payload, _scitt(payload), artifacts={"primary": DIGEST_B}) + assert result.status is CompositionStatus.FAIL + assert result.reason == "artifact binding mismatch" + + +def test_valid_registration_does_not_upgrade_failed_vstd_claim(): + payload = _payload(result="FAIL") + result = _compose(payload, _scitt(payload)) + assert result.status is CompositionStatus.FAIL + assert result.native_vstd_result == "FAIL" + + +@pytest.mark.parametrize("native", ["UNKNOWN", "INDETERMINATE", "UNSUPPORTED"]) +def test_registered_statement_preserves_vstd_indeterminacy(native): + payload = _payload(result=native) + result = _compose(payload, _scitt(payload)) + assert result.status is CompositionStatus.UNKNOWN + assert result.native_vstd_result == native + + +@pytest.mark.parametrize( + "state", + [ + ScittEvidenceState.MISSING, + ScittEvidenceState.STALE, + ScittEvidenceState.REVOKED, + ScittEvidenceState.SUPERSEDED, + ScittEvidenceState.UNKNOWN, + ], +) +def test_noncurrent_scitt_evidence_caps_vstd_pass_at_unknown(state): + payload = _payload() + result = _compose(payload, _scitt(payload, state=state)) + assert result.status is CompositionStatus.UNKNOWN + assert state.value in result.reason + + +def test_conflicted_evidence_is_not_collapsed_to_unknown_or_pass(): + payload = _payload() + result = _compose( + payload, _scitt(payload, state=ScittEvidenceState.CONFLICTED) + ) + assert result.status is CompositionStatus.CONFLICTED + + +def test_payload_transplant_is_detected_despite_verified_scitt_receipt(): + payload = _payload() + evidence = _scitt(payload, payload_digest=DIGEST_B) + result = _compose(payload, evidence) + assert result.status is CompositionStatus.FAIL + assert "payload" in result.reason + + +def test_wrong_issuer_and_subject_fail_relying_party_policy(): + payload = _payload() + wrong_issuer = _scitt(payload, issuer="https://other.example") + assert _compose(payload, wrong_issuer).status is CompositionStatus.FAIL + + wrong_subject = _scitt(payload, subject="artifact:other") + assert _compose(payload, wrong_subject).status is CompositionStatus.FAIL + + +def test_unverified_statement_or_receipt_cannot_be_called_registered(): + payload = _payload() + with pytest.raises(InteropError, match="REGISTERED requires"): + _scitt(payload, signed=False) + with pytest.raises(InteropError, match="REGISTERED requires"): + _scitt(payload, receipt=False) + + +def test_scitt_evidence_adapter_never_emits_computational_verdict(): + payload = _payload() + evidence = consume_scitt_evidence( + _scitt(payload), + expected_payload_sha256=payload.payload_sha256(), + expected_subject=SUBJECT, + accepted_issuers=[ISSUER], + ) + assert evidence["normalized_state"] == "REGISTERED" + assert evidence["computational_verdict"] == "NOT_EVALUATED" + + +def test_malformed_evidence_and_unknown_vstd_result_are_rejected(): + payload = _payload() + malformed = _scitt(payload).to_dict() + malformed["extra"] = "guess me" + with pytest.raises(InteropError, match="keys mismatch"): + ScittVerificationEvidence.from_dict(malformed) + + unsupported = _payload(result="VALID") + with pytest.raises(InteropError, match="refusing to guess"): + _compose(unsupported, _scitt(unsupported)) + + +def test_scitt_verification_evidence_round_trip(): + payload = _payload() + evidence = _scitt(payload) + decoded = ScittVerificationEvidence.from_dict(evidence.to_dict()) + assert decoded == evidence + + +def test_vstd_verification_evidence_round_trip_and_closed_shape(): + evidence = _vstd(_payload()) + assert evidence.to_dict()["conformance_status"] == "NOT_ESTABLISHED" + assert VstdVerificationEvidence.from_dict(evidence.to_dict()) == evidence + + legacy = evidence.to_dict() + del legacy["conformance_status"] + assert VstdVerificationEvidence.from_dict(legacy) == evidence + + promoted = evidence.to_dict() + promoted["conformance_status"] = "ESTABLISHED" + with pytest.raises(InteropError, match="cannot establish VSTD conformance"): + VstdVerificationEvidence.from_dict(promoted) + + malformed = evidence.to_dict() + malformed["extra"] = "guess me" + with pytest.raises(InteropError, match="keys mismatch"): + VstdVerificationEvidence.from_dict(malformed) From 3979a564220c77123d45c9dfa45a3452d3998ad3 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:46:47 -0700 Subject: [PATCH 05/34] Add the experimental workflow profile Reason: Represent bounded task allocation and hosted-result observations without upgrading repository or platform state into a verification verdict. Evidence: Profile model, schema, GitHub projection, command surface, example, experiment manifest, generated index, and tests change together. Coordinate: Experimental workflow interchange profile; non-normative release 1.2.0 surface. Falsification: Placement, assignment, repetition, or a hosted label increases assurance without a named checking mechanism. Compatibility: Optional experimental surface; it does not alter numbered-profile conformance or the base dependency set. --- docs/profiles/competition-evaluation.md | 21 +- docs/profiles/experimental-workflow.md | 182 ++++ .../experimental-workflow.schema.json | 898 ++++++++++++++++++ examples/experimental_workflow/README.md | 24 + examples/experimental_workflow/demo.py | 37 + .../github_snapshot.json | 46 + experiments/INDEX.md | 22 + .../github_verdict_neutrality/experiment.json | 190 ++++ scripts/build_experiment_index.py | 122 +++ .../experimental_workflow/__init__.py | 36 + src/verifier/experimental_workflow/github.py | 218 +++++ src/verifier/experimental_workflow/profile.py | 709 ++++++++++++++ src/verifier/experimental_workflow/schema.py | 276 ++++++ .../runtime/experimental_workflow_cli.py | 141 +++ tests/test_experimental_workflow_cli.py | 67 ++ tests/test_experimental_workflow_profile.py | 328 +++++++ 16 files changed, 3315 insertions(+), 2 deletions(-) create mode 100644 docs/profiles/experimental-workflow.md create mode 100644 docs/profiles/experimental-workflow.schema.json create mode 100644 examples/experimental_workflow/README.md create mode 100644 examples/experimental_workflow/demo.py create mode 100644 examples/experimental_workflow/github_snapshot.json create mode 100644 experiments/INDEX.md create mode 100644 experiments/github_verdict_neutrality/experiment.json create mode 100644 scripts/build_experiment_index.py create mode 100644 src/verifier/experimental_workflow/__init__.py create mode 100644 src/verifier/experimental_workflow/github.py create mode 100644 src/verifier/experimental_workflow/profile.py create mode 100644 src/verifier/experimental_workflow/schema.py create mode 100644 src/verifier/runtime/experimental_workflow_cli.py create mode 100644 tests/test_experimental_workflow_cli.py create mode 100644 tests/test_experimental_workflow_profile.py diff --git a/docs/profiles/competition-evaluation.md b/docs/profiles/competition-evaluation.md index 44f63cd..960dd54 100644 --- a/docs/profiles/competition-evaluation.md +++ b/docs/profiles/competition-evaluation.md @@ -1,5 +1,9 @@ # Competition evaluation profile +> **Acronyms:** artificial intelligence (AI); machine learning (ML); Verifier Standard (VSTD). + +> Reader aid: [concept glossary and primary precedents](../CONCEPTS_AND_PRECEDENTS.md). + **Status:** non-normative VSTD-1/VSTD-Graph integration profile **Version:** 0.1 **Date:** 2026-08-21 @@ -9,6 +13,19 @@ scientific-ML, agent, and other scored evaluations. It does not add a new VSTD v and does not claim adoption, affiliation, certification, or endorsement by any conference, competition, benchmark, or organizer. +The bounded public wording in +[`docs/CLAIMS_AND_LIMITS.md`](../CLAIMS_AND_LIMITS.md#competition-and-scored-evaluation-claims) +controls if a shorter phrase in this non-normative profile could be read more broadly. + +## VSTD-2 relationship + +Conceptually, this profile selects Verifier Standard (VSTD)-2 coordinates across the +submission, evaluator, environment, score, and their seams. It does not emit a +`VSTD-2` receipt or establish VSTD-2 conformance by itself. Each native scorer or +benchmark adapter must attribute its output to the exact selected coordinates, preserve +translation loss and horizons, and bind a separate assessment before any native result +becomes a VSTD judgment. + ## 1. Evaluation surface An integration declares the exact surface before it reports a verified result: @@ -43,7 +60,7 @@ rules + data snapshots + permitted externals Each artifact receives a stable identifier and content digest. Each transformation records its input and output roles, software identity, parameters, environment, and evidence classification. A declaration is not relabeled as direct observation or -independent reproduction. +reproduction by a distinct actor. ## 3. Predictive-evaluation time boundary @@ -67,7 +84,7 @@ artifact. A participant normally cannot observe or serialize hidden tests. The participant receipt therefore records an explicit horizon. An organizer can later close part of that horizon by publishing a commitment, signed attestation, disclosed snapshot, or -independently reproducible evaluator receipt. +evaluator receipt reproducible by a distinct actor. Absence of access is not evidence of hidden-test integrity. A participant-side `VERIFIED` result MUST NOT imply that the organizer's hidden corpus was uncontaminated, diff --git a/docs/profiles/experimental-workflow.md b/docs/profiles/experimental-workflow.md new file mode 100644 index 0000000..9b8b4d4 --- /dev/null +++ b/docs/profiles/experimental-workflow.md @@ -0,0 +1,182 @@ +# Experimental workflow profile + +> **Acronyms:** application programming interface (API); American Standard Code for Information Interchange (ASCII); +> command-line interface (CLI); JavaScript Object Notation (JSON); Boolean satisfiability problem (SAT); +> Secure Hash Algorithm 256-bit (SHA-256); Unicode Transformation Format, 8-bit (UTF-8); Verifier Standard (VSTD). + +**Status:** experimental, non-normative VSTD-1/VSTD-Graph integration profile +**Profile identifier:** `vstd.experimental-workflow` +**Version:** `0.1` +**Date:** 2026-08-24 + +This profile gives experiments a portable record of **what question is being tested, +what verification work was selected, why it was selected, how much work was allowed, +what the native tools actually returned, and what remains unresolved**. It lets two +workflow systems exchange the same experiment boundary without pretending that a GitHub +merge, successful job, publication, or verifier exit code is automatically a VSTD +`PASS`. + +The application profile is not a new numbered VSTD profile or verdict. It does not change +any serialized receipt identifier, canonical digest, schema `$id`, conformance behavior, or normative VSTD +semantics. + +## VSTD-2 relationship + +Conceptually, this profile is a reusable constraint over Verifier Standard (VSTD)-2 +verification geometry: the experiment is a subject; artifacts, actions, adapters, native +tools, and verifier mechanisms can be loci; dependencies and mappings are seams; and +horizons preserve uncovered coordinates. The profile does not emit a `VSTD-2` receipt, +so it establishes no VSTD-2 conformance by itself. A mapped judgment requires a separate +VSTD-2 surface and evidence-bearing assessment; profile identity or workflow completion +does not transfer a verdict. + +## 1. The portable unit + +A profile manifest binds these surfaces: + +| Surface | Required meaning | +|---|---| +| `experiment` | Stable identifier, question, lifecycle state, and start boundary. | +| `hypotheses` | Falsifiable statements. `SUPPORTED` remains evidence-bounded rather than universally true. | +| `preregistration` | Whether a plan was absent, drafted, frozen, or later amended, plus the bound artifact when frozen. | +| `artifacts` | Portable locators and lowercase SHA-256 digests. Local machine paths are prohibited. | +| `budgets` | Integer resource limits and recorded consumption. Every selected action binds at least one budget. | +| `actions` | The work selected, its priority, reason, alternatives, dependencies, trigger, substrate, and expected artifact effect. | +| `observations` | What was observed, with evidence references and limitations. | +| `interventions` | The declared change applied to bound artifacts and the artifacts it produced. | +| `native_results` | The exact native verifier status and its artifact. A separate mapping field records whether VSTD evaluation occurred. | +| `adaptations` | Which observations or challenges changed later actions or artifacts, and why. | +| `amendments` | Additive corrections that name what they supersede; history is not overwritten. | +| `challenges` | Open, resolved, or rejected attempts to refute a bound record. | +| `horizons` | Explicit `UNKNOWN`, `CONFLICTED`, `BLOCKED`, or out-of-scope surfaces. | +| `publication` | Distribution state only. Publication does not establish correctness or adoption. | +| `workflow_events` | Platform observations whose `verification_effect` is always `NONE`. | +| `manifest_digest` | SHA-256 over deterministic JSON for every other field. | + +The machine-readable shape is in +[`experimental-workflow.schema.json`](experimental-workflow.schema.json). The +standard-library validator is +[`profile.py`](../../src/verifier/experimental_workflow/profile.py). + +## 2. Bounded verification allocation + +An action records: + +1. a target and verifier substrate; +2. a positive integer priority; +3. why this action was selected; +4. evidence used for that selection; +5. alternatives considered; +6. an explicit resource budget and consumed amount; +7. dependencies and observations that triggered it; and +8. its expected effect on the artifact under construction. + +This makes allocation inspectable. It does **not** prove that the allocation was optimal, +unbiased, safe, or the only reasonable allocation. Priority is a scheduling coordinate, +not a truth coordinate. Exhausted work remains visible through the action state and +horizons instead of being rewritten as success. + +The profile deliberately does not prescribe Bayesian inference, decision trees, +boosted trees, control theory, embeddings, or any other selection engine. Those are +orchestrated substrates. Their native outputs can be bound as selection evidence, while +the portable fields above preserve the claim boundary between the allocation operator +and the mechanism it orchestrates. + +## 3. Native result and VSTD mapping boundary + +Every native result has a `mapping` object: + +- `NOT_EVALUATED` requires the VSTD verdict, mapping profile, and receipt reference to + remain `null`. +- `MAPPED` requires an explicit VSTD verdict, mapping profile, receipt artifact, and + reason. + +Recording `native_status = "PASS"`, `"SAT"`, `"proof verified"`, or any other tool +vocabulary does not authorize `mapping.status = "MAPPED"`. The actual mapping and bound +VSTD receipt are separate evidence. `UNKNOWN` and `CONFLICTED` remain distinct mapping +outcomes and cannot be dropped because the surrounding workflow completed. + +## 4. GitHub adapter + +[`github.py`](../../src/verifier/experimental_workflow/github.py) consumes a strict, +normalized snapshot rather than an unconstrained GitHub API response. It maps: + +| GitHub observation | Workflow event | +|---|---| +| issue state | `PLATFORM_ISSUE` | +| commit identity | `PLATFORM_COMMIT` | +| workflow run and conclusion | `PLATFORM_WORKFLOW_RUN` | +| workflow artifact availability | `PLATFORM_ARTIFACT` | +| pull-request and merge state | `PLATFORM_PULL_REQUEST` | + +Every emitted event sets `verification_effect = "NONE"`. In particular: + +- a successful Actions run is not a VSTD `PASS`; +- a merge is an integration event, not verification; +- an available artifact is not evidence that its bytes satisfy a claim; and +- a closed issue is not evidence that the underlying defect was corrected. + +Unknown fields are rejected rather than guessed into the portable representation. A +different workflow platform can implement the same event boundary without adopting +GitHub identifiers. + +## 5. Canonicalization + +The manifest digest uses UTF-8 JSON with: + +- keys sorted recursively; +- compact `,` and `:` separators; +- ASCII escaping enabled; +- no floating-point values; and +- `manifest_digest` omitted from its own input. + +The stored value is `sha256:<64 lowercase hexadecimal characters>`. This digest binds +the workflow record. It does not verify the bytes at an artifact locator; each artifact +has its own digest for that check. + +## 6. Dogfooding and index + +Experimental manifests live below `experiments/` as `experiment.json`. The command + +```bash +PYTHONPATH=src python scripts/build_experiment_index.py --check +``` + +validates every manifest and confirms that [`experiments/INDEX.md`](../../experiments/INDEX.md) +is current. The first bound record is the deterministic GitHub verdict-neutrality +specimen. A blocked experiment remains eligible for indexing once its intentional files +are isolated and its manifest honestly records the blocker; indexing is not publication +of a positive result. + +The runnable example under +[`examples/experimental_workflow/`](../../examples/experimental_workflow/) demonstrates +that a successful GitHub workflow and merged pull request remain verdict-neutral. + +The installed CLI exposes the same bounded surface: + +```bash +vstd experiment validate experiments/github_verdict_neutrality/experiment.json --json +vstd experiment github-events examples/experimental_workflow/github_snapshot.json --json +``` + +`validate` checks the strict profile shape and manifest digest. If a manifest contains +`repo:` artifact locators, supply `--repo-root PATH`; otherwise the command returns exit +code `2` and reports `VALID_WITH_UNCHECKED_REPOSITORY_ARTIFACTS` rather than silently +claiming those bytes were checked. + +## 7. Claims licensed by profile conformance + +For a valid, digest-matching manifest an implementation may state: + +> The experiment record conforms to experimental workflow profile 0.1 for the declared +> question, artifacts, budgets, actions, native results, adaptations, and horizons. + +This means the record is structurally valid and internally bound. It does not establish: + +- that the experiment was executed as recorded without supporting evidence; +- that a hypothesis is true outside its declared evidence; +- that a native verifier is correct; +- that a VSTD mapping is valid without checking its bound receipt; +- that a selected action was optimal; +- that a publication, commit, workflow, pull request, or merge is correct; +- external adoption, endorsement, independence, identity, authorization, or safety. diff --git a/docs/profiles/experimental-workflow.schema.json b/docs/profiles/experimental-workflow.schema.json new file mode 100644 index 0000000..f3d224a --- /dev/null +++ b/docs/profiles/experimental-workflow.schema.json @@ -0,0 +1,898 @@ +{ + "$comment": "Terminology: Verifier Standard (VSTD).", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://timelordraps.github.io/verifier/profiles/experimental-workflow.schema.json", + "title": "VSTD experimental workflow profile 0.1", + "description": "Non-normative, verdict-neutral interchange for bounded experimental work. Schema validity does not verify referenced evidence or native results.", + "type": "object", + "additionalProperties": false, + "properties": { + "profile": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "const": "vstd.experimental-workflow" + }, + "version": { + "const": "0.1" + }, + "status": { + "const": "EXPERIMENTAL_NON_NORMATIVE" + } + }, + "required": [ + "id", + "version", + "status" + ] + }, + "experiment": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "question": { + "type": "string", + "minLength": 1 + }, + "state": { + "enum": [ + "DRAFT", + "PREREGISTERED", + "RUNNING", + "BLOCKED", + "COMPLETED", + "ABANDONED" + ] + }, + "started_at": { + "type": [ + "string", + "null" + ], + "minLength": 1 + } + }, + "required": [ + "id", + "title", + "question", + "state", + "started_at" + ] + }, + "hypotheses": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "statement": { + "type": "string", + "minLength": 1 + }, + "falsification_condition": { + "type": "string", + "minLength": 1 + }, + "state": { + "enum": [ + "OPEN", + "SUPPORTED", + "REFUTED", + "UNKNOWN", + "CONFLICTED" + ] + } + }, + "required": [ + "id", + "statement", + "falsification_condition", + "state" + ] + }, + "minItems": 1 + }, + "preregistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "enum": [ + "NONE", + "DRAFT", + "FROZEN", + "AMENDED" + ] + }, + "recorded_at": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "artifact_id": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "limitations": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "state", + "recorded_at", + "artifact_id", + "limitations" + ] + }, + "artifacts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "role": { + "type": "string", + "minLength": 1 + }, + "media_type": { + "type": "string", + "minLength": 1 + }, + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "locator": { + "type": "string", + "pattern": "^(artifact:|git:|https://|repo:|urn:).+" + } + }, + "required": [ + "id", + "role", + "media_type", + "digest", + "locator" + ] + } + }, + "budgets": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "resource": { + "type": "string", + "minLength": 1 + }, + "limit": { + "type": "integer", + "minimum": 0 + }, + "consumed": { + "type": "integer", + "minimum": 0 + }, + "unit": { + "type": "string", + "minLength": 1 + }, + "scope": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "id", + "resource", + "limit", + "consumed", + "unit", + "scope" + ] + } + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "target": { + "type": "string", + "minLength": 1 + }, + "state": { + "enum": [ + "PLANNED", + "RUNNING", + "BLOCKED", + "COMPLETED", + "ABANDONED" + ] + }, + "priority": { + "type": "integer", + "minimum": 1 + }, + "selected_because": { + "type": "string", + "minLength": 1 + }, + "selection_evidence_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "alternatives_considered": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "budget_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "minItems": 1 + }, + "depends_on": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "triggered_by": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "expected_artifact_effect": { + "type": "string", + "minLength": 1 + }, + "substrate": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + }, + "coordinate": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "kind", + "name", + "version", + "coordinate" + ] + }, + "native_result_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "produced_artifact_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + } + }, + "required": [ + "id", + "kind", + "target", + "state", + "priority", + "selected_because", + "selection_evidence_ids", + "alternatives_considered", + "budget_ids", + "depends_on", + "triggered_by", + "expected_artifact_effect", + "substrate", + "native_result_ids", + "produced_artifact_ids" + ] + } + }, + "observations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "action_id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "recorded_at": { + "type": "string", + "minLength": 1 + }, + "statement": { + "type": "string", + "minLength": 1 + }, + "status": { + "enum": [ + "OBSERVED", + "UNKNOWN", + "CONFLICTED" + ] + }, + "evidence_artifact_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "limitations": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "id", + "action_id", + "recorded_at", + "statement", + "status", + "evidence_artifact_ids", + "limitations" + ] + } + }, + "interventions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "action_id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "description": { + "type": "string", + "minLength": 1 + }, + "applied_at": { + "type": "string", + "minLength": 1 + }, + "target_artifact_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "produced_artifact_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + } + }, + "required": [ + "id", + "action_id", + "description", + "applied_at", + "target_artifact_ids", + "produced_artifact_ids" + ] + } + }, + "native_results": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "action_id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "verifier": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + }, + "coordinate": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "kind", + "name", + "version", + "coordinate" + ] + }, + "native_status": { + "type": "string", + "minLength": 1 + }, + "result_artifact_id": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "mapping": { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "enum": [ + "NOT_EVALUATED", + "MAPPED" + ] + }, + "vstd_verdict": { + "type": [ + "string", + "null" + ], + "enum": [ + "PASS", + "FAIL", + "UNKNOWN", + "CONFLICTED", + "REJECTED", + null + ] + }, + "mapping_profile": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "receipt_artifact_id": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "status", + "vstd_verdict", + "mapping_profile", + "receipt_artifact_id", + "reason" + ] + } + }, + "required": [ + "id", + "action_id", + "verifier", + "native_status", + "result_artifact_id", + "mapping" + ] + } + }, + "adaptations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "trigger_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "decision": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + }, + "action_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "artifact_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + } + }, + "required": [ + "id", + "trigger_ids", + "decision", + "reason", + "action_ids", + "artifact_ids" + ] + } + }, + "amendments": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "recorded_at": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + }, + "supersedes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "artifact_id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + }, + "required": [ + "id", + "recorded_at", + "reason", + "supersedes", + "artifact_id" + ] + } + }, + "challenges": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "target_id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "state": { + "enum": [ + "OPEN", + "RESOLVED", + "REJECTED" + ] + }, + "statement": { + "type": "string", + "minLength": 1 + }, + "evidence_artifact_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + } + }, + "required": [ + "id", + "target_id", + "state", + "statement", + "evidence_artifact_ids" + ] + } + }, + "horizons": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "status": { + "enum": [ + "UNKNOWN", + "CONFLICTED", + "BLOCKED", + "OUT_OF_SCOPE" + ] + }, + "description": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "id", + "status", + "description", + "reason" + ] + } + }, + "publication": { + "type": "object", + "additionalProperties": false, + "properties": { + "state": { + "enum": [ + "PRIVATE", + "INTERNAL", + "CANDIDATE", + "PUBLISHED", + "RETRACTED" + ] + }, + "artifact_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + } + }, + "required": [ + "state", + "artifact_ids" + ] + }, + "workflow_events": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "kind": { + "enum": [ + "PLATFORM_ISSUE", + "PLATFORM_COMMIT", + "PLATFORM_WORKFLOW_RUN", + "PLATFORM_ARTIFACT", + "PLATFORM_PULL_REQUEST" + ] + }, + "recorded_at": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "object", + "additionalProperties": false, + "properties": { + "platform": { + "type": "string", + "minLength": 1 + }, + "repository": { + "type": "string", + "minLength": 1 + }, + "coordinate": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "platform", + "repository", + "coordinate" + ] + }, + "native_state": { + "type": "string", + "minLength": 1 + }, + "verification_effect": { + "const": "NONE" + }, + "details": { + "type": "object" + } + }, + "required": [ + "id", + "kind", + "recorded_at", + "source", + "native_state", + "verification_effect", + "details" + ] + } + }, + "manifest_digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + } + }, + "required": [ + "profile", + "experiment", + "hypotheses", + "preregistration", + "artifacts", + "budgets", + "actions", + "observations", + "interventions", + "native_results", + "adaptations", + "amendments", + "challenges", + "horizons", + "publication", + "workflow_events", + "manifest_digest" + ] +} diff --git a/examples/experimental_workflow/README.md b/examples/experimental_workflow/README.md new file mode 100644 index 0000000..65922ad --- /dev/null +++ b/examples/experimental_workflow/README.md @@ -0,0 +1,24 @@ +# Experimental workflow example + +This deterministic example maps a normalized GitHub snapshot containing a successful +workflow, an available artifact, a closed issue, a commit, and a merged pull request. +All five records remain platform observations with `verification_effect = "NONE"`. +The canonical profile manifest is indexed at +[`experiments/github_verdict_neutrality/experiment.json`](../../experiments/github_verdict_neutrality/experiment.json). + +From the repository root: + +```bash +PYTHONPATH=src python examples/experimental_workflow/demo.py +``` + +Expected boundary: + +```text +events: 5 +vstd_verdicts_granted: 0 +``` + +The example demonstrates portable workflow serialization and non-upgrade behavior. It +does not contact GitHub, validate a signature, execute a domain verifier, or establish +that the issue, commit, workflow, artifact, or merged change is correct. diff --git a/examples/experimental_workflow/demo.py b/examples/experimental_workflow/demo.py new file mode 100644 index 0000000..3b8b4f0 --- /dev/null +++ b/examples/experimental_workflow/demo.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Terminology: Verifier Standard (VSTD). + +Demonstrate that GitHub success and merge state grant no VSTD verdict.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from verifier.experimental_workflow import github_snapshot_to_events, load_manifest + + +HERE = Path(__file__).resolve().parent +MANIFEST = HERE.parents[1] / "experiments" / "github_verdict_neutrality" / "experiment.json" + + +def main() -> int: + snapshot = json.loads((HERE / "github_snapshot.json").read_text(encoding="utf-8")) + expected = load_manifest(MANIFEST) + events = github_snapshot_to_events(snapshot) + if list(events) != expected["workflow_events"]: + raise SystemExit("generated GitHub events do not match the bound manifest") + if any(event["verification_effect"] != "NONE" for event in events): + raise SystemExit("a platform event was incorrectly upgraded") + summary = { + "events": len(events), + "native_states": sorted({event["native_state"] for event in events}), + "vstd_verdicts_granted": 0, + "manifest_digest": expected["manifest_digest"], + } + print(json.dumps(summary, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/experimental_workflow/github_snapshot.json b/examples/experimental_workflow/github_snapshot.json new file mode 100644 index 0000000..d6bdadd --- /dev/null +++ b/examples/experimental_workflow/github_snapshot.json @@ -0,0 +1,46 @@ +{ + "commits": [ + { + "committed_at": "2026-08-24T12:00:00Z", + "sha": "1111111111111111111111111111111111111111", + "subject": "Run bounded checker" + } + ], + "issues": [ + { + "number": 41, + "state": "closed", + "title": "Test the bounded checker", + "updated_at": "2026-08-24T12:04:00Z" + } + ], + "pull_requests": [ + { + "base_sha": "0000000000000000000000000000000000000000", + "head_sha": "1111111111111111111111111111111111111111", + "merged": true, + "number": 42, + "state": "closed", + "updated_at": "2026-08-24T12:05:00Z" + } + ], + "repository": "github:example/verifier-integration", + "workflow_runs": [ + { + "artifacts": [ + { + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "expired": false, + "id": 9002, + "name": "checker-output" + } + ], + "conclusion": "success", + "head_sha": "1111111111111111111111111111111111111111", + "id": 9001, + "status": "completed", + "updated_at": "2026-08-24T12:03:00Z", + "workflow": "conformance" + } + ] +} diff --git a/experiments/INDEX.md b/experiments/INDEX.md new file mode 100644 index 0000000..6639035 --- /dev/null +++ b/experiments/INDEX.md @@ -0,0 +1,22 @@ +# Experimental work index + +> **Acronym:** Verifier Standard (VSTD). + +> **Experimental and non-normative.** Inclusion means that a profile manifest +> is structurally valid and its `repo:` artifacts match their bound digests. It +> does not establish a hypothesis, verifier, publication, or VSTD verdict. + +Regenerate or check this file with: + +```bash +PYTHONPATH=src python scripts/build_experiment_index.py --check +``` + +| Experiment | State | Question | Publication | Open horizons | Manifest | +|---|---|---|---|---:|---| +| experiment-artifact-first-mechanisms | RUNNING | Which bounded event serialization, TRUST (mechanism-earned forward artifact support) transfer algebra, ROT (typed time-indexed current-admissibility degradation) derivation and propagation, RUST (inverse-TRUST diagnostic backtrace) concentration and localization rules, and hidden-witness trichotomy mechanisms can implement the governing artifact-first causal-provenance orientation without actor reputation, scalar cancellation, causal-localization overclaim, or making that orientation contingent on the study? | CANDIDATE | 6 | [`experiments/artifact_first_mechanisms/experiment.json`](artifact_first_mechanisms/experiment.json)
`sha256:98f1aaf7d5d7c8fa3cea8e3f58ce93c50111fbd1e41bf5e49ead8276b438e9dc` | +| experiment-github-verdict-neutrality | COMPLETED | Does the GitHub adapter preserve successful workflow and merge states without converting them into a VSTD verdict? | INTERNAL | 1 | [`experiments/github_verdict_neutrality/experiment.json`](github_verdict_neutrality/experiment.json)
`sha256:3b98310d35c20e7099d242e2c655e4bf8dc62d91298adc04e4dc2f56f2f79d89` | + +Platform events, including successful workflows and merges, retain +`verification_effect = NONE` unless a separate native result is explicitly +mapped through a bound VSTD receipt. diff --git a/experiments/github_verdict_neutrality/experiment.json b/experiments/github_verdict_neutrality/experiment.json new file mode 100644 index 0000000..e9e5b62 --- /dev/null +++ b/experiments/github_verdict_neutrality/experiment.json @@ -0,0 +1,190 @@ +{ + "profile": { + "id": "vstd.experimental-workflow", + "version": "0.1", + "status": "EXPERIMENTAL_NON_NORMATIVE" + }, + "experiment": { + "id": "experiment-github-verdict-neutrality", + "title": "GitHub workflow observations remain verdict-neutral", + "question": "Does the GitHub adapter preserve successful workflow and merge states without converting them into a VSTD verdict?", + "state": "COMPLETED", + "started_at": "2026-08-24T12:00:00Z" + }, + "hypotheses": [ + { + "id": "hypothesis-platform-non-upgrade", + "statement": "Every supported GitHub observation maps with verification_effect NONE.", + "falsification_condition": "Any supported snapshot produces a workflow event that grants or implies a VSTD verdict.", + "state": "SUPPORTED" + } + ], + "preregistration": { + "state": "NONE", + "recorded_at": null, + "artifact_id": null, + "limitations": [ + "This is a deterministic adapter specimen rather than a preregistered empirical study." + ] + }, + "artifacts": [], + "budgets": [ + { + "id": "budget-github-events", + "resource": "normalized-platform-events", + "limit": 5, + "consumed": 5, + "unit": "event", + "scope": "checked-in GitHub snapshot" + } + ], + "actions": [ + { + "id": "action-map-github-snapshot", + "kind": "WORKFLOW_ADAPTER_MAPPING", + "target": "normalized GitHub issue, commit, workflow, artifact, and pull-request states", + "state": "COMPLETED", + "priority": 1, + "selected_because": "The public repository is hosted on GitHub, so the first adapter must demonstrate that native repository success does not become verification success.", + "selection_evidence_ids": [ + "hypothesis-platform-non-upgrade" + ], + "alternatives_considered": [ + "treat Git history as the experiment record", + "map workflow success to PASS" + ], + "budget_ids": [ + "budget-github-events" + ], + "depends_on": [], + "triggered_by": [], + "expected_artifact_effect": "Produce a portable event record whose platform successes have no verification effect.", + "substrate": { + "kind": "workflow-platform-adapter", + "name": "VSTD normalized GitHub adapter", + "version": "0.1", + "coordinate": "repo:src/verifier/experimental_workflow/github.py" + }, + "native_result_ids": [], + "produced_artifact_ids": [] + } + ], + "observations": [ + { + "id": "observation-five-neutral-events", + "action_id": "action-map-github-snapshot", + "recorded_at": "2026-08-24T12:06:00Z", + "statement": "Five supported GitHub observations were mapped and every event retained verification_effect NONE.", + "status": "OBSERVED", + "evidence_artifact_ids": [], + "limitations": [ + "The normalized snapshot is a checked-in specimen and is not a live GitHub API observation." + ] + } + ], + "native_results": [], + "adaptations": [], + "amendments": [], + "challenges": [], + "horizons": [ + { + "id": "horizon-underlying-correctness", + "status": "UNKNOWN", + "description": "Correctness of the change represented by the workflow and pull request", + "reason": "Repository state alone does not include a bound domain-verifier result and VSTD receipt." + } + ], + "publication": { + "state": "INTERNAL", + "artifact_ids": [] + }, + "workflow_events": [ + { + "id": "github-event-16194f8f10fe0e61e766", + "kind": "PLATFORM_WORKFLOW_RUN", + "recorded_at": "2026-08-24T12:03:00Z", + "source": { + "platform": "github", + "repository": "github:example/verifier-integration", + "coordinate": "workflow-run:9001" + }, + "native_state": "completed/success", + "verification_effect": "NONE", + "details": { + "id": 9001, + "workflow": "conformance", + "head_sha": "1111111111111111111111111111111111111111" + } + }, + { + "id": "github-event-542a5a9a6b0b9b37d3a3", + "kind": "PLATFORM_PULL_REQUEST", + "recorded_at": "2026-08-24T12:05:00Z", + "source": { + "platform": "github", + "repository": "github:example/verifier-integration", + "coordinate": "pull-request:42" + }, + "native_state": "closed/MERGED", + "verification_effect": "NONE", + "details": { + "number": 42, + "head_sha": "1111111111111111111111111111111111111111", + "base_sha": "0000000000000000000000000000000000000000", + "merged": true + } + }, + { + "id": "github-event-6d179bfe41711ce7be21", + "kind": "PLATFORM_COMMIT", + "recorded_at": "2026-08-24T12:00:00Z", + "source": { + "platform": "github", + "repository": "github:example/verifier-integration", + "coordinate": "commit:1111111111111111111111111111111111111111" + }, + "native_state": "RECORDED", + "verification_effect": "NONE", + "details": { + "sha": "1111111111111111111111111111111111111111", + "subject": "Run bounded checker" + } + }, + { + "id": "github-event-bef92a6556f4ddf05651", + "kind": "PLATFORM_ISSUE", + "recorded_at": "2026-08-24T12:04:00Z", + "source": { + "platform": "github", + "repository": "github:example/verifier-integration", + "coordinate": "issue:41" + }, + "native_state": "closed", + "verification_effect": "NONE", + "details": { + "number": 41, + "title": "Test the bounded checker" + } + }, + { + "id": "github-event-d32d5b76ae8d24c7d5f7", + "kind": "PLATFORM_ARTIFACT", + "recorded_at": "2026-08-24T12:03:00Z", + "source": { + "platform": "github", + "repository": "github:example/verifier-integration", + "coordinate": "workflow-artifact:9002" + }, + "native_state": "AVAILABLE", + "verification_effect": "NONE", + "details": { + "id": 9002, + "name": "checker-output", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "run_id": 9001 + } + } + ], + "interventions": [], + "manifest_digest": "sha256:3b98310d35c20e7099d242e2c655e4bf8dc62d91298adc04e4dc2f56f2f79d89" +} diff --git a/scripts/build_experiment_index.py b/scripts/build_experiment_index.py new file mode 100644 index 0000000..88b2fc0 --- /dev/null +++ b/scripts/build_experiment_index.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Terminology: Verifier Standard (VSTD). + +Validate experimental manifests and build their deterministic public index.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = ROOT / "src" +if str(SOURCE_ROOT) not in sys.path: + sys.path.insert(0, str(SOURCE_ROOT)) + +from verifier.experimental_workflow import load_manifest, verify_repo_artifacts + + +EXPERIMENTS = ROOT / "experiments" +INDEX = EXPERIMENTS / "INDEX.md" + + +def _cell(value: object) -> str: + return str(value).replace("|", "\\|").replace("\n", " ") + + +def discover(root: Path = ROOT) -> tuple[tuple[Path, dict[str, object]], ...]: + """Load every intentional experiment manifest and verify bound repo artifacts.""" + + experiments = root / "experiments" + records: list[tuple[Path, dict[str, object]]] = [] + for path in sorted(experiments.glob("**/experiment.json")): + payload = load_manifest(path) + verify_repo_artifacts(payload, root) + records.append((path.relative_to(root), payload)) + if not records: + raise RuntimeError("no experiments/**/experiment.json manifests were found") + return tuple(records) + + +def render(records: tuple[tuple[Path, dict[str, object]], ...]) -> str: + """Render a stable, human-readable view without granting experiment verdicts.""" + + lines = [ + "# Experimental work index", + "", + "> **Acronym:** Verifier Standard (VSTD).", + "", + "> **Experimental and non-normative.** Inclusion means that a profile manifest", + "> is structurally valid and its `repo:` artifacts match their bound digests. It", + "> does not establish a hypothesis, verifier, publication, or VSTD verdict.", + "", + "Regenerate or check this file with:", + "", + "```bash", + "PYTHONPATH=src python scripts/build_experiment_index.py --check", + "```", + "", + "| Experiment | State | Question | Publication | Open horizons | Manifest |", + "|---|---|---|---|---:|---|", + ] + for relative, payload in records: + experiment = payload["experiment"] + publication = payload["publication"] + horizons = payload["horizons"] + digest = payload["manifest_digest"] + assert isinstance(experiment, dict) + assert isinstance(publication, dict) + assert isinstance(horizons, list) + assert isinstance(digest, str) + unresolved = sum( + 1 + for horizon in horizons + if isinstance(horizon, dict) + and horizon.get("status") in {"UNKNOWN", "CONFLICTED", "BLOCKED"} + ) + path_text = relative.as_posix() + link_text = relative.relative_to("experiments").as_posix() + lines.append( + "| {identifier} | {state} | {question} | {publication} | {horizons} | " + "[`{path}`]({link})
`{digest}` |".format( + identifier=_cell(experiment["id"]), + state=_cell(experiment["state"]), + question=_cell(experiment["question"]), + publication=_cell(publication["state"]), + horizons=unresolved, + path=path_text, + link=link_text, + digest=digest, + ) + ) + lines.extend( + [ + "", + "Platform events, including successful workflows and merges, retain", + "`verification_effect = NONE` unless a separate native result is explicitly", + "mapped through a bound VSTD receipt.", + "", + ] + ) + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true", help="Fail if INDEX.md is stale.") + args = parser.parse_args(argv) + expected = render(discover()) + if args.check: + if not INDEX.is_file() or INDEX.read_text(encoding="utf-8") != expected: + print("[EXPERIMENT INDEX FAILED] experiments/INDEX.md is stale") + return 1 + print("[EXPERIMENT INDEX OK] manifests and repository artifacts verified") + return 0 + INDEX.write_text(expected, encoding="utf-8", newline="\n") + print(f"[EXPERIMENT INDEX WRITTEN] {INDEX.relative_to(ROOT).as_posix()}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/verifier/experimental_workflow/__init__.py b/src/verifier/experimental_workflow/__init__.py new file mode 100644 index 0000000..99a3e95 --- /dev/null +++ b/src/verifier/experimental_workflow/__init__.py @@ -0,0 +1,36 @@ +"""Terminology: identifier (ID); Verifier Standard (VSTD). + +Experimental workflow profile; non-normative and verdict-neutral.""" + +from __future__ import annotations + +from .github import GitHubAdapterError, github_snapshot_to_events +from .profile import ( + PROFILE_ID, + PROFILE_STATUS, + PROFILE_VERSION, + WorkflowProfileError, + canonical_bytes, + load_manifest, + manifest_digest, + seal_manifest, + validate_manifest, + verify_repo_artifacts, +) +from .schema import workflow_manifest_schema + +__all__ = [ + "GitHubAdapterError", + "PROFILE_ID", + "PROFILE_STATUS", + "PROFILE_VERSION", + "WorkflowProfileError", + "canonical_bytes", + "github_snapshot_to_events", + "load_manifest", + "manifest_digest", + "seal_manifest", + "validate_manifest", + "verify_repo_artifacts", + "workflow_manifest_schema", +] diff --git a/src/verifier/experimental_workflow/github.py b/src/verifier/experimental_workflow/github.py new file mode 100644 index 0000000..c043c31 --- /dev/null +++ b/src/verifier/experimental_workflow/github.py @@ -0,0 +1,218 @@ +"""Terminology: application programming interface (API); Verifier Standard (VSTD). + +Deterministic GitHub-to-workflow observations with no verification upgrade.""" + +from __future__ import annotations + +import hashlib +import json +from typing import Any, Mapping + + +class GitHubAdapterError(ValueError): + """Raised when the normalized GitHub snapshot is incomplete or unsupported.""" + + +def _expect_object(value: Any, path: str) -> Mapping[str, Any]: + if not isinstance(value, Mapping): + raise GitHubAdapterError(f"{path} must be an object") + return value + + +def _expect_array(value: Any, path: str) -> list[Any]: + if not isinstance(value, list): + raise GitHubAdapterError(f"{path} must be an array") + return value + + +def _exact(value: Mapping[str, Any], path: str, fields: set[str]) -> None: + missing = sorted(fields - set(value)) + unknown = sorted(set(value) - fields) + if missing: + raise GitHubAdapterError(f"{path} missing fields: {', '.join(missing)}") + if unknown: + raise GitHubAdapterError(f"{path} unsupported fields: {', '.join(unknown)}") + + +def _text(value: Any, path: str, *, nullable: bool = False) -> str | None: + if nullable and value is None: + return None + if not isinstance(value, str) or not value.strip(): + raise GitHubAdapterError(f"{path} must be a non-empty string") + return value + + +def _integer(value: Any, path: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise GitHubAdapterError(f"{path} must be a non-negative integer") + return value + + +def _event_id(kind: str, repository: str, coordinate: str) -> str: + stable = json.dumps( + [kind, repository, coordinate], ensure_ascii=True, separators=(",", ":") + ).encode("utf-8") + return f"github-event-{hashlib.sha256(stable).hexdigest()[:20]}" + + +def _event( + *, + kind: str, + repository: str, + coordinate: str, + recorded_at: str, + native_state: str, + details: Mapping[str, Any], +) -> dict[str, Any]: + return { + "id": _event_id(kind, repository, coordinate), + "kind": kind, + "recorded_at": recorded_at, + "source": { + "platform": "github", + "repository": repository, + "coordinate": coordinate, + }, + "native_state": native_state, + "verification_effect": "NONE", + "details": dict(details), + } + + +def github_snapshot_to_events(snapshot: Mapping[str, Any]) -> tuple[dict[str, Any], ...]: + """Map the documented normalized snapshot to verdict-neutral workflow events. + + The input is not the unconstrained GitHub API response. Rejecting unknown fields + prevents a caller from assuming that unparsed platform semantics were preserved. + A successful workflow or merged pull request remains a platform fact only. + """ + + root = _expect_object(snapshot, "$") + _exact( + root, + "$", + {"repository", "issues", "commits", "workflow_runs", "pull_requests"}, + ) + repository = _text(root["repository"], "$.repository") + assert repository is not None + events: list[dict[str, Any]] = [] + + for index, value in enumerate(_expect_array(root["issues"], "$.issues")): + path = f"$.issues[{index}]" + item = _expect_object(value, path) + _exact(item, path, {"number", "title", "state", "updated_at"}) + number = _integer(item["number"], f"{path}.number") + title = _text(item["title"], f"{path}.title") + state = _text(item["state"], f"{path}.state") + updated_at = _text(item["updated_at"], f"{path}.updated_at") + assert title is not None and state is not None and updated_at is not None + events.append( + _event( + kind="PLATFORM_ISSUE", + repository=repository, + coordinate=f"issue:{number}", + recorded_at=updated_at, + native_state=state, + details={"number": number, "title": title}, + ) + ) + + for index, value in enumerate(_expect_array(root["commits"], "$.commits")): + path = f"$.commits[{index}]" + item = _expect_object(value, path) + _exact(item, path, {"sha", "subject", "committed_at"}) + sha = _text(item["sha"], f"{path}.sha") + subject = _text(item["subject"], f"{path}.subject") + committed_at = _text(item["committed_at"], f"{path}.committed_at") + assert sha is not None and subject is not None and committed_at is not None + events.append( + _event( + kind="PLATFORM_COMMIT", + repository=repository, + coordinate=f"commit:{sha}", + recorded_at=committed_at, + native_state="RECORDED", + details={"sha": sha, "subject": subject}, + ) + ) + + for index, value in enumerate(_expect_array(root["workflow_runs"], "$.workflow_runs")): + path = f"$.workflow_runs[{index}]" + item = _expect_object(value, path) + _exact( + item, + path, + {"id", "workflow", "status", "conclusion", "head_sha", "updated_at", "artifacts"}, + ) + run_id = _integer(item["id"], f"{path}.id") + workflow = _text(item["workflow"], f"{path}.workflow") + status = _text(item["status"], f"{path}.status") + conclusion = _text(item["conclusion"], f"{path}.conclusion", nullable=True) + head_sha = _text(item["head_sha"], f"{path}.head_sha") + updated_at = _text(item["updated_at"], f"{path}.updated_at") + assert workflow is not None and status is not None and head_sha is not None and updated_at is not None + native_state = status if conclusion is None else f"{status}/{conclusion}" + events.append( + _event( + kind="PLATFORM_WORKFLOW_RUN", + repository=repository, + coordinate=f"workflow-run:{run_id}", + recorded_at=updated_at, + native_state=native_state, + details={"id": run_id, "workflow": workflow, "head_sha": head_sha}, + ) + ) + for artifact_index, artifact_value in enumerate( + _expect_array(item["artifacts"], f"{path}.artifacts") + ): + artifact_path = f"{path}.artifacts[{artifact_index}]" + artifact = _expect_object(artifact_value, artifact_path) + _exact(artifact, artifact_path, {"id", "name", "digest", "expired"}) + artifact_id = _integer(artifact["id"], f"{artifact_path}.id") + name = _text(artifact["name"], f"{artifact_path}.name") + digest = _text(artifact["digest"], f"{artifact_path}.digest", nullable=True) + expired = artifact["expired"] + if not isinstance(expired, bool): + raise GitHubAdapterError(f"{artifact_path}.expired must be boolean") + assert name is not None + events.append( + _event( + kind="PLATFORM_ARTIFACT", + repository=repository, + coordinate=f"workflow-artifact:{artifact_id}", + recorded_at=updated_at, + native_state="EXPIRED" if expired else "AVAILABLE", + details={"id": artifact_id, "name": name, "digest": digest, "run_id": run_id}, + ) + ) + + for index, value in enumerate(_expect_array(root["pull_requests"], "$.pull_requests")): + path = f"$.pull_requests[{index}]" + item = _expect_object(value, path) + _exact(item, path, {"number", "state", "merged", "head_sha", "base_sha", "updated_at"}) + number = _integer(item["number"], f"{path}.number") + state = _text(item["state"], f"{path}.state") + merged = item["merged"] + if not isinstance(merged, bool): + raise GitHubAdapterError(f"{path}.merged must be boolean") + head_sha = _text(item["head_sha"], f"{path}.head_sha") + base_sha = _text(item["base_sha"], f"{path}.base_sha") + updated_at = _text(item["updated_at"], f"{path}.updated_at") + assert state is not None and head_sha is not None and base_sha is not None and updated_at is not None + events.append( + _event( + kind="PLATFORM_PULL_REQUEST", + repository=repository, + coordinate=f"pull-request:{number}", + recorded_at=updated_at, + native_state=f"{state}/{'MERGED' if merged else 'NOT_MERGED'}", + details={ + "number": number, + "head_sha": head_sha, + "base_sha": base_sha, + "merged": merged, + }, + ) + ) + + return tuple(sorted(events, key=lambda item: item["id"])) diff --git a/src/verifier/experimental_workflow/profile.py b/src/verifier/experimental_workflow/profile.py new file mode 100644 index 0000000..0e4af86 --- /dev/null +++ b/src/verifier/experimental_workflow/profile.py @@ -0,0 +1,709 @@ +"""Terminology: identifier (ID); JavaScript Object Notation (JSON); +Secure Hash Algorithm 256-bit (SHA-256); Unicode Transformation Format, 8-bit (UTF-8); +Verifier Standard (VSTD). + +Validate the non-normative experimental-workflow profile. + +This module records allocation and workflow facts. It deliberately does not execute +domain verifiers, derive VSTD verdicts, or treat repository state as verification. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import re +from pathlib import Path, PurePosixPath +from typing import Any, Mapping + + +PROFILE_ID = "vstd.experimental-workflow" +PROFILE_VERSION = "0.1" +PROFILE_STATUS = "EXPERIMENTAL_NON_NORMATIVE" + +EXPERIMENT_STATES = frozenset( + {"DRAFT", "PREREGISTERED", "RUNNING", "BLOCKED", "COMPLETED", "ABANDONED"} +) +HYPOTHESIS_STATES = frozenset({"OPEN", "SUPPORTED", "REFUTED", "UNKNOWN", "CONFLICTED"}) +PREREGISTRATION_STATES = frozenset({"NONE", "DRAFT", "FROZEN", "AMENDED"}) +ACTION_STATES = frozenset({"PLANNED", "RUNNING", "BLOCKED", "COMPLETED", "ABANDONED"}) +OBSERVATION_STATES = frozenset({"OBSERVED", "UNKNOWN", "CONFLICTED"}) +MAPPING_STATES = frozenset({"NOT_EVALUATED", "MAPPED"}) +VSTD_VERDICTS = frozenset({"PASS", "FAIL", "UNKNOWN", "CONFLICTED", "REJECTED"}) +CHALLENGE_STATES = frozenset({"OPEN", "RESOLVED", "REJECTED"}) +HORIZON_STATES = frozenset({"UNKNOWN", "CONFLICTED", "BLOCKED", "OUT_OF_SCOPE"}) +PUBLICATION_STATES = frozenset({"PRIVATE", "INTERNAL", "CANDIDATE", "PUBLISHED", "RETRACTED"}) +PLATFORM_EVENT_KINDS = frozenset( + { + "PLATFORM_ISSUE", + "PLATFORM_COMMIT", + "PLATFORM_WORKFLOW_RUN", + "PLATFORM_ARTIFACT", + "PLATFORM_PULL_REQUEST", + } +) + +_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") +_PORTABLE_LOCATOR_PREFIXES = ( + "artifact:", + "git:", + "https://", + "repo:", + "urn:", +) + + +class WorkflowProfileError(ValueError): + """Raised when a workflow manifest exceeds or violates the profile boundary.""" + + +def _fail(path: str, message: str) -> None: + raise WorkflowProfileError(f"{path}: {message}") + + +def _mapping(value: Any, path: str) -> Mapping[str, Any]: + if not isinstance(value, Mapping): + _fail(path, "must be an object") + return value + + +def _sequence(value: Any, path: str) -> list[Any]: + if not isinstance(value, list): + _fail(path, "must be an array") + return value + + +def _string(value: Any, path: str, *, nullable: bool = False) -> str | None: + if nullable and value is None: + return None + if not isinstance(value, str) or not value.strip(): + _fail(path, "must be a non-empty string") + return value + + +def _string_list(value: Any, path: str) -> list[str]: + items = _sequence(value, path) + for index, item in enumerate(items): + _string(item, f"{path}[{index}]") + if len(items) != len(set(items)): + _fail(path, "must not contain duplicates") + return items + + +def _exact_keys( + value: Mapping[str, Any], + path: str, + *, + required: set[str], + optional: set[str] | None = None, +) -> None: + optional = optional or set() + missing = sorted(required - set(value)) + unknown = sorted(set(value) - required - optional) + if missing: + _fail(path, f"missing fields: {', '.join(missing)}") + if unknown: + _fail(path, f"unsupported fields: {', '.join(unknown)}") + + +def _enum(value: Any, allowed: frozenset[str], path: str) -> str: + text = _string(value, path) + assert text is not None + if text not in allowed: + _fail(path, f"unsupported value {text!r}") + return text + + +def _nonnegative_integer(value: Any, path: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + _fail(path, "must be a non-negative integer") + return value + + +def _reject_floats(value: Any, path: str = "$", *, seen: set[int] | None = None) -> None: + if isinstance(value, float): + _fail(path, "floating-point values are not canonical in this profile") + if isinstance(value, Mapping): + seen = seen or set() + identity = id(value) + if identity in seen: + _fail(path, "cyclic objects cannot be serialized") + seen.add(identity) + for key, item in value.items(): + if not isinstance(key, str): + _fail(path, "object keys must be strings") + _reject_floats(item, f"{path}.{key}", seen=seen) + seen.remove(identity) + elif isinstance(value, (list, tuple)): + seen = seen or set() + identity = id(value) + if identity in seen: + _fail(path, "cyclic arrays cannot be serialized") + seen.add(identity) + for index, item in enumerate(value): + _reject_floats(item, f"{path}[{index}]", seen=seen) + seen.remove(identity) + elif value is not None and not isinstance(value, (str, int, bool)): + _fail(path, f"unsupported canonical type {type(value).__name__}") + + +def canonical_bytes(payload: Any) -> bytes: + """Return deterministic UTF-8 JSON bytes after rejecting ambiguous numeric input.""" + + _reject_floats(payload) + return json.dumps( + payload, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + + +def manifest_digest(payload: Mapping[str, Any]) -> str: + """Digest every manifest field except the digest that seals those fields.""" + + stable = dict(payload) + stable.pop("manifest_digest", None) + return "sha256:" + hashlib.sha256(canonical_bytes(stable)).hexdigest() + + +def seal_manifest(payload: Mapping[str, Any]) -> dict[str, Any]: + """Deep-copy and seal a manifest without mutating the caller's object.""" + + sealed = copy.deepcopy(dict(payload)) + sealed["manifest_digest"] = manifest_digest(sealed) + validate_manifest(sealed) + return sealed + + +def _register_id(identifier: Any, path: str, ids: dict[str, str]) -> str: + text = _string(identifier, path) + assert text is not None + if text in ids: + _fail(path, f"duplicates {ids[text]}") + ids[text] = path + return text + + +def _validate_artifact(value: Any, index: int, ids: dict[str, str]) -> str: + path = f"$.artifacts[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={"id", "role", "media_type", "digest", "locator"}, + ) + identifier = _register_id(item["id"], f"{path}.id", ids) + _string(item["role"], f"{path}.role") + _string(item["media_type"], f"{path}.media_type") + digest = _string(item["digest"], f"{path}.digest") + assert digest is not None + if not _DIGEST_RE.fullmatch(digest): + _fail(f"{path}.digest", "must be lowercase sha256:<64 hex>") + locator = _string(item["locator"], f"{path}.locator") + assert locator is not None + if not locator.startswith(_PORTABLE_LOCATOR_PREFIXES): + _fail( + f"{path}.locator", + "must use artifact:, git:, https://, repo:, or urn: coordinates", + ) + if locator.startswith("repo:"): + relative = locator.removeprefix("repo:") + candidate = PurePosixPath(relative) + if ( + not relative + or "\\" in relative + or candidate.is_absolute() + or ".." in candidate.parts + or "." in candidate.parts + ): + _fail(f"{path}.locator", "repo: coordinates must be normalized repository-relative paths") + return identifier + + +def _validate_substrate(value: Any, path: str) -> None: + item = _mapping(value, path) + _exact_keys(item, path, required={"kind", "name", "version", "coordinate"}) + for field in ("kind", "name", "version", "coordinate"): + _string(item[field], f"{path}.{field}") + + +def _validate_mapping(value: Any, path: str) -> None: + item = _mapping(value, path) + _exact_keys( + item, + path, + required={"status", "vstd_verdict", "mapping_profile", "receipt_artifact_id", "reason"}, + ) + state = _enum(item["status"], MAPPING_STATES, f"{path}.status") + verdict = _string(item["vstd_verdict"], f"{path}.vstd_verdict", nullable=True) + mapping_profile = _string(item["mapping_profile"], f"{path}.mapping_profile", nullable=True) + receipt_id = _string(item["receipt_artifact_id"], f"{path}.receipt_artifact_id", nullable=True) + _string(item["reason"], f"{path}.reason") + if state == "NOT_EVALUATED": + if any(value is not None for value in (verdict, mapping_profile, receipt_id)): + _fail(path, "NOT_EVALUATED cannot carry a VSTD verdict, profile, or receipt") + else: + if verdict not in VSTD_VERDICTS: + _fail(f"{path}.vstd_verdict", "MAPPED requires an explicit VSTD verdict") + if mapping_profile is None or receipt_id is None: + _fail(path, "MAPPED requires a mapping profile and receipt artifact") + + +def _validate_action_graph(actions: Mapping[str, list[str]]) -> None: + visiting: set[str] = set() + visited: set[str] = set() + + def visit(identifier: str) -> None: + if identifier in visiting: + _fail("$.actions", f"dependency cycle includes {identifier!r}") + if identifier in visited: + return + visiting.add(identifier) + for dependency in actions[identifier]: + if dependency not in actions: + _fail("$.actions", f"{identifier!r} depends on unknown action {dependency!r}") + visit(dependency) + visiting.remove(identifier) + visited.add(identifier) + + for identifier in actions: + visit(identifier) + + +def _validate_references(references: list[tuple[str, str]], ids: Mapping[str, str]) -> None: + for path, target in references: + if target not in ids: + _fail(path, f"references unknown id {target!r}") + + +def validate_manifest(payload: Mapping[str, Any], *, verify_digest: bool = True) -> None: + """Validate syntax, references, bounds, and non-upgrade invariants. + + Validation says that the workflow record is internally well-formed. It does not + verify any referenced artifact, native result, hypothesis, or VSTD receipt. + """ + + root = _mapping(payload, "$") + _reject_floats(root) + _exact_keys( + root, + "$", + required={ + "profile", + "experiment", + "hypotheses", + "preregistration", + "artifacts", + "budgets", + "actions", + "observations", + "interventions", + "native_results", + "adaptations", + "amendments", + "challenges", + "horizons", + "publication", + "workflow_events", + "manifest_digest", + }, + ) + + profile = _mapping(root["profile"], "$.profile") + _exact_keys(profile, "$.profile", required={"id", "version", "status"}) + if profile["id"] != PROFILE_ID or profile["version"] != PROFILE_VERSION: + _fail("$.profile", "unsupported profile identifier or version") + if profile["status"] != PROFILE_STATUS: + _fail("$.profile.status", f"must be {PROFILE_STATUS}") + + ids: dict[str, str] = {} + references: list[tuple[str, str]] = [] + + experiment = _mapping(root["experiment"], "$.experiment") + _exact_keys( + experiment, + "$.experiment", + required={"id", "title", "question", "state", "started_at"}, + ) + _register_id(experiment["id"], "$.experiment.id", ids) + _string(experiment["title"], "$.experiment.title") + _string(experiment["question"], "$.experiment.question") + _enum(experiment["state"], EXPERIMENT_STATES, "$.experiment.state") + _string(experiment["started_at"], "$.experiment.started_at", nullable=True) + + hypotheses = _sequence(root["hypotheses"], "$.hypotheses") + if not hypotheses: + _fail("$.hypotheses", "must declare at least one falsifiable hypothesis") + for index, value in enumerate(hypotheses): + path = f"$.hypotheses[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={"id", "statement", "falsification_condition", "state"}, + ) + _register_id(item["id"], f"{path}.id", ids) + _string(item["statement"], f"{path}.statement") + _string(item["falsification_condition"], f"{path}.falsification_condition") + _enum(item["state"], HYPOTHESIS_STATES, f"{path}.state") + + preregistration = _mapping(root["preregistration"], "$.preregistration") + _exact_keys( + preregistration, + "$.preregistration", + required={"state", "recorded_at", "artifact_id", "limitations"}, + ) + preregistration_state = _enum( + preregistration["state"], PREREGISTRATION_STATES, "$.preregistration.state" + ) + _string(preregistration["recorded_at"], "$.preregistration.recorded_at", nullable=True) + preregistration_artifact = _string( + preregistration["artifact_id"], "$.preregistration.artifact_id", nullable=True + ) + _string_list(preregistration["limitations"], "$.preregistration.limitations") + if preregistration_state in {"FROZEN", "AMENDED"} and preregistration_artifact is None: + _fail("$.preregistration", "FROZEN or AMENDED requires a bound artifact") + if preregistration_artifact is not None: + references.append(("$.preregistration.artifact_id", preregistration_artifact)) + + artifact_ids = { + _validate_artifact(value, index, ids) + for index, value in enumerate(_sequence(root["artifacts"], "$.artifacts")) + } + + budget_ids: set[str] = set() + for index, value in enumerate(_sequence(root["budgets"], "$.budgets")): + path = f"$.budgets[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={"id", "resource", "limit", "consumed", "unit", "scope"}, + ) + identifier = _register_id(item["id"], f"{path}.id", ids) + budget_ids.add(identifier) + _string(item["resource"], f"{path}.resource") + limit = _nonnegative_integer(item["limit"], f"{path}.limit") + consumed = _nonnegative_integer(item["consumed"], f"{path}.consumed") + if consumed > limit: + _fail(path, "consumed work exceeds the declared limit") + _string(item["unit"], f"{path}.unit") + _string(item["scope"], f"{path}.scope") + + action_dependencies: dict[str, list[str]] = {} + action_ids: set[str] = set() + for index, value in enumerate(_sequence(root["actions"], "$.actions")): + path = f"$.actions[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={ + "id", + "kind", + "target", + "state", + "priority", + "selected_because", + "selection_evidence_ids", + "alternatives_considered", + "budget_ids", + "depends_on", + "triggered_by", + "expected_artifact_effect", + "substrate", + "native_result_ids", + "produced_artifact_ids", + }, + ) + identifier = _register_id(item["id"], f"{path}.id", ids) + action_ids.add(identifier) + _string(item["kind"], f"{path}.kind") + _string(item["target"], f"{path}.target") + _enum(item["state"], ACTION_STATES, f"{path}.state") + priority = _nonnegative_integer(item["priority"], f"{path}.priority") + if priority == 0: + _fail(f"{path}.priority", "must be at least 1") + _string(item["selected_because"], f"{path}.selected_because") + _string(item["expected_artifact_effect"], f"{path}.expected_artifact_effect") + _validate_substrate(item["substrate"], f"{path}.substrate") + selection_evidence = _string_list( + item["selection_evidence_ids"], f"{path}.selection_evidence_ids" + ) + _string_list(item["alternatives_considered"], f"{path}.alternatives_considered") + action_budgets = _string_list(item["budget_ids"], f"{path}.budget_ids") + if not action_budgets: + _fail(f"{path}.budget_ids", "every selected action must bind at least one budget") + unknown_budgets = sorted(set(action_budgets) - budget_ids) + if unknown_budgets: + _fail(f"{path}.budget_ids", f"unknown budgets: {', '.join(unknown_budgets)}") + dependencies = _string_list(item["depends_on"], f"{path}.depends_on") + action_dependencies[identifier] = dependencies + for field in ("triggered_by", "native_result_ids", "produced_artifact_ids"): + values = _string_list(item[field], f"{path}.{field}") + references.extend((f"{path}.{field}", target) for target in values) + references.extend( + (f"{path}.selection_evidence_ids", target) for target in selection_evidence + ) + + observation_ids: set[str] = set() + for index, value in enumerate(_sequence(root["observations"], "$.observations")): + path = f"$.observations[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={ + "id", + "action_id", + "recorded_at", + "statement", + "status", + "evidence_artifact_ids", + "limitations", + }, + ) + identifier = _register_id(item["id"], f"{path}.id", ids) + observation_ids.add(identifier) + action_id = _string(item["action_id"], f"{path}.action_id") + assert action_id is not None + references.append((f"{path}.action_id", action_id)) + _string(item["recorded_at"], f"{path}.recorded_at") + _string(item["statement"], f"{path}.statement") + _enum(item["status"], OBSERVATION_STATES, f"{path}.status") + evidence_ids = _string_list( + item["evidence_artifact_ids"], f"{path}.evidence_artifact_ids" + ) + references.extend((f"{path}.evidence_artifact_ids", target) for target in evidence_ids) + _string_list(item["limitations"], f"{path}.limitations") + + for index, value in enumerate(_sequence(root["interventions"], "$.interventions")): + path = f"$.interventions[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={ + "id", + "action_id", + "description", + "applied_at", + "target_artifact_ids", + "produced_artifact_ids", + }, + ) + _register_id(item["id"], f"{path}.id", ids) + action_id = _string(item["action_id"], f"{path}.action_id") + assert action_id is not None + references.append((f"{path}.action_id", action_id)) + _string(item["description"], f"{path}.description") + _string(item["applied_at"], f"{path}.applied_at") + for field in ("target_artifact_ids", "produced_artifact_ids"): + values = _string_list(item[field], f"{path}.{field}") + references.extend((f"{path}.{field}", target) for target in values) + + native_result_ids: set[str] = set() + for index, value in enumerate(_sequence(root["native_results"], "$.native_results")): + path = f"$.native_results[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={"id", "action_id", "verifier", "native_status", "result_artifact_id", "mapping"}, + ) + identifier = _register_id(item["id"], f"{path}.id", ids) + native_result_ids.add(identifier) + action_id = _string(item["action_id"], f"{path}.action_id") + assert action_id is not None + references.append((f"{path}.action_id", action_id)) + _validate_substrate(item["verifier"], f"{path}.verifier") + _string(item["native_status"], f"{path}.native_status") + result_artifact = _string( + item["result_artifact_id"], f"{path}.result_artifact_id", nullable=True + ) + if result_artifact is not None: + references.append((f"{path}.result_artifact_id", result_artifact)) + _validate_mapping(item["mapping"], f"{path}.mapping") + mapped_receipt = item["mapping"]["receipt_artifact_id"] + if mapped_receipt is not None: + references.append((f"{path}.mapping.receipt_artifact_id", mapped_receipt)) + + for index, value in enumerate(_sequence(root["adaptations"], "$.adaptations")): + path = f"$.adaptations[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={"id", "trigger_ids", "decision", "reason", "action_ids", "artifact_ids"}, + ) + _register_id(item["id"], f"{path}.id", ids) + _string(item["decision"], f"{path}.decision") + _string(item["reason"], f"{path}.reason") + for field in ("trigger_ids", "action_ids", "artifact_ids"): + values = _string_list(item[field], f"{path}.{field}") + references.extend((f"{path}.{field}", target) for target in values) + + for collection_name, required, enum_field, allowed in ( + ( + "amendments", + {"id", "recorded_at", "reason", "supersedes", "artifact_id"}, + None, + None, + ), + ( + "challenges", + {"id", "target_id", "state", "statement", "evidence_artifact_ids"}, + "state", + CHALLENGE_STATES, + ), + ( + "horizons", + {"id", "status", "description", "reason"}, + "status", + HORIZON_STATES, + ), + ): + for index, value in enumerate(_sequence(root[collection_name], f"$.{collection_name}")): + path = f"$.{collection_name}[{index}]" + item = _mapping(value, path) + _exact_keys(item, path, required=required) + _register_id(item["id"], f"{path}.id", ids) + if enum_field is not None and allowed is not None: + _enum(item[enum_field], allowed, f"{path}.{enum_field}") + if collection_name == "amendments": + _string(item["recorded_at"], f"{path}.recorded_at") + _string(item["reason"], f"{path}.reason") + supersedes = _string_list(item["supersedes"], f"{path}.supersedes") + references.extend((f"{path}.supersedes", target) for target in supersedes) + artifact_id = _string(item["artifact_id"], f"{path}.artifact_id") + assert artifact_id is not None + references.append((f"{path}.artifact_id", artifact_id)) + elif collection_name == "challenges": + target_id = _string(item["target_id"], f"{path}.target_id") + assert target_id is not None + references.append((f"{path}.target_id", target_id)) + _string(item["statement"], f"{path}.statement") + evidence_ids = _string_list( + item["evidence_artifact_ids"], f"{path}.evidence_artifact_ids" + ) + references.extend( + (f"{path}.evidence_artifact_ids", target) for target in evidence_ids + ) + else: + _string(item["description"], f"{path}.description") + _string(item["reason"], f"{path}.reason") + + publication = _mapping(root["publication"], "$.publication") + _exact_keys(publication, "$.publication", required={"state", "artifact_ids"}) + _enum(publication["state"], PUBLICATION_STATES, "$.publication.state") + publication_artifacts = _string_list(publication["artifact_ids"], "$.publication.artifact_ids") + references.extend(("$.publication.artifact_ids", target) for target in publication_artifacts) + + event_ids: set[str] = set() + for index, value in enumerate(_sequence(root["workflow_events"], "$.workflow_events")): + path = f"$.workflow_events[{index}]" + item = _mapping(value, path) + _exact_keys( + item, + path, + required={ + "id", + "kind", + "recorded_at", + "source", + "native_state", + "verification_effect", + "details", + }, + ) + identifier = _register_id(item["id"], f"{path}.id", ids) + event_ids.add(identifier) + _enum(item["kind"], PLATFORM_EVENT_KINDS, f"{path}.kind") + _string(item["recorded_at"], f"{path}.recorded_at") + _string(item["native_state"], f"{path}.native_state") + if item["verification_effect"] != "NONE": + _fail(f"{path}.verification_effect", "platform events cannot grant a verification verdict") + source = _mapping(item["source"], f"{path}.source") + _exact_keys(source, f"{path}.source", required={"platform", "repository", "coordinate"}) + for field in ("platform", "repository", "coordinate"): + _string(source[field], f"{path}.source.{field}") + details = _mapping(item["details"], f"{path}.details") + canonical_bytes(details) + + _validate_action_graph(action_dependencies) + _validate_references(references, ids) + + for index, action in enumerate(root["actions"]): + unknown_results = sorted(set(action["native_result_ids"]) - native_result_ids) + if unknown_results: + _fail( + f"$.actions[{index}].native_result_ids", + f"not native results: {', '.join(unknown_results)}", + ) + unknown_artifacts = sorted(set(action["produced_artifact_ids"]) - artifact_ids) + if unknown_artifacts: + _fail( + f"$.actions[{index}].produced_artifact_ids", + f"not artifacts: {', '.join(unknown_artifacts)}", + ) + for index, result in enumerate(root["native_results"]): + if result["action_id"] not in action_ids: + _fail(f"$.native_results[{index}].action_id", "must reference an action") + for index, observation in enumerate(root["observations"]): + if observation["action_id"] not in action_ids: + _fail(f"$.observations[{index}].action_id", "must reference an action") + for index, intervention in enumerate(root["interventions"]): + if intervention["action_id"] not in action_ids: + _fail(f"$.interventions[{index}].action_id", "must reference an action") + + digest = _string(root["manifest_digest"], "$.manifest_digest") + assert digest is not None + if not _DIGEST_RE.fullmatch(digest): + _fail("$.manifest_digest", "must be lowercase sha256:<64 hex>") + if verify_digest and digest != manifest_digest(root): + _fail("$.manifest_digest", "does not match the canonical stable payload") + + +def load_manifest(path: Path) -> dict[str, Any]: + """Load and validate a UTF-8 JSON workflow manifest.""" + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise WorkflowProfileError(f"cannot load {path.name}: {exc}") from exc + if not isinstance(payload, dict): + raise WorkflowProfileError("workflow manifest root must be an object") + validate_manifest(payload) + return payload + + +def verify_repo_artifacts(payload: Mapping[str, Any], repository_root: Path) -> None: + """Verify every repository-relative artifact against its bound SHA-256 digest. + + Other locator schemes require their own retriever and trust policy. Skipping those + schemes here does not verify them and does not change any recorded result. + """ + + validate_manifest(payload) + root = repository_root.resolve() + for index, artifact in enumerate(payload["artifacts"]): + locator = artifact["locator"] + if not locator.startswith("repo:"): + continue + relative = PurePosixPath(locator.removeprefix("repo:")) + candidate = root.joinpath(*relative.parts).resolve() + try: + candidate.relative_to(root) + except ValueError as exc: + _fail(f"$.artifacts[{index}].locator", "resolves outside the repository") + raise AssertionError("unreachable") from exc + if not candidate.is_file(): + _fail(f"$.artifacts[{index}].locator", "bound repository artifact is missing") + actual = "sha256:" + hashlib.sha256(candidate.read_bytes()).hexdigest() + if actual != artifact["digest"]: + _fail( + f"$.artifacts[{index}].digest", + f"does not match {locator}; expected {artifact['digest']}, observed {actual}", + ) diff --git a/src/verifier/experimental_workflow/schema.py b/src/verifier/experimental_workflow/schema.py new file mode 100644 index 0000000..11dd620 --- /dev/null +++ b/src/verifier/experimental_workflow/schema.py @@ -0,0 +1,276 @@ +"""Terminology: identifier (ID); JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +JSON Schema generator for the experimental workflow interchange profile.""" + +from __future__ import annotations + +from typing import Any + +from .profile import PROFILE_ID, PROFILE_STATUS, PROFILE_VERSION + + +def _object(properties: dict[str, Any], required: tuple[str, ...] | None = None) -> dict[str, Any]: + return { + "type": "object", + "additionalProperties": False, + "properties": properties, + "required": list(required or properties), + } + + +def _array(items: dict[str, Any], *, minimum: int = 0) -> dict[str, Any]: + schema: dict[str, Any] = {"type": "array", "items": items} + if minimum: + schema["minItems"] = minimum + return schema + + +def workflow_manifest_schema() -> dict[str, Any]: + """Return the complete draft-2020-12 interchange schema.""" + + nonempty = {"type": "string", "minLength": 1} + nullable_nonempty = {"type": ["string", "null"], "minLength": 1} + identifier = {"type": "string", "minLength": 1, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"} + identifier_list = _array(identifier) + digest = {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} + substrate = _object( + { + "kind": nonempty, + "name": nonempty, + "version": nonempty, + "coordinate": nonempty, + } + ) + artifact = _object( + { + "id": identifier, + "role": nonempty, + "media_type": nonempty, + "digest": digest, + "locator": { + "type": "string", + "pattern": "^(artifact:|git:|https://|repo:|urn:).+", + }, + } + ) + mapping = _object( + { + "status": {"enum": ["NOT_EVALUATED", "MAPPED"]}, + "vstd_verdict": { + "type": ["string", "null"], + "enum": ["PASS", "FAIL", "UNKNOWN", "CONFLICTED", "REJECTED", None], + }, + "mapping_profile": nullable_nonempty, + "receipt_artifact_id": nullable_nonempty, + "reason": nonempty, + } + ) + platform_event = _object( + { + "id": identifier, + "kind": { + "enum": [ + "PLATFORM_ISSUE", + "PLATFORM_COMMIT", + "PLATFORM_WORKFLOW_RUN", + "PLATFORM_ARTIFACT", + "PLATFORM_PULL_REQUEST", + ] + }, + "recorded_at": nonempty, + "source": _object( + {"platform": nonempty, "repository": nonempty, "coordinate": nonempty} + ), + "native_state": nonempty, + "verification_effect": {"const": "NONE"}, + "details": {"type": "object"}, + } + ) + + return { + "$comment": "Terminology: Verifier Standard (VSTD).", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://timelordraps.github.io/verifier/profiles/experimental-workflow.schema.json", + "title": "VSTD experimental workflow profile 0.1", + "description": ( + "Non-normative, verdict-neutral interchange for bounded experimental work. " + "Schema validity does not verify referenced evidence or native results." + ), + **_object( + { + "profile": _object( + { + "id": {"const": PROFILE_ID}, + "version": {"const": PROFILE_VERSION}, + "status": {"const": PROFILE_STATUS}, + } + ), + "experiment": _object( + { + "id": identifier, + "title": nonempty, + "question": nonempty, + "state": { + "enum": [ + "DRAFT", + "PREREGISTERED", + "RUNNING", + "BLOCKED", + "COMPLETED", + "ABANDONED", + ] + }, + "started_at": nullable_nonempty, + } + ), + "hypotheses": _array( + _object( + { + "id": identifier, + "statement": nonempty, + "falsification_condition": nonempty, + "state": { + "enum": ["OPEN", "SUPPORTED", "REFUTED", "UNKNOWN", "CONFLICTED"] + }, + } + ), + minimum=1, + ), + "preregistration": _object( + { + "state": {"enum": ["NONE", "DRAFT", "FROZEN", "AMENDED"]}, + "recorded_at": nullable_nonempty, + "artifact_id": nullable_nonempty, + "limitations": _array(nonempty), + } + ), + "artifacts": _array(artifact), + "budgets": _array( + _object( + { + "id": identifier, + "resource": nonempty, + "limit": {"type": "integer", "minimum": 0}, + "consumed": {"type": "integer", "minimum": 0}, + "unit": nonempty, + "scope": nonempty, + } + ) + ), + "actions": _array( + _object( + { + "id": identifier, + "kind": nonempty, + "target": nonempty, + "state": { + "enum": ["PLANNED", "RUNNING", "BLOCKED", "COMPLETED", "ABANDONED"] + }, + "priority": {"type": "integer", "minimum": 1}, + "selected_because": nonempty, + "selection_evidence_ids": identifier_list, + "alternatives_considered": _array(nonempty), + "budget_ids": _array(identifier, minimum=1), + "depends_on": identifier_list, + "triggered_by": identifier_list, + "expected_artifact_effect": nonempty, + "substrate": substrate, + "native_result_ids": identifier_list, + "produced_artifact_ids": identifier_list, + } + ) + ), + "observations": _array( + _object( + { + "id": identifier, + "action_id": identifier, + "recorded_at": nonempty, + "statement": nonempty, + "status": {"enum": ["OBSERVED", "UNKNOWN", "CONFLICTED"]}, + "evidence_artifact_ids": identifier_list, + "limitations": _array(nonempty), + } + ) + ), + "interventions": _array( + _object( + { + "id": identifier, + "action_id": identifier, + "description": nonempty, + "applied_at": nonempty, + "target_artifact_ids": identifier_list, + "produced_artifact_ids": identifier_list, + } + ) + ), + "native_results": _array( + _object( + { + "id": identifier, + "action_id": identifier, + "verifier": substrate, + "native_status": nonempty, + "result_artifact_id": nullable_nonempty, + "mapping": mapping, + } + ) + ), + "adaptations": _array( + _object( + { + "id": identifier, + "trigger_ids": identifier_list, + "decision": nonempty, + "reason": nonempty, + "action_ids": identifier_list, + "artifact_ids": identifier_list, + } + ) + ), + "amendments": _array( + _object( + { + "id": identifier, + "recorded_at": nonempty, + "reason": nonempty, + "supersedes": identifier_list, + "artifact_id": identifier, + } + ) + ), + "challenges": _array( + _object( + { + "id": identifier, + "target_id": identifier, + "state": {"enum": ["OPEN", "RESOLVED", "REJECTED"]}, + "statement": nonempty, + "evidence_artifact_ids": identifier_list, + } + ) + ), + "horizons": _array( + _object( + { + "id": identifier, + "status": {"enum": ["UNKNOWN", "CONFLICTED", "BLOCKED", "OUT_OF_SCOPE"]}, + "description": nonempty, + "reason": nonempty, + } + ) + ), + "publication": _object( + { + "state": { + "enum": ["PRIVATE", "INTERNAL", "CANDIDATE", "PUBLISHED", "RETRACTED"] + }, + "artifact_ids": identifier_list, + } + ), + "workflow_events": _array(platform_event), + "manifest_digest": digest, + } + ), + } diff --git a/src/verifier/runtime/experimental_workflow_cli.py b/src/verifier/runtime/experimental_workflow_cli.py new file mode 100644 index 0000000..b5e3f2c --- /dev/null +++ b/src/verifier/runtime/experimental_workflow_cli.py @@ -0,0 +1,141 @@ +"""Terminology: command-line interface (CLI); JavaScript Object Notation (JSON); +Verifier Standard (VSTD). + +CLI boundary for the experimental, non-normative workflow profile.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +from verifier.experimental_workflow import ( + GitHubAdapterError, + github_snapshot_to_events, + load_manifest, + verify_repo_artifacts, +) + + +def add_experiment_parsers(subparsers: argparse._SubParsersAction) -> None: + """Add verdict-neutral experimental-workflow commands to the public parser.""" + + parser = subparsers.add_parser( + "experiment", + help="Validate or adapt experimental, non-normative workflow records.", + ) + commands = parser.add_subparsers(dest="experiment_command", required=True) + + validate_parser = commands.add_parser( + "validate", + help="Validate a profile manifest without granting a VSTD verdict.", + ) + validate_parser.add_argument("manifest", help="Experimental workflow manifest JSON.") + validate_parser.add_argument( + "--repo-root", + help="Repository root used to verify every repo: artifact locator.", + ) + validate_parser.add_argument("--json", action="store_true") + + github_parser = commands.add_parser( + "github-events", + help="Map a strict normalized GitHub snapshot to verdict-neutral events.", + ) + github_parser.add_argument("snapshot", help="Normalized GitHub snapshot JSON.") + github_parser.add_argument("--json", action="store_true") + + +def _repository_artifact_count(payload: dict[str, Any]) -> int: + artifacts = payload.get("artifacts", []) + if not isinstance(artifacts, list): + return 0 + return sum( + 1 + for artifact in artifacts + if isinstance(artifact, dict) + and isinstance(artifact.get("locator"), str) + and artifact["locator"].startswith("repo:") + ) + + +def _validate(args: argparse.Namespace) -> int: + manifest_path = Path(args.manifest).resolve() + payload = load_manifest(manifest_path) + repo_artifact_count = _repository_artifact_count(payload) + if not repo_artifact_count: + repository_artifacts = "NOT_APPLICABLE" + elif args.repo_root: + verify_repo_artifacts(payload, Path(args.repo_root).resolve()) + repository_artifacts = "VERIFIED" + else: + repository_artifacts = "NOT_CHECKED" + + experiment = payload["experiment"] + profile = payload["profile"] + assert isinstance(experiment, dict) and isinstance(profile, dict) + result = { + "status": ( + "VALID" + if repository_artifacts != "NOT_CHECKED" + else "VALID_WITH_UNCHECKED_REPOSITORY_ARTIFACTS" + ), + "profile": { + "id": profile["id"], + "version": profile["version"], + "status": profile["status"], + }, + "experiment": { + "id": experiment["id"], + "state": experiment["state"], + }, + "manifest_digest": payload["manifest_digest"], + "repository_artifact_count": repo_artifact_count, + "repository_artifacts": repository_artifacts, + "vstd_verdict_granted": False, + "claim_boundary": ( + "Structural validity and bound-byte checks do not establish the hypothesis, " + "native verifier, publication, independence, or a VSTD verdict." + ), + } + if args.json: + print(json.dumps(result, indent=2, sort_keys=True)) + else: + print(f"[{result['status']}] experimental workflow {experiment['id']}") + print(f" Manifest digest: {payload['manifest_digest']}") + print(f" Repository artifacts: {repository_artifacts}") + print(" VSTD verdict granted: no") + print(f" Boundary: {result['claim_boundary']}") + return 2 if repository_artifacts == "NOT_CHECKED" else 0 + + +def _github_events(args: argparse.Namespace) -> int: + snapshot_path = Path(args.snapshot).resolve() + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + if not isinstance(snapshot, dict): + raise GitHubAdapterError("normalized GitHub snapshot must be a JSON object") + events = github_snapshot_to_events(snapshot) + result = { + "adapter": "github-normalized-0.1", + "events": list(events), + "event_count": len(events), + "verification_effects": sorted({event["verification_effect"] for event in events}), + "vstd_verdicts_granted": 0, + } + if args.json: + print(json.dumps(result, indent=2, sort_keys=True)) + else: + print(f"[ADAPTED] {len(events)} normalized GitHub events") + print(" Verification effects: NONE") + print(" VSTD verdicts granted: 0") + return 0 + + +def handle_experiment_command(args: argparse.Namespace) -> int: + """Dispatch one experimental-workflow command without widening its result.""" + + if args.experiment_command == "validate": + return _validate(args) + if args.experiment_command == "github-events": + return _github_events(args) + return 1 diff --git a/tests/test_experimental_workflow_cli.py b/tests/test_experimental_workflow_cli.py new file mode 100644 index 0000000..c4184bc --- /dev/null +++ b/tests/test_experimental_workflow_cli.py @@ -0,0 +1,67 @@ +"""Terminology: command-line interface (CLI); Verifier Standard (VSTD). + +CLI tests for the verdict-neutral experimental-workflow surface.""" + +from __future__ import annotations + +import json +import hashlib +from pathlib import Path + +from verifier.experimental_workflow import seal_manifest +from verifier.runtime.public_cli import main + + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "experiments" / "github_verdict_neutrality" / "experiment.json" +SNAPSHOT = ROOT / "examples" / "experimental_workflow" / "github_snapshot.json" + + +def test_experiment_validate_reports_exact_non_verdict_scope(capsys) -> None: + assert main(["experiment", "validate", str(MANIFEST), "--json"]) == 0 + result = json.loads(capsys.readouterr().out) + assert result["status"] == "VALID" + assert result["repository_artifacts"] == "NOT_APPLICABLE" + assert result["vstd_verdict_granted"] is False + assert result["experiment"]["id"] == "experiment-github-verdict-neutrality" + + +def test_experiment_validate_rejects_tampered_digest(tmp_path: Path, capsys) -> None: + payload = json.loads(MANIFEST.read_text(encoding="utf-8")) + payload["experiment"]["question"] = "Substituted question" + path = tmp_path / "tampered.json" + path.write_text(json.dumps(payload), encoding="utf-8") + assert main(["experiment", "validate", str(path), "--json"]) == 1 + assert "manifest_digest" in capsys.readouterr().err + + +def test_experiment_validate_does_not_skip_repository_artifacts( + tmp_path: Path, capsys +) -> None: + payload = json.loads(MANIFEST.read_text(encoding="utf-8")) + payload.pop("manifest_digest") + payload["artifacts"].append( + { + "id": "artifact-repository-evidence", + "role": "repository-evidence", + "media_type": "text/plain", + "digest": "sha256:" + hashlib.sha256(b"evidence").hexdigest(), + "locator": "repo:evidence.txt", + } + ) + path = tmp_path / "unchecked.json" + path.write_text(json.dumps(seal_manifest(payload)), encoding="utf-8") + + assert main(["experiment", "validate", str(path), "--json"]) == 2 + result = json.loads(capsys.readouterr().out) + assert result["status"] == "VALID_WITH_UNCHECKED_REPOSITORY_ARTIFACTS" + assert result["repository_artifacts"] == "NOT_CHECKED" + assert result["vstd_verdict_granted"] is False + + +def test_experiment_github_events_remain_verdict_neutral(capsys) -> None: + assert main(["experiment", "github-events", str(SNAPSHOT), "--json"]) == 0 + result = json.loads(capsys.readouterr().out) + assert result["event_count"] == 5 + assert result["verification_effects"] == ["NONE"] + assert result["vstd_verdicts_granted"] == 0 diff --git a/tests/test_experimental_workflow_profile.py b/tests/test_experimental_workflow_profile.py new file mode 100644 index 0000000..e55ecb8 --- /dev/null +++ b/tests/test_experimental_workflow_profile.py @@ -0,0 +1,328 @@ +"""Terminology: line feed (LF); zero-identity/zero-knowledge (ZIZK). + +Adversarial tests for the non-normative experimental-workflow profile.""" + +from __future__ import annotations + +import copy +import hashlib +import importlib.util +import json +from pathlib import Path + +import jsonschema +import pytest + +from verifier.experimental_workflow import ( + GitHubAdapterError, + WorkflowProfileError, + github_snapshot_to_events, + load_manifest, + seal_manifest, + validate_manifest, + verify_repo_artifacts, + workflow_manifest_schema, +) + + +ROOT = Path(__file__).resolve().parents[1] +EXAMPLE = ROOT / "examples" / "experimental_workflow" +EXPERIMENT_MANIFEST = ( + ROOT / "experiments" / "github_verdict_neutrality" / "experiment.json" +) +ARTIFACT_FIRST_MECHANISMS_MANIFEST = ( + ROOT / "experiments" / "artifact_first_mechanisms" / "experiment.json" +) + + +def _example_payload() -> dict[str, object]: + return json.loads(EXPERIMENT_MANIFEST.read_text(encoding="utf-8")) + + +def _github_snapshot() -> dict[str, object]: + return json.loads((EXAMPLE / "github_snapshot.json").read_text(encoding="utf-8")) + + +def _add_mapped_result(payload: dict[str, object], verdict: str) -> None: + artifacts = payload["artifacts"] + actions = payload["actions"] + native_results = payload["native_results"] + assert isinstance(artifacts, list) + assert isinstance(actions, list) and isinstance(actions[0], dict) + assert isinstance(native_results, list) + artifacts.append( + { + "id": "artifact-vstd-receipt", + "role": "mapped-vstd-receipt", + "media_type": "application/json", + "digest": "sha256:" + "3" * 64, + "locator": "artifact:vstd-receipt", + } + ) + native_results.append( + { + "id": "result-mapped", + "action_id": actions[0]["id"], + "verifier": { + "kind": "domain-verifier", + "name": "bounded-example", + "version": "1", + "coordinate": "urn:example:bounded-verifier", + }, + "native_status": "INDETERMINATE", + "result_artifact_id": None, + "mapping": { + "status": "MAPPED", + "vstd_verdict": verdict, + "mapping_profile": "urn:example:vstd-mapping:1", + "receipt_artifact_id": "artifact-vstd-receipt", + "reason": "A separate receipt records the bounded mapping.", + }, + } + ) + actions[0]["native_result_ids"] = ["result-mapped"] + + +def test_checked_in_manifests_validate_and_match_schema() -> None: + schema = workflow_manifest_schema() + payload = load_manifest(EXPERIMENT_MANIFEST) + jsonschema.Draft202012Validator(schema).validate(payload) + + +def test_artifact_first_mechanism_manifest_preserves_causal_provenance_boundary() -> None: + payload = load_manifest(ARTIFACT_FIRST_MECHANISMS_MANIFEST) + verify_repo_artifacts(payload, ROOT) + + assert payload["experiment"]["id"] == "experiment-artifact-first-mechanisms" + assert "governing" in payload["experiment"]["title"] + + artifacts = {item["id"]: item for item in payload["artifacts"]} + assert artifacts["artifact-zk-receipt"]["locator"].endswith( + "recorded-proof/receipt.msgpack" + ) + assert artifacts["artifact-zk-public-envelope"]["locator"].endswith( + "recorded-proof/public.json" + ) + assert artifacts["artifact-zk-self-test"]["locator"].endswith( + "recorded-proof/self-test-results.json" + ) + + hypotheses = {item["id"]: item for item in payload["hypotheses"]} + assert hypotheses["hypothesis-artifact-first-zero-actor-trust"]["state"] == "OPEN" + assert hypotheses["hypothesis-contextual-actor-artifact-roles"]["state"] == "OPEN" + assert hypotheses["hypothesis-rust-memetic-backtrace"]["state"] == "OPEN" + assert hypotheses["hypothesis-rot-current-admissibility"]["state"] == "OPEN" + assert hypotheses["hypothesis-dual-causal-propagation"]["state"] == "OPEN" + + adaptation = payload["adaptations"][0] + assert "standard/LADDER.md section 1.1" in adaptation["decision"] + assert "TRUST transfer" in adaptation["decision"] + assert "ROT derivation/propagation" in adaptation["decision"] + assert "RUST backtrace/concentration" in adaptation["decision"] + + horizons = {item["id"]: item["status"] for item in payload["horizons"]} + assert horizons["horizon-contextual-role-protocol"] == "UNKNOWN" + assert horizons["horizon-rot-current-admissibility"] == "UNKNOWN" + assert horizons["horizon-rust-memetic-backtrace"] == "UNKNOWN" + assert horizons["horizon-forward-artifact-trust"] == "UNKNOWN" + + +def test_manifest_bound_text_artifacts_use_repository_lf_bytes() -> None: + payload = load_manifest(ARTIFACT_FIRST_MECHANISMS_MANIFEST) + for artifact in payload["artifacts"]: + if artifact["media_type"] != "text/markdown": + continue + locator = artifact["locator"] + assert locator.startswith("repo:") + data = (ROOT / locator.removeprefix("repo:")).read_bytes() + assert b"\r\n" not in data, f"{locator} must match Git's LF-normalized bytes" + + +def test_checked_in_schema_is_generated_from_one_source() -> None: + checked_in = json.loads( + (ROOT / "docs" / "profiles" / "experimental-workflow.schema.json").read_text( + encoding="utf-8" + ) + ) + assert checked_in == workflow_manifest_schema() + + +def test_manifest_digest_detects_semantic_tampering() -> None: + payload = _example_payload() + experiment = payload["experiment"] + assert isinstance(experiment, dict) + experiment["question"] = "A substituted question" + with pytest.raises(WorkflowProfileError, match="canonical stable payload"): + validate_manifest(payload) + + +def test_seal_manifest_does_not_mutate_caller() -> None: + payload = _example_payload() + payload.pop("manifest_digest") + original = copy.deepcopy(payload) + sealed = seal_manifest(payload) + assert payload == original + assert sealed["manifest_digest"].startswith("sha256:") + + +@pytest.mark.parametrize("value", [-1, 1.5, True]) +def test_budget_rejects_negative_float_and_boolean_limits(value: object) -> None: + payload = _example_payload() + payload.pop("manifest_digest") + budgets = payload["budgets"] + assert isinstance(budgets, list) and isinstance(budgets[0], dict) + budgets[0]["limit"] = value + with pytest.raises(WorkflowProfileError): + seal_manifest(payload) + + +def test_consumed_work_cannot_exceed_bound() -> None: + payload = _example_payload() + payload.pop("manifest_digest") + budgets = payload["budgets"] + assert isinstance(budgets, list) and isinstance(budgets[0], dict) + budgets[0]["consumed"] = budgets[0]["limit"] + 1 + with pytest.raises(WorkflowProfileError, match="exceeds"): + seal_manifest(payload) + + +def test_every_selected_action_requires_a_budget() -> None: + payload = _example_payload() + payload.pop("manifest_digest") + actions = payload["actions"] + assert isinstance(actions, list) and isinstance(actions[0], dict) + actions[0]["budget_ids"] = [] + with pytest.raises(WorkflowProfileError, match="bind at least one budget"): + seal_manifest(payload) + + +def test_action_dependency_cycles_fail_closed() -> None: + payload = _example_payload() + payload.pop("manifest_digest") + actions = payload["actions"] + assert isinstance(actions, list) and isinstance(actions[0], dict) + actions[0]["depends_on"] = [actions[0]["id"]] + with pytest.raises(WorkflowProfileError, match="dependency cycle"): + seal_manifest(payload) + + +@pytest.mark.parametrize( + "locator", + [ + "C" + ":\\private\\result.json", + "/" + "home/person/result.json", + "repo:../private/result.json", + "repo:folder\\result.json", + ], +) +def test_nonportable_or_escaping_artifact_locators_are_rejected(locator: str) -> None: + payload = _example_payload() + payload.pop("manifest_digest") + artifacts = payload["artifacts"] + assert isinstance(artifacts, list) + artifacts.append( + { + "id": "artifact-bad-locator", + "role": "test", + "media_type": "application/json", + "digest": "sha256:" + "4" * 64, + "locator": locator, + } + ) + with pytest.raises(WorkflowProfileError): + seal_manifest(payload) + + +def test_not_evaluated_mapping_cannot_smuggle_a_verdict() -> None: + payload = _example_payload() + payload.pop("manifest_digest") + _add_mapped_result(payload, "PASS") + native_results = payload["native_results"] + assert isinstance(native_results, list) and isinstance(native_results[0], dict) + mapping = native_results[0]["mapping"] + assert isinstance(mapping, dict) + mapping["status"] = "NOT_EVALUATED" + with pytest.raises(WorkflowProfileError, match="cannot carry"): + seal_manifest(payload) + + +@pytest.mark.parametrize("verdict", ["UNKNOWN", "CONFLICTED"]) +def test_uncertain_mapped_verdicts_remain_representable(verdict: str) -> None: + payload = _example_payload() + payload.pop("manifest_digest") + _add_mapped_result(payload, verdict) + sealed = seal_manifest(payload) + result = sealed["native_results"][0] + assert result["mapping"]["vstd_verdict"] == verdict + + +def test_successful_workflow_and_merge_have_no_verification_effect() -> None: + events = github_snapshot_to_events(_github_snapshot()) + assert len(events) == 5 + assert {event["verification_effect"] for event in events} == {"NONE"} + assert any(event["native_state"] == "completed/success" for event in events) + assert any(event["native_state"] == "closed/MERGED" for event in events) + assert all("vstd_verdict" not in event for event in events) + + +def test_platform_event_verification_upgrade_is_rejected() -> None: + payload = _example_payload() + payload.pop("manifest_digest") + events = payload["workflow_events"] + assert isinstance(events, list) and isinstance(events[0], dict) + events[0]["verification_effect"] = "PASS" + with pytest.raises(WorkflowProfileError, match="cannot grant"): + seal_manifest(payload) + + +def test_github_adapter_rejects_unknown_fields_instead_of_guessing() -> None: + snapshot = _github_snapshot() + snapshot["deployment_statuses"] = [] + with pytest.raises(GitHubAdapterError, match="unsupported fields"): + github_snapshot_to_events(snapshot) + + +def test_github_adapter_is_deterministic_and_matches_specimen() -> None: + events = github_snapshot_to_events(_github_snapshot()) + manifest = load_manifest(EXPERIMENT_MANIFEST) + assert list(events) == manifest["workflow_events"] + assert events == github_snapshot_to_events(_github_snapshot()) + + +def test_repo_artifact_binding_detects_substitution(tmp_path: Path) -> None: + artifact = tmp_path / "evidence.txt" + artifact.write_bytes(b"original") + payload = _example_payload() + payload.pop("manifest_digest") + artifacts = payload["artifacts"] + assert isinstance(artifacts, list) + artifacts.append( + { + "id": "artifact-repo-test", + "role": "test-evidence", + "media_type": "text/plain", + "digest": "sha256:" + hashlib.sha256(b"original").hexdigest(), + "locator": "repo:evidence.txt", + } + ) + sealed = seal_manifest(payload) + verify_repo_artifacts(sealed, tmp_path) + artifact.write_bytes(b"substituted") + with pytest.raises(WorkflowProfileError, match="does not match"): + verify_repo_artifacts(sealed, tmp_path) + + +def test_indexed_repository_artifacts_match_manifest() -> None: + payload = load_manifest(EXPERIMENT_MANIFEST) + verify_repo_artifacts(payload, ROOT) + + +def test_experiment_index_is_current() -> None: + spec = importlib.util.spec_from_file_location( + "build_experiment_index", ROOT / "scripts" / "build_experiment_index.py" + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + expected = module.render(module.discover(ROOT)) + assert (ROOT / "experiments" / "INDEX.md").read_text(encoding="utf-8") == expected From 764f33c75a1271db010b57dced7ade3aea887ced Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:47:00 -0700 Subject: [PATCH 06/34] Publish the ZIZK artifact-first reference mechanisms Reason: Make zero-identity and zero-knowledge mechanisms inspectable as artifact-bound references rather than actor-trust shortcuts or a buried side study. Evidence: Executable fixtures, RISC Zero source, exact recorded proof bytes, public commitments, self-test results, threat boundaries, experiment records, and offline-verification tests change together. Coordinate: ZIZK artifact-first architecture; bounded optional proof mechanisms for VSTD 1.2.0. Falsification: A proof artifact is missing or digest-mismatched, identity becomes computational trust, or a backend claim exceeds the predicate actually checked. Compatibility: Additive reference mechanisms; optional proof backends remain bounded and do not redefine core receipts. --- examples/zizk_artifact_first/README.md | 35 + examples/zizk_artifact_first/risc0/.gitignore | 4 + .../risc0/CLAIM_BOUNDARY.md | 55 + examples/zizk_artifact_first/risc0/Cargo.lock | 3674 +++++++++++++++++ examples/zizk_artifact_first/risc0/Cargo.toml | 11 + examples/zizk_artifact_first/risc0/README.md | 209 + .../risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md | 219 + .../zizk_artifact_first/risc0/THREAT_MODEL.md | 96 + .../risc0/fixtures/README.md | 25 + .../zizk_artifact_first/risc0/host/Cargo.toml | 15 + .../risc0/host/src/main.rs | 457 ++ .../risc0/methods/Cargo.toml | 11 + .../risc0/methods/build.rs | 3 + .../risc0/methods/guest/Cargo.lock | 1485 +++++++ .../risc0/methods/guest/Cargo.toml | 11 + .../risc0/methods/guest/src/main.rs | 70 + .../risc0/methods/src/lib.rs | 1 + .../risc0/recorded-proof/public.json | 215 + .../risc0/recorded-proof/receipt.msgpack | Bin 0 -> 301811 bytes .../recorded-proof/self-test-results.json | 12 + .../risc0/rust-toolchain.toml | 4 + .../risc0/scripts/run_real_proof.sh | 14 + .../risc0/scripts/verify_recorded_proof.sh | 16 + .../risc0/types/Cargo.toml | 8 + .../risc0/types/src/lib.rs | 62 + .../zero_identity/README.md | 67 + .../ROUND1_ZERO_IDENTITY_REPORT.md | 313 ++ .../zero_identity/SEMANTIC_MODEL.md | 124 + .../zero_identity/THREAT_MODEL.md | 53 + .../zero_identity/evaluate.py | 546 +++ ...conflicted_authorship_degree_vs_chain.json | 96 + .../conflicted_identity_evidence.json | 92 + .../positive_bounded_authorization.json | 84 + .../positive_minimized_boundary_narrowed.json | 100 + .../rejected_delegation_widens_scope.json | 97 + .../fixtures/rejected_expired_authority.json | 84 + .../fixtures/rejected_key_compromise.json | 85 + ...ected_minimization_erases_key_binding.json | 99 + ...rejected_minimization_widens_boundary.json | 100 + .../fixtures/rejected_missing_challenge.json | 82 + .../rejected_relay_claims_origination.json | 96 + .../fixtures/rejected_replayed_challenge.json | 86 + .../fixtures/rejected_revoked_ancestor.json | 97 + .../fixtures/rejected_revoked_authority.json | 84 + ...ected_unlinkability_erases_trust_root.json | 99 + .../fixtures/unknown_absent_authorship.json | 79 + .../fixtures/unknown_distinct_pseudonyms.json | 89 + .../unknown_missing_authorization.json | 75 + ...unknown_shared_pseudonym_independence.json | 89 + .../unknown_unattested_ancestry_link.json | 95 + .../fixtures/unknown_unattested_rotation.json | 90 + .../fixtures/unknown_uniqueness_absent.json | 84 + .../model/zero_identity_model.json | 311 ++ .../zero_identity/run_validation.py | 48 + .../zero_identity/tests/test_zero_identity.py | 310 ++ .../artifact_first_mechanisms/README.md | 28 + .../artifact_first_mechanisms/experiment.json | 436 ++ .../reverification/ROUND2_DESIGN_NOTE.md | 638 +++ tests/test_zizk_artifact_first.py | 130 + 59 files changed, 11698 insertions(+) create mode 100644 examples/zizk_artifact_first/README.md create mode 100644 examples/zizk_artifact_first/risc0/.gitignore create mode 100644 examples/zizk_artifact_first/risc0/CLAIM_BOUNDARY.md create mode 100644 examples/zizk_artifact_first/risc0/Cargo.lock create mode 100644 examples/zizk_artifact_first/risc0/Cargo.toml create mode 100644 examples/zizk_artifact_first/risc0/README.md create mode 100644 examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md create mode 100644 examples/zizk_artifact_first/risc0/THREAT_MODEL.md create mode 100644 examples/zizk_artifact_first/risc0/fixtures/README.md create mode 100644 examples/zizk_artifact_first/risc0/host/Cargo.toml create mode 100644 examples/zizk_artifact_first/risc0/host/src/main.rs create mode 100644 examples/zizk_artifact_first/risc0/methods/Cargo.toml create mode 100644 examples/zizk_artifact_first/risc0/methods/build.rs create mode 100644 examples/zizk_artifact_first/risc0/methods/guest/Cargo.lock create mode 100644 examples/zizk_artifact_first/risc0/methods/guest/Cargo.toml create mode 100644 examples/zizk_artifact_first/risc0/methods/guest/src/main.rs create mode 100644 examples/zizk_artifact_first/risc0/methods/src/lib.rs create mode 100644 examples/zizk_artifact_first/risc0/recorded-proof/public.json create mode 100644 examples/zizk_artifact_first/risc0/recorded-proof/receipt.msgpack create mode 100644 examples/zizk_artifact_first/risc0/recorded-proof/self-test-results.json create mode 100644 examples/zizk_artifact_first/risc0/rust-toolchain.toml create mode 100644 examples/zizk_artifact_first/risc0/scripts/run_real_proof.sh create mode 100644 examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh create mode 100644 examples/zizk_artifact_first/risc0/types/Cargo.toml create mode 100644 examples/zizk_artifact_first/risc0/types/src/lib.rs create mode 100644 examples/zizk_artifact_first/zero_identity/README.md create mode 100644 examples/zizk_artifact_first/zero_identity/ROUND1_ZERO_IDENTITY_REPORT.md create mode 100644 examples/zizk_artifact_first/zero_identity/SEMANTIC_MODEL.md create mode 100644 examples/zizk_artifact_first/zero_identity/THREAT_MODEL.md create mode 100644 examples/zizk_artifact_first/zero_identity/evaluate.py create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/conflicted_authorship_degree_vs_chain.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/conflicted_identity_evidence.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/positive_bounded_authorization.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/positive_minimized_boundary_narrowed.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_delegation_widens_scope.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_expired_authority.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_key_compromise.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_minimization_erases_key_binding.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_minimization_widens_boundary.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_missing_challenge.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_relay_claims_origination.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_replayed_challenge.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_revoked_ancestor.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_revoked_authority.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/rejected_unlinkability_erases_trust_root.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/unknown_absent_authorship.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/unknown_distinct_pseudonyms.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/unknown_missing_authorization.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/unknown_shared_pseudonym_independence.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/unknown_unattested_ancestry_link.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/unknown_unattested_rotation.json create mode 100644 examples/zizk_artifact_first/zero_identity/fixtures/unknown_uniqueness_absent.json create mode 100644 examples/zizk_artifact_first/zero_identity/model/zero_identity_model.json create mode 100644 examples/zizk_artifact_first/zero_identity/run_validation.py create mode 100644 examples/zizk_artifact_first/zero_identity/tests/test_zero_identity.py create mode 100644 experiments/artifact_first_mechanisms/README.md create mode 100644 experiments/artifact_first_mechanisms/experiment.json create mode 100644 experiments/artifact_first_mechanisms/reverification/ROUND2_DESIGN_NOTE.md create mode 100644 tests/test_zizk_artifact_first.py diff --git a/examples/zizk_artifact_first/README.md b/examples/zizk_artifact_first/README.md new file mode 100644 index 0000000..f5ce12f --- /dev/null +++ b/examples/zizk_artifact_first/README.md @@ -0,0 +1,35 @@ +# Artifact-first reference surfaces + +> **Acronyms:** reduced instruction set computer (RISC); Verifier Standard (VSTD); +> zero-identity/zero-knowledge (ZIZK). + +VSTD's governing ZIZK artifact-first architecture is normative in +[`standard/LADDER.md` section 1.1](../../standard/LADDER.md#11-artifact-first-causal-provenance-orientation). +It gives actor identity and reputation no assurance weight and evaluates bounded process +claims represented by software and artifacts. Its formal semantic names are TRUST for +mechanism-earned forward support, ROT for typed time-indexed loss of current admissibility, +and RUST for inverse-TRUST diagnostic traversal toward recorded ancestors. They are not +acronyms, actor ratings, serialized receipt values, scalar scores, or references to the Rust programming +language. + +This directory contains bounded reference mechanisms under that architecture. A +mechanism may be optional without making the architecture optional. + +## Bounded identity-disclosure evaluator + +[`zero_identity/`](zero_identity/) is a standard-library reference evaluator that +preserves the identity, authorization, provenance, `UNKNOWN`, and `CONFLICTED` +boundaries exposed by a disclosure record. It earns no identity-derived trust, carries +no serialized receipt identifier, and establishes no VSTD conformance result. + +## RISC Zero hidden-witness mechanism + +[`risc0/`](risc0/) contains the pinned Rust prover/verifier, its claim boundary and +threat model, and the exact tracked public artifacts from one real composite scalable +transparent argument of knowledge proof. The private witness is excluded. Start with +[`risc0/README.md`](risc0/README.md) to verify the recorded receipt offline. + +The proof establishes only execution of its fixed hidden-witness predicate under the +named image identifier and proof-system assumptions. It does not establish external +truth, identity, authorization, independence, complete VSTD trichotomy semantics, or a +general VSTD conformance result. diff --git a/examples/zizk_artifact_first/risc0/.gitignore b/examples/zizk_artifact_first/risc0/.gitignore new file mode 100644 index 0000000..279e18f --- /dev/null +++ b/examples/zizk_artifact_first/risc0/.gitignore @@ -0,0 +1,4 @@ +target/ +local-artifacts/ +private-witness.json +private-*.json diff --git a/examples/zizk_artifact_first/risc0/CLAIM_BOUNDARY.md b/examples/zizk_artifact_first/risc0/CLAIM_BOUNDARY.md new file mode 100644 index 0000000..96c8a61 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/CLAIM_BOUNDARY.md @@ -0,0 +1,55 @@ +# Claim boundary + +> **Acronyms:** identifier (ID); JavaScript Object Notation (JSON); reduced instruction set computer (RISC); +> Verifier Standard (VSTD); zero-identity/zero-knowledge (ZIZK); +> zero-knowledge virtual machine (zkVM). + +## Permitted claim after the recorded real-proof run + +This bounded reference mechanism demonstrates that RISC Zero zkVM 3.0.6 can produce a real, +locally verified zero-knowledge receipt for one fixed bounded predicate, while keeping +the mechanism's private witness out of the serialized public artifact package. + +The concrete verified statement is: + +> The program identified by the expected image ID halted successfully and authenticated +> a journal stating that its private encoded input satisfied the fixed mechanism +> predicate and was bound to the journal's subject, policy, challenge, threshold, and +> salted evidence commitment. + +The zero-knowledge basis is the selected protocol and implementation, not merely the +absence of witness text from JSON. The artifact scan is an additional serialization +check, not a proof of zero knowledge. + +## Prohibited claims + +The reference mechanism does not prove: + +- that the hidden evidence is true, complete, authentic, fresh, or lawfully obtained; +- that its producer is authorized, unique, independent, honest, or non-revoked; +- that the private `Supported` tag was assigned correctly; +- that the subject or policy digest resolves to trustworthy external content; +- freshness beyond possession of the journal's challenge; +- prevention of replay for the same challenge; +- host confidentiality, constant-time behavior, or side-channel resistance; +- security of every RISC Zero component or transitive dependency; +- independent implementation or external adoption; +- VSTD conformance for this mechanism; or +- that VSTD should require zero knowledge for full-disclosure receipts. + +An `Unknown` or `Conflicted` mechanism input is rejected by this particular predicate. +That rejection does not turn uncertainty into falsity, and it never upgrades either +state into a clean result. Other VSTD mechanisms must continue to preserve `UNKNOWN` and +`CONFLICTED` when those are the evidence-supported outcomes. + +## Architecture consequence + +This mechanism implements one bounded proof-carrying privacy path under VSTD's governing +ZIZK artifact-first architecture. Specifically, it places a cryptographic zero-knowledge +enclosure around the architectural rule that no unevidenced proposition is presumed. The +proof binds one exact program, predicate, public commitment set, output, parameter set, +and verifier while withholding its witness; prover identity and reputation add no TRUST, +VSTD's formal name for mechanism-earned artifact support. +It neither creates the architecture nor makes its specific proof system mandatory. No +serialized receipt identifier, schema, canonical digest, lifecycle token, console alias, or +existing receipt interpretation changes. diff --git a/examples/zizk_artifact_first/risc0/Cargo.lock b/examples/zizk_artifact_first/risc0/Cargo.lock new file mode 100644 index 0000000..6490386 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/Cargo.lock @@ -0,0 +1,3674 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-r1cs-std", + "ark-std", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0c292754729c8a190e50414fd1a37093c786c709899f29c9f7daccecfa855e" +dependencies = [ + "ahash", + "ark-crypto-primitives-macros", + "ark-ec", + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest", + "fnv", + "merlin", + "sha2", +] + +[[package]] +name = "ark-crypto-primitives-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-groth16" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f1d0f3a534bb54188b8dcc104307db6c56cdae574ddc3212aec0625740fc7e" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-relations", + "ark-std", + "educe", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-snark" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d368e2848c2d4c129ce7679a7d0d2d612b6a274d3ea6a13bad4445d61b381b88" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bonsai-sdk" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fc4edab3bb401344292b3de527d15663b6bbcba76d98485d96b1bd3061c7987" +dependencies = [ + "duplicate", + "maybe-async", + "reqwest", + "serde", + "thiserror", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "docker-generate" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf673e0848ef09fa4aeeba78e681cf651c0c7d35f76ee38cec8e55bc32fa111" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "duplicate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" +dependencies = [ + "heck", + "proc-macro2", + "proc-macro2-diagnostics", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "include_bytes_aligned" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee796ad498c8d9a1d68e477df8f754ed784ef875de1414ebdaf169f70a6a784" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lazy-regex" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4994ba703f78b083e2f7946dac9251abd83fd43a0365f030e99b69be5b4b9ef9" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97232314824e6dbef1918a871bb93f51070455e3715bf26e19a6d01aa977a0" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.119", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-async" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "no_std_strings" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "version_check", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "risc0-binfmt" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d836c6ad82f4ced7c61d5feedf905a17780312e393aa681d29cc0bbc5131672b" +dependencies = [ + "anyhow", + "borsh", + "bytemuck", + "derive_more", + "elf", + "lazy_static", + "postcard", + "rand 0.9.5", + "risc0-zkp", + "risc0-zkvm-platform", + "ruint", + "semver", + "serde", + "tracing", +] + +[[package]] +name = "risc0-build" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8216cdd9f573808a94769767480b06ad1e74ae60841c9582fdf51b8e29ba53" +dependencies = [ + "anyhow", + "cargo_metadata", + "derive_builder", + "dirs", + "docker-generate", + "hex", + "risc0-binfmt", + "risc0-zkos-v1compat", + "risc0-zkp", + "risc0-zkvm-platform", + "rzup", + "semver", + "serde", + "serde_json", + "stability", + "tempfile", +] + +[[package]] +name = "risc0-circuit-keccak" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c731e12429eb4457e1ddc69c56ee7343a1e10b86e4aa55bc8f4d2b13734abb9" +dependencies = [ + "anyhow", + "bytemuck", + "paste", + "risc0-binfmt", + "risc0-circuit-recursion", + "risc0-core", + "risc0-zkp", + "tracing", +] + +[[package]] +name = "risc0-circuit-recursion" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40dd640122abcc67d4d4e4f055c68cbc3ad2efb8589c65c2b23d354632971b60" +dependencies = [ + "anyhow", + "bytemuck", + "hex", + "metal", + "risc0-core", + "risc0-zkp", + "tracing", +] + +[[package]] +name = "risc0-circuit-rv32im" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb11231aa4b74bcc0c8d16597893fbd7ea6f6a9ebbc35e16bfd06b467c7ee104" +dependencies = [ + "anyhow", + "bit-vec", + "bytemuck", + "derive_more", + "paste", + "risc0-binfmt", + "risc0-core", + "risc0-zkp", + "serde", + "tracing", +] + +[[package]] +name = "risc0-core" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eb2d2b2c6cac0e43cbb2202daacee1a2f24d0dfa03fd08887a11dc6defdcc1" +dependencies = [ + "bytemuck", + "rand_core 0.9.5", +] + +[[package]] +name = "risc0-groth16" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ca702ea7d0162766defe7ed6a79bda4a747ad9e2684000a6edd14df0a6d1f3" +dependencies = [ + "anyhow", + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-groth16", + "ark-serialize", + "bytemuck", + "hex", + "num-bigint", + "num-traits", + "risc0-binfmt", + "risc0-zkp", + "serde", +] + +[[package]] +name = "risc0-zkos-v1compat" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b0b598ba7946354b10ca5c56e382de801e6c7fce9fccad0396ec436bc5072b" +dependencies = [ + "include_bytes_aligned", + "no_std_strings", + "risc0-zkvm-platform", +] + +[[package]] +name = "risc0-zkp" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21c0c921e5e2d44197940d387a45e29c6165e318b5a168fdfdbd50f50ba03678" +dependencies = [ + "anyhow", + "blake2", + "borsh", + "bytemuck", + "cfg-if", + "digest", + "hex", + "hex-literal", + "metal", + "paste", + "rand_core 0.9.5", + "risc0-core", + "risc0-zkvm-platform", + "serde", + "sha2", + "stability", + "tracing", +] + +[[package]] +name = "risc0-zkvm" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4f24ec767f71a1663a4d24cf9d02b6bfee44c64647cae677227817051007a" +dependencies = [ + "anyhow", + "bincode", + "bonsai-sdk", + "borsh", + "bytemuck", + "bytes", + "derive_more", + "hex", + "lazy-regex", + "prost", + "risc0-binfmt", + "risc0-build", + "risc0-circuit-keccak", + "risc0-circuit-recursion", + "risc0-circuit-rv32im", + "risc0-core", + "risc0-groth16", + "risc0-zkos-v1compat", + "risc0-zkp", + "risc0-zkvm-platform", + "rrs-lib", + "rzup", + "semver", + "serde", + "sha2", + "stability", + "tempfile", + "tracing", +] + +[[package]] +name = "risc0-zkvm-platform" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb37a97ff7e8e4ee1b2a1c43ec143b4887759883c343507af9e4787a57914cd" +dependencies = [ + "bytemuck", + "cfg-if", + "getrandom 0.2.17", + "getrandom 0.3.4", + "libm", + "num_enum", + "paste", + "stability", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rrs-lib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4382d3af3a4ebdae7f64ba6edd9114fff92c89808004c4943b393377a25d001" +dependencies = [ + "downcast-rs", + "paste", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "ruint" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" +dependencies = [ + "borsh", + "proptest", + "rand 0.8.5", + "rand 0.9.5", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "rzup" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96909a7ea8fdf7e18da727d7facbc43eea8a4f77635e7ec75a69794dede16fb6" +dependencies = [ + "hex", + "rsa", + "semver", + "serde", + "serde_with", + "sha2", + "strum", + "tempfile", + "thiserror", + "toml", + "yaml-rust2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vstd-zk-host" +version = "0.1.0" +dependencies = [ + "hex", + "rand 0.8.5", + "risc0-zkvm", + "rmp-serde", + "serde", + "serde_json", + "vstd-zk-methods", + "vstd-zk-types", +] + +[[package]] +name = "vstd-zk-methods" +version = "0.1.0" +dependencies = [ + "risc0-build", +] + +[[package]] +name = "vstd-zk-types" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] diff --git a/examples/zizk_artifact_first/risc0/Cargo.toml b/examples/zizk_artifact_first/risc0/Cargo.toml new file mode 100644 index 0000000..8f57aea --- /dev/null +++ b/examples/zizk_artifact_first/risc0/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +resolver = "2" +members = ["host", "methods", "types"] + +# RISC Zero guest builds are prohibitively slow without optimization. +[profile.dev] +opt-level = 3 + +[profile.release] +debug = 1 +lto = true diff --git a/examples/zizk_artifact_first/risc0/README.md b/examples/zizk_artifact_first/risc0/README.md new file mode 100644 index 0000000..8c71df2 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/README.md @@ -0,0 +1,209 @@ +# Proof-carrying reference mechanism for Verifier Standard (VSTD) + +> **Acronyms:** gigabyte (GB); identifier (ID); random-access memory (RAM); reduced instruction set computer (RISC); +> RISC Zero (RISC0); software development kit (SDK); Secure Hash Algorithm 256-bit (SHA-256); +> scalable transparent argument of knowledge (STARK); Windows Subsystem for Linux 2 (WSL2); +> zero-knowledge virtual machine (zkVM). + +**Status:** bounded reference mechanism under VSTD's governing +zero-identity/zero-knowledge (ZIZK) artifact-first architecture; not a numbered VSTD +profile, serialized receipt identifier, conformance result, or compatibility promise. The proof backend is optional; +the artifact-first and zero-actor-trust architecture is not. + +This directory answers one narrow question: can a prover show that a hidden, bounded +evidence payload satisfies a fixed predicate while publishing enough authenticated +coordinates for another party to verify the proof? It is a cryptographic zero-knowledge +enclosure around VSTD's architectural zero-knowledge starting rule: no unevidenced +proposition is presumed, and the proof earns only its exact artifact-bound result. The +enclosure binds the program, predicate, commitments, output, parameters, and verifier +without importing prover identity into TRUST, VSTD's formal name for mechanism-earned +artifact support. It does not make all VSTD receipts +cryptographically zero knowledge. Existing full-disclosure receipts remain valid and +unchanged. + +## Selected system + +The reference mechanism selects exactly one proof system: **RISC Zero zkVM 3.0.6**, using its +local composite STARK receipt. The selection is pinned in every Cargo manifest and in +`Cargo.lock`. + +Reasons for selection: + +- the official SDK describes a `Receipt` as a zero-knowledge proof of execution; +- `Receipt::verify` checks successful execution, the expected image ID, and journal + integrity; +- arbitrary Rust guest code can express the bounded predicate without designing a + new arithmetic circuit; +- the composite STARK path uses transparent setup rather than a mechanism-specific + trusted ceremony; and +- the documented local prover requires at least 16 GB of RAM, which the tested Linux + x86-64 environment satisfies. + +Primary references: + +- [RISC Zero installation](https://dev.risczero.com/api/zkvm/install) +- [RISC Zero real-proof quick start](https://dev.risczero.com/api/zkvm/quickstart) +- [`Receipt` verification contract](https://docs.rs/risc0-zkvm/3.0.6/risc0_zkvm/struct.Receipt.html) +- [`DevModeProver` warning](https://docs.rs/risc0-zkvm/3.0.6/risc0_zkvm/struct.DevModeProver.html) +- [RISC Zero proof-system analysis](https://dev.risczero.com/proof-system-in-detail.pdf) + +The host crate enables `disable-dev-mode`. It also rejects the `Fake` receipt variant +and refuses a truthy `RISC0_DEV_MODE` setting. Development-mode output cannot satisfy +this reference mechanism. + +## Statement, witness, and public output + +The fixed predicate is defined byte-for-byte by `PREDICATE_TEXT` in the shared types +crate. A successful proof establishes that one private input accepted by the pinned +guest program contained: + +- a nonempty evidence byte string no longer than 64 bytes; +- a mechanism-local `Supported` input tag rather than `Unknown` or `Conflicted`; +- a private measurement at least as large as the public threshold; and +- a private 32-byte salt used in the evidence commitment. + +The private witness consists of the evidence bytes, salt, measurement, and candidate +state. The authenticated public journal contains: + +- SHA-256 digests of the historical mechanism profile and exact predicate text; +- subject and policy digests; +- a public challenge; +- the public threshold; +- a salted commitment to the private evidence, length, and measurement; and +- the Boolean result of the fixed predicate. + +The RISC Zero image ID is the program trust coordinate. The verifier supplies or uses +the compiled expected image ID; it does not trust the convenience metadata in +`public.json`. RISC Zero receipt metadata is not cryptographically bound and is not an +acceptance input here. + +Canonical evidence commitment input: + +`UTF8` means Unicode Transformation Format, 8-bit (UTF-8) encoding; `U32_BE` and +`U64_BE` mean unsigned 32-bit and unsigned 64-bit big-endian encoding. + +```text +UTF8("vstd-zk-evidence-commitment-v1\\0") +|| U32_BE(evidence_length) +|| evidence_bytes +|| salt_32_bytes +|| U64_BE(private_measurement) +``` + +The commitment is SHA-256 of those bytes. The journal itself is encoded by the pinned +RISC Zero serde codec and authenticated by the receipt. + +## Platform and pinned setup + +The tested platform is Linux x86-64 under WSL2. The RISC Zero documentation lists +x86-64 Linux as a supported installer target. The selected components are: + +```text +rzup 0.5.0 +cargo-risczero 3.0.6 +r0vm 3.0.6 +RISC Zero Rust guest toolchain 1.97.0-dev +risc0-zkvm 3.0.6 +``` + +Install the official tool manager and then the pinned components: + +```bash +curl --proto '=https' --tlsv1.2 -fsSL https://risczero.com/install -o /tmp/rzup-install.sh +bash /tmp/rzup-install.sh +export PATH="$HOME/.risc0/bin:$HOME/.cargo/bin:$PATH" +rzup install cargo-risczero 3.0.6 +rzup install r0vm 3.0.6 +rzup install rust 1.97.0 +rzup default cargo-risczero 3.0.6 +rzup default r0vm 3.0.6 +rzup default rust 1.97.0 +``` + +No dependency from this Rust workspace is added to the `verifier-standard` Python +distribution. + +## Verify the recorded public proof artifact + +The exact non-secret artifacts from the recorded run are tracked under +[`recorded-proof/`](recorded-proof/): + +| Artifact | Bytes | Secure Hash Algorithm 256-bit (SHA-256) | +|---|---:|---| +| `receipt.msgpack` | 301811 | `5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df` | +| `public.json` | 2575 | `6324c3c5d77ea4df4034f61131059289d5228f190d69e34c59bd7416fa9ac823` | +| `self-test-results.json` | 377 | `e4c1bff21fb6161221276157fa96af6661af8635da35970ba12e462881f2c6fe` | + +The private witness and salt are not tracked and are not required for verification. +After installing the pinned toolchain and obtaining the locked Cargo dependencies, run +this command from this directory: + +```bash +./scripts/verify_recorded_proof.sh +``` + +The script executes this direct verifier command: + +```bash +export PATH="$HOME/.risc0/bin:$HOME/.cargo/bin:$PATH" +export CARGO_TARGET_DIR="${HOME}/.cache/vstd-zk-target" +export RISC0_DEV_MODE=0 +cargo run --locked --release -p vstd-zk-host -- \ + verify recorded-proof/receipt.msgpack recorded-proof/public.json \ + e1e9bf4f68ef60ff9af6b50e144082bc475cc20cab47e8187201153da597dcd8 +``` + +The final argument is the exact RISC Zero guest image identifier recorded by the +public envelope and independently pinned by this repository. It is an explicit +program trust coordinate, not actor identity or reputation. Omitting it verifies +newly produced artifacts against the guest image built by the current checkout; +supplying it permits offline verification of this immutable historical receipt +without silently substituting the current build's image identifier. To require Cargo to +use only an already populated local cache, run +`CARGO_NET_OFFLINE=true ./scripts/verify_recorded_proof.sh`. + +The expected successful output is: + +```text +PASS: real RISC Zero receipt and public statement verified +``` + +## Reproduce the proof and negative tests + +From this directory in the supported Linux environment: + +```bash +export PATH="$HOME/.risc0/bin:$HOME/.cargo/bin:$PATH" +export CARGO_TARGET_DIR="${HOME}/.cache/vstd-zk-target" +export RISC0_DEV_MODE=0 +cargo run --locked --release -p vstd-zk-host -- self-test local-artifacts/self-test +``` + +The self-test produces one real receipt, verifies it, and then exercises the negative +fixtures described in `fixtures/README.md`. Generated receipts and private inputs are +ignored by Git. + +For a separate prove/verify flow: + +```bash +mkdir -p local-artifacts/manual +cargo run --locked --release -p vstd-zk-host -- \ + generate-inputs local-artifacts/private-witness.json local-artifacts/manual/statement.json +cargo run --locked --release -p vstd-zk-host -- \ + prove local-artifacts/private-witness.json local-artifacts/manual/statement.json \ + local-artifacts/manual/receipt.msgpack local-artifacts/manual/public.json +rm local-artifacts/private-witness.json +cargo run --locked --release -p vstd-zk-host -- \ + verify local-artifacts/manual/receipt.msgpack local-artifacts/manual/public.json +``` + +The last command is the verifier path. It needs the receipt, public envelope, pinned +verifier implementation, and expected image ID. It does not need the private witness or +a network service; Cargo itself may need the network until the locked dependencies and +toolchain have been installed or cached. + +## Interpretation + +The reference mechanism provides a concrete cryptographic privacy option for one bounded +predicate. It does not establish that zero knowledge should be mandatory for VSTD. +See `CLAIM_BOUNDARY.md` and `THREAT_MODEL.md` before making any public claim. diff --git a/examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md b/examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md new file mode 100644 index 0000000..06d2cf8 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md @@ -0,0 +1,219 @@ +# Recorded reduced instruction set computer (RISC) Zero proof-mechanism report + +> **Acronyms:** gigabyte (GB); identifier (ID); random-access memory (RAM); reduced instruction set computer (RISC); +> RISC Zero (RISC0); random number generator (RNG); software development kit (SDK); +> Secure Hash Algorithm 256-bit (SHA-256); scalable transparent argument of knowledge (STARK); +> Verifier Standard (VSTD); Windows Subsystem for Linux 2 (WSL2); zero-identity/zero-knowledge (ZIZK); +> zero-knowledge virtual machine (zkVM). + +**Date:** 2026-08-23 +**Status:** completed recorded run; non-secret proof artifacts tracked as a bounded +reference mechanism; no VSTD receipt mapping + +## Repository coordinates + +- Repository: `TimeLordRaps/verifier` +- Immutable base: `598c545be3833d6d81bb7e252ca5837f3bb2a449` +- Branch: `codex/zizk-zero-knowledge` +- Worktree: isolated worktree named `zizk-zk-codex`; its machine-specific absolute + path is intentionally excluded from this public report +- Primary worktree modified: no +- Existing serialized receipt identifiers modified: no +- Push, pull request, merge, tag, release, or publication performed: no + +## Selected proof system + +Exactly one proof system was selected and used: + +| Coordinate | Value | +|---|---| +| SDK and verifier | RISC Zero zkVM `3.0.6` | +| Receipt kind | composite STARK | +| Program trust coordinate | RISC Zero image ID | +| Image ID | `e1e9bf4f68ef60ff9af6b50e144082bc475cc20cab47e8187201153da597dcd8` | +| Tool manager | `rzup 0.5.0` | +| Prover executable | `r0vm 3.0.6` | +| Guest build tool | `cargo-risczero 3.0.6` | +| Guest Rust toolchain | `rustc 1.97.0-dev` | +| Tested platform | Linux x86-64 under WSL2 | +| Trusted setup | transparent STARK setup; no experiment-specific ceremony | + +The official installer script used in the local environment had SHA-256 +`5699878af779351ec0f931fa84c3d5e35263279f66bd915af225f530a77341bf`. +The experiment pins every direct Rust dependency and commits both host and guest lock +files: + +- workspace `Cargo.lock`: `9b6f1a739c2acbe01581828fa37691af7288adb4642d158cb5f6a7383470483d` +- guest `Cargo.lock`: `1c1ef45133eb24090dfc136a479c1e007b0d2a6bab9b9ae0954a25f03dea27e9` + +No alternative proof system was attempted. + +## Selection basis + +RISC Zero was selected because its official 3.0 documentation supports local real-proof +generation on x86-64 Linux, describes `Receipt` as a zero-knowledge proof of execution, +binds verification to an image ID and authenticated journal, and provides a transparent +STARK path. The local environment had more than the documented 16 GB minimum RAM. + +The host crate compiles with `disable-dev-mode`, rejects `InnerReceipt::Fake`, requires +the selected `Composite` receipt variant, and rejects a truthy `RISC0_DEV_MODE` value. + +## Proved predicate + +The private witness contains: + +- one to 64 evidence bytes; +- a private 32-byte salt; +- a private measurement; and +- an experiment-local candidate state. + +The fixed guest accepts only an experiment-local `Supported` candidate state and a +measurement at least as large as the public threshold. It commits an authenticated public +journal containing the exact profile and predicate digests, subject digest, policy +digest, challenge, threshold, salted evidence commitment, and satisfied result. + +The proof does not establish whether the private input was truthful or whether the +`Supported` tag was assigned correctly. + +## Completeness, soundness, and zero-knowledge basis + +### Completeness + +One satisfying input produced a receipt that verified against the expected image ID and +authenticated journal. This is direct implementation evidence for the tested program and +environment, not a general proof about every possible input or platform. + +### Soundness + +The soundness basis is the selected RISC Zero STARK construction and its published +analysis, including the Fiat-Shamir transformation and documented hash assumptions. The +negative tests below provide implementation-level falsification attempts; they do not +replace the cryptographic analysis or an independent audit. + +### Zero knowledge + +The zero-knowledge basis is the RISC Zero protocol and verified non-fake receipt, which +hide guest execution inputs while exposing the journal. The exact private evidence and +salt byte strings were additionally scanned against every generated public artifact and +were absent. That byte scan checks this serializer path only; absence from files alone is +not a proof of zero knowledge. + +## Commands and observed results + +Toolchain and build: + +```text +rzup show +cargo-risczero 3.0.6; r0vm 3.0.6; rust 1.97.0 + +cargo check --locked --workspace +PASS + +cargo build --locked --release -p vstd-zk-host +PASS +``` + +Real proof plus automated negative cases: + +```text +RISC0_DEV_MODE=0 vstd-zk-host self-test local-artifacts/recorded-final +PASS +elapsed wall time: 6.10 seconds +maximum resident set: 1,214,664 KiB +``` + +Offline verifier invocation without the witness: + +```text +RISC0_DEV_MODE=0 vstd-zk-host verify receipt.msgpack public.json +PASS +elapsed wall time: 0.10 seconds +maximum resident set: 5,632 KiB +``` + +Repository validation: + +```text +python -m pytest -q +258 passed, 3 skipped + +python scripts/check_presentation.py +[PRESENTATION OK] links, versions, boundaries, paths, and visual assets + +python -m compileall -q src scripts +PASS +``` + +The three guest panic messages printed during self-test are the expected rejection paths +for below-threshold, `Unknown`, and `Conflicted` inputs. They do not contain witness bytes. + +## Negative-test results + +| Test | Result | +|---|---| +| valid proof and matching public inputs | pass | +| below-threshold private measurement | rejected | +| experiment-local `Unknown` input | rejected | +| experiment-local `Conflicted` input | rejected | +| mutated public threshold | rejected | +| wrong image ID | rejected | +| corrupted proof bytes | rejected | +| tampered authenticated journal | rejected | +| subject and challenge transplantation | rejected | +| private evidence or salt copied to public artifacts | not detected; test passed | + +All ten recorded Boolean checks were `true`. + +## Recorded public artifacts + +The exact receipt, public envelope, and self-test result are tracked under +[`recorded-proof/`](recorded-proof/) so a consumer can verify the recorded run rather than +only generating a new proof. The ephemeral private witness and salt remain excluded. + +| Artifact | Bytes | SHA-256 | +|---|---:|---| +| `receipt.msgpack` | 301811 | `5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df` | +| `public.json` | 2575 | `6324c3c5d77ea4df4034f61131059289d5228f190d69e34c59bd7416fa9ac823` | +| `self-test-results.json` | 377 | `e4c1bff21fb6161221276157fa96af6661af8635da35970ba12e462881f2c6fe` | +| `corrupted-receipt.msgpack` | 301811 | `389117e63a429e55c3f3616b9cbf2339fb1c99b48712ef7a3e5f5f16b32b6d81` | +| `tampered-journal.msgpack` | 301811 | `4e443f5084b8665a7185e6e4e62fd72eed5130673b3d7bfde488cdc6c405555c` | +| `mutated-public.json` | 2575 | `c2a056b71b2019daa8ac9f3aefcb4c2cc28a1b56ef97f8c61947bf37c5f2b7b9` | +| `transplanted-public.json` | 2575 | `95cf0d97e20777e2ad49234b7b27074dc8931ee7de215632a7d22a6b5466f2b6` | + +## Unresolved assumptions + +- The selected cryptographic implementation and transitive dependencies were not + independently audited in this work. +- The image ID was produced once in this environment; a second independent build has not + yet corroborated it. +- The host, compiler, installer, and operating system remain trusted for witness secrecy. +- The experiment does not establish constant-time or side-channel-resistant proving. +- The challenge is cryptographically bound, but challenge issuance, expiry, uniqueness, + and replay storage are external. +- Salt quality is generated from the host operating-system RNG but is not itself proved. +- The public subject and policy digests need external resolution and provenance rules. +- A private `Supported` tag is merely an input to this predicate, not independently + established VSTD evidence. + +## Public claims currently justified + +The local evidence justifies saying that the bounded RISC Zero 3.0.6 reference mechanism produced +and re-verified within the reference program a real composite STARK receipt for one bounded +hidden-witness predicate, with the recorded negative cases rejected. It does not establish +distinct prover/verifier actors. + +It also supports keeping VSTD core disclosure-neutral: this result demonstrates one +optional privacy mechanism without requiring or invalidating full-disclosure receipts. + +## Claims still prohibited + +Do not claim that this experiment proves: + +- real-world truth, completeness, provenance, authorization, independence, identity, + uniqueness, freshness, revocation, or legal compliance; +- protection against a malicious or compromised prover host; +- general zero-knowledge support for every VSTD predicate; +- independent implementation, third-party audit, external adoption, or production + readiness; +- a frozen `ZIZK-VSTD` wire profile; or +- that zero knowledge should be mandatory for VSTD. diff --git a/examples/zizk_artifact_first/risc0/THREAT_MODEL.md b/examples/zizk_artifact_first/risc0/THREAT_MODEL.md new file mode 100644 index 0000000..b59a1f1 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/THREAT_MODEL.md @@ -0,0 +1,96 @@ +# Threat model + +> **Acronyms:** Executable and Linkable Format (ELF); identifier (ID); JavaScript Object Notation (JSON); +> reduced instruction set computer (RISC); Secure Hash Algorithm 256-bit (SHA-256); +> scalable transparent argument of knowledge (STARK); zero-identity/zero-knowledge (ZIZK); +> zero-knowledge virtual machine (zkVM). + +**Scope:** this bounded optional zero-knowledge proof mechanism only; not the governing +ZIZK artifact-first architecture as a whole. + +## Protected secret + +The intended secret is the private witness supplied to the pinned guest: evidence +bytes, a 32-byte salt, a measurement, and a mechanism-local candidate state. The +receipt intentionally reveals the public journal. Subject, policy, challenge, +threshold, predicate result, and salted evidence commitment are not secrets. + +## Trust roots + +Acceptance depends on all of the following: + +1. the expected RISC Zero image ID obtained from the reviewed guest ELF; +2. RISC Zero zkVM 3.0.6 verification code and its proof-system parameters; +3. the pinned Rust sources and `Cargo.lock`; +4. SHA-256 collision and preimage resistance for the public digests; +5. correct public-statement comparison after receipt verification; and +6. a verifier obtaining the expected image ID independently rather than trusting an + unbound metadata field supplied by the prover. + +The composite STARK uses transparent public setup. Its non-interactive security relies +on the proof system's Fiat-Shamir construction and its documented hash assumptions. +This repository does not independently prove the cryptographic reduction. + +## Attacks tested + +| Attack | Required result | +|---|---| +| private measurement below threshold | proof attempt rejected | +| private `Unknown` candidate state | proof attempt rejected | +| private `Conflicted` candidate state | proof attempt rejected | +| mutated public threshold | wrapper verification rejected | +| different subject or challenge | statement transplantation rejected | +| wrong image ID | receipt verification rejected | +| corrupted receipt bytes | decoding or verification rejected | +| authenticated journal mutation | receipt verification rejected | +| private byte strings copied to public files | serialization scan rejected | + +## Residual risks + +### Host compromise and operational leakage + +The proof system hides guest inputs from a receipt verifier. It does not protect the +witness from the prover's operating system, shell history, swap, crash dumps, malware, +debuggers, or a modified host binary. The manual workflow writes a temporary private +JSON file and requires the operator to protect and remove it. + +### Side channels + +The reference mechanism does not claim constant-time host behavior, traffic-analysis resistance, +or protection from proof-time, memory-use, file-size, power, or hardware side channels. +The evidence length is hidden by the proof but could be correlated with prover-side +observations. + +### Low-entropy evidence + +The public commitment includes a private random 32-byte salt to impede offline guessing. +Weak or reused salts, disclosure of the salt, or host compromise can make low-entropy +evidence guessable. The proof does not certify salt quality. + +### Replay and freshness + +The public challenge is authenticated by the journal, so a proof cannot be transplanted +to a different challenge without rejection. The same valid proof can still be replayed +for the same challenge. Challenge issuance, uniqueness, expiry, clock trust, and replay +storage are outside this mechanism and must remain explicit assumptions or UNKNOWN. + +### Parser and denial of service + +Receipt and envelope reads have size limits. MessagePack is used because RISC Zero's +receipt documentation recommends a serde format with depth limits for untrusted input. +The reference mechanism does not establish a complete resource-exhaustion bound for all malformed +receipts. + +### Supply chain + +Version pins and a committed lock file constrain dependencies but do not independently +audit every transitive crate, compiler binary, installer, or build host. Reproducing an +image ID on another trusted build host is useful evidence, not supplied here as an +independent implementation. + +### Semantic overreach + +A prover selects the private bytes and candidate tag. The proof does not show that those +bytes are truthful, complete, authorized, fresh, legally valid, independently sourced, +or causally connected to the real world. It proves only execution of the fixed predicate +over the committed input. diff --git a/examples/zizk_artifact_first/risc0/fixtures/README.md b/examples/zizk_artifact_first/risc0/fixtures/README.md new file mode 100644 index 0000000..eb81d24 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/fixtures/README.md @@ -0,0 +1,25 @@ +# Generated fixtures + +> **Acronym:** scalable transparent argument of knowledge (STARK). + +The real-proof self-test creates fixtures under the ignored `local-artifacts/` directory +instead of committing a reusable private witness or a large proof binary. + +Generated positive fixtures: + +- `receipt.msgpack` — real composite STARK receipt; +- `public.json` — authenticated journal plus non-authoritative convenience metadata. + +Generated negative fixtures: + +- `mutated-public.json` — changed public threshold; +- `transplanted-public.json` — changed subject and challenge; +- `corrupted-receipt.msgpack` — corrupted serialized receipt; +- `tampered-journal.msgpack` — decoded journal changed without regenerating the seal. + +Additional negative witnesses are generated only in memory: below-threshold, +`Unknown`, and `Conflicted`. The self-test requires every negative case to be rejected +and writes the Boolean results to `self-test-results.json`. + +This layout avoids publishing the private witness bytes in a fixture while retaining a +reproducible generator and verifier. diff --git a/examples/zizk_artifact_first/risc0/host/Cargo.toml b/examples/zizk_artifact_first/risc0/host/Cargo.toml new file mode 100644 index 0000000..a93f508 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/host/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "vstd-zk-host" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +hex = "=0.4.3" +rand = "=0.8.5" +risc0-zkvm = { version = "=3.0.6", features = ["disable-dev-mode"] } +rmp-serde = "=1.3.0" +serde = { version = "=1.0.228", features = ["derive"] } +serde_json = "=1.0.145" +vstd-zk-methods = { path = "../methods" } +vstd-zk-types = { path = "../types" } diff --git a/examples/zizk_artifact_first/risc0/host/src/main.rs b/examples/zizk_artifact_first/risc0/host/src/main.rs new file mode 100644 index 0000000..ad45c0e --- /dev/null +++ b/examples/zizk_artifact_first/risc0/host/src/main.rs @@ -0,0 +1,457 @@ +//! Terminology: Executable and Linkable Format (ELF); identifier (ID); +//! reduced instruction set computer (RISC); RISC Zero (RISC0); +//! Secure Hash Algorithm 256-bit (SHA-256); scalable transparent argument of knowledge (STARK); +//! Verifier Standard (VSTD); zero-knowledge (ZK). +use hex::FromHex; +use rand::{rngs::OsRng, RngCore}; +use risc0_zkvm::{ + default_prover, + sha::{Digest, Impl, Sha256}, + ExecutorEnv, InnerReceipt, Receipt, +}; +use serde::Serialize; +use std::{ + env, + error::Error, + fs, + io, + path::Path, +}; +use vstd_zk_methods::{VSTD_ZK_GUEST_ELF, VSTD_ZK_GUEST_ID}; +use vstd_zk_types::{ + CandidateState, PrivateWitness, ProverInput, PublicEnvelope, PublicJournal, + PublicStatement, COMMITMENT_DOMAIN, PREDICATE_TEXT, PROFILE_LABEL, +}; + +const PROOF_SYSTEM: &str = "risc0-zkvm-3.0.6-composite-stark"; +const MAX_RECEIPT_BYTES: u64 = 32 * 1024 * 1024; +const MAX_ENVELOPE_BYTES: u64 = 1024 * 1024; + +type AppResult = Result>; + +#[derive(Serialize)] +struct SelfTestResults { + real_proof_verified: bool, + unsatisfied_witness_rejected: bool, + unknown_rejected: bool, + conflicted_rejected: bool, + mutated_public_input_rejected: bool, + wrong_image_id_rejected: bool, + corrupted_proof_rejected: bool, + tampered_journal_rejected: bool, + statement_transplant_rejected: bool, + private_bytes_absent_from_public_artifacts: bool, +} + +fn main() { + if let Err(error) = run() { + eprintln!("error: {error}"); + std::process::exit(1); + } +} + +fn run() -> AppResult<()> { + let args: Vec = env::args().collect(); + match args.get(1).map(String::as_str) { + Some("generate-inputs") if args.len() == 4 => { + generate_inputs(Path::new(&args[2]), Path::new(&args[3])) + } + Some("prove") if args.len() == 6 => prove_from_files( + Path::new(&args[2]), + Path::new(&args[3]), + Path::new(&args[4]), + Path::new(&args[5]), + ), + Some("verify") if args.len() == 4 || args.len() == 5 => { + let expected_id = args.get(4).map(|value| parse_digest(value)).transpose()?; + verify_artifacts(Path::new(&args[2]), Path::new(&args[3]), expected_id)?; + println!("PASS: real RISC Zero receipt and public statement verified"); + Ok(()) + } + Some("image-id") if args.len() == 2 => { + println!("{}", method_id()); + Ok(()) + } + Some("self-test") if args.len() == 3 => self_test(Path::new(&args[2])), + _ => Err(usage_error()), + } +} + +fn usage_error() -> Box { + io::Error::new( + io::ErrorKind::InvalidInput, + "usage:\n vstd-zk-host generate-inputs PRIVATE.json STATEMENT.json\n vstd-zk-host prove PRIVATE.json STATEMENT.json RECEIPT.bin PUBLIC.json\n vstd-zk-host verify RECEIPT.bin PUBLIC.json [EXPECTED_IMAGE_ID]\n vstd-zk-host image-id\n vstd-zk-host self-test OUTPUT_DIR", + ) + .into() +} + +fn method_id() -> Digest { + Digest::from(VSTD_ZK_GUEST_ID) +} + +fn parse_digest(value: &str) -> AppResult { + Ok(Digest::from_hex(value)?) +} + +fn digest_bytes(value: &[u8]) -> [u8; 32] { + let digest = Impl::hash_bytes(value); + digest.as_bytes().try_into().expect("SHA-256 is 32 bytes") +} + +fn digest_hex(value: &[u8]) -> String { + hex::encode(digest_bytes(value)) +} + +fn evidence_commitment(witness: &PrivateWitness) -> [u8; 32] { + let mut input = Vec::with_capacity( + COMMITMENT_DOMAIN.len() + 4 + witness.evidence.len() + 32 + 8, + ); + input.extend_from_slice(COMMITMENT_DOMAIN); + input.extend_from_slice(&(witness.evidence.len() as u32).to_be_bytes()); + input.extend_from_slice(&witness.evidence); + input.extend_from_slice(&witness.salt); + input.extend_from_slice(&witness.measurement.to_be_bytes()); + digest_bytes(&input) +} + +fn random_array() -> [u8; 32] { + let mut value = [0_u8; 32]; + OsRng.fill_bytes(&mut value); + value +} + +fn sample_inputs() -> (PrivateWitness, PublicStatement) { + let mut evidence = vec![0_u8; 48]; + OsRng.fill_bytes(&mut evidence); + let witness = PrivateWitness { + evidence, + salt: random_array(), + measurement: 73, + candidate_state: CandidateState::Supported, + }; + let statement = PublicStatement { + subject_digest: random_array(), + policy_digest: digest_bytes(b"vstd-zk-fixed-threshold-policy-v1"), + challenge: random_array(), + threshold: 70, + }; + (witness, statement) +} + +fn generate_inputs(private_path: &Path, statement_path: &Path) -> AppResult<()> { + let (witness, statement) = sample_inputs(); + write_json(private_path, &witness)?; + write_json(statement_path, &statement)?; + println!( + "generated a local private witness and public statement; do not publish {}", + private_path.display() + ); + Ok(()) +} + +fn ensure_real_mode() -> AppResult<()> { + if let Ok(value) = env::var("RISC0_DEV_MODE") { + let normalized = value.trim().to_ascii_lowercase(); + if !normalized.is_empty() && normalized != "0" && normalized != "false" { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "RISC0_DEV_MODE must be unset, 0, or false; this binary also compiles with disable-dev-mode", + ) + .into()); + } + } + Ok(()) +} + +fn prove_from_files( + private_path: &Path, + statement_path: &Path, + receipt_path: &Path, + public_path: &Path, +) -> AppResult<()> { + ensure_real_mode()?; + let witness: PrivateWitness = read_json_bounded(private_path, MAX_ENVELOPE_BYTES)?; + let statement: PublicStatement = read_json_bounded(statement_path, MAX_ENVELOPE_BYTES)?; + prove_to_files(&witness, &statement, receipt_path, public_path)?; + println!("wrote a verified real receipt and public envelope"); + Ok(()) +} + +fn prove_to_files( + witness: &PrivateWitness, + statement: &PublicStatement, + receipt_path: &Path, + public_path: &Path, +) -> AppResult { + ensure_real_mode()?; + let input = ProverInput { + statement: statement.clone(), + witness: witness.clone(), + }; + let env = ExecutorEnv::builder().write(&input)?.build()?; + let prove_info = default_prover().prove(env, VSTD_ZK_GUEST_ELF)?; + let receipt = prove_info.receipt; + require_composite_receipt(&receipt)?; + receipt.verify(method_id())?; + + let journal: PublicJournal = receipt.journal.decode()?; + validate_public_journal(&journal, statement)?; + if journal.evidence_commitment != evidence_commitment(witness) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "authenticated evidence commitment does not match the supplied witness", + ) + .into()); + } + + let receipt_bytes = rmp_serde::to_vec_named(&receipt)?; + let envelope = PublicEnvelope { + experiment_profile: String::from_utf8(PROFILE_LABEL.to_vec())?, + proof_system: PROOF_SYSTEM.to_string(), + image_id: method_id().to_string(), + receipt_sha256: digest_hex(&receipt_bytes), + receipt_size: receipt_bytes.len() as u64, + journal, + }; + write_bytes(receipt_path, &receipt_bytes)?; + write_json(public_path, &envelope)?; + Ok(envelope) +} + +fn require_composite_receipt(receipt: &Receipt) -> AppResult<()> { + match &receipt.inner { + InnerReceipt::Composite(_) => Ok(()), + InnerReceipt::Fake(_) => Err(io::Error::new( + io::ErrorKind::InvalidData, + "fake RISC Zero receipt rejected", + ) + .into()), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "receipt kind differs from the selected composite STARK path", + ) + .into()), + } +} + +fn validate_public_journal( + journal: &PublicJournal, + expected: &PublicStatement, +) -> AppResult<()> { + if journal.profile_digest != digest_bytes(PROFILE_LABEL) + || journal.predicate_digest != digest_bytes(PREDICATE_TEXT) + || journal.subject_digest != expected.subject_digest + || journal.policy_digest != expected.policy_digest + || journal.challenge != expected.challenge + || journal.threshold != expected.threshold + || !journal.predicate_satisfied + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "authenticated journal does not match the expected public statement", + ) + .into()); + } + Ok(()) +} + +fn verify_artifacts( + receipt_path: &Path, + public_path: &Path, + expected_id: Option, +) -> AppResult { + let receipt_bytes = read_bytes_bounded(receipt_path, MAX_RECEIPT_BYTES)?; + let envelope: PublicEnvelope = read_json_bounded(public_path, MAX_ENVELOPE_BYTES)?; + let receipt: Receipt = rmp_serde::from_slice(&receipt_bytes)?; + require_composite_receipt(&receipt)?; + + let trusted_id = expected_id.unwrap_or_else(method_id); + receipt.verify(trusted_id)?; + let journal: PublicJournal = receipt.journal.decode()?; + + if envelope.image_id != trusted_id.to_string() + || envelope.receipt_sha256 != digest_hex(&receipt_bytes) + || envelope.receipt_size != receipt_bytes.len() as u64 + || envelope.experiment_profile != String::from_utf8(PROFILE_LABEL.to_vec())? + || envelope.proof_system != PROOF_SYSTEM + || envelope.journal != journal + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "public envelope, receipt, image ID, or authenticated journal mismatch", + ) + .into()); + } + + let expected_statement = PublicStatement { + subject_digest: envelope.journal.subject_digest, + policy_digest: envelope.journal.policy_digest, + challenge: envelope.journal.challenge, + threshold: envelope.journal.threshold, + }; + validate_public_journal(&journal, &expected_statement)?; + Ok(journal) +} + +fn proof_attempt_rejected( + witness: &PrivateWitness, + statement: &PublicStatement, +) -> AppResult { + let input = ProverInput { + statement: statement.clone(), + witness: witness.clone(), + }; + let env = ExecutorEnv::builder().write(&input)?.build()?; + match default_prover().prove(env, VSTD_ZK_GUEST_ELF) { + Ok(prove_info) => Ok(prove_info.receipt.verify(method_id()).is_err()), + Err(_) => Ok(true), + } +} + +fn self_test(output_dir: &Path) -> AppResult<()> { + ensure_real_mode()?; + if output_dir.exists() { + fs::remove_dir_all(output_dir)?; + } + fs::create_dir_all(output_dir)?; + + let (witness, statement) = sample_inputs(); + let receipt_path = output_dir.join("receipt.msgpack"); + let public_path = output_dir.join("public.json"); + let envelope = prove_to_files(&witness, &statement, &receipt_path, &public_path)?; + let real_proof_verified = verify_artifacts(&receipt_path, &public_path, None).is_ok(); + + let mut low_witness = witness.clone(); + low_witness.measurement = statement.threshold.saturating_sub(1); + let unsatisfied_witness_rejected = proof_attempt_rejected(&low_witness, &statement)?; + + let mut unknown_witness = witness.clone(); + unknown_witness.candidate_state = CandidateState::Unknown; + let unknown_rejected = proof_attempt_rejected(&unknown_witness, &statement)?; + + let mut conflicted_witness = witness.clone(); + conflicted_witness.candidate_state = CandidateState::Conflicted; + let conflicted_rejected = proof_attempt_rejected(&conflicted_witness, &statement)?; + + let mut mutated_envelope = envelope.clone(); + mutated_envelope.journal.threshold = mutated_envelope.journal.threshold.saturating_add(1); + let mutated_path = output_dir.join("mutated-public.json"); + write_json(&mutated_path, &mutated_envelope)?; + let mutated_public_input_rejected = + verify_artifacts(&receipt_path, &mutated_path, None).is_err(); + + let mut transplanted = envelope.clone(); + transplanted.journal.subject_digest[0] ^= 1; + transplanted.journal.challenge[0] ^= 1; + let transplanted_path = output_dir.join("transplanted-public.json"); + write_json(&transplanted_path, &transplanted)?; + let statement_transplant_rejected = + verify_artifacts(&receipt_path, &transplanted_path, None).is_err(); + + let mut wrong_id = method_id(); + wrong_id.as_mut_bytes()[0] ^= 1; + let wrong_image_id_rejected = + verify_artifacts(&receipt_path, &public_path, Some(wrong_id)).is_err(); + + let receipt_bytes = read_bytes_bounded(&receipt_path, MAX_RECEIPT_BYTES)?; + let mut corrupted_bytes = receipt_bytes.clone(); + let corrupt_index = corrupted_bytes.len() / 2; + corrupted_bytes[corrupt_index] ^= 1; + let corrupted_path = output_dir.join("corrupted-receipt.msgpack"); + write_bytes(&corrupted_path, &corrupted_bytes)?; + let corrupted_proof_rejected = + verify_artifacts(&corrupted_path, &public_path, None).is_err(); + + let mut tampered_receipt: Receipt = rmp_serde::from_slice(&receipt_bytes)?; + if tampered_receipt.journal.bytes.is_empty() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "empty journal").into()); + } + tampered_receipt.journal.bytes[0] ^= 1; + let tampered_path = output_dir.join("tampered-journal.msgpack"); + write_bytes(&tampered_path, &rmp_serde::to_vec_named(&tampered_receipt)?)?; + let tampered_journal_rejected = + verify_artifacts(&tampered_path, &public_path, None).is_err(); + + let private_bytes_absent_from_public_artifacts = !directory_contains( + output_dir, + &[witness.evidence.as_slice(), witness.salt.as_slice()], + )?; + + let results = SelfTestResults { + real_proof_verified, + unsatisfied_witness_rejected, + unknown_rejected, + conflicted_rejected, + mutated_public_input_rejected, + wrong_image_id_rejected, + corrupted_proof_rejected, + tampered_journal_rejected, + statement_transplant_rejected, + private_bytes_absent_from_public_artifacts, + }; + let all_passed = results.real_proof_verified + && results.unsatisfied_witness_rejected + && results.unknown_rejected + && results.conflicted_rejected + && results.mutated_public_input_rejected + && results.wrong_image_id_rejected + && results.corrupted_proof_rejected + && results.tampered_journal_rejected + && results.statement_transplant_rejected + && results.private_bytes_absent_from_public_artifacts; + write_json(&output_dir.join("self-test-results.json"), &results)?; + println!("{}", serde_json::to_string_pretty(&results)?); + if !all_passed { + return Err(io::Error::new(io::ErrorKind::Other, "one or more self-tests failed").into()); + } + Ok(()) +} + +fn directory_contains(directory: &Path, needles: &[&[u8]]) -> AppResult { + for entry in fs::read_dir(directory)? { + let path = entry?.path(); + if !path.is_file() { + continue; + } + let bytes = fs::read(path)?; + for needle in needles { + if !needle.is_empty() && bytes.windows(needle.len()).any(|window| window == *needle) { + return Ok(true); + } + } + } + Ok(false) +} + +fn read_bytes_bounded(path: &Path, maximum: u64) -> AppResult> { + let metadata = fs::metadata(path)?; + if metadata.len() > maximum { + return Err(io::Error::new(io::ErrorKind::InvalidData, "input exceeds size bound").into()); + } + Ok(fs::read(path)?) +} + +fn read_json_bounded(path: &Path, maximum: u64) -> AppResult +where + T: serde::de::DeserializeOwned, +{ + let bytes = read_bytes_bounded(path, maximum)?; + Ok(serde_json::from_slice(&bytes)?) +} + +fn write_bytes(path: &Path, bytes: &[u8]) -> AppResult<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, bytes)?; + Ok(()) +} + +fn write_json(path: &Path, value: &T) -> AppResult<()> +where + T: Serialize, +{ + let mut bytes = serde_json::to_vec_pretty(value)?; + bytes.push(b'\n'); + write_bytes(path, &bytes) +} diff --git a/examples/zizk_artifact_first/risc0/methods/Cargo.toml b/examples/zizk_artifact_first/risc0/methods/Cargo.toml new file mode 100644 index 0000000..1baf35a --- /dev/null +++ b/examples/zizk_artifact_first/risc0/methods/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "vstd-zk-methods" +version = "0.1.0" +edition = "2021" +publish = false + +[build-dependencies] +risc0-build = { version = "=3.0.6" } + +[package.metadata.risc0] +methods = ["guest"] diff --git a/examples/zizk_artifact_first/risc0/methods/build.rs b/examples/zizk_artifact_first/risc0/methods/build.rs new file mode 100644 index 0000000..08a8a4e --- /dev/null +++ b/examples/zizk_artifact_first/risc0/methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/examples/zizk_artifact_first/risc0/methods/guest/Cargo.lock b/examples/zizk_artifact_first/risc0/methods/guest/Cargo.lock new file mode 100644 index 0000000..9591684 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/methods/guest/Cargo.lock @@ -0,0 +1,1485 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-r1cs-std", + "ark-std", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0c292754729c8a190e50414fd1a37093c786c709899f29c9f7daccecfa855e" +dependencies = [ + "ahash", + "ark-crypto-primitives-macros", + "ark-ec", + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest", + "fnv", + "merlin", + "sha2", +] + +[[package]] +name = "ark-crypto-primitives-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-groth16" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f1d0f3a534bb54188b8dcc104307db6c56cdae574ddc3212aec0625740fc7e" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-relations", + "ark-std", + "educe", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-snark" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d368e2848c2d4c129ce7679a7d0d2d612b6a274d3ea6a13bad4445d61b381b88" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "include_bytes_aligned" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee796ad498c8d9a1d68e477df8f754ed784ef875de1414ebdaf169f70a6a784" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "no_std_strings" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "risc0-binfmt" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d836c6ad82f4ced7c61d5feedf905a17780312e393aa681d29cc0bbc5131672b" +dependencies = [ + "anyhow", + "borsh", + "bytemuck", + "derive_more", + "elf", + "lazy_static", + "postcard", + "rand 0.9.5", + "risc0-zkp", + "risc0-zkvm-platform", + "ruint", + "semver", + "serde", + "tracing", +] + +[[package]] +name = "risc0-circuit-keccak" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c731e12429eb4457e1ddc69c56ee7343a1e10b86e4aa55bc8f4d2b13734abb9" +dependencies = [ + "anyhow", + "bytemuck", + "paste", + "risc0-binfmt", + "risc0-circuit-recursion", + "risc0-core", + "risc0-zkp", + "tracing", +] + +[[package]] +name = "risc0-circuit-recursion" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40dd640122abcc67d4d4e4f055c68cbc3ad2efb8589c65c2b23d354632971b60" +dependencies = [ + "anyhow", + "bytemuck", + "hex", + "metal", + "risc0-core", + "risc0-zkp", + "tracing", +] + +[[package]] +name = "risc0-circuit-rv32im" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb11231aa4b74bcc0c8d16597893fbd7ea6f6a9ebbc35e16bfd06b467c7ee104" +dependencies = [ + "anyhow", + "bit-vec", + "bytemuck", + "derive_more", + "paste", + "risc0-binfmt", + "risc0-core", + "risc0-zkp", + "serde", + "tracing", +] + +[[package]] +name = "risc0-core" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eb2d2b2c6cac0e43cbb2202daacee1a2f24d0dfa03fd08887a11dc6defdcc1" +dependencies = [ + "bytemuck", + "rand_core 0.9.5", +] + +[[package]] +name = "risc0-groth16" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ca702ea7d0162766defe7ed6a79bda4a747ad9e2684000a6edd14df0a6d1f3" +dependencies = [ + "anyhow", + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-groth16", + "ark-serialize", + "bytemuck", + "hex", + "num-bigint", + "num-traits", + "risc0-binfmt", + "risc0-zkp", + "serde", +] + +[[package]] +name = "risc0-zkos-v1compat" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b0b598ba7946354b10ca5c56e382de801e6c7fce9fccad0396ec436bc5072b" +dependencies = [ + "include_bytes_aligned", + "no_std_strings", + "risc0-zkvm-platform", +] + +[[package]] +name = "risc0-zkp" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21c0c921e5e2d44197940d387a45e29c6165e318b5a168fdfdbd50f50ba03678" +dependencies = [ + "anyhow", + "blake2", + "borsh", + "bytemuck", + "cfg-if", + "digest", + "hex", + "hex-literal", + "metal", + "paste", + "rand_core 0.9.5", + "risc0-core", + "risc0-zkvm-platform", + "serde", + "sha2", + "stability", + "tracing", +] + +[[package]] +name = "risc0-zkvm" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4f24ec767f71a1663a4d24cf9d02b6bfee44c64647cae677227817051007a" +dependencies = [ + "anyhow", + "borsh", + "bytemuck", + "derive_more", + "hex", + "risc0-binfmt", + "risc0-circuit-keccak", + "risc0-circuit-recursion", + "risc0-circuit-rv32im", + "risc0-core", + "risc0-groth16", + "risc0-zkos-v1compat", + "risc0-zkp", + "risc0-zkvm-platform", + "rrs-lib", + "semver", + "serde", + "sha2", + "stability", + "tracing", +] + +[[package]] +name = "risc0-zkvm-platform" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb37a97ff7e8e4ee1b2a1c43ec143b4887759883c343507af9e4787a57914cd" +dependencies = [ + "bytemuck", + "cfg-if", + "getrandom 0.2.17", + "getrandom 0.3.4", + "libm", + "num_enum", + "paste", + "stability", +] + +[[package]] +name = "rrs-lib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4382d3af3a4ebdae7f64ba6edd9114fff92c89808004c4943b393377a25d001" +dependencies = [ + "downcast-rs", + "paste", +] + +[[package]] +name = "ruint" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" +dependencies = [ + "borsh", + "proptest", + "rand 0.8.7", + "rand 0.9.5", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vstd-zk-guest" +version = "0.1.0" +dependencies = [ + "risc0-zkvm", + "vstd-zk-types", +] + +[[package]] +name = "vstd-zk-types" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/examples/zizk_artifact_first/risc0/methods/guest/Cargo.toml b/examples/zizk_artifact_first/risc0/methods/guest/Cargo.toml new file mode 100644 index 0000000..6f11780 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/methods/guest/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "vstd-zk-guest" +version = "0.1.0" +edition = "2021" +publish = false + +[workspace] + +[dependencies] +risc0-zkvm = { version = "=3.0.6", default-features = false, features = ["std"] } +vstd-zk-types = { path = "../../types" } diff --git a/examples/zizk_artifact_first/risc0/methods/guest/src/main.rs b/examples/zizk_artifact_first/risc0/methods/guest/src/main.rs new file mode 100644 index 0000000..e15ddd7 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/methods/guest/src/main.rs @@ -0,0 +1,70 @@ +//! Terminology: Secure Hash Algorithm 256-bit (SHA-256); Verifier Standard (VSTD). +use risc0_zkvm::{ + guest::env, + sha::{Impl, Sha256}, +}; +use vstd_zk_types::{ + CandidateState, ProverInput, PublicJournal, COMMITMENT_DOMAIN, MAX_EVIDENCE_LEN, + MAX_THRESHOLD, PREDICATE_TEXT, PROFILE_LABEL, +}; + +fn digest_bytes(value: &[u8]) -> [u8; 32] { + let digest = Impl::hash_bytes(value); + digest.as_bytes().try_into().expect("SHA-256 is 32 bytes") +} + +fn main() { + let input: ProverInput = env::read(); + + assert!(!input.witness.evidence.is_empty(), "evidence must not be empty"); + assert!( + input.witness.evidence.len() <= MAX_EVIDENCE_LEN, + "evidence exceeds the bounded predicate" + ); + assert!( + input.witness.candidate_state == CandidateState::Supported, + "UNKNOWN and CONFLICTED inputs do not satisfy this predicate" + ); + assert!( + input.statement.threshold <= MAX_THRESHOLD, + "threshold exceeds the experiment bound" + ); + assert!( + input.witness.measurement >= input.statement.threshold, + "private measurement is below the public threshold" + ); + assert!( + input.statement.subject_digest != [0_u8; 32], + "subject digest must be explicit" + ); + assert!( + input.statement.policy_digest != [0_u8; 32], + "policy digest must be explicit" + ); + assert!( + input.statement.challenge != [0_u8; 32], + "challenge must be explicit" + ); + + let mut commitment_input = Vec::with_capacity( + COMMITMENT_DOMAIN.len() + 4 + input.witness.evidence.len() + 32 + 8, + ); + commitment_input.extend_from_slice(COMMITMENT_DOMAIN); + commitment_input.extend_from_slice(&(input.witness.evidence.len() as u32).to_be_bytes()); + commitment_input.extend_from_slice(&input.witness.evidence); + commitment_input.extend_from_slice(&input.witness.salt); + commitment_input.extend_from_slice(&input.witness.measurement.to_be_bytes()); + + let journal = PublicJournal { + profile_digest: digest_bytes(PROFILE_LABEL), + predicate_digest: digest_bytes(PREDICATE_TEXT), + subject_digest: input.statement.subject_digest, + policy_digest: input.statement.policy_digest, + challenge: input.statement.challenge, + threshold: input.statement.threshold, + evidence_commitment: digest_bytes(&commitment_input), + predicate_satisfied: true, + }; + + env::commit(&journal); +} diff --git a/examples/zizk_artifact_first/risc0/methods/src/lib.rs b/examples/zizk_artifact_first/risc0/methods/src/lib.rs new file mode 100644 index 0000000..1bdb308 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/examples/zizk_artifact_first/risc0/recorded-proof/public.json b/examples/zizk_artifact_first/risc0/recorded-proof/public.json new file mode 100644 index 0000000..3e845cc --- /dev/null +++ b/examples/zizk_artifact_first/risc0/recorded-proof/public.json @@ -0,0 +1,215 @@ +{ + "experiment_profile": "ZIZK-VSTD-ZK-EXPERIMENT-0.1", + "proof_system": "risc0-zkvm-3.0.6-composite-stark", + "image_id": "e1e9bf4f68ef60ff9af6b50e144082bc475cc20cab47e8187201153da597dcd8", + "receipt_sha256": "5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df", + "receipt_size": 301811, + "journal": { + "profile_digest": [ + 95, + 251, + 195, + 21, + 230, + 229, + 109, + 1, + 28, + 110, + 20, + 30, + 223, + 234, + 203, + 26, + 63, + 120, + 214, + 9, + 248, + 115, + 124, + 108, + 213, + 9, + 253, + 71, + 110, + 244, + 139, + 20 + ], + "predicate_digest": [ + 255, + 145, + 56, + 237, + 74, + 58, + 230, + 50, + 99, + 139, + 147, + 194, + 19, + 245, + 53, + 122, + 137, + 163, + 150, + 155, + 154, + 8, + 215, + 119, + 34, + 42, + 211, + 189, + 129, + 194, + 229, + 33 + ], + "subject_digest": [ + 22, + 122, + 99, + 204, + 97, + 34, + 89, + 53, + 32, + 92, + 242, + 247, + 10, + 210, + 88, + 172, + 220, + 231, + 224, + 30, + 26, + 196, + 131, + 46, + 214, + 182, + 32, + 56, + 55, + 236, + 22, + 203 + ], + "policy_digest": [ + 75, + 139, + 199, + 64, + 37, + 3, + 206, + 42, + 111, + 213, + 121, + 136, + 173, + 42, + 208, + 52, + 146, + 86, + 2, + 12, + 221, + 139, + 62, + 39, + 68, + 25, + 86, + 9, + 132, + 100, + 82, + 95 + ], + "challenge": [ + 70, + 25, + 227, + 207, + 4, + 53, + 122, + 247, + 196, + 116, + 55, + 94, + 192, + 15, + 67, + 94, + 13, + 20, + 211, + 145, + 35, + 230, + 197, + 120, + 234, + 38, + 157, + 44, + 156, + 99, + 136, + 154 + ], + "threshold": 70, + "evidence_commitment": [ + 197, + 74, + 14, + 209, + 241, + 162, + 135, + 66, + 59, + 124, + 229, + 70, + 93, + 53, + 243, + 147, + 120, + 47, + 41, + 34, + 80, + 125, + 63, + 61, + 137, + 137, + 80, + 249, + 27, + 95, + 80, + 95 + ], + "predicate_satisfied": true + } +} diff --git a/examples/zizk_artifact_first/risc0/recorded-proof/receipt.msgpack b/examples/zizk_artifact_first/risc0/recorded-proof/receipt.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..e058835b952bde43ec21e6bf5be847d483a9c949 GIT binary patch literal 301811 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

$MK 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�Q$JZw)GXkY$?zU_(cEL;dAOMn74`*h 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 +BgtCxRfG@H-8TW%-mvm;Ce`<=ijnou_&E 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 literal 0 HcmV?d00001 diff --git a/examples/zizk_artifact_first/risc0/recorded-proof/self-test-results.json b/examples/zizk_artifact_first/risc0/recorded-proof/self-test-results.json new file mode 100644 index 0000000..72c3645 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/recorded-proof/self-test-results.json @@ -0,0 +1,12 @@ +{ + "real_proof_verified": true, + "unsatisfied_witness_rejected": true, + "unknown_rejected": true, + "conflicted_rejected": true, + "mutated_public_input_rejected": true, + "wrong_image_id_rejected": true, + "corrupted_proof_rejected": true, + "tampered_journal_rejected": true, + "statement_transplant_rejected": true, + "private_bytes_absent_from_public_artifacts": true +} diff --git a/examples/zizk_artifact_first/risc0/rust-toolchain.toml b/examples/zizk_artifact_first/risc0/rust-toolchain.toml new file mode 100644 index 0000000..c6096c7 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.97" +components = ["rust-src"] +profile = "minimal" diff --git a/examples/zizk_artifact_first/risc0/scripts/run_real_proof.sh b/examples/zizk_artifact_first/risc0/scripts/run_real_proof.sh new file mode 100644 index 0000000..68511f1 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/scripts/run_real_proof.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Terminology: reduced instruction set computer (RISC); RISC Zero (RISC0); Verifier Standard (VSTD). +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +MECHANISM_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)" + +export PATH="${HOME}/.risc0/bin:${HOME}/.cargo/bin:${PATH}" +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-${HOME}/.cache/vstd-zk-target}" +export RISC0_DEV_MODE=0 + +cd "${MECHANISM_DIR}" +cargo run --locked --release -p vstd-zk-host -- \ + self-test local-artifacts/self-test diff --git a/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh b/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh new file mode 100644 index 0000000..a75ab97 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Terminology: reduced instruction set computer (RISC); RISC Zero (RISC0); +# Verifier Standard (VSTD). +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +MECHANISM_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)" + +export PATH="${HOME}/.risc0/bin:${HOME}/.cargo/bin:${PATH}" +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-${HOME}/.cache/vstd-zk-target}" +export RISC0_DEV_MODE=0 + +cd "${MECHANISM_DIR}" +cargo run --locked --release -p vstd-zk-host -- \ + verify recorded-proof/receipt.msgpack recorded-proof/public.json \ + e1e9bf4f68ef60ff9af6b50e144082bc475cc20cab47e8187201153da597dcd8 diff --git a/examples/zizk_artifact_first/risc0/types/Cargo.toml b/examples/zizk_artifact_first/risc0/types/Cargo.toml new file mode 100644 index 0000000..66ec870 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/types/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "vstd-zk-types" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +serde = { version = "=1.0.228", features = ["derive"] } diff --git a/examples/zizk_artifact_first/risc0/types/src/lib.rs b/examples/zizk_artifact_first/risc0/types/src/lib.rs new file mode 100644 index 0000000..f10f4d2 --- /dev/null +++ b/examples/zizk_artifact_first/risc0/types/src/lib.rs @@ -0,0 +1,62 @@ +//! Terminology: Verifier Standard (VSTD); zero-identity/zero-knowledge (ZIZK); zero-knowledge (ZK). +//! +//! Shared, experiment-local types for the ZIZK-VSTD zero-knowledge probe. + +use serde::{Deserialize, Serialize}; + +pub const PROFILE_LABEL: &[u8] = b"ZIZK-VSTD-ZK-EXPERIMENT-0.1"; +pub const PREDICATE_TEXT: &[u8] = b"A private bounded evidence payload has a nonempty byte string of at most 64 bytes, an experiment-local SUPPORTED input tag, and a private measurement greater than or equal to the public threshold."; +pub const COMMITMENT_DOMAIN: &[u8] = b"vstd-zk-evidence-commitment-v1\0"; +pub const MAX_EVIDENCE_LEN: usize = 64; +pub const MAX_THRESHOLD: u64 = 1_000_000; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum CandidateState { + Supported, + Unknown, + Conflicted, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct PrivateWitness { + pub evidence: Vec, + pub salt: [u8; 32], + pub measurement: u64, + pub candidate_state: CandidateState, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct PublicStatement { + pub subject_digest: [u8; 32], + pub policy_digest: [u8; 32], + pub challenge: [u8; 32], + pub threshold: u64, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ProverInput { + pub statement: PublicStatement, + pub witness: PrivateWitness, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct PublicJournal { + pub profile_digest: [u8; 32], + pub predicate_digest: [u8; 32], + pub subject_digest: [u8; 32], + pub policy_digest: [u8; 32], + pub challenge: [u8; 32], + pub threshold: u64, + pub evidence_commitment: [u8; 32], + pub predicate_satisfied: bool, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct PublicEnvelope { + pub experiment_profile: String, + pub proof_system: String, + pub image_id: String, + pub receipt_sha256: String, + pub receipt_size: u64, + pub journal: PublicJournal, +} diff --git a/examples/zizk_artifact_first/zero_identity/README.md b/examples/zizk_artifact_first/zero_identity/README.md new file mode 100644 index 0000000..e0f575b --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/README.md @@ -0,0 +1,67 @@ +# Bounded identity disclosure reference evaluator + +> **Acronyms:** Verifier Standard (VSTD); zero-identity/zero-knowledge (ZIZK). + +**Status:** bounded non-normative reference mechanism. Not part of any numbered VSTD or +receipt profile, not implemented by +the `verifier` package, and not referenced by any receipt. Nothing here carries a serialized receipt +identifier, a schema `$id`, or a canonical digest. + +## The question + +Can "Zero Identity" be an operationally safe optional VSTD mode, or is the correct +mechanism something bounded — identity minimization, pseudonymity, selective disclosure? + +## The answer + +**The label is rejected for public use.** The construction it names does not remove +identity; it withholds *civil* identity while retaining a pseudonymous coordinate, a key +binding, a trust root, an issuer, and a revocation source — every one of which is an +identity coordinate and a correlation handle. Calling that "zero identity" overstates the +privacy achieved and hides the coordinates that remain. The mechanism this evaluation +retains is **bounded identity disclosure**: civil identity withheld, authorization +semantically reevaluable from public coordinates conditional on declared external checks, +and every other identity property reported honestly as `UNKNOWN`, +`CONFLICTED`, or `REFUTED` rather than assumed. + +This rejects “zero identity†as a privacy-profile claim. It does not reject VSTD's +architecture-wide zero-identity rule, which says only that identity or reputation alone +cannot strengthen an artifact-bound result. + +Full reasoning and the exact claims that are and are not justified: +[`ROUND1_ZERO_IDENTITY_REPORT.md`](ROUND1_ZERO_IDENTITY_REPORT.md). + +## Contents + +| Path | What it is | +|---|---| +| [`SEMANTIC_MODEL.md`](SEMANTIC_MODEL.md) | term separation, statuses, minimum coordinates, prohibited inferences | +| [`THREAT_MODEL.md`](THREAT_MODEL.md) | sixteen threats, mitigations, residual risk, falsification conditions | +| [`model/zero_identity_model.json`](model/zero_identity_model.json) | the machine-readable model | +| [`evaluate.py`](evaluate.py) | standard-library evaluator over one disclosure record | +| [`fixtures/`](fixtures) | positive, negative, `UNKNOWN`, and `CONFLICTED` records with expected results | +| [`tests/test_zero_identity.py`](tests/test_zero_identity.py) | validation suite, one test per blocked inference | +| [`run_validation.py`](run_validation.py) | pytest-free runner for the same fixtures | + +## Running it + +```bash +python examples/zizk_artifact_first/zero_identity/run_validation.py +python -m pytest examples/zizk_artifact_first/zero_identity/tests -q +``` + +The repository suite (`python -m pytest -q`) sets `testpaths = ["tests"]` and does not +collect this directory, which is deliberate: an optional reference mechanism must not +gate conformance. + +## Constraints observed + +- No dependency added to `verifier-standard`; the evaluator is standard library only. +- No serialized receipt identifier, schema `$id`, receipt digest, console alias, lifecycle token, + or conformance behavior is touched. See + [`../../../standard/WIRE_IDENTIFIERS.md`](../../../standard/WIRE_IDENTIFIERS.md). +- No cryptographic guarantee is invented. Signature and revocation results are fixture + inputs here. A deployment would have to produce them through a named real protocol; the + model decides only what may be concluded from the asserted results. +- `UNKNOWN` and `CONFLICTED` are preserved as results, per + [`../../../AGENTS.md`](../../../AGENTS.md) section 2. diff --git a/examples/zizk_artifact_first/zero_identity/ROUND1_ZERO_IDENTITY_REPORT.md b/examples/zizk_artifact_first/zero_identity/ROUND1_ZERO_IDENTITY_REPORT.md new file mode 100644 index 0000000..906be9b --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/ROUND1_ZERO_IDENTITY_REPORT.md @@ -0,0 +1,313 @@ +# Round 1 report: bounded identity disclosure under the zero-identity/zero-knowledge (ZIZK) Verifier Standard (VSTD) architecture + +> **Acronym:** carriage return and line feed (CRLF). + +**Status:** bounded non-normative reference result. No adoption is claimed or implied. + +Reading rule for this report: where evidence is insufficient the result is `UNKNOWN`, and +where evidence contradicts itself the result is `CONFLICTED`. Both are retained as results. +Neither is a gap to be filled, and neither may be read as authorization, independence, +uniqueness, Sybil resistance, privacy, or safety. + +## 1. Coordinates + +- Base commit: `598c545be3833d6d81bb7e252ca5837f3bb2a449` +- Branch: `claude/zizk-zero-identity` +- Worktree label: `zizk-zi-claude` (isolated; its absolute host path is intentionally + excluded from this public report; the primary checkout and separate ZIZK roadmap + worktree were not modified) +- Remote: `github.com/TimeLordRaps/verifier` +- Layer: none. This reference evaluation discharges no ladder rung. +- Seam: `examples/zizk_artifact_first/zero_identity/` only. + +## 2. Terminology decision + +**"Zero Identity" is rejected as a public label for a privacy profile.** It is retained +only as the name of the question this reference evaluation answered, never as a +description of what the profile provides. This does not reject the architecture-level +rule that identity or reputation alone cannot strengthen an artifact-bound result. + +The falsification succeeded. A profile that "removes identity" was tested against its own +required coordinates and the requirement survived: bounded reverification needs a +pseudonymous coordinate, a key identifier, a trust root, an issuer, a grant, and a +revocation source. Those are identity coordinates. What is actually removed is *civil* +identity, and removing it changes nothing about correlation, uniqueness, or independence. + +Accepted term: **bounded identity disclosure**. Where a shorter phrase is needed, +*identity minimization* is accurate and *selective disclosure* is accurate only if a real +selective-disclosure protocol is actually deployed. "Anonymous" is rejected outright: the +profile is pseudonymous, and a stable pseudonym is a correlation handle. + +## 3. Identity properties this profile supports + +| Property | Best attainable here | Basis and boundary | +|---|---|---| +| Authentication | `SUPPORTED` | semantic result over an asserted external signature check and a declared trust root; no signature is verified here | +| Authorization | `SUPPORTED` | semantic result over authentication, an asserted grant, liveness inputs, and scope coverage | +| Authority liveness | `SUPPORTED` / `REFUTED` | semantic result over asserted revocation state plus validity window against the evaluation instant | +| Freshness | `SUPPORTED` / `REFUTED` | challenge coordinate and verifier-held nonce history | +| Attribution | not separately evaluated | the record binds a pseudonymous coordinate; any real-world actor binding is `ATTESTED` at best, never inferred | +| Authorship degree | `ATTESTED` / `REFUTED` | declared role and remove, checked against the recorded delegation hops | +| Credential ancestry | `ATTESTED` / `REFUTED` | recorded chain from a declared trust root to the signing key | +| Accountability | `ATTESTED` | a declared escalation authority that can act on the coordinate | +| Uniqueness / Sybil resistance | `ATTESTED` | only with an attested mechanism; default `UNKNOWN` | +| Verifier independence | `ATTESTED` | only from named attested evidence; shared or distinct pseudonyms alone leave actor independence `UNKNOWN` | +| Recovery | `ATTESTED` | a declared credential-loss mechanism; strength not evaluated | +| Unlinkability | `ASSUMED` | never `SUPPORTED`; assumptions must be declared | +| Confidentiality | not evaluated | out of scope; any declaration remains an assumption, not an evaluator result | +| Civil identity | `UNSUPPORTED_BY_DESIGN` | withheld deliberately | + +`ACCEPTED_BOUNDED` means exactly: this key was authorized for this claim scope at this +instant. It means nothing about who the actor is, whether they are one actor, whether two +records came from independent actors, or whether the signer authored what it signed. + +Authorship degree and credential ancestry were added after the first round, on the +observation that authorization alone cannot tell a first-party claim from a relayed one. +Three questions are now kept apart: authorization asks whether this key was permitted this +scope; authorship degree asks who is speaking and at what remove; credential ancestry asks +how the key came to hold the authority. A record can be fully authorized with `UNKNOWN` +authorship, and that pairing is reported rather than merged. Neither new property can ever +reach `SUPPORTED`: both are assertions about the world outside the record, so `ATTESTED` is +their ceiling. + +### 3.1 Evidence classes, kept separate + +The four classes below are never merged, and no verdict promotes one into another. A +reader who collapses them recovers exactly the overclaim this reference evaluation exists to block. + +| Class | What it means | Handling in this reference evaluator | Ceiling in this model | +|---|---|---|---| +| Semantic result | decided by the stated rules from coordinates present in the record | any reader running `evaluate.py` on the record | `SUPPORTED`, `REFUTED`, `UNKNOWN`, `CONFLICTED` | +| External attestation | a named third party asserts a fact this model records but does not check | a deployment may authenticate it under an external protocol; this evaluator does neither that nor truth validation | `ATTESTED` | +| Declared assumption | the record states a condition it needs and cannot demonstrate | carried unchanged and never established by this record | `ASSUMED` | +| Protocol guarantee | whatever an actual named cryptographic protocol provides | absent here; it would be checked under that protocol outside this evaluator | not represented; enters only as an input | + +Concretely: `authentication` is a semantic result *about an asserted signature check*, not +a cryptographic guarantee — this model never verifies a signature. `uniqueness`, +`verifier_independence`, `authorship_degree`, and `credential_ancestry` are attestations at +their ceiling. `unlinkability` is an assumption at its ceiling; `confidentiality` is not an +evaluator output at all. No protocol guarantee is claimed anywhere, because no protocol is +bound yet. + +## 4. Prohibited inferences + +Each is encoded in `model/zero_identity_model.json` and guarded by at least one test: + +1. absent civil identity implies anonymity; +2. absent civil identity implies unlinkability; +3. a pseudonym implies a distinct actor; +4. a shared pseudonym implies a single actor; +5. two distinct pseudonyms imply two independent actors; +6. a verified signature implies authorization; +7. a grant implies currently active authority; +8. absent revocation evidence implies active authority; +9. absent uniqueness evidence implies Sybil resistance; +10. hashing, redaction, encryption, omission, or pseudonymity alone implies zero identity; +11. disclosure minimization preserves the original claim boundary; +12. missing evidence implies safety; +13. a signer is the author of the claim; +14. a relayed, delegated, or aggregated claim is first-party authorship; +15. an absent authorship role means degree zero; +16. a recorded ancestry chain establishes that authority survived every hop; +17. no ancestor marked revoked means every ancestor is valid; +18. a rotation link merges two key coordinates into one actor; +19. a delegation may carry a scope its ancestor did not hold. + +Inferences 16 and 17 are the credential-side form of the recorded-lineage discipline +already normative in `standard/VSTD-Graph-1.md`, which states that an edge records ancestry +without establishing influence, and that no ancestor being marked revoked does not +establish that every ancestor is valid. + +## 5. Trust roots and revocation dependencies + +The profile does not reduce trust-root dependence; it makes it explicit. A reader who +accepts an `ACCEPTED_BOUNDED` verdict is accepting, at minimum: + +- the issuer named in `authorization.issuer`; +- the trust root named in `actor.key_binding.trust_root`; +- the revocation service named in `revocation.source`, as of `revocation.checked_at`; +- whatever protocol produced `signature_verified`, which this model does not check; +- every attestor named in the recorded credential ancestry, one per link. + +Recorded ancestry increases the number of parties a reader depends on rather than reducing +it, and the report states that plainly: each delegation hop adds an attestor whose honesty +is assumed. A chain is refused when an ancestor is recorded as revoked or when a delegation +carries a scope its ancestor never held; it stays `UNKNOWN` when any link is unattested, +when it does not begin at a declared trust root, or when it does not terminate at the +signing key. A truncated chain therefore cannot be laundered into a clean one without also +declaring the shorter root as trusted; the model cannot establish whether that declaration +is honest. + +Revocation is a liveness dependency with a staleness bound, not a one-time check. A +record whose revocation source is absent is `UNKNOWN`; a record whose minimization request +deleted that source is `REJECTED` as unevaluable. Minimization is enforced by deletion +before evaluation, so a withheld coordinate cannot be silently read anyway. + +## 6. Privacy and correlation leak analysis + +Retained and observable in every `ACCEPTED_BOUNDED` record: the pseudonymous coordinate, the key +identifier, the trust root, the issuer, the scope name, the validity window, the +evaluation instant, and the revocation source. Any two of these are joinable across +records. Publication timing and volume are not addressed at all. + +Recorded credential ancestry makes this strictly worse, and the trade is deliberate. Every +link publishes a parent coordinate, a child coordinate, a link type, and an attestor, so a +chain is a durable join key across every record that carries it: two records sharing one +delegation hop are linkable even when their pseudonyms differ, and a rotation link is an +explicit statement that two key coordinates are related. Authorship provenance and +unlinkability are therefore in direct tension. This reference evaluation resolves the tension toward +provenance and reports the cost rather than claiming both. + +Consequence: an observer who sees two records under one pseudonym learns they share an +actor coordinate, not that they share one natural person. An observer who sees two records +under one issuer learns that they name the same issuer, not necessarily the same trust +root. Withholding civil identity does not remove either correlation handle. Coercion risk +is not removed either — it may move to an issuer that holds a civil binding. This is a +displacement of risk, not a demonstrated reduction, and the reference evaluation reports it as such. + +## 7. Test results + +All required checks pass at the committed state. **Failed tests: none.** No assertion was +weakened, skipped, or marked expected-failure to reach this state. + +| Check | Result | +|---|---| +| `python examples/zizk_artifact_first/zero_identity/run_validation.py` | 22 fixtures, 0 failures | +| `python -m pytest examples/zizk_artifact_first/zero_identity/tests -q` | 65 passed | +| `python -m pytest -q` (repository suite) | 255 passed, 3 skipped | +| `python scripts/check_presentation.py` | passes | + +The repository suite sets `testpaths = ["tests"]` and does not collect this directory. That +is deliberate: a non-normative reference evaluator must not gate conformance. The 3 skips are pre-existing and +unrelated to this work. On a machine where another checkout of the package is installed, +the repository suite needs the `PYTHONPATH=src` prefix described in `AGENTS.md` section 3; +that is an environment condition, not a repository defect. + +### 7.1 Diff inspection + +The complete diff against the base is confined to `examples/zizk_artifact_first/zero_identity/`: +30 files, 3734 added lines, **zero files changed outside that directory**. A pattern scan +over every added line reports: + +| Category | Findings | +|---|---| +| Private filesystem paths | none | +| Private model identifiers | none | +| Credentials or secrets | none | +| Email addresses | none | +| Business plans | none | +| Unsupported adoption claims | none | +| Unsupported privacy or anonymity claims | none in assertion position | +| Recorded ancestry described as causal | none | +| CRLF line endings | none | + +Literal pattern hits were adjudicated and retained deliberately, because each occurs +in negating or guarding position rather than as a claim: the word *untraceable* appears +only in section 10 as a prohibited claim; the four serialized receipt identifiers appear only in a +test asserting that no fixture may bind one; and `$id` appears only in prose stating that +none is introduced. + +### 7.2 Non-regression of frozen surfaces + +Verified directly against the base commit, not assumed: + +- `pyproject.toml` is byte-unchanged, and `dependencies = []` still holds. The evaluator + imports only `copy`, `dataclasses`, `json`, `pathlib`, and `typing`; `pytest` appears + only in the reference evaluator's own tests, which the repository suite does not collect. +- Zero files changed under `standard/`, `receipts/schema/`, `src/`, `examples/`, or + `scripts/`. No serialized receipt identifier, schema `$id`, receipt digest, console alias, or + lifecycle token is added, renamed, or rebound. +- The stdlib-purity smoke check (`python -S -c "import verifier; ..."`) reports `1.1.3`. +- Existing conformance behavior is untouched: this reference evaluation adds no code path that any + shipped module imports. + +Fixture coverage, one per required case: + +| Fixture | Verdict | +|---|---| +| `positive_bounded_authorization` | `ACCEPTED_BOUNDED` | +| `positive_minimized_boundary_narrowed` | `ACCEPTED_BOUNDED` | +| `unknown_missing_authorization` | `UNKNOWN` | +| `unknown_distinct_pseudonyms` | `UNKNOWN` | +| `unknown_uniqueness_absent` | `UNKNOWN` | +| `conflicted_identity_evidence` | `CONFLICTED` | +| `rejected_revoked_authority` | `REJECTED` | +| `rejected_expired_authority` | `REJECTED` | +| `unknown_shared_pseudonym_independence` | `UNKNOWN` | +| `rejected_unlinkability_erases_trust_root` | `REJECTED` | +| `rejected_replayed_challenge` | `REJECTED` | +| `rejected_missing_challenge` | `REJECTED` | +| `rejected_minimization_widens_boundary` | `REJECTED` | +| `rejected_minimization_erases_key_binding` | `REJECTED` | +| `rejected_key_compromise` | `REJECTED` | +| `unknown_absent_authorship` | `UNKNOWN` | +| `unknown_unattested_ancestry_link` | `UNKNOWN` | +| `unknown_unattested_rotation` | `UNKNOWN` | +| `conflicted_authorship_degree_vs_chain` | `CONFLICTED` | +| `rejected_relay_claims_origination` | `REJECTED` | +| `rejected_revoked_ancestor` | `REJECTED` | +| `rejected_delegation_widens_scope` | `REJECTED` | + +No final test failed. No assertion was weakened to obtain a green suite. Validation instead +closed two fail-open surfaces: a minimizer cannot evade a protected leaf by deleting its +parent object, and a shared pseudonym no longer becomes a claim about how many actors use +that credential. + +## 8. Unresolved assumptions + +1. `signature_verified` and `revocation.state` are consumed as asserted evidence. No + protocol is bound yet, so no protocol's assumptions have been inherited or checked. +2. Attestation quality is unmodelled. `ATTESTED` records that someone said so. This now + carries more weight than it did in the first round, because every ancestry link and + every authorship role rests on it. +3. An internally consistent but dishonest authorship role is undetectable from the record. + The model catches a relay that contradicts its own chain; it cannot catch a relay that + lies consistently. +4. Chain truncation before publication is only partially addressed. A chain that does not + reach a declared trust root stays `UNKNOWN`, but a chain trimmed to a plausible shorter + root is not distinguishable from an honest short chain. +5. Rotation is treated conservatively in one direction only: an unattested rotation does + not merge two coordinates. An actor rotating keys to shed a history is not detected. +6. Nonce history is verifier-held state that this model does not carry; replay detection + is only as good as that history. +7. No selective-disclosure or unlinkable-presentation scheme has been selected. Until one + is named, `unlinkability` stays `ASSUMED` at best. +8. Timing and volume side channels are out of scope and unmitigated. +9. Whether an issuer that grants many coordinates to one operator can be detected at all + from published records is open, and probably not decidable within one record. +10. Whether this profile should ever become normative is not decided here. Nothing in this + round argues that it should. + +## 9. Public claims currently justified + +- "Civil identity can be withheld while the evaluator can recompute a bounded + authorization result from public coordinates, conditional on asserted external checks + and declared trust roots." +- "Missing identity evidence yields `UNKNOWN`; conflicting identity evidence yields + `CONFLICTED`; revoked or expired authority yields a refutation." +- "The reference evaluation enumerates the identity coordinates that remain, rather than implying + none remain." +- "Authorship degree and credential ancestry are recorded and checked for internal + consistency; a relayed claim cannot be read as first-party authorship, and a chain from a + revoked ancestor is refused." +- "A recorded ancestry chain is recorded ancestry, not proof that authority survived every + hop." +- "The reference evaluation adds no required package dependency and the complete base-to-branch diff + does not modify a serialized receipt identifier or conformance implementation." + +## 10. Public claims still prohibited + +- "VSTD supports a zero-identity privacy mode", or any privacy claim using "zero identity" without the qualification + that civil identity alone is withheld. +- "Anonymous", "untraceable", "uncorrelatable", or "privacy-preserving" as unqualified + descriptions of this profile. +- Any claim that hashing, redaction, encryption, omission, or a pseudonym provides + unlinkability. +- Any claim of Sybil resistance, actor uniqueness, or verifier independence that is not + backed by named attested evidence. +- Any claim that a zero-knowledge proof system is used, implemented, or relied upon. None + is present in this reference evaluation. +- "Provenance is verified", or any phrasing that reads recorded ancestry as established + authority, established influence, or a verified chain of custody. +- Any claim that authorship is proven. Authorship degree is `ATTESTED` at its ceiling. +- Any statement that this profile is production-ready, adopted, reviewed, or standardised. diff --git a/examples/zizk_artifact_first/zero_identity/SEMANTIC_MODEL.md b/examples/zizk_artifact_first/zero_identity/SEMANTIC_MODEL.md new file mode 100644 index 0000000..9c3535f --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/SEMANTIC_MODEL.md @@ -0,0 +1,124 @@ +# Semantic model: bounded identity disclosure + +> **Acronym:** Verifier Standard (VSTD). + +**Status:** bounded non-normative reference model. No serialized receipt identifier, schema route, or receipt digest. + +This document defines what the evaluator means by each identity-adjacent term, which +properties a record can support, and which inferences are prohibited. The executable +form is [`model/zero_identity_model.json`](model/zero_identity_model.json) and +[`evaluate.py`](evaluate.py); where prose and code disagree, the code plus its fixtures +are the artifact under test and this document is the defect. + +## 1. Separated terms + +These are distinct properties. None implies another. + +| Term | Meaning here | Profile position | +|---|---|---| +| Civil or legal identity | a natural or legal person recognised by a jurisdiction | withheld; `UNSUPPORTED_BY_DESIGN` | +| Persistent public identity | a durable public name reused across contexts | out of scope; the profile uses a pseudonymous coordinate instead | +| Key or credential coordinate | `key_id`, its trust root, and the grant that references it | required | +| Authentication | evidence that a given key signed the record | evaluable, may be `SUPPORTED` | +| Authorization | evidence that the signer was permitted this claim scope | evaluable, may be `SUPPORTED` | +| Accountability | a named authority that can act on the pseudonymous coordinate | at best `ATTESTED` | +| Attribution | binding a record to a pseudonymous coordinate, never to a person | at best `ATTESTED` | +| Authorship degree | how far the signing party sits from the origin of the claim: originator, delegate, relay, aggregator | at best `ATTESTED`, default `UNKNOWN` | +| Credential ancestry | the recorded chain of issuance, delegation, and rotation links from a trust root to the signing key | at best `ATTESTED`, refutable | +| Uniqueness / Sybil resistance | evidence that one coordinate corresponds to one actor | at best `ATTESTED`, default `UNKNOWN` | +| Verifier independence | evidence that two receipts came from actors that do not share a root | at best `ATTESTED`, refutable | +| Revocation and expiry | current liveness of a grant | evaluable, refutable | +| Confidentiality | protection of the record in transit and at rest | out of scope, at best `ASSUMED` | +| Unlinkability | inability of an observer to join two records to one actor | never `SUPPORTED`, at best `ASSUMED` | +| Anonymity / pseudonymity | absence of any actor coordinate versus a stable non-civil one | the profile is pseudonymous, never anonymous | + +## 2. Statuses + +`SUPPORTED` — decided from coordinates present in the record under stated rules. +`ATTESTED` — an external party asserts it; the assertion is recorded, not checked here. +`ASSUMED` — declared by the record as an assumption, carried forward as an assumption. +`UNKNOWN` — the coordinate needed to decide is absent. This is a result, not a gap to fill. +`CONFLICTED` — two retained pieces of evidence disagree. Terminal; never resolved by preference. +`REFUTED` — a positive negative result: the property is contradicted by evidence. +`UNSUPPORTED_BY_DESIGN` — the profile deliberately withholds the coordinate. + +Record verdicts are `ACCEPTED_BOUNDED`, `UNKNOWN`, `CONFLICTED`, and `REJECTED`. They are +aggregated without erasing per-property uncertainty: any `REFUTED` property makes the +record `REJECTED`; otherwise any `CONFLICTED` property makes it `CONFLICTED`. +`ACCEPTED_BOUNDED` requires `SUPPORTED` authentication and authorization plus satisfaction +of every explicitly claimed property. An `UNKNOWN` ancillary property remains visible but +does not widen or erase that bounded authorization result. Every other record is `UNKNOWN`. +`ACCEPTED_BOUNDED` therefore asserts exactly one thing: authentication and authorization +hold for the declared claim scope at the declared instant. It asserts nothing about +uniqueness, independence, unlinkability, or the actor behind the coordinate. + +## 3. Minimum public actor coordinates + +Bounded authorization reverification without civil identity needs all of: + +- `actor.pseudonym` — the coordinate a verdict attaches to; +- `actor.key_binding.key_id`, `.signature_verified`, `.trust_root`; +- `authorization.grant_id`, `.issuer`, `.scope`, `.not_before`, `.not_after`; +- `revocation.source`, `.state`, `.checked_at`; +- `trust_roots` — the roots the reader must already accept. + +The provenance extension may additionally disclose: + +- `authorship.role`, `.degree`, `.attested_by` — the asserted author role and remove; +- `credential_ancestry[].parent`, `.child`, `.link_type`, `.attested_by` — the recorded path + by which the signing key obtained its authority. + +Authorship degree and credential ancestry are distinct from authorization. Authorization +asks whether this key was permitted this scope; authorship asks who is speaking and at what +remove; ancestry asks how the key came to hold the authority at all. A record can be fully +authorized while its authorship is `UNKNOWN`, and that combination is reported, not merged. + +Remove a required coordinate from an ordinary record and the dependent property becomes +`UNKNOWN`. Remove a required coordinate under a minimization request — whether by naming +the leaf or a parent path — and the record is `REJECTED` as unevaluable. Minimization is +enforced, not trusted: `evaluate.py` checks the requested paths and then deletes every +withheld coordinate before evaluating, so a coordinate an actor asked to withhold cannot +quietly still be read. + +## 4. Prohibited inferences + +Encoded in the model and each guarded by a test: + +1. Absent civil identity implies anonymity or unlinkability. +2. A pseudonym implies a distinct actor. +3. A shared pseudonym implies a single actor. +4. Two distinct pseudonyms imply two independent actors. +5. A verified signature implies authorization. +6. A grant implies that the authority is currently active. +7. Absent revocation evidence implies active authority. +8. Absent uniqueness evidence implies Sybil resistance. +9. Hashing, redaction, encryption, omission, or pseudonymity alone implies zero identity. +10. Disclosure minimization preserves the original claim boundary. +11. Missing evidence implies safety. +12. A signer is the author of the claim. +13. A relayed, delegated, or aggregated claim is first-party authorship. +14. An absent authorship role means degree zero. +15. A recorded ancestry chain establishes that authority survived every hop. +16. No ancestor marked revoked means every ancestor is valid. +17. A rotation link merges two key coordinates into one actor. +18. A delegation may carry a scope its ancestor did not hold. + +Inferences 15 and 16 mirror the recorded-lineage discipline of +[`../../../standard/VSTD-Graph-1.md`](../../../standard/VSTD-Graph-1.md): an edge records +ancestry, and a clean-ancestor policy must require validity explicitly rather than reading +it out of the absence of a revocation mark. + +## 5. Relationship to cryptography + +This model contains no cryptographic construction and asserts no cryptographic guarantee. +`signature_verified`, `state`, and any proof result are *inputs*: a deployment obtains them +from a real protocol and the model decides what may be concluded from them. If a +deployment wants selective disclosure or unlinkable presentation, it must name the actual +scheme it uses, state that scheme's assumptions, and record the outcome as evidence here. +Nothing in this reference evaluator substitutes for that. + +## 6. Relationship to VSTD + +Nothing here changes a serialized receipt identifier, a schema `$id`, a console alias, a lifecycle +token, or any conformance behavior. See [`../../../standard/WIRE_IDENTIFIERS.md`](../../../standard/WIRE_IDENTIFIERS.md). +The profile adds no dependency: `evaluate.py` is standard library only. diff --git a/examples/zizk_artifact_first/zero_identity/THREAT_MODEL.md b/examples/zizk_artifact_first/zero_identity/THREAT_MODEL.md new file mode 100644 index 0000000..207e6ba --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/THREAT_MODEL.md @@ -0,0 +1,53 @@ +# Threat model: bounded identity disclosure + +**Status:** bounded non-normative reference threat model. + +Scope: one bounded disclosure record and the conclusions a reader may draw from it. +Out of scope: transport security, storage security, the correctness of any cryptographic +protocol, and the honesty of an issuer's internal process. + +The adversary is assumed to be able to read every published record, to submit records of +their own, to create as many pseudonymous coordinates as an issuer will grant, and to +observe timing and volume of publication. The adversary is not assumed to break signature +schemes; where a key fails, it fails by compromise or misuse, not by cryptanalysis. + +| # | Threat | What the model does | Residual risk | +|---|---|---|---| +| T1 | Correlation across receipts | Omits a civil-identity field; `unlinkability` is never `SUPPORTED`, at best `ASSUMED` under declared assumptions | Real. Remaining coordinates or side information may resolve to civil identity. A stable pseudonym, key, issuer, and publication timing are all joinable | +| T2 | Replay | When `freshness.required` is set, an absent challenge fails closed and a previously observed challenge is `REFUTED` | A verifier that never requires freshness gets `UNKNOWN`, which is honest but not protective. Nonce history must be kept by the verifier | +| T3 | Key compromise | `key_compromised_during_interval` refutes authentication and therefore authorization | The model learns of compromise only when someone reports it. Silent compromise is indistinguishable from normal signing | +| T4 | Revoked or expired authority | Revocation state `revoked`, or an evaluation instant outside the validity window, is `REFUTED`, never `UNKNOWN`; a missing revocation source is `UNKNOWN`, never active | Revocation freshness is bounded by `revocation.checked_at`; the model does not fetch status | +| T5 | One actor presenting as many independent actors | Independence requires attested evidence with distinct trust roots; distinct pseudonyms alone leave it `UNKNOWN` | An issuer that grants many credentials to one operator can produce evidence that looks distinct. Independence is `ATTESTED` at best, never proven here | +| T6 | Many actors sharing one credential | A shared pseudonymous coordinate cannot supply independent corroboration, but actor independence and `uniqueness` stay `UNKNOWN` | The model cannot detect sharing from a single record. Attribution binds a coordinate, never a person | +| T7 | Coerced identity disclosure | The profile omits a civil-identity field and explicitly retains the remaining correlation coordinates | Side information may still identify an actor. Coercion also moves to the issuer, which may hold a civil binding. This displaces risk rather than removing it | +| T8 | Metadata and timing leakage | Not mitigated. Declared as out of scope and reported as such | Publication time, volume, scope names, and issuer choice remain observable | +| T9 | Colluding issuers or verifiers | Trust roots must be declared explicitly, so a reader can see that two records share one root | Collusion between a declared issuer and a declared verifier defeats the profile. The model surfaces the shared root; it cannot rule collusion out | +| T10 | Unverifiable claims of independence | `verifier_independence` never becomes `SUPPORTED`; a claim of it that lacks evidence downgrades the record verdict to `UNKNOWN` | Attestation quality is outside the model | +| T11 | Missing authorization | A record with no grant is `UNKNOWN`; it never fails open | A verifier that treats `UNKNOWN` as permission defeats this. The verdict is honest; the deployment must respect it | +| T12 | Recovery after credential loss | `recovery` is `ATTESTED` only when a mechanism is declared, otherwise `UNKNOWN` | Any recovery path is also an impersonation path. The model records that a path exists; it does not evaluate its strength | +| T13 | Authorship inflation: a relay or aggregator presenting a claim as its own | Role and degree are asserted and checked for internal consistency; a non-originator that claims origination is `REFUTED`; an absent role stays `UNKNOWN` | The role itself is an assertion about the world. A dishonest originator claim that is internally consistent is not detectable from the record | +| T14 | Delegation laundering: manufacturing authority the issuer never granted | A delegation whose scope exceeds its ancestor scope is `REFUTED`; a chain from a revoked ancestor is `REFUTED`; an unattested link stays `UNKNOWN` | Ancestor state is as fresh as the evidence supplied. A chain can be truncated before publication, which is why a chain that misses a declared trust root stays `UNKNOWN` | +| T15 | Identity merge through key rotation | An unattested rotation leaves the chain `UNKNOWN`; two key coordinates are not merged into one actor without attestation | The inverse also holds and is unaddressed: an actor can rotate to escape a reputation history, which this model cannot detect | +| T16 | Privacy laundering through minimization | A minimization request that removes a required trust root makes the record `REJECTED`; a request that widens the claim boundary is `REJECTED` | An actor can still choose to publish less and accept a weaker verdict, which is the intended trade | + +## Falsification conditions + +This reference mechanism is refuted if any of the following can be demonstrated: + +- a record reaches `ACCEPTED_BOUNDED` while any property is `REFUTED`; +- a `CONFLICTED` property is resolved to a favourable status by adding no new evidence; +- `unlinkability`, `authorship_degree`, or `credential_ancestry` reaches `SUPPORTED`; +- a non-originator role is read as first-party authorship; +- a chain containing a revoked ancestor evaluates as anything other than a refutation; +- absence of a required evidence coordinate produces a favourable property result; +- a minimization request removes a required public coordinate, directly or through a + parent path, and the record still evaluates as anything other than `REJECTED`. + +These conditions are asserted in [`tests/test_zero_identity.py`](tests/test_zero_identity.py), +including leaf-path and parent-path minimization fixtures. + +## What this threat model does not claim + +It does not claim that the profile provides anonymity, that it defeats correlation, or +that it is safe to deploy. It claims only that the evaluator refuses to convert missing +identity information into a favourable conclusion. diff --git a/examples/zizk_artifact_first/zero_identity/evaluate.py b/examples/zizk_artifact_first/zero_identity/evaluate.py new file mode 100644 index 0000000..61684a7 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/evaluate.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python3 +"""Terminology: Verifier Standard (VSTD); zero-identity/zero-knowledge (ZIZK). + +Bounded reference evaluator for identity disclosure under the ZIZK-VSTD architecture. + +Discharges no VSTD closure coordinate. This module is non-normative scaffolding for +the terminology and safety question recorded in ``SEMANTIC_MODEL.md``: it decides +which identity-adjacent properties a bounded disclosure record can support, and it +fails closed everywhere else. + +The evaluator never verifies a signature, a revocation list, or a proof. It consumes +*asserted* evidence coordinates and decides what may be concluded from them. Any +cryptographic verification happens outside this module and enters here as evidence. +""" + +from __future__ import annotations + +import copy +from dataclasses import dataclass, field +import json +from pathlib import Path +from typing import Any + +MODEL_FILE = Path(__file__).resolve().parent / "model" / "zero_identity_model.json" + +SUPPORTED = "SUPPORTED" +ATTESTED = "ATTESTED" +ASSUMED = "ASSUMED" +UNKNOWN = "UNKNOWN" +CONFLICTED = "CONFLICTED" +REFUTED = "REFUTED" +UNSUPPORTED_BY_DESIGN = "UNSUPPORTED_BY_DESIGN" + +ACCEPTED_BOUNDED = "ACCEPTED_BOUNDED" +REJECTED = "REJECTED" + +REQUIRED_PUBLIC_COORDINATES = ( + "trust_roots", + "actor.pseudonym", + "actor.key_binding.key_id", + "actor.key_binding.trust_root", + "authorization.issuer", + "revocation.source", +) + + +def load_model() -> dict[str, Any]: + """Return the bounded non-normative machine-readable model.""" + + return json.loads(MODEL_FILE.read_text(encoding="utf-8")) + + +@dataclass(frozen=True) +class Evaluation: + """Result of evaluating one bounded disclosure record.""" + + verdict: str + properties: dict[str, str] + reasons: list[str] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return { + "verdict": self.verdict, + "properties": dict(self.properties), + "reasons": list(self.reasons), + } + + +def _get(record: dict[str, Any], dotted: str) -> Any: + node: Any = record + for part in dotted.split("."): + if not isinstance(node, dict) or part not in node: + return None + node = node[part] + return node + + +def _conflicted(record: dict[str, Any], prop: str) -> bool: + for entry in record.get("conflicts", []) or []: + if entry.get("property") == prop: + return True + return False + + +def _evaluate_civil_identity(record: dict[str, Any], reasons: list[str]) -> str: + if _conflicted(record, "civil_identity"): + reasons.append("civil_identity: conflicting evidence retained as CONFLICTED") + return CONFLICTED + disclosed = _get(record, "actor.civil_identity") + if disclosed not in (None, "withheld"): + reasons.append("civil_identity: a disclosed value is outside this profile") + return CONFLICTED + reasons.append( + "civil_identity: withheld by profile; absence is neither anonymity nor unlinkability" + ) + return UNSUPPORTED_BY_DESIGN + + +def _evaluate_authentication(record: dict[str, Any], reasons: list[str]) -> str: + if _conflicted(record, "authentication"): + reasons.append("authentication: conflicting evidence retained as CONFLICTED") + return CONFLICTED + binding = _get(record, "actor.key_binding") + if not isinstance(binding, dict): + reasons.append("authentication: no key binding coordinate") + return UNKNOWN + if not _get(record, "actor.pseudonym"): + reasons.append("authentication: no pseudonymous actor coordinate") + return UNKNOWN + if not binding.get("key_id"): + reasons.append("authentication: no signing-key coordinate") + return UNKNOWN + if binding.get("key_compromised_during_interval") is True: + reasons.append("authentication: signing key reported compromised for the interval") + return REFUTED + verified = binding.get("signature_verified") + if verified is False: + reasons.append("authentication: asserted signature verification failed") + return REFUTED + if verified is not True: + reasons.append("authentication: signature verification result absent") + return UNKNOWN + root = binding.get("trust_root") + if root not in (record.get("trust_roots") or []): + reasons.append("authentication: key trust root is not among the declared trust roots") + return UNKNOWN + return SUPPORTED + + +def _evaluate_authority_active(record: dict[str, Any], reasons: list[str]) -> str: + if _conflicted(record, "authority_active"): + reasons.append("authority_active: conflicting evidence retained as CONFLICTED") + return CONFLICTED + grant = record.get("authorization") + revocation = record.get("revocation") + if not isinstance(grant, dict): + reasons.append("authority_active: no authorization grant to evaluate") + return UNKNOWN + if not isinstance(revocation, dict) or not revocation.get("source"): + reasons.append("authority_active: no revocation source; absence is not liveness") + return UNKNOWN + state = revocation.get("state") + if state == "revoked": + reasons.append("authority_active: authority is revoked") + return REFUTED + if state != "active": + reasons.append("authority_active: revocation state is not asserted active") + return UNKNOWN + evaluated_at = record.get("evaluated_at") + not_before = grant.get("not_before") + not_after = grant.get("not_after") + if not (evaluated_at and not_before and not_after): + reasons.append("authority_active: validity window or evaluation instant absent") + return UNKNOWN + if not (not_before <= evaluated_at <= not_after): + reasons.append("authority_active: evaluation instant is outside the validity window") + return REFUTED + if not revocation.get("checked_at"): + reasons.append("authority_active: revocation check instant absent") + return UNKNOWN + return SUPPORTED + + +def _evaluate_authorization( + record: dict[str, Any], authentication: str, authority: str, reasons: list[str] +) -> str: + if _conflicted(record, "authorization"): + reasons.append("authorization: conflicting evidence retained as CONFLICTED") + return CONFLICTED + grant = record.get("authorization") + if not isinstance(grant, dict) or not grant.get("grant_id"): + reasons.append("authorization: no grant coordinate; missing authorization stays UNKNOWN") + return UNKNOWN + issuer = grant.get("issuer") + if not issuer: + reasons.append("authorization: no issuer coordinate") + return UNKNOWN + if issuer not in (record.get("trust_roots") or []): + reasons.append("authorization: issuer is not among the declared trust roots") + return UNKNOWN + if authority == REFUTED: + reasons.append("authorization: refuted because the authority is not active") + return REFUTED + if authentication == REFUTED: + reasons.append("authorization: refuted because authentication is refuted") + return REFUTED + if authentication != SUPPORTED or authority != SUPPORTED: + reasons.append("authorization: preconditions are not both SUPPORTED") + return UNKNOWN + scope = grant.get("scope") or [] + claim_scope = record.get("claim_scope") + if not claim_scope: + reasons.append("authorization: record declares no claim scope to cover") + return UNKNOWN + if claim_scope not in scope: + reasons.append("authorization: grant scope does not cover the claim scope") + return REFUTED + return SUPPORTED + + +def _evaluate_freshness(record: dict[str, Any], reasons: list[str]) -> str: + freshness = record.get("freshness") or {} + if not freshness.get("required"): + reasons.append("freshness: not required by this record; replay is not excluded") + return UNKNOWN + nonce = freshness.get("nonce") + if not nonce or not freshness.get("challenge_source"): + reasons.append("freshness: required but the challenge coordinate is absent; fails closed") + return REFUTED + if nonce in (freshness.get("previously_observed_nonces") or []): + reasons.append("freshness: challenge value was previously observed; replay detected") + return REFUTED + return SUPPORTED + + +def _evaluate_uniqueness(record: dict[str, Any], reasons: list[str]) -> str: + if _conflicted(record, "uniqueness"): + reasons.append("uniqueness: conflicting evidence retained as CONFLICTED") + return CONFLICTED + evidence = record.get("uniqueness_evidence") or [] + if not [entry for entry in evidence if entry.get("attested_by")]: + reasons.append( + "uniqueness: no attested mechanism; absence does not imply Sybil resistance" + ) + return UNKNOWN + return ATTESTED + + +def _evaluate_independence(record: dict[str, Any], reasons: list[str]) -> str: + if _conflicted(record, "verifier_independence"): + reasons.append("verifier_independence: conflicting evidence retained as CONFLICTED") + return CONFLICTED + peers = record.get("peer_receipts") or [] + if not peers: + reasons.append("verifier_independence: no peer receipt to compare; independence UNKNOWN") + return UNKNOWN + own = _get(record, "actor.pseudonym") + for peer in peers: + if peer.get("pseudonym") == own: + reasons.append( + "verifier_independence: peer shares this pseudonymous coordinate; the " + "coordinate cannot supply independent corroboration, but credential sharing " + "means actor independence remains UNKNOWN" + ) + return UNKNOWN + evidence = record.get("independence_evidence") or [] + attested = [ + entry + for entry in evidence + if entry.get("attested_by") and entry.get("distinct_trust_root") + ] + if not attested: + reasons.append( + "verifier_independence: distinct pseudonyms are not evidence of distinct actors" + ) + return UNKNOWN + return ATTESTED + + +AUTHORSHIP_ROLES = ("ORIGINATOR", "DELEGATE", "RELAY", "AGGREGATOR") + + +def _evaluate_authorship_degree(record: dict[str, Any], reasons: list[str]) -> str: + """Decide how far the signing party sits from the origin of the claim. + + Degree is asserted, never inferred. An absent role does not default to + ORIGINATOR, and a relay is never readable as first-party authorship. + """ + + if _conflicted(record, "authorship_degree"): + reasons.append("authorship_degree: conflicting evidence retained as CONFLICTED") + return CONFLICTED + authorship = record.get("authorship") + if not isinstance(authorship, dict): + reasons.append( + "authorship_degree: no authorship coordinate; a signer is not assumed to be an author" + ) + return UNKNOWN + role = authorship.get("role") + degree = authorship.get("degree") + if role not in AUTHORSHIP_ROLES or type(degree) is not int or degree < 0: + reasons.append("authorship_degree: role or degree absent or unrecognised") + return UNKNOWN + if (role == "ORIGINATOR") != (degree == 0): + reasons.append("authorship_degree: declared role and declared degree disagree") + return CONFLICTED + chain = record.get("credential_ancestry") or [] + delegations = [link for link in chain if link.get("link_type") == "delegation"] + if chain and degree != len(delegations): + reasons.append( + "authorship_degree: declared degree disagrees with the number of recorded " + "delegation hops" + ) + return CONFLICTED + if role != "ORIGINATOR" and "authorship_origination" in ( + record.get("claimed_properties") or [] + ): + reasons.append( + f"authorship_degree: a {role} record claims origination; relayed authorship " + "is not first-party authorship" + ) + return REFUTED + if not authorship.get("attested_by"): + reasons.append("authorship_degree: role is declared but not attested") + return UNKNOWN + return ATTESTED + + +def _evaluate_credential_ancestry(record: dict[str, Any], reasons: list[str]) -> str: + """Decide what the recorded chain from a trust root to this credential supports. + + The chain records ancestry; it does not by itself establish that authority + survived every hop. An unattested link stays UNKNOWN, and a revoked ancestor + refutes the chain rather than leaving it merely uncertain. + """ + + if _conflicted(record, "credential_ancestry"): + reasons.append("credential_ancestry: conflicting evidence retained as CONFLICTED") + return CONFLICTED + chain = record.get("credential_ancestry") + if not chain: + reasons.append( + "credential_ancestry: no recorded chain; an authority origin is not assumed" + ) + return UNKNOWN + for link in chain: + if link.get("parent_state") == "revoked": + reasons.append( + "credential_ancestry: a recorded ancestor is revoked; authority does not " + "survive delegation from a revoked ancestor" + ) + return REFUTED + parent_scope = link.get("parent_scope") + child_scope = link.get("child_scope") + if parent_scope is not None and child_scope is not None: + if not set(child_scope) <= set(parent_scope): + reasons.append( + "credential_ancestry: a delegation widens scope beyond its ancestor" + ) + return REFUTED + if not all(link.get("attested_by") for link in chain): + reasons.append( + "credential_ancestry: a recorded link is unattested; an unattested chain is " + "not a verified chain" + ) + return UNKNOWN + if chain[0].get("parent") not in (record.get("trust_roots") or []): + reasons.append( + "credential_ancestry: the chain does not begin at a declared trust root" + ) + return UNKNOWN + for older, newer in zip(chain, chain[1:]): + if older.get("child") != newer.get("parent"): + reasons.append("credential_ancestry: the recorded chain is not contiguous") + return CONFLICTED + if chain[-1].get("child") != _get(record, "actor.key_binding.key_id"): + reasons.append( + "credential_ancestry: the chain does not terminate at the signing key" + ) + return UNKNOWN + if any( + link.get("link_type") == "rotation" and not link.get("same_actor_attested_by") + for link in chain + ): + reasons.append( + "credential_ancestry: an unattested rotation does not merge two key " + "coordinates into one actor" + ) + return UNKNOWN + return ATTESTED + + +def _evaluate_unlinkability(record: dict[str, Any], reasons: list[str]) -> str: + request = record.get("disclosure_minimization") or {} + if not request: + reasons.append("unlinkability: not requested") + return UNKNOWN + if not request.get("declared_assumptions"): + reasons.append("unlinkability: requested without declared assumptions") + return UNKNOWN + reasons.append( + "unlinkability: ASSUMED under declared assumptions only; this model cannot observe " + "the correlation surface available to an adversary" + ) + return ASSUMED + + +def _evaluate_accountability(record: dict[str, Any], reasons: list[str]) -> str: + if not record.get("escalation_authority"): + reasons.append("accountability: no escalation authority bound to the pseudonym") + return UNKNOWN + return ATTESTED + + +def _evaluate_recovery(record: dict[str, Any], reasons: list[str]) -> str: + recovery = record.get("recovery") or {} + if not recovery.get("mechanism"): + reasons.append("recovery: no credential-loss recovery mechanism declared") + return UNKNOWN + return ATTESTED + + +def _apply_minimization(record: dict[str, Any]) -> dict[str, Any]: + """Return a copy of the record with every withheld coordinate actually removed. + + Minimization is enforced rather than trusted: a coordinate the actor asked to + withhold is deleted before evaluation, so a removed trust root really does make + the dependent property unevaluable instead of quietly remaining available. + """ + + request = record.get("disclosure_minimization") or {} + withheld = request.get("withheld_coordinates") or [] + if not withheld: + return record + reduced = copy.deepcopy(record) + for dotted in withheld: + parts = dotted.split(".") + node: Any = reduced + for part in parts[:-1]: + if not isinstance(node, dict) or part not in node: + node = None + break + node = node[part] + if isinstance(node, dict): + node.pop(parts[-1], None) + return reduced + + +def _removes_coordinate(withheld: str, required: str) -> bool: + """Return whether withholding a path removes a required coordinate. + + Withholding ``actor.key_binding`` removes its ``trust_root`` child just as surely as + naming the leaf itself. Descendant paths do not remove their parent coordinate. + """ + + return withheld == required or required.startswith(withheld + ".") + + +def _check_structural_rejections(record: dict[str, Any], reasons: list[str]) -> list[str]: + """Return the reasons that make a record unevaluable, that is, REJECTED outright.""" + + fatal: list[str] = [] + request = record.get("disclosure_minimization") or {} + withheld = set(request.get("withheld_coordinates") or []) + for coordinate in REQUIRED_PUBLIC_COORDINATES: + removing_path = next( + ( + path + for path in withheld + if isinstance(path, str) and _removes_coordinate(path, coordinate) + ), + None, + ) + if removing_path is not None: + fatal.append( + f"minimization path {removing_path} removed required public coordinate " + f"{coordinate}; " + "disclosure minimization cannot erase coordinates required for bounded " + "reverification" + ) + before = request.get("claim_boundary_before") + after = request.get("claim_boundary_after") + if before is not None and after is not None: + before_set = set(before if isinstance(before, list) else [before]) + after_set = set(after if isinstance(after, list) else [after]) + if not after_set <= before_set: + fatal.append( + "minimization widened the claim boundary; minimization may only narrow it" + ) + reasons.extend(fatal) + return fatal + + +def evaluate(record: dict[str, Any]) -> Evaluation: + """Evaluate one bounded disclosure record, failing closed on missing coordinates.""" + + reasons: list[str] = [] + fatal = _check_structural_rejections(record, reasons) + record = _apply_minimization(record) + + properties: dict[str, str] = {} + properties["civil_identity"] = _evaluate_civil_identity(record, reasons) + properties["authentication"] = _evaluate_authentication(record, reasons) + properties["authority_active"] = _evaluate_authority_active(record, reasons) + properties["authorization"] = _evaluate_authorization( + record, properties["authentication"], properties["authority_active"], reasons + ) + properties["freshness"] = _evaluate_freshness(record, reasons) + properties["uniqueness"] = _evaluate_uniqueness(record, reasons) + properties["verifier_independence"] = _evaluate_independence(record, reasons) + properties["unlinkability"] = _evaluate_unlinkability(record, reasons) + properties["authorship_degree"] = _evaluate_authorship_degree(record, reasons) + properties["credential_ancestry"] = _evaluate_credential_ancestry(record, reasons) + properties["accountability"] = _evaluate_accountability(record, reasons) + properties["recovery"] = _evaluate_recovery(record, reasons) + + if fatal: + return Evaluation(REJECTED, properties, reasons) + + values = set(properties.values()) + if REFUTED in values: + verdict = REJECTED + elif CONFLICTED in values: + verdict = CONFLICTED + elif properties["authorization"] == SUPPORTED and properties["authentication"] == SUPPORTED: + verdict = ACCEPTED_BOUNDED + else: + verdict = UNKNOWN + + unmet = [ + name + for name in (record.get("claimed_properties") or []) + if properties.get(name, UNKNOWN) not in (SUPPORTED, ATTESTED) + ] + if unmet and verdict == ACCEPTED_BOUNDED: + reasons.append( + "verdict: claimed properties " + + ", ".join(sorted(unmet)) + + " are not supported; the record stays UNKNOWN rather than widening" + ) + verdict = UNKNOWN + return Evaluation(verdict, properties, reasons) + + +def evaluate_file(path: Path) -> Evaluation: + """Evaluate the ``record`` object stored in a fixture file.""" + + fixture = json.loads(Path(path).read_text(encoding="utf-8")) + return evaluate(fixture["record"]) + + +def main(argv: list[str] | None = None) -> int: + import sys + + args = list(sys.argv[1:] if argv is None else argv) + if not args: + print("usage: evaluate.py FIXTURE [FIXTURE ...]") + return 2 + for raw in args: + result = evaluate_file(Path(raw)) + print(json.dumps({"fixture": raw, **result.to_dict()}, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/conflicted_authorship_degree_vs_chain.json b/examples/zizk_artifact_first/zero_identity/fixtures/conflicted_authorship_degree_vs_chain.json new file mode 100644 index 0000000..aa8d88f --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/conflicted_authorship_degree_vs_chain.json @@ -0,0 +1,96 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "CONFLICTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "CONFLICTED" + }, + "falsification_question": "Is the more convenient of two disagreeing degree claims preferred?", + "fixture_id": "conflicted_authorship_degree_vs_chain", + "intent": "A declared degree that disagrees with the recorded chain stays CONFLICTED.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:delegate-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 2, + "role": "DELEGATE" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + }, + { + "attested_by": "root:issuer-a", + "child": "key:delegate-1", + "child_scope": [ + "vstd4-refutation-run" + ], + "link_type": "delegation", + "parent": "key:alpha-1", + "parent_scope": [ + "vstd4-refutation-run" + ] + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-authorship-degree-conflict", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/conflicted_identity_evidence.json b/examples/zizk_artifact_first/zero_identity/fixtures/conflicted_identity_evidence.json new file mode 100644 index 0000000..833c7ef --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/conflicted_identity_evidence.json @@ -0,0 +1,92 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "CONFLICTED", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "CONFLICTED" + }, + "falsification_question": "Can a conflict be resolved by preferring the convenient source?", + "fixture_id": "conflicted_identity_evidence", + "intent": "Conflicting identity evidence is retained as CONFLICTED.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [ + { + "evidence": [ + "issuer directory binds this pseudonym to one subject", + "operator attestation binds the same pseudonym to a different subject" + ], + "property": "civil_identity" + } + ], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-conflicted", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/positive_bounded_authorization.json b/examples/zizk_artifact_first/zero_identity/fixtures/positive_bounded_authorization.json new file mode 100644 index 0000000..fef352d --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/positive_bounded_authorization.json @@ -0,0 +1,84 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "ACCEPTED_BOUNDED" + }, + "falsification_question": "Does withholding civil identity remove the ability to reverify authorization?", + "fixture_id": "positive_bounded_authorization", + "intent": "Civil identity is withheld while a bounded authorization coordinate stays verifiable.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-positive-1", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/positive_minimized_boundary_narrowed.json b/examples/zizk_artifact_first/zero_identity/fixtures/positive_minimized_boundary_narrowed.json new file mode 100644 index 0000000..797dc3e --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/positive_minimized_boundary_narrowed.json @@ -0,0 +1,100 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "ASSUMED", + "verifier_independence": "UNKNOWN" + }, + "verdict": "ACCEPTED_BOUNDED" + }, + "falsification_question": "Does narrowing disclosure silently weaken the retained claim?", + "fixture_id": "positive_minimized_boundary_narrowed", + "intent": "Minimization that narrows the boundary keeps the bounded authorization result.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "disclosure_minimization": { + "claim_boundary_after": [ + "vstd4-refutation-run" + ], + "claim_boundary_before": [ + "vstd4-refutation-run", + "vstd4-availability-run" + ], + "declared_assumptions": [ + "issuer does not collude with the verifier" + ], + "requested_by": "actor", + "withheld_coordinates": [ + "actor.civil_identity" + ] + }, + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-minimized-narrowed", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_delegation_widens_scope.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_delegation_widens_scope.json new file mode 100644 index 0000000..a995ec5 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_delegation_widens_scope.json @@ -0,0 +1,97 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "REFUTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Can delegation manufacture authority the issuer never granted?", + "fixture_id": "rejected_delegation_widens_scope", + "intent": "A delegation may not carry a scope its ancestor did not hold.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:delegate-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 1, + "role": "DELEGATE" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + }, + { + "attested_by": "root:issuer-a", + "child": "key:delegate-1", + "child_scope": [ + "vstd4-refutation-run", + "vstd4-availability-run" + ], + "link_type": "delegation", + "parent": "key:alpha-1", + "parent_scope": [ + "vstd4-refutation-run" + ] + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-ancestry-scope-escalation", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_expired_authority.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_expired_authority.json new file mode 100644 index 0000000..9eee4cf --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_expired_authority.json @@ -0,0 +1,84 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "REFUTED", + "authorization": "REFUTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Does an expired window silently remain usable?", + "fixture_id": "rejected_expired_authority", + "intent": "An evaluation instant outside the validity window refutes the authority.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2027-02-01T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-expired", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2027-02-01T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_key_compromise.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_key_compromise.json new file mode 100644 index 0000000..5bc7443 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_key_compromise.json @@ -0,0 +1,85 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "REFUTED", + "authority_active": "SUPPORTED", + "authorization": "REFUTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Does a syntactically valid signature survive key compromise?", + "fixture_id": "rejected_key_compromise", + "intent": "A key reported compromised for the signing interval refutes authentication.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_compromised_during_interval": true, + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-key-compromise", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_minimization_erases_key_binding.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_minimization_erases_key_binding.json new file mode 100644 index 0000000..ff4c50d --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_minimization_erases_key_binding.json @@ -0,0 +1,99 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "UNKNOWN", + "authority_active": "SUPPORTED", + "authorization": "UNKNOWN", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "UNKNOWN", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "ASSUMED", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Can minimization bypass a protected leaf by deleting its parent object?", + "fixture_id": "rejected_minimization_erases_key_binding", + "intent": "Withholding actor.key_binding removes required key coordinates and is rejected.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "disclosure_minimization": { + "claim_boundary_after": [ + "vstd4-refutation-run" + ], + "claim_boundary_before": [ + "vstd4-refutation-run" + ], + "declared_assumptions": [ + "issuer does not collude with the verifier" + ], + "requested_by": "actor", + "withheld_coordinates": [ + "actor.key_binding" + ] + }, + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-minimization-parent-path", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_minimization_widens_boundary.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_minimization_widens_boundary.json new file mode 100644 index 0000000..a7432af --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_minimization_widens_boundary.json @@ -0,0 +1,100 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "ASSUMED", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Can redaction be used to enlarge what a receipt asserts?", + "fixture_id": "rejected_minimization_widens_boundary", + "intent": "Disclosure minimization may only narrow the claim boundary.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "disclosure_minimization": { + "claim_boundary_after": [ + "vstd4-refutation-run", + "vstd4-availability-run" + ], + "claim_boundary_before": [ + "vstd4-refutation-run" + ], + "declared_assumptions": [ + "issuer does not collude with the verifier" + ], + "requested_by": "actor", + "withheld_coordinates": [ + "actor.civil_identity" + ] + }, + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-boundary-widened", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_missing_challenge.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_missing_challenge.json new file mode 100644 index 0000000..b13274f --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_missing_challenge.json @@ -0,0 +1,82 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "REFUTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Does an absent nonce read as freshness?", + "fixture_id": "rejected_missing_challenge", + "intent": "Required freshness with no challenge coordinate fails closed.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-missing-challenge", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_relay_claims_origination.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_relay_claims_origination.json new file mode 100644 index 0000000..5581fc8 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_relay_claims_origination.json @@ -0,0 +1,96 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "REFUTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Can a relay present a claim as its own?", + "fixture_id": "rejected_relay_claims_origination", + "intent": "A relayed claim is not first-party authorship.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:delegate-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 1, + "role": "RELAY" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorship_origination" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + }, + { + "attested_by": "root:issuer-a", + "child": "key:delegate-1", + "child_scope": [ + "vstd4-refutation-run" + ], + "link_type": "delegation", + "parent": "key:alpha-1", + "parent_scope": [ + "vstd4-refutation-run" + ] + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-authorship-relay", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_replayed_challenge.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_replayed_challenge.json new file mode 100644 index 0000000..f0ee98b --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_replayed_challenge.json @@ -0,0 +1,86 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "REFUTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Is a reused challenge indistinguishable from a fresh one?", + "fixture_id": "rejected_replayed_challenge", + "intent": "A previously observed challenge value is a detected replay.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [ + "challenge:0001" + ], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-replay", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_revoked_ancestor.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_revoked_ancestor.json new file mode 100644 index 0000000..02e5494 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_revoked_ancestor.json @@ -0,0 +1,97 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "REFUTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Does a revoked ancestor leave its descendants merely uncertain?", + "fixture_id": "rejected_revoked_ancestor", + "intent": "Authority does not survive delegation from a revoked ancestor.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:delegate-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 1, + "role": "DELEGATE" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + }, + { + "attested_by": "root:issuer-a", + "child": "key:delegate-1", + "child_scope": [ + "vstd4-refutation-run" + ], + "link_type": "delegation", + "parent": "key:alpha-1", + "parent_scope": [ + "vstd4-refutation-run" + ], + "parent_state": "revoked" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-ancestry-revoked", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_revoked_authority.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_revoked_authority.json new file mode 100644 index 0000000..92200d2 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_revoked_authority.json @@ -0,0 +1,84 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "REFUTED", + "authorization": "REFUTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Can a revoked grant still be treated as active?", + "fixture_id": "rejected_revoked_authority", + "intent": "Revoked authority is refuted rather than degraded to UNKNOWN.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-revoked", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "revoked" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/rejected_unlinkability_erases_trust_root.json b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_unlinkability_erases_trust_root.json new file mode 100644 index 0000000..c2ce20b --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/rejected_unlinkability_erases_trust_root.json @@ -0,0 +1,99 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "UNKNOWN", + "authorization": "UNKNOWN", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "ASSUMED", + "verifier_independence": "UNKNOWN" + }, + "verdict": "REJECTED" + }, + "falsification_question": "Can privacy be bought by deleting the revocation source?", + "fixture_id": "rejected_unlinkability_erases_trust_root", + "intent": "An unlinkability request may not remove a required trust-root coordinate.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "disclosure_minimization": { + "claim_boundary_after": [ + "vstd4-refutation-run" + ], + "claim_boundary_before": [ + "vstd4-refutation-run" + ], + "declared_assumptions": [ + "issuer does not collude with the verifier" + ], + "requested_by": "actor", + "withheld_coordinates": [ + "revocation.source" + ] + }, + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-minimization-trust-root", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/unknown_absent_authorship.json b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_absent_authorship.json new file mode 100644 index 0000000..d76c339 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_absent_authorship.json @@ -0,0 +1,79 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "UNKNOWN", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "UNKNOWN" + }, + "falsification_question": "Does signing a record make you its author?", + "fixture_id": "unknown_absent_authorship", + "intent": "A signer is not assumed to be the author of the claim.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorship_degree" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-authorship-absent", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/unknown_distinct_pseudonyms.json b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_distinct_pseudonyms.json new file mode 100644 index 0000000..55f97f9 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_distinct_pseudonyms.json @@ -0,0 +1,89 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "UNKNOWN" + }, + "falsification_question": "Do two pseudonyms establish two actors?", + "fixture_id": "unknown_distinct_pseudonyms", + "intent": "Two distinct pseudonyms are not evidence of two distinct actors.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "verifier_independence" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [ + { + "pseudonym": "pseudonym:beta", + "receipt_id": "peer:2" + } + ], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-independence-distinct", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/unknown_missing_authorization.json b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_missing_authorization.json new file mode 100644 index 0000000..ab9f6c3 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_missing_authorization.json @@ -0,0 +1,75 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "UNKNOWN", + "authorization": "UNKNOWN", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "UNKNOWN" + }, + "falsification_question": "Can a missing grant be read as permission?", + "fixture_id": "unknown_missing_authorization", + "intent": "A record with no authorization grant stays UNKNOWN and never fails open.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "authorization" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-unknown-authorization", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/unknown_shared_pseudonym_independence.json b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_shared_pseudonym_independence.json new file mode 100644 index 0000000..496039f --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_shared_pseudonym_independence.json @@ -0,0 +1,89 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "UNKNOWN" + }, + "falsification_question": "Does a repeated pseudonym supply independent corroboration?", + "fixture_id": "unknown_shared_pseudonym_independence", + "intent": "A shared pseudonymous coordinate cannot supply independent corroboration and does not establish how many actors use it.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "verifier_independence" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [ + { + "pseudonym": "pseudonym:alpha", + "receipt_id": "peer:1" + } + ], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-independence-shared", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/unknown_unattested_ancestry_link.json b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_unattested_ancestry_link.json new file mode 100644 index 0000000..a5e02a0 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_unattested_ancestry_link.json @@ -0,0 +1,95 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "UNKNOWN", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "UNKNOWN" + }, + "falsification_question": "Does a written-down chain establish that authority survived every hop?", + "fixture_id": "unknown_unattested_ancestry_link", + "intent": "An unattested link in a recorded chain is not a verified chain.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:delegate-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 1, + "role": "DELEGATE" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "credential_ancestry" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + }, + { + "child": "key:delegate-1", + "child_scope": [ + "vstd4-refutation-run" + ], + "link_type": "delegation", + "parent": "key:alpha-1", + "parent_scope": [ + "vstd4-refutation-run" + ] + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-ancestry-unattested", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/unknown_unattested_rotation.json b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_unattested_rotation.json new file mode 100644 index 0000000..5c42d87 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_unattested_rotation.json @@ -0,0 +1,90 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "UNKNOWN", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "UNKNOWN" + }, + "falsification_question": "Do two keys become one actor because a rotation was recorded?", + "fixture_id": "unknown_unattested_rotation", + "intent": "An unattested rotation does not merge two key coordinates into one actor.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-2", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "credential_ancestry" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + }, + { + "attested_by": "root:issuer-a", + "child": "key:alpha-2", + "link_type": "rotation", + "parent": "key:alpha-1" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-ancestry-rotation", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/fixtures/unknown_uniqueness_absent.json b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_uniqueness_absent.json new file mode 100644 index 0000000..8a42f63 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/fixtures/unknown_uniqueness_absent.json @@ -0,0 +1,84 @@ +{ + "expected": { + "properties": { + "accountability": "ATTESTED", + "authentication": "SUPPORTED", + "authority_active": "SUPPORTED", + "authorization": "SUPPORTED", + "authorship_degree": "ATTESTED", + "civil_identity": "UNSUPPORTED_BY_DESIGN", + "credential_ancestry": "ATTESTED", + "freshness": "SUPPORTED", + "recovery": "ATTESTED", + "uniqueness": "UNKNOWN", + "unlinkability": "UNKNOWN", + "verifier_independence": "UNKNOWN" + }, + "verdict": "UNKNOWN" + }, + "falsification_question": "Does the absence of duplicates prove there are none?", + "fixture_id": "unknown_uniqueness_absent", + "intent": "Absent uniqueness evidence does not imply Sybil resistance.", + "record": { + "actor": { + "civil_identity": "withheld", + "key_binding": { + "key_id": "key:alpha-1", + "signature_verified": true, + "trust_root": "root:issuer-a" + }, + "pseudonym": "pseudonym:alpha" + }, + "authorization": { + "grant_id": "grant:alpha-1", + "issuer": "root:issuer-a", + "not_after": "2026-12-31T00:00:00Z", + "not_before": "2026-01-01T00:00:00Z", + "scope": [ + "vstd4-refutation-run" + ] + }, + "authorship": { + "attested_by": "root:issuer-a", + "degree": 0, + "role": "ORIGINATOR" + }, + "claim_scope": "vstd4-refutation-run", + "claimed_properties": [ + "uniqueness" + ], + "conflicts": [], + "credential_ancestry": [ + { + "attested_by": "root:issuer-a", + "child": "key:alpha-1", + "link_type": "issuance", + "parent": "root:issuer-a" + } + ], + "escalation_authority": "root:issuer-a", + "evaluated_at": "2026-08-23T00:00:00Z", + "freshness": { + "challenge_source": "verifier:v1", + "nonce": "challenge:0001", + "previously_observed_nonces": [], + "required": true + }, + "independence_evidence": [], + "peer_receipts": [], + "profile": "zizk-vstd/bounded-identity-disclosure/reference-0", + "record_id": "zi-uniqueness-absent", + "recovery": { + "mechanism": "issuer reissue on quorum of two custodians" + }, + "revocation": { + "checked_at": "2026-08-23T00:00:00Z", + "source": "root:issuer-a/status", + "state": "active" + }, + "trust_roots": [ + "root:issuer-a" + ], + "uniqueness_evidence": [] + } +} diff --git a/examples/zizk_artifact_first/zero_identity/model/zero_identity_model.json b/examples/zizk_artifact_first/zero_identity/model/zero_identity_model.json new file mode 100644 index 0000000..e2916d4 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/model/zero_identity_model.json @@ -0,0 +1,311 @@ +{ + "model_id": "zizk-vstd/bounded-identity-disclosure/reference-0", + "status": "REFERENCE_NON_NORMATIVE", + "normative": false, + "serialized_receipt_identifier": null, + "notes": [ + "This bounded reference model carries no serialized receipt identifier, no schema $id route, and no receipt digest.", + "It does not alter, extend, or reinterpret any VSTD serialized receipt identifier.", + "Nothing in this model asserts a cryptographic guarantee. Cryptographic mechanisms named here are inputs whose verification is performed elsewhere and asserted as evidence." + ], + "terminology_decision": { + "public_label_zero_identity": "REJECTED_AS_UNQUALIFIED_PUBLIC_LABEL", + "accepted_label": "bounded identity disclosure", + "rationale": "The profile never removes identity; it withholds civil identity while retaining cryptographic and authorization coordinates. The architecture-wide zero-identity rule means only that identity or reputation alone cannot strengthen an artifact-bound result; it is not a privacy guarantee." + }, + "identity_dimensions": [ + "civil_identity", + "persistent_public_identity", + "key_or_credential_coordinate", + "authentication", + "authorization", + "accountability", + "attribution", + "authorship_degree", + "credential_ancestry", + "uniqueness", + "verifier_independence", + "revocation_or_expiry", + "confidentiality", + "unlinkability", + "anonymity_or_pseudonymity" + ], + "property_statuses": [ + "SUPPORTED", + "ATTESTED", + "ASSUMED", + "UNKNOWN", + "CONFLICTED", + "REFUTED", + "UNSUPPORTED_BY_DESIGN" + ], + "verdicts": [ + "ACCEPTED_BOUNDED", + "UNKNOWN", + "CONFLICTED", + "REJECTED" + ], + "verdict_aggregation": { + "terminal_property_results": [ + "any REFUTED property makes the record REJECTED", + "otherwise any CONFLICTED property makes the record CONFLICTED" + ], + "acceptance_boundary": "otherwise authentication and authorization must both be SUPPORTED and every explicitly claimed property must be SUPPORTED or ATTESTED", + "ancillary_unknowns": "UNKNOWN on an unclaimed ancillary property remains visible and does not widen the ACCEPTED_BOUNDED authorization result", + "otherwise": "UNKNOWN" + }, + "minimum_public_actor_coordinates": [ + "actor.pseudonym", + "actor.key_binding.key_id", + "actor.key_binding.signature_verified", + "actor.key_binding.trust_root", + "authorization.grant_id", + "authorization.issuer", + "authorization.scope", + "authorization.not_before", + "authorization.not_after", + "revocation.source", + "revocation.state", + "revocation.checked_at", + "trust_roots" + ], + "optional_provenance_coordinates": [ + "authorship.role", + "authorship.degree", + "authorship.attested_by", + "credential_ancestry[].parent", + "credential_ancestry[].child", + "credential_ancestry[].link_type", + "credential_ancestry[].attested_by" + ], + "prohibited_inferences": [ + "absent civil identity implies anonymity", + "absent civil identity implies unlinkability", + "a pseudonym implies a distinct actor", + "a shared pseudonym implies a single actor", + "two distinct pseudonyms imply two independent actors", + "a verified signature implies authorization", + "an authorization grant implies that authority is currently active", + "absent revocation evidence implies active authority", + "absent uniqueness evidence implies Sybil resistance", + "hashing, redaction, encryption, omission, or pseudonymity alone implies zero identity", + "disclosure minimization preserves the original claim boundary", + "missing evidence implies safety", + "a signer is the author of the claim", + "a relayed or delegated claim is first-party authorship", + "an absent authorship role means degree zero", + "a recorded ancestry chain establishes that authority survived every hop", + "no ancestor marked revoked means every ancestor is valid", + "a key rotation link merges two key coordinates into one actor", + "a delegation may carry a scope its ancestor did not hold" + ], + "properties": { + "civil_identity": { + "profile_intent": "withheld", + "attainable_statuses": [ + "UNSUPPORTED_BY_DESIGN", + "CONFLICTED" + ] + }, + "authentication": { + "attainable_statuses": [ + "SUPPORTED", + "REFUTED", + "UNKNOWN", + "CONFLICTED" + ], + "requires": [ + "actor.key_binding.signature_verified", + "resolvable trust_root" + ] + }, + "authority_active": { + "attainable_statuses": [ + "SUPPORTED", + "REFUTED", + "UNKNOWN", + "CONFLICTED" + ], + "requires": [ + "revocation.state", + "revocation.source", + "validity window containing evaluated_at" + ] + }, + "authorization": { + "attainable_statuses": [ + "SUPPORTED", + "REFUTED", + "UNKNOWN", + "CONFLICTED" + ], + "requires": [ + "authentication SUPPORTED", + "authority_active SUPPORTED", + "scope covers claim_scope" + ] + }, + "attribution": { + "attainable_statuses": [ + "ATTESTED", + "UNKNOWN", + "CONFLICTED" + ], + "bound_to": "pseudonymous coordinate only, never civil identity" + }, + "uniqueness": { + "attainable_statuses": [ + "ATTESTED", + "UNKNOWN", + "CONFLICTED" + ], + "default_when_absent": "UNKNOWN" + }, + "verifier_independence": { + "attainable_statuses": [ + "ATTESTED", + "UNKNOWN", + "CONFLICTED" + ], + "default_when_absent": "UNKNOWN" + }, + "freshness": { + "attainable_statuses": [ + "SUPPORTED", + "REFUTED", + "UNKNOWN" + ], + "fail_closed_when_required_and_absent": true + }, + "unlinkability": { + "attainable_statuses": [ + "ASSUMED", + "UNKNOWN", + "REFUTED" + ], + "never": "SUPPORTED", + "reason": "This model observes one record at a time and cannot observe the adversary's full correlation surface." + }, + "accountability": { + "attainable_statuses": [ + "ATTESTED", + "UNKNOWN" + ], + "requires": [ + "a named escalation authority that can act on the pseudonymous coordinate" + ] + }, + "confidentiality": { + "attainable_statuses": [ + "ASSUMED", + "UNKNOWN" + ], + "reason": "Transport and storage confidentiality are outside this record." + }, + "recovery": { + "attainable_statuses": [ + "ATTESTED", + "UNKNOWN" + ], + "default_when_absent": "UNKNOWN" + }, + "authorship_degree": { + "attainable_statuses": [ + "ATTESTED", + "REFUTED", + "UNKNOWN", + "CONFLICTED" + ], + "never": "SUPPORTED", + "roles": [ + "ORIGINATOR", + "DELEGATE", + "RELAY", + "AGGREGATOR" + ], + "default_when_absent": "UNKNOWN", + "reason": "Authorship distance is an assertion about the world outside the record; this model can check it for internal consistency but cannot observe who wrote a claim." + }, + "credential_ancestry": { + "attainable_statuses": [ + "ATTESTED", + "REFUTED", + "UNKNOWN", + "CONFLICTED" + ], + "never": "SUPPORTED", + "default_when_absent": "UNKNOWN", + "reason": "The chain records ancestry. It does not by itself establish that authority survived every hop, mirroring the recorded-lineage discipline of VSTD-Graph-1." + } + }, + "rules": [ + { + "id": "ZI-R1", + "statement": "A missing coordinate yields UNKNOWN, never a favourable status." + }, + { + "id": "ZI-R2", + "statement": "CONFLICTED is terminal for the property and propagates to the record verdict." + }, + { + "id": "ZI-R3", + "statement": "Revoked or expired authority is REFUTED, never UNKNOWN." + }, + { + "id": "ZI-R4", + "statement": "A shared pseudonymous coordinate cannot supply independent corroboration, but it leaves actor independence UNKNOWN because multiple actors may share one credential." + }, + { + "id": "ZI-R5", + "statement": "Distinct pseudonymous coordinates leave both independence and actor-distinctness UNKNOWN." + }, + { + "id": "ZI-R6", + "statement": "A minimization request that removes a required public coordinate, whether directly or through a parent path, makes the record unevaluable and is REJECTED." + }, + { + "id": "ZI-R7", + "statement": "When freshness is required, an absent challenge coordinate fails closed and a replayed challenge is REFUTED." + }, + { + "id": "ZI-R8", + "statement": "A claim boundary may only narrow under minimization; widening is REJECTED." + }, + { + "id": "ZI-R9", + "statement": "A key marked compromised for the signing interval REFUTES authentication." + }, + { + "id": "ZI-R10", + "statement": "unlinkability is never SUPPORTED by this model; at best it is ASSUMED under declared assumptions." + }, + { + "id": "ZI-R11", + "statement": "Authorship degree is asserted, never inferred; an absent role stays UNKNOWN and never defaults to ORIGINATOR." + }, + { + "id": "ZI-R12", + "statement": "A relay, delegate, or aggregator that claims origination is REFUTED." + }, + { + "id": "ZI-R13", + "statement": "A revoked recorded ancestor REFUTES the chain; authority does not survive delegation from a revoked ancestor." + }, + { + "id": "ZI-R14", + "statement": "A delegation whose scope exceeds its ancestor scope is REFUTED." + }, + { + "id": "ZI-R15", + "statement": "An unattested link, a chain that misses a declared trust root, or a chain that misses the signing key stays UNKNOWN." + }, + { + "id": "ZI-R16", + "statement": "An unattested rotation does not merge two key coordinates into one actor." + }, + { + "id": "ZI-R17", + "statement": "A declared degree that disagrees with the recorded chain length is CONFLICTED." + } + ] +} diff --git a/examples/zizk_artifact_first/zero_identity/run_validation.py b/examples/zizk_artifact_first/zero_identity/run_validation.py new file mode 100644 index 0000000..e195922 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/run_validation.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Run the complete validation suite for this reference evaluator. + +Uses the standard library only, so it runs without pytest. When pytest is present, +``python -m pytest examples/zizk_artifact_first/zero_identity/tests -q`` runs the same +fixtures plus the inference-blocking assertions. +""" + +from __future__ import annotations + +import json +from pathlib import Path +import sys + +HERE = Path(__file__).resolve().parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from evaluate import evaluate # noqa: E402 + + +def main() -> int: + failures: list[str] = [] + fixtures = sorted((HERE / "fixtures").glob("*.json")) + if not fixtures: + print("no fixtures found") + return 1 + for path in fixtures: + fixture = json.loads(path.read_text(encoding="utf-8")) + outcome = evaluate(fixture["record"]) + expected = fixture["expected"] + if outcome.verdict != expected["verdict"]: + failures.append( + f"{path.name}: verdict {outcome.verdict} != {expected['verdict']}" + ) + for name, want in expected["properties"].items(): + got = outcome.properties.get(name) + if got != want: + failures.append(f"{path.name}: {name} {got} != {want}") + print(f"{outcome.verdict:<17} {path.stem}") + for failure in failures: + print(f"FAIL {failure}") + print(f"{len(fixtures)} fixtures, {len(failures)} failures") + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/zizk_artifact_first/zero_identity/tests/test_zero_identity.py b/examples/zizk_artifact_first/zero_identity/tests/test_zero_identity.py new file mode 100644 index 0000000..1d2d465 --- /dev/null +++ b/examples/zizk_artifact_first/zero_identity/tests/test_zero_identity.py @@ -0,0 +1,310 @@ +"""Terminology: Verifier Standard (VSTD). + +Validation suite for the bounded identity disclosure reference evaluator. + +Each test names the inference it exists to block. A test that starts passing because +a status was upgraded to something more favourable is a defect, not a fix. +""" + +from __future__ import annotations + +import json +from pathlib import Path +import sys + +import pytest + +REFERENCE_SURFACE = Path(__file__).resolve().parents[1] +if str(REFERENCE_SURFACE) not in sys.path: + sys.path.insert(0, str(REFERENCE_SURFACE)) + +from evaluate import ( # noqa: E402 + ACCEPTED_BOUNDED, + ATTESTED, + CONFLICTED, + REFUTED, + REJECTED, + SUPPORTED, + UNKNOWN, + evaluate, + load_model, +) + +FIXTURES = sorted((REFERENCE_SURFACE / "fixtures").glob("*.json")) + + +def load(name: str) -> dict: + return json.loads( + (REFERENCE_SURFACE / "fixtures" / f"{name}.json").read_text(encoding="utf-8") + ) + + +def result(name: str): + return evaluate(load(name)["record"]) + + +def test_fixture_corpus_is_non_empty() -> None: + assert FIXTURES, "the fixture corpus must not be empty" + + +@pytest.mark.parametrize("path", FIXTURES, ids=lambda p: p.stem) +def test_fixture_matches_declared_expectation(path: Path) -> None: + fixture = json.loads(path.read_text(encoding="utf-8")) + outcome = evaluate(fixture["record"]) + assert outcome.verdict == fixture["expected"]["verdict"] + assert outcome.properties == fixture["expected"]["properties"] + assert outcome.reasons, "every evaluation must carry at least one stated reason" + + +def test_civil_identity_withheld_keeps_authorization_verifiable() -> None: + outcome = result("positive_bounded_authorization") + assert outcome.verdict == ACCEPTED_BOUNDED + assert outcome.properties["civil_identity"] == "UNSUPPORTED_BY_DESIGN" + assert outcome.properties["authorization"] == SUPPORTED + + +def test_bounded_acceptance_does_not_imply_uniqueness_or_independence() -> None: + outcome = result("positive_bounded_authorization") + assert outcome.properties["uniqueness"] == UNKNOWN + assert outcome.properties["verifier_independence"] == UNKNOWN + assert outcome.properties["unlinkability"] == UNKNOWN + + +def test_missing_authorization_stays_unknown() -> None: + outcome = result("unknown_missing_authorization") + assert outcome.verdict == UNKNOWN + assert outcome.properties["authorization"] == UNKNOWN + + +def test_revoked_authority_is_refuted_not_unknown() -> None: + outcome = result("rejected_revoked_authority") + assert outcome.verdict == REJECTED + assert outcome.properties["authority_active"] == REFUTED + + +def test_expired_authority_is_refuted() -> None: + outcome = result("rejected_expired_authority") + assert outcome.properties["authority_active"] == REFUTED + + +def test_shared_pseudonym_does_not_establish_actor_independence_or_nonindependence() -> None: + outcome = result("unknown_shared_pseudonym_independence") + assert outcome.properties["verifier_independence"] == UNKNOWN + assert outcome.verdict == UNKNOWN + + +def test_distinct_pseudonyms_do_not_establish_distinct_actors() -> None: + outcome = result("unknown_distinct_pseudonyms") + assert outcome.properties["verifier_independence"] == UNKNOWN + assert outcome.verdict == UNKNOWN + + +def test_minimization_cannot_delete_a_required_trust_root() -> None: + outcome = result("rejected_unlinkability_erases_trust_root") + assert outcome.verdict == REJECTED + assert outcome.properties["authority_active"] == UNKNOWN + assert any("revocation.source" in reason for reason in outcome.reasons) + + +def test_minimization_cannot_bypass_a_protected_leaf_by_deleting_its_parent() -> None: + outcome = result("rejected_minimization_erases_key_binding") + assert outcome.verdict == REJECTED + assert outcome.properties["authentication"] == UNKNOWN + assert any("actor.key_binding" in reason for reason in outcome.reasons) + + +def test_replayed_challenge_is_detected() -> None: + outcome = result("rejected_replayed_challenge") + assert outcome.properties["freshness"] == REFUTED + assert outcome.verdict == REJECTED + + +def test_required_freshness_without_a_challenge_fails_closed() -> None: + outcome = result("rejected_missing_challenge") + assert outcome.properties["freshness"] == REFUTED + + +def test_absent_uniqueness_evidence_is_not_sybil_resistance() -> None: + outcome = result("unknown_uniqueness_absent") + assert outcome.properties["uniqueness"] == UNKNOWN + assert outcome.verdict == UNKNOWN + + +def test_conflicting_identity_evidence_stays_conflicted() -> None: + outcome = result("conflicted_identity_evidence") + assert outcome.properties["civil_identity"] == CONFLICTED + assert outcome.verdict == CONFLICTED + + +def test_minimization_may_not_widen_the_claim_boundary() -> None: + outcome = result("rejected_minimization_widens_boundary") + assert outcome.verdict == REJECTED + assert any("widened" in reason for reason in outcome.reasons) + + +def test_minimization_that_narrows_keeps_the_bounded_result() -> None: + outcome = result("positive_minimized_boundary_narrowed") + assert outcome.verdict == ACCEPTED_BOUNDED + assert outcome.properties["unlinkability"] == "ASSUMED" + + +def test_key_compromise_refutes_authentication() -> None: + outcome = result("rejected_key_compromise") + assert outcome.properties["authentication"] == REFUTED + assert outcome.verdict == REJECTED + + +def test_unlinkability_is_never_supported() -> None: + for path in FIXTURES: + fixture = json.loads(path.read_text(encoding="utf-8")) + assert evaluate(fixture["record"]).properties["unlinkability"] != SUPPORTED + + +def test_no_fixture_reaches_acceptance_with_a_refuted_property() -> None: + for path in FIXTURES: + outcome = evaluate(json.loads(path.read_text(encoding="utf-8"))["record"]) + if REFUTED in outcome.properties.values(): + assert outcome.verdict == REJECTED + + +def test_accountability_requires_a_bound_escalation_authority() -> None: + record = load("positive_bounded_authorization")["record"] + assert evaluate(record).properties["accountability"] == ATTESTED + record.pop("escalation_authority") + assert evaluate(record).properties["accountability"] == UNKNOWN + + +def test_recovery_absence_stays_unknown() -> None: + record = load("positive_bounded_authorization")["record"] + record.pop("recovery") + assert evaluate(record).properties["recovery"] == UNKNOWN + + +def test_unknown_trust_root_does_not_authenticate() -> None: + record = load("positive_bounded_authorization")["record"] + record["trust_roots"] = ["root:other"] + outcome = evaluate(record) + assert outcome.properties["authentication"] == UNKNOWN + assert outcome.verdict == UNKNOWN + + +@pytest.mark.parametrize("coordinate", ["pseudonym", "key_id", "issuer"]) +def test_required_public_identity_coordinates_cannot_be_omitted(coordinate: str) -> None: + record = load("positive_bounded_authorization")["record"] + if coordinate == "pseudonym": + record["actor"].pop("pseudonym") + elif coordinate == "key_id": + record["actor"]["key_binding"].pop("key_id") + else: + record["authorization"].pop("issuer") + outcome = evaluate(record) + assert outcome.verdict == UNKNOWN + + +def test_undeclared_issuer_does_not_authorize() -> None: + record = load("positive_bounded_authorization")["record"] + record["authorization"]["issuer"] = "root:undeclared" + outcome = evaluate(record) + assert outcome.properties["authorization"] == UNKNOWN + assert outcome.verdict == UNKNOWN + + +def test_scope_mismatch_is_refuted() -> None: + record = load("positive_bounded_authorization")["record"] + record["claim_scope"] = "vstd4-availability-run" + outcome = evaluate(record) + assert outcome.properties["authorization"] == REFUTED + + +def test_signing_is_not_authorship() -> None: + outcome = result("unknown_absent_authorship") + assert outcome.properties["authorship_degree"] == UNKNOWN + assert outcome.verdict == UNKNOWN + + +def test_relayed_claim_is_not_first_party_authorship() -> None: + outcome = result("rejected_relay_claims_origination") + assert outcome.properties["authorship_degree"] == REFUTED + assert outcome.verdict == REJECTED + + +def test_declared_degree_must_agree_with_recorded_delegation_hops() -> None: + outcome = result("conflicted_authorship_degree_vs_chain") + assert outcome.properties["authorship_degree"] == CONFLICTED + assert outcome.verdict == CONFLICTED + + +@pytest.mark.parametrize("degree", [True, -1]) +def test_authorship_degree_must_be_a_nonnegative_integer(degree: object) -> None: + record = load("positive_bounded_authorization")["record"] + record["authorship"]["degree"] = degree + assert evaluate(record).properties["authorship_degree"] == UNKNOWN + + +def test_unattested_ancestry_link_is_not_a_verified_chain() -> None: + outcome = result("unknown_unattested_ancestry_link") + assert outcome.properties["credential_ancestry"] == UNKNOWN + + +def test_authority_does_not_survive_a_revoked_ancestor() -> None: + outcome = result("rejected_revoked_ancestor") + assert outcome.properties["credential_ancestry"] == REFUTED + assert outcome.verdict == REJECTED + + +def test_delegation_may_not_widen_scope_beyond_its_ancestor() -> None: + outcome = result("rejected_delegation_widens_scope") + assert outcome.properties["credential_ancestry"] == REFUTED + + +def test_unattested_rotation_does_not_merge_two_key_coordinates() -> None: + outcome = result("unknown_unattested_rotation") + assert outcome.properties["credential_ancestry"] == UNKNOWN + + +def test_absent_ancestry_chain_stays_unknown() -> None: + record = load("positive_bounded_authorization")["record"] + record.pop("credential_ancestry") + assert evaluate(record).properties["credential_ancestry"] == UNKNOWN + + +def test_chain_must_terminate_at_the_signing_key() -> None: + record = load("positive_bounded_authorization")["record"] + record["credential_ancestry"][0]["child"] = "key:someone-else" + assert evaluate(record).properties["credential_ancestry"] == UNKNOWN + + +def test_chain_must_begin_at_a_declared_trust_root() -> None: + record = load("positive_bounded_authorization")["record"] + record["credential_ancestry"][0]["parent"] = "root:undeclared" + assert evaluate(record).properties["credential_ancestry"] == UNKNOWN + + +def test_authorship_and_ancestry_are_never_supported() -> None: + for path in FIXTURES: + outcome = evaluate(json.loads(path.read_text(encoding="utf-8"))["record"]) + assert outcome.properties["authorship_degree"] != SUPPORTED + assert outcome.properties["credential_ancestry"] != SUPPORTED + + +def test_model_declares_the_terminology_decision_and_prohibited_inferences() -> None: + model = load_model() + assert model["status"] == "REFERENCE_NON_NORMATIVE" + assert model["serialized_receipt_identifier"] is None + decision = model["terminology_decision"]["public_label_zero_identity"] + assert decision == "REJECTED_AS_UNQUALIFIED_PUBLIC_LABEL" + assert "verdict_aggregation" in model + assert "verdict_precedence" not in model + assert len(model["prohibited_inferences"]) >= 10 + + +def test_model_never_lists_unlinkability_as_supported() -> None: + model = load_model() + assert SUPPORTED not in model["properties"]["unlinkability"]["attainable_statuses"] + + +def test_reference_surface_declares_no_new_serialized_receipt_identifier() -> None: + for path in (REFERENCE_SURFACE / "fixtures").glob("*.json"): + text = path.read_text(encoding="utf-8") + for identifier in ("VSTD-1", "VSTD-2", "VSTD-3.0", "VSTD-DATA-0.1"): + assert identifier not in text, f"{path.name} must not bind a VSTD serialized receipt identifier" diff --git a/experiments/artifact_first_mechanisms/README.md b/experiments/artifact_first_mechanisms/README.md new file mode 100644 index 0000000..71bc436 --- /dev/null +++ b/experiments/artifact_first_mechanisms/README.md @@ -0,0 +1,28 @@ +# Experimental artifact-first mechanisms + +> **Acronyms:** reduced instruction set computer (RISC); Verifier Standard (VSTD); +> zero-identity/zero-knowledge (ZIZK). + +This directory does **not** make VSTD's ZIZK artifact-first architecture experimental. +That governing orientation is normative in +[`standard/LADDER.md` section 1.1](../../standard/LADDER.md#11-artifact-first-causal-provenance-orientation). + +TRUST is mechanism-earned forward artifact support; ROT is typed, time-indexed +degradation of current admissibility; and RUST is the inverse-TRUST diagnostic backtrace +toward recorded ancestors. These are formal semantic names, not acronyms, actor ratings, +serialized receipt values, scalar scores, or references to the Rust programming language. + +Only the following unfinished mechanisms are experimental here: + +- event serialization; +- bounded TRUST-transfer algebra; +- ROT derivation and propagation; +- RUST concentration and localization; +- complete hidden-witness trichotomy derivation; and +- specific optional proof backends while they remain unfinished. + +The bounded identity-disclosure evaluator and tracked RISC Zero proof-carrying reference +mechanism are under +[`examples/zizk_artifact_first/`](../../examples/zizk_artifact_first/). The +[`experiment.json`](experiment.json) manifest records the mechanism studies and their +remaining horizons without assigning experimental status to the governing architecture. diff --git a/experiments/artifact_first_mechanisms/experiment.json b/experiments/artifact_first_mechanisms/experiment.json new file mode 100644 index 0000000..82e2023 --- /dev/null +++ b/experiments/artifact_first_mechanisms/experiment.json @@ -0,0 +1,436 @@ +{ + "profile": { + "id": "vstd.experimental-workflow", + "version": "0.1", + "status": "EXPERIMENTAL_NON_NORMATIVE" + }, + "experiment": { + "id": "experiment-artifact-first-mechanisms", + "title": "Mechanism completion under VSTD's governing ZIZK artifact-first architecture", + "question": "Which bounded event serialization, TRUST (mechanism-earned forward artifact support) transfer algebra, ROT (typed time-indexed current-admissibility degradation) derivation and propagation, RUST (inverse-TRUST diagnostic backtrace) concentration and localization rules, and hidden-witness trichotomy mechanisms can implement the governing artifact-first causal-provenance orientation without actor reputation, scalar cancellation, causal-localization overclaim, or making that orientation contingent on the study?", + "state": "RUNNING", + "started_at": "2026-08-23T00:00:00Z" + }, + "hypotheses": [ + { + "id": "hypothesis-hidden-witness", + "statement": "A real proof can establish the fixed reference-mechanism predicate without publishing the private witness bytes.", + "falsification_condition": "The accepted public artifacts disclose the witness bytes or a documented verifier accepts a proof not bound to the fixed predicate and program identifier.", + "state": "SUPPORTED" + }, + { + "id": "hypothesis-identity-boundary", + "statement": "The bounded identity evaluator preserves UNKNOWN and CONFLICTED rather than inferring uniqueness, independence, or authorization from absent identity information.", + "falsification_condition": "A fixture with absent or contradictory identity evidence produces an unqualified accepted identity inference.", + "state": "SUPPORTED" + }, + { + "id": "hypothesis-trustless-reverification", + "statement": "Reverification can reproduce a verdict over bound public coordinates without accumulating actor reputation or historical trust.", + "falsification_condition": "The proposed substrate requires actor identity or prior reputation to reproduce the bounded verdict, or repeated identical receipts increase epistemic strength without new evidence.", + "state": "OPEN" + }, + { + "id": "hypothesis-artifact-first-zero-actor-trust", + "statement": "An operational reverification protocol can derive acceptance from bound artifacts, evidence, predicates, mechanisms, and declared trust roots while preventing actor identity, popularity, repetition, or reputation from strengthening the verdict.", + "falsification_condition": "Changing only actor identity or reputation changes acceptance, repeated equivalent actor events raise status, or an unbound artifact is accepted.", + "state": "OPEN" + }, + { + "id": "hypothesis-contextual-actor-artifact-roles", + "statement": "An event schema can preserve actor and artifact as contextual roles, so a coding agent may be an artifact when created or evaluated and an actor when it creates or transforms another artifact.", + "falsification_condition": "The model requires permanent disjoint actor and artifact categories, loses a claim-relevant creation edge, or treats a role assignment as identity, authority, or trust.", + "state": "OPEN" + }, + { + "id": "hypothesis-rust-memetic-backtrace", + "statement": "An operational ledger can transfer typed RUST backward from an observed child deviation through admissible bound creation paths and concentrate independent backtraces on shared ancestor claims without reporting ancestry as localized causation.", + "falsification_condition": "RUST cannot reproduce its child-to-ancestor paths, fails to concentrate distinct comparable sources, crosses non-contributing edges, or reports concentration as direct observation, proven causation, actor reputation, or a VSTD verdict.", + "state": "OPEN" + }, + { + "id": "hypothesis-rot-current-admissibility", + "statement": "A typed current-state mechanism can derive ROT from exact lifecycle or dependency evidence and expose affected descendants without rewriting historical receipts or treating age as falsity.", + "falsification_condition": "Historical receipt bytes or results are mutated, age or actor reputation alone creates ROT, an inadmissible required dependency remains clean for current use, or ROT is treated as proof that the historical result was false.", + "state": "OPEN" + }, + { + "id": "hypothesis-dual-causal-propagation", + "statement": "An operational substrate can carry scoped TRUST from parent to child, derive typed ROT for current admissibility, and carry diagnostic RUST from child to parent without collapsing them into one scalar or allowing any relation to bypass claim-local evidence.", + "falsification_condition": "TRUST flows backward, RUST flows forward as inherited guilt, ROT rewrites historical truth, parent TRUST automatically proves a child, missing or conflicted support becomes clean, or actor identity changes any relation.", + "state": "OPEN" + } + ], + "preregistration": { + "state": "AMENDED", + "recorded_at": "2026-08-24T00:00:00Z", + "artifact_id": "artifact-round2-design", + "limitations": [ + "This experimental workflow manifest records only unfinished mechanisms and their evidence; it does not classify VSTD's governing ZIZK artifact-first architecture as experimental.", + "Round 2 is a design synthesis rather than a completed trustless protocol.", + "The bounded identity-disclosure reference evaluator is semantic and does not itself provide cryptographic anonymity or unlinkability." + ] + }, + "artifacts": [ + { + "id": "artifact-zk-report", + "role": "round-1-zero-knowledge-report", + "media_type": "text/markdown", + "digest": "sha256:ea53f18c4bc46a26a311ebf7ba6d5a44b924d18f3f5b51ce3fabb99f3fe0a4c3", + "locator": "repo:examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md" + }, + { + "id": "artifact-zk-receipt", + "role": "recorded-risc0-proof-receipt", + "media_type": "application/msgpack", + "digest": "sha256:5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df", + "locator": "repo:examples/zizk_artifact_first/risc0/recorded-proof/receipt.msgpack" + }, + { + "id": "artifact-zk-public-envelope", + "role": "recorded-risc0-public-envelope", + "media_type": "application/json", + "digest": "sha256:6324c3c5d77ea4df4034f61131059289d5228f190d69e34c59bd7416fa9ac823", + "locator": "repo:examples/zizk_artifact_first/risc0/recorded-proof/public.json" + }, + { + "id": "artifact-zk-self-test", + "role": "recorded-risc0-self-test-result", + "media_type": "application/json", + "digest": "sha256:e4c1bff21fb6161221276157fa96af6661af8635da35970ba12e462881f2c6fe", + "locator": "repo:examples/zizk_artifact_first/risc0/recorded-proof/self-test-results.json" + }, + { + "id": "artifact-zi-report", + "role": "round-1-zero-identity-report", + "media_type": "text/markdown", + "digest": "sha256:f303cf6a2e047c09517187a74a9bc850214b1971ca98f51997e5898ec04765ae", + "locator": "repo:examples/zizk_artifact_first/zero_identity/ROUND1_ZERO_IDENTITY_REPORT.md" + }, + { + "id": "artifact-round2-design", + "role": "round-2-reverification-design", + "media_type": "text/markdown", + "digest": "sha256:7c119945d1446ad2253dc3d9d51ef15024e32be86d92924eca0ab27587d09a46", + "locator": "repo:experiments/artifact_first_mechanisms/reverification/ROUND2_DESIGN_NOTE.md" + } + ], + "budgets": [ + { + "id": "budget-zk-proof", + "resource": "real-proof-runs", + "limit": 1, + "consumed": 1, + "unit": "proof-run", + "scope": "RISC Zero round-1 self-test" + }, + { + "id": "budget-identity-fixtures", + "resource": "semantic-fixture-evaluations", + "limit": 22, + "consumed": 22, + "unit": "fixture", + "scope": "bounded identity round-1 fixture corpus" + }, + { + "id": "budget-round2-design", + "resource": "design-synthesis", + "limit": 1, + "consumed": 1, + "unit": "bounded-review", + "scope": "trustless reverification round-2 note" + } + ], + "actions": [ + { + "id": "action-zk-proof", + "kind": "CRYPTOGRAPHIC_PROOF_EXPERIMENT", + "target": "fixed hidden-evidence predicate and public VSTD-facing coordinates", + "state": "COMPLETED", + "priority": 1, + "selected_because": "A zero-knowledge claim required a real proof and offline native verification rather than a commitment-only or development-mode substitute.", + "selection_evidence_ids": [ + "hypothesis-hidden-witness" + ], + "alternatives_considered": [ + "full disclosure", + "commitment-only evidence" + ], + "budget_ids": [ + "budget-zk-proof" + ], + "depends_on": [], + "triggered_by": [], + "expected_artifact_effect": "Produce a bounded proof-carrying reference mechanism and retrievable public proof artifacts without changing core VSTD receipts.", + "substrate": { + "kind": "proof-engine", + "name": "RISC Zero zkVM", + "version": "3.0.6", + "coordinate": "repo:examples/zizk_artifact_first/risc0" + }, + "native_result_ids": [ + "result-zk-proof" + ], + "produced_artifact_ids": [ + "artifact-zk-report", + "artifact-zk-receipt", + "artifact-zk-public-envelope", + "artifact-zk-self-test" + ] + }, + { + "id": "action-identity-evaluation", + "kind": "SEMANTIC_NEGATIVE_TESTING", + "target": "identity minimization, authorization boundaries, ancestry, and prohibited inferences", + "state": "COMPLETED", + "priority": 2, + "selected_because": "Identity minimization needed explicit negative fixtures before any operational protocol could be justified.", + "selection_evidence_ids": [ + "hypothesis-identity-boundary" + ], + "alternatives_considered": [ + "actor reputation scoring", + "implicit identity inference" + ], + "budget_ids": [ + "budget-identity-fixtures" + ], + "depends_on": [], + "triggered_by": [], + "expected_artifact_effect": "Expose UNKNOWN, CONFLICTED, and rejected identity inferences in a reproducible fixture corpus.", + "substrate": { + "kind": "semantic-evaluator", + "name": "ZIZK zero-identity fixture evaluator", + "version": "round-1", + "coordinate": "repo:examples/zizk_artifact_first/zero_identity" + }, + "native_result_ids": [ + "result-identity-fixtures" + ], + "produced_artifact_ids": [ + "artifact-zi-report" + ] + }, + { + "id": "action-reverification-synthesis", + "kind": "DESIGN_SYNTHESIS", + "target": "candidate operational mechanics for normative artifact-first TRUST, time-indexed ROT, and backward memetic RUST across contextual actor/artifact roles", + "state": "COMPLETED", + "priority": 3, + "selected_because": "The normative verification complex fixes the causal-provenance orientation, while the proof and identity studies expose the still-open event, transfer, concentration, and localization mechanics needed to implement it without actor reputation or causal-localization overclaim.", + "selection_evidence_ids": [ + "observation-zk-proof", + "observation-identity-boundary", + "hypothesis-contextual-actor-artifact-roles", + "hypothesis-rust-memetic-backtrace", + "hypothesis-artifact-first-zero-actor-trust", + "hypothesis-dual-causal-propagation", + "hypothesis-rot-current-admissibility" + ], + "alternatives_considered": [ + "identity-bound trust", + "actor reputation accumulation", + "object-only RUST with erased actor-artifact relations", + "permanent disjoint actor and artifact categories" + ], + "budget_ids": [ + "budget-round2-design" + ], + "depends_on": [ + "action-zk-proof", + "action-identity-evaluation" + ], + "triggered_by": [ + "observation-zk-proof", + "observation-identity-boundary" + ], + "expected_artifact_effect": "Specify candidate contextual-role, TRUST, ROT, RUST, and concentration mechanics while leaving event schemas, transfer/derivation algebras, and localization protocols open.", + "substrate": { + "kind": "design-review", + "name": "ZIZK artifact-first actor-artifact reverification synthesis", + "version": "round-2", + "coordinate": "repo:experiments/artifact_first_mechanisms/reverification" + }, + "native_result_ids": [], + "produced_artifact_ids": [ + "artifact-round2-design" + ] + } + ], + "observations": [ + { + "id": "observation-zk-proof", + "action_id": "action-zk-proof", + "recorded_at": "2026-08-23T00:00:00Z", + "statement": "The recorded round-1 run generated and re-verified one real RISC Zero receipt within the reference program and exercised the declared negative cases; distinct actors were not established.", + "status": "OBSERVED", + "evidence_artifact_ids": [ + "artifact-zk-report", + "artifact-zk-receipt", + "artifact-zk-public-envelope", + "artifact-zk-self-test" + ], + "limitations": [ + "The private witness and salt are excluded; the exact non-secret receipt, public envelope, self-test result, implementation, and report are tracked and digest-bound." + ] + }, + { + "id": "observation-identity-boundary", + "action_id": "action-identity-evaluation", + "recorded_at": "2026-08-24T00:00:00Z", + "statement": "The recorded fixture suite preserved bounded acceptance, rejection, UNKNOWN, and CONFLICTED outcomes across the declared cases.", + "status": "OBSERVED", + "evidence_artifact_ids": [ + "artifact-zi-report" + ], + "limitations": [ + "Semantic fixture behavior is not a cryptographic privacy, anonymity, authorization, or Sybil-resistance guarantee." + ] + } + ], + "native_results": [ + { + "id": "result-zk-proof", + "action_id": "action-zk-proof", + "verifier": { + "kind": "proof-verifier", + "name": "RISC Zero Receipt::verify", + "version": "3.0.6", + "coordinate": "repo:examples/zizk_artifact_first/risc0" + }, + "native_status": "REAL_RECEIPT_VERIFIED_AND_NEGATIVE_CASES_REJECTED", + "result_artifact_id": "artifact-zk-receipt", + "mapping": { + "status": "NOT_EVALUATED", + "vstd_verdict": null, + "mapping_profile": null, + "receipt_artifact_id": null, + "reason": "The proof engine's native result is recorded without inventing a VSTD receipt mapping." + } + }, + { + "id": "result-identity-fixtures", + "action_id": "action-identity-evaluation", + "verifier": { + "kind": "fixture-evaluator", + "name": "zero_identity.evaluate", + "version": "round-1", + "coordinate": "repo:examples/zizk_artifact_first/zero_identity/evaluate.py" + }, + "native_status": "22_FIXTURES_0_FAILURES", + "result_artifact_id": "artifact-zi-report", + "mapping": { + "status": "NOT_EVALUATED", + "vstd_verdict": null, + "mapping_profile": null, + "receipt_artifact_id": null, + "reason": "Fixture validation is preserved as its native result and does not become a core VSTD verdict." + } + } + ], + "adaptations": [ + { + "id": "adaptation-round2-trustless-boundary", + "trigger_ids": [ + "observation-zk-proof", + "observation-identity-boundary" + ], + "decision": "Treat standard/LADDER.md section 1.1 as the controlling semantic invariant and confine Round 2 to candidate operational mechanics for TRUST transfer, ROT derivation/propagation, and RUST backtrace/concentration.", + "reason": "The architecture is normative; only event serialization, TRUST-transfer algebra, ROT derivation and propagation, RUST concentration and localization, complete trichotomy derivation, and specific unfinished optional proof backends remain experimental.", + "action_ids": [ + "action-reverification-synthesis" + ], + "artifact_ids": [ + "artifact-round2-design" + ] + } + ], + "amendments": [ + { + "id": "amendment-round2-design", + "recorded_at": "2026-08-24T00:00:00Z", + "reason": "Round 1 findings narrowed the design from identity-bound trust to artifact-bound trustless reverification.", + "supersedes": [ + "hypothesis-trustless-reverification" + ], + "artifact_id": "artifact-round2-design" + }, + { + "id": "amendment-actor-artifact-rust-correction", + "recorded_at": "2026-08-25T00:00:00Z", + "reason": "Correct the object-only framing and distinguish the normative causal-provenance orientation from its experimental event, transfer, RUST, trichotomy, and optional proof-backend mechanisms: artifact support propagates ancestor-to-descendant, while RUST memetically backtraces descendant-to-ancestor without becoming actor reputation, scalar cancellation, causal localization, or guilt.", + "supersedes": [ + "amendment-round2-design" + ], + "artifact_id": "artifact-round2-design" + }, + { + "id": "amendment-trust-rot-rust-architecture", + "recorded_at": "2026-08-27T00:00:00Z", + "reason": "Formalize artifact/process-only TRUST, ROT, and RUST under zero identity and zero unevidenced knowledge, with cryptographic zero knowledge enclosing confidential-witness mechanisms rather than importing prover identity into verdict weight.", + "supersedes": [ + "amendment-actor-artifact-rust-correction" + ], + "artifact_id": "artifact-round2-design" + } + ], + "challenges": [], + "horizons": [ + { + "id": "horizon-operational-protocol", + "status": "UNKNOWN", + "description": "End-to-end trustless reverification protocol", + "reason": "Round 2 records a design substrate; no complete operational protocol or independent implementation is included." + }, + { + "id": "horizon-anonymity", + "status": "OUT_OF_SCOPE", + "description": "Universal anonymity or unlinkability", + "reason": "Neither bounded reference mechanism establishes anonymity, unlinkability, identity uniqueness, or resistance to correlation outside its declared predicate." + }, + { + "id": "horizon-independent-reimplementation", + "status": "UNKNOWN", + "description": "Independent implementation by an unrelated party", + "reason": "No independent implementation has been demonstrated." + }, + { + "id": "horizon-contextual-role-protocol", + "status": "UNKNOWN", + "description": "Operational actor/artifact role and creation-edge protocol", + "reason": "The design states the contextual-role invariant but includes no approved schema or implementation." + }, + { + "id": "horizon-rot-current-admissibility", + "status": "UNKNOWN", + "description": "Typed ROT derivation and dependency-aware current-admissibility propagation", + "reason": "Some Graph queries enforce bounded lifecycle consequences, but no general ROT result, cross-surface binder, or propagation protocol is implemented." + }, + { + "id": "horizon-rust-memetic-backtrace", + "status": "UNKNOWN", + "description": "Typed backward RUST transfer, concentration, and localization over bound ancestry", + "reason": "The child-to-parent transfer states and concentration boundary are design-only; no event ledger, propagation implementation, or localization experiment is included." + }, + { + "id": "horizon-forward-artifact-trust", + "status": "UNKNOWN", + "description": "Scoped forward TRUST transfer through developmental claim space", + "reason": "The design requires meet-like bounded support and child-local obligations, but no transfer algebra or implementation is included." + } + ], + "publication": { + "state": "CANDIDATE", + "artifact_ids": [ + "artifact-zk-report", + "artifact-zk-self-test", + "artifact-zk-public-envelope", + "artifact-zk-receipt", + "artifact-zi-report", + "artifact-round2-design" + ] + }, + "workflow_events": [], + "interventions": [], + "manifest_digest": "sha256:98f1aaf7d5d7c8fa3cea8e3f58ce93c50111fbd1e41bf5e49ead8276b438e9dc" +} diff --git a/experiments/artifact_first_mechanisms/reverification/ROUND2_DESIGN_NOTE.md b/experiments/artifact_first_mechanisms/reverification/ROUND2_DESIGN_NOTE.md new file mode 100644 index 0000000..6e01181 --- /dev/null +++ b/experiments/artifact_first_mechanisms/reverification/ROUND2_DESIGN_NOTE.md @@ -0,0 +1,638 @@ +# Round 2 design note: operationalizing artifact-first reverification and RUST + +> **Acronyms:** Common Vulnerabilities and Exposures (CVE); identifier (ID); reduced instruction set computer (RISC); +> Secure Hash Algorithm 256-bit (SHA-256); scalable transparent argument of knowledge (STARK); +> Verifier Standard (VSTD); zero-identity (ZI); zero-knowledge (ZK); zero-knowledge virtual machine (zkVM). + +**Status:** experimental design note; non-normative; no wire profile is defined. + +TRUST, ROT, and RUST are formal Verifier Standard semantic names, not acronyms, wire +values, scalar scores, actor ratings, or references to the Rust programming language. + +This note uses **trustless** in one bounded sense: acceptance of a submitted +reverification result must not require knowing or trusting the submitter. It does not +mean assumption-free, trust-root-free, or immune to compromised software, unavailable +evidence, or false observations. + +The controlling semantic orientation is normative in +[`standard/LADDER.md` section 1.1](../../../standard/LADDER.md#11-artifact-first-causal-provenance-orientation). +This experiment does not decide whether that orientation is valid repository architecture; +it tests the still-open event format, transfer algebra, concentration rule, and localization +mechanics needed to operationalize it. + +The design decisions are: + +> Tier 0 is artifact-first: it binds claims, artifacts, predicates, verifier mechanisms, +> boundary snapshots, proofs, and checkable results. Actor identity, popularity, and +> reputation contribute no verdict weight and accumulate no TRUST. + +> Actor and artifact are contextual roles on creation and operation events, not disjoint +> kinds of entity. A coding agent can be an artifact when created, serialized, versioned, +> or evaluated and an actor when it performs a transformation or creates another artifact. + +> TRUST moves forward through checked developmental claim space; ROT describes typed, +> time-indexed degradation of current admissibility; and RUST genetically backtraces from +> descendant deviations toward recorded ancestor states. The three are artifact/process +> semantics, never actor standing, and do not collapse into a scalar. + +> Architectural zero knowledge presumes no unevidenced proposition. When its witness is +> confidential, cryptographic zero knowledge encloses the exact program, predicate, public +> commitments, output, proof parameters, and verifier without attaching TRUST to a prover. + +Authorship, authorization, issuer identity, organizational accountability, and descriptive +history may exist in adjacent optional profiles. Tier 0 may bind their coordinates when a +claim requires them, but their mere presence cannot strengthen the result. + +## 1. Where the research components stand + +Round 1 began from commit `598c545be3833d6d81bb7e252ca5837f3bb2a449`. + +| Work | Source coordinate | What it established | Round 2 treatment | +|---|---|---|---| +| Zero Identity | `claude/zizk-zero-identity` at `48fab87b05ad5ddaf24d08b6391cde99d05fc8f1` | A bounded identity-disclosure reference evaluation, with 22 fixtures and 65 focused tests | Retained as an adjacent reference mechanism; its coordinates carry only the claim meaning explicitly checked | +| Zero Knowledge | `codex/zizk-zero-knowledge` at `14d31e0426656c5208f2b6579a5217af3a6bb2bd` | A real RISC Zero zkVM 3.0.6 composite STARK receipt for one hidden-witness predicate | Retained as the confidential-evidence mechanism; its bearer, artifact-bound form is compatible with Tier 0 | +| Zero actor trust | this Round 2 design | No actor identity, popularity, or reputation may strengthen a result | Open as an operational protocol; stated here as a required invariant | +| TRUST | existing VSTD artifact, evidence, mechanism, and predicate bindings | Bounded positive support can move from verified parent artifacts into the declared obligations of descendants | Retained as the Tier 0 starting point; child obligations remain separately checked | +| ROT | `standard/LADDER.md` section 1.1 plus existing lifecycle status, current-admission, and blast-radius machinery | Typed lifecycle evidence can degrade current admissibility without rewriting historical receipts | Open as a general derivation and propagation protocol; some current Graph queries implement bounded consequences | +| Actor-artifact role semantics | no prior implementation | An entity's role depends on the creation or operation event; coding agents can occupy both roles | Open; this note corrects the earlier object-only partition | +| RUST | `standard/LADDER.md` section 1.1 | The inverse-TRUST diagnostic mechanic: a viral backtrace of measured deviation through bound creation ancestry, never an actor score or verdict | This note tests candidate relation-bound transfer and concentration mechanics beneath the governing term | + +One premise in the initial Round 2 plan is corrected here. The two finished halves did +**not** both put trust in credentials: + +- The Zero Identity half intentionally modeled a pseudonym, signing key, trust root, + issuer, authorization grant, and revocation source. Its conclusion follows from that + actor-bound problem definition. +- The Zero Knowledge half binds a subject digest, policy digest, challenge, threshold, + image ID, authenticated journal, and proof. It adds no actor coordinate and expressly + prohibits inferring identity, authorization, uniqueness, or independence. + +The architectural correction applies to the evidentiary effect of the Zero Identity +model, not to the existence of actor coordinates or to the Zero Knowledge proof. The +identity model is not discarded: it remains an optional adjacent profile for deployments +that need authorization or accountability. Its coordinates cannot become actor trust, +and the actor/artifact role model below prevents the profile boundary from becoming a +false permanent partition between parties and things. + +VSTD already contains much of the required substrate discipline. In particular, +`standard/VSTD-4.md` requires post-verdict checking without cooperation from the +declarant, disallows undeclared state from becoming verdict material, defines verifier +descriptors through content hashes, requires a checker that shares no verdict-producing +code, and requires a declared verification interface for confidential evidence. That +does not make every VSTD layer identity-free: observational evidence and external trust +roots still have sources. It makes actor identity unnecessary as verdict weight while +preserving any actor-artifact relation needed to state the bounded claim. + +## 2. Zero actor trust through artifact-first convergent recomputation + +### 2.1 Reverification unit + +A Tier 0 reverification attempt is defined over these public coordinates: + +1. **subject coordinate** — an artifact digest or an immutable receipt digest; +2. **statement coordinate** — the canonical claim and predicate digest; +3. **declared inputs** — content digests for every sealed input; +4. **boundary snapshot** — the content-addressed result of resolving every declared + external dependency under a pinned resolver policy; +5. **mechanism descriptor** — specification, implementation, and parser digests, plus a + proof-system program identifier or verification key when applicable; +6. **expected result** — the result committed by the claim being reverified; and +7. **observed result and trace** — enough public material to repeat the check, or to + verify a proof when the witness is confidential; and +8. **role-relation snapshot, when claim-relevant** — content-addressed creation, + execution, input, and output edges without converting an endpoint into verdict weight. + +An experimental event body can be modeled as: + +```text +ReverificationEventBody = { + subject_digest, + statement_digest, + declared_input_digests, + boundary_snapshot_digest, + resolver_policy_digest, + mechanism_descriptor_digest, + expected_result_digest, + observed_result_digest, + outcome, + trace_or_proof_digest, + role_relation_snapshot_digest?, + prior_event_digest +} + +event_id = SHA-256(canonicalize(ReverificationEventBody)) +``` + +This is a design sketch, not a new schema. Its names are not reserved serialized receipt identifiers. +Canonicalization, supported digest algorithms, event-chain rules, and admissible outcome +values require a later approved experiment before any schema can be proposed. + +No field identifies the submitter merely to weight the result. A claim-relevant role edge +may identify a bounded entity coordinate, but possession of a valid event confers no +authorization and proves no authorship. + +### 2.2 Actor and artifact are event-relative roles + +The model must not define permanent disjoint `Actor` and `Artifact` universes. It records +roles on bound events: + +- `produced_by(event, entity, output)` places `entity` in an actor role and `output` in an + artifact role for that creation event; +- `created_as(event, entity)` places the created entity in an artifact role; +- `executed_as(event, entity)` places a running entity in an actor role; and +- `used_as_input(event, entity)` may place the same entity in an artifact role for a + different operation. + +A coding-agent model, package, checkpoint, or executable is therefore an artifact of its +training or build event. A bound execution of it is an actor in a patch-producing event, +and the patch is an artifact. The roles follow declared creation and operation semantics; +they do not establish civil identity, authorship, ownership, authorization, independence, +or reputation. + +The precise event schema, instance coordinate, and relation vocabulary remain `OPEN`. +This note establishes only that erasing the relation or forcing a permanent category is +incorrect. + +### 2.3 Agreement is not trust + +Repeating the same deterministic implementation over the same frozen inputs is expected +to return the same result. Ten, one thousand, or one million matching submissions do not +make the result more true. They must not be counted as votes, averaged, or converted into +standing. + +Agreement can establish only the bounded fact that the accepted traces produced matching +outputs under their declared coordinates. An independently implemented checker can add a +different falsification opportunity because it may expose a specification or +implementation disagreement. Even then, agreement does not establish actor independence, +real-world truth, or a probability of correctness. + +### 2.4 Divergence is a falsification candidate, not self-certifying truth + +A submitted divergence is admissible only when the verifier can establish all of the +following without trusting the submitter: + +- both results bind the same subject, statement, declared inputs, resolver policy, and + comparison unit; +- the mechanism coordinates are explicit; +- the claim under test declared the relevant computation deterministic or otherwise + declared the expected equivalence relation; +- the divergent trace can be repeated, or its proof can be checked; and +- no hidden input, unpinned dependency, or incomparable environment explains the + difference. + +An admissible divergence can refute a declared determinacy or reproducibility claim. It +does not, by itself, determine which output is correct or establish truth outside the +predicate. If comparability is insufficient, the result is `UNKNOWN`. If admissible +evidence supports incompatible results, the result is `CONFLICTED`. + +Calling divergence **self-certifying** would be too strong. A malformed or +non-reproducible divergence report certifies nothing. Actor identity and Sybil resistance +are unnecessary for verdict material because duplicate or invalid reports cannot change +the result; however, anonymous spam can still create storage, bandwidth, and triage costs. +Rate limiting and admission control may address that operational denial-of-service risk, +but must not become evidence about correctness. + +## 3. Forward TRUST without actor-trust accumulation + +Tier 0 records both bounded TRUST and accepted opportunities to refute +a claim. It never turns either into a producer's or verifier's reputation, and a claim +does not gain standing merely by surviving repeated attempts. + +TRUST is a positive signal bound to an exact artifact, process claim, predicate, +mechanism, evidence set, boundary snapshot, and time coordinate. It moves forward only +through a declared creation or dependency edge whose transformation obligations pass. A +child receives the intersection of applicable parent support, capped by the weakest +required parent and edge; it does not receive a sum, vote, average, or confidence boost. +The child must still discharge every new predicate, transformation, and boundary +obligation it introduces. + +In schematic form: + +```text +development: ancestor artifact --TRUST through a checked transformation--> descendant +lifecycle: recorded TRUST --ROT under typed current-state evidence--> reassessment +diagnosis: descendant deviation --RUST memetic causal backtrace--> ancestor candidates +``` + +`UNKNOWN`, `CONFLICTED`, revoked, unavailable, or out-of-scope parent support cannot be +laundered into a clean child signal. Repeating the same parent coordinate does not create +additional support. Actor identity and reputation do not participate in the transfer. + +| Tier 0 event outcome | Bounded interpretation | Forbidden interpretation | +|---|---|---| +| matching result | this accepted check matched the committed result and may satisfy one declared child obligation | the claim, submitter, or mechanism is globally trustworthy | +| admissible divergence | the declared equivalence or reproducibility condition has a checkable counterexample | the divergent result is automatically the true result | +| unresolved boundary | required material could not be resolved or checked; preserve `UNKNOWN` | missing evidence is clean evidence | +| incompatible admissible records | preserve `CONFLICTED` and expose both records | choose the more popular result | + +The Tier 0 state is a function over immutable records. Its TRUST is +typed and scoped; it does not have a cumulative confidence counter, majority rule, actor +weight, or time-decayed reputation. + +Tier 1 may provide descriptive analysis over Tier 0 events. Tier 1 is optional, +non-normative, and forbidden from supplying `PASS`, `FAIL`, `UNKNOWN`, `CONFLICTED`, +`VALID`, `STALE`, or any ladder result. Evidence in one layer does not silently supply +evidence in another. + +## 4. RUST: an optional relation-bound deviation ledger + +**RUST** is the governing name for the inverse-TRUST diagnostic mechanic. This section +tests one candidate Tier 1 view of past measured deviation. Its inverse is directional and +diagnostic, not arithmetic: RUST does not cancel TRUST, create actor distrust, supply a +verdict, or predict behavior. + +### 4.1 Bound relations, not actor reputation + +RUST may bind only to immutable coordinates and explicitly typed relations: + +- an artifact digest; +- a statement or predicate digest; +- a specification, implementation, and parser digest tuple; or +- a content-addressed creation or transformation edge; +- a contextual actor-role/artifact relation for one declared event; or +- an explicit basin coordinate defining a common comparison unit. + +It does not create a scalar score for a person, pseudonym, account, organization, author, +issuer, key holder, submitter, or coding agent. An actor coordinate may be a relation +endpoint, but RUST cannot aggregate upward across unrelated artifacts, predicates, +operations, or basins and cannot change a Tier 0 verdict. + +A coding agent may therefore participate only through exact software-artifact, execution, +creation, or transformation coordinates. RUST attaches to those bounded process or +relation coordinates, never to the person, account, organization, or agent identity as a +good/bad score. Records remain separate unless an explicit common comparison unit and +evidence justify composition. + +Content addressing prevents an unchanged byte sequence from shedding its history while +keeping the same digest. It does **not** eliminate whitewashing in general: a trivial +repackaging, semantically near-identical fork, or changed mechanism descriptor creates a +new coordinate. Because recorded ancestry does not establish that a defect transferred, +the parent's RUST cannot be copied into the descendant. The old coordinate and relation +records remain, while any inference about the new entity or relation remains `UNKNOWN` +until measured. + +### 4.2 Exposure denominator and deduplication + +RUST requires an exposure denominator so that no observation and many observations are +not confused. An exposure is not a submitted run. It is a unique, admissible opportunity +for the declared expectation to fail. + +The proposed exposure key is the digest of: + +```text +(subject, statement, mechanism, role relation, comparison unit, boundary snapshot) +``` + +Identical submissions collapse to one exposure. A different submitter does not create a +new exposure. A new exposure requires a distinct admissible test vector, independently +implemented mechanism, or resolved boundary snapshot that can reveal something not fixed +by the earlier event. + +For every basin, report at least: + +- `exposure_count` — unique admissible exposure keys; +- the ordered deviation observations; +- the deviation mean for each declared horizon; +- dispersion for each horizon; and +- the number of `UNKNOWN` and `CONFLICTED` comparisons excluded from numerical + aggregation and still reported separately. + +`exposure_count = 0` is `UNKNOWN`, not clean. A positive exposure count with zero measured +deviation is still only a history of observed matches, not a favorable verdict. + +### 4.3 Reference and measured quantity + +The reference is the claim's own declared expectation: committed output, tolerance, +falsification condition, availability condition, reproducibility level, or other bounded +predicate. RUST therefore records **deviation from a declared expectation**, not error +against unknowable real-world truth. + +Each expectation type needs a fixed comparison rule outside the measured relation +endpoints' control. Examples include binary mismatch, normalized numeric error under a +declared unit, or set-distance under a fixed canonicalizer. A measured endpoint must not +choose a weaker penalty after seeing a result. + +`UNKNOWN` and `CONFLICTED` are not numeric zero. They remain typed observations. A +comparison that lacks a common unit is not forced into a number. + +### 4.4 Memetic backtrace, basins, and horizons + +A **basin** is an explicitly described analytical grouping of events that share a +predicate, mechanism family, comparison unit, and deviation rule. Clustering may suggest +a basin, but a clustering algorithm does not establish that the members are comparable. +The basin definition and its digest must be published with the view. + +RUST acts as a viral **truth-disease backtrace** through causal provenance. A directly +measured descendant deviation is the source event, and the recorded creation and input +graph determines which ancestor states receive the memetic trace. The genetic metaphor +names transmission through developmental ancestry: actor/artifact role edges allow the +trace to cross a coding-agent execution into the bound model, package, checkpoint, or +executable that acted and then into that artifact's own creation ancestry. Transmission +establishes provenance reachability; localization still requires its own evidence. + +Propagation is typed rather than silently re-described as direct observation: + +| RUST state | Meaning | +|---|---| +| `OBSERVED` | the deviation was measured directly at this descendant coordinate | +| `TRANSFERRED` | an observed RUST event reached this ancestor through a recorded admissible path | +| `LOCALIZED` | additional evidence identifies this ancestor or edge as contributing to the deviation | +| `UNKNOWN` | the required lineage or edge semantics are incomplete or unavailable | +| `CONFLICTED` | admissible backtraces disagree about the relation or contribution | + +For each admissible path from ancestor `a` to rusted descendant `d`, the transferred state +must bind at least the source RUST event, `a`, `d`, every traversed edge digest, the +predicate, mechanism, comparison unit, basin, and transfer rule. A source event, ancestor, +and path tuple is counted once. Mere co-occurrence, reference, authorship, or identity is +not a transmission edge. An unknown edge stops that path and preserves `UNKNOWN`. + +RUST **concentrates** where distinct descendant infection events share an ancestor. For an +ancestor and basin, the concentration record is the set of unique source RUST event and +admissible path digests that reach it. Multiple paths or duplicate reports of one source +event remain visible but do not multiply its weight. Intersections of independent +backtraces prioritize earlier claims, predicates, mechanisms, or artifacts for diagnostic +examination because they are common candidate loci of falsehood. + +`TRANSFERRED` is actual RUST inheritance, but it is not a claim that the ancestor was +directly measured or proved causal. `LOCALIZED` requires an intervention, ablation, +an independently bound execution or mechanism, or other declared evidence that +distinguishes contribution from ancestry. The backtrace is append-only, does not decay, +and does not alter existing +Verifier Standard status or blast-radius calculations until a separate normative rule is +approved. + +Horizon summaries are indexed by accepted exposures rather than wall-clock time, for +example the last 10, 100, and 1,000 exposures plus lifetime. The complete vector and its +dispersion are reported. It is not collapsed into one rankable scalar. + +### 4.5 Dual causal representation + +Forward TRUST and backward RUST are messages over the same directed development graph, +not positive and negative values on one scalar. The forward message asks which +bounded parent obligations are available to a child. The backward message asks which +recorded ancestors can explain an observed child deviation. A node may carry both without +cancellation: positive support for one predicate does not erase RUST for another, and +RUST on one descendant does not erase unrelated support. + +The recorded causal-provenance graph therefore represents both the generative direction +used to explain how claims and artifacts develop and the diagnostic direction used to +identify where a later contradiction may have entered the architecture. This memetic +propagation is causally meaningful as recorded provenance without, by traversal alone, +establishing intervention-level physical causality or causal localization. + +### 4.6 Prior art boundary + +Proper scoring rules provide a lower-is-better penalty analogy when a claim is genuinely +probabilistic; the original references include [Brier (1950)](https://journals.ametsoc.org/view/journals/mwre/78/1/1520-0493_1950_078_0001_vofeit_2_0_co_2.xml) +and [Good (1952)](https://rss.onlinelibrary.wiley.com/doi/10.1111/j.2517-6161.1952.tb00104.x). +RUST is not itself a proper scoring rule unless its declared expectation and comparison +rule satisfy the corresponding conditions. + +[Friedman and Resnick (2001)](https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1430-9134.2001.00173.x) +analyze the social cost of cheap pseudonyms in party reputation systems. Content and +relation binding change the coordinate being measured, but, as noted above, do not prove +that semantically equivalent repackaging is impossible. + +[CVE](https://www.cve.org/), [OpenSSF Scorecard](https://openssf.org/scorecard/), and +[Certificate Transparency](https://www.rfc-editor.org/rfc/rfc9162.html) are useful +comparisons for public negative signals, automated project checks, and append-only public +records. They are not equivalent mechanisms and do not validate this design. This note +claims only a proposed composition of relation-bound deviation, memetic causal backtrace, +exposure deduplication, typed uncertainty, and separation from verdict material. It makes +no novelty claim. + +## 5. The information-free limit + +Re-running the exact same pure deterministic function, implementation, and frozen inputs +is expected to be information-poor after the first successful check. It exercises the +plumbing again but introduces no new world state. + +Reverification can add information in three places: + +1. **independent implementation:** a checker with no shared verdict-producing code may + reveal a specification or implementation disagreement; +2. **new bounded test vector:** a previously unexercised input may falsify a general + declaration; and +3. **boundary re-resolution:** an external artifact can be rehashed, a reference can be + resolved again under a pinned policy, or a declared revocation/availability source can + expose a changed state. + +The sealed core and the changing boundary must remain distinguishable. A boundary +snapshot is content-addressed; the fact that it was retrieved later is not itself verdict +material. If a current boundary cannot be resolved, freshness is `UNKNOWN`. Absence of a +new event never proves that an old event remains current. + +For a hidden witness, cryptographic zero knowledge encloses architectural zero knowledge: +proof verification is the repeatable public interface around the exact program, predicate, +commitments, journal, parameters, and verifier. It proves only the execution and journal +bound by the selected proof system. It does not reveal or independently observe the +witness, establish that the witness was externally truthful, or add prover-identity TRUST. + +## 6. ROT: proposed `STALE` entry and successor semantics + +ROT is typed, time-indexed degradation of current admissibility. It is not passive decay, +an actor-reputation penalty, or a rewrite of historical truth. This section tests one +possible ROT entry condition for the existing `STALE` states; it does not define a general +ROT serialized receipt format or transition algebra. + +This repository currently has two distinct `STALE` enum members: + +- `CoordinateStatus.STALE` in `src/verifier/core/geometry.py`, serialized by the + VSTD-2 receipt schema; and +- `ArtifactStatus.STALE` in `src/verifier/data/models.py`, consumed by graph admission and + the Layer 4 degradation order. + +They are not the same type. Both tokens are existing wire vocabulary, and this note does +not change or reserve their meaning. + +The proposed non-normative entry rule is: + +> A coordinate or artifact is derivably `STALE` only when an accepted append-only +> reverification event, under the subject's declared resolver policy, shows that an +> external binding on which the earlier result depended no longer resolves to the content +> or admissible state committed by that earlier result. + +Required event evidence includes the prior receipt digest, subject kind and digest, +resolver-policy digest, previous boundary snapshot digest, newly resolved boundary +snapshot digest, and a repeatable trace or verifiable proof of the mismatch. Mere age, +wall-clock passage, missing availability, accumulated RUST, popularity, or a reporter's +assertion is not a `STALE` entry condition. + +If the boundary cannot be resolved, the result is `UNKNOWN`, not `STALE`. If two +admissible current snapshots are incompatible and the resolver policy does not order +them, the result is `CONFLICTED`, not silently selected. + +There is no historical mutation and therefore no literal exit from `STALE`. The earlier +receipt and its derived stale event remain immutable. Recovery creates a successor +receipt or coordinate bound to the new boundary state. That successor can be evaluated +on its own evidence; it does not cleanse the earlier coordinate. A current-view function +may follow an append-only, digest-linked event chain to the declared head, but an absent or +unavailable head leaves the current view `UNKNOWN`. + +Before implementation, separate transition functions are required for +`CoordinateStatus` and `ArtifactStatus`; shared prose is not permission to conflate the +two frozen enum families. Ledger ordering, fork handling, inclusion proofs, and resolver +trust coordinates also remain to be specified. + +RUST has no ROT status-transition role. A RUST view may describe deviations that accompanied +a stale event, but no magnitude of historical deviation is sufficient to produce or clear +`STALE`. + +## 7. Zero-knowledge trichotomy correction + +The Round 1 guest accepts only `CandidateState::Supported` and always commits +`predicate_satisfied: true`. A valid proof therefore authenticates one favorable path. +Failure to present a proof is ambiguous among no attempt, prover failure, an unsatisfied +witness, `UNKNOWN`, and `CONFLICTED`. + +The exact proposed journal shape change is: + +```rust +pub struct PublicJournal { + // Existing binding fields remain. + pub verdict: CandidateState, + pub predicate_satisfied: bool, +} +``` + +Required invariant: + +```text +predicate_satisfied == true if and only if verdict == Supported + and the fixed predicate is satisfied +``` + +`Unknown` and `Conflicted` must be valid authenticated journal outcomes when the guest's +fixed rules derive them. They must never be encoded as a missing proof. The existing +assertion that rejects both states would be replaced by a total verdict calculation, and +the public-envelope checker would compare both fields to the authenticated journal. + +This structural correction is necessary but not sufficient. The current private witness +contains a caller-supplied candidate state and only one measurement. It lacks the evidence +structure needed to **derive** a conflict or to distinguish genuine insufficiency from a +caller merely labeling an input `Unknown`. Publishing a private input tag as a public +verdict would authenticate the tag, not establish the verdict. Before implementation, the +fixed predicate must define how all three states are derived from bounded witness data and +must add enough witness structure to derive `Conflicted`. + +No change is made in Round 2. The existing proof remains accurately described as one real +proof for one favorable bounded predicate, not as a full trichotomy implementation. + +## 8. What the Zero Identity half keeps and loses + +### Kept as portable discipline + +- Missing evidence stays `UNKNOWN`. +- Contradictory admissible evidence stays `CONFLICTED`. +- Minimization may narrow a claim boundary but must not widen one. +- Recorded ancestry does not establish that an authority, property, or defect transferred + across every edge. +- Semantic results, external attestations, declared assumptions, and protocol guarantees + remain separate evidence classes. +- The 19 prohibited inferences remain useful in the optional actor-facing profile. The + actor-agnostic subset also constrains Tier 0: missing evidence is not safety; recorded + ancestry is not established influence; and one evidence class cannot silently upgrade + another. + +### Forbidden as automatic trust or verdict weight + +- pseudonym or signing-key identity; +- issuer, authorization grant, actor trust root, or revocation source; +- uniqueness, Sybil-resistance, independence, or accountability claims; and +- authorship degree or credential ancestry. + +These coordinates may still be bound when the declared claim needs them. None is a +general trust signal, none upgrades an artifact result, and none permanently classifies an +entity as an actor rather than an artifact. + +Deployments may still use the bounded identity-disclosure reference +model alongside Tier 0 when they need authenticated authorization. Its results must not +raise or lower the artifact-bound reverification result. + +### Recorded model-to-code drift + +`model/zero_identity_model.json` lists 13 `minimum_public_actor_coordinates` and seven +`optional_provenance_coordinates`. `evaluate.py` defines six structural +`REQUIRED_PUBLIC_COORDINATES`; other coordinates are checked later by individual rules. +The tests currently do not enforce equality between the declarative list and the +structural list. + +This mismatch does not justify a favorable result from missing evidence—the individual +rules generally preserve `UNKNOWN` or reject—but it makes the declarative contract stale. +The follow-on should establish one source of truth and add a containment test. It is not +changed in this design-only round. + +## 9. Explicit non-claims + +This design does not establish or claim: + +- anonymity, unlinkability, untraceability, confidentiality, or protection from traffic + analysis; +- actor uniqueness, actor independence, authorization, accountability, or Sybil + resistance; +- that all operational abuse is harmless; identity-less submission still permits spam + and resource exhaustion; +- real-world truth, complete evidence, honest witnesses, or correct external observations; +- that a divergent output is automatically correct; +- that repeated agreement increases trust, probability, ladder level, or status; +- that trustless means no trust roots, no cryptographic assumptions, or no trusted + software; +- that pure recomputation supplies new information under unchanged coordinates; +- that `STALE` can be inferred from elapsed time, RUST, missing records, or popularity; +- that a RUST history predicts future behavior; it records only past measured deviation; +- that RUST is comparable across predicates, units, or basins; +- that a relation-bound RUST ledger prevents semantically equivalent repackaging; +- that `TRANSFERRED` proves direct observation, causation, intent, or fault at an ancestor; +- that RUST concentration is a probability of guilt or a substitute for localization; +- that forward TRUST proves a child claim without its own transformation and + predicate evidence; +- that authorship is actor identity, or that either is required by Tier 0; +- independent implementation, external audit, external adoption, production readiness, + or a security review of this synthesis; +- a new ladder rung, conformance requirement, schema, lifecycle token, or serialized receipt contract + identifier; or +- novelty of the individual ingredients or of their proposed composition. + +## 10. Open questions + +1. **Transmission rules:** which typed creation, input, execution, and transformation + edges admit RUST transfer, and which reference-only edges stop it? +2. **Localization:** which intervention or independent evidence promotes inherited + `TRANSFERRED` RUST to `LOCALIZED` contribution? +3. **Role coordinates:** how are a coding-agent artifact and its bound acting instance + related without claiming they are identical or permanently assigning either category? +4. **Forward trust transfer:** which parent evidence classes and edge checks supply a + bounded positive signal to each child obligation, and how is the weakest required + support preserved? +5. **Concentration:** which independence and basin conditions let intersecting backtraces + prioritize a common ancestor without converting frequency into causal proof? +6. **Deviation rules:** which fixed magnitude rule applies to each expectation type, and + who may define it without letting the measured object tune its own penalty? +7. **Cross-basin comparison:** should comparison be explicitly undefined unless predicate, + unit, mechanism class, and deviation rule all match? +8. **Exposure admission:** which distinct test vectors and boundary snapshots are + sufficiently non-duplicative to count as new falsification opportunities without + converting an actor-role coordinate into actor trust? +9. **Independent implementation:** what evidence is sufficient to show that two checkers + share no verdict-producing code? +10. **Ledger convergence:** how are concurrent append-only event branches, unavailable log + heads, and resolver equivocation represented without a privileged mutable registry? +11. **`STALE` governance:** should `CoordinateStatus.STALE` and `ArtifactStatus.STALE` share + one abstract event model while retaining separate transition functions and schemas? +12. **ZK trichotomy:** what bounded private witness structure lets the guest derive + `Supported`, `Unknown`, and `Conflicted` rather than authenticate a caller's label? +13. **Observational evidence:** VSTD-3 device observations cannot be recreated from artifact + bytes. What source testimony and attestation assumptions must a boundary snapshot expose + without turning the observer's identity into verdict weight? +14. **Author is not actor:** if optional authorship is later marked inside artifact bytes, + the mark changes the content digest. Should authorship instead use a detached, + separately content-addressed statement, and what claim could it safely support? +15. **ZI declarative drift:** should the evaluator import a generated coordinate contract, + or should a repository containment test require the model and code lists to agree? +16. **Operational controls:** how can anonymous admission control limit denial-of-service + without becoming a correctness signal or a de facto identity requirement? + +Round 2 takes the normative role and propagation directions as input but implements none +of these open mechanics. The next safe step is a bounded dual-direction event-ledger +experiment with no new serialized receipt identifier, followed separately by the ZK trichotomy +experiment once its derivation rule is specified. diff --git a/tests/test_zizk_artifact_first.py b/tests/test_zizk_artifact_first.py new file mode 100644 index 0000000..5d54b00 --- /dev/null +++ b/tests/test_zizk_artifact_first.py @@ -0,0 +1,130 @@ +"""Terminology: Verifier Standard (VSTD).""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +import subprocess + + +ROOT = Path(__file__).resolve().parents[1] +MECHANISM = ROOT / "examples" / "zizk_artifact_first" / "risc0" + + +def test_zero_knowledge_mechanism_is_optional_and_pinned() -> None: + host_manifest = (MECHANISM / "host" / "Cargo.toml").read_text(encoding="utf-8") + guest_manifest = ( + MECHANISM / "methods" / "guest" / "Cargo.toml" + ).read_text(encoding="utf-8") + methods_manifest = (MECHANISM / "methods" / "Cargo.toml").read_text( + encoding="utf-8" + ) + + assert 'version = "=3.0.6"' in host_manifest + assert 'features = ["disable-dev-mode"]' in host_manifest + assert 'version = "=3.0.6"' in guest_manifest + assert 'version = "=3.0.6"' in methods_manifest + assert "zizk" not in (ROOT / "pyproject.toml").read_text(encoding="utf-8").lower() + + +def test_zero_knowledge_claim_boundary_is_explicit() -> None: + boundary = (MECHANISM / "CLAIM_BOUNDARY.md").read_text(encoding="utf-8") + assert "does not prove" in boundary + assert "bounded reference mechanism" in boundary + assert "UNKNOWN" in boundary + assert "CONFLICTED" in boundary + + +def test_experimental_scope_does_not_absorb_the_governing_architecture() -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + architecture = (ROOT / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + experiment_index = ( + ROOT / "experiments" / "artifact_first_mechanisms" / "README.md" + ).read_text(encoding="utf-8") + design = ( + ROOT + / "experiments" + / "artifact_first_mechanisms" + / "reverification" + / "ROUND2_DESIGN_NOTE.md" + ).read_text(encoding="utf-8") + + assert "Governing VSTD architecture" in readme + assert "not an optional research" in readme + assert "architecture, not a side experiment" in architecture + for phrase in ( + "event serialization", + "TRUST-transfer algebra", + "ROT derivation and propagation", + "RUST concentration and localization", + "complete hidden-witness trichotomy derivation", + "specific optional proof backends", + ): + assert phrase in experiment_index + assert "semantic experiment for bounded identity disclosure" not in design + assert "Zero Identity experiment" not in design + assert "bounded identity-disclosure reference" in design + + +def test_zizk_preserves_memetic_causality_without_localization_overclaim() -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + ladder = (ROOT / "standard" / "LADDER.md").read_text(encoding="utf-8") + mechanism = (MECHANISM / "README.md").read_text(encoding="utf-8") + + assert "Zero identity means zero identity-derived verdict weight" in readme + assert "zero unevidenced knowledge is presumed" in readme + assert "cryptographic zero knowledge can enclose" in readme + assert "without attaching TRUST to the prover's identity" in readme + assert "TRUST is mechanism-earned artifact support" in readme + assert "ROT is typed, time-indexed degradation" in readme + assert "RUST is the inverse-TRUST diagnostic mechanic" in readme + assert "cryptographic zero-knowledge\nenclosure" in mechanism + assert "without importing prover identity into TRUST" in mechanism + assert "memetic causal backtrace" in ladder + assert "genetic or viral language names this inheritance mechanic" in ladder + assert "does not by itself establish\nintervention-level physical causality" in ladder + + +def test_private_inputs_are_excluded_and_public_proof_artifacts_are_versioned() -> None: + ignore = (MECHANISM / ".gitignore").read_text(encoding="utf-8") + assert "private-*.json" in ignore + assert "local-artifacts/" in ignore + tracked = subprocess.run( + ["git", "ls-files", "--", "examples/zizk_artifact_first/risc0"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.splitlines() + assert any(path.endswith("recorded-proof/receipt.msgpack") for path in tracked) + assert any(path.endswith("recorded-proof/public.json") for path in tracked) + assert any(path.endswith("recorded-proof/self-test-results.json") for path in tracked) + assert not any("private-" in path and path.endswith(".json") for path in tracked) + + +def test_recorded_public_proof_artifact_hashes_match_the_reported_run() -> None: + expected = { + "receipt.msgpack": "5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df", + "public.json": "6324c3c5d77ea4df4034f61131059289d5228f190d69e34c59bd7416fa9ac823", + "self-test-results.json": "e4c1bff21fb6161221276157fa96af6661af8635da35970ba12e462881f2c6fe", + } + for name, digest in expected.items(): + artifact = MECHANISM / "recorded-proof" / name + assert hashlib.sha256(artifact.read_bytes()).hexdigest() == digest + + +def test_recorded_verification_pins_the_historical_program_trust_coordinate() -> None: + public = json.loads( + (MECHANISM / "recorded-proof" / "public.json").read_text(encoding="utf-8") + ) + expected_image_id = public["image_id"] + script = (MECHANISM / "scripts" / "verify_recorded_proof.sh").read_text( + encoding="utf-8" + ) + host = (MECHANISM / "host" / "src" / "main.rs").read_text(encoding="utf-8") + + assert expected_image_id in script + assert "verify recorded-proof/receipt.msgpack recorded-proof/public.json" in script + assert "let trusted_id = expected_id.unwrap_or_else(method_id);" in host + assert "trusted_id != method_id()" not in host From 3cc2186c648edd0f3e908d3b0bbf25d8b92684a1 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:47:14 -0700 Subject: [PATCH 07/34] Ground VSTD-3 execution evidence Reason: Keep runtime, software, machine, provider, and optional actor coordinates distinct while exposing exactly what hardware evidence establishes. Evidence: Hardware models, adapters, provenance checks, command handlers, renamed profile documentation, and capability tests change together. Coordinate: VSTD-3 execution and substrate evidence; release 1.2.0. Falsification: Assignment implies trust or responsibility, provider evidence exceeds its binding, or an unsupported substrate validates cleanly. Compatibility: Existing VSTD-3 serialized identifiers remain exact; documentation moves from ambiguous layer naming to numbered-profile terminology. --- .../vstd-3/compatibility.md | 20 +++++++++++++------ .../{layers => profiles}/vstd-3/references.md | 13 +++++++++++- .../vstd-3/threat-model.md | 10 ++++++++-- .../vstd-3/vendor-integration.md | 12 ++++++++++- src/verifier/hardware/__init__.py | 4 +++- src/verifier/hardware/adapters/__init__.py | 4 +++- src/verifier/hardware/adapters/amd.py | 5 ++++- src/verifier/hardware/adapters/generic.py | 4 +++- src/verifier/hardware/adapters/nvidia.py | 6 +++++- src/verifier/hardware/adapters/provider.py | 4 +++- src/verifier/hardware/anchors.py | 4 +++- src/verifier/hardware/attestation.py | 16 +++++++++++---- src/verifier/hardware/canonical.py | 5 ++++- src/verifier/hardware/claims.py | 4 +++- src/verifier/hardware/conformance.py | 4 +++- src/verifier/hardware/continuity.py | 5 ++++- src/verifier/hardware/emulator.py | 6 +++++- src/verifier/hardware/fleet.py | 4 +++- src/verifier/hardware/models.py | 6 +++++- src/verifier/hardware/provenance.py | 8 +++++--- src/verifier/hardware/provider_evidence.py | 7 ++++++- src/verifier/hardware/receipt.py | 4 +++- src/verifier/hardware/registry.py | 4 +++- src/verifier/hardware/schema.py | 12 ++++++++++- src/verifier/hardware/validation.py | 14 +++++++------ src/verifier/runtime/hardware_cli.py | 9 +++++++-- tests/test_vstd3_capabilities.py | 3 +++ tests/test_vstd3_cli.py | 2 ++ tests/test_vstd3_emulator.py | 7 ++++++- tests/test_vstd3_provenance.py | 1 + tests/test_vstd3_schema.py | 2 ++ 31 files changed, 165 insertions(+), 44 deletions(-) rename docs/{layers => profiles}/vstd-3/compatibility.md (66%) rename docs/{layers => profiles}/vstd-3/references.md (77%) rename docs/{layers => profiles}/vstd-3/threat-model.md (93%) rename docs/{layers => profiles}/vstd-3/vendor-integration.md (90%) diff --git a/docs/layers/vstd-3/compatibility.md b/docs/profiles/vstd-3/compatibility.md similarity index 66% rename from docs/layers/vstd-3/compatibility.md rename to docs/profiles/vstd-3/compatibility.md index 01f9ba8..ad6a2b4 100644 --- a/docs/layers/vstd-3/compatibility.md +++ b/docs/profiles/vstd-3/compatibility.md @@ -1,14 +1,22 @@ -# VSTD-3 implementation compatibility +# Verifier Standard (VSTD)-3 implementation compatibility -VSTD-3 is additive. It does not reinterpret earlier receipt wire formats. For -the historical filename and wire-identifier table, see +> Reader aid: [concept glossary and primary precedents](../../CONCEPTS_AND_PRECEDENTS.md). + +VSTD-3 is additive. It does not reinterpret adjacent receipt surfaces. For the current +wire-identifier table, see `../../../standard/WIRE_IDENTIFIERS.md`. +The currently shipped adapter boundary is centralized in +[`docs/CLAIMS_AND_LIMITS.md`](../../CLAIMS_AND_LIMITS.md#what-the-current-adapters-can-say): +host-visible metadata is not device attestation, and the virtual accelerator establishes +only its emulator-scoped claims. + ## Existing receipts -- `VSTD-0.1` receipt validators keep their existing VSTD-1 wire semantics. +- `VSTD-1` claim-mechanics and generic-run receipts retain their separate required + `receipt_kind` values. - `VSTD-DATA-0.1` hypergraphs remain readable as historical VSTD-Graph-1 receipts. -- `VSTD-0.2` geometry remains the frozen VSTD-2 wire surface. +- `VSTD-2` geometry remains a separate verification-surface receipt. - The public `validate`, `inspect`, `reproduce`, `data`, and `impact` commands retain their earlier behavior. @@ -42,5 +50,5 @@ execution records are not converted into device attestation without new evidence ## Schema/version dispatch -Dispatch by exact `schema_version`. VSTD-3 receipts use the frozen `VSTD-3.0` wire identifier. Unknown versions +Dispatch by exact `schema_version`. VSTD-3 receipts use the serialized receipt identifier `VSTD-3.0`. Unknown versions must fail closed. Do not guess a compatible decoder from field similarity. diff --git a/docs/layers/vstd-3/references.md b/docs/profiles/vstd-3/references.md similarity index 77% rename from docs/layers/vstd-3/references.md rename to docs/profiles/vstd-3/references.md index f59b215..0ea44f9 100644 --- a/docs/layers/vstd-3/references.md +++ b/docs/profiles/vstd-3/references.md @@ -1,4 +1,15 @@ -# VSTD-3 official public references +# Verifier Standard (VSTD)-3 official public references + +> **Acronyms:** Advanced Micro Devices (AMD); application programming interface (API); Amazon Web Services (AWS); +> command-line interface (CLI); Device Identifier Composition Engine (DICE); DMTF standards organization (DMTF); +> design of experiments (DOE); Engineering Change Notice (ECN); graphics processing unit (GPU); +> integrated development environment (IDE); Internet Engineering Task Force (IETF); multi-instance GPU (MIG); +> NVIDIA Management Library (NVML); Peripheral Component Interconnect (PCI); PCI Special Interest Group (PCI-SIG); +> Remote Attestation Procedures (RATS); Request for Comments (RFC); Reference Integrity Manifest (RIM); +> software development kit (SDK); system management interface (SMI); Security Protocol and Data Model (SPDM); +> Trusted Device Interface Security Protocol (TDISP). + +> Reader aid: [cross-profile concept glossary and primary precedents](../../CONCEPTS_AND_PRECEDENTS.md). **Retrieved:** 2026-08-21 diff --git a/docs/layers/vstd-3/threat-model.md b/docs/profiles/vstd-3/threat-model.md similarity index 93% rename from docs/layers/vstd-3/threat-model.md rename to docs/profiles/vstd-3/threat-model.md index b697e40..0a99d08 100644 --- a/docs/layers/vstd-3/threat-model.md +++ b/docs/profiles/vstd-3/threat-model.md @@ -1,6 +1,12 @@ -# VSTD-3 threat model +# Verifier Standard (VSTD)-3 threat model -**Layer:** VSTD-3; historical receipt wire identifier `VSTD-3.0` +> **Acronyms:** Advanced Micro Devices (AMD); command-line interface (CLI); +> hash-based message authentication code (HMAC); identifier (ID); trusted computing base (TCB); +> Coordinated Universal Time (UTC); virtual machine (VM). + +> Reader aid: [concept glossary and primary precedents](../../CONCEPTS_AND_PRECEDENTS.md). + +**Numbered profile:** VSTD-3; required closure coordinate: Substrate Accountability; serialized receipt identifier `VSTD-3.0` **Purpose:** defensive verification and conformance; not offensive exploit guidance ## Boundary diff --git a/docs/layers/vstd-3/vendor-integration.md b/docs/profiles/vstd-3/vendor-integration.md similarity index 90% rename from docs/layers/vstd-3/vendor-integration.md rename to docs/profiles/vstd-3/vendor-integration.md index 91d9671..dba3255 100644 --- a/docs/layers/vstd-3/vendor-integration.md +++ b/docs/profiles/vstd-3/vendor-integration.md @@ -1,8 +1,18 @@ -# VSTD-3 accelerator vendor integration kit +# Verifier Standard (VSTD)-3 accelerator vendor integration kit + +> **Acronyms:** graphics processing unit (GPU); identifier (ID); multi-instance GPU (MIG); +> single-root input/output virtualization (SR-IOV). + +> Reader aid: [concept glossary and primary precedents](../../CONCEPTS_AND_PRECEDENTS.md). This is the minimum review surface for a firmware or silicon security team evaluating VSTD-3. It does not require adopting VSTD product names in firmware. +Before selecting a profile, read the centralized +[`current-adapter claim boundary`](../../CLAIMS_AND_LIMITS.md#what-the-current-adapters-can-say). +Host-visible metadata is not device attestation, and the virtual accelerator establishes +only its emulator-scoped claims. + ## 1. Select the honest profile Implement only the profiles the device can demonstrate: diff --git a/src/verifier/hardware/__init__.py b/src/verifier/hardware/__init__.py index af906bc..8db00e6 100644 --- a/src/verifier/hardware/__init__.py +++ b/src/verifier/hardware/__init__.py @@ -1,4 +1,6 @@ -"""VSTD 3 accelerator-accountability reference implementation.""" +"""Terminology: Verifier Standard (VSTD). + +VSTD 3 accelerator-accountability reference implementation.""" from .conformance import ConformanceProfile, evaluate_conformance from .emulator import VirtualVSTDAccelerator diff --git a/src/verifier/hardware/adapters/__init__.py b/src/verifier/hardware/adapters/__init__.py index dffde60..5f0584c 100644 --- a/src/verifier/hardware/adapters/__init__.py +++ b/src/verifier/hardware/adapters/__init__.py @@ -1,4 +1,6 @@ -"""Built-in VSTD 3 evidence adapters.""" +"""Terminology: Verifier Standard (VSTD). + +Built-in VSTD 3 evidence adapters.""" from .amd import AmdAdapter from .base import AdapterError, EvidenceAdapter diff --git a/src/verifier/hardware/adapters/amd.py b/src/verifier/hardware/adapters/amd.py index 9d4f22a..8175d48 100644 --- a/src/verifier/hardware/adapters/amd.py +++ b/src/verifier/hardware/adapters/amd.py @@ -1,4 +1,7 @@ -"""AMD SMI/ROCm discovery and offline evidence normalization.""" +"""Terminology: Advanced Micro Devices (AMD); application-specific integrated circuit (ASIC); +JavaScript Object Notation (JSON); system management interface (SMI); Verifier Standard (VSTD). + +AMD SMI/ROCm discovery and offline evidence normalization.""" from __future__ import annotations diff --git a/src/verifier/hardware/adapters/generic.py b/src/verifier/hardware/adapters/generic.py index ea07028..7481430 100644 --- a/src/verifier/hardware/adapters/generic.py +++ b/src/verifier/hardware/adapters/generic.py @@ -1,4 +1,6 @@ -"""Registry-driven generic fixture adapter for unknown and future accelerators.""" +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Registry-driven generic fixture adapter for unknown and future accelerators.""" from __future__ import annotations diff --git a/src/verifier/hardware/adapters/nvidia.py b/src/verifier/hardware/adapters/nvidia.py index 3287b6e..660c0bc 100644 --- a/src/verifier/hardware/adapters/nvidia.py +++ b/src/verifier/hardware/adapters/nvidia.py @@ -1,4 +1,8 @@ -"""NVIDIA NVML/nvidia-smi discovery and offline evidence normalization.""" +"""Terminology: JavaScript Object Notation (JSON); NVIDIA Management Library (NVML); +Reference Integrity Manifest (RIM); Security Protocol and Data Model (SPDM); +Verifier Standard (VSTD). + +NVIDIA NVML/nvidia-smi discovery and offline evidence normalization.""" from __future__ import annotations diff --git a/src/verifier/hardware/adapters/provider.py b/src/verifier/hardware/adapters/provider.py index f3e63ce..478db06 100644 --- a/src/verifier/hardware/adapters/provider.py +++ b/src/verifier/hardware/adapters/provider.py @@ -1,4 +1,6 @@ -"""Cloud/provider control-plane evidence kept separate from hardware attestation.""" +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Cloud/provider control-plane evidence kept separate from hardware attestation.""" from __future__ import annotations diff --git a/src/verifier/hardware/anchors.py b/src/verifier/hardware/anchors.py index 157a747..578315e 100644 --- a/src/verifier/hardware/anchors.py +++ b/src/verifier/hardware/anchors.py @@ -1,4 +1,6 @@ -"""External continuity-anchor interfaces and deterministic local implementations.""" +"""Terminology: JavaScript Object Notation (JSON); JSON Lines (JSONL); Verifier Standard (VSTD). + +External continuity-anchor interfaces and deterministic local implementations.""" from __future__ import annotations diff --git a/src/verifier/hardware/attestation.py b/src/verifier/hardware/attestation.py index 57e2029..91a9cdb 100644 --- a/src/verifier/hardware/attestation.py +++ b/src/verifier/hardware/attestation.py @@ -1,4 +1,11 @@ -"""Canonical binding and independent verification for VSTD 3 attestations.""" +"""Terminology: hash-based message authentication code (HMAC); +Security Protocol and Data Model (SPDM); Verifier Standard (VSTD). + +Canonical binding and verifier-side recomputation for VSTD 3 attestations. + +The legacy public function name ``independently_verify_attestation`` denotes recomputation +from supplied evidence rather than trust in a collector's status field. It does not +establish distinct producer and checker actors under VSTD-1.""" from __future__ import annotations @@ -17,7 +24,7 @@ def attestation_signed_payload(evidence: AttestationEvidence) -> dict[str, objec """Return every semantic attestation field covered by its signature. ``signature`` and the collector's ``verification_state`` are deliberately not - self-authenticating inputs. The latter is independently recomputed by a verifier. + self-authenticating inputs. The latter is recomputed by the verifier. """ return { @@ -50,8 +57,9 @@ def independently_verify_attestation( """Recompute verification state for algorithms implemented by the core. The reference package implements only its explicitly test-only HMAC envelope. - Vendor/SPDM evidence must be verified by an adapter that supplies an independently - checked result; unknown algorithms remain NOT_VERIFIED rather than being guessed. + Vendor/SPDM evidence must be verified by an adapter that supplies a mechanism-checked + result; unknown algorithms remain NOT_VERIFIED rather than being guessed. This check + does not establish distinct actors. """ signature = evidence.signature diff --git a/src/verifier/hardware/canonical.py b/src/verifier/hardware/canonical.py index 5458912..82ad336 100644 --- a/src/verifier/hardware/canonical.py +++ b/src/verifier/hardware/canonical.py @@ -1,4 +1,7 @@ -"""Strict deterministic serialization primitives for VSTD 3 signed records.""" +"""Terminology: JavaScript Object Notation (JSON); Secure Hash Algorithm 256-bit (SHA-256); +Verifier Standard (VSTD). + +Strict deterministic serialization primitives for VSTD 3 signed records.""" from __future__ import annotations diff --git a/src/verifier/hardware/claims.py b/src/verifier/hardware/claims.py index 447ef93..a9e9182 100644 --- a/src/verifier/hardware/claims.py +++ b/src/verifier/hardware/claims.py @@ -1,4 +1,6 @@ -"""Evidence-monotone VSTD 3 claim evaluation.""" +"""Terminology: Verifier Standard (VSTD). + +Evidence-monotone VSTD 3 claim evaluation.""" from __future__ import annotations diff --git a/src/verifier/hardware/conformance.py b/src/verifier/hardware/conformance.py index e514de0..9cde2ed 100644 --- a/src/verifier/hardware/conformance.py +++ b/src/verifier/hardware/conformance.py @@ -1,4 +1,6 @@ -"""Incremental, evidence-bounded VSTD 3 conformance profiles.""" +"""Terminology: Verifier Standard (VSTD). + +Incremental, evidence-bounded VSTD 3 conformance profiles.""" from __future__ import annotations diff --git a/src/verifier/hardware/continuity.py b/src/verifier/hardware/continuity.py index 459c60b..36ac73b 100644 --- a/src/verifier/hardware/continuity.py +++ b/src/verifier/hardware/continuity.py @@ -1,4 +1,7 @@ -"""Authenticated event sequencing and reset-epoch verification for VSTD 3.""" +"""Terminology: hash-based message authentication code (HMAC); +International Organization for Standardization (ISO); Verifier Standard (VSTD). + +Authenticated event sequencing and reset-epoch verification for VSTD 3.""" from __future__ import annotations diff --git a/src/verifier/hardware/emulator.py b/src/verifier/hardware/emulator.py index 5e7a3f8..1c184a7 100644 --- a/src/verifier/hardware/emulator.py +++ b/src/verifier/hardware/emulator.py @@ -1,4 +1,8 @@ -"""Executable reference model for the VSTD 3 firmware-accountability contract.""" +"""Terminology: artificial intelligence (AI); application-specific integrated circuit (ASIC); +hash-based message authentication code (HMAC); Secure Hash Algorithm 256-bit (SHA-256); +Verifier Standard (VSTD). + +Executable reference model for the VSTD 3 firmware-accountability contract.""" from __future__ import annotations diff --git a/src/verifier/hardware/fleet.py b/src/verifier/hardware/fleet.py index 963cdda..92f595d 100644 --- a/src/verifier/hardware/fleet.py +++ b/src/verifier/hardware/fleet.py @@ -1,4 +1,6 @@ -"""Fleet-boundary and partition-safe accounting checks for VSTD 3.""" +"""Terminology: Verifier Standard (VSTD). + +Fleet-boundary and partition-safe accounting checks for VSTD 3.""" from __future__ import annotations diff --git a/src/verifier/hardware/models.py b/src/verifier/hardware/models.py index 4ab5001..3e3110b 100644 --- a/src/verifier/hardware/models.py +++ b/src/verifier/hardware/models.py @@ -1,4 +1,8 @@ -"""Accelerator-agnostic records for VSTD 3 hardware accountability.""" +"""Terminology: artificial intelligence (AI); application-specific integrated circuit (ASIC); +graphics processing unit (GPU); neural processing unit (NPU); tensor processing unit (TPU); +Verifier Standard (VSTD). + +Accelerator-agnostic records for VSTD 3 hardware accountability.""" from __future__ import annotations diff --git a/src/verifier/hardware/provenance.py b/src/verifier/hardware/provenance.py index 83fa022..1d45cd1 100644 --- a/src/verifier/hardware/provenance.py +++ b/src/verifier/hardware/provenance.py @@ -1,4 +1,6 @@ -"""Composition of VSTD 3 hardware evidence into the existing provenance hypergraph.""" +"""Terminology: Verifier Standard (VSTD). + +Composition of VSTD 3 hardware evidence into the existing provenance hypergraph.""" from __future__ import annotations @@ -60,8 +62,8 @@ def attach_vstd3_receipt( ) -> HardwareProvenanceBinding: """Attach a validated receipt so evidence invalidation reaches derived artifacts. - The function refuses receipts whose passing claims cannot be independently - reproduced under the supplied key resolver. ``output_artifact_ids`` defaults to + The function refuses receipts whose passing claims cannot be recomputed from bound + evidence under the supplied key resolver. ``output_artifact_ids`` defaults to the receipt's declared provenance links and every target must already exist. """ diff --git a/src/verifier/hardware/provider_evidence.py b/src/verifier/hardware/provider_evidence.py index 0790008..3b84297 100644 --- a/src/verifier/hardware/provider_evidence.py +++ b/src/verifier/hardware/provider_evidence.py @@ -1,4 +1,9 @@ -"""Canonical binding and independent verification of provider control-plane evidence.""" +"""Canonical binding and verifier-side recomputation of provider control-plane evidence. + +The legacy public function name ``independently_verify_provider_evidence`` distinguishes +recomputation from trusting a provider's status field. It does not establish distinct +producer and checker actors under Verifier Standard (VSTD) profile 1. +""" from __future__ import annotations diff --git a/src/verifier/hardware/receipt.py b/src/verifier/hardware/receipt.py index 5e568e5..26ce3ad 100644 --- a/src/verifier/hardware/receipt.py +++ b/src/verifier/hardware/receipt.py @@ -1,4 +1,6 @@ -"""Strict persistence helpers for VSTD 3 receipts.""" +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Strict persistence helpers for VSTD 3 receipts.""" from __future__ import annotations diff --git a/src/verifier/hardware/registry.py b/src/verifier/hardware/registry.py index 6b99da8..af10cb1 100644 --- a/src/verifier/hardware/registry.py +++ b/src/verifier/hardware/registry.py @@ -1,4 +1,6 @@ -"""Data-driven accelerator profile registry; profiles do not define claim policy.""" +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Data-driven accelerator profile registry; profiles do not define claim policy.""" from __future__ import annotations diff --git a/src/verifier/hardware/schema.py b/src/verifier/hardware/schema.py index 1ed1cf6..12033c6 100644 --- a/src/verifier/hardware/schema.py +++ b/src/verifier/hardware/schema.py @@ -1,4 +1,8 @@ -"""Deterministic JSON Schema generation for the normative VSTD 3 records.""" +"""Terminology: artificial intelligence (AI); application-specific integrated circuit (ASIC); +graphics processing unit (GPU); JavaScript Object Notation (JSON); neural processing unit (NPU); +tensor processing unit (TPU); Verifier Standard (VSTD). + +Deterministic JSON Schema generation for the normative VSTD 3 records.""" from __future__ import annotations @@ -117,6 +121,12 @@ def schema_for(model_type: type, *, schema_id: str, title: str) -> dict[str, obj builder = _SchemaBuilder() root = builder.reference(model_type) return { + "$comment": ( + "Terminology: artificial intelligence (AI); application-specific integrated " + "circuit (ASIC); graphics processing unit (GPU); JavaScript Object Notation " + "(JSON); neural processing unit (NPU); tensor processing unit (TPU); " + "Verifier Standard (VSTD)." + ), "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": SCHEMA_BASE + schema_id, "title": title, diff --git a/src/verifier/hardware/validation.py b/src/verifier/hardware/validation.py index 16426ba..eaf68ca 100644 --- a/src/verifier/hardware/validation.py +++ b/src/verifier/hardware/validation.py @@ -1,4 +1,6 @@ -"""Fail-closed structural and epistemic validation for VSTD 3 receipts.""" +"""Terminology: International Organization for Standardization (ISO); Verifier Standard (VSTD). + +Fail-closed structural and epistemic validation for VSTD 3 receipts.""" from __future__ import annotations @@ -254,10 +256,10 @@ def validate_vstd3_receipt( and checked_evidence.verification_state is not VerificationState.VERIFIED ): warnings.append( - f"attestation evidence {evidence.evidence_id} could not be independently verified: {verification_detail}" + f"attestation evidence {evidence.evidence_id} could not be verified against configured trust material: {verification_detail}" ) - independently_verified_source_ids = { + verified_source_ids = { evidence.evidence_source_id for evidence in attestation_by_id.values() if evidence.verification_state is VerificationState.VERIFIED @@ -265,7 +267,7 @@ def validate_vstd3_receipt( claim_source_by_id = { source_id: ( source - if source_id in independently_verified_source_ids + if source_id in verified_source_ids or source.verification_state is not VerificationState.VERIFIED else replace(source, verification_state=VerificationState.NOT_VERIFIED) ) @@ -466,7 +468,7 @@ def validate_vstd3_receipt( and checked_provider.verification_state is not VerificationState.VERIFIED ): warnings.append( - f"provider evidence {provider_evidence.evidence_id} could not be independently verified: " + f"provider evidence {provider_evidence.evidence_id} could not be verified against configured trust material: " f"{verification_detail}" ) @@ -600,7 +602,7 @@ def validate_vstd3_receipt( errors.append("physical-world completeness must remain UNSUPPORTED") epistemic_key_warning = any( - "could not be independently verified" in warning + "could not be verified against configured trust material" in warning and ("key unavailable" in warning or "unsupported signature verifier" in warning) for warning in warnings ) diff --git a/src/verifier/runtime/hardware_cli.py b/src/verifier/runtime/hardware_cli.py index b98809e..06df9df 100644 --- a/src/verifier/runtime/hardware_cli.py +++ b/src/verifier/runtime/hardware_cli.py @@ -1,4 +1,9 @@ -"""Public VSTD 3 accelerator-accountability CLI surfaces.""" +"""Terminology: application programming interface (API); command-line interface (CLI); +hash-based message authentication code (HMAC); identifier (ID); +International Organization for Standardization (ISO); JavaScript Object Notation (JSON); +Verifier Standard (VSTD). + +Public VSTD 3 accelerator-accountability CLI surfaces.""" from __future__ import annotations @@ -499,7 +504,7 @@ def _handle_claims(args: argparse.Namespace) -> int: "claims": [item.to_dict() for item in receipt.claim_evaluations], "validation_errors": list(validation.errors), "validation_warnings": list(validation.warnings), - "note": "Claim statuses are accepted only when receipt validation independently reproduces every PASS.", + "note": "Claim statuses are accepted only when receipt validation recomputes every PASS from bound evidence; this does not establish distinct actors.", } _emit(payload, as_json=args.json) return _status_exit(status) diff --git a/tests/test_vstd3_capabilities.py b/tests/test_vstd3_capabilities.py index 324ce7d..bbe3fe6 100644 --- a/tests/test_vstd3_capabilities.py +++ b/tests/test_vstd3_capabilities.py @@ -1,3 +1,6 @@ +"""Terminology: Advanced Micro Devices (AMD); application-specific integrated circuit (ASIC); +Verifier Standard (VSTD).""" + from __future__ import annotations import base64 diff --git a/tests/test_vstd3_cli.py b/tests/test_vstd3_cli.py index 374175b..5e8393a 100644 --- a/tests/test_vstd3_cli.py +++ b/tests/test_vstd3_cli.py @@ -1,3 +1,5 @@ +"""Terminology: identifier (ID); Verifier Standard (VSTD).""" + from __future__ import annotations import json diff --git a/tests/test_vstd3_emulator.py b/tests/test_vstd3_emulator.py index 9d4ce2e..f40cf02 100644 --- a/tests/test_vstd3_emulator.py +++ b/tests/test_vstd3_emulator.py @@ -1,3 +1,5 @@ +"""Terminology: floating-point operation (FLOP); Verifier Standard (VSTD).""" + from __future__ import annotations from dataclasses import replace @@ -127,7 +129,10 @@ def test_verified_flags_without_keys_cannot_bootstrap_strong_claims() -> None: validation = validate_vstd3_receipt(receipt) assert not validation.valid assert validation.status is ClaimStatus.UNKNOWN - assert any("could not be independently verified" in warning for warning in validation.warnings) + assert any( + "could not be verified against configured trust material" in warning + for warning in validation.warnings + ) overclaims = "\n".join(validation.errors) assert "overclaims DEVICE_IDENTITY" in overclaims assert "overclaims FIRMWARE_INTEGRITY" in overclaims diff --git a/tests/test_vstd3_provenance.py b/tests/test_vstd3_provenance.py index 7d00a87..a6ceaf1 100644 --- a/tests/test_vstd3_provenance.py +++ b/tests/test_vstd3_provenance.py @@ -131,6 +131,7 @@ def test_missing_declared_output_is_rejected_without_partial_mutation() -> None: "transformations": [], "contributors": [], "rights": [], + "conflicts": [], } diff --git a/tests/test_vstd3_schema.py b/tests/test_vstd3_schema.py index 1f1b92c..4feeb74 100644 --- a/tests/test_vstd3_schema.py +++ b/tests/test_vstd3_schema.py @@ -1,3 +1,5 @@ +"""Terminology: Verifier Standard (VSTD).""" + from __future__ import annotations import json From 0c7cec41b42cf0a8b5148f5d6861fb0e553e3a48 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:47:26 -0700 Subject: [PATCH 08/34] Define the supported public runtime API Reason: Expose the implemented receipt, artifact, workflow, and hardware mechanisms through one lazy, dependency-bounded package and canonical command surface. Evidence: Package metadata, lazy exports, command dispatch, API policy, and installed-surface tests change together. Coordinate: verifier-standard 1.2.0 Python API and vstd command-line interface. Falsification: Base import requires an optional dependency, an advertised export is unreachable, or command dispatch accepts an unsupported receipt as valid. Compatibility: The canonical vstd command and retained aliases remain available; current API additions are explicit and no retired receipt reader is restored. --- docs/API_STABILITY.md | 56 +++++++ pyproject.toml | 41 +++-- src/verifier/__init__.py | 44 ++++- src/verifier/runtime/public_cli.py | 260 ++++++++++++++++++++++++++--- tests/test_public_api.py | 61 +++++++ tests/test_public_cli.py | 37 +++- 6 files changed, 460 insertions(+), 39 deletions(-) create mode 100644 docs/API_STABILITY.md create mode 100644 tests/test_public_api.py diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md new file mode 100644 index 0000000..065a2bf --- /dev/null +++ b/docs/API_STABILITY.md @@ -0,0 +1,56 @@ +# Python application programming interface (API) stability + +> **Term:** Verifier Standard (VSTD). + +This policy applies beginning with the first release that contains it. It does not +retroactively change frozen receipts or earlier release bytes. + +## Supported boundary + +The supported Python runtime API is the set of names exported by `verifier.__all__` and +rendered under **Top-level Python exports** in the generated +[reference](https://timelordraps.github.io/verifier/reference.html). The implementation +tests that the two surfaces agree and that every exported name resolves. + +`verifier.__version__`, `verifier.__standard__`, and `verifier.__standard_status__` are +stable read-only metadata names. The standard coordinate and status describe this project; +they do not claim standards-body recognition, conformance, adoption, or endorsement. + +The supported artifact-control exports are `freeze_artifact`, `seal_artifact`, +`verify_frozen_artifact`, `thaw_artifact`, `thawed_artifact_status`, +`ArtifactVerification`, and `ArtifactControlError`. Seal creation and seal verification +require the optional `seal` dependency extra; importing the base package and freeze-only +operations retain the zero-third-party-dependency boundary. + +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 +frozen receipt identifiers, schemas, packaged specification bytes, command compatibility, +or historical refutation obligations. + +## Version and deprecation rules + +- Patch releases preserve supported signatures and behavior while correcting defects. +- Minor releases may add supported names and compatible parameters. +- Removing or incompatibly changing a supported name requires the next major release. +- Before removal, the name remains usable through the current major series, emits + `DeprecationWarning`, names a supported replacement, and appears in release notes. +- `_API_DEPRECATIONS` in `verifier.__init__` is the testable warning registry. A warning + cannot change the returned object, verdict, or failure semantics. + +Semantic Versioning governs only this declared software compatibility surface. It does not +increase assurance or establish an external standard. + +## Separate compatibility surfaces + +- `vstd` is the canonical command-line interface (CLI). `verifier` remains a compatibility + alias where unambiguous; `verifiable` is permanent because published refutation steps + bind it. +- Serialized receipt identifiers and released receipt bytes follow + [`WIRE_IDENTIFIERS.md`](../standard/WIRE_IDENTIFIERS.md), not this Python policy. +- Published JavaScript Object Notation (JSON) Schemas change only under their declared + profile and compatibility rules. +- Artifact-control formats follow `standard/ARTIFACT_CONTROL.md`. They are not receipts; + an incompatible format change requires a new artifact-control mechanism identifier. +- `verifier.experimental_workflow` is experimental and outside the supported Python API; + its outputs still cannot strengthen a VSTD verdict by naming or placement. diff --git a/pyproject.toml b/pyproject.toml index bca87c6..e8e70e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,22 @@ build-backend = "setuptools.build_meta" [project] name = "verifier-standard" -version = "1.1.3" -description = "Reference implementation for bounded verification receipts and provenance." +version = "1.2.0" +description = "Verification-domain language and reference implementation for bounded computational claims." readme = "README.md" requires-python = ">=3.10" license = "Apache-2.0" license-files = ["LICENSE", "NOTICE"] authors = [{name = "TimeLordRaps"}] -keywords = ["verification", "provenance", "reproducibility", "artificial-intelligence"] +keywords = [ + "verification", + "evidence", + "provenance", + "refutability", + "reproducibility", + "software-supply-chain", + "artificial-intelligence", +] classifiers = [ "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3", @@ -25,14 +33,24 @@ dependencies = [] [project.urls] Homepage = "https://github.com/TimeLordRaps/verifier" Documentation = "https://timelordraps.github.io/verifier/" +"API Policy" = "https://github.com/TimeLordRaps/verifier/blob/main/docs/API_STABILITY.md" Issues = "https://github.com/TimeLordRaps/verifier/issues" +Specification = "https://github.com/TimeLordRaps/verifier/tree/main/standard" +Changelog = "https://github.com/TimeLordRaps/verifier/blob/main/CHANGELOG.md" +Security = "https://github.com/TimeLordRaps/verifier/security/policy" [project.optional-dependencies] yaml = ["pyyaml>=6.0"] llguidance = ["llguidance==1.8.0"] torch = ["torch>=2.2"] jsonschema = ["jsonschema>=4.18"] -test = ["pytest>=8.0", "pyyaml>=6.0", "jsonschema>=4.18"] +seal = ["cryptography==50.0.0"] +scitt = [ + "scitt-cose==0.2.2", + "cbor2==6.1.4", + "cryptography==50.0.0", +] +test = ["pytest>=8.0", "coverage==7.15.4", "pyyaml>=6.0", "jsonschema>=4.18"] release = ["build==1.5.0", "twine==7.0.0"] [project.scripts] @@ -41,16 +59,19 @@ release = ["build==1.5.0", "twine==7.0.0"] vstd = "verifier.runtime.public_cli:main" # Retain the project-name alias for compatibility on platforms where it is unambiguous. verifier = "verifier.runtime.public_cli:main" -# `verifiable` is retained as a deprecated alias and MUST NOT be removed: published -# VSTD receipts bind falsification conditions that invoke it by name (see -# examples/generic_run/receipt.json). Removing it would render already-published -# refutation instructions unrunnable. The alias is a command name only; it no longer -# corresponds to any import package. +# `verifiable` is retained as a deprecated alias and MUST NOT be removed: receipts +# published in the v0.1.0 and v0.2.0 release artifacts, which predate the rename, bind +# falsification conditions that invoke it by name. Removing it would render those +# already-published refutation instructions unrunnable. No file in the current tree +# binds it. The alias is a command name only; it no longer corresponds to any import +# package. verifiable = "verifier.runtime.public_cli:main" [tool.setuptools.package-data] -verifier = ["hardware/*.json", "specifications/*.md"] +verifier = ["artifact_control/*.json", "hardware/*.json", "specifications/*.md"] [tool.pytest.ini_options] +pythonpath = ["src"] testpaths = ["tests"] norecursedirs = ["artifacts_tmp", "build", "dist", ".git", ".venv"] +asyncio_default_fixture_loop_scope = "function" diff --git a/src/verifier/__init__.py b/src/verifier/__init__.py index 83a7232..6df741e 100644 --- a/src/verifier/__init__.py +++ b/src/verifier/__init__.py @@ -1,14 +1,33 @@ -"""VSTD reference implementation public API.""" +"""Terminology: application programming interface (API); Verifier Standard (VSTD). + +VSTD reference implementation public API.""" from __future__ import annotations from importlib import import_module +import warnings from typing import TYPE_CHECKING, Any -__version__ = "1.1.3" +__version__ = "1.2.0" +# This names the highest project-specification coordinate exposed by the package; +# it is not a conformance claim. Keep the adjacent status when presenting it. __standard__ = "VSTD-4" +__standard_status__ = "CANDIDATE; CONFORMANCE NOT_ESTABLISHED" _LAZY_EXPORTS = { + "ArtifactControlError": ("verifier.artifact_control", "ArtifactControlError"), + "ArtifactVerification": ("verifier.artifact_control", "ArtifactVerification"), + "freeze_artifact": ("verifier.artifact_control", "freeze_artifact"), + "seal_artifact": ("verifier.artifact_control", "seal_artifact"), + "thaw_artifact": ("verifier.artifact_control", "thaw_artifact"), + "thawed_artifact_status": ( + "verifier.artifact_control", + "thawed_artifact_status", + ), + "verify_frozen_artifact": ( + "verifier.artifact_control", + "verify_frozen_artifact", + ), "VerificationVerdict": ("verifier.core.checker", "VerificationVerdict"), "VstdReceipt": ("verifier.core.receipt", "VstdReceipt"), "compute_canonical_digest": ("verifier.core.receipt", "compute_canonical_digest"), @@ -25,6 +44,10 @@ "require_vstd5_entry": ("verifier.core.depth", "require_vstd5_entry"), } +# Supported names remain in _LAZY_EXPORTS while deprecated. Each entry records the +# first release carrying the warning and the supported replacement. +_API_DEPRECATIONS: dict[str, tuple[str, str]] = {} + __all__ = list(_LAZY_EXPORTS) @@ -32,6 +55,14 @@ def __getattr__(name: str) -> Any: target = _LAZY_EXPORTS.get(name) if target is None: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + deprecation = _API_DEPRECATIONS.get(name) + if deprecation is not None: + since, replacement = deprecation + warnings.warn( + f"verifier.{name} is deprecated since {since}; use {replacement}", + DeprecationWarning, + stacklevel=2, + ) module_name, attribute_name = target value = getattr(import_module(module_name), attribute_name) globals()[name] = value @@ -43,6 +74,15 @@ def __dir__() -> list[str]: if TYPE_CHECKING: + from verifier.artifact_control import ( + ArtifactControlError as ArtifactControlError, + ArtifactVerification as ArtifactVerification, + freeze_artifact as freeze_artifact, + seal_artifact as seal_artifact, + thaw_artifact as thaw_artifact, + thawed_artifact_status as thawed_artifact_status, + verify_frozen_artifact as verify_frozen_artifact, + ) from verifier.core.checker import VerificationVerdict as VerificationVerdict from verifier.core.geometry import VerificationGeometry as VerificationGeometry from verifier.core.certificate import ( diff --git a/src/verifier/runtime/public_cli.py b/src/verifier/runtime/public_cli.py index cd81454..e9d83de 100644 --- a/src/verifier/runtime/public_cli.py +++ b/src/verifier/runtime/public_cli.py @@ -1,4 +1,7 @@ -"""Public, target-neutral CLI for the VSTD reference implementation. +"""Terminology: command-line interface (CLI); identifier (ID); JavaScript Object Notation (JSON); +Verifier Standard (VSTD); YAML Ain't Markup Language (YAML). + +Public, target-neutral CLI for the VSTD reference implementation. This entry point deliberately excludes repository-specific generators and verifiers. It operates only on declared generic-run manifests and stored VSTD-Graph receipts. @@ -7,12 +10,22 @@ from __future__ import annotations import argparse +import contextlib +import io import json import shutil import sys from pathlib import Path -from typing import Any - +from typing import Any, Callable + +from verifier.artifact_control import ( + freeze_artifact, + seal_artifact, + thaw_artifact, + thawed_artifact_status, + verify_frozen_artifact, +) +from verifier.core.checker import independence_is_evidenced from verifier.core.run import ( RunError, capture_run, @@ -33,6 +46,10 @@ handle_vstd3_command, parse_verification_keys, ) +from verifier.runtime.experimental_workflow_cli import ( + add_experiment_parsers, + handle_experiment_command, +) from verifier.runtime.demo import SCENARIOS, demo_report, emit_specimens, run_demo @@ -59,7 +76,7 @@ def _load_hypergraph(path_or_dir: Path) -> tuple[dict[str, Any], ProvenanceHyper payload = _read_receipt(path_or_dir) if payload is None or not _is_data_receipt(payload): raise ValueError( - "not a readable VSTD-Graph-1 receipt with frozen wire identifier " + "not a readable VSTD-Graph-1 receipt with serialized schema_version identifier " f"VSTD-DATA-0.1: {_receipt_file(path_or_dir)}" ) return payload, ProvenanceHypergraph.from_dict(payload["hypergraph"]) @@ -114,13 +131,72 @@ def _inspect_data_receipt(path_or_dir: Path) -> int: print("=" * 70) print(f"Canonical Digest: {payload.get('canonical_digest')}") print(f"Target Artifact: {payload.get('dataset_spec', {}).get('target_artifact_id')}") - print(f"Audit Verdict: {payload.get('independent_audit', {}).get('overall_verdict')}") + print(f"Checker Verdict: {payload.get('independent_audit', {}).get('overall_verdict')}") + basis = payload.get("independent_audit", {}).get("independence_basis", {}) + print( + "Independence: " + + ("EVIDENCED" if independence_is_evidenced(basis) else "NOT_DEMONSTRATED") + ) print(f"Artifacts: {len(graph.artifacts)}") print(f"Transformations: {len(graph.transformations)}") print("=" * 70) return 0 +def _run_receipt_handler_as_json( + command: str, receipt_kind: str, handler: Callable[[], int] +) -> int: + """Keep the common receipt commands machine-readable without changing their APIs.""" + + stdout = io.StringIO() + stderr = io.StringIO() + with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + exit_code = handler() + result = ( + "COMPLETED" + if exit_code == 0 + else "UNSUPPORTED" + if exit_code == 2 + else "FAILED" + ) + print( + json.dumps( + { + "command": command, + "receipt_kind": receipt_kind, + "result": result, + "exit_code": exit_code, + "messages": stdout.getvalue().splitlines(), + "errors": stderr.getvalue().splitlines(), + }, + indent=2, + sort_keys=True, + ) + ) + return exit_code + + +def _receipt_command_failure(args: argparse.Namespace, message: str) -> int: + if args.json: + print( + json.dumps( + { + "command": args.command, + "receipt_kind": "UNKNOWN", + "result": "FAILED", + "exit_code": 1, + "messages": [], + "errors": [message], + }, + indent=2, + sort_keys=True, + ) + ) + else: + print(f"[FAIL] {message}", file=sys.stderr) + return 1 + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="vstd", @@ -160,8 +236,8 @@ def build_parser() -> argparse.ArgumentParser: plan_parser.add_argument("--json", action="store_true") for command, help_text in ( - ("validate", "Validate a generic-run or VSTD-Graph receipt."), - ("inspect", "Inspect a generic-run or VSTD-Graph receipt."), + ("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."), ("reproduce", "Replay the mechanisms available in a stored receipt."), ): command_parser = subparsers.add_parser(command, help=help_text) @@ -200,6 +276,60 @@ def build_parser() -> argparse.ArgumentParser: export_parser = data_commands.add_parser("export") export_parser.add_argument("receipt") + + artifact_parser = subparsers.add_parser( + "artifact", + help="Freeze exact artifact bytes, add or verify a seal, or thaw a descendant.", + ) + artifact_commands = artifact_parser.add_subparsers( + dest="artifact_command", required=True + ) + freeze_parser = artifact_commands.add_parser( + "freeze", help="Copy exact file or directory bytes into a guarded artifact bundle." + ) + freeze_parser.add_argument("source") + freeze_parser.add_argument("bundle") + freeze_parser.add_argument("--media-type", default="application/octet-stream") + freeze_parser.add_argument("--parent", action="append", default=[]) + freeze_parser.add_argument("--context", action="append", default=[]) + freeze_parser.add_argument("--json", action="store_true") + + seal_parser = artifact_commands.add_parser( + "seal", help="Add a readable finite self-closing Ed25519 seal." + ) + seal_parser.add_argument("bundle") + seal_parser.add_argument("--private-key", required=True) + seal_parser.add_argument("--json", action="store_true") + + verify_parser = artifact_commands.add_parser( + "verify", help="Recompute exact bytes, guards, seals, and optional external anchors." + ) + verify_parser.add_argument("bundle") + verify_parser.add_argument("--expected-artifact-id") + verify_parser.add_argument("--expected-key-id") + verify_parser.add_argument( + "--freeze-only", + action="store_true", + help="Accept a clean freeze without claiming seal-backed identity.", + ) + verify_parser.add_argument("--json", action="store_true") + + thaw_parser = artifact_commands.add_parser( + "thaw", help="Copy a clean sealed parent into a mutable descendant." + ) + thaw_parser.add_argument("bundle") + thaw_parser.add_argument("destination") + thaw_parser.add_argument("--expected-artifact-id") + thaw_parser.add_argument("--expected-key-id") + thaw_parser.add_argument("--json", action="store_true") + + status_parser = artifact_commands.add_parser( + "status", help="Compare a thawed descendant with its sealed parent identity." + ) + status_parser.add_argument("artifact") + status_parser.add_argument("--record") + status_parser.add_argument("--json", action="store_true") + add_experiment_parsers(subparsers) add_vstd3_parsers(subparsers) return parser @@ -208,34 +338,55 @@ def _handle_receipt_command(args: argparse.Namespace) -> int: receipt_path = Path(args.receipt).resolve() payload = _read_receipt(receipt_path) if payload is None: - print(f"[FAIL] Receipt is missing or malformed: {_receipt_file(receipt_path)}", file=sys.stderr) - return 1 + return _receipt_command_failure( + args, f"Receipt is missing or malformed: {_receipt_file(receipt_path)}" + ) if is_generic_run_receipt(payload): if args.command == "validate": - return validate_run_receipt(receipt_path) - if args.command == "inspect": - return inspect_run_receipt(receipt_path) - return reproduce_run_receipt(receipt_path, rerun=args.rerun) + handler = lambda: validate_run_receipt(receipt_path) + elif args.command == "inspect": + handler = lambda: inspect_run_receipt(receipt_path) + else: + handler = lambda: reproduce_run_receipt(receipt_path, rerun=args.rerun) + return ( + _run_receipt_handler_as_json(args.command, "generic_computational_run", handler) + if args.json + else handler() + ) if _is_data_receipt(payload): if args.command == "validate": - return validate_data_receipt(receipt_path) - if args.command == "inspect": - return _inspect_data_receipt(receipt_path) - if args.rerun: - print("[FAIL] --rerun is not defined for stored VSTD-Graph receipts", file=sys.stderr) - return 1 - return reproduce_data_receipt(receipt_path) + handler = lambda: validate_data_receipt(receipt_path) + elif args.command == "inspect": + handler = lambda: _inspect_data_receipt(receipt_path) + elif args.rerun: + handler = lambda: _receipt_command_failure( + argparse.Namespace(command=args.command, json=False), + "--rerun is not defined for stored VSTD-Graph receipts", + ) + else: + handler = lambda: reproduce_data_receipt(receipt_path) + return ( + _run_receipt_handler_as_json(args.command, "vstd_graph", handler) + if args.json + else handler() + ) if is_vstd3_receipt(payload): receipt = load_vstd3_receipt(receipt_path) if args.command == "reproduce": - print( - "[UNSUPPORTED] A stored hardware receipt cannot replay physical execution; " - "use its declared emulator or vendor collection mechanism.", - file=sys.stderr, + message = ( + "A stored hardware receipt cannot replay physical execution; use its " + "declared emulator or vendor collection mechanism." ) + if args.json: + return _run_receipt_handler_as_json( + args.command, + "vstd3_hardware", + lambda: (print(f"[UNSUPPORTED] {message}", file=sys.stderr) or 2), + ) + print(f"[UNSUPPORTED] {message}", file=sys.stderr) return 2 resolver, _ = parse_verification_keys(args.key) validation = validate_vstd3_receipt(receipt, key_resolver=resolver) @@ -258,8 +409,7 @@ def _handle_receipt_command(args: argparse.Namespace) -> int: print(f" - {message}") return 0 if validation.status.value == "PASS" else (1 if validation.status.value == "FAIL" else 2) - print("[FAIL] Unsupported receipt kind or schema", file=sys.stderr) - return 1 + return _receipt_command_failure(args, "Unsupported receipt kind or schema") def _handle_data_command(args: argparse.Namespace) -> int: @@ -308,6 +458,60 @@ def _handle_data_command(args: argparse.Namespace) -> int: return 0 +def _print_artifact_result(result: dict[str, Any], as_json: bool) -> None: + if as_json: + print(json.dumps(result, indent=2, sort_keys=True)) + return + print(f"[{result.get('state', 'COMPLETED')}] artifact control") + for key, value in result.items(): + if key != "state": + print(f" {key}: {value}") + + +def _handle_artifact_command(args: argparse.Namespace) -> int: + if args.artifact_command == "freeze": + result = freeze_artifact( + args.source, + args.bundle, + media_type=args.media_type, + parent_bundles=args.parent, + context_bundles=args.context, + ) + output = {"state": "FROZEN_UNSEALED", **result} + _print_artifact_result(output, args.json) + return 0 + if args.artifact_command == "seal": + result = seal_artifact(args.bundle, args.private_key) + output = {"state": "SEALED", **result} + _print_artifact_result(output, args.json) + return 0 + if args.artifact_command == "verify": + verification = verify_frozen_artifact( + args.bundle, + expected_artifact_id=args.expected_artifact_id, + expected_key_id=args.expected_key_id, + require_seal=not args.freeze_only, + ) + output = verification.to_dict() + _print_artifact_result(output, args.json) + if verification.state in {"SEALED", "FROZEN_UNSEALED"}: + return 0 + return 2 if verification.state == "NOT_ESTABLISHED" else 1 + if args.artifact_command == "thaw": + result = thaw_artifact( + args.bundle, + args.destination, + expected_artifact_id=args.expected_artifact_id, + expected_key_id=args.expected_key_id, + ) + output = {"state": "THAWED_CLEAN", **result} + _print_artifact_result(output, args.json) + return 0 + result = thawed_artifact_status(args.artifact, args.record) + _print_artifact_result(result, args.json) + return 0 if result["state"] == "THAWED_CLEAN" else 1 + + def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) try: @@ -390,6 +594,10 @@ def main(argv: list[str] | None = None) -> int: if args.command == "data": return _handle_data_command(args) + if args.command == "artifact": + return _handle_artifact_command(args) + if args.command == "experiment": + return handle_experiment_command(args) if args.command in {"hardware", "continuity", "fleet", "evidence", "claims"}: return handle_vstd3_command(args) except (OSError, RunError, ValueError, KeyError) as exc: diff --git a/tests/test_public_api.py b/tests/test_public_api.py new file mode 100644 index 0000000..7d04dba --- /dev/null +++ b/tests/test_public_api.py @@ -0,0 +1,61 @@ +"""Supported Python application programming interface (API) characterization tests.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +import verifier + + +ROOT = Path(__file__).resolve().parents[1] +EXPECTED_EXPORTS = { + "ArtifactControlError", + "ArtifactVerification", + "DecisionCertificate", + "ReproducibilityLevel", + "VerificationGeometry", + "VerificationVerdict", + "VstdReceipt", + "capture_run", + "certificate_from_canonical_bytes", + "compute_canonical_digest", + "freeze_artifact", + "require_vstd5_entry", + "seal_artifact", + "thaw_artifact", + "thawed_artifact_status", + "validate_run_receipt", + "vstd4_depth", + "verify_frozen_artifact", +} + + +def test_supported_top_level_exports_are_explicit_and_resolvable() -> None: + assert set(verifier.__all__) == EXPECTED_EXPORTS + assert set(verifier._LAZY_EXPORTS) == EXPECTED_EXPORTS + for name in verifier.__all__: + assert getattr(verifier, name) is not None + + +def test_deprecation_registry_warns_without_replacing_the_export( + monkeypatch: pytest.MonkeyPatch, +) -> None: + name = "capture_run" + monkeypatch.delattr(verifier, name, raising=False) + monkeypatch.setitem(verifier._API_DEPRECATIONS, name, ("1.3.0", "replacement_name")) + + with pytest.warns(DeprecationWarning, match="deprecated since 1.3.0"): + resolved = getattr(verifier, name) + + assert resolved is verifier._LAZY_EXPORTS[name] or callable(resolved) + + +def test_every_declared_deprecation_is_supported_and_release_noted() -> None: + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + for name, (since, replacement) in verifier._API_DEPRECATIONS.items(): + assert name in verifier.__all__ + assert since in changelog + assert name in changelog + assert replacement in changelog diff --git a/tests/test_public_cli.py b/tests/test_public_cli.py index b0fa70d..d9a3a31 100644 --- a/tests/test_public_cli.py +++ b/tests/test_public_cli.py @@ -1,4 +1,6 @@ -"""Tests for the target-neutral public CLI surface.""" +"""Terminology: command-line interface (CLI); Verifier Standard (VSTD). + +Tests for the target-neutral public CLI surface.""" from __future__ import annotations @@ -46,6 +48,14 @@ 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(["artifact", "verify", "bundle"]).artifact_command + == "verify" + ) + assert ( + parser.parse_args(["experiment", "validate", "experiment.json"]).experiment_command + == "validate" + ) def test_public_cli_flagship_demo_is_side_effect_free_and_machine_readable( @@ -115,6 +125,31 @@ def test_public_cli_generic_run_lifecycle(tmp_path: Path, capsys) -> None: assert "[UNSANDBOXED EXECUTION]" in capsys.readouterr().err +def test_generic_receipt_validate_and_inspect_honor_json(tmp_path: Path, capsys) -> None: + manifest = _manifest(tmp_path) + receipt_dir = tmp_path / "receipt" + assert main(["run", str(manifest), "--output", str(receipt_dir)]) == 0 + capsys.readouterr() + + for command in ("validate", "inspect"): + assert main([command, str(receipt_dir), "--json"]) == 0 + result = json.loads(capsys.readouterr().out) + assert result["command"] == command + assert result["receipt_kind"] == "generic_computational_run" + assert result["result"] == "COMPLETED" + assert result["exit_code"] == 0 + + +def test_unknown_receipt_failure_honors_json(tmp_path: Path, capsys) -> None: + path = tmp_path / "receipt.json" + path.write_text('{"schema_version": "UNKNOWN"}', encoding="utf-8") + + assert main(["validate", str(path), "--json"]) == 1 + result = json.loads(capsys.readouterr().out) + assert result["result"] == "FAILED" + assert result["errors"] == ["Unsupported receipt kind or schema"] + + def test_public_cli_rejects_unknown_receipt(tmp_path: Path) -> None: path = tmp_path / "receipt.json" path.write_text('{"schema_version": "UNKNOWN"}', encoding="utf-8") From 002ff24c3c24852f80c4bc2aa804532c915c2544 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:47:34 -0700 Subject: [PATCH 09/34] Remove the unreproducible SimulacraBench rehearsal Reason: Eliminate a first-impression example whose local synthetic packet did not reproduce the hosted submission, image, hardware, or public retrieval path. Evidence: The complete rehearsal, copied upstream snapshot, generated packet, and its repository-only test are removed together. Coordinate: Public examples and onboarding boundary for release 1.2.0. Falsification: Any navigation, manifest, test, or current documentation still presents the removed rehearsal as public verification evidence. Compatibility: No runtime or numbered-profile wire behavior changes; Git history retains the deleted forensic state. --- .../simulacrabench_synthetic/CORRECTION.md | 37 - .../simulacrabench_synthetic/CROSSWALK.md | 41 - examples/simulacrabench_synthetic/README.md | 80 -- examples/simulacrabench_synthetic/UPSTREAM.md | 27 - .../artifacts/marginal_counts_submission.zip | Bin 1242 -> 0 bytes .../artifacts/sandbox_schema.json | 133 ---- .../challenge_demo.json | 111 --- .../public_packet.json | 549 ------------- .../source_snapshot/LICENSE | 21 - .../source_snapshot/README.md | 615 --------------- .../baseline/marginal_counts/main.py | 42 - .../baseline/marginal_counts/requirements.txt | 6 - .../source_snapshot/config.yml | 74 -- .../source_snapshot/data/sample.json | 86 -- .../source_snapshot/make_sandbox.py | 337 -------- .../source_snapshot/score.py | 734 ------------------ .../tools/check_submission_zip.py | 681 ---------------- .../simulacrabench_synthetic/verify_packet.py | 663 ---------------- tests/test_simulacrabench_packet.py | 123 --- 19 files changed, 4360 deletions(-) delete mode 100644 examples/simulacrabench_synthetic/CORRECTION.md delete mode 100644 examples/simulacrabench_synthetic/CROSSWALK.md delete mode 100644 examples/simulacrabench_synthetic/README.md delete mode 100644 examples/simulacrabench_synthetic/UPSTREAM.md delete mode 100644 examples/simulacrabench_synthetic/artifacts/marginal_counts_submission.zip delete mode 100644 examples/simulacrabench_synthetic/artifacts/sandbox_schema.json delete mode 100644 examples/simulacrabench_synthetic/challenge_demo.json delete mode 100644 examples/simulacrabench_synthetic/public_packet.json delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/LICENSE delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/README.md delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/baseline/marginal_counts/main.py delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/baseline/marginal_counts/requirements.txt delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/config.yml delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/data/sample.json delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/make_sandbox.py delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/score.py delete mode 100644 examples/simulacrabench_synthetic/source_snapshot/tools/check_submission_zip.py delete mode 100644 examples/simulacrabench_synthetic/verify_packet.py delete mode 100644 tests/test_simulacrabench_packet.py diff --git a/examples/simulacrabench_synthetic/CORRECTION.md b/examples/simulacrabench_synthetic/CORRECTION.md deleted file mode 100644 index b0336c8..0000000 --- a/examples/simulacrabench_synthetic/CORRECTION.md +++ /dev/null @@ -1,37 +0,0 @@ -# Additive correction to `VSTD-SB-SYNTH-001` - -**Correction date:** 2026-08-22 -**Corrected packet:** `VSTD-SB-SYNTH-002` - -The first public specimen is preserved at immutable commit -[`a37e6128fc6eccb66160a2f7c3af2f43341c227e`](https://github.com/TimeLordRaps/verifier/tree/a37e6128fc6eccb66160a2f7c3af2f43341c227e/examples/simulacrabench_synthetic). -Its packet digest is -`sha256:f182bfce5a5ae8e7137795300d42e285f365e6707b7c3517b3cee7b02331963b`; -its challenge digest is -`sha256:9ce25775826ef90f3eea0abdaa62268c4e5ce34092e63e2cc6cc88248a9395d6`. - -## What was wrong - -1. The packet used a locator scheme with no shipped resolver and treated nonempty locator - and retention strings as enough to derive `AVAILABLE`. -2. The public verifier did not retrieve any private artifact or receive observed bytes. -3. A founder-authored transcript under the same trust root was accepted as an authorized - adjudication, moving a deliberate mutant from `CHALLENGED` to `REVOKED` without public - score recomputation or an independent adjudicator. - -Those statements overstated what the public artifacts established. - -## Correction - -- Private artifacts now have no invented locator and derive only `IDENTIFIED`. -- The bundle fails the `AVAILABLE` requirement; public score reproduction remains - `UNAVAILABLE`. -- The challenge demonstration contains a filing but no private transcript and no - adjudication. Its terminal public state is `CHALLENGED`. -- `ArtifactAvailability` now requires an observed-byte retrieval binding before deriving - `AVAILABLE` or `PORTABLE`; locator and retention declarations alone do not elevate it. -- The recorded local `PASS` and `0.33` are retained only as a claim made under the same - founder-operated trust root, not as a public rerun or independent result. - -The old commit and digests remain immutable. Current documentation and tests point to the -corrected specimen rather than silently reinterpreting the historical bytes. diff --git a/examples/simulacrabench_synthetic/CROSSWALK.md b/examples/simulacrabench_synthetic/CROSSWALK.md deleted file mode 100644 index 5a7d18e..0000000 --- a/examples/simulacrabench_synthetic/CROSSWALK.md +++ /dev/null @@ -1,41 +0,0 @@ -# SimulacraBench-to-VSTD crosswalk - -This crosswalk is pinned to the upstream commit recorded in [`UPSTREAM.md`](UPSTREAM.md). -It maps observable public evaluator mechanics; it does not infer hidden infrastructure or -organizer intent. - -| SimulacraBench public mechanic | Pinned evidence | VSTD representation in this example | Preserved limitation | -| :-- | :-- | :-- | :-- | -| A submission ZIP supplies `main.py`, optional `requirements.txt`, and `predict(frame, schema)` | `README.md`, `tools/check_submission_zip.py`, baseline files | Exact ZIP and source bytes are `SELF_CONTAINED` and content-addressed | Passing the ZIP checker does not establish a successful evaluation | -| Dependencies are installed before the scored run | `score.py`, `config.yml` | Dependency declaration is committed separately from the run transcript | This local rehearsal did not reproduce the hosted image or hardware | -| Runtime sockets are disabled before submission import | `score.py` | `network_control` is a claim-coordinate parameter and an admissible execution-receipt challenge target | The observed control was in-process socket denial, not container-level isolation | -| Phase 1 exposes TRAIN and scores DEV under a 900-second prediction budget | `README.md`, `config.yml`, `score.py` | Phase, data view, timeout, source commit, and scoring seed are bounded execution fields | No protected TEST data or hosted API path was exercised | -| The participant receives a privacy-processed aggregate and runtime; the organizer keeps raw detail | `README.md`, `score.py` | Saved participant-visible result is `SELF_CONTAINED`; raw log and synthetic fixture are access-controlled and only `IDENTIFIED` in the public packet | Public recomputation is `UNAVAILABLE`; a digest and retention promise are not retrieval evidence or proof of correctness | -| A score mismatch can be challenged without publishing respondent rows | VSTD profile construction over the public evaluator interface | `metric_recomputation_mismatch` moves the filed mutant to `CHALLENGED` | No adjudication or revocation follows without separately evidenced authorized checking | - -## Exactness audit - -| Question | Answer | -| :-- | :-- | -| Are the upstream files pinned to a full commit and bundled byte-for-byte? | Yes | -| Is the exact submitted ZIP bundled? | Yes | -| Is every input to the measured run synthetic? | Yes; the private fixture was produced only by the pinned synthetic generator, public toy schema, configuration, and a private high-entropy seed | -| Does the public packet demonstrate retrieval of every verdict-critical artifact? | No; it contains no retrieval observation and no private locator | -| Can an arbitrary public reviewer retrieve the hidden fixture and raw log? | No | -| Can the public verifier recompute the score? | No | -| Was the fixture commitment externally timestamped before execution? | No | -| Was hosted H100, CPU, memory, container, API, or leaderboard parity established? | No | -| Was protected SimulacraBench data used? | No | -| Has an organizer reviewed, adopted, or endorsed this mapping? | No | -| Is the synthetic evaluator independent or a VSTD-5 witness? | No | -| Does this example claim aggregate VSTD-4 depth? | No | - -## Failure semantics - -- A bundled-byte mismatch rejects the packet. -- The private artifacts remain `IDENTIFIED` unless an additive observation binds actual - retrieved bytes to the declared artifact, locator, observer, and observation time. -- A filed `metric_recomputation_mismatch` leaves the targeted mutant `CHALLENGED` until a - separate authorized adjudication is evidenced. -- The declared retention horizon does not elevate availability and is not silently - rewritten into a retrieval claim. diff --git a/examples/simulacrabench_synthetic/README.md b/examples/simulacrabench_synthetic/README.md deleted file mode 100644 index 0ed0290..0000000 --- a/examples/simulacrabench_synthetic/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# SimulacraBench synthetic closed-evaluation packet - -> **Corrected specimen:** packet `VSTD-SB-SYNTH-002` supersedes the challenged -> `VSTD-SB-SYNTH-001` specimen. See [`CORRECTION.md`](CORRECTION.md). - -This non-normative example maps one recorded local, synthetic run of the pinned -SimulacraBench public evaluator into VSTD's disclosure and challenge mechanisms. It -demonstrates what a public packet can honestly retain when verdict-critical private bytes -are not available to the public checker. - -## Bounded recorded claim - -Under one founder-operated trust root, the pinned phase-1 scorer was recorded as -evaluating the pinned marginal-counts baseline against a committed 12,000-respondent -**synthetic** sandbox with scoring seed `20260822`. The saved participant-visible output -is `PASS` with reported skill `0.33`. - -The public package establishes the identity and internal binding of the public artifacts -and that saved aggregate. It does **not** rerun the score. It does not establish a -protected-data run, hosted runner parity, leaderboard entry, organizer review, or -independent verification. - -## Availability result - -The exact scored schema, hidden synthetic respondent table, organizer log, execution -transcript, and generator seed have content addresses and a declared retention horizon. -They have no public locator and no executed retrieval observation in this packet. -Therefore their derived level is `IDENTIFIED`, not `AVAILABLE`. - -The bundle's public availability assessment is consequently: - -```text -required: AVAILABLE -derived floor: IDENTIFIED -accepted: false -public score reproduction: UNAVAILABLE -``` - -A retention promise is not retrieval evidence. An authorized party could later publish -an additive retrieval observation, but that observation would remain scoped to its named -trust root and would not automatically become independent verification. - -## Verify the public view - -From a VSTD source checkout: - -```bash -PYTHONPATH=src python examples/simulacrabench_synthetic/verify_packet.py -PYTHONPATH=src python examples/simulacrabench_synthetic/verify_packet.py --json -``` - -The verifier performs no network access and receives no hidden records. It checks: - -- canonical packet and challenge digests; -- byte identity of the bundled upstream snapshot and public artifacts; -- the `IDENTIFIED` availability floor and its limiting private artifacts; -- the explicit disclosure, correction, and trust boundaries; and -- admission of a non-disclosing challenge, which ends at `CHALLENGED`. - -It does not accept a private transcript, execute a retrieval, adjudicate the challenge, -or move the mutant claim to `REVOKED`. - -## Public and private views - -| View | Can inspect | Can conclude | Cannot conclude | -| :-- | :-- | :-- | :-- | -| Public | Pinned source bytes, exact submission ZIP, generated schema view, commitments, saved participant-visible result, challenge filing | The corrected packet is internally bound; the private artifacts are identified; the mutant filing is `CHALLENGED` | The hidden-fixture score was recomputed; private bytes are available; the challenge was adjudicated; the evaluator is independent | -| Private holder | Private bytes in addition to the public view | Only what a separately executed, recorded check actually observes under its declared trust root | Organizer endorsement, hosted parity, protected-data performance, public reproducibility, or independent verification | - -The deliberate mutant changes only the saved reported skill from `0.33` to `0.34`. Filing -the declared mismatch challenge changes the mutant claim to `CHALLENGED`. No public -artifact in this package authorizes an adjudication, so the verifier stops there. - -## What VSTD does not claim - -VSTD is not accredited or a consensus standard, and this mapping does not claim -SimulacraBench adoption, endorsement, protected-data use, or independent implementation. - -See [`CROSSWALK.md`](CROSSWALK.md) for the source-to-VSTD mapping and -[`UPSTREAM.md`](UPSTREAM.md) for exact provenance and licensing. diff --git a/examples/simulacrabench_synthetic/UPSTREAM.md b/examples/simulacrabench_synthetic/UPSTREAM.md deleted file mode 100644 index acbb65b..0000000 --- a/examples/simulacrabench_synthetic/UPSTREAM.md +++ /dev/null @@ -1,27 +0,0 @@ -# Upstream provenance and license - -The source snapshot in this example is copied from: - -- Repository: -- Commit: [`1bb2d46026fe0d91979448c3d916506be0608513`](https://github.com/SituatedEvals/public/commit/1bb2d46026fe0d91979448c3d916506be0608513) -- License: MIT, reproduced byte-for-byte at [`source_snapshot/LICENSE`](source_snapshot/LICENSE) - -`public_packet.json` records the SHA-256 digest, byte length, pinned source URL, and local -snapshot path for every copied file. Each snapshot is the canonical Git-blob byte stream, -not a platform newline conversion. `verify_packet.py` refuses any mismatch. - -The copied files are: - -- `README.md` -- `LICENSE` -- `config.yml` -- `data/sample.json` -- `make_sandbox.py` -- `score.py` -- `baseline/marginal_counts/main.py` -- `baseline/marginal_counts/requirements.txt` -- `tools/check_submission_zip.py` - -The VSTD packet, crosswalk, verifier, and challenge demonstration are original to this -repository. The snapshot is included to make the public, verdict-critical source bytes -self-contained rather than treating a remote digest as availability. diff --git a/examples/simulacrabench_synthetic/artifacts/marginal_counts_submission.zip b/examples/simulacrabench_synthetic/artifacts/marginal_counts_submission.zip deleted file mode 100644 index 6e9326118fe95ac22963e016075b3dd738ca1952..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1242 zcmWIWW@Zs#U|`^2U`P;)t&6lM)n#U2n8wDyzz!76P0Y;GE2!)Z@$J8DCb0K)IM0;s z7OqoqRX3CP9g|F|Pb|8@#>(Xw(Q{&0){$Y&^e>Z2tvlv*bB2^|@K31)h=PeVP_kmN*>FolW zNkR!t`9TZbMkX5PUYfd}VeSdNgc&Zg7c}&&`SkkS9%0Yf1&t3@_y!hD{ito`nDsn& zhQyVs3odOt#2V%9n@OhGy*$NSC8Vz&(s-(F?K@+3$qN%F#c(?>;GO6F(LMXzBfGD2 z3T&1M%`SPQCT!R(;_A_+*#02WKH=L*1#yAH1^tV~{4Skz4CL;Yd*UdqW+HyxcuS!JWP zac1I_89(p;-!#uB%ZfI&*mL??*Uj~2AuIR32|nx}a5v%o zzSYhDqJpXw;+J0D#1_=I;Pva6qb=VQOjpUTPBwA;oiQs-{c%q4zUehzL|5lOP+n!5 zQ1)ozEjA_5lfk)?DAXHQ!}4DoWXUWxcl0ppi;?-MH$Uy;+Ffe>-UA8Sgn3(%cAMO zww2a0SEpWxKe^*V&C9oJPv3sKy2~ImJG^LpennaB>$Bdw?6n_GpDB0dgZTXlOZR6}qy0+nODbTP|hB zB$v%u9(lKV-kkr80Vp{t$>pB=MPN=+V`N|u0MbRNg{7HAsky0nCB=Fr6(z3y8~u(r z@VNZ0IqGq}p+!khMQY0Hbvyc+u7;DBOxUsPP1}x{kBm({uWh2=Wnd>h;czEFO{h9mf^LBrGy78j!y5l?j)}QsV znl$(Ky0^3P1H2iTM3`|GDZnrQgC&h13R=LRYeEk-h!zHhC5=JA(g{5b1H4&*

-VSTD object and graph verification layers, each requiring its own separate evidence - -# VSTD +# Verifier Standard (VSTD) **Portable, bounded, refutable evidence for computational claims.** -[![Conformance](https://github.com/TimeLordRaps/verifier/actions/workflows/ci.yml/badge.svg)](https://github.com/TimeLordRaps/verifier/actions/workflows/ci.yml) +[![Repository checks](https://github.com/TimeLordRaps/verifier/actions/workflows/ci.yml/badge.svg)](https://github.com/TimeLordRaps/verifier/actions/workflows/ci.yml) [![Latest release](https://img.shields.io/github/v/release/TimeLordRaps/verifier?display_name=tag&sort=semver)](https://github.com/TimeLordRaps/verifier/releases/latest) [![Python 3.10–3.13](https://img.shields.io/badge/python-3.10%E2%80%933.13-3776AB.svg)](https://www.python.org/) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-2f7d6d.svg)](LICENSE) -[![Status: alpha](https://img.shields.io/badge/status-founder--maintained%20alpha-d97706.svg)](#project-status) +[![Status: alpha](https://img.shields.io/badge/status-alpha-d97706.svg)](#current-maturity) -*A PASS is not enough. Show what passed, under which meaning, against which -evidence, inside which bounds, and how somebody else can prove it wrong.* +
-[Run the demo](#see-it-fail-correctly) · -[Read the quickstart](docs/QUICKSTART.md) · -[Inspect the standard](standard/LADDER.md) · -[Challenge a claim](https://github.com/TimeLordRaps/verifier/discussions/8) · -[See the roadmap](ROADMAP.md) +> **Acronyms used below:** identifier (ID); reduced instruction set computer (RISC). - +VSTD is a verification-domain language and Python reference implementation for packaging +bounded computational claims with their evidence, checking mechanisms, limits, +refutation conditions, provenance, and reproducibility information. It does **not** +replace native domain verifiers, proof systems, signatures, identity systems, +transparency logs, or provenance formats, and it never strengthens their results merely +by translating or storing them. + +VSTD evaluates bounded validity propositions about computational processes represented by +software and evidence-bearing artifacts. It does not decide whether an actor is good, bad, +reputable, or trustworthy; identity and reputation alone contribute no verdict weight. + +It addresses a practical review problem: a final answer or green check rarely says +exactly what was checked, which evidence was used, where the conclusion stops, or what +would overturn it. VSTD carries those boundaries with the result. + +**Current boundary:** implemented reference paths cover receipts, generic computation +capture, provenance graphs, verification geometry, accelerator evidence, grounded +certificate checking, reproduction, exact-byte artifact freezing, finite self-closing +seals, copy-on-write thawing, and a flagship adversarial demo. VSTD-4 candidate depth and +Graph profiles 2–5 are candidate computations with conformance `NOT_ESTABLISHED`; +VSTD-5 is not implemented. See [current maturity](#current-maturity) and +[claims and limits](docs/CLAIMS_AND_LIMITS.md). -## See it fail correctly +[Normative specifications](standard/LADDER.md) · +[60-second quickstart](docs/QUICKSTART.md) · +[Implementation reference](https://timelordraps.github.io/verifier/reference.html) · +[Report an ambiguity or counterexample](https://github.com/TimeLordRaps/verifier/issues/new/choose) · +[Report a vulnerability privately](SECURITY.md) + +## 30–60 second demonstration ```bash git clone https://github.com/TimeLordRaps/verifier.git @@ -32,7 +52,7 @@ python -m pip install . vstd demo ``` -The side-effect-free flagship demo runs four adversarial specimens. Abridged output: +The side-effect-free demo runs four public adversarial specimens: ```text VSTD flagship adversarial demo @@ -40,35 +60,88 @@ VSTD flagship adversarial demo [DEMO OK] Valid-looking proof, wrong artifact → REJECTED [DEMO OK] Bound exhausted without a false answer → ACCEPTED/UNKNOWN [DEMO OK] Inflated verification-cost claim → REJECTED -[DEMO OK] Revoked ancestor behind valid descendants → GRAPH-LEVEL-0 +[DEMO OK] Revoked ancestor behind valid descendants → GRAPH-CANDIDATE-0 ``` -These are bounded checks over included specimens—not evidence of empirical truth, -complete provenance, external adoption, or general AI safety. Run `vstd demo --json` -for the complete machine-readable results or `vstd demo --emit-specimens PATH` to -emit each specimen. - -## What VSTD adds +`[DEMO OK]` means the expected defensive outcome occurred; it is not a VSTD `PASS`. +The scenarios establish bounded behavior of this reference implementation over the +included specimens. They do not establish empirical truth, complete provenance, +external adoption, independent implementation, or general artificial intelligence (AI) +safety. Use `vstd demo --json` for JavaScript Object Notation (JSON) output or +`vstd demo --emit-specimens PATH` to inspect the generated files. + +## What a result means + +VSTD result terms remain tied to one exact proposition, mechanism, evidence set, and +bound: + +| Result | Bounded meaning | It does not mean | +|---|---|---| +| `PASS` | The named mechanism established its declared proposition inside the stated coordinate and bounds. | The proposition is universally or permanently true. | +| `FAIL` | The mechanism found a checked violation, rejected certificate, or counterexample at the named surface. | Every broader interpretation is false. | +| `UNKNOWN` | Available evidence, capability, or resources did not establish `PASS` or `FAIL`. | False, safe, unsupported forever, or “probably PASS.†| +| `CONFLICTED` | Incompatible evidence or assertions remain explicit. | The conflict was resolved by choosing one side. | +| `NOT_ESTABLISHED` | The repository computes a candidate, but a required evidence-binding or conformance mechanism is absent. | Conformance, readiness, or a weak form of `PASS`. | + +A VSTD `PASS` never means “true in the real world†without the exact real-world +proposition and observation boundary being part of the checked claim. + +## Current maturity + +This is the canonical repository status table. “Implemented†applies only to the named +reference surface; it does not imply adoption, external interoperability, certification, +or a second implementation. + +| Surface | Normative status | Reference implementation | Evidence binding | Conformance status | Missing mechanism or evidence | +|---|---|---|---|---|---| +| 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 | +| 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 | A grounded decision certificate (GDC) parser/kernel plus VSTD-4 candidate-depth computation | The certificate binds formula, grounding, claim, roots, and bounds; rung references and VSTD-1/2/3 preconditions are not evidence-bound by the candidate-depth runtime | `NOT_ESTABLISHED` | Rung-by-rung evidence validation, prerequisite-profile composition, and an independent checker implementation | +| VSTD-5 | Draft | Fail-closed rejection of current VSTD-4 candidates only | No witness-corroboration binding is implemented | Not implemented | Witness protocol, qualifying VSTD-4 input, distinct actors, independence evidence, and operational experience | +| VSTD-Graph-1 | Project specification with implemented reference subset | Content-addressed artifacts, transformations, conflicts, policy queries, receipts, and recorded reachability | Binds recorded objects and edges; it does not establish real-world completeness or causality | Implemented reference subset | Independent implementation and external provenance-profile interoperability | +| VSTD-Graph-2 | Project specification | Candidate Bounded Collection Surface profile and ceiling-certificate computation | Uses caller-supplied object and edge ratings; the ratings are not validated against profile-2 evidence | `NOT_ESTABLISHED` | Rating-to-evidence validators for members, ancestors, statuses, and transformation edges | +| VSTD-Graph-3 | Project specification | Candidate Accountable Provenance Closure profile and ceiling-certificate computation | Uses caller-supplied object and edge ratings; no mechanism establishes that VSTD-3 produced them | `NOT_ESTABLISHED` | VSTD-3 rating evidence for every member, reachable ancestor, and transformation edge | +| VSTD-Graph-4 | Project specification | Candidate Refutable Transformation Closure profile and ceiling-certificate computation | Uses caller-supplied object and edge ratings; claimed refutability-closure records are not validated | `NOT_ESTABLISHED` | VSTD-4 rating evidence and validation of every reached refutability closure | +| VSTD-Graph-5 | Draft profile | Candidate Graph profile 5 can be computed from caller-supplied ratings | No independent-witness or rating-evidence binding | `NOT_ESTABLISHED` | Graph-2–4 evidence binding plus a corroborated verification-network protocol | +| 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 | +| 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 | +| zero-identity/zero-knowledge (ZIZK) artifact-first TRUST | Governing VSTD architecture in `standard/LADDER.md` section 1.1; not a separate numbered or receipt profile | Artifact-bound process semantics, forward TRUST, time-indexed ROT, and reverse diagnostic RUST | Existing mechanism-specific evidence only; identity or reputation alone, repetition, age, and topology add no assurance | Governing architectural invariant; not a separate VSTD conformance result | Event serialization, TRUST-transfer algebra, ROT derivation/propagation, RUST concentration/localization, complete trichotomy derivation, and maturation of specific optional proof backends | +| RISC Zero proof-carrying reference mechanism | Bounded non-normative mechanism example under the governing ZIZK architecture | Pinned prover/verifier source plus a tracked real receipt, public envelope, self-test result, and verifier command that can run network-offline after setup | Authenticates one fixed hidden-witness predicate and expected image identifier; it does not establish the witness's external truth | Native proof verified; no VSTD receipt mapping | Complete VSTD trichotomy predicate, second build, external audit, and additional proof backends | + +The authoritative implementation-to-specification map is +[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Normative meaning remains under +[standard/](standard/). + +## Why VSTD exists Ordinary computational results often omit machine-readable answers to four questions: -1. **What exactly was claimed?** The subject, predicate, parameters, and limits. -2. **Which exact evidence supports it?** Digests, mechanisms, provenance, and trust roots. -3. **Where does the verdict stop?** Explicit coordinates and resource bounds. -4. **How can it change?** Reproduction, counterexample, challenge, and degradation rules. +1. **What exactly was claimed?** Subject, predicate, parameters, scope, and limits. +2. **Which evidence supports it?** Exact bytes, digests, mechanisms, provenance, and trust roots. +3. **Where does the verdict stop?** Explicit coordinates, exclusions, and resource bounds. +4. **How can it change?** Reproduction, counterexample, challenge, invalidation, and degradation rules. + +VSTD packages that review boundary in receipts and provenance hypergraphs. The core +design rule is: -VSTD stores those answers in receipts and provenance hypergraphs. The reference -implementation can validate stable receipt content, reproduce declared mechanisms, -check grounded decision certificates, and compute collection-level ceilings from -recorded ancestry and caller-supplied object and edge ratings. +> No assurance is gained from storage location, field name, repetition, graph +> multiplicity, actor reputation, or propagation. Every increase must identify the +> verification mechanism that earned it. -## Two axes; evidence never substitutes +## Architecture -Specification numbers identify verification depth, not revisions. Every row is a -different question with its own evidence. A higher-layer result does **not** supply, -imply, upgrade, or repair a lower-layer result. +Verifier Standard object and Graph numbered profiles, each requiring separate evidence for its closure coordinate -| Depth | VSTD object mechanics | VSTD-Graph collection dynamics | +VSTD is a verification complex of named closure coordinates and evidence-bearing +relations. Specification numbers select cumulative profiles, not software revisions, +interchangeable layers, or scalar assurance levels. The object axis evaluates one +computational claim; the Graph axis evaluates a bounded collection and its recorded +transformations. + +| Profile number | Object closure coordinate | Graph closure coordinate | |---:|---|---| | 1 | Claim mechanics | Recorded lineage | | 2 | Verification surface | Bounded collection surface | @@ -76,136 +149,237 @@ imply, upgrade, or repair a lower-layer result. | 4 | Refutability | Refutable transformation closure | | 5 | Witness corroboration | Corroborated verification network | -An aggregate depth of `N` is valid only when distinct evidence passes every layer from -1 through `N`. Layers 1–4 are self-discernable; layer 5 requires another party to -exist, act, and be independent. VSTD-5 and its witness protocol remain **DRAFT**. +A later-profile result does **not** supply, imply, upgrade, or repair a prerequisite +coordinate. Object profile depth requires separate passing evidence for every required +coordinate. + +As an operational traversal, an implementation may capture a run through VSTD-1, map +profiler or domain observations through adjacent adapters into a VSTD-2 surface, bind the +execution substrate through VSTD-3, make the result portably refutable through VSTD-4, and +record independently evidenced witness corroboration through VSTD-5. This traversal does +not collapse the named coordinates. VSTD-Graph is the orthogonal collection axis: a bounded Graph result +may be materialized as a content-addressed artifact and enter a later verification loop only +with its source graph, selected surface, mechanism, lineage, losses, limitations, conflicts, +and current admissibility preserved. Current Graph-2 through Graph-5 results remain +`NOT_ESTABLISHED` candidates because their supplied ratings are not evidence-bound. + +The formal names **TRUST**, **ROT**, and **RUST** are semantic terms, not acronyms, actor +ratings, scalar scores, serialized receipt values, or references to the Rust programming language. +TRUST is mechanism-earned artifact support moving forward through checked development; +ROT is typed, time-indexed degradation of current admissibility without rewriting +historical evidence; RUST is the inverse-TRUST diagnostic mechanic moving backward from a +descendant deviation toward recorded ancestor candidates. This memetic propagation does +not prove guilt, responsibility, falsehood, causal localization, or automatic ancestor +falsification. See [the governing +architecture](standard/LADDER.md#11-artifact-first-causal-provenance-orientation). + +This is VSTD's **ZIZK artifact-first TRUST architecture**, not an optional research +profile. Zero identity means zero identity-derived verdict weight, not anonymity or the +absence of identifiers. Zero knowledge means zero unevidenced knowledge is presumed: a +proposition remains `UNKNOWN` until a named mechanism earns a bounded result. When a +witness must remain confidential, cryptographic zero knowledge can enclose that +architectural rule by binding the exact program, predicate, public commitments, output, +proof parameters, and verifier without attaching TRUST to the prover's identity. Only a +named proof system can earn that privacy property; a digest or hidden input cannot. The +runnable +[RISC Zero reference mechanism](examples/zizk_artifact_first/) is one bounded backend, +while its proof system and unfinished transfer mechanics remain mechanism-specific. + +## Install and use + +The distribution name is `verifier-standard`. The published base package has no +required third-party runtime dependencies. -Start with [`standard/LADDER.md`](standard/LADDER.md). Wire identifiers are frozen -separately in [`standard/WIRE_IDENTIFIERS.md`](standard/WIRE_IDENTIFIERS.md). +```bash +python -m pip install verifier-standard # latest published release +python -m pip install . # current release-candidate checkout +python -m pip install ".[yaml]" # YAML Ain't Markup Language (YAML) manifests +python -m pip install ".[jsonschema]" # JSON Schema validation +python -m pip install ".[seal]" # optional Ed25519 artifact sealing +python -m pip install ".[scitt]" # optional SCITT/COSE experiment +``` -## Choose a path +`vstd` is the canonical cross-platform CLI name. `verifier` remains a compatibility +alias but can resolve to Windows Driver Verifier. `verifiable` is a permanent legacy +alias because historical receipts may bind it in falsification instructions. -| If you want to… | Start here | -|---|---| -| Understand the claim model in ten minutes | [`docs/QUICKSTART.md`](docs/QUICKSTART.md) | -| Try to break the core claim | [`examples/flagship_demo`](examples/flagship_demo) | -| Inspect a disclosure-bounded closed evaluation | [`examples/simulacrabench_synthetic`](examples/simulacrabench_synthetic) | -| Implement an independent checker | [`standard/VSTD-4.md`](standard/VSTD-4.md) and [`VSTD4-GDC-1` schema](receipts/schema/vstd4_certificate.json) | -| Model a provenance collection | [`standard/VSTD-Graph-1.md`](standard/VSTD-Graph-1.md) | -| Integrate accelerator evidence | [`docs/layers/vstd-3/vendor-integration.md`](docs/layers/vstd-3/vendor-integration.md) | -| Use VSTD beside existing supply-chain/provenance systems | [`docs/ECOSYSTEM.md`](docs/ECOSYSTEM.md) | -| Review exact public claim limits | [`docs/CLAIMS_AND_LIMITS.md`](docs/CLAIMS_AND_LIMITS.md) | - -## Capture a generic computation - -**Security boundary:** a manifest contains an executable command. `vstd run` does not -sandbox it. Inspect the plan first; run only a trusted manifest inside an operating -system or container boundary appropriate to that command. Declared-path checks expose -capture scope, not everything the subprocess can access. +An unrelated PyPI distribution named `verifier` exports the same top-level Python +import. Do not co-install it with `verifier-standard`. + +### Freeze, seal, verify, and thaw an artifact + +Freezing preserves exact regular-file bytes and portable paths. Sealing is a separate, +readable authentication and closure action; it is **not encryption**. Generate an +Ed25519 key with a suitable local key tool, then run: + +```bash +openssl genpkey -algorithm Ed25519 -out ed25519-private.pem +vstd artifact freeze PATH ARTIFACT.vstd --media-type application/octet-stream +vstd artifact verify ARTIFACT.vstd --freeze-only +vstd artifact seal ARTIFACT.vstd --private-key ed25519-private.pem +vstd artifact verify ARTIFACT.vstd --expected-artifact-id EXPECTED_ID +vstd artifact thaw ARTIFACT.vstd MUTABLE_COPY +vstd artifact status MUTABLE_COPY +``` + +The finite seal signs the complete envelope with its signature and identifier fields +explicitly empty, then derives the seal identifier over the signature-bearing envelope +with only its identifier empty. Verification recomputes both projections, avoiding an +infinite seal-of-seal regress. The carried public key establishes internal consistency; +an expected artifact identifier, expected key identifier, or separately verified +manifest/log coordinate is still required to detect whole-bundle substitution. + +A freeze or seal establishes bounded integrity and closure only—not correctness, +freshness, ownership, authorization, trusted time, external preservation, or actor trust. +Thaw is copy-on-write: it creates a mutable descendant and leaves the sealed parent +unchanged. See the normative +[artifact-control mechanism](standard/ARTIFACT_CONTROL.md) and the architectural +[realm/time-capsule model](docs/REALMS_AND_TIME_CAPSULES.md). + +### Capture a generic computation + +A manifest contains an executable command. `vstd run` does not sandbox it. Inspect the +plan first and execute only trusted manifests inside an appropriate operating-system or +container boundary. ```bash vstd plan examples/generic_run/manifest.json --json vstd run examples/generic_run/manifest.json --output /tmp/vstd-receipt -vstd inspect /tmp/vstd-receipt vstd validate /tmp/vstd-receipt +vstd inspect /tmp/vstd-receipt vstd reproduce /tmp/vstd-receipt --rerun ``` -`validate` checks stable receipt content. `reproduce --rerun` executes the recorded -command again when permitted and compares the declared outputs. Neither operation -widens the receipt into a claim about the unobserved world. +Generic `validate` checks the strict profile shape and stable-payload digest. It does +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. -## The grounded certificate +### Use the Python application programming interface (API) -`VSTD4-GDC-1` binds a decision to the claim and evidence it is supposed to describe: +```python +from pathlib import Path -```text -DecisionCertificate -├── header verdict, tightest cost tier, counts, binding digest -├── formula normalized finite clauses -├── grounding variables → facts; clauses → named encoding rules -├── decision model, proof, witness, or bounded UNKNOWN transcript -└── hints untrusted, optional, and strippable +from verifier.core.run import describe_run_plan, load_manifest + +manifest_path = Path("examples/generic_run/manifest.json") +manifest = load_manifest(manifest_path) +plan = describe_run_plan(manifest, manifest_path.parent) +print(plan["command"], plan["executes_without_sandbox"]) ``` -The checker rejects over-budget headers before proof work, rejects cost-tier inflation, -checks grounding before the decision block, and preserves `UNKNOWN` when a declared -bound is exhausted. `VSTD4-GDC-1` is a VSTD project format; reference-kernel acceptance -is not external validation. +The installed wheel contains byte-identical copies of every normative specification, so +a verifier descriptor can retain its exact specification binding outside a source +checkout. See the generated [CLI and API +reference](https://timelordraps.github.io/verifier/reference.html). -## Install and command names +## Receipts, Graphs, and grounded certificates -The distribution name is `verifier-standard`; the base install has no required -third-party runtime dependencies. +- [VSTD-1 receipts](standard/VSTD-1.md) carry claim coordinates, evidence, + checker results, trust boundaries, and reproducibility information. +- [VSTD-Graph-1](standard/VSTD-Graph-1.md) records content-addressed artifacts, + many-to-many transformations, conflicts, and bounded downstream reachability. +- [`VSTD4-GDC-1`](standard/VSTD-4.md) binds a decision certificate to a formula, + grounding, claim coordinate, verifier descriptor, roots, and resource bounds. -```bash -python -m pip install "verifier-standard==1.1.3" -python -m pip install . -python -m pip install ".[yaml]" # YAML manifests -python -m pip install ".[jsonschema]" # schema validation -python -m pip install ".[llguidance]" # optional constraint adapter -python -m pip install ".[torch]" # optional tensor adapter +The grounded-certificate checker rejects over-budget headers before proof work, rejects +cost-tier inflation, validates grounding before the decision block, and preserves +`UNKNOWN` when a bound is exhausted. Kernel acceptance establishes only the bounded +certificate result; it is not VSTD-4 conformance, evidence authenticity, external +validation, or proof of the unobserved world. + +## Interoperability + +VSTD composes beside native systems rather than replacing them: + +```text +native object ──native verifier──> native result + └──── exact bytes + identity ──> loss-declared adapter + └──> VSTD claim boundary ``` -`vstd` is the canonical cross-platform command. `verifier` remains an alias, but an -unqualified `verifier` command on Windows commonly resolves to Windows Driver Verifier. -`verifiable` remains a permanent compatibility alias because published project receipts -may bind it in falsification instructions. +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 +adjacent verification under test keys. SCITT registration proves neither payload +correctness nor VSTD conformance. See the [crosswalk](docs/standards/VSTD_SCITT_CROSSWALK.md), +[semantic boundary](docs/standards/SCITT_SEMANTIC_BOUNDARY.md), and +[runnable example](examples/scitt_interop/). -An unrelated PyPI distribution named `verifier` exports the same top-level Python -import. Do not co-install it with `verifier-standard`: Python packaging does not prevent -two distributions from overwriting one import package. Install this project by its full -distribution name and use `vstd` as the command. +The [ecosystem map](docs/ECOSYSTEM.md) separately covers adjacent provenance, +software-supply-chain, signing, and transparency systems without implying endorsement or +adoption. + +## Specifications and navigation -## Verify a release +Read authoritative material in this order: -Release assets include an external manifest binding the exact public source ref, -commit, archive digest, file set, and member bytes. The release builder produces a -platform-independent canonical source ZIP, wheel, and source distribution from that -source coordinate. CI independently builds the full set on Windows and Linux and fails -unless every artifact is byte-identical. -GitHub/Sigstore artifact attestations bind the ZIP, wheel, source distribution, and -manifest to the release workflow: +1. [Verification complex, terminology, and profile composition](standard/LADDER.md) +2. [Object and Graph numbered-profile documents](standard/) +3. [Serialized receipt identifiers](standard/WIRE_IDENTIFIERS.md) +4. [Published schemas](receipts/schema/) +5. [Implementation ownership](docs/ARCHITECTURE.md) +6. [Claims and limits](docs/CLAIMS_AND_LIMITS.md) + +Additional entry points: + +| Goal | Document | +|---|---| +| Install and exercise the first-run path | [Quickstart](docs/QUICKSTART.md) | +| Understand terminology and precedents | [Concepts and precedents](docs/CONCEPTS_AND_PRECEDENTS.md) | +| Inspect abbreviated terms | [Acronyms](docs/ACRONYMS.md) | +| Review experimental profiles | [Experiment index](experiments/INDEX.md) | +| Understand human claim traversal | [Human operating guide](HUMANS.md) | +| Inspect project direction and non-goals | [Roadmap](ROADMAP.md) | + +## Reproducibility and releases + +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 +artifact attestations bind uploaded bytes to the workflow; they do not establish source +correctness, tag identity, or adoption. ```bash gh attestation verify PATH_TO_DOWNLOADED_ASSET --repo TimeLordRaps/verifier ``` -Release notes report the tag-signature status separately. An artifact attestation is -not a tag signature. The signed `v1.1.2` GitHub release was not uploaded to PyPI because -its Windows and Linux builds differed. PyPI publication now requires the cross-platform -equality gate plus approval in the protected `pypi` environment. See -[`RELEASING.md`](RELEASING.md) for the complete gate. - -## Project status - -VSTD is a founder-maintained **alpha project specification**. There is no demonstrated -external adoption, independent implementation, interoperability deployment, or -third-party security review. It is not an accredited, consensus, IETF, ISO, or W3C -standard. A `VERIFIED` result is always relative to declared coordinates, evidence, -mechanisms, bounds, and trust roots. - -Current public-review priorities are counterexamples to normative statements, -ambiguous wire rules, independent parser results, interoperability failures, and -receipts that pass when they should fail. Use the -[issue forms](https://github.com/TimeLordRaps/verifier/issues/new/choose). Send sensitive -findings through [`SECURITY.md`](SECURITY.md), not a public issue. - -VSTD may improve auditability, reproducibility, incident analysis, and challenge -propagation over observable records. It cannot prove general AI safety, reveal hidden -model internals, establish physical-world completeness, or compensate for missing -instrumentation. - -## Project process - -- Specification order: [`LADDER`](standard/LADDER.md) → layer documents → schemas → - independent checker → conformance tests. -- Public technical direction: [`ROADMAP.md`](ROADMAP.md). -- Contribution rules: [`CONTRIBUTING.md`](CONTRIBUTING.md). -- Automated-contributor rules: [`AGENTS.md`](AGENTS.md). -- Governance and release authority: [`GOVERNANCE.md`](GOVERNANCE.md). -- Security and disclosure: [`SECURITY.md`](SECURITY.md). -- Release construction and attestations: [`RELEASING.md`](RELEASING.md). - -Apache License 2.0. See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE). VSTD is not +Use [RELEASING.md](RELEASING.md) to verify the manifest, tag, artifact attestations, +package name, and historical compatibility. The current checkout is an unreleased +1.2.0 candidate; use the [latest release page](https://github.com/TimeLordRaps/verifier/releases/latest) +for published citation and artifact coordinates. + +## Claims, security, and contribution + +Review [claims and limits](docs/CLAIMS_AND_LIMITS.md) before publishing a VSTD result. +The reference implementation may improve auditability, reproducibility, incident +analysis, and challenge routing over observable records. It cannot prove general AI +safety, reveal hidden model state, establish physical-world completeness, or compensate +for missing instrumentation. + +`vstd run` executes manifest commands without sandboxing. See the +[security policy](SECURITY.md) and use GitHub private vulnerability reporting for +sensitive findings. + +Contributors should start with [CONTRIBUTING.md](CONTRIBUTING.md), which identifies +normative, implementation, schema, adapter, test, compatibility, and release pathways. +Use the issue forms for a +[specification ambiguity](https://github.com/TimeLordRaps/verifier/issues/new?template=specification-ambiguity.yml), +[counterexample](https://github.com/TimeLordRaps/verifier/issues/new?template=counterexample.yml), +or [implementation/interoperability report](https://github.com/TimeLordRaps/verifier/issues/new?template=implementation-report.yml). + +Project authority and centralization are documented in [GOVERNANCE.md](GOVERNANCE.md). +Automated-contributor rules live in [AGENTS.md](AGENTS.md); the human operating model in +[HUMANS.md](HUMANS.md); and live repository contradictions only in [TIME.md](TIME.md). + +## Citation and license + +Cite a published release from its versioned GitHub release metadata or +`CITATION.cff` at that tagged coordinate. Do not cite unreleased candidate metadata as +a published release. + +Licensed under the [Apache License 2.0](LICENSE); see [NOTICE](NOTICE). VSTD is not affiliated with or endorsed by the Apache Software Foundation. diff --git a/ROADMAP.md b/ROADMAP.md index 1f9300c..05cd737 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,9 +1,21 @@ -# VSTD public technical roadmap +# Verifier Standard (VSTD) public technical roadmap + +> **Acronyms:** Concise Binary Object Representation (CBOR); CBOR Object Signing and Encryption (COSE); +> grounded decision certificate (GDC); Internet Engineering Task Force (IETF); +> reduced instruction set computer (RISC); Boolean satisfiability problem (SAT); +> Supply Chain Integrity, Transparency, and Trust (SCITT); +> zero-identity/zero-knowledge (ZIZK). + +TRUST is mechanism-earned forward artifact support; ROT is typed, time-indexed +degradation of current admissibility; and RUST is inverse-TRUST diagnostic traversal +toward recorded ancestors. They are formal semantic names, not acronyms or actor ratings. **Status:** direction, not a promise of delivery or adoption **Scope:** the public specification, reference implementation, and interoperability surface only +**Reader context:** [`Concept guide and intellectual precedents`](docs/CONCEPTS_AND_PRECEDENTS.md) + ## The near-term problem “Speed superintelligence†is used here as an operational condition, not as a model @@ -18,11 +30,59 @@ record of: - the time, memory, disclosure, and availability bounds; - the conditions that produce `FAIL`, `UNKNOWN`, challenge, or degradation. -VSTD's intended role is to make that review object cheap to transfer, independently -checkable within stated bounds, and capable of being overturned. It is evidence +VSTD's intended role is to make that review object cheap to transfer, checkable outside +its producer within stated bounds, and capable of being overturned. It is evidence infrastructure around fast systems—not proof that a system is aligned, safe, conscious, superintelligent, or fully observed. +## The next question: what should we check first? + +Verification is never free. A project can usually identify more claims, artifacts, and +dependencies worth checking than its available time, compute, evidence access, and human +attention can cover. Hiding that constraint does not remove it; it only makes the choice +of what went unchecked harder to inspect. + +The intended next direction is straightforward for a newcomer: + +1. record the available verification budget; +2. choose which check to run next under a declared policy; +3. record why that check was selected and what was deferred; +4. preserve the native verifier's actual result and VSTD claim boundary; and +5. observe whether the policy makes artifacts easier to check—or merely easier to game. + +This is **bounded verification allocation**. A priority is a scheduling result, not a +truth result. “Check this first†does not mean “this is false,†“this is important in +every context,†or “everything else is safe.†Budget exhaustion leaves the deferred +surface explicit and unresolved. + +The longer-term objective is a portable, verifier-neutral way to: + +- allocate bounded verification work across different proof engines, domain verifiers, + tests, reproduction procedures, and challenge routes; +- bind the policy, evidence, expected cost, downstream blast radius, and recorded reason + for each allocation decision; +- measure **verification yield** without reducing it to solver time alone; +- make certificate-friendly, modular, replayable, and cheaply refutable artifacts easier + to select and deploy; and +- expose feedback loops in which artifacts or adaptive systems change their behavior + because they anticipate what will be checked. + +The allocation policy is itself a versioned software artifact. It can therefore be +tested, challenged, meta-verified, and represented in VSTD-Graph alongside the artifacts +and verifier actions it influences. A stable feedback loop is not automatically a true +one: randomized challenges, counterevidence searches, dependency-aware updates, and +explicit `UNKNOWN` outcomes remain necessary to resist self-confirming verification. + +This direction composes established work on +[bounded optimality](https://www.cs.cmu.edu/afs/cs/project/jair/pub/volume2/russell95a.pdf), +[active testing](https://proceedings.mlr.press/v139/kossen21a.html), +[cost-sensitive testing trees](https://proceedings.mlr.press/v32/cicalese14.html), +[proof-carrying code](https://people.eecs.berkeley.edu/~necula/papers.html), and +[certifying algorithms](https://www.sciencedirect.com/science/article/pii/S1574013710000560). +The roadmap does not claim those foundations as VSTD inventions. The research question +is whether VSTD can provide interoperable claim boundaries and portable result semantics +for their combined use across heterogeneous verification substrates. + ## Vision board ```text @@ -30,7 +90,7 @@ TODAY NEXT TARGET CONDITION fast opaque result result + bounded receipt claims travel with challenges green check only → PASS / FAIL / UNKNOWN → wrong claims degrade visibly flat artifact list provenance hypergraph poisoned ancestry has blast radius -producer's own word independent checker kit multiple implementations can disagree +producer's own word separate checker kit multiple implementations can disagree manual after-the-fact audit policy-bound event capture review scales with evidence, not rhetoric ``` @@ -42,8 +102,118 @@ claim → evidence → bounded check → publish → challenge → adjudicate └────────────────────── new evidence / corrected claim ──────────────────┘ ``` -No arrow in that loop upgrades one VSTD layer with another layer's evidence. Each -layer still requires its own evidence; the loop only carries results and challenges. +No arrow in that loop upgrades one VSTD closure coordinate with another coordinate's +evidence. Each coordinate still requires its own evidence; the loop only carries results +and challenges. + +## Implemented 1.2 artifact-control foundation + +[`standard/ARTIFACT_CONTROL.md`](standard/ARTIFACT_CONTROL.md) defines a mechanism beneath +the numbered profiles: exact regular-file byte and path preservation, dual-algorithm +artifact-derived identity, an observable read-only payload-tree guard, finite readable +self-closing seals, external artifact/key anchor checks, and copy-on-write thaw descendants. +The mechanism is implemented through `vstd artifact` and the supported Python interface. + +This is structural closure, not encryption, archival custody, semantic correctness, +trusted time, actor trust, or a numbered VSTD profile result. The +[realm/time-capsule architecture](docs/REALMS_AND_TIME_CAPSULES.md) permits continuous, +discrete, causal, problem-space, branching, cyclic, and atemporal structures, but VSTD +1.2 does not yet define a realm receipt, continuity-law verifier, cross-realm mapping +verifier, or language-model transition verifier. + +## Classical interoperability vocabulary target + +The near-term interoperability scope is classical computation. **Deterministic** means +that every semantically relevant source of choice is absent or bound as an explicit +input. A seed, repeated output, or deterministic-mode flag alone does not establish that +condition. + +These are roadmap-level interoperability **meta-classes**, not new receipt fields or +frozen identifiers: + +| Term | Minimum meaning | +|---|---| +| Semantic frame | Exact language, logic, theory, type system, operation set, machine and numerical semantics, versions, and undefined or implementation-defined behavior. | +| Problem frame | Bound instance, declarations, inputs, assumptions, options, objectives, constraints, and initial or session state. | +| Proposition frame | Exact relation being checked, its quantifiers, subject, scope, bounds, horizon, and required counterexample or witness condition. | +| Mechanism contract | Supported frames and claim kinds, checker and trust roots, soundness basis, completeness or incompleteness boundary, resource limits, and known exclusions. | +| Native outcome | The tool's exact status and native meaning; it remains distinct from the VSTD assessment earned by checking it. | +| Evidence payload | Typed model, witness, proof, certificate, core, trace, counterexample, diagnostic, reproducer, coverage record, or primal/dual bound. | +| Transformation obligation | Source and target frames, mapping, claimed relation—such as equivalence, refinement, implication, or equisatisfiability—information loss, and the mechanism checking that relation. | +| Exploration scope | Exhaustive, sampled, bounded, abstracted, under-approximated, or over-approximated search; explored states, paths, regions, and stopping reason. | +| Choice schedule | Random-number-generator algorithm and state, sampler, tie-breaking, concurrency schedule, external responses, and every other choice that affects replay. | +| Numerical contract | Data types, precision, rounding, accumulation order, tolerances, overflow, exceptional values, quantization, and comparison rule. | +| Operational trace | Bound states, transitions, events, causal or topological order, external effects, checkpoints, and omitted observation surface. | +| Composition obligation | Typed dependency relation, imported assumptions or axioms, discharged guarantees, conflicts, and the rule preventing repetition or topology from increasing assurance. | + +### Meta-class and native-object boundary + +A meta-class names a cross-domain semantic role. A **meta-object** is one bounded VSTD +instance of that role. A **native object kind** is defined by the source verifier, and a +**native object** is an exact instance governed by that verifier's semantics. An adapter +maps the native object into one or more meta-objects while preserving its identity, +native result, assumptions, bounds, and declared information loss. + +A classical verification episode should be expressible through these meta-classes, but +an individual artifact need not instantiate all twelve, and one artifact may occupy +several roles. Missing, inapplicable, and unobserved roles remain distinct. Meta-class +membership is organization, not verification; it earns no assurance without the named +mechanism that checks the object and its mapping. + +For example, Lean retains its own objects and semantics: + +| Interoperability meta-class | Lean native object kind | Example meta-object binding | +|---|---|---| +| Semantic frame | Type theory and declaration environment | Exact Lean version, imported environment, options, and module identities. | +| Proposition frame | Theorem declaration and its type | Exact proposition, universe parameters, and declaration coordinate. | +| Evidence payload | Elaborated proof term | Exact term checked for the bound proposition. | +| Mechanism contract | Kernel and its accepted core language | Kernel implementation/version, configuration, trust roots, and exclusions. | +| Transformation obligation | Elaboration from syntax or tactics to a core proof term | Bound source, produced term, mapping, dependencies, and information loss. | +| Composition obligation | Imported definitions, theorems, and axioms | Exact dependency and axiom set retained as prerequisites rather than inherited truth. | +| Native outcome | Kernel acceptance or rejection | Exact native result and diagnostics before any VSTD assessment. | + +The first machine-learning specialization is a classically executed autoregressive +transition: bound model and weight bytes, tokenizer, operation graph, prefix, cache/state, +numerical contract, logits transformations, choice schedule, and external tool inputs map +to a selected token and next state. Passing establishes only conformance of that declared +transition. It does not establish that the emitted text is true; that requires a separate +proposition-specific verifier. + +The vocabulary is grounded in distinctions already exposed by primary interfaces such as +[Lean proof terms and kernel checking](https://lean-lang.org/doc/reference/latest/), +[the satisfiability modulo theories library language](https://smt-lib.org/language.shtml), +[TLA+ behaviors and model checking](https://lamport.azurewebsites.net/tla/high-level-view.html), +[the Static Analysis Results Interchange Format](https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html), +[in-toto attestations](https://github.com/in-toto/attestation/tree/main/spec/v1), +[PyTorch reproducibility limits](https://docs.pytorch.org/docs/stable/notes/randomness.html), +[StableHLO program semantics](https://openxla.org/stablehlo/spec), and +[Transformers generation controls](https://huggingface.co/docs/transformers/main_classes/text_generation). + +A public “99%+ coverage†claim is prohibited until a versioned taxonomy names the +included classical fields and subfields, representative native specimens exist, and +round-trip plus adversarial loss tests show which mandatory distinctions survive each +adapter. Coverage means expressibility over that declared denominator; it is not market +share, adoption, correctness, or evidence that every tool has been tested. + +Quantum, thermodynamic, deoxyribonucleic acid (DNA), chemical, and chemputer verification +are outside the first denominator. A later operational-landscape extension must map this +shared interoperability vocabulary without redefining VSTD outcomes. Whether an extension +belongs in an optional module or a separately governed repository remains a future coupling +and ownership decision. + +## Current experimental development tracks + +This dated register records substantive work as of **2026-08-25**. A committed experiment, +passing test, or generated index is not normative, released, reproduced by a distinct actor, +or evidence of adoption merely because it exists. Profile manifests and the generated +[`experiments/INDEX.md`](experiments/INDEX.md) are the portable experiment register when +intentional experiment artifacts are present. + +| Track | Public artifact | Current boundary | Next gate | +|---|---|---|---| +| 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 | [`experiments/artifact_first_mechanisms/experiment.json`](experiments/artifact_first_mechanisms/experiment.json) | Experimental event serialization, TRUST-transfer algebra, ROT derivation/propagation, RUST concentration/localization, complete trichotomy derivation, and specific unfinished optional proof backends under the already-governing ZIZK architecture. The bounded identity evaluator and tracked RISC Zero reference mechanism are under `examples/`. | Implement and falsify each mechanism without treating the governing orientation as contingent or creating actor-tied trust. | +| 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. | ## Milestone 1 — make refutation the front door @@ -57,11 +227,11 @@ layer still requires its own evidence; the loop only carries results and challen - Public counterexample, ambiguity, implementation, and private-security routes are distinct and usable. -## Milestone 2 — independent checker kit +## Milestone 2 — separate checker kit **Build** -- a language-neutral `VSTD4-GDC-1` byte-level test vector bundle; +- a language-neutral `VSTD4-GDC-1` exact-byte test-vector bundle; - positive, negative, malformed, over-budget, and semantic-misbinding corpora; - a checker implementer's guide that does not require importing this Python package; - differential test instructions and a machine-readable conformance report. @@ -73,11 +243,24 @@ layer still requires its own evidence; the loop only carries results and challen - disagreements are preserved as public interoperability failures until resolved; - no “independent†label is used merely because two entry points call shared logic. -## Milestone 3 — agent-work profile +## Milestone 3 — experimental-workflow and agent-work profiles -**Build** +**Implemented in experimental profile 0.1** + +- a platform-independent, non-normative experimental-workflow profile for questions, + hypotheses, preregistration, interventions, observations, native-verifier results, + budgets, amendments, challenges, and publication state; +- a GitHub adapter that maps issues, commits, workflow runs, artifacts, pull requests, + and merges without treating repository state as a verification verdict; +- bounded verification-allocation records that preserve the policy, reason, budget, + deferred surface, and native outcome without assigning truth by priority; +- deterministic canonicalization, repository-artifact binding, a generated experiment + index, adversarial tests, a verdict-neutral checked-in specimen, and an + artifact-first-mechanism dogfood manifest. + +**Still build** -- a non-normative profile for observable user, agent, and tool messages; +- an agent-harness specialization for observable user, agent, and tool messages; - bindings for repository state, patches, file reads, commands, outputs, tests, failures, retries, and final claims; - explicit serialization gaps for hidden prompts, inaccessible reasoning, and @@ -86,7 +269,10 @@ layer still requires its own evidence; the loop only carries results and challen **Exit evidence** -- the same trace can be checked by two independent consumers; +- the SCITT, artifact-first-mechanism, and SAT tracks can be indexed through the same experimental-workflow + vocabulary without changing their native verifiers or erasing their blockers; +- a GitHub merge remains an integration event rather than becoming a VSTD pass; +- the same trace can be checked by two separately maintained consumers; - deleting or substituting a bound tool output changes the receipt digest or fails a declared rule; - missing observability yields a named gap or `UNKNOWN`, never reconstructed fiction. @@ -142,4 +328,4 @@ This roadmap does not promise to: - prove all physical execution has been recorded; - replace sandboxing, signatures, identity systems, transparency logs, or domain truth tests; -- treat one layer's evidence as proof of another layer. +- treat one closure coordinate's evidence as proof of another coordinate. diff --git a/SECURITY.md b/SECURITY.md index cf97653..01fbdad 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,9 @@ # Security policy +> **Acronyms:** application programming interface (API); hash-based message authentication code (HMAC); +> Secure Hash Algorithm 256-bit (SHA-256); Secure Hash Algorithm 3 256-bit (SHA3-256); +> Verifier Standard (VSTD). + ## Supported release Only the latest tagged public release is supported. Historical receipts and standards @@ -13,10 +17,9 @@ GitHub repository to open a private vulnerability report with the maintainer: `https://github.com/TimeLordRaps/verifier/security/advisories/new` -GitHub private vulnerability reporting was enabled and verified through the repository -API on 2026-08-21. If GitHub does not show the private-reporting form, do not disclose -sensitive details in a public issue; report only the non-sensitive fact that the private -route is unavailable. +GitHub private vulnerability reporting is the intended sensitive-reporting route. If +GitHub does not show the private-reporting form, do not disclose sensitive details in a +public issue; report only the non-sensitive fact that the private route is unavailable. ## Scope @@ -37,3 +40,18 @@ failure, or accidental credential/raw-evidence disclosure. The HMAC emulator and anchor keys are explicitly test-only and are not production cryptography. Vendor or cloud product vulnerabilities should also be reported to the affected vendor through its own process; VSTD does not authorize testing third-party infrastructure. + +## Artifact-control boundary + +`vstd artifact freeze` rejects symbolic links and special filesystem objects, preserves +regular-file bytes, and makes the payload tree read-only. That guard is observable state, +not a sandbox or a defense against privileged writes. Keep an independently controlled +copy when durable preservation matters. + +Version 1 seals are readable Ed25519 signatures, not encryption. Protect private keys +outside artifact bundles and never commit a production private key. Because each bundle +carries its own public key, self-verification alone cannot detect substitution of the +entire bundle and key; relying parties should bind an expected artifact identifier, +expected key identifier, or independently verified external manifest/log coordinate. +SHA-256 and SHA3-256 are both recomputed over preserved bytes so later algorithm concerns +can be addressed by additive re-anchoring rather than rewriting historical artifacts. diff --git a/TIME.md b/TIME.md new file mode 100644 index 0000000..297804c --- /dev/null +++ b/TIME.md @@ -0,0 +1,23 @@ +# TIME + +Status: CLEAR + +TIME is the live repository-contradiction annunciator. Its status is repository process +metadata, not Verifier Standard (VSTD) receipt vocabulary. A live entry belongs here only +when current authoritative surfaces make incompatible claims about current semantics or +implementation. Runtime `CONFLICTED`, an honest `UNKNOWN`, roadmaps, ordinary work items, +limitations, and speculative research do not belong here. + +For agent response rules, see [`AGENTS.md`](AGENTS.md). For human interpretation and +escalation, see [`HUMANS.md`](HUMANS.md). + +## Live contradictions + +None. + +When a contradiction is open, change the status to `Status: OPEN` and record the exact +coordinates, both incompatible claims, evidence for each side, and affected behavior. An +evidence-backed repair removes the resolved live entry and returns this file to +`Status: CLEAR`; Git history preserves the prior state. Development branches may remain +open. The tag-triggered publication workflow checks the exact tagged checkout and fails +unless this file contains exactly one `Status: CLEAR` line. diff --git a/docs/ACRONYMS.md b/docs/ACRONYMS.md new file mode 100644 index 0000000..1e355ae --- /dev/null +++ b/docs/ACRONYMS.md @@ -0,0 +1,129 @@ +# Acronyms and abbreviated terms + +This is the canonical expansion key for the Verifier Standard (VSTD) repository. Every +independently readable document and source file must still expand each term at its first +reader-facing use; this page is a reference, not a substitute for local clarity. Frozen wire +identifiers, code symbols, filenames, and third-party names remain byte-for-byte unchanged. + +`TRUST`, `ROT`, and `RUST` are deliberately absent from the expansion table because they +are formal semantic names, not acronyms. They mean mechanism-earned forward artifact +support, typed time-indexed degradation of current admissibility, and inverse-TRUST +diagnostic traversal, respectively. `RUST` is not the Rust programming language. Their +normative definitions are in [`standard/LADDER.md`](../standard/LADDER.md). + +| Term | Expansion used in this repository | Scope note | +|---|---|---| +| `AI` | artificial intelligence | General field. | +| `AMD` | Advanced Micro Devices | Vendor name. | +| `API` | application programming interface | Software interface. | +| `ASCII` | American Standard Code for Information Interchange | Text encoding. | +| `ASIC` | application-specific integrated circuit | Purpose-built processor class. | +| `AST` | abstract syntax tree | Parsed program structure. | +| `AWS` | Amazon Web Services | Cloud provider. | +| `CBOR` | Concise Binary Object Representation | Binary data format. | +| `CCF` | Confidential Consortium Framework | Ledger framework used by one SCITT profile. | +| `CD` | continuous delivery or deployment | The delivery/deployment half of CI/CD workflow shorthand. | +| `CI` | continuous integration | Automated repository checks. | +| `CLI` | command-line interface | Terminal-facing program surface. | +| `CNF` | conjunctive normal form | Boolean-formula representation. | +| `COSE` | CBOR Object Signing and Encryption | Signed-message and receipt envelope family. | +| `CPU` | central processing unit | Processor class. | +| `CRLF` | carriage return and line feed | Two-character line ending. | +| `CT` | Certificate Transparency | Public certificate-log system. | +| `CUDA` | Compute Unified Device Architecture | NVIDIA parallel-computing platform. | +| `CVE` | Common Vulnerabilities and Exposures | Public vulnerability identifier system. | +| `CWT` | CBOR Web Token | Claim set used in COSE messages. | +| `DAG` | directed acyclic graph | Graph with directed edges and no directed cycle. | +| `DICE` | Device Identifier Composition Engine | Device-attestation architecture. | +| `DMTF` | DMTF standards organization | Current organizational name; do not invent a modern expansion. | +| `DOE` | design of experiments | Experimental-design method. | +| `DOI` | digital object identifier | Publication identifier. | +| `DPE` | DICE Protection Environment | DICE execution and key-derivation component. | +| `DPLL` | Davis-Putnam-Logemann-Loveland | Boolean satisfiability algorithm. | +| `DRAT` | deletion resolution asymmetric tautology | Clausal refutation format. | +| `EAT` | Entity Attestation Token | Attestation claim format. | +| `ECN` | Engineering Change Notice | Standards-change document. | +| `ELF` | Executable and Linkable Format | Binary executable format. | +| `EU` | European Union | Political and regulatory body. | +| `FLOP` | floating-point operation | Compute-work unit. | +| `FRAT` | flexible SAT proof format | Solver-to-elaborator proof format; use the proper format name rather than inventing a letter-by-letter expansion. | +| `FSM` | finite-state machine | State-transition model. | +| `GB` | gigabyte | Storage or memory capacity unit. | +| `GDC` | grounded decision certificate | VSTD-4 certificate family. | +| `GPG` | GNU Privacy Guard | Signature tool. | +| `GPU` | graphics processing unit | Accelerator class. | +| `GRAT` | GRAT proof format | Proper name of a hinted SAT proof format; no documented letter-by-letter expansion is asserted here. | +| `HMAC` | hash-based message authentication code | Keyed authentication construction. | +| `HTML` | Hypertext Markup Language | Web-page format. | +| `HTTP` | Hypertext Transfer Protocol | Web transfer protocol. | +| `HTTPS` | Hypertext Transfer Protocol Secure | HTTP protected by transport security. | +| `ID` | identifier | Stable name or coordinate. | +| `IDE` | integrated development environment | Programming application. | +| `IETF` | Internet Engineering Task Force | Internet standards organization. | +| `IR` | intermediate representation | Program or proof representation. | +| `ISO` | International Organization for Standardization | Standards organization. | +| `JSON` | JavaScript Object Notation | Structured text format. | +| `JSONL` | JSON Lines | One-JSON-value-per-line format. | +| `LF` | line feed | Single-character line ending. | +| `LRAT` | linear resolution asymmetric tautology | Hint-carrying clausal refutation format. | +| `MIG` | multi-instance GPU | NVIDIA accelerator-partitioning feature. | +| `ML` | machine learning | General field. | +| `NIST` | National Institute of Standards and Technology | United States standards agency. | +| `NP` | nondeterministic polynomial time | Computational-complexity class. | +| `NPU` | neural processing unit | Machine-learning accelerator class. | +| `NVML` | NVIDIA Management Library | NVIDIA device-management interface. | +| `OS` | operating system | Host software environment. | +| `PCC` | proof-carrying code | Producer-supplied proof checked by a consumer. | +| `PCI` | Peripheral Component Interconnect | Hardware interconnect family. | +| `PCI-SIG` | PCI Special Interest Group | PCI standards consortium. | +| `POPL` | Principles of Programming Languages | Research conference. | +| `PROV` | World Wide Web Consortium provenance vocabulary | W3C provenance standard family. | +| `PROV-DM` | PROV data model | W3C provenance data model. | +| `PS` | Protect the Software | NIST SSDF practice group. | +| `RAM` | random-access memory | Working memory. | +| `RAT` | resolution asymmetric tautology | Clausal redundancy property. | +| `RATS` | Remote Attestation Procedures | IETF attestation architecture. | +| `RFC` | Request for Comments | IETF publication series. | +| `RIM` | Reference Integrity Manifest | Trusted reference-measurement set. | +| `RISC` | reduced instruction set computer | Processor architecture family. | +| `RISC0` | RISC Zero | Product-name prefix used by RISC Zero tooling. | +| `RNG` | random number generator | Entropy or pseudorandomness source. | +| `RUP` | reverse unit propagation | Clausal proof-checking rule. | +| `SAT` | Boolean satisfiability problem | Decision problem and solver class. | +| `SCITT` | Supply Chain Integrity, Transparency, and Trust | IETF architecture and working group. | +| `SCRAPI` | SCITT Reference APIs | SCITT registration and receipt-resolution interface draft. | +| `SDK` | software development kit | Developer-facing library and tools. | +| `SHA-256` | Secure Hash Algorithm 256-bit | Cryptographic digest algorithm. | +| `SLSA` | Supply-chain Levels for Software Artifacts | Software supply-chain framework. | +| `SMI` | system management interface | Vendor device-management interface. | +| `SMT` | satisfiability modulo theories | Decision-procedure family. | +| `SMT-LIB` | SMT library standard | Common language and benchmark format for SMT solvers. | +| `SPDM` | Security Protocol and Data Model | Device authentication and measurement protocol. | +| `SPDX` | Software Package Data Exchange | Software-package metadata standard. | +| `SR-IOV` | single-root input/output virtualization | Hardware virtualization interface. | +| `SSDF` | Secure Software Development Framework | NIST software-development framework. | +| `SSH` | Secure Shell | Remote command and transport protocol. | +| `STARK` | scalable transparent argument of knowledge | Cryptographic proof-system family. | +| `TCB` | trusted computing base | Components on which a result depends. | +| `TDISP` | Trusted Device Interface Security Protocol | Device-interface isolation protocol. | +| `TPU` | tensor processing unit | Machine-learning accelerator class. | +| `TS` | Transparency Service | SCITT registration and receipt service. | +| `TUF` | The Update Framework | Software-update security framework. | +| `UNSAT` | unsatisfiable | Solver result meaning no satisfying assignment exists. | +| `URI` | uniform resource identifier | Resource name or locator. | +| `URL` | uniform resource locator | Network resource locator. | +| `UTC` | Coordinated Universal Time | Time standard. | +| `UTF-8` | Unicode Transformation Format, 8-bit | Text encoding. | +| `VDP` | verifiable data structure proof | Proof format for a VDS. | +| `VDS` | verifiable data structure | Append-only or otherwise provable data structure. | +| `VM` | virtual machine | Software-defined machine environment. | +| `VSTD` | Verifier Standard | Repository standard and reference implementation. | +| `W3C` | World Wide Web Consortium | Web standards organization. | +| `WG` | working group | Standards-development group. | +| `WSL2` | Windows Subsystem for Linux 2 | Windows-hosted Linux environment. | +| `YAML` | YAML Ain't Markup Language | Structured data format. | +| `ZI` | zero-identity | Historical study coordinate; not a trust or conformance class. | +| `ZIP` | ZIP archive format | Compressed archive format; treat ZIP as the format's proper name. | +| `ZIZK` | zero-identity/zero-knowledge | Governing VSTD artifact-first architecture; particular privacy and propagation mechanisms have their own maturity. | +| `ZK` | zero-knowledge | Cryptographic or semantic privacy property, only when explicitly supported. | +| `zkVM` | zero-knowledge virtual machine | Virtual machine that emits a zero-knowledge proof. | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..09db034 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,325 @@ +# Verifier Standard (VSTD) conformance architecture + +> **Acronyms:** application programming interface (API); Boolean satisfiability problem (SAT); command-line interface (CLI); +> JavaScript Object Notation (JSON); reduced instruction set computer (RISC); +> Supply Chain Integrity, Transparency, and Trust (SCITT); Verifier Standard (VSTD); +> zero-identity/zero-knowledge (ZIZK). + +**Status:** implementation and ownership map; normative meaning remains in `standard/` + +TRUST is mechanism-earned forward artifact support; ROT is typed, time-indexed +degradation of current admissibility; and RUST is inverse-TRUST diagnostic traversal +toward recorded ancestors. They are formal semantic names, not acronyms or actor ratings. + +Use this order when two surfaces appear to disagree: + +1. normative numbered-profile document; +2. serialized receipt identifier (`schema_version`) and profile discriminator; +3. published JSON Schema; +4. typed model and validator; +5. conformance tests; +6. generated reference and examples. + +A lower item cannot silently redefine a higher item. A passing schema check establishes +shape only; a passing validator establishes only its named implemented checks. + +## Numbered-profile ownership + +| 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-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 unbound candidate depth in `verifier.core.depth` | `vstd4_certificate.json`, `vstd4_receipt.json` | `test_gdc_certificate.py`, `test_vstd4_depth.py` | +| VSTD-5 | `standard/VSTD-5.md` | fail-closed candidate rejection only | `vstd5_receipt.json` | `test_vstd4_depth.py`, `test_vstd_schemas.py` | +| VSTD-Graph-1 | `standard/VSTD-Graph-1.md` | `verifier.data.models`, `verifier.data.receipt` | `vstd_graph_receipt.json` | `test_public_data.py` | +| VSTD-Graph-2..5 | matching Graph documents | `verifier.data.graph_level` | `computed_graph_level` within `vstd_graph_receipt.json` | `test_graph_level.py` | +| ZIZK artifact-first TRUST/ROT/RUST | `standard/LADDER.md` section 1.1 | Governs every mechanism; bounded RISC Zero example under `examples/zizk_artifact_first/` | No separate serialized receipt identifier or profile | presentation, experiment-manifest, and ZIZK mechanism tests | +| Artifact freeze, seal, and thaw | `standard/ARTIFACT_CONTROL.md` | `verifier.artifact_control` and `vstd artifact` | `standard/schemas/artifact-control-1.schema.json`; these are mechanism objects, not receipts | `test_artifact_control.py`, public API/CLI tests | + +VSTD-5 is draft. The VSTD-4 candidate-depth runtime and Graph-2 through Graph-5 compute candidates +from caller-supplied references or ratings and return +`conformance_status = NOT_ESTABLISHED`; evidence binding is not implemented. Neither +candidate is numbered-profile conformance. + +Artifact control is an orthogonal mechanism beneath the axes. It can preserve and close +an artifact used in any numbered profile, but its successful verification establishes only exact-byte +integrity, structural closure, and any separately supplied external anchor. It cannot +supply a numbered-profile result, semantic correctness, encryption, trusted time, actor trust, or +continuous temporal mediation. + +## Verification complex and profile satisfaction + +VSTD is organized by named **closure coordinates**, not interchangeable layers or scalar +assurance levels. A **numbered profile** is a cumulative requirement formula over those +coordinates. The two axes use different coordinate sets even when their profile numbers +match: + +| Profile number | Object closure coordinate | Graph closure coordinate | +|---:|---|---| +| 1 | Claim Mechanics | Recorded Lineage | +| 2 | Verification Surface | Bounded Collection Surface | +| 3 | Substrate Accountability | Accountable Provenance Closure | +| 4 | Refutability | Refutable Transformation Closure | +| 5 | Witness Corroboration | Corroborated Verification Network | + +Evidence enters through a named mechanism, establishes or fails to establish exact +coordinate facts, and is then evaluated against the selected profile formula. A complete +assessment preserves `ESTABLISHED`, `REFUTED`, `UNKNOWN`, `CONFLICTED`, and +`NOT_ESTABLISHED` rather than collapsing absent evidence into false or a satisfiable +encoding into conformance. A satisfying assignment over unvalidated caller assertions is +only a candidate. Satisfaction suffices only when every satisfying fact is itself bound to +evidence by the mechanism that earned it. + +“Closure†is always qualified. VSTD-2 surface closure, Graph provenance closure, +refutability closure, and artifact-seal structural closure are distinct propositions. +“Profile†is likewise qualified as a numbered, receipt, application, or geometry profile +when context does not make the category unique. The exact compatibility names and +exceptions are normative in [`standard/LADDER.md`](../standard/LADDER.md#terminology-contract). + +## Operational traversal and recursive Graph materialization + +This non-serialized implementation view does not redefine the numbered profiles: + +```text +native computation + -> VSTD-1 execution and claim capture + -> profiler or domain adapter + -> VSTD-2 verification-surface normalization + -> VSTD-3 substrate accountability + -> VSTD-4 portable refutation + -> VSTD-5 independently evidenced witness corroboration + -> VSTD-Graph collection assessment + -> content-addressed result artifact + -> later bounded verification loop +``` + +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. +The current `VSTD-2` receipt has no geometry-profile or profile-composition field, so this +relationship is conceptual rather than a new serialized contract. See +[`VSTD-2` section 8.1](../standard/VSTD-2.md#81-profiles-and-profiler-adapters). + +The complete apparatus that constructs or assesses a Graph is a verifying process. In a +later order it may become the subject of a new VSTD-2 surface, while an adjacent adapter +maps its selected observable outputs into that geometry. Treating the whole apparatus as +the adapter would erase the builder, mapping, verifier, and output seams that the next +assessment must examine. + +A materialized Graph result must retain or bind the source Graph receipt, target collection +or induced subgraph, selection query, object and edge ratings with their evidence, selected +surface, lifecycle and conflict state, candidate Graph profile certificate, materialization mechanism, +and declared information loss. It earns no strength from size, path count, repetition, +storage, or agreement. Its result is capped by every applicable member, transformation, +mapping, substrate, refutation, witness, and materialization obligation. Because current +Graph-2 through Graph-5 ratings are caller-supplied, present materialization can establish +only the candidate computation over those ratings, not independently verified Graph +conformance. + +## Governing ZIZK architecture and mechanism ownership + +ZIZK artifact-first TRUST is a governing VSTD architecture, not a side experiment, +numbered profile, scalar trust system, or actor +reputation system. VSTD evaluates bounded validity propositions about computational +processes represented by software and evidence-bearing artifacts; it does not determine +whether an actor is good or bad. Its normative source is `standard/LADDER.md` section 1.1. + +Zero identity means zero identity-derived verdict weight, not anonymity or absence of +identifiers. Checked identity evidence may establish only its exact attribution, +authorization, or separation proposition, adjacent to the process claim. Zero knowledge +means zero unevidenced knowledge is presumed: absent a mechanism-earned result, the exact +proposition remains `UNKNOWN`. When a witness must remain confidential, cryptographic zero +knowledge may enclose this architectural rule by binding the exact program, predicate, +public commitments, output, proof parameters, and verifier. The proof remains bearer- and +artifact-bound; prover identity or reputation supplies no TRUST. This property applies +only where a named proof system establishes it under explicit assumptions. + +TRUST, ROT, and RUST are formal semantic names, not acronyms, serialized receipt values, actor ratings, +scalar scores, or references to the Rust programming language: + +- TRUST is mechanism-earned artifact support moving forward only across admissible bound + transformations while every child discharges its new obligations; +- ROT is typed, time-indexed degradation of current admissibility without rewriting + historical evidence; and +- RUST is the inverse-TRUST diagnostic mechanic moving backward from an observed descendant + deviation through recorded contributing ancestry. + +The three never cancel, reverse direction, or manufacture a clean signal from `UNKNOWN` or +`CONFLICTED` inputs. Identity, popularity, repetition, age alone, topology, and propagation +supply no assurance. + +Maturity attaches to mechanisms beneath that architecture: + +| Mechanism surface | Current status | Ownership boundary | +|---|---|---| +| RISC Zero hidden-witness predicate | Bounded reference mechanism with tracked public proof artifacts | `examples/zizk_artifact_first/risc0/`; native verification only, no VSTD receipt mapping | +| Bounded identity-disclosure evaluator | Bounded non-normative reference mechanism | `examples/zizk_artifact_first/zero_identity/`; no identity-derived trust | +| Event serialization and TRUST-transfer algebra | Experimental and unimplemented | May implement the governing direction but cannot redefine it | +| ROT derivation and cross-surface propagation | Experimental and incomplete | Current Graph admission handles some lifecycle states; no general ROT result or propagation protocol exists | +| RUST concentration and localization | Experimental and unimplemented | Diagnostic reachability only until a separately specified mechanism earns more | +| Complete `PASS`/`FAIL`/`UNKNOWN`/`CONFLICTED` hidden-witness derivation | Experimental and unimplemented | A caller-supplied state tag is not an earned verdict | +| Specific optional proof backends | Backend-specific maturity; the RISC Zero example has one recorded native proof | Optional proof machinery cannot make the governing architecture optional or establish broader VSTD conformance | + +## Serialized receipt dispatch + +Dispatch first by `schema_version`, then by the required profile discriminator. VSTD-1 +claim receipts require `receipt_kind = "claim_mechanics"`; generic-run receipts require +`receipt_kind = "generic_computational_run"`. Unknown identifiers, absent discriminators, +and mismatched shapes fail closed. The current reader does not infer a profile from +retired pre-current-profile field arrangements. + +## Installed specification ownership + +Every `standard/*.md` file has a byte-identical installed resource under +`src/verifier/specifications/`. Verifier descriptors use those resources when no source +checkout is present. The installed-wheel gate runs outside the checkout and rejects an +unavailable specification digest. + +The artifact-control schema has a byte-identical installed copy under +`src/verifier/artifact_control/`. GitHub Pages publishes it at its declared `/schemas/` +route alongside receipt schemas without reclassifying it as a receipt. + +## Generic-run validation contract + +`vstd validate` is an integrity/profile validator for the +`generic_computational_run` profile. It enforces the strict receipt shape and recomputes +the stable-payload digest. The dynamic path keys inside +`source_state.source_file_hashes`, unconstrained recorded evaluator values, and +additional declarations inside `assessment_context.refutation_surface` are explicit data or +extension surfaces. The refutation surface remains open for caller-defined domain +refutations, but every additional value remains a declaration until an applicable mechanism +checks it. Unknown object properties outside those named surfaces fail closed. + +Validation does not rehash referenced artifacts, rerun the command, resolve evidence +references, or establish that recorded declarations are true. Those are separate +mechanisms. `validate`, `inspect`, and `reproduce` honor `--json` for generic-run and +VSTD-Graph receipts; the envelope reports command completion without upgrading the +receipt's claim semantics. + +### Generic-run assessment context + +`assessment_context` is a required VSTD-1 generic-run container, not a VSTD-4 object. It +participates in the canonical digest and retains the manifest-declared mechanism, +resource-bound, commitment, and refutation coordinates without using a numbered-profile +identifier as a generic container name. + +| Member | Five-As role | Maximum current meaning | +|---|---|---| +| verifier identity | Assessment | Names the generic mechanism; identity alone earns no result. | +| specification identity | Attribution and Assessment | Binds the mechanism to VSTD-1 bytes, not VSTD-4. | +| implementation identity | Assignment | Identifies implementation bytes; it does not prove an independent implementation. | +| parser identity | Assignment | Identifies parser bytes; equality with the implementation hash records shared bytes, not separation. | +| format identity | Attribution | Names the generic capture/validate/reproduce fragment. | +| resource bounds | Assurance input and Assessment bound | Records manifest declarations; the generic runtime does not establish their enforcement. | +| 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`. | + +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 +`assessment_context` supplies a VSTD-4 result. + +## Five-As human traversal + +**Status:** non-serialized architecture guide. The five As are roles in a human traversal +of existing VSTD records, not five new object types, numbered profiles, statuses, or a scalar assurance +score. No receipt or schema format is defined here. + +`ASSURANCE_0 -> ATTRIBUTION -> ASSIGNMENT -> ASSESSMENT -> ASSURANCE_1` reads as follows: + +| Stage | Operational meaning | Existing VSTD machinery | Current gap | +|---|---|---|---| +| `ASSURANCE_0` | Identified evidence or a previously assessed claim, with its evidence basis, provenance, bounds, trust roots, limitations, current state, and unresolved conflicts or unknowns. | Generic-run receipts and external-evaluation evidence; `EvidenceClassification`; Graph artifacts, statuses, and `ConflictRecord`; VSTD-3 evidence sources, gaps, and claim evaluations; VSTD-4 certificates and kernel results. | There is no universal Assurance record or cross-profile scalar ordering. | +| `ATTRIBUTION` | The explicit relation from evidence to the exact subject/predicate it supports, including the mapping, extraction, or transformation, scope, bounds, provenance, and information loss. | Generic-run bound-output extraction and recorded external references; Graph transformation hyperedges; VSTD-4 `ClaimCoordinate`, `ClaimBinding`, and `Grounding`; loss-sensitive SCITT coordinates. | Mapping and loss declarations remain profile-specific; a reference alone is not a checked mapping. | +| `ASSIGNMENT` | The most precise evidenced execution coordinate available: computation, execution instance, software/runtime, machine/substrate, then optional actor/operator bindings. Missing coordinates remain partial or `UNKNOWN`. | Generic-run execution and source-state records; VSTD-3 `WorkloadIdentity`, `ExecutionIdentity`, topology, device, runtime, and evidence-source records; VSTD-1 `independence_basis` for the separate independence question. | Generic actor/execution evidence binding is not implemented. Assignment alone establishes no trust, authorization, independence, or responsibility. | +| `ASSESSMENT` | An identified verifier or mechanism evaluates one bounded proposition under the applicable input Assurance, Attribution, Assignment, specification/profile, trust roots, and bounds. It earns only the predicates it checks. | Generic validation, artifact rehash, and rerun mechanisms; VSTD-3 recomputed `ClaimEvaluation`; Graph validation and candidate-profile certificates; the VSTD-4 grounded certificate kernel; native VSTD plus native SCITT composition. | No one verifier covers every profile; mechanism results remain adjacent rather than silently merged. | +| `ASSURANCE_1` | The assessment output recorded as new evidence with complete lineage to its inputs, mechanism, proposition, and limits. It may be `PASS`, `FAIL`, `UNKNOWN`, `CONFLICTED`, or a profile-specific equivalent. | Receipts, claim evaluations, kernel results, certificates, artifact digests, Graph artifacts/hyperedges, and prior commitments can preserve and reference the output. | There is no universal recursive-loop envelope; any future wire representation requires a separate proposal. | + +First-hand and second-hand describe **provenance**, not strength. A first-hand +self-observation may be weak; a second-hand certificate may be strongly bound to a narrow +proposition. `EvidenceClassification` records how evidence entered a profile, but its name, +source, or placement never substitutes for the profile's verification mechanism. + +The smallest operational loop is: + +1. select one proposition and retain every applicable input state, limitation, conflict, + unknown, trust root, and freshness bound; +2. bind each input to that proposition through an inspectable attribution, preserving + transformations and declared information loss; +3. record Assignment only to the specificity evidenced, leaving absent coordinates `UNKNOWN`; +4. run the named assessment mechanism under its specification and bounds; and +5. record the output as a new evidence artifact and transformation, without changing any + input record. A later loop may consume that output only as lineage-preserving input to + another explicitly identified assessment. + +> No semantic strength is gained by storage location, field name, repetition, graph +> multiplicity, actor reputation, or propagation. Every increase in assurance must +> identify the verification mechanism that earned it. + +This is the human forward traversal of the same topology VSTD-Graph stores for machines. +TRUST is bounded, mechanism-earned support across an admissible recorded transformation; +the child still discharges its new obligations. ROT is typed, time-indexed degradation of +current admissibility while historical evidence remains immutable. RUST is inverse-TRUST +diagnostic reachability from a downstream deviation toward recorded ancestors. Together +they describe memetic causal-provenance and lifecycle behavior over one development graph. +They do not establish actor standing, guilt, responsibility, causal localization, or +automatic ancestor falsification, and no current runtime emits a general TRUST, ROT, or +RUST transfer result. + +### Recursive-amplification falsification outcomes + +| Probe | Required outcome | +|---|---| +| Duplicate evidence or a duplicate identifier | No extra support; public receipts reject duplicates and in-memory graph construction rejects replacement. | +| Duplicate graph paths | Reachability is set-valued; path count never raises assurance or the candidate Graph profile. | +| Repeated identical reruns | At most the same bounded equivalence result; repetition does not prove independence or a stronger reproduction state. | +| Assessment consumes its own output | Invalid within that assessment; an output can enter only a later, distinct assessment with preserved lineage. | +| `A -> B -> A` or a self-loop | Invalid Graph topology; acyclicity checking rejects the loop. | +| Second-hand evidence relabeled first-hand | Provenance conflict or unsupported declaration; no strength change. | +| Attribution without a checked mapping | Declaration or `UNKNOWN`, never mapped support. | +| Machine Assignment treated as responsibility | Prohibited inference; Assignment records execution coordinates only. | +| Actor identity treated as trust | Prohibited inference; identity and reputation do not strengthen an artifact result. | +| Conflicted upstream evidence collapsed | Conflict remains explicit and blocks a clean candidate Graph profile. | +| Stale, revoked, challenged, or unknown evidence reused as clean current support | Inadmissible to a clean current Graph candidate; this is ROT in current admissibility, not revision of the historical record. | +| Recursive propagation with no new mechanism | No transition from `ASSURANCE_0` to stronger `ASSURANCE_1`; lineage growth is not assurance growth. | + +## Separation and Graph boundaries + +The historical `independent_audit` field name does not prove independence. Its +`independence_basis` records actor, implementation, and runtime separation. Repeated or +matching results are artifact agreement, not evidence that separate actors performed the +runs; absent separation evidence is `NOT_DEMONSTRATED`. Serialized status words and +evidence-reference strings cannot self-promote that result. Because version 1.2.0 has no +actor/execution evidence-binding adapter, the bundled runtime treats supplied assertions +as no stronger than `DECLARED`, rejects receipts that label them `EVIDENCED`, and never +derives `EVIDENCED`. + +Graph conflict records retain incompatible values and their evidence references without +adding a scalar score or changing the frozen artifact-status vocabulary. A conflict makes +the subject inadmissible to a clean candidate Graph profile. + +### Recursive current-state audit + +Historical receipt bytes and their recorded `PASS` remain unchanged. A later current-state +question is a new assessment over the retained graph and applicable lifecycle records: + +| Scenario | Implemented outcome | +|---|---| +| An ancestor is `CHALLENGED`, `REVOKED`, or `STALE` | Candidate Graph recomputation follows the full ancestor closure and returns compatibility field `level = 0`, meaning no numbered Graph profile is satisfied. This implements a bounded ROT consequence for that query; it does not emit a general ROT result or rewrite the historical receipt. | +| An ancestor is `SUPERSEDED` | The historical Graph candidate remains admissible by design; the stricter all-ancestors-`VALID` policy rejects it for current-use admission. Supersession does not retroactively falsify its prior lineage role. | +| Upstream evidence conflicts | A retained `ConflictRecord` blocks a clean candidate. Conflict resolution is not implemented; any future resolution must be additive and retain the competing evidence. | +| Evidence arrives by multiple paths or one run receipt repeats a reference | Reachability and impact sets deduplicate identifiers. Multiplicity supplies no independence or strength. | +| A descendant deviation points toward shared ancestors | Existing ancestor queries establish recorded reverse reachability only. No runtime emits RUST, measures independent concentration, or attributes causal responsibility. | +| A challenge ledger changes a claim's current status | The append-only ledger derives `CHALLENGED` or `REVOKED`, but no adapter binds that claim status into a Graph artifact. Cross-surface propagation is `NOT_ESTABLISHED`, not silently clean. | +| Later evidence is intended to clear a conflict | No conflict-resolution transition exists in version 1.2.0. Removing the old record would violate additive correction; a future mechanism must preserve it and identify what resolved it. | +| Candidate calculation encounters cyclic ancestry | Rejected before candidate calculation; recursive topology cannot manufacture assurance. | + +The implemented forward blast-radius query discovers recorded downstream artifacts and +generic-run receipts that require reconsideration when given an invalidated artifact. It is +a discovery mechanism, not automatic status mutation, current-admissibility adjudication, +or proof of causal influence. Automatic propagation for challenge, staleness, supersession, +conflict resolution, general TRUST transfer, ROT propagation, and RUST remains +`NOT_ESTABLISHED` until a distinct mechanism binds the lifecycle event to the exact Graph +artifact and proposition. diff --git a/docs/CLAIMS_AND_LIMITS.md b/docs/CLAIMS_AND_LIMITS.md index 5533a9e..15c1905 100644 --- a/docs/CLAIMS_AND_LIMITS.md +++ b/docs/CLAIMS_AND_LIMITS.md @@ -1,5 +1,17 @@ # Claims and limits in plain language +> **Acronyms:** artificial intelligence (AI); Advanced Micro Devices (AMD); application programming interface (API); +> Amazon Web Services (AWS); Concise Binary Object Representation (CBOR); CBOR Object Signing and Encryption (COSE); +> conjunctive normal form (CNF); Device Identifier Composition Engine (DICE); +> grounded decision certificate (GDC); identifier (ID); machine learning (ML); NVIDIA Management Library (NVML); +> Secure Hash Algorithm 256-bit (SHA-256); Secure Hash Algorithm 3 256-bit (SHA3-256); +> system management interface (SMI); Security Protocol and Data Model (SPDM); +> Software Package Data Exchange (SPDX); Supply Chain Integrity, Transparency, and Trust (SCITT); +> trusted computing base (TCB); Coordinated Universal Time (UTC); +> Verifier Standard (VSTD). + +> Reader aid: [concept glossary and primary precedents](CONCEPTS_AND_PRECEDENTS.md). + **Status:** normative interpretation guide for the VSTD object and Graph ladders This guide translates VSTD claim language into ordinary language. When a short claim @@ -9,20 +21,41 @@ conflicts with the bounded wording here, the bounded wording controls. A VSTD result always has this form: -> For this identified subject snapshot, this declared verification surface passed this -> identified mechanism using this bound evidence, subject to these limitations, trust -> roots, and horizons. +> For this **identified subject snapshot**, this **identified mechanism** returned this +> **bounded result** over this **declared verification surface**, using this +> **bound evidence**, subject to these **limitations**, **trust roots**, and **horizons**. Omitting any bolded idea changes the claim. `VERIFIED` never means universally true, safe, complete, permanent, legally authorized, or endorsed. +## Skeptical review summary + +VSTD has no single scalar “strongest claimâ€; mechanisms establish different predicates. +The strongest generally reusable implemented statement is therefore an exact, bounded +checker result—not a claim of universal truth or whole-project conformance. + +| Reviewer question | Current answer | Mechanism and trust roots | Boundary or missing mechanism | +|---|---|---|---| +| What can the generic validator establish? | Stable receipt content and strict profile shape. | Canonicalization, recorded digest, profile discriminator, and bundled validator bytes. | It does not verify the recorded native claim, external evidence, actor identity, or independence. | +| What can the grounded-certificate kernel establish? | The exact `VSTD4-GDC-1` decision was accepted, rejected, or left `UNKNOWN` under its claim binding and resource bound. | Certificate bytes, formula, grounding, policy/evidence roots, verifier descriptor, and kernel. | VSTD-4 conformance is `NOT_ESTABLISHED`; rung evidence and prerequisite-profile coordinates are not bound by the candidate-depth runtime. | +| What can VSTD-Graph establish? | Stored objects, transformations, conflicts, recorded reachability, and policy results over that graph. | Content digests, Graph receipt, hyperedges, statuses, and named query or policy. | Graph profiles 2–5 are candidates from caller-supplied ratings; completeness, causality, and rating-to-evidence binding are absent. | +| 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; a thawed descendant currently matches or differs from its sealed parent. | Preserved bytes, SHA-256 and SHA3-256 commitments, read-only payload-tree guard, Ed25519 signature, artifact-derived identifiers, and any supplied external artifact/key anchor. | Read-only is not privileged access control; a seal is not encryption, correctness, trusted time, ownership, durable external archiving, or a numbered-profile result. | +| 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 actor independence, automatic challenge-to-Graph propagation, VSTD-5, and unqualified provenance truth. | No current ordinary VSTD mechanism observes or validates those propositions. | Preserve `UNKNOWN`, `UNSUPPORTED`, `CONFLICTED`, or `NOT_ESTABLISHED`; do not infer a clean result. | + +Every claim below expands one of these boundaries into publishable wording and its +required falsification surface. + ## Claim translation table | Claim | May it be made? | Why | Required evidence | What it does not mean | |---|---|---|---|---| | “This receipt's stable content has not changed.†| **Yes, after validation passes.** | The validator recomputes the canonical digest over the specified stable fields and compares it with the recorded digest. | Receipt bytes, canonicalization version, recorded digest, passing validator result. | The statements inside the receipt are true or authentic. | | “These observed bytes match this SHA-256 digest.†| **Yes, conditionally.** | A named mechanism can hash accessible bytes at an observation time and compare them with the recorded digest. | The bytes, hashing mechanism, observation time, expected digest, comparison result. | The bytes came from the claimed source, existed before observation, are uncontaminated, or are legally usable. | -| “VSTD-Graph records this lineage graph.†| **Yes.** | The receipt binds the stored artifact nodes, transformation edges, roles, statuses, and declarations. Historical Graph-1 receipts retain the `VSTD-DATA-0.1` wire identifier. | Valid receipt and structurally valid hypergraph. | The graph contains every real-world input or transformation. | +| “This artifact is frozen.†| **Yes, after freeze verification passes.** | The current regular-file bytes, portable paths, manifest identifiers, and read-only payload-tree guard recompute. | Complete bundle, passing `vstd artifact verify --freeze-only`, and the exact mechanism version. | Privileged mutation is impossible, an external archive retained the artifact, the artifact is correct, or a signature exists. | +| “This artifact is sealed.†| **Yes, after seal verification passes.** | The carried Ed25519 key verifies the finite signature closure and the seal identifier closes the signature-bearing envelope. | Passing seal verification plus an expected artifact/key coordinate when whole-bundle substitution is in scope. | Encryption, secrecy, ownership, authorization, trusted time, semantic correctness, continuous custody, actor trust, or a numbered VSTD profile result. | +| “VSTD-Graph records this lineage graph.†| **Yes.** | The receipt binds the stored artifact nodes, transformation edges, roles, statuses, and declarations. Historical Graph-1 receipts retain the serialized receipt identifier `VSTD-DATA-0.1`. | Valid receipt and structurally valid hypergraph. | The graph contains every real-world input or transformation. | | “This is the complete provenance of the model or dataset.†| **No, unless completeness is independently evidenced for the declared boundary.** | A graph cannot infer hidden inputs, pre-observation history, out-of-band processing, or missing instrumentation. | Independent coverage evidence for every declared boundary plus explicit horizons outside it. | That a high coverage summary proves complete real-world lineage. | | “This transformation actually ran and produced this output.†| **Only with execution evidence.** | Software, parameters, and environment fields are declarations until a run trace, rerun, attestation, or equivalent evidence binds execution to the output. | Identified inputs and outputs, execution trace or rerun, software identity, parameters, environment, and evidence classification. | Recording a script name or commit proves execution. | | “The recorded Boolean provenance policy passed.†| **Yes, when the policy result validates.** | The reference solver evaluates the recorded CNF formula. | Formula, variable map, graph snapshot, solver identity, passing result. | The prose-to-formula translation was complete, the external facts were true, or broader policy compliance was established. | @@ -30,14 +63,39 @@ safe, complete, permanent, legally authorized, or endorsed. | “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. | -| “This was independently verified.†| **Only when the relevant independence seam is demonstrated.** | Independence requires separation from the producer's relevant state and logic plus a declared trusted computing base. | Producer/auditor boundary, TCB, source identities, isolation evidence, independent result. | Running the bundled verifier on its own output is automatically independent. | +| “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 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.** | Blast radius is forward reachability over the stored graph. | Challenged artifact ID and bound hypergraph. | Historical receipts were automatically mutated or that unrecorded downstream systems were found. | +| “The compatibility API returned Graph `level = N`.†| **Not yet as a conformance claim.** | The current implementation computes candidate Graph profile `N` from caller-supplied artifact and edge ratings; `level` is the retained field name for that profile number. It labels the result `CALLER_SUPPLIED` and `NOT_ESTABLISHED`. | A structurally valid graph and explicit supplied ratings. Conformance additionally requires implemented rating-to-evidence bindings for every required profile coordinate. | The supplied ratings were independently derived, every coordinate's evidence passed, or Graph conformance was established. | + +## Competition and scored-evaluation claims + +For predictive-AI, scientific-ML, agent, and other scored evaluations, bind the exact +rules, data, model, submission, evaluator, metrics, score, transformations, environment, +and evidence classes. Mark hidden tests as a horizon—not evidence of integrity. This adds +no verdict, affiliation, certification, endorsement, ranking, prize eligibility, or +organizer acceptance. + +For later-resolved predictions, also bind emission and resolution times, the frozen +prediction digest, update or abstention policy, resolution source and digest, scoring +rule, and channel independence. Corrections are additive; never overwrite a frozen +prediction. See the complete non-normative +[`competition profile`](profiles/competition-evaluation.md). + +Use the coordinate-bounded wording: + +> The submission and score receipt binds the declared artifact, evaluator, and +> provenance surface. Hidden-test integrity and organizer acceptance remain outside the +> participant-observable surface. + +Do not shorten this to “the model,†“the competition result,†or “the prediction is +verified.†## VSTD-4 grounded-decision claim translations -`VSTD4-GDC-1` makes a decision certificate independently checkable against an +`VSTD4-GDC-1` makes a decision certificate checkable outside its producer against an explicit claim coordinate, formula, grounding map, verifier identity, resource bounds, and prior commitment. It does not make the certificate independent of the evidence source or make the grounded claim true outside that coordinate. @@ -46,13 +104,13 @@ the evidence source or make the grounded claim true outside that coordinate. |---|---|---|---| | “This VSTD4-GDC-1 certificate was accepted.†| The identified reference kernel accepted the exact canonical certificate under the declared claim binding, fragment, verifier, and resource bounds. | Name the certificate digest, implementation commit, claim coordinate, cost tier, bounds, and kernel result. | The underlying evidence is authentic, the policy captured every intended condition, or the claim is globally true. | | “This decision is grounded.†| Every variable and clause in the accepted certificate maps to declared subjects, predicates, values, and encoding rules whose roots are bound by the certificate. | Preserve the evidence root, policy root, grounding map, and exclusions. | Unrecorded evidence does not exist, the grounding source is independent, or the physical world is completely represented. | -| “`vstd4_depth = k`.†| Rungs `1..k` have accepted evidence in dependency order and, when `k < 14`, an accepted ceiling certificate refutes or blocks rung `k+1`. | Name the rung profile, witness certificates, ceiling certificate, budgets, and horizons. | Rungs above `k` are universally impossible or no proof can ever be found. | +| “The reference implementation computed VSTD-4 candidate depth `k`.†| Caller-supplied nonempty references were structurally consistent through rungs `1..k` and, when `k < 14`, the candidate ceiling certificate blocks rung `k+1`. | State `CANDIDATE`, `conformance_status = NOT_ESTABLISHED`, the supplied references, certificates, budgets, and horizons. | The references establish their rung propositions, VSTD-1/2/3 passed, normative VSTD-4 conformance was established, or VSTD-5 entry is permitted. | | “The result is refutable.†| The published result exposes a machine-checkable falsification surface and admissible counterevidence within the declared boundary. | Name that surface, the admissible counterevidence, exclusions, and decision rule. | A separate party actually attempted refutation or independently witnessed the evidence. | | “The verifier returned `UNKNOWN`.†| The declared check could not establish `PASS` or `FAIL` within the implemented fragment, available evidence, or resource bound. | Preserve the indeterminacy reason and transcript. | The proposition is false, no proof exists, or a larger bound could not decide it. | -| “The artifact is ready for VSTD-5 evaluation.†| The VSTD-4 result reached depth 14 with an accepted `PASS` witness and no ceiling refutation. | This is only the mechanical entry gate implemented by `require_vstd5_entry`. | VSTD-5 conformance, independent witnessing, or external certification has occurred. | +| “The artifact is ready for VSTD-5 evaluation.†| **Not established by the current reference implementation.** | VSTD-5 requires evidence-bound VSTD-1/2/3 preconditions and normative VSTD-4 conformance at depth 14. `require_vstd5_entry` rejects the current unbound candidate. | Candidate depth 14, a `PASS` over the candidate formula, or nonempty references satisfy the gate. | -The public reference implementation and its tests are one implementation. This -release does not claim an external implementation, interoperability result, +The public reference implementation and its tests are one implementation. This source +coordinate does not claim an external implementation, interoperability result, security audit, independent witness, or third-party certification. ## VSTD-3 accelerator claim translations @@ -104,6 +162,11 @@ Always cite the exact VSTD version, implementation commit, receipt type, mechani and demonstrated test or receipt. Do not turn specification text into an implementation claim. +The bundled checker records a checker verdict. Its historical `independent_audit` field +name is not evidence of independence. Claim independent verification only when the +receipt's `independence_basis` demonstrates distinct actors plus the relevant +implementation and runtime separation. Matching run results cannot supply that evidence. + ## Safe claim template > Using VSTD-Graph-1 at commit ``, receipt `` validated the stored @@ -130,12 +193,13 @@ VSTD-4 safe template: > establish evidence authenticity, complete policy coverage, independent > witnessing, or truth outside the coordinate. -VSTD-5 draft boundary template: +VSTD-5 draft boundary template for the current implementation: -> The artifact passed the implemented VSTD-5 entry gate because its VSTD-4 -> result reached depth 14 with an accepted `PASS` witness. VSTD-5 remains a -> draft specification and this release implements no VSTD-5 witness procedure. -> Therefore no VSTD-5 conformance or independent-witness claim is made. +> The artifact reached VSTD-4 structural candidate depth 14 with an accepted +> certificate over caller-supplied references. Conformance remains +> `NOT_ESTABLISHED`; the VSTD-5 entry gate rejected the candidate. VSTD-5 remains +> a draft specification, and no VSTD-5 readiness, conformance, or +> independent-witness claim is made. ## Prohibited shortcuts @@ -158,3 +222,5 @@ Do not publish any of these without the missing qualification: - “All compute was accounted for†without complete-mediation evidence for every path in the named governed boundary. - “No undeclared compute occurred†from a device, host, provider, or fleet receipt. +- “Sealed means encrypted, immutable, correct, externally archived, or continuously + guarded.†diff --git a/docs/CONCEPTS_AND_PRECEDENTS.md b/docs/CONCEPTS_AND_PRECEDENTS.md new file mode 100644 index 0000000..c0c3704 --- /dev/null +++ b/docs/CONCEPTS_AND_PRECEDENTS.md @@ -0,0 +1,110 @@ +# Concept guide and intellectual precedents + +> **Acronyms:** conjunctive normal form (CNF); Certificate Transparency (CT); +> deletion resolution asymmetric tautology (DRAT); Internet Engineering Task Force (IETF); +> JavaScript Object Notation (JSON); National Institute of Standards and Technology (NIST); proof-carrying code (PCC); +> Principles of Programming Languages (POPL); World Wide Web Consortium provenance vocabulary (PROV); +> PROV data model (PROV-DM); Protect the Software (PS); Request for Comments (RFC); reverse unit propagation (RUP); +> Boolean satisfiability problem (SAT); Supply-chain Levels for Software Artifacts (SLSA); +> satisfiability modulo theories (SMT); SMT library standard (SMT-LIB); The Update Framework (TUF); +> Verifier Standard (VSTD); World Wide Web Consortium (W3C). + +**Status:** non-normative reader aid + +VSTD did not arise in a vacuum, but it also does not inherit another system's guarantees +merely by citing it. This guide separates two jobs: + +1. **Orientation links** answer only “what neighboring concept should I recognize?†so an + unfamiliar reader can stay in the flow of the guide. On the GitHub Pages site, hovering + over or focusing one displays a wiki-style card whose short definition is versioned in + this repository. The link itself opens optional Wikipedia background; repository + Markdown degrades to that ordinary link and its boundary title. The popup performs no + network request, and neither the popup nor the external page is VSTD evidence. +2. **Primary references** support the stated historical or technical precedent by pointing + to a standard, specification, or original paper. They do not prove that VSTD is correct, + adopted, interoperable, accredited, or conformant to the referenced system. + +Implemented commands and supported Python interfaces are documented separately in the +generated [command-line interface (CLI) and application programming interface (API) +reference](https://timelordraps.github.io/verifier/reference.html). A concept is linked to +that reference only when the implementation exposes an exact public coordinate; the guide +does not invent an API mapping for a conceptual resemblance. + +When a repository orientation definition and a primary source differ, use the primary +source for the external concept. When a primary source and a VSTD requirement differ, the +VSTD document controls VSTD conformance and the difference must remain explicit. + +## Orientation glossary + +| Concept | Optional background | Repository-owned definition and VSTD boundary | +|---|---|---| +| Assurance | [Information assurance](https://en.wikipedia.org/wiki/Information_assurance "Wikipedia orientation; not a VSTD authority") | VSTD reports evidence-bounded results, not universal confidence or institutional accreditation. | +| TRUST | [Proof-carrying code](https://en.wikipedia.org/wiki/Proof-carrying_code "Wikipedia orientation; not a VSTD authority") | Formal VSTD name for mechanism-earned, artifact-bound support that may move forward only through checked transformations. It is not actor trust, a scalar, or an acronym. | +| ROT | [Software rot](https://en.wikipedia.org/wiki/Software_rot "Wikipedia orientation; not a VSTD authority") | Formal VSTD name for typed, time-indexed degradation of current admissibility. It preserves historical results and is not inferred from age alone. It is not an acronym. | +| RUST | [Fault localization](https://en.wikipedia.org/wiki/Fault_localization "Wikipedia orientation; not a VSTD authority") | Formal VSTD name for inverse-TRUST diagnostic traversal from a descendant deviation toward recorded ancestor candidates. Reachability is not causal localization. It is not an acronym or the Rust programming language. | +| Verification complex | [Constraint satisfaction problem](https://en.wikipedia.org/wiki/Constraint_satisfaction_problem "Wikipedia orientation; not a VSTD authority") | Named closure coordinates and evidence-bearing relations form the semantic space. Numbered profiles are cumulative requirement formulas over that space, not scalar assurance levels. | +| Closure coordinate | [Security assurance component](https://en.wikipedia.org/wiki/Common_Criteria "Wikipedia orientation; not a VSTD authority") | One named verification question and failure class. Evidence for one coordinate never supplies another. Closure is always proposition-qualified. | +| Numbered profile | [Conformance testing](https://en.wikipedia.org/wiki/Conformance_testing "Wikipedia orientation; not a VSTD authority") | `VSTD-N` and `VSTD-Graph-N` select cumulative coordinate requirements. Matching object and Graph numbers do not identify the same coordinate. | +| Layer and level | [Abstraction layer](https://en.wikipedia.org/wiki/Abstraction_layer "Wikipedia orientation; not a VSTD authority") | VSTD reserves layer for actual implementation, protocol, or physical stacks and level for named external taxonomies or compatibility identifiers. Neither word is a synonym for a numbered VSTD profile. | +| Defense in depth | [Defense in depth](https://en.wikipedia.org/wiki/Defense_in_depth_%28computing%29 "Wikipedia orientation; primary references are mapped below") | Multiple independent controls limit the effect of one control failing. VSTD applies the pattern to distinct closure coordinates without claiming that their profile numbers are assurance levels. | +| Fail-closed decisions | [Fail-safe](https://en.wikipedia.org/wiki/Fail-safe "Wikipedia orientation; not a VSTD authority") | Missing or exhausted evidence stays `UNKNOWN`, `INDETERMINATE`, or `UNSUPPORTED`; it does not become a pass. | +| Trusted computing base | [Trusted computing base](https://en.wikipedia.org/wiki/Trusted_computing_base "Wikipedia orientation; not a VSTD authority") | Every result must expose the mechanism and trust roots on which it depends. | +| Zero trust | [Zero trust architecture](https://en.wikipedia.org/wiki/Zero_trust_architecture "Wikipedia orientation; not a VSTD authority") | VSTD borrows no product architecture wholesale; it uses explicit verification rather than identity or location as an automatic correctness signal. | +| Canonicalization | [Canonicalization](https://en.wikipedia.org/wiki/Canonicalization "Wikipedia orientation; not a VSTD authority") | Stable fields need one declared byte representation before hashing. VSTD's formats are not thereby RFC 8785 implementations. Public API: `compute_canonical_digest` ([reference](reference.html#api-compute_canonical_digest)). | +| Content addressing | [Content-addressable storage](https://en.wikipedia.org/wiki/Content-addressable_storage "Wikipedia orientation; not a VSTD authority") | Artifact and receipt coordinates bind declared bytes through digests; a digest alone does not establish origin or truth. | +| Cryptographic digest | [Cryptographic hash function](https://en.wikipedia.org/wiki/Cryptographic_hash_function "Wikipedia orientation; not a VSTD authority") | Hash observations can establish byte identity within an algorithm and observation boundary, not semantic correctness. | +| Provenance | [Data provenance](https://en.wikipedia.org/wiki/Data_provenance "Wikipedia orientation; not a VSTD authority") | VSTD-Graph records declared entities, transformations, and ancestry while preserving incomplete or unauthenticated history as such. | +| Hypergraph | [Hypergraph](https://en.wikipedia.org/wiki/Hypergraph "Wikipedia orientation; not a VSTD authority") | N-ary transformation edges preserve many-input and many-output structure without flattening it into ambiguous binary links. | +| Attestation | [Attestation](https://en.wikipedia.org/wiki/Attestation "Wikipedia orientation; not a VSTD authority") | VSTD-3 records who or what supplied evidence, the mechanism used, and the resulting evidence ceiling. | +| Trust root | [Trust anchor](https://en.wikipedia.org/wiki/Trust_anchor "Wikipedia orientation; not a VSTD authority") | A declared root is an explicit dependency and stopping boundary, not evidence that the root is honest. | +| Reproducibility | [Reproducibility](https://en.wikipedia.org/wiki/Reproducibility "Wikipedia orientation; not a VSTD authority") | VSTD binds the exact mechanism, inputs, environment, and equivalence relation required by the claim rather than treating the word as self-defining. Public API: `ReproducibilityLevel` ([reference](reference.html#api-ReproducibilityLevel)). | +| Reproducible build | [Reproducible builds](https://en.wikipedia.org/wiki/Reproducible_builds "Wikipedia orientation; not a VSTD authority") | Recreating identical artifacts is an important special case of portable checking, not a proof of every property of the artifact or of distinct actors. | +| Falsifiability | [Falsifiability](https://en.wikipedia.org/wiki/Falsifiability "Wikipedia orientation; not a VSTD authority") | VSTD-4 requires an explicit, bounded way for an outside checker to refute the exact claim. It does not turn Popper's philosophy into a software theorem. | +| Proof-carrying artifact | [Proof-carrying code](https://en.wikipedia.org/wiki/Proof-carrying_code "Wikipedia orientation; not a VSTD authority") | The engineering precedent is that an untrusted producer can ship a result with a smaller consumer-checkable certificate under a declared policy. Public API: `DecisionCertificate` ([reference](reference.html#api-DecisionCertificate)). | +| SAT | [Boolean satisfiability problem](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem "Wikipedia orientation; not a VSTD authority") | The reference subset encodes finite admission questions; SAT success establishes only the encoded formula. | +| CNF | [Conjunctive normal form](https://en.wikipedia.org/wiki/Conjunctive_normal_form "Wikipedia orientation; not a VSTD authority") | VSTD's bounded policy encodings use finite CNF and do not equate arbitrary CNF with 3-SAT. | +| Resolution | [Resolution](https://en.wikipedia.org/wiki/Resolution_%28logic%29 "Wikipedia orientation; not a VSTD authority") | Clausal refutations provide checkable evidence for an unsatisfiable result within the implemented proof format. | +| Unit propagation | [Unit propagation](https://en.wikipedia.org/wiki/Unit_propagation "Wikipedia orientation; not a VSTD authority") | The minimal trusted checker validates the supported reverse-unit-propagation certificate path rather than trusting the producer's solver. | +| Three-valued result | [Three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic "Wikipedia orientation; not a VSTD authority") | `UNKNOWN` is a first-class refusal to overstate, not a Boolean false and never a pass. VSTD's statuses are not claimed to implement one historical three-valued logic. | +| Append-only transparency | [Certificate Transparency](https://en.wikipedia.org/wiki/Certificate_Transparency "Wikipedia orientation; not a VSTD authority") | Immutable receipts and additive corrections share an auditability goal with append-only logs; VSTD is not a Certificate Transparency implementation. | +| Update freshness | [The Update Framework](https://en.wikipedia.org/wiki/The_Update_Framework "Wikipedia orientation; not a VSTD authority") | Staleness, rollback, revocation, and key compromise are separate from content integrity and require explicit current-state evidence. | +| Semantic versioning | [Semantic Versioning](https://en.wikipedia.org/wiki/Software_versioning#Semantic_versioning "Wikipedia orientation; not a VSTD authority") | Repository releases use semantic versions independently of the VSTD object and Graph profile numbers. | +| Object language and metalanguage | [Metalogic](https://en.wikipedia.org/wiki/Metalogic "Wikipedia orientation; not a VSTD authority") | VSTD uses this only as a design analogy for examining a verification surface; it does not claim that every adjacent profile is a formal metalanguage. | +| Undefinability of truth | [Tarski's undefinability theorem](https://en.wikipedia.org/wiki/Tarski%27s_undefinability_theorem "Wikipedia orientation; not a VSTD authority") | The verification complex expressly does not derive its architecture or observational limits from Tarski's theorem. | + +## Primary reference map + +| VSTD design seam | Primary or official reference | Relevant precedent and explicit limit | +|---|---|---| +| Separate failure controls and fail-safe defaults | Saltzer and Schroeder, [*The Protection of Information in Computer Systems*](https://web.mit.edu/Saltzer/www/publications/pubs.html) (1975) | Classic security-design principles include fail-safe defaults, complete mediation, separation of privilege, least privilege, and least common mechanism. They motivate separating failure surfaces; they do not derive VSTD's five coordinates on either axis. | +| Security-assurance components and packages | Common Criteria, [Part 3: Security assurance components](https://www.commoncriteriaportal.org/files/ccfiles/CC2022PART3R1.pdf) (CC:2022 Revision 1) | Established precedent for decomposing assurance into named components and packages. VSTD is not Common Criteria, accredited evaluation, or an Evaluation Assurance Level. | +| Canonical JSON as cryptographic input | IETF Independent Stream, [RFC 8785: JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html) | Shows why cryptographic operations over JSON require invariant representation. VSTD uses its own declared canonicalization rules and must not claim RFC 8785 conformance unless a format actually implements it. | +| Provenance entities, activities, and agents | W3C, [PROV-DM: The PROV Data Model](https://www.w3.org/TR/prov-dm/) | Standardized vocabulary and constraints for interoperable provenance. VSTD-Graph's artifact and transformation model is adjacent, not a PROV implementation or complete history claim. | +| Supply-chain step and artifact attestations | [in-toto specification v1.0](https://in-toto.io/docs/specs/) and [SLSA v1.2](https://slsa.dev/spec/v1.2/) | Established formats and levels for materials, products, builders, steps, and provenance. VSTD may bind their outputs as evidence but does not manufacture their authorization or assurance level. | +| Release preservation and provenance integrity | NIST, [Special Publication (SP) 800-218: Secure Software Development Framework 1.1](https://doi.org/10.6028/NIST.SP.800-218) | Protect the Software practices PS.3.1 and PS.3.2 cover archiving releases, maintaining provenance, protecting its integrity, and enabling recipient verification. This is operational precedent, not VSTD certification. | +| Independent recreation of artifacts | Reproducible Builds, [formal definition](https://reproducible-builds.org/docs/definition/) | Defines the source, environment, instruction, and artifact relationship needed for bit-for-bit recreation. VSTD permits other explicitly declared equivalence relations and does not infer truth from reproducibility alone. | +| Producer-supplied, consumer-checked certificates | Necula, [*Proof-Carrying Code*](https://doi.org/10.1145/263699.263712) (POPL 1997) | Primary precedent for an untrusted producer supplying a proof checked under a defined policy by the consumer. VSTD generalizes the receipt pattern but does not inherit PCC's safety theorem. | +| Checkable SAT refutations | Wetzler, Heule, and Hunt, [*DRAT-trim: Efficient Checking and Trimming Using Expressive Clausal Proofs*](https://www.cs.cmu.edu/~mheule/publications/drat-trim.pdf) (2014) | Demonstrates checking unsatisfiability proofs outside the solver rather than trusting its answer. VSTD's implemented certificate is a narrower declared RUP path, not arbitrary DRAT. | +| Explicit indeterminate solver results | [SMT-LIB Standard 2.7](https://smt-lib.org/papers/smt-lib-reference-v2.7-r2025-04-09.pdf) | The standard response grammar includes `sat`, `unsat`, and `unknown`. VSTD's richer status vocabulary is independently defined, but the refusal to fabricate a Boolean answer has established solver precedent. | +| Append-only evidence and independently detectable equivocation | IETF, [RFC 9162: Certificate Transparency Version 2.0](https://www.rfc-editor.org/rfc/rfc9162.html) | Merkle inclusion and consistency proofs support auditing an append-only log, while the RFC also names split-view limitations. VSTD's additive history is analogous but not a CT log. | +| Freshness, rollback, freeze, and key-compromise boundaries | [The Update Framework specification](https://theupdateframework.github.io/specification/latest/) | Separates current-version metadata, expiration, delegated roles, and compromise recovery from artifact bytes. VSTD does not implement TUF, but shares the requirement that old authentic data is not automatically current data. | + +## How to cite these precedents + +Use language such as: + +- "VSTD's portable-certificate design is adjacent to proof-carrying code." +- "VSTD-Graph overlaps W3C PROV, in-toto, and SLSA at the provenance boundary." +- "The refusal to convert resource exhaustion into a false result has precedent in the + `unknown` response of SMT-LIB." + +Do not write: + +- "Saltzer and Schroeder prove the VSTD verification complex." +- "VSTD implements PROV, SLSA, in-toto, TUF, Common Criteria, or Certificate + Transparency," unless separately demonstrated by a named conformance mechanism. +- "These citations establish VSTD's security, completeness, adoption, or novelty." + +The point of the map is traceable intellectual context: which established problem a VSTD +rule resembles, where the design deliberately differs, and what remains original project +architecture rather than inherited authority. diff --git a/docs/ECOSYSTEM.md b/docs/ECOSYSTEM.md index 62ef450..861d7e9 100644 --- a/docs/ECOSYSTEM.md +++ b/docs/ECOSYSTEM.md @@ -1,14 +1,29 @@ # Ecosystem boundary map +> **Acronyms:** Concise Binary Object Representation (CBOR); CBOR Object Signing and Encryption (COSE); +> Internet Engineering Task Force (IETF); World Wide Web Consortium provenance vocabulary (PROV); +> Request for Comments (RFC); Supply Chain Integrity, Transparency, and Trust (SCITT); +> Supply-chain Levels for Software Artifacts (SLSA); verifiable data structure (VDS); Verifier Standard (VSTD); +> World Wide Web Consortium (W3C). + +> Reader aid: [concept glossary and primary precedents](CONCEPTS_AND_PRECEDENTS.md). + **Status:** non-normative positioning note -**Reviewed:** 2026-08-22 +**Reviewed:** 2026-08-23 VSTD is designed to compose with established provenance, software-supply-chain, and artifact-authentication systems. It does not rename their guarantees as its own and does not claim to replace them. +VSTD supplies a common operator language for claim coordinates, evidence references, +bounds, native outcomes, assumptions, and degradation rules. Native verifiers retain +their own semantics and authority. A loss-declared adapter maps between those roles; it +does not transfer authority to VSTD, strengthen a native result, or require consumers to +adopt the producer's private orchestration logic. + | System | Its documented center of gravity | What VSTD may bind or add | What VSTD must not claim | |---|---|---|---| +| [IETF SCITT RFC 9943](https://datatracker.ietf.org/doc/html/rfc9943) and [COSE Receipts RFC 9942](https://datatracker.ietf.org/doc/html/rfc9942) | Signed Statements, registration policy, append-only/non-equivocating transparency services, and portable VDS receipts. | Carry a complete VSTD receipt as an application payload; consume native-verified registration/inclusion as narrowly typed transparency evidence. See the [experimental crosswalk](standards/VSTD_SCITT_CROSSWALK.md). | That registration establishes computational truth, distinct actors, or that VSTD replaces COSE, a Transparency Service, VDS proof profiles, or SCITT trust policy. | | [SLSA v1.2](https://slsa.dev/spec/v1.2/) | Levels and tracks for incrementally improving software supply-chain security, including recommended provenance and verification-summary formats. | A SLSA statement or verification summary as evidence under an explicit VSTD claim coordinate; separate refutation and degradation conditions. | That a VSTD receipt establishes a SLSA level without satisfying and assessing the relevant SLSA requirements. | | [in-toto](https://in-toto.io/docs/getting-started/) | Signed layouts and link metadata describing authorized supply-chain steps, functionaries, materials, and products. | in-toto layout/link bytes as named evidence; graph edges that point to checked step metadata. | That VSTD re-authorizes a functionary or repairs a missing/invalid in-toto chain. | | [Sigstore](https://docs.sigstore.dev/) | Artifact signing associated with identity, short-lived certificates, and transparency-log evidence. | Sigstore bundle, certificate identity, trust root, and verification result as explicit evidence and trust-root fields. | That a digest alone authenticates a signer, or that VSTD reference-kernel acceptance substitutes for signature and transparency-log verification. | @@ -23,20 +38,28 @@ that result and which information remains outside the mapping. ```text native object ──native verifier──> native result │ │ - └──── preserved bytes + identity ──┴──> VSTD evidence reference + └──── preserved bytes + identity ──┴──> loss-declared adapter │ - └── bounded VSTD claim + â–¼ + VSTD claim boundary + portable result + │ + â–¼ + another verifier, framework, or relying party ``` The VSTD claim does not flow backward and strengthen the native result. If the native verifier returns an unknown, unsupported, expired, or invalid outcome, the adapter must preserve it rather than translating it into a clean VSTD result. +Mapping through VSTD is not automatic semantic equivalence. Every adapter must state +what was preserved, what was omitted, what was transformed, and which native +assumptions remain authoritative. + ## Adapter acceptance checklist An ecosystem adapter is not ready until it declares and tests: -1. exact accepted upstream versions and wire identifiers; +1. exact accepted upstream versions and serialized receipt identifiers; 2. canonical bytes and digest rules; 3. upstream verification mechanism and trust roots; 4. field-by-field mapping, including information loss; @@ -47,3 +70,7 @@ An ecosystem adapter is not ready until it declares and tests: No adapter is included merely to populate a compatibility list. Each adapter increases the trusted and maintained surface and therefore needs its own evidence and tests. + +The current SCITT adapter is explicitly experimental and non-normative. Its exact +claim boundary is documented in +[`standards/SCITT_SEMANTIC_BOUNDARY.md`](standards/SCITT_SEMANTIC_BOUNDARY.md). diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index a77c09a..b003dba 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -1,4 +1,6 @@ -# VSTD quickstart +# Verifier Standard (VSTD) quickstart + +> Reader aid: [concept glossary and primary precedents](CONCEPTS_AND_PRECEDENTS.md). ## 1. Install the public source @@ -14,6 +16,10 @@ python -m pip install . Use `vstd` as the cross-platform command. The `verifier` compatibility alias can be shadowed by Windows Driver Verifier. +Before evaluating a broader claim, review the canonical +[implementation-maturity table](../README.md#current-maturity). It separates implemented +checks from candidate calculations and unimplemented mechanisms. + ## 2. Run the adversarial demo ```bash @@ -55,9 +61,11 @@ vstd validate /tmp/vstd-receipt vstd inspect /tmp/vstd-receipt ``` -This establishes that the receipt is structurally valid and that its stable recorded -content agrees with the declared artifacts. It does not establish that the claim is -empirically true beyond that observation surface. +`validate` applies the bundled profile's structural checks and recomputes the receipt's +stable-payload digest. It does not invoke an external JavaScript Object Notation (JSON) +Schema engine, rehash the +declared artifacts, verify external evidence, or establish that the claim is true. Use +`reproduce` for the separately bounded artifact comparison. ## 5. Exercise the falsification route @@ -72,7 +80,7 @@ silently converted into success. ## 6. Read the normative path -1. [`standard/LADDER.md`](../standard/LADDER.md) — numbering, independent evidence, +1. [`standard/LADDER.md`](../standard/LADDER.md) — verification-complex terminology, numbered profiles, separate evidence per closure coordinate, and composition. 2. [`standard/VSTD-4.md`](../standard/VSTD-4.md) — refutability and the grounded decision certificate. @@ -81,4 +89,7 @@ silently converted into success. To evaluate the project rather than merely run it, start by trying to create a receipt that passes outside its declared coordinate. A reproducible counterexample is more -valuable than a general endorsement. +valuable than a general endorsement. Report a +[specification ambiguity](https://github.com/TimeLordRaps/verifier/issues/new?template=specification-ambiguity.yml), +[counterexample](https://github.com/TimeLordRaps/verifier/issues/new?template=counterexample.yml), +or [security issue](../SECURITY.md) through its designated route. diff --git a/docs/assets/vstd-overview.png b/docs/assets/vstd-overview.png index bfe77e73024aa6e5959868546739ecdb4d228f2b..08e738a49cd1f899a77b5514f09ef51450336ebd 100644 GIT binary patch literal 89535 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

6dehJo!uqTA(@rn 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 W3tDHX5VMH@0000 - VSTD two-axis verification overview - Five object-mechanics layers and five collection-dynamics layers. Every layer requires separate evidence; higher layers never substitute for lower layers. + Verifier Standard (VSTD) two-axis verification overview + Five cumulative object profiles and five cumulative Graph profiles over distinct closure coordinates. Every coordinate requires separate evidence; later profiles never substitute for prerequisites. @@ -28,7 +28,7 @@ portable · bounded · refutable - FOUNDER-MAINTAINED ALPHA + ALPHA PROJECT SPECIFICATION OBJECT MECHANICS COLLECTION DYNAMICS @@ -67,14 +67,14 @@ Witness corroborationCorroborated verification network - REF. SUBSETREF. SUBSET - EXPERIMENTALIMPLEMENTED - IMPLEMENTEDIMPLEMENTED - IMPLEMENTEDIMPLEMENTED - DRAFTDRAFT + REF. SUBSETREF. SUBSET + EXPERIMENTALCANDIDATE + IMPLEMENTEDCANDIDATE + CANDIDATECANDIDATE + DRAFTDRAFT - Higher depth = more checked questions · never evidence substitution + Cumulative profiles = more checked coordinates · never evidence substitution From 4ceb60499b5b4f4070c790abb7fc82855735c790 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:48:17 -0700 Subject: [PATCH 11/34] Build reviewable documentation and API references Reason: Make the authoritative standard, newcomer guides, source-linked API, first-party definition previews, schemas, and reference pages navigable from one commit-addressed Pages build. Evidence: Deterministic builders, generated source pages, local definition cards, presentation and link gates, Pages workflow, and regression tests change together. Coordinate: GitHub Pages and repository documentation surface for release 1.2.0. Falsification: Generated pages are stale, a local link or schema route breaks, a definition card depends on a runtime wiki fetch, or API links do not resolve to source. Compatibility: Generated HTML is presentation only; Markdown under standard remains normative and no receipt bytes are reinterpreted. --- .github/external-links-allowlist.txt | 7 + .github/workflows/external-links.yml | 27 + .github/workflows/pages.yml | 2 +- docs/assets/orientation-previews.js | 107 ++++ docs/assets/site.css | 94 +++- docs/guides.html | 115 +++++ docs/index.html | 71 ++- docs/reference.html | 576 +++++++++++++++++++++ scripts/build_docs.py | 724 +++++++++++++++++++++++++++ scripts/build_pages.py | 73 ++- scripts/build_reference.py | 480 ++++++++++++++++++ scripts/check_acronyms.py | 121 +++++ scripts/check_external_links.py | 163 ++++++ scripts/check_presentation.py | 443 +++++++++++++++- scripts/check_terminology.py | 132 +++++ tests/test_external_links.py | 64 +++ tests/test_presentation_surface.py | 468 ++++++++++++++++- 17 files changed, 3623 insertions(+), 44 deletions(-) create mode 100644 .github/external-links-allowlist.txt create mode 100644 .github/workflows/external-links.yml create mode 100644 docs/assets/orientation-previews.js create mode 100644 docs/guides.html create mode 100644 docs/reference.html create mode 100644 scripts/build_docs.py create mode 100644 scripts/build_reference.py create mode 100644 scripts/check_acronyms.py create mode 100644 scripts/check_external_links.py create mode 100644 scripts/check_terminology.py create mode 100644 tests/test_external_links.py diff --git a/.github/external-links-allowlist.txt b/.github/external-links-allowlist.txt new file mode 100644 index 0000000..ac8f0d8 --- /dev/null +++ b/.github/external-links-allowlist.txt @@ -0,0 +1,7 @@ +# One exact URL or trailing-* prefix and a tab-separated reason per line. +# Entries are limited to publishers that reject this audit's automated request. +https://doi.org/10.1145/263699.263712 Publisher returns HTTP 403 to automated probes. +https://journals.ametsoc.org/view/journals/mwre/78/1/1520-0493_1950_078_0001_vofeit_2_0_co_2.xml Publisher returns HTTP 403 to automated probes. +https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1430-9134.2001.00173.x Publisher returns HTTP 403 to automated probes. +https://rss.onlinelibrary.wiley.com/doi/10.1111/j.2517-6161.1952.tb00104.x Publisher returns HTTP 403 to automated probes. +https://www.sciencedirect.com/science/article/pii/S1574013710000560 Publisher returns HTTP 400 to automated probes. diff --git a/.github/workflows/external-links.yml b/.github/workflows/external-links.yml new file mode 100644 index 0000000..f0d8dfc --- /dev/null +++ b/.github/workflows/external-links.yml @@ -0,0 +1,27 @@ +name: external-link-audit + +on: + schedule: + - cron: "23 11 * * 2" + workflow_dispatch: + +permissions: + contents: read + +jobs: + audit: + 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" + - name: Audit external documentation links with retry + run: python scripts/check_external_links.py --retries 2 --workers 8 --report external-links.json + - if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: external-link-audit-${{ github.run_id }} + path: external-links.json + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6345a20..52a00a4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - name: Assemble site and canonical schema routes - run: python scripts/build_pages.py --output _site + run: python scripts/build_pages.py --output _site --source-ref "$GITHUB_SHA" - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 with: path: _site diff --git a/docs/assets/orientation-previews.js b/docs/assets/orientation-previews.js new file mode 100644 index 0000000..49998a3 --- /dev/null +++ b/docs/assets/orientation-previews.js @@ -0,0 +1,107 @@ +(() => { + "use strict"; + + const links = [...document.querySelectorAll('a[data-orientation-preview="repository"]')]; + if (!links.length) return; + + const card = document.createElement("aside"); + card.id = "orientation-preview"; + card.className = "orientation-preview"; + card.hidden = true; + card.setAttribute("role", "tooltip"); + document.body.append(card); + + let activeLink = null; + let timer = null; + + const textElement = (tag, className, text) => { + const element = document.createElement(tag); + element.className = className; + element.textContent = text; + return element; + }; + + const position = (link) => { + if (card.hidden) return; + const rect = link.getBoundingClientRect(); + const margin = 12; + const width = Math.min(420, window.innerWidth - margin * 2); + card.style.width = `${width}px`; + let left = Math.min(rect.left, window.innerWidth - width - margin); + left = Math.max(margin, left); + let top = rect.bottom + 10; + if (top + card.offsetHeight > window.innerHeight - margin) { + top = Math.max(margin, rect.top - card.offsetHeight - 10); + } + card.style.left = `${left}px`; + card.style.top = `${top}px`; + }; + + const render = (link) => { + card.replaceChildren(); + card.append( + textElement( + "span", + "orientation-preview-eyebrow", + "Repository definition · versioned with VSTD", + ), + ); + card.append( + textElement("strong", "orientation-preview-title", link.dataset.orientationConcept), + ); + card.append( + textElement("p", "orientation-preview-body", link.dataset.orientationDefinition), + ); + card.append( + textElement( + "small", + "orientation-preview-hint", + "The link opens optional external background; it is not VSTD authority.", + ), + ); + requestAnimationFrame(() => position(link)); + }; + + const show = (link) => { + clearTimeout(timer); + activeLink = link; + link.setAttribute("aria-describedby", card.id); + card.hidden = false; + render(link); + }; + + const hide = (link) => { + if ( + activeLink !== link || + link.matches(":hover") || + document.activeElement === link + ) { + return; + } + link.removeAttribute("aria-describedby"); + activeLink = null; + card.hidden = true; + }; + + for (const link of links) { + link.addEventListener("mouseenter", () => { + clearTimeout(timer); + timer = setTimeout(() => show(link), 250); + }); + link.addEventListener("mouseleave", () => { + clearTimeout(timer); + timer = setTimeout(() => hide(link), 120); + }); + link.addEventListener("focus", () => show(link)); + link.addEventListener("blur", () => hide(link)); + } + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && activeLink) { + const link = activeLink; + link.blur(); + hide(link); + } + }); + window.addEventListener("resize", () => activeLink && position(activeLink)); +})(); diff --git a/docs/assets/site.css b/docs/assets/site.css index 68d667a..8aa8aff 100644 --- a/docs/assets/site.css +++ b/docs/assets/site.css @@ -27,6 +27,10 @@ body { a { color: var(--teal); text-underline-offset: .2em; } a:hover { color: #8be6d6; } +a:focus-visible { outline: 3px solid var(--amber); outline-offset: 4px; border-radius: 3px; } + +.skip-link { position: fixed; left: 18px; top: -80px; z-index: 10; padding: 10px 14px; color: #06141f; background: var(--amber); font-weight: 800; } +.skip-link:focus { top: 12px; } .wrap { width: min(1120px, calc(100% - 36px)); margin: 0 auto; } @@ -47,7 +51,13 @@ nav { .eyebrow { color: var(--teal); font-size: .78rem; font-weight: 780; letter-spacing: .16em; text-transform: uppercase; } h1 { max-width: 760px; margin: 12px 0 22px; font-size: clamp(2.8rem, 6vw, 5.6rem); line-height: .98; letter-spacing: -.055em; } .lead { color: #c6d6da; font-size: clamp(1.12rem, 2vw, 1.34rem); max-width: 670px; } +.lead-defs { margin: 18px 0 0; max-width: 670px; color: #94a9ae; font-size: 0.95rem; line-height: 1.55; border-left: 2px solid #24373c; padding-left: 16px; } +.lead-defs dt { color: #c6d6da; font-weight: 600; letter-spacing: 0.01em; } +.lead-defs dd { margin: 2px 0 12px; } +.lead-defs dd:last-child { margin-bottom: 0; } +.lead-close { margin-top: 18px; } .actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 30px; } +.hero-actions { margin: 26px 0 30px; } .button { display: inline-flex; align-items: center; min-height: 46px; padding: 0 18px; border: 1px solid var(--line); border-radius: 9px; color: var(--ink); text-decoration: none; font-weight: 700; background: rgba(14, 39, 52, .72); } .button.primary { color: #061a1b; background: var(--teal); border-color: var(--teal); } .hero-card, .card, pre { border: 1px solid var(--line); border-radius: 15px; background: var(--panel); box-shadow: 0 22px 65px rgba(0, 0, 0, .22); } @@ -71,20 +81,102 @@ pre { margin: 0; padding: 24px; color: #dcebed; font: 500 .92rem/1.75 ui-monospa .boundary { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-top: 28px; } .boundary ul { margin: 10px 0 0; padding-left: 20px; color: var(--muted); } .status { border-left: 3px solid var(--amber); padding: 3px 0 3px 18px; color: #d6e2e5; max-width: 850px; } +.release-coordinate { max-width: 850px; color: var(--muted); } + +.ref-hero { padding: 54px 0 10px; } +.ref-hero h1 { font-size: clamp(2.4rem, 5vw, 4.2rem); margin-bottom: 18px; } +.ref-hero .status { margin-top: 20px; } +.ref-table, .ref-list { margin-top: 26px; } +.ref-table { overflow-x: auto; } +table { width: 100%; border-collapse: collapse; font-size: .93rem; } +th, td { text-align: left; vertical-align: top; padding: 10px 14px; border-bottom: 1px solid var(--line); } +th { color: var(--teal); font-size: .74rem; letter-spacing: .12em; text-transform: uppercase; } +td { color: var(--muted); } +td code, .ref-help code, .section-lead code, .lead code, li code, p code { color: #dcebed; background: rgba(9, 30, 41, .8); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; font: 500 .86em/1.5 ui-monospace, SFMono-Regular, Consolas, monospace; } +.ref-item { border: 1px solid var(--line); border-radius: 15px; background: var(--panel); padding: 20px 22px; margin-bottom: 14px; overflow-x: auto; } +.ref-item h3 { margin: 0 0 8px; font-size: 1.05rem; } +.ref-item h3 code { background: none; border: none; padding: 0; color: var(--ink); font-size: 1em; } +.ref-tag { color: var(--amber); font-size: .7rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; } +.ref-help, .ref-none, .ref-source { color: var(--muted); font-size: .93rem; margin: 0 0 12px; } +.ref-source { font-size: .84rem; } +.ref-signature { padding: 14px 16px; margin: 0 0 12px; font-size: .84rem; border-radius: 10px; overflow-x: auto; } +.guide-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; } +.guide-card { border: 1px solid var(--line); border-radius: 15px; background: var(--panel); padding: 22px; } +.guide-card h2 { font-size: 1.35rem; margin-bottom: 10px; } +.guide-card ul { margin: 0; padding-left: 20px; } +.guide-card li { margin: 8px 0; color: var(--muted); } +.links a[aria-current="page"] { color: var(--ink); text-decoration: underline; text-decoration-color: var(--teal); text-underline-offset: .42em; } + +.doc-shell { display: grid; grid-template-columns: minmax(220px, 285px) minmax(0, 1fr); gap: clamp(34px, 6vw, 78px); align-items: start; padding-top: 44px; } +.doc-sidebar { position: sticky; top: 18px; max-height: calc(100vh - 36px); overflow: auto; padding: 18px 18px 20px; border: 1px solid var(--line); border-radius: 14px; background: rgba(8, 28, 39, .92); scrollbar-color: var(--line) transparent; } +.doc-sidebar section { padding: 0; } +.doc-sidebar section + section { margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--line); } +.doc-sidebar h2 { margin: 0 0 9px; color: var(--teal); font-size: .7rem; letter-spacing: .13em; text-transform: uppercase; } +.doc-sidebar ul { margin: 0; padding: 0; list-style: none; } +.doc-sidebar li { margin: 6px 0; line-height: 1.35; } +.doc-sidebar a { display: block; padding: 4px 7px; border-radius: 6px; color: var(--muted); text-decoration: none; font-size: .81rem; } +.doc-sidebar a:hover { color: var(--ink); background: rgba(85, 211, 189, .08); } +.doc-sidebar a[aria-current="page"] { color: var(--ink); background: rgba(85, 211, 189, .13); box-shadow: inset 2px 0 0 var(--teal); } +.doc-sidebar .toc-depth-3 a { padding-left: 17px; font-size: .76rem; } + +.doc-content { min-width: 0; max-width: 820px; } +.doc-coordinate { display: flex; justify-content: space-between; gap: 20px; align-items: center; margin-bottom: 12px; color: var(--teal); font-size: .74rem; font-weight: 760; letter-spacing: .1em; text-transform: uppercase; } +.doc-coordinate a { color: var(--muted); letter-spacing: normal; text-transform: none; } +.doc-boundary { margin: 0 0 38px; padding: 13px 16px; border-left: 3px solid var(--amber); color: var(--muted); background: rgba(242, 191, 104, .055); font-size: .88rem; } +.doc-content h1 { max-width: none; margin: 0 0 20px; font-size: clamp(2.35rem, 5vw, 4rem); line-height: 1.04; letter-spacing: -.045em; } +.doc-content h2 { margin: 54px 0 14px; padding-top: 8px; font-size: clamp(1.65rem, 3vw, 2.25rem); } +.doc-content h3 { margin: 36px 0 12px; font-size: 1.35rem; } +.doc-content h4 { margin: 28px 0 10px; font-size: 1.08rem; } +.doc-content h5, .doc-content h6 { margin: 24px 0 8px; color: var(--teal); font-size: .94rem; letter-spacing: .03em; } +.doc-content p, .doc-content li { color: #bdcdd1; } +.doc-content p { margin: 13px 0; } +.doc-content ul, .doc-content ol { padding-left: 25px; } +.doc-content li { margin: 7px 0; } +.doc-content blockquote { margin: 22px 0; padding: 3px 20px; border-left: 3px solid var(--teal); background: rgba(85, 211, 189, .055); } +.doc-content blockquote p { color: #d5e3e5; } +.doc-content hr { height: 1px; margin: 44px 0; border: 0; background: var(--line); } +.doc-content pre { margin: 20px 0; overflow: auto; box-shadow: none; } +.doc-content .doc-table { margin: 22px 0; overflow-x: auto; border: 1px solid var(--line); border-radius: 11px; } +.doc-content .doc-table table { min-width: 560px; } +.doc-content .doc-table tr:last-child td { border-bottom: 0; } +.doc-content .doc-image { padding: 18px; border: 1px solid var(--line); border-radius: 14px; background: var(--panel); } +.doc-content .doc-image img, .doc-content p > img { display: block; max-width: 100%; height: auto; margin: 0 auto; } +.math, .math-block { color: #dcebed; font-family: "Cambria Math", "STIX Two Math", ui-monospace, Consolas, monospace; } +.math { white-space: nowrap; } +.math-block { margin: 22px 0; padding: 16px 20px; overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; background: rgba(9, 30, 41, .8); text-align: center; } +.heading-anchor { margin-left: 9px; color: transparent; text-decoration: none; font-size: .65em; } +.doc-content h1:hover .heading-anchor, .doc-content h2:hover .heading-anchor, .doc-content h3:hover .heading-anchor, .heading-anchor:focus { color: var(--muted); } +.orientation-link { text-decoration-style: dotted; text-underline-offset: .18em; } +.orientation-link::after { content: " ?"; color: var(--teal); font-size: .7em; font-weight: 800; vertical-align: super; } +.orientation-preview { position: fixed; z-index: 100; width: min(420px, calc(100vw - 24px)); padding: 16px 18px; border: 1px solid var(--teal); border-radius: 13px; background: #081c27; box-shadow: 0 18px 50px rgba(0, 0, 0, .42); color: var(--ink); pointer-events: none; } +.orientation-preview[hidden] { display: none; } +.orientation-preview-eyebrow { display: block; margin-bottom: 7px; color: var(--teal); font-size: .66rem; font-weight: 800; letter-spacing: .11em; text-transform: uppercase; } +.orientation-preview-title { display: block; margin-bottom: 8px; color: var(--ink); font-size: 1.02rem; } +.orientation-preview-body { margin: 0; color: #c8d7da; font-size: .86rem; line-height: 1.5; } +.orientation-preview-hint { display: block; margin-top: 9px; color: var(--muted); font-size: .72rem; } footer { border-top: 1px solid var(--line); margin-top: 54px; padding: 28px 0 44px; color: var(--muted); font-size: .9rem; } @media (max-width: 900px) { .hero { grid-template-columns: 1fr; padding-top: 40px; } .grid { grid-template-columns: 1fr 1fr; } + .doc-shell { grid-template-columns: 1fr; } + .doc-sidebar { position: static; max-height: 420px; } } @media (max-width: 620px) { nav { align-items: flex-start; flex-wrap: wrap; } .links { width: 100%; justify-content: flex-start; gap: 14px; } - .grid, .boundary { grid-template-columns: 1fr; } + .grid, .boundary, .guide-grid { grid-template-columns: 1fr; } h1 { font-size: 3.1rem; } .eyebrow { font-size: .7rem; overflow-wrap: anywhere; } .actions { display: grid; grid-template-columns: 1fr; } .button { width: 100%; justify-content: center; } + .doc-sidebar { max-height: 280px; } + .doc-coordinate { align-items: flex-start; flex-direction: column; gap: 5px; } + .orientation-preview { right: 12px !important; bottom: 12px; left: 12px !important; top: auto !important; width: auto; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } } diff --git a/docs/guides.html b/docs/guides.html new file mode 100644 index 0000000..f383672 --- /dev/null +++ b/docs/guides.html @@ -0,0 +1,115 @@ + + + + + + + + + + + + VSTD guides and standards + + + + + + +

+ +
+ +
+
+
Public documentation
+

Find the exact boundary.

+

This page links the maintained guides and specifications without + restating them. Normative requirements live under standard/; guides and + profiles must not silently strengthen those requirements.

+

Interoperability material below uses Supply Chain Integrity, + Transparency, and Trust (SCITT).

+

Documentation coordinate: + verifier-standard 1.2.0 unreleased candidate. The assembled site also + carries machine-readable build metadata; + published tags and release + artifacts preserve historical coordinates. Normative ownership remains under + standard/.

+ +
+ +
+
+

Start here

+ +
+ + + +
+

Profiles and interoperability

+ +
+ +
+

Project and contribution

+ +
+
+
+ +
VSTD · Apache-2.0 · Maintainer-led alpha · No standards-body endorsement claimed.
+ + diff --git a/docs/index.html b/docs/index.html index 8c24c75..bcc7867 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,22 +3,29 @@ - + + VSTD — portable, bounded, refutable + + +
@@ -29,14 +36,27 @@
Verification language for computational work

Make the claim challengeable.

-

VSTD makes computational claims portable, bounded, and refutable—so a result travels with its meaning, evidence, limits, and failure route.

-
- Run the four-scenario demo - Read the quickstart +

VSTD packages bounded computational claims with their evidence, + checking mechanisms, limits, refutation conditions, provenance, and + reproducibility information. It does not replace native domain verifiers or + strengthen their results.

+ +

VSTD makes computational claims:

+
+
Portable:
+
checkable without post-verdict cooperation from the declarant because every verdict-critical byte is included or retrievable and digest-bound; a locator or retention promise alone does not qualify.
+
Bounded:
+
carrying their own claim coordinates and resource ceilings, so exhausted work remains UNKNOWN rather than being answered outside the checked boundary.
+
Refutable:
+
exposing falsification conditions, admissible counterevidence, exclusions, and decision rules so another party can challenge the exact result.
+
+

A result travels with its meaning, evidence, limits, and failure routes so downstream humans and agents can draw conclusions without silently widening it.

- The VSTD object and graph axes, where every layer requires its own separate evidence + The VSTD object and Graph numbered profiles, where every closure coordinate requires separate evidence
@@ -54,6 +74,19 @@

A green check is not a complete explanation.

+
+
+
Artifact-first semantics
+

Verify the process, not the actor.

+

VSTD evaluates bounded validity propositions about computational processes represented by software and evidence-bearing artifacts. Identity, popularity, and reputation alone add no verdict weight. TRUST, ROT, and RUST are formal semantic names, not acronyms or actor ratings. Architectural zero knowledge presumes no unevidenced proposition; cryptographic zero knowledge can enclose a confidential witness only through a named proof system bound to the exact program and predicate.

+
+
TRUST · FORWARD

Mechanism-earned support

Exact artifact support may move through a checked transformation. It is never a rating of whether an actor is good or trustworthy.

+
ROT · CURRENT STATE

Admissibility degrades

Typed lifecycle evidence can require reassessment without rewriting an immutable historical result. Age alone is insufficient.

+
RUST · BACKWARD

Diagnostic ancestry

A descendant deviation can trace toward recorded ancestor candidates. Reachability is not guilt, falsehood, or causal localization.

+
+
+
+
Executable first impression
@@ -66,7 +99,7 @@

See it reject, preserve uncertainty, and degrade.

[DEMO OK] Valid-looking proof, wrong artifact → REJECTED [DEMO OK] Bound exhausted without a false answer → ACCEPTED/UNKNOWN [DEMO OK] Inflated verification-cost claim → REJECTED -[DEMO OK] Revoked ancestor behind valid descendants → GRAPH-LEVEL-0
+[DEMO OK] Revoked ancestor behind valid descendants → GRAPH-CANDIDATE-0
@@ -75,8 +108,8 @@

See it reject, preserve uncertainty, and degrade.

Boundaries

Useful without pretending to be total.

-

VSTD can help

  • carry exact claims and evidence between systems;
  • preserve PASS, FAIL, and UNKNOWN distinctly;
  • trace poisoned ancestry and downstream impact;
  • make challenge conditions machine-readable.
-

VSTD cannot establish

  • general AI safety, alignment, or intent;
  • hidden model state or unobserved tool context;
  • complete physical-world execution history;
  • truth outside the declared observation surface.
+

VSTD can help

  • carry exact claims and evidence between systems;
  • preserve PASS, FAIL, and UNKNOWN distinctly;
  • query recorded ancestry and bounded downstream impact;
  • make challenge conditions machine-readable.
+

VSTD cannot establish

  • general artificial intelligence (AI) safety, alignment, or intent;
  • hidden model state or unobserved tool context;
  • complete physical-world execution history;
  • truth outside the declared observation surface.
@@ -84,12 +117,16 @@

Useful without pretending to be total.

Current status
-

Built for adversarial review, not ceremonial adoption.

-

VSTD is a founder-maintained alpha project specification. It has no demonstrated external adoption, independent implementation, interoperability deployment, or third-party security review. VSTD-5 remains draft.

+

Current implementation status

+

VSTD is a maintainer-led alpha project specification. VSTD-4 candidate depth and Graph profiles 2-5 are candidate computations with conformance NOT_ESTABLISHED; VSTD-5 is draft and not implemented. The project has no demonstrated external adoption, independent implementation, interoperability deployment, or third-party security review.

+

Release coordinate: this branch + documents verifier-standard 1.2.0 as an unreleased candidate. Use + GitHub Releases for + the latest published artifact.

diff --git a/docs/reference.html b/docs/reference.html new file mode 100644 index 0000000..716a9f7 --- /dev/null +++ b/docs/reference.html @@ -0,0 +1,576 @@ + + + + + + + + + + + + VSTD docs — command-line interface (CLI) and application programming interface (API) reference + + + + + + +
+ +
+ +
+
+
Reference · verifier-standard 1.2.0 · VSTD-4 CANDIDATE; CONFORMANCE NOT_ESTABLISHED
+

Inspect the whole pipeline.

+

Terms used below: hash-based message authentication + code (HMAC); International Organization for Standardization (ISO); JavaScript Object + Notation (JSON); Secure Hash Algorithm 256-bit (SHA-256); and YAML Ain't Markup Language + (YAML).

+

Every command, argument, top-level export, and listed dispatch edge below + is read out of the installed package when this page is built, by + scripts/build_reference.py, and the presentation tests fail closed when the + committed page drifts — so it cannot describe behaviour the implementation no + longer has.

+

This page states the declared public surface of one implementation. It + does not establish that any individual claim checked by these commands is true, nor that + an external implementation exists.

+ +
+ +
+
+
Pipeline
+

Command to implementation, without a gap.

+

Each entry point below is imported while this page is built. A + rename, move, or deletion fails the build instead of publishing a stale map.

+
+ + + + + + + + + + + +
CommandWhat it doesImplementation entry points
vstd demoRuns the four adversarial specimens in-process and reports whether each defensive outcome matched its declared invariant.verifier.runtime.demo:run_demo
verifier.runtime.demo:demo_report
vstd planResolves a manifest's command and declared paths without executing anything.verifier.core.run_planning:load_manifest
verifier.core.run_planning:describe_run_plan
vstd runExecutes a trusted manifest without sandboxing, captures the observed execution, and writes a canonically digested receipt.verifier.core.run_planning:load_manifest
verifier.core.run:capture_run
verifier.core.receipt:compute_canonical_digest
vstd validateDispatches on the receipt's serialized `schema_version` identifier and runs its implemented checks. Generic-run validation enforces its required structure and stable digest; other receipt kinds enforce their separately documented structure and evidence rules.verifier.core.run_validation:validate_run_receipt
verifier.data.receipt:validate_data_receipt
verifier.hardware.validation:validate_vstd3_receipt
vstd inspectPrints the claim coordinate, digest, and verdict surface of a stored receipt.verifier.core.run_inspection:inspect_run_receipt
verifier.hardware.receipt:load_vstd3_receipt
vstd reproduceReplays only the mechanisms a stored receipt actually carries; physical hardware execution is refused rather than simulated.verifier.core.run_reproduction:reproduce_run_receipt
verifier.data.receipt:reproduce_data_receipt
vstd impactFinds stored run receipts whose recorded ancestry reaches a revoked provenance artifact.verifier.core.run_impact:find_run_receipts_impacted_by_revocation
vstd dataTraces, renders, or exports the provenance hypergraph carried by a VSTD-Graph receipt.verifier.data.models:ProvenanceHypergraph
vstd artifactFreezes exact regular-file bytes, adds or verifies finite self-closing seals, and creates observable copy-on-write thaw descendants.verifier.artifact_control:freeze_artifact
verifier.artifact_control:seal_artifact
verifier.artifact_control:verify_frozen_artifact
verifier.artifact_control:thaw_artifact
vstd experimentValidates experimental workflow manifests or maps normalized GitHub snapshots without granting a VSTD verdict.verifier.runtime.experimental_workflow_cli:handle_experiment_command
verifier.experimental_workflow.profile:load_manifest
verifier.experimental_workflow.github:github_snapshot_to_events
vstd hardware / continuity / fleet / evidence / claimsEvaluates VSTD-3 substrate-accountability receipts, their continuity and fleet evidence, and their declared claims.verifier.runtime.hardware_cli:handle_vstd3_command
verifier.hardware.validation:validate_vstd3_receipt
+
+
+ +
+
+
CLI
+

The vstd command reference.

+

Extracted from the live argument parser in + verifier.runtime.public_cli. + vstd is the canonical cross-platform command; verifier is + retained as an alias only on platforms where it is unambiguous.

+
+

vstd

+

Subcommand group.

+

No arguments; this command only groups subcommands.

+
+
+

vstd demo

+

Run the side-effect-free VSTD adversarial flagship demonstration.

+ + + + +
ArgumentKindMeaning
--scenariooptionalRun all scenarios or one named scenario. (one of: all, wrong-artifact, honest-unknown, inflated-tier, poisoned-ancestor) [default: all]
--jsonoptional
--emit-specimensoptionalWrite deterministic JSON specimens and observations to DIR.
+
+
+

vstd run

+

Execute a trusted manifest without sandboxing and capture a VSTD receipt.

+ + + + +
ArgumentKindMeaning
manifestpositionalJSON or YAML run manifest.
--outputoptionalReceipt output directory.
--receipt-idoptionalOverride the manifest claim id.
+
+
+

vstd plan

+

Show a manifest's declared command and paths without executing it.

+ + + +
ArgumentKindMeaning
manifestpositionalJSON or YAML run manifest.
--jsonoptional
+
+
+

vstd validate

+

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

+ + + + +
ArgumentKindMeaning
receiptpositionalReceipt directory or receipt.json.
--jsonoptional
--keyoptional
+
+
+

vstd inspect

+

Inspect a generic-run or VSTD-Graph receipt; validate and report VSTD-3.

+ + + + +
ArgumentKindMeaning
receiptpositionalReceipt directory or receipt.json.
--jsonoptional
--keyoptional
+
+
+

vstd reproduce

+

Replay the mechanisms available in a stored receipt.

+ + + + +
ArgumentKindMeaning
receiptpositionalReceipt directory or receipt.json.
--jsonoptional
--rerunoptionalGeneric-run receipts only: execute the recorded command again.
+
+
+

vstd impact

+

Find run receipts affected by a provenance-artifact revocation.

+ + + + +
ArgumentKindMeaning
dataset_receiptpositional
artifact_idpositional
--search-rootoptional[default: receipts]
+
+
+

vstd data

+

Inspect a stored VSTD-Graph hypergraph.

+

No arguments; this command only groups subcommands.

+
+
+

vstd data trace

+

Subcommand group.

+ + + + +
ArgumentKindMeaning
artifact_idpositional
--receiptoptional
--directionoptional(one of: ancestors, descendants, blast_radius) [default: ancestors]
+
+
+

vstd data graph

+

Subcommand group.

+ + +
ArgumentKindMeaning
receiptpositional
+
+
+

vstd data export

+

Subcommand group.

+ + +
ArgumentKindMeaning
receiptpositional
+
+
+

vstd artifact

+

Freeze exact artifact bytes, add or verify a seal, or thaw a descendant.

+

No arguments; this command only groups subcommands.

+
+
+

vstd artifact freeze

+

Copy exact file or directory bytes into a guarded artifact bundle.

+ + + + + + + +
ArgumentKindMeaning
sourcepositional
bundlepositional
--media-typeoptional[default: application/octet-stream]
--parentoptional
--contextoptional
--jsonoptional
+
+
+

vstd artifact seal

+

Add a readable finite self-closing Ed25519 seal.

+ + + + +
ArgumentKindMeaning
bundlepositional
--private-keyoptional
--jsonoptional
+
+
+

vstd artifact verify

+

Recompute exact bytes, guards, seals, and optional external anchors.

+ + + + + + +
ArgumentKindMeaning
bundlepositional
--expected-artifact-idoptional
--expected-key-idoptional
--freeze-onlyoptionalAccept a clean freeze without claiming seal-backed identity.
--jsonoptional
+
+
+

vstd artifact thaw

+

Copy a clean sealed parent into a mutable descendant.

+ + + + + + +
ArgumentKindMeaning
bundlepositional
destinationpositional
--expected-artifact-idoptional
--expected-key-idoptional
--jsonoptional
+
+
+

vstd artifact status

+

Compare a thawed descendant with its sealed parent identity.

+ + + + +
ArgumentKindMeaning
artifactpositional
--recordoptional
--jsonoptional
+
+
+

vstd experiment

+

Validate or adapt experimental, non-normative workflow records.

+

No arguments; this command only groups subcommands.

+
+
+

vstd experiment validate

+

Validate a profile manifest without granting a VSTD verdict.

+ + + + +
ArgumentKindMeaning
manifestpositionalExperimental workflow manifest JSON.
--repo-rootoptionalRepository root used to verify every repo: artifact locator.
--jsonoptional
+
+
+

vstd experiment github-events

+

Map a strict normalized GitHub snapshot to verdict-neutral events.

+ + + +
ArgumentKindMeaning
snapshotpositionalNormalized GitHub snapshot JSON.
--jsonoptional
+
+
+

vstd hardware

+

Discover or emulate accelerator evidence.

+

No arguments; this command only groups subcommands.

+
+
+

vstd hardware list

+

List accelerator profiles.

+ + + +
ArgumentKindMeaning
--vendoroptional
--jsonoptionalEmit stable machine-readable JSON.
+
+
+

vstd hardware inspect

+

Inspect one accelerator profile.

+ + + +
ArgumentKindMeaning
profile_idpositional
--jsonoptionalEmit stable machine-readable JSON.
+
+
+

vstd hardware discover

+

Run a vendor or generic adapter.

+ + + + + +
ArgumentKindMeaning
--adapteroptional(one of: generic, nvidia, amd, intel)
--fixtureoptional
--outputoptionalWrite the normalized adapter result JSON.
--jsonoptionalEmit stable machine-readable JSON.
+
+
+

vstd hardware emulate

+

Run the deterministic virtual firmware contract probe.

+ + + + + + + + +
ArgumentKindMeaning
--outputoptional
--created-atoptionalFinal ISO-8601 receipt timestamp.
--device-idoptional[default: vstd3-virtual-0]
--firmware-versionoptional[default: 1.0.0]
--key-idoptional[default: vstd3-virtual-device-key]
--key-hexoptionalTest-only emulator HMAC key in hex.
--jsonoptionalEmit stable machine-readable JSON.
+
+
+

vstd hardware attest

+

Run an explicitly virtual attestation probe; no commodity claim is made.

+ + + + + + + + + +
ArgumentKindMeaning
--virtualoptional
--outputoptional
--created-atoptional
--device-idoptional[default: vstd3-virtual-0]
--firmware-versionoptional[default: 1.0.0]
--key-idoptional[default: vstd3-virtual-device-key]
--key-hexoptional
--jsonoptionalEmit stable machine-readable JSON.
+
+
+

vstd hardware capabilities

+

Evaluate incremental VSTD 3 conformance profiles.

+ + + + +
ArgumentKindMeaning
receiptpositional
--jsonoptionalEmit stable machine-readable JSON.
--keyoptionalTest-only HMAC verification key; repeat for multiple key ids.
+
+
+

vstd hardware verify

+

Verify a VSTD 3 receipt and all recorded passing claims.

+ + + + +
ArgumentKindMeaning
receiptpositional
--jsonoptionalEmit stable machine-readable JSON.
--keyoptionalTest-only HMAC verification key; repeat for multiple key ids.
+
+
+

vstd continuity

+

Verify authenticated event continuity.

+

No arguments; this command only groups subcommands.

+
+
+

vstd continuity verify

+

Subcommand group.

+ + + + +
ArgumentKindMeaning
receiptpositional
--jsonoptionalEmit stable machine-readable JSON.
--keyoptionalTest-only HMAC verification key; repeat for multiple key ids.
+
+
+

vstd fleet

+

Verify a declared enrolled fleet boundary.

+

No arguments; this command only groups subcommands.

+
+
+

vstd fleet verify

+

Subcommand group.

+ + + +
ArgumentKindMeaning
receiptpositional
--jsonoptionalEmit stable machine-readable JSON.
+
+
+

vstd evidence

+

Inspect VSTD 3 evidence strength.

+

No arguments; this command only groups subcommands.

+
+
+

vstd evidence inspect

+

Subcommand group.

+ + + + +
ArgumentKindMeaning
receiptpositional
--jsonoptionalEmit stable machine-readable JSON.
--keyoptionalTest-only HMAC verification key; repeat for multiple key ids.
+
+
+

vstd claims

+

Evaluate or explain VSTD 3 claims.

+

No arguments; this command only groups subcommands.

+
+
+

vstd claims evaluate

+

Subcommand group.

+ + + + +
ArgumentKindMeaning
receiptpositional
--jsonoptionalEmit stable machine-readable JSON.
--keyoptionalTest-only HMAC verification key; repeat for multiple key ids.
+
+
+

vstd claims explain

+

Subcommand group.

+ + + +
ArgumentKindMeaning
kindpositional(one of: DEVICE_IDENTITY, FIRMWARE_INTEGRITY, EXECUTION_OBSERVED, EXECUTION_ATTESTATION, EXECUTION_ACCOUNTING, ACCOUNTING_CONTINUITY, COMPLETE_MEDIATION, FLEET_COMPLETENESS, PHYSICAL_WORLD_COMPLETENESS)
--jsonoptionalEmit stable machine-readable JSON.
+
+
+
+ +
+
+
API
+

Top-level Python exports.

+

The names in verifier.__all__, with their live + signatures and declared docstrings, are the supported runtime surface under the + Python API stability policy. + Subpackage imports are internal unless a published policy names them.

+
+

ArtifactControlError class

+
ArtifactControlError
+

Raised when an artifact-control action cannot fail closed.

+

Defined in verifier.artifact_control

+ +
+
+

ArtifactVerification class

+
ArtifactVerification(state: 'str', artifact_id: 'str | None', content_id: 'str | None', freeze_id: 'str | None', freeze_valid: 'bool', guard_valid: 'bool', valid_seal_ids: 'tuple[str, ...]', key_ids: 'tuple[str, ...]', external_anchor: 'str', errors: 'tuple[str, ...]', warnings: 'tuple[str, ...]') -> None
+

Result of independently recomputing a frozen artifact and its seals.

+

Defined in verifier.artifact_control

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

DecisionCertificate class

+
DecisionCertificate(header: 'CertificateHeader', formula: 'tuple[tuple[int, ...], ...]', grounding: 'Grounding', decision: 'DecisionBlock', hints: 'dict[str, Any]' = <factory>) -> None
+

Canonical grounded decision certificate (GDC) blocks for the bounded checker.

+

Defined in verifier.core.certificate

+ + + + +
MethodSummary
digest(self) -> 'str'
to_dict(self) -> 'dict[str, Any]'
without_hints(self) -> "'DecisionCertificate'"Hint-stripped form.
+
+
+

ReproducibilityLevel enum

+

Monotone reproduction-fidelity states; class name retained for compatibility.

+

Defined in verifier.core.reproducibility

+

Members: BITWISE_IDENTICAL, CONTENT_IDENTICAL, EVIDENCE_EQUIVALENT, RESULT_EQUIVALENT, SEMANTIC_REPRODUCTION

+
+
+

VerificationGeometry class

+
VerificationGeometry(geometry_id: 'str', primary_subject_id: 'str', subjects: 'list[Subject]', loci: 'list[Locus]', facets: 'list[Facet]', coordinates: 'list[Coordinate]', surface: 'VerificationSurface', seams: 'list[Seam]' = <factory>, mechanisms: 'list[VerificationMechanism]' = <factory>, judgments: 'list[CoordinateJudgment]' = <factory>, horizons: 'list[Horizon]' = <factory>, residuals: 'list[Residual]' = <factory>, valences: 'list[VerificationValence]' = <factory>, reconstructions: 'list[ReconstructionAttempt]' = <factory>, verification_layers: 'list[VerificationLayer]' = <factory>, novelties: 'list[Novelty]' = <factory>, secondary_subject_id: 'Optional[str]' = None, focus_coordinate_ids: 'tuple[str, ...]' = (), meta_focus_coordinate_ids: 'tuple[str, ...]' = (), schema_version: 'str' = 'VSTD-2') -> None
+

A finite verification geometry and its higher-order audit surface.

+

Defined in verifier.core.geometry

+ + + + + +
MethodSummary
assess_closure(self) -> 'ClosureAssessment'Assess declared closure and higher-order self-closure separately.
canonical_digest(self) -> 'str'
to_dict(self) -> 'dict[str, Any]'
validate(self) -> 'list[str]'Return structural and epistemic errors; an empty list means valid.
+
+
+

VerificationVerdict enum

+

Outcome vocabulary returned by the VSTD-1 claim-mechanics checker.

+

Defined in verifier.core.checker

+

Members: VERIFIED, FALSIFIED, INDETERMINATE, UNSUPPORTED

+
+
+

VstdReceipt class

+
VstdReceipt(schema_version: 'str', receipt_kind: 'str', receipt_id: 'str', claim: 'ClaimSpec', evidence: 'EvidencePayload', target_result: 'dict[str, Any]', independent_audit: 'IndependentAuditReport', provenance: 'ProvenanceRecord', reproducibility: 'dict[str, Any]', canonical_digest: 'str' = '', execution_metadata: 'Optional[ExecutionMetadata]' = None) -> None
+

Mutable in-memory model of a canonically digested VSTD-1 claim receipt.

+

Defined in verifier.core.receipt

+ + + + + + +
MethodSummary
compute_and_set_digest(self) -> 'str'
get_stable_payload(self) -> 'dict[str, Any]'Extract only deterministic, location-independent fields for canonical hashing.
save_to_directory(self, out_dir: 'Path') -> 'Path'
to_dict(self) -> 'dict[str, Any]'
verify_digest_integrity(self) -> 'bool'
+
+
+

capture_run function

+
capture_run(manifest: 'Mapping[str, Any]', manifest_dir: 'Path', receipt_id: 'Optional[str]' = None) -> 'GenericRunReceipt'
+

Execute the manifest-declared command and capture a computational run receipt.

+

Defined in verifier.core.run

+ +
+
+

certificate_from_canonical_bytes function

+
certificate_from_canonical_bytes(data: 'bytes') -> 'DecisionCertificate'
+

Decode only the canonical JSON representation used in commitment digests.

+

Defined in verifier.core.certificate

+ +
+
+

compute_canonical_digest function

+
compute_canonical_digest(stable_payload: 'Mapping[str, Any]') -> 'str'
+

Compute SHA-256 digest of canonicalized stable payload.

+

Defined in verifier.core.receipt

+ +
+
+

freeze_artifact function

+
freeze_artifact(source: 'str | Path', bundle: 'str | Path', *, media_type: 'str' = 'application/octet-stream', parent_bundles: 'Iterable[str | Path]' = (), context_bundles: 'Iterable[str | Path]' = ()) -> 'dict[str, Any]'
+

Preserve exact bytes in a new guarded bundle without creating a seal.

+

Defined in verifier.artifact_control

+ +
+
+

require_vstd5_entry function

+
require_vstd5_entry(result: 'DepthResult') -> 'DepthResult'
+

Reject the current unbound candidate result at the VSTD-5 boundary.

+

Defined in verifier.core.depth

+ +
+
+

seal_artifact function

+
seal_artifact(bundle: 'str | Path', private_key: 'str | Path') -> 'dict[str, Any]'
+

Add one deterministic, readable, self-closing Ed25519 seal.

+

Defined in verifier.artifact_control

+ +
+
+

thaw_artifact function

+
thaw_artifact(bundle: 'str | Path', destination: 'str | Path', *, expected_artifact_id: 'str | None' = None, expected_key_id: 'str | None' = None) -> 'dict[str, Any]'
+

Create a mutable descendant from a cleanly sealed frozen artifact.

+

Defined in verifier.artifact_control

+ +
+
+

thawed_artifact_status function

+
thawed_artifact_status(artifact: 'str | Path', thaw_record: 'str | Path | None' = None) -> 'dict[str, Any]'
+

Compare a mutable descendant with its sealed parent's initial identity.

+

Defined in verifier.artifact_control

+ +
+
+

validate_run_receipt function

+
validate_run_receipt(receipt_path_or_dir: 'Path') -> 'int'
+

Validate one generic-run receipt's required fields and stable canonical digest.

+

Defined in verifier.core.run_validation

+ +
+
+

verify_frozen_artifact function

+
verify_frozen_artifact(bundle: 'str | Path', *, expected_artifact_id: 'str | None' = None, expected_key_id: 'str | None' = None, require_seal: 'bool' = True) -> 'ArtifactVerification'
+

Recompute preserved bytes, write guards, closure, and optional external anchors.

+

Defined in verifier.artifact_control

+ +
+
+

vstd4_depth function

+
vstd4_depth(evidence: 'Mapping[str, str]', *, claim_id: 'str', binding: 'ClaimBinding') -> 'DepthResult'
+

Compute a structural candidate depth from caller-supplied references.

+

Defined in verifier.core.depth

+ +
+
+
+ +
+
+
Wire
+

Canonical schemas and identifiers.

+

Receipt schemas are served from this site at their canonical + $id routes, and their serialized `schema_version` identifiers are listed in the + standard.

+ +
+
+
+ +
VSTD · Apache-2.0 · Reference generated from the implementation by scripts/build_reference.py.
+ + diff --git a/scripts/build_docs.py b/scripts/build_docs.py new file mode 100644 index 0000000..5cd2296 --- /dev/null +++ b/scripts/build_docs.py @@ -0,0 +1,724 @@ +#!/usr/bin/env python3 +"""Terminology: Hypertext Markup Language (HTML); uniform resource locator (URL); +Verifier Standard (VSTD). + +Render repository Markdown into the navigable GitHub Pages documentation site. + +The repository Markdown remains authoritative. This builder changes presentation and +links only; it does not maintain a second hand-edited copy of any specification or guide. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import html +import os +from pathlib import Path, PurePosixPath +import re +from typing import Iterable, Mapping +from urllib.parse import quote + + +ROOT = Path(__file__).resolve().parents[1] +CANONICAL_BASE = "https://timelordraps.github.io/verifier/" +SOURCE_REPOSITORY = "https://github.com/TimeLordRaps/verifier" + + +@dataclass(frozen=True) +class Document: + source: Path + route: PurePosixPath + group: str + title: str + + +@dataclass(frozen=True) +class OrientationDefinition: + concept: str + definition: str + + +ORIENTATION_LINK = re.compile( + r'^\[([^]]+)\]\((https://en\.wikipedia\.org/wiki/[^)\s]+)\s+"Wikipedia orientation;[^"]+"\)$' +) + + +def _plain_markdown(value: str) -> str: + value = re.sub(r"\[([^]]+)\]\([^)]+\)", r"\1", value) + value = value.replace("`", "") + value = re.sub(r"\*\*([^*]+)\*\*", r"\1", value) + value = re.sub(r"~~([^~]+)~~", r"\1", value) + return re.sub(r"\s+", " ", value).strip() + + +def orientation_definitions() -> dict[str, OrientationDefinition]: + """Read the versioned hover-card definitions from the concepts glossary.""" + + definitions: dict[str, OrientationDefinition] = {} + source = ROOT / "docs/CONCEPTS_AND_PRECEDENTS.md" + for line in source.read_text(encoding="utf-8").splitlines(): + if not line.startswith("|"): + continue + cells = [cell.strip() for cell in line.strip().strip("|").split("|")] + if len(cells) != 3: + continue + match = ORIENTATION_LINK.fullmatch(cells[1]) + if not match: + continue + definitions.setdefault( + match.group(2), + OrientationDefinition( + concept=_plain_markdown(cells[0]), + definition=_plain_markdown(cells[2]), + ), + ) + + # Two VSTD concepts deliberately use the same adjacent precedent. The popup + # defines that shared precedent neutrally; each table row retains its own bound. + definitions["https://en.wikipedia.org/wiki/Proof-carrying_code"] = ( + OrientationDefinition( + concept="Proof-carrying code", + definition=( + "An untrusted producer supplies a result with a consumer-checkable " + "certificate under a declared policy. VSTD treats this as an adjacent " + "engineering precedent, not an inherited safety theorem." + ), + ) + ) + if not definitions: + raise ValueError("orientation glossary contains no repository definitions") + return definitions + + +def _first_heading(path: Path) -> str: + for line in path.read_text(encoding="utf-8").splitlines(): + match = re.match(r"^#\s+(.+?)\s*$", line) + if match: + return re.sub(r"[`*_]", "", match.group(1)).strip() + return path.stem.replace("_", " ").replace("-", " ") + + +def _standard_sort(path: Path) -> tuple[int, int, str]: + name = path.stem + if name == "LADDER": + return (0, 0, name) + object_match = re.fullmatch(r"VSTD-(\d+)", name) + if object_match: + return (1, int(object_match.group(1)), name) + graph_match = re.fullmatch(r"VSTD-Graph-(\d+)", name) + if graph_match: + return (2, int(graph_match.group(1)), name) + if name == "ARTIFACT_CONTROL": + return (3, 0, name) + if name == "WIRE_IDENTIFIERS": + return (4, 0, name) + return (5, 0, name) + + +def documents() -> tuple[Document, ...]: + """Return every Markdown source that is intentionally rendered on the site.""" + + found: list[Document] = [] + standards = sorted((ROOT / "standard").glob("*.md"), key=_standard_sort) + for source in standards: + route = ( + PurePosixPath("standard/index.html") + if source.name == "LADDER.md" + else PurePosixPath("standard") / f"{source.stem}.html" + ) + found.append(Document(source, route, "Normative specifications", _first_heading(source))) + + guides = sorted((ROOT / "docs").rglob("*.md")) + for source in guides: + relative = source.relative_to(ROOT).with_suffix(".html") + group = "Guides and concepts" + if "profiles" in relative.parts or "standards" in relative.parts: + group = "Profiles and interoperability" + found.append( + Document( + source, + PurePosixPath(relative.as_posix()), + group, + _first_heading(source), + ) + ) + + experiments = sorted((ROOT / "experiments").rglob("*.md")) + for source in experiments: + relative = source.relative_to(ROOT).with_suffix(".html") + if source.name == "INDEX.md": + relative = Path("experiments/index.html") + found.append( + Document( + source, + PurePosixPath(relative.as_posix()), + "Experiments", + _first_heading(source), + ) + ) + + project_names = ( + "README.md", + "ROADMAP.md", + "GOVERNANCE.md", + "CONTRIBUTING.md", + "SECURITY.md", + "RELEASING.md", + "CODE_OF_CONDUCT.md", + "CHANGELOG.md", + "HUMANS.md", + "AGENTS.md", + "TIME.md", + ) + for name in project_names: + source = ROOT / name + if source.is_file(): + found.append( + Document( + source, + PurePosixPath("project") / f"{source.stem}.html", + "Project and contribution", + _first_heading(source), + ) + ) + return tuple(found) + + +def _slug(value: str) -> str: + plain = re.sub(r"<[^>]*>", "", value) + plain = re.sub(r"[`*_~]", "", plain).lower() + plain = re.sub(r"[^a-z0-9\s-]", "", plain) + return re.sub(r"[-\s]+", "-", plain).strip("-") or "section" + + +def _relative_link(source_route: PurePosixPath, target_route: PurePosixPath) -> str: + return PurePosixPath( + os.path.relpath(target_route.as_posix(), source_route.parent.as_posix()).replace("\\", "/") + ).as_posix() + + +class MarkdownRenderer: + """Small deterministic renderer for the Markdown constructs used in this repository.""" + + FENCE = re.compile(r"^\s*(```+|~~~+)\s*([^\s`]*)\s*$") + HEADING = re.compile(r"^(#{1,6})\s+(.+?)\s*#*\s*$") + LIST_ITEM = re.compile(r"^(\s*)([-+*]|\d+[.)])\s+(.+)$") + TABLE_RULE = re.compile(r"^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$") + HORIZONTAL_RULE = re.compile(r"^\s*(?:-{3,}|\*\s*\*\s*\*|_{3,})\s*$") + INLINE_CODE = re.compile(r"`([^`]+)`") + INLINE_MATH = re.compile(r"(? None: + self.source = source.resolve() + self.route = route + self.route_map = route_map + self.source_ref = "main" if source_ref == "WORKTREE" else source_ref + self.orientation_map = orientation_map + self.heading_counts: dict[str, int] = {} + self.outline: list[tuple[int, str, str]] = [] + + def _repository_link(self, relative: Path, *, directory: bool) -> str: + operation = "tree" if directory else "blob" + ref = quote(self.source_ref, safe="") + encoded = "/".join(quote(part) for part in relative.parts) + return f"{SOURCE_REPOSITORY}/{operation}/{ref}/{encoded}" + + def _target(self, raw: str, *, image: bool = False) -> str: + repository_relative = False + repository_prefix = f"{SOURCE_REPOSITORY}/blob/main/" + if raw.startswith(repository_prefix): + raw = raw.removeprefix(repository_prefix) + repository_relative = True + if raw.startswith(("#", "http://", "https://", "mailto:", "data:")): + return raw + target_text, separator, fragment = raw.partition("#") + base = ROOT if repository_relative else self.source.parent + resolved = (base / target_text).resolve() + if resolved in self.route_map: + rewritten = _relative_link(self.route, self.route_map[resolved]) + else: + try: + relative = resolved.relative_to(ROOT) + except ValueError: + return raw + parts = relative.parts + if parts == ("docs", "reference.html"): + rewritten = _relative_link(self.route, PurePosixPath("reference.html")) + elif parts[:2] == ("docs", "assets"): + asset_route = PurePosixPath(*parts[1:]) + rewritten = _relative_link(self.route, asset_route) + elif ( + parts[:2] in {("receipts", "schema"), ("standard", "schemas")} + and resolved.is_file() + ): + rewritten = _relative_link( + self.route, PurePosixPath("schemas") / relative.name + ) + else: + rewritten = self._repository_link(relative, directory=resolved.is_dir()) + if separator and fragment: + rewritten += "#" + quote(fragment, safe="-._~") + return rewritten + + def inline(self, value: str) -> str: + tokens: list[str] = [] + + def token(rendered: str) -> str: + marker = f"\x00{len(tokens)}\x00" + tokens.append(rendered) + return marker + + def render_link(match: re.Match[str]) -> str: + target = self._target(match.group(2)) + title = match.group(3) + orientation = self.orientation_map.get(match.group(2)) + attributes = [f'href="{html.escape(target, quote=True)}"'] + if orientation and title and title.startswith("Wikipedia orientation"): + attributes.extend( + ( + 'class="orientation-link"', + 'data-orientation-preview="repository"', + f'data-orientation-concept="{html.escape(orientation.concept, quote=True)}"', + f'data-orientation-definition="{html.escape(orientation.definition, quote=True)}"', + f'data-orientation-boundary="{html.escape(title, quote=True)}"', + 'rel="noreferrer"', + ) + ) + elif title: + attributes.append(f'title="{html.escape(title, quote=True)}"') + return f'{self.inline(match.group(1))}' + + value = self.INLINE_CODE.sub( + lambda match: token(f"{html.escape(match.group(1))}"), value + ) + value = self.INLINE_MATH.sub( + lambda match: token( + f'' + f"{html.escape(match.group(1))}" + ), + value, + ) + value = self.IMAGE.sub( + lambda match: token( + f'' + ), + value, + ) + value = self.LINK.sub( + lambda match: token(render_link(match)), + value, + ) + value = html.escape(value, quote=False) + value = re.sub(r"\*\*([^*]+)\*\*", r"\1", value) + value = re.sub(r"__([^_]+)__", r"\1", value) + value = re.sub(r"(?\1", value) + value = re.sub(r"(?\1", value) + value = re.sub(r"~~([^~]+)~~", r"\1", value) + value = re.sub( + r"<(https?://[^&]+)>", + lambda match: f'{match.group(1)}', + value, + ) + for index, rendered in enumerate(tokens): + value = value.replace(f"\x00{index}\x00", rendered) + return value + + @staticmethod + def _table_cells(line: str) -> list[str]: + stripped = line.strip().strip("|") + cells = re.split(r"(? tuple[str, int]: + headings = self._table_cells(lines[index]) + index += 2 + rows: list[list[str]] = [] + while index < len(lines) and "|" in lines[index] and lines[index].strip(): + rows.append(self._table_cells(lines[index])) + index += 1 + head = "".join(f"{self.inline(cell)}" for cell in headings) + body = "" + for row in rows: + padded = row + [""] * max(0, len(headings) - len(row)) + body += "" + "".join( + f"{self.inline(cell)}" for cell in padded[: len(headings)] + ) + "\n" + return ( + '
' + + head + + "\n" + + body + + "
", + index, + ) + + def _render_list(self, lines: list[str], index: int) -> tuple[str, int]: + first = self.LIST_ITEM.match(lines[index]) + assert first is not None + base_indent = len(first.group(1).replace("\t", " ")) + ordered = first.group(2)[0].isdigit() + tag = "ol" if ordered else "ul" + items: list[str] = [] + while index < len(lines): + match = self.LIST_ITEM.match(lines[index]) + if match is None: + break + indent = len(match.group(1).replace("\t", " ")) + is_ordered = match.group(2)[0].isdigit() + if indent != base_indent or is_ordered != ordered: + break + parts = [match.group(3).strip()] + nested: list[str] = [] + index += 1 + while index < len(lines): + next_match = self.LIST_ITEM.match(lines[index]) + if next_match: + next_indent = len(next_match.group(1).replace("\t", " ")) + next_ordered = next_match.group(2)[0].isdigit() + if next_indent == base_indent and next_ordered == ordered: + break + if next_indent <= base_indent: + break + child, index = self._render_list(lines, index) + nested.append(child) + continue + if lines[index].strip() and ( + len(lines[index]) - len(lines[index].lstrip()) > base_indent + ): + parts.append(lines[index].strip()) + index += 1 + continue + break + items.append(self.inline(" ".join(parts)) + "".join(nested)) + return ( + f"<{tag}>" + "".join(f"
  • {item}
  • " for item in items) + f"", + index, + ) + + def render(self, markdown: str) -> str: + lines = markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n") + output: list[str] = [] + paragraph: list[str] = [] + + def flush_paragraph() -> None: + if paragraph: + joined = " ".join(part.strip() for part in paragraph) + output.append(f"

    {self.inline(joined)}

    ") + paragraph.clear() + + index = 0 + while index < len(lines): + line = lines[index] + display_math = re.match(r"^\s*\$\$(.+)\$\$\s*$", line) + if display_math: + flush_paragraph() + output.append( + '
    ' + + html.escape(display_math.group(1).strip()) + + "
    " + ) + index += 1 + continue + fence = self.FENCE.match(line) + if fence: + flush_paragraph() + marker, language = fence.groups() + index += 1 + code: list[str] = [] + while index < len(lines) and not lines[index].lstrip().startswith(marker[:3]): + code.append(lines[index]) + index += 1 + if index < len(lines): + index += 1 + language_class = ( + f' class="language-{html.escape(language, quote=True)}"' if language else "" + ) + output.append( + f"
    {html.escape(chr(10).join(code))}
    " + ) + continue + heading = self.HEADING.match(line) + if heading: + flush_paragraph() + level = len(heading.group(1)) + text = heading.group(2) + anchor = _slug(text) + count = self.heading_counts.get(anchor, 0) + self.heading_counts[anchor] = count + 1 + if count: + anchor = f"{anchor}-{count}" + output.append( + f'{self.inline(text)}' + f'#' + f"" + ) + self.outline.append((level, re.sub(r"[`*_~]", "", text), anchor)) + index += 1 + continue + if ( + index + 1 < len(lines) + and "|" in line + and self.TABLE_RULE.match(lines[index + 1]) + ): + flush_paragraph() + table, index = self._render_table(lines, index) + output.append(table) + continue + if self.LIST_ITEM.match(line): + flush_paragraph() + rendered_list, index = self._render_list(lines, index) + output.append(rendered_list) + continue + if line.lstrip().startswith(">"): + flush_paragraph() + quote_lines: list[str] = [] + while index < len(lines) and lines[index].lstrip().startswith(">"): + quote_lines.append(re.sub(r"^\s*>\s?", "", lines[index])) + index += 1 + quoted = MarkdownRenderer( + self.source, + self.route, + self.route_map, + self.source_ref, + self.orientation_map, + ).render("\n".join(quote_lines)) + output.append(f"
    {quoted}
    ") + continue + if self.HORIZONTAL_RULE.match(line): + flush_paragraph() + output.append("
    ") + index += 1 + continue + if line.startswith(" "): + flush_paragraph() + code = [] + while index < len(lines) and (lines[index].startswith(" ") or not lines[index]): + code.append(lines[index][4:] if lines[index].startswith(" ") else "") + index += 1 + output.append(f"
    {html.escape(chr(10).join(code).rstrip())}
    ") + continue + if re.match(r"^\s*

    ' + ) + index += 1 + continue + if re.match(r"^\s*]*)?>\s*$", line, re.IGNORECASE): + flush_paragraph() + index += 1 + continue + if not line.strip(): + flush_paragraph() + index += 1 + continue + paragraph.append(line) + index += 1 + flush_paragraph() + return "\n".join(output) + + +def _canonical(route: PurePosixPath) -> str: + value = route.as_posix() + if value.endswith("/index.html"): + value = value[: -len("index.html")] + return CANONICAL_BASE + value + + +def _top_navigation(route: PurePosixPath, *, current: str) -> str: + prefix = _relative_link(route, PurePosixPath("index.html")) + root = prefix.removesuffix("index.html") + links = ( + ("Overview", root + "index.html", "overview"), + ("Guides", root + "guides.html", "guides"), + ("Reference", root + "reference.html", "reference"), + ("Demo", "https://github.com/TimeLordRaps/verifier#30-60-second-demonstration", "demo"), + ("Standard", root + "standard/", "standard"), + ("Experiments", root + "experiments/", "experiments"), + ("Project", root + "project/ROADMAP.html", "project"), + ("GitHub", "https://github.com/TimeLordRaps/verifier", "github"), + ) + rendered = [] + for label, target, key in links: + marker = ' aria-current="page"' if key == current else "" + rendered.append(f'{label}') + return ( + '
    ' + ) + + +def _sidebar( + current: Document, + all_documents: Iterable[Document], + outline: Iterable[tuple[int, str, str]], +) -> str: + groups: dict[str, list[Document]] = {} + for document in all_documents: + groups.setdefault(document.group, []).append(document) + sections: list[str] = [] + on_this_page = [entry for entry in outline if entry[0] in {2, 3}] + if on_this_page: + links = "".join( + f'
  • {html.escape(title)}
  • ' + for level, title, anchor in on_this_page + ) + sections.append(f'

    On this page

      {links}
    ') + for group, entries in groups.items(): + links = [] + for document in entries: + target = _relative_link(current.route, document.route) + marker = ' aria-current="page"' if document.route == current.route else "" + links.append( + f'
  • {html.escape(document.title)}
  • ' + ) + sections.append( + f'

    {html.escape(group)}

      {"".join(links)}
    ' + ) + return ( + '" + ) + + +def render_document( + document: Document, + all_documents: tuple[Document, ...], + *, + source_ref: str, + orientation_map: Mapping[str, OrientationDefinition], +) -> str: + route_map = {item.source.resolve(): item.route for item in all_documents} + markdown = document.source.read_text(encoding="utf-8") + renderer = MarkdownRenderer( + document.source, document.route, route_map, source_ref, orientation_map + ) + content = renderer.render(markdown) + source_relative = document.source.relative_to(ROOT).as_posix() + source_link = renderer._repository_link(Path(source_relative), directory=False) + if document.group == "Normative specifications": + current = "standard" + elif document.group == "Experiments": + current = "experiments" + elif document.group == "Project and contribution": + current = "project" + else: + current = "guides" + if document.group == "Normative specifications": + status = "Normative source" + elif document.group == "Experiments": + status = "Non-normative experiment record" + else: + status = "Maintained repository documentation" + orientation_script = "" + if 'data-orientation-preview="repository"' in content: + script_source = _relative_link( + document.route, PurePosixPath("assets/orientation-previews.js") + ) + orientation_script = f'' + return f""" + + + + + + + + + {html.escape(document.title)} — VSTD documentation + + + + + + {_top_navigation(document.route, current=current)} +
    + {_sidebar(document, all_documents, renderer.outline)} +
    + +

    Rendered from {html.escape(source_relative)} at build time without changing its status. The repository source controls if this presentation differs.

    + {content} +
    +
    +
    VSTD · Apache-2.0 · Maintainer-led alpha · No standards-body endorsement claimed.
    + {orientation_script} + + +""" + + +def build(output: Path, *, source_ref: str = "WORKTREE") -> tuple[Path, ...]: + """Render the documentation into an existing Pages output directory.""" + + output = output.resolve() + if not output.is_dir(): + raise ValueError(f"documentation output directory does not exist: {output}") + all_documents = documents() + orientation_map = orientation_definitions() + used_orientation_urls = { + match.group(2) + for document in all_documents + for line in document.source.read_text(encoding="utf-8").splitlines() + for match in MarkdownRenderer.LINK.finditer(line) + if match.group(2).startswith("https://en.wikipedia.org/wiki/") + and (match.group(3) or "").startswith("Wikipedia orientation") + } + missing = sorted(used_orientation_urls - orientation_map.keys()) + if missing: + raise ValueError( + "orientation links lack repository definitions: " + ", ".join(missing) + ) + written: list[Path] = [] + targets = [output / Path(document.route.as_posix()) for document in all_documents] + existing = [target for target in targets if target.exists()] + if existing: + names = ", ".join(target.relative_to(output).as_posix() for target in existing[:3]) + raise ValueError(f"refusing to overwrite generated documentation: {names}") + for document, target in zip(all_documents, targets): + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text( + render_document( + document, + all_documents, + source_ref=source_ref, + orientation_map=orientation_map, + ), + encoding="utf-8", + newline="\n", + ) + written.append(target) + return tuple(written) + + +def main() -> int: + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--source-ref", default="WORKTREE") + args = parser.parse_args() + written = build(args.output, source_ref=args.source_ref) + print(f"[DOCS OK] rendered {len(written)} navigable pages") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/build_pages.py b/scripts/build_pages.py index eb8a241..4d45832 100644 --- a/scripts/build_pages.py +++ b/scripts/build_pages.py @@ -1,25 +1,69 @@ #!/usr/bin/env python3 -"""Assemble the exact GitHub Pages artifact without duplicating schema sources.""" +"""Terminology: uniform resource locator (URL). + +Assemble the exact GitHub Pages artifact without duplicating schema sources. +""" from __future__ import annotations import argparse +import importlib.util import json from pathlib import Path +import re import shutil +import sys ROOT = Path(__file__).resolve().parents[1] DOCS = ROOT / "docs" -SCHEMAS = ROOT / "receipts/schema" +SCHEMA_SOURCES = (ROOT / "receipts/schema", ROOT / "standard/schemas") PUBLIC_SCHEMA_PREFIX = "https://timelordraps.github.io/verifier/schemas/" +CANONICAL_BASE_URL = "https://timelordraps.github.io/verifier/" class PagesBuildError(RuntimeError): pass -def build(output: Path) -> tuple[Path, ...]: +def _build_documentation(output: Path, *, source_ref: str) -> tuple[Path, ...]: + path = ROOT / "scripts/build_docs.py" + spec = importlib.util.spec_from_file_location("vstd_build_docs", path) + if spec is None or spec.loader is None: + raise PagesBuildError("cannot load scripts/build_docs.py") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + try: + spec.loader.exec_module(module) + return module.build(output, source_ref=source_ref) + except Exception as exc: + raise PagesBuildError(f"documentation rendering failed: {exc}") from exc + finally: + sys.modules.pop(spec.name, None) + + +def _documentation_coordinate(source_ref: str) -> dict[str, str | int]: + project = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + version_match = re.search(r'^version\s*=\s*"([^"]+)"\s*$', project, re.MULTILINE) + if version_match is None: + raise PagesBuildError("project version is not readable") + version = version_match.group(1) + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + heading = re.search(rf"^## {re.escape(version)} - (.+)$", changelog, re.MULTILINE) + if heading is None: + raise PagesBuildError(f"changelog has no coordinate for version {version}") + release_state = "UNRELEASED_CANDIDATE" if heading.group(1) == "UNRELEASED" else "RELEASED" + return { + "schema_version": 1, + "documentation_version": version, + "release_state": release_state, + "source_ref": source_ref, + "canonical_base_url": CANONICAL_BASE_URL, + "normative_source": "standard/", + } + + +def build(output: Path, *, source_ref: str = "WORKTREE") -> tuple[Path, ...]: """Build into a new or empty directory and return every copied schema path.""" output = output.resolve() if output == ROOT: @@ -33,7 +77,13 @@ def build(output: Path) -> tuple[Path, ...]: schema_output = output / "schemas" schema_output.mkdir() copied: list[Path] = [] - for source in sorted(SCHEMAS.glob("*.json")): + sources = sorted( + (source for directory in SCHEMA_SOURCES for source in directory.glob("*.json")), + key=lambda path: path.name, + ) + if len({source.name for source in sources}) != len(sources): + raise PagesBuildError("public schema source names must be unique") + for source in sources: payload = json.loads(source.read_text(encoding="utf-8")) schema_id = payload.get("$id", "") expected_id = PUBLIC_SCHEMA_PREFIX + source.name @@ -47,15 +97,26 @@ def build(output: Path) -> tuple[Path, ...]: if not copied: raise PagesBuildError("no public schemas were assembled") + coordinate = _documentation_coordinate(source_ref) + (output / "documentation-coordinate.json").write_text( + json.dumps(coordinate, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + newline="\n", + ) + _build_documentation(output, source_ref=source_ref) return tuple(copied) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--source-ref", default="WORKTREE") args = parser.parse_args(argv) - copied = build(args.output) - print(f"[PAGES OK] site assembled with {len(copied)} schema routes") + copied = build(args.output, source_ref=args.source_ref) + print( + f"[PAGES OK] site assembled with {len(copied)} schema routes " + "and navigable documentation" + ) return 0 diff --git a/scripts/build_reference.py b/scripts/build_reference.py new file mode 100644 index 0000000..57dac5f --- /dev/null +++ b/scripts/build_reference.py @@ -0,0 +1,480 @@ +#!/usr/bin/env python3 +"""Terminology: application programming interface (API); command-line interface (CLI); +hash-based message authentication code (HMAC); International Organization for Standardization (ISO); +JavaScript Object Notation (JSON); Secure Hash Algorithm 256-bit (SHA-256); +Verifier Standard (VSTD); YAML Ain't Markup Language (YAML). + +Generate the public CLI and top-level API reference page from the live implementation. + +Nothing on the generated page is hand-written prose about behaviour: every command, +option, top-level export, signature, and listed pipeline edge is read out of the +importable package at build time. `scripts/check_presentation.py` and +`tests/test_presentation_surface.py` regenerate this file and fail closed when the +committed page drifts from the code.""" + +from __future__ import annotations + +import argparse +import enum +import html +import importlib +import inspect +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = ROOT / "src" +if str(SOURCE_ROOT) not in sys.path: + sys.path.insert(0, str(SOURCE_ROOT)) +OUTPUT = ROOT / "docs/reference.html" +SOURCE_BASE = "https://github.com/TimeLordRaps/verifier/blob/main/" + +# command -> the declared implementation stages it dispatches into. Every target is +# imported during generation, so a rename or removal breaks the build rather than +# silently publishing a stale pipeline map. +PIPELINE: tuple[tuple[str, str, tuple[str, ...]], ...] = ( + ( + "vstd demo", + "Runs the four adversarial specimens in-process and reports whether each " + "defensive outcome matched its declared invariant.", + ("verifier.runtime.demo:run_demo", "verifier.runtime.demo:demo_report"), + ), + ( + "vstd plan", + "Resolves a manifest's command and declared paths without executing anything.", + ( + "verifier.core.run_planning:load_manifest", + "verifier.core.run_planning:describe_run_plan", + ), + ), + ( + "vstd run", + "Executes a trusted manifest without sandboxing, captures the observed " + "execution, and writes a canonically digested receipt.", + ( + "verifier.core.run_planning:load_manifest", + "verifier.core.run:capture_run", + "verifier.core.receipt:compute_canonical_digest", + ), + ), + ( + "vstd validate", + "Dispatches on the receipt's serialized `schema_version` identifier and runs its implemented " + "checks. Generic-run validation enforces its required structure and stable " + "digest; other receipt kinds enforce their separately documented structure " + "and evidence rules.", + ( + "verifier.core.run_validation:validate_run_receipt", + "verifier.data.receipt:validate_data_receipt", + "verifier.hardware.validation:validate_vstd3_receipt", + ), + ), + ( + "vstd inspect", + "Prints the claim coordinate, digest, and verdict surface of a stored receipt.", + ( + "verifier.core.run_inspection:inspect_run_receipt", + "verifier.hardware.receipt:load_vstd3_receipt", + ), + ), + ( + "vstd reproduce", + "Replays only the mechanisms a stored receipt actually carries; physical " + "hardware execution is refused rather than simulated.", + ( + "verifier.core.run_reproduction:reproduce_run_receipt", + "verifier.data.receipt:reproduce_data_receipt", + ), + ), + ( + "vstd impact", + "Finds stored run receipts whose recorded ancestry reaches a revoked " + "provenance artifact.", + ("verifier.core.run_impact:find_run_receipts_impacted_by_revocation",), + ), + ( + "vstd data", + "Traces, renders, or exports the provenance hypergraph carried by a " + "VSTD-Graph receipt.", + ("verifier.data.models:ProvenanceHypergraph",), + ), + ( + "vstd artifact", + "Freezes exact regular-file bytes, adds or verifies finite self-closing " + "seals, and creates observable copy-on-write thaw descendants.", + ( + "verifier.artifact_control:freeze_artifact", + "verifier.artifact_control:seal_artifact", + "verifier.artifact_control:verify_frozen_artifact", + "verifier.artifact_control:thaw_artifact", + ), + ), + ( + "vstd experiment", + "Validates experimental workflow manifests or maps normalized GitHub snapshots " + "without granting a VSTD verdict.", + ( + "verifier.runtime.experimental_workflow_cli:handle_experiment_command", + "verifier.experimental_workflow.profile:load_manifest", + "verifier.experimental_workflow.github:github_snapshot_to_events", + ), + ), + ( + "vstd hardware / continuity / fleet / evidence / claims", + "Evaluates VSTD-3 substrate-accountability receipts, their continuity and " + "fleet evidence, and their declared claims.", + ( + "verifier.runtime.hardware_cli:handle_vstd3_command", + "verifier.hardware.validation:validate_vstd3_receipt", + ), + ), +) + + +class ReferenceBuildError(RuntimeError): + pass + + +def _resolve(target: str) -> tuple[object, str]: + module_name, _, attribute = target.partition(":") + try: + module = importlib.import_module(module_name) + except ImportError as exc: + raise ReferenceBuildError(f"pipeline target is not importable: {target}: {exc}") from exc + if not hasattr(module, attribute): + raise ReferenceBuildError(f"pipeline target no longer exists: {target}") + return getattr(module, attribute), module_name + + +def _source_link(module_name: str) -> str: + relative = "src/" + module_name.replace(".", "/") + ".py" + if not (ROOT / relative).is_file(): + package_relative = "src/" + module_name.replace(".", "/") + "/__init__.py" + if not (ROOT / package_relative).is_file(): + raise ReferenceBuildError(f"cannot locate source file for {module_name}") + relative = package_relative + return SOURCE_BASE + relative + + +def _summary(obj: object) -> str: + doc = inspect.getdoc(obj) or "" + return doc.split("\n\n", 1)[0].strip().replace("\n", " ") + + +def _esc(text: str) -> str: + return html.escape(text, quote=False) + + +def _subparser_actions(parser: argparse.ArgumentParser) -> list[argparse._SubParsersAction]: + return [ + action + for action in parser._actions # noqa: SLF001 - argparse exposes no public walk + if isinstance(action, argparse._SubParsersAction) # noqa: SLF001 + ] + + +def _walk(parser: argparse.ArgumentParser, help_text: str = "") -> list[dict[str, object]]: + arguments: list[dict[str, str]] = [] + for action in parser._actions: # noqa: SLF001 + if isinstance(action, argparse._SubParsersAction) or action.dest == "help": # noqa: SLF001 + continue + name = ", ".join(action.option_strings) if action.option_strings else ( + action.metavar or action.dest + ) + choices = "" + if action.choices: + choices = "one of: " + ", ".join(str(choice) for choice in action.choices) + arguments.append( + { + "name": str(name), + "kind": "optional" if action.option_strings else "positional", + "choices": choices, + "default": "" if action.default in (None, False, [], "") else str(action.default), + "help": action.help or "", + } + ) + commands: list[dict[str, object]] = [ + {"prog": parser.prog, "help": help_text, "arguments": arguments} + ] + for action in _subparser_actions(parser): + help_by_name = { + choice.dest: choice.help or "" for choice in action._choices_actions # noqa: SLF001 + } + for name, subparser in action.choices.items(): + commands.extend(_walk(subparser, help_by_name.get(name, ""))) + return commands + + +def _cli_section() -> str: + from verifier.runtime.public_cli import build_parser + + blocks: list[str] = [] + for command in _walk(build_parser()): + prog = str(command["prog"]) + anchor = "cli-" + prog.replace(" ", "-") + rows = "" + for argument in command["arguments"]: # type: ignore[union-attr] + detail = " ".join( + part + for part in ( + argument["help"], + f"({argument['choices']})" if argument["choices"] else "", + f"[default: {argument['default']}]" if argument["default"] else "", + ) + if part + ) + rows += ( + f"{_esc(argument['name'])}" + f"{_esc(argument['kind'])}" + f"{_esc(detail)}\n" + ) + table = ( + "" + f"\n{rows}
    ArgumentKindMeaning
    " + if rows + else '

    No arguments; this command only groups subcommands.

    ' + ) + help_text = str(command["help"]) or "Subcommand group." + blocks.append( + f'
    \n' + f"

    {_esc(prog)}

    \n" + f'

    {_esc(help_text)}

    \n' + f"{table}\n
    " + ) + return "\n".join(blocks) + + +def _api_section() -> str: + package = importlib.import_module("verifier") + blocks: list[str] = [] + for name in sorted(package.__all__): + value = getattr(package, name) + module_name = value.__module__ + if inspect.isclass(value): + kind = "enum" if issubclass(value, enum.Enum) else "class" + elif inspect.isfunction(value): + kind = "function" + else: + kind = type(value).__name__ + signature = "" + if kind != "enum": + try: + signature = f"{name}{inspect.signature(value)}" + except (TypeError, ValueError): + signature = name + members = "" + if kind == "enum": + values = ", ".join(member.name for member in value) + members = f'

    Members: {_esc(values)}

    ' + elif kind == "class": + rows = "" + for member_name, member in sorted(inspect.getmembers(value, inspect.isfunction)): + if member_name.startswith("_"): + continue + try: + member_signature = f"{member_name}{inspect.signature(member)}" + except (TypeError, ValueError): + member_signature = member_name + rows += ( + f"{_esc(member_signature)}" + f"{_esc(_summary(member))}\n" + ) + if rows: + members = ( + "" + f"\n{rows}
    MethodSummary
    " + ) + # ``str, Enum`` can inherit a version-specific builtin ``str`` docstring when + # no class docstring is declared. Never publish that as VSTD documentation. + summary = _summary(value) + if kind == "enum" and (not summary or summary.startswith("str(")): + summary = "Enumeration of the exported result values." + if not summary or summary.startswith(f"{name}("): + # A dataclass with no docstring of its own repeats its signature; that is + # not documentation, so say so instead of publishing the repetition. + summary = ( + "No docstring is declared for this export; the signature above is its " + "whole declared surface." + ) + signature_html = ( + f'
    {_esc(signature)}
    \n' + if signature + else "" + ) + blocks.append( + f'
    \n' + f'

    {_esc(name)} {_esc(kind)}

    \n' + + signature_html + + f'

    {_esc(summary)}

    \n' + f'

    Defined in ' + f"{_esc(module_name)}

    \n" + f"{members}\n
    " + ) + return "\n".join(blocks) + + +def _pipeline_section() -> str: + rows = "" + for command, description, targets in PIPELINE: + links = [] + for target in targets: + _, module_name = _resolve(target) + links.append(f'{_esc(target)}') + rows += ( + f"{_esc(command)}{_esc(description)}" + f"{'
    '.join(links)}\n" + ) + return ( + "" + f"\n{rows}
    CommandWhat it doesImplementation entry points
    " + ) + + +def render() -> str: + package = importlib.import_module("verifier") + version = package.__version__ + standard = package.__standard__ + standard_status = package.__standard_status__ + return f""" + + + + + + + + + + + VSTD docs — command-line interface (CLI) and application programming interface (API) reference + + + + + + +
    + +
    + +
    +
    +
    Reference · verifier-standard {_esc(version)} · {_esc(standard)} {_esc(standard_status)}
    +

    Inspect the whole pipeline.

    +

    Terms used below: hash-based message authentication + code (HMAC); International Organization for Standardization (ISO); JavaScript Object + Notation (JSON); Secure Hash Algorithm 256-bit (SHA-256); and YAML Ain't Markup Language + (YAML).

    +

    Every command, argument, top-level export, and listed dispatch edge below + is read out of the installed package when this page is built, by + scripts/build_reference.py, and the presentation tests fail closed when the + committed page drifts — so it cannot describe behaviour the implementation no + longer has.

    +

    This page states the declared public surface of one implementation. It + does not establish that any individual claim checked by these commands is true, nor that + an external implementation exists.

    + +
    + +
    +
    +
    Pipeline
    +

    Command to implementation, without a gap.

    +

    Each entry point below is imported while this page is built. A + rename, move, or deletion fails the build instead of publishing a stale map.

    +
    {_pipeline_section()}
    +
    +
    + +
    +
    +
    CLI
    +

    The vstd command reference.

    +

    Extracted from the live argument parser in + verifier.runtime.public_cli. + vstd is the canonical cross-platform command; verifier is + retained as an alias only on platforms where it is unambiguous.

    +
    {_cli_section()}
    +
    +
    + +
    +
    +
    API
    +

    Top-level Python exports.

    +

    The names in verifier.__all__, with their live + signatures and declared docstrings, are the supported runtime surface under the + Python API stability policy. + Subpackage imports are internal unless a published policy names them.

    +
    {_api_section()}
    +
    +
    + +
    +
    +
    Wire
    +

    Canonical schemas and identifiers.

    +

    Receipt schemas are served from this site at their canonical + $id routes, and their serialized `schema_version` identifiers are listed in the + standard.

    + +
    +
    +
    + +
    VSTD · Apache-2.0 · Reference generated from the implementation by scripts/build_reference.py.
    + + +""" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--check", + action="store_true", + help="Fail instead of writing when the committed page is out of date.", + ) + args = parser.parse_args(argv) + rendered = render() + if args.check: + current = OUTPUT.read_text(encoding="utf-8") if OUTPUT.exists() else "" + if current != rendered: + print( + "[REFERENCE DRIFT] docs/reference.html is stale; " + "run python scripts/build_reference.py", + file=sys.stderr, + ) + return 1 + print("[REFERENCE OK] docs/reference.html matches the implementation") + return 0 + OUTPUT.write_text(rendered, encoding="utf-8") + print(f"[REFERENCE OK] wrote {OUTPUT.relative_to(ROOT).as_posix()}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_acronyms.py b/scripts/check_acronyms.py new file mode 100644 index 0000000..efaff61 --- /dev/null +++ b/scripts/check_acronyms.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Terminology: Verifier Standard (VSTD). + +Enforce newcomer-readable acronym expansion across Verifier Standard (VSTD) prose.""" + +from __future__ import annotations + +from pathlib import Path +import re +import sys + + +ROOT = Path(__file__).resolve().parents[1] +GLOSSARY = ROOT / "docs" / "ACRONYMS.md" +GLOSSARY_ROW = re.compile(r"^\| `([^`]+)` \| ([^|]+?) \|", re.MULTILINE) +SOURCE_SUFFIXES = {".py", ".rs", ".sh"} +DOCUMENT_SUFFIXES = {".cff", ".html", ".md", ".svg"} +IGNORED_PARTS = {".git", ".pytest_cache", ".venv", "build", "dist", "__pycache__"} + + +def load_expansions() -> dict[str, str]: + """Read the one canonical acronym key used by prose and the checker.""" + + text = GLOSSARY.read_text(encoding="utf-8") + expansions = {term: expansion.strip() for term, expansion in GLOSSARY_ROW.findall(text)} + if not expansions or "VSTD" not in expansions: + raise ValueError("docs/ACRONYMS.md has no parseable VSTD expansion table") + return expansions + + +def _is_schema(path: Path) -> bool: + relative = path.relative_to(ROOT).as_posix() + return ( + relative.startswith("receipts/schema/") and path.suffix == ".json" + ) or relative.endswith(".schema.json") + + +def _is_issue_form(path: Path) -> bool: + relative = path.relative_to(ROOT).as_posix() + return relative.startswith(".github/ISSUE_TEMPLATE/") and path.suffix in {".yml", ".yaml"} + + +def public_reader_files() -> list[Path]: + """Return standalone prose and source surfaces, excluding generated dependency data.""" + + files: list[Path] = [] + for path in ROOT.rglob("*"): + if not path.is_file() or any( + part in IGNORED_PARTS for part in path.relative_to(ROOT).parts + ): + continue + if path == GLOSSARY: + continue + if ( + path.suffix.lower() in SOURCE_SUFFIXES | DOCUMENT_SUFFIXES + or _is_schema(path) + or _is_issue_form(path) + or path.name == ".zenodo.json" + ): + files.append(path) + return sorted(files) + + +def _term_pattern(term: str) -> re.Pattern[str]: + return re.compile( + rf"(? re.Pattern[str]: + """Match a definition even when Markdown wraps it across physical lines.""" + + words = re.split(r"\s+", expansion.strip()) + expanded = r"\s+".join(re.escape(word) for word in words) + return re.compile(rf"{expanded}\s+\({re.escape(term)}\)") + + +def _required_terms(text: str, expansions: dict[str, str]) -> set[str]: + return { + term for term in expansions if _term_pattern(term).search(text) is not None + } + + +def validate_repo() -> list[str]: + """Return every missing or late first-use expansion.""" + + expansions = load_expansions() + errors: list[str] = [] + for path in public_reader_files(): + text = path.read_text(encoding="utf-8") + for term in sorted(_required_terms(text, expansions)): + definition = f"{expansions[term]} ({term})" + definition_match = _definition_pattern(expansions[term], term).search(text) + definition_at = -1 if definition_match is None else definition_match.start() + first = _term_pattern(term).search(text) + if definition_at < 0: + errors.append( + f"{path.relative_to(ROOT).as_posix()}: {term} is not expanded as " + f"{definition!r}" + ) + elif first is not None and definition_at > first.start(): + line = text.count("\n", 0, first.start()) + 1 + errors.append( + f"{path.relative_to(ROOT).as_posix()}:{line}: {term} appears before " + "its expansion" + ) + return errors + + +def main() -> int: + errors = validate_repo() + if errors: + for error in errors: + print(f"[ACRONYM FAIL] {error}", file=sys.stderr) + return 1 + print("[ACRONYM OK] registered terms are expanded at first use") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_external_links.py b/scripts/check_external_links.py new file mode 100644 index 0000000..fac9720 --- /dev/null +++ b/scripts/check_external_links.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Audit external Hypertext Transfer Protocol (HTTP) documentation links.""" + +from __future__ import annotations + +import argparse +from concurrent.futures import ThreadPoolExecutor +from dataclasses import asdict, dataclass +from datetime import datetime, timezone +from html.parser import HTMLParser +import json +from pathlib import Path +import re +import time +from typing import Iterable +from urllib.error import HTTPError, URLError +from urllib.parse import urldefrag +from urllib.request import Request, urlopen + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_ALLOWLIST = ROOT / ".github/external-links-allowlist.txt" +MARKDOWN_LINK = re.compile(r"!?\[[^\]]*\]\((https?://[^)\s]+)(?:\s+[^)]*)?\)") + + +class _HtmlLinks(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.links: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag not in {"a", "img", "link", "script"}: + return + attributes = dict(attrs) + value = attributes.get("href") or attributes.get("src") + if value and value.startswith(("http://", "https://")): + self.links.append(value) + + +@dataclass(frozen=True) +class Result: + url: str + status: str + detail: str + + +def collect_links(paths: Iterable[Path]) -> tuple[str, ...]: + links: set[str] = set() + for path in paths: + text = path.read_text(encoding="utf-8") + if path.suffix.lower() == ".html": + parser = _HtmlLinks() + parser.feed(text) + links.update(parser.links) + elif path.suffix.lower() == ".md": + links.update(match.group(1) for match in MARKDOWN_LINK.finditer(text)) + return tuple(sorted({urldefrag(link)[0] for link in links})) + + +def documentation_paths(root: Path) -> tuple[Path, ...]: + excluded = {".git", "build", "dist", "artifacts_tmp"} + return tuple( + path + for path in sorted(root.rglob("*")) + if path.is_file() + and path.suffix.lower() in {".html", ".md"} + and not excluded.intersection(path.relative_to(root).parts) + ) + + +def read_allowlist(path: Path) -> tuple[tuple[str, str], ...]: + entries: list[tuple[str, str]] = [] + for number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if not raw or raw.startswith("#"): + continue + try: + pattern, reason = raw.split("\t", 1) + except ValueError as exc: + raise ValueError(f"{path}:{number}: allowlist entry needs a tab and reason") from exc + if not pattern.startswith(("http://", "https://")) or not reason.strip(): + raise ValueError(f"{path}:{number}: invalid allowlist entry") + entries.append((pattern, reason.strip())) + return tuple(entries) + + +def allowlist_reason(url: str, entries: Iterable[tuple[str, str]]) -> str | None: + for pattern, reason in entries: + if (pattern.endswith("*") and url.startswith(pattern[:-1])) or url == pattern: + return reason + return None + + +def _request(url: str, method: str, timeout: float) -> int: + headers = {"User-Agent": "TimeLordRaps-verifier-link-audit/1.0"} + if method == "GET": + headers["Range"] = "bytes=0-0" + request = Request(url, headers=headers, method=method) + with urlopen(request, timeout=timeout) as response: + return int(response.status) + + +def probe(url: str, *, retries: int, timeout: float) -> Result: + last = "no attempt" + for attempt in range(retries + 1): + try: + return Result(url, "OK", str(_request(url, "HEAD", timeout))) + except HTTPError as exc: + if exc.code in {403, 405}: + try: + return Result(url, "OK", str(_request(url, "GET", timeout))) + except (HTTPError, URLError, TimeoutError, OSError) as get_exc: + last = str(get_exc) + else: + last = str(exc) + except (URLError, TimeoutError, OSError) as exc: + last = str(exc) + if attempt < retries: + time.sleep(2**attempt) + return Result(url, "FAILED", last) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--allowlist", type=Path, default=DEFAULT_ALLOWLIST) + parser.add_argument("--report", type=Path, default=Path("external-links.json")) + parser.add_argument("--retries", type=int, default=2) + parser.add_argument("--timeout", type=float, default=15.0) + parser.add_argument("--workers", type=int, default=8) + args = parser.parse_args(argv) + + entries = read_allowlist(args.allowlist) + pending: list[str] = [] + results: list[Result] = [] + for url in collect_links(documentation_paths(ROOT)): + reason = allowlist_reason(url, entries) + if reason: + results.append(Result(url, "ALLOWLISTED", reason)) + else: + pending.append(url) + with ThreadPoolExecutor(max_workers=args.workers) as executor: + results.extend( + executor.map( + lambda url: probe(url, retries=args.retries, timeout=args.timeout), pending + ) + ) + results.sort(key=lambda result: result.url) + payload = { + "schema_version": 1, + "checked_at_utc": datetime.now(timezone.utc).isoformat(), + "results": [asdict(result) for result in results], + } + args.report.write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8", newline="\n" + ) + failures = [result for result in results if result.status == "FAILED"] + print(f"[LINK AUDIT] checked={len(results)} failed={len(failures)}") + for result in failures: + print(f"[FAILED] {result.url}: {result.detail}") + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_presentation.py b/scripts/check_presentation.py index 8c70c8c..71ea31d 100644 --- a/scripts/check_presentation.py +++ b/scripts/check_presentation.py @@ -1,9 +1,15 @@ #!/usr/bin/env python3 -"""Fail closed when public presentation surfaces drift from executable truth.""" +"""Terminology: artificial intelligence (AI); application programming interface (API); +Amazon Web Services (AWS); Concise Binary Object Representation (CBOR); CBOR Object Signing and +Encryption (COSE); command-line interface (CLI); Supply Chain Integrity, Transparency, and +Trust (SCITT); reduced instruction set computer (RISC); Verifier Standard (VSTD). + +Fail closed when public presentation surfaces drift from executable truth.""" from __future__ import annotations from html.parser import HTMLParser +import importlib.util import json from pathlib import Path import re @@ -29,6 +35,12 @@ ".yaml", ".yml", } +RETIRED_SURFACES = ( + "VSTD-" + "0.1", + "VSTD-" + "0.2", + "layer" + "4_binding", + "vstd" + "4_conformance", +) MARKDOWN_LINK = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)") LOCAL_WINDOWS_PATH = re.compile( r"(?i)(?:[A-Za-z]:[\\/](?:Users|Documents and Settings)[\\/]|" @@ -79,18 +91,83 @@ re.compile(r"(?i)\bcausally\s+contribut(?:e|ed|es|ing)\b"), ), ) +CURRENT_TIME_STATUS = re.compile( + r"(?i)\bTIME(?:\.md)?`?\s+(?:is|=|==|has\s+status|status\s*(?:is|=|:))\s+" + r"(?:`?Status:\s*)?`?(?:CLEAR|OPEN)\b" +) +CURRENT_FACING_SURFACES = ( + "README.md", + "docs/CLAIMS_AND_LIMITS.md", + "docs/QUICKSTART.md", + "docs/guides.html", + "docs/index.html", +) +MATURITY_CONFORMANCE = { + "VSTD-1": "Implemented reference subset", + "VSTD-2": "Implemented vertical slice", + "VSTD-3": "Implemented reference surface", + "VSTD-4": "`NOT_ESTABLISHED`", + "VSTD-5": "Not implemented", + "VSTD-Graph-1": "Implemented reference subset", + "VSTD-Graph-2": "`NOT_ESTABLISHED`", + "VSTD-Graph-3": "`NOT_ESTABLISHED`", + "VSTD-Graph-4": "`NOT_ESTABLISHED`", + "VSTD-Graph-5": "`NOT_ESTABLISHED`", + "Generic run": "Implemented VSTD-1 profile", + "Experimental workflow": "No VSTD conformance claim", + "Supply Chain Integrity, Transparency, and Trust (SCITT) interoperability": ( + "VSTD-4 remains `NOT_ESTABLISHED`" + ), + "zero-identity/zero-knowledge (ZIZK) artifact-first TRUST": ( + "Governing architectural invariant; not a separate VSTD conformance result" + ), + "RISC Zero proof-carrying reference mechanism": ( + "Native proof verified; no VSTD receipt mapping" + ), +} class LinkCollector(HTMLParser): def __init__(self) -> None: super().__init__() self.links: list[str] = [] + self.html_lang = "" + self.has_viewport = False + self.in_title = False + self.title = "" + self.main_ids: list[str] = [] + self.skip_targets: list[str] = [] + self.images_without_alt = 0 + self.unlabelled_navs = 0 def handle_starttag(self, tag: str, attrs) -> None: + attributes = dict(attrs) + if tag == "html": + self.html_lang = attributes.get("lang", "") + elif tag == "meta" and attributes.get("name") == "viewport": + self.has_viewport = True + elif tag == "title": + self.in_title = True + elif tag == "main": + self.main_ids.append(attributes.get("id", "")) + elif tag == "a" and "skip-link" in attributes.get("class", "").split(): + self.skip_targets.append(attributes.get("href", "")) + elif tag == "img" and "alt" not in attributes: + self.images_without_alt += 1 + elif tag == "nav" and not attributes.get("aria-label"): + self.unlabelled_navs += 1 for name, value in attrs: if name in {"href", "src"} and value: self.links.append(value) + def handle_endtag(self, tag: str) -> None: + if tag == "title": + self.in_title = False + + def handle_data(self, data: str) -> None: + if self.in_title: + self.title += data + def _public_files() -> list[Path]: return sorted( @@ -112,7 +189,24 @@ def _local_target(source: Path, raw: str) -> Path | None: return (source.parent / relative).resolve() +def _generated_documentation_routes() -> set[str]: + path = ROOT / "scripts/build_docs.py" + spec = importlib.util.spec_from_file_location("build_docs_links", path) + if spec is None or spec.loader is None: + return set() + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + try: + spec.loader.exec_module(module) + return {document.route.as_posix() for document in module.documents()} + except Exception: + return set() + finally: + sys.modules.pop(spec.name, None) + + def check_local_links(errors: list[str]) -> None: + generated = _generated_documentation_routes() for source in _public_files(): suffix = source.suffix.lower() text = source.read_text(encoding="utf-8") @@ -126,11 +220,40 @@ def check_local_links(errors: list[str]) -> None: for raw in links: target = _local_target(source, raw) if target is not None and not target.exists(): + try: + site_relative = target.relative_to(ROOT / "docs").as_posix() + except ValueError: + site_relative = "" + if site_relative in generated or f"{site_relative}/index.html" in generated: + continue errors.append( f"broken local link in {source.relative_to(ROOT)}: {raw}" ) +def check_html_accessibility(errors: list[str]) -> None: + """Enforce the small structural accessibility floor for every Pages document.""" + + for path in sorted((ROOT / "docs").glob("*.html")): + parser = LinkCollector() + parser.feed(path.read_text(encoding="utf-8")) + name = path.relative_to(ROOT).as_posix() + if not parser.html_lang: + errors.append(f"{name} has no html language") + if not parser.title.strip(): + errors.append(f"{name} has no document title") + if not parser.has_viewport: + errors.append(f"{name} has no viewport metadata") + if len(parser.main_ids) != 1 or not parser.main_ids[0]: + errors.append(f"{name} must have exactly one identified main region") + elif f"#{parser.main_ids[0]}" not in parser.skip_targets: + errors.append(f"{name} has no skip link to its main region") + if parser.images_without_alt: + errors.append(f"{name} has {parser.images_without_alt} image(s) without alt text") + if parser.unlabelled_navs: + errors.append(f"{name} has {parser.unlabelled_navs} navigation region(s) without labels") + + def check_versions(errors: list[str]) -> None: pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") project_section = pyproject.split("[project]", 1) @@ -142,9 +265,10 @@ def check_versions(errors: list[str]) -> None: expected = project_match.group(1) init_text = (ROOT / "src/verifier/__init__.py").read_text(encoding="utf-8") init_match = re.search(r'^__version__ = "([^"]+)"$', init_text, re.MULTILINE) + citation_text = (ROOT / "CITATION.cff").read_text(encoding="utf-8") citation_match = re.search( r"^version:\s*([^\s]+)$", - (ROOT / "CITATION.cff").read_text(encoding="utf-8"), + citation_text, re.MULTILINE, ) zenodo = json.loads((ROOT / ".zenodo.json").read_text(encoding="utf-8")) @@ -157,31 +281,159 @@ def check_versions(errors: list[str]) -> None: for label, version in found.items(): if version != expected: errors.append(f"version mismatch: pyproject={expected}, {label}={version}") - if not re.search(rf"^## {re.escape(expected)} - \d{{4}}-\d{{2}}-\d{{2}}$", changelog, re.MULTILINE): - errors.append(f"CHANGELOG.md has no dated {expected} release heading") + dated = re.search( + rf"^## {re.escape(expected)} - (\d{{4}}-\d{{2}}-\d{{2}})$", + changelog, + re.MULTILINE, + ) + unreleased = re.search( + rf"^## {re.escape(expected)} - UNRELEASED$", changelog, re.MULTILINE + ) + citation_date = re.search(r"^date-released:\s*(\d{4}-\d{2}-\d{2})$", citation_text, re.MULTILINE) + if dated is None and unreleased is None: + errors.append(f"CHANGELOG.md has no dated or UNRELEASED {expected} heading") + elif unreleased is not None: + if citation_date is not None: + errors.append("unreleased CITATION.cff must not fabricate date-released") + if "release candidate" not in citation_text.lower(): + errors.append("unreleased CITATION.cff must identify the release candidate") + elif citation_date is None or citation_date.group(1) != dated.group(1): + errors.append("CITATION.cff date-released must match the dated CHANGELOG heading") + + +def maturity_table_violations(readme: str) -> list[str]: + """Require one reviewable status row for every advertised major surface.""" + + heading = "## Current maturity" + if heading not in readme: + return ["README.md has no canonical current-maturity section"] + section = readme.split(heading, 1)[1].split("\n## ", 1)[0] + header = ( + "| Surface | Normative status | Reference implementation | Evidence binding | " + "Conformance status | Missing mechanism or evidence |" + ) + errors: list[str] = [] + if header not in section: + errors.append("README.md maturity table does not expose all six required fields") + rows: dict[str, list[str]] = {} + for line in section.splitlines(): + if not line.startswith("|") or line.startswith("|---"): + continue + cells = [cell.strip() for cell in line.strip().strip("|").split("|")] + if cells and cells[0] != "Surface": + rows.setdefault(cells[0], []).append(line) + if len(cells) != 6: + errors.append( + f"README.md maturity row {cells[0]!r} has {len(cells)} fields, expected 6" + ) + for surface, conformance in MATURITY_CONFORMANCE.items(): + observed = rows.get(surface, []) + if len(observed) != 1: + errors.append( + f"README.md maturity table requires exactly one {surface!r} row, " + f"observed {len(observed)}" + ) + elif conformance not in observed[0]: + errors.append( + f"README.md maturity row {surface!r} is missing conformance boundary " + f"{conformance!r}" + ) + return errors + + +def transient_time_status_violations(text: str) -> list[str]: + """Find transient TIME state copied into long-lived explanatory prose.""" + + return [match.group(0) for match in CURRENT_TIME_STATUS.finditer(text)] def check_claim_boundaries(errors: list[str]) -> None: readme = (ROOT / "README.md").read_text(encoding="utf-8") roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8") wire = (ROOT / "standard/WIRE_IDENTIFIERS.md").read_text(encoding="utf-8") + reference = (ROOT / "docs/reference.html").read_text(encoding="utf-8") + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + scitt_demo = (ROOT / "examples/scitt_interop/demo.py").read_text(encoding="utf-8") + scitt_result = json.loads( + (ROOT / "examples/scitt_interop/generated/verification_result.json").read_text( + encoding="utf-8" + ) + ) required_readme = ( "Portable, bounded, refutable evidence for computational claims.", + "VSTD is a verification-domain language and Python reference implementation", + "does **not**\nreplace native domain verifiers", + "## 30–60 second demonstration", + "## What a result means", + "## Current maturity", + "## Why VSTD exists", "vstd demo", - "founder-maintained **alpha project specification**", - "A higher-layer result does **not** supply", - "It cannot prove general AI safety", + "A later-profile result does **not** supply", + "It cannot prove general AI", + "[Normative specifications](standard/LADDER.md)", + "[Report an ambiguity or counterexample]", + "[Report a vulnerability privately]", + "SCITT registration proves neither payload", + "VSTD evaluates bounded validity propositions about computational processes", + "RUST is the inverse-TRUST diagnostic mechanic", + "cryptographic zero knowledge can enclose", + "The current checkout is an unreleased", ) for phrase in required_readme: if phrase not in readme: errors.append(f"README.md is missing presentation boundary: {phrase!r}") - if "`vstd` is the canonical cross-platform command" not in readme: + if "`vstd` is the canonical cross-platform CLI name" not in readme: errors.append("README.md does not disclose the canonical cross-platform CLI") + errors.extend(maturity_table_violations(readme)) + expected_order = ( + "VSTD is a verification-domain language", + "## 30–60 second demonstration", + "## What a result means", + "## Current maturity", + "## Why VSTD exists", + "## Architecture", + "## Install and use", + "## Interoperability", + "## Reproducibility and releases", + "## Claims, security, and contribution", + "## Citation and license", + ) + positions = [readme.find(marker) for marker in expected_order] + if any(position < 0 for position in positions) or positions != sorted(positions): + errors.append("README.md first-view information hierarchy has drifted") + for relative in CURRENT_FACING_SURFACES: + text = (ROOT / relative).read_text(encoding="utf-8") + for match in transient_time_status_violations(text): + errors.append(f"transient TIME state copied into {relative}: {match!r}") + for relative in ( + "README.md", + "AGENTS.md", + "CODE_OF_CONDUCT.md", + "GOVERNANCE.md", + "docs/index.html", + "docs/guides.html", + "docs/assets/vstd-overview.svg", + ): + if "founder-maintained" in (ROOT / relative).read_text(encoding="utf-8").lower(): + errors.append(f"{relative} uses reputation-centric founder-maintained wording") if "`vstd` is the canonical cross-platform CLI name" not in wire: errors.append("WIRE_IDENTIFIERS.md does not preserve the CLI compatibility rule") + if "VSTD-4 CANDIDATE; CONFORMANCE NOT_ESTABLISHED" not in reference: + errors.append("generated reference does not bound its VSTD-4 implementation status") + if "reproducible COSE specimen" in changelog or "reproducible specimen" in roadmap: + errors.append("SCITT ephemeral-key specimen is described as byte-reproducible") + if "ephemeral-key COSE artifacts" not in scitt_demo: + errors.append("SCITT producer does not disclose its ephemeral-key artifact boundary") + if scitt_result.get("vstd_observation", {}).get("conformance_status") != "NOT_ESTABLISHED": + errors.append("SCITT verification result drops VSTD conformance status") + composition = scitt_result.get("composition", {}) + if composition.get("vstd_conformance_status") != "NOT_ESTABLISHED": + errors.append("SCITT composition drops VSTD conformance status") + if composition.get("status_scope") != "NATIVE_VSTD_RESULT_AND_SCITT_REGISTRATION": + errors.append("SCITT composition does not state the scope of PASS") if "## Explicit non-goals" not in roadmap or "operational condition" not in roadmap: errors.append("ROADMAP.md lacks its capability and non-goal boundary") - if (ROOT / "docs/layers/vstd-3/migration.md").exists(): + if (ROOT / "docs/profiles/vstd-3/migration.md").exists(): errors.append("obsolete adopter-migration path has reappeared") @@ -223,6 +475,20 @@ def check_lineage_claims(errors: list[str]) -> None: errors.append(f"{label} in {path.relative_to(ROOT)}:{line}") +def check_retired_surfaces(errors: list[str]) -> None: + """Prevent removed partial-profile identifiers and developmental fields from returning.""" + + for path in _public_files(): + text = path.read_text(encoding="utf-8") + for retired in RETIRED_SURFACES: + offset = text.find(retired) + if offset >= 0: + line = text.count("\n", 0, offset) + 1 + errors.append( + f"retired surface {retired!r} returned in {path.relative_to(ROOT)}:{line}" + ) + + def check_visual_assets(errors: list[str]) -> None: svg = ROOT / "docs/assets/vstd-overview.svg" try: @@ -239,18 +505,18 @@ def check_visual_assets(errors: list[str]) -> None: "vstd-1": "REF. SUBSET", "vstd-2": "EXPERIMENTAL", "vstd-3": "IMPLEMENTED", - "vstd-4": "IMPLEMENTED", + "vstd-4": "CANDIDATE", "vstd-5": "DRAFT", "graph-1": "REF. SUBSET", - "graph-2": "IMPLEMENTED", - "graph-3": "IMPLEMENTED", - "graph-4": "IMPLEMENTED", + "graph-2": "CANDIDATE", + "graph-3": "CANDIDATE", + "graph-4": "CANDIDATE", "graph-5": "DRAFT", } observed_status = { - element.attrib["data-layer"]: "".join(element.itertext()).strip() + element.attrib["data-profile"]: "".join(element.itertext()).strip() for element in root.iter() - if "data-layer" in element.attrib + if "data-profile" in element.attrib } if observed_status != expected_status: errors.append( @@ -272,14 +538,155 @@ def check_visual_assets(errors: list[str]) -> None: ) +def check_generated_reference(errors: list[str]) -> None: + """The published CLI/API reference must still match the importable package.""" + + path = ROOT / "scripts/build_reference.py" + spec = importlib.util.spec_from_file_location("build_reference", path) + if spec is None or spec.loader is None: + errors.append("cannot load scripts/build_reference.py") + return + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + rendered = module.render() + except Exception as exc: # noqa: BLE001 - any failure is a presentation failure + errors.append(f"reference page cannot be generated: {exc}") + return + target = ROOT / "docs/reference.html" + if not target.is_file(): + errors.append("docs/reference.html is missing; run python scripts/build_reference.py") + return + if target.read_text(encoding="utf-8") != rendered: + errors.append( + "docs/reference.html drifted from the implementation; " + "run python scripts/build_reference.py" + ) + index = (ROOT / "docs/index.html").read_text(encoding="utf-8") + for link in ('Guides', 'Reference'): + if link not in index: + errors.append(f"docs/index.html navigation is missing {link}") + + +def check_generated_documentation(errors: list[str]) -> None: + """Every maintained Markdown source must have one unambiguous site route.""" + + path = ROOT / "scripts/build_docs.py" + spec = importlib.util.spec_from_file_location("build_docs", path) + if spec is None or spec.loader is None: + errors.append("cannot load scripts/build_docs.py") + return + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + try: + spec.loader.exec_module(module) + documents = module.documents() + except Exception as exc: # any generation failure blocks publication + errors.append(f"documentation routes cannot be generated: {exc}") + return + finally: + sys.modules.pop(spec.name, None) + routes = [document.route.as_posix() for document in documents] + if not routes: + errors.append("documentation renderer declares no source pages") + if len(routes) != len(set(routes)): + errors.append("documentation renderer declares duplicate site routes") + for document in documents: + if not document.source.is_file(): + errors.append(f"documentation source is missing: {document.source}") + + pages = { + name: (ROOT / name).read_text(encoding="utf-8") + for name in ("docs/index.html", "docs/guides.html", "docs/reference.html") + } + required = ( + 'href="standard/"', + 'href="experiments/"', + 'href="project/ROADMAP.html"', + ) + for name, page in pages.items(): + for link in required: + if link not in page: + errors.append(f"{name} navigation is missing the on-site route {link}") + if '>Standard' not in page or '>Specifications' in page: + errors.append(f"{name} navigation must label standard/ as Standard") + guides = pages["docs/guides.html"] + if "github.com/TimeLordRaps/verifier/blob/main/docs/" in guides: + errors.append("docs/guides.html sends maintained guides to the GitHub file viewer") + if "github.com/TimeLordRaps/verifier/blob/main/standard/" in guides: + errors.append("docs/guides.html sends specifications to the GitHub file viewer") + + +def check_experiment_index(errors: list[str]) -> None: + """Profile manifests, bound repo artifacts, and the public index must agree.""" + + path = ROOT / "scripts/build_experiment_index.py" + spec = importlib.util.spec_from_file_location("build_experiment_index", path) + if spec is None or spec.loader is None: + errors.append("cannot load scripts/build_experiment_index.py") + return + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + rendered = module.render(module.discover(ROOT)) + except Exception as exc: # the gate reports any bounded generation failure + errors.append(f"experiment index cannot be generated: {exc}") + return + target = ROOT / "experiments/INDEX.md" + if not target.is_file() or target.read_text(encoding="utf-8") != rendered: + errors.append( + "experiments/INDEX.md drifted from profile manifests; " + "run python scripts/build_experiment_index.py" + ) + + +def check_acronyms(errors: list[str]) -> None: + """Require first-use expansion on every registered reader-facing surface.""" + + path = ROOT / "scripts/check_acronyms.py" + spec = importlib.util.spec_from_file_location("check_acronyms", path) + if spec is None or spec.loader is None: + errors.append("cannot load scripts/check_acronyms.py") + return + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + errors.extend(module.validate_repo()) + except Exception as exc: # any glossary or scan failure is a presentation failure + errors.append(f"acronym presentation gate failed: {exc}") + + +def check_terminology(errors: list[str]) -> None: + """Reject ambiguous structural terms on current public surfaces.""" + + path = ROOT / "scripts/check_terminology.py" + spec = importlib.util.spec_from_file_location("check_terminology", path) + if spec is None or spec.loader is None: + errors.append("cannot load scripts/check_terminology.py") + return + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + errors.extend(module.validate_repo()) + except Exception as exc: # any terminology scan failure is a presentation failure + errors.append(f"terminology presentation gate failed: {exc}") + + def run() -> list[str]: errors: list[str] = [] check_local_links(errors) + check_html_accessibility(errors) check_versions(errors) check_claim_boundaries(errors) check_public_paths(errors) check_lineage_claims(errors) + check_retired_surfaces(errors) check_visual_assets(errors) + check_generated_reference(errors) + check_experiment_index(errors) + check_generated_documentation(errors) + check_acronyms(errors) + check_terminology(errors) return errors @@ -289,7 +696,11 @@ def main() -> int: for error in errors: print(f"[PRESENTATION FAIL] {error}", file=sys.stderr) return 1 - print("[PRESENTATION OK] links, versions, boundaries, paths, and visual assets") + print( + "[PRESENTATION OK] links, accessibility, versions, boundaries, paths, " + "maturity, transient status, visual assets, generated reference, experiment " + "index, acronym expansion, and structural terminology" + ) return 0 diff --git a/scripts/check_terminology.py b/scripts/check_terminology.py new file mode 100644 index 0000000..210043b --- /dev/null +++ b/scripts/check_terminology.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Terminology: Verifier Standard (VSTD). + +Reject prose that treats numbered VSTD profiles as interchangeable layers or +scalar levels. Public compatibility identifiers remain unchanged and are not +matched by this prose-focused gate. +""" + +from __future__ import annotations + +from pathlib import Path +import re +import subprocess + + +ROOT = Path(__file__).resolve().parents[1] +TEXT_SUFFIXES = { + ".cff", + ".html", + ".json", + ".md", + ".py", + ".svg", + ".toml", + ".txt", + ".yaml", + ".yml", +} +IGNORED_PARTS = { + ".git", + ".pytest_cache", + ".venv", + "build", + "dist", + "__pycache__", +} +GENERATED_PREFIXES = ( + "examples/flagship_demo/specimens/", + "src/verifier/specifications/", +) +SCAN_EXCLUSIONS = { + "examples/zizk_artifact_first/zero_identity/ROUND1_ZERO_IDENTITY_REPORT.md", + "experiments/artifact_first_mechanisms/reverification/ROUND2_DESIGN_NOTE.md", + "tests/test_presentation_surface.py", +} +AMBIGUOUS_PATTERNS = ( + ("VSTD profiles called layers", re.compile(r"(?i)\bVSTD\s+layers?\b")), + ("object profiles called layers", re.compile(r"(?i)\bobject\s+layers?\b")), + ("Graph profiles called layers", re.compile(r"(?i)\bGraph\s+layers?\b")), + ("Graph profile called a level", re.compile(r"(?i)\bGraph\s+levels?\b")), + ( + "candidate Graph profile called a level", + re.compile(r"(?i)\bcandidate\s+graph\s+levels?\b"), + ), + ("profile dependency called lower-layer", re.compile(r"(?i)\blower[- ]layers?\b")), + ("profile dependency called higher-layer", re.compile(r"(?i)\bhigher[- ]layers?\b")), + ("numbered profile called a layer", re.compile(r"(?i)\bnumbered\s+layers?\b")), + ("profile result called a layer result", re.compile(r"(?i)\blayer\s+results?\b")), + ( + "profile conformance called layer conformance", + re.compile(r"(?i)\blayer\s+conformance\b"), + ), + ("verification complex called a VSTD ladder", re.compile(r"(?i)\bVSTD\s+ladder\b")), + ("VSTD-4 rung called a ladder rung", re.compile(r"(?i)\bladder\s+rungs?\b")), + ("VSTD-4 depth left unqualified", re.compile(r"(?i)\bVSTD-4\s+depth\b")), + ( + "VSTD-4 candidate depth inverted into a structural depth candidate", + re.compile(r"(?i)\bstructural\s+depth\s+candidate\b"), + ), +) + + +def terminology_violations(text: str) -> list[tuple[str, int]]: + """Return ambiguous phrase labels and one-based line numbers.""" + + violations: list[tuple[str, int]] = [] + for label, pattern in AMBIGUOUS_PATTERNS: + for match in pattern.finditer(text): + violations.append((label, text.count("\n", 0, match.start()) + 1)) + return violations + + +def _tracked_text_files() -> list[Path]: + result = subprocess.run( + ["git", "ls-files", "-z"], + cwd=ROOT, + check=True, + capture_output=True, + ) + paths: list[Path] = [] + for raw_relative in result.stdout.decode("utf-8").split("\0"): + if not raw_relative: + continue + relative = Path(raw_relative).as_posix() + path = ROOT / raw_relative + if path.suffix.lower() not in TEXT_SUFFIXES: + continue + if any(part in IGNORED_PARTS for part in Path(relative).parts): + continue + if relative == "scripts/check_terminology.py": + continue + if relative in SCAN_EXCLUSIONS: + continue + if relative.startswith(GENERATED_PREFIXES): + continue + paths.append(path) + return sorted(paths) + + +def validate_repo() -> list[str]: + """Validate current source surfaces; generated copies are checked elsewhere.""" + + errors: list[str] = [] + for path in _tracked_text_files(): + text = path.read_text(encoding="utf-8") + for label, line in terminology_violations(text): + errors.append(f"{label} in {path.relative_to(ROOT)}:{line}") + return errors + + +def main() -> int: + errors = validate_repo() + if errors: + for error in errors: + print(f"[TERMINOLOGY FAIL] {error}") + return 1 + print("[TERMINOLOGY OK] numbered profiles and closure coordinates remain distinct") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_external_links.py b/tests/test_external_links.py new file mode 100644 index 0000000..0d7dcd6 --- /dev/null +++ b/tests/test_external_links.py @@ -0,0 +1,64 @@ +"""Tests for the external Hypertext Transfer Protocol (HTTP) link audit.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path +import sys + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts/check_external_links.py" +SPEC = importlib.util.spec_from_file_location("check_external_links", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +external_links = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = external_links +SPEC.loader.exec_module(external_links) + + +def test_collect_links_deduplicates_and_removes_fragments(tmp_path: Path) -> None: + markdown = tmp_path / "guide.md" + markdown.write_text( + "[one](https://example.com/path#one) [two](https://example.com/path#two)\n", + encoding="utf-8", + ) + html = tmp_path / "guide.html" + html.write_text('page', encoding="utf-8") + + assert external_links.collect_links((markdown, html)) == ( + "https://example.com/path", + "https://example.org/page", + ) + + +def test_allowlist_requires_a_reviewable_reason(tmp_path: Path) -> None: + allowlist = tmp_path / "allowlist.txt" + allowlist.write_text("https://example.com/*\n", encoding="utf-8") + + with pytest.raises(ValueError, match="needs a tab and reason"): + external_links.read_allowlist(allowlist) + + +def test_allowlist_matches_only_exact_or_declared_prefix() -> None: + entries = (("https://example.com/bounded/*", "upstream blocks probes"),) + + assert external_links.allowlist_reason( + "https://example.com/bounded/page", entries + ) == "upstream blocks probes" + assert external_links.allowlist_reason("https://example.com/other", entries) is None + + +def test_external_audit_is_scheduled_and_not_a_pull_request_gate() -> None: + workflow = (ROOT / ".github/workflows/external-links.yml").read_text(encoding="utf-8") + entries = external_links.read_allowlist( + ROOT / ".github/external-links-allowlist.txt" + ) + + assert "schedule:" in workflow + assert "workflow_dispatch:" in workflow + assert "pull_request:" not in workflow + assert "--retries 2 --workers 8" in workflow + assert entries + assert all(reason for _, reason in entries) diff --git a/tests/test_presentation_surface.py b/tests/test_presentation_surface.py index eb670b9..d250660 100644 --- a/tests/test_presentation_surface.py +++ b/tests/test_presentation_surface.py @@ -1,15 +1,37 @@ -"""The public first impression is a checked repository surface.""" +"""Terminology: application programming interface (API); Concise Binary Object Representation (CBOR); +CBOR Object Signing and Encryption (COSE); continuous integration (CI); Hypertext Markup Language (HTML); +Supply Chain Integrity, Transparency, and Trust (SCITT); uniform resource locator (URL); +Verifier Standard (VSTD). + +The public first impression is a checked repository surface.""" from __future__ import annotations +from html.parser import HTMLParser import importlib.util import json from pathlib import Path +import sys +from urllib.parse import unquote + +import yaml ROOT = Path(__file__).resolve().parents[1] +class _BuiltPageLinks(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.links: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + attributes = dict(attrs) + for name in ("href", "src"): + if attributes.get(name): + self.links.append(attributes[name]) + + def test_professional_presentation_surface_has_no_drift() -> None: path = ROOT / "scripts/check_presentation.py" spec = importlib.util.spec_from_file_location("check_presentation", path) @@ -19,6 +41,55 @@ def test_professional_presentation_surface_has_no_drift() -> None: assert module.run() == [] +def test_acronym_gate_rejects_missing_and_late_first_use(tmp_path: Path) -> None: + path = ROOT / "scripts/check_acronyms.py" + spec = importlib.util.spec_from_file_location("check_acronyms_fixture", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + docs = tmp_path / "docs" + docs.mkdir() + glossary = docs / "ACRONYMS.md" + glossary.write_text( + "| Term | Expansion | Note |\n" + "|---|---|---|\n" + "| `API` | application programming interface | interface |\n" + "| `VSTD` | Verifier Standard | standard |\n", + encoding="utf-8", + ) + readme = tmp_path / "README.md" + readme.write_text("# VSTD API\n\nVerifier Standard (VSTD).\n", encoding="utf-8") + module.ROOT = tmp_path + module.GLOSSARY = glossary + + errors = module.validate_repo() + assert any("VSTD appears before its expansion" in error for error in errors) + assert any("API is not expanded" in error for error in errors) + + readme.write_text( + "# Verifier Standard (VSTD) application programming interface (API)\n", + encoding="utf-8", + ) + assert module.validate_repo() == [] + + +def test_terminology_gate_rejects_interchangeable_structural_terms() -> None: + path = ROOT / "scripts/check_terminology.py" + spec = importlib.util.spec_from_file_location("check_terminology_fixture", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + ambiguous = "Graph level 3 depends on lower-layer conformance." + labels = [label for label, _line in module.terminology_violations(ambiguous)] + assert "Graph profile called a level" in labels + assert "profile dependency called lower-layer" in labels + assert module.terminology_violations( + "Candidate Graph profile 3 depends on prerequisite-profile conformance." + ) == [] + + def test_public_boundary_catches_private_coordinates_without_naming_them() -> None: path = ROOT / "scripts" / "check_presentation.py" spec = importlib.util.spec_from_file_location("check_presentation_boundaries", path) @@ -36,6 +107,85 @@ def test_public_boundary_catches_private_coordinates_without_naming_them() -> No assert "private deployment field" in module.public_boundary_violations(deployment_field) +def test_maturity_table_requires_each_major_surface_and_explicit_conformance() -> None: + path = ROOT / "scripts" / "check_presentation.py" + spec = importlib.util.spec_from_file_location("check_presentation_maturity", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + readme = (ROOT / "README.md").read_text(encoding="utf-8") + assert module.maturity_table_violations(readme) == [] + + combined = readme.replace("| VSTD-Graph-3 |", "| VSTD-Graph-2 |", 1) + errors = module.maturity_table_violations(combined) + assert any("VSTD-Graph-2" in error and "observed 2" in error for error in errors) + assert any("VSTD-Graph-3" in error and "observed 0" in error for error in errors) + + +def test_artifact_state_vocabulary_is_process_bound_and_unambiguous() -> None: + ladder = (ROOT / "standard" / "LADDER.md").read_text(encoding="utf-8") + humans = (ROOT / "HUMANS.md").read_text(encoding="utf-8") + agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8") + architecture = (ROOT / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + + for text in (ladder, humans, agents, architecture): + assert "TRUST" in text + assert "ROT" in text + assert "RUST" in text + assert "whether an actor is good, bad" in ladder + assert "zero unevidenced knowledge is presumed" in ladder + assert "bearer- and artifact-bound, never prover-identity-bound" in ladder + assert "inverse-TRUST diagnostic mechanic" in ladder + assert "historical receipt" in ladder + assert "actor ratings" in architecture + + +def test_standard_orients_readers_before_formal_terminology() -> None: + ladder = (ROOT / "standard" / "LADDER.md").read_text(encoding="utf-8") + orientation = ladder.index("### Read this first:") + terminology = ladder.index("### Terminology contract") + assert orientation < terminology + assert "A field, document, or actor merely saying" in ladder + assert "Its **object profile depth is 1**" in ladder + assert "The cumulative checklist cannot skip the missing" in ladder + assert "not a new verdict, evidence-strength rating" in ladder + + +def test_object_receipts_use_full_ladder_identifiers() -> None: + vstd1 = json.loads( + (ROOT / "receipts" / "schema" / "vstd1_receipt.json").read_text(encoding="utf-8") + ) + generic = json.loads( + (ROOT / "receipts" / "schema" / "vstd1_generic_run_receipt.json").read_text( + encoding="utf-8" + ) + ) + vstd2 = json.loads( + (ROOT / "receipts" / "schema" / "vstd2_receipt.json").read_text(encoding="utf-8") + ) + + assert vstd1["properties"]["schema_version"]["enum"] == ["VSTD-1"] + assert vstd1["properties"]["receipt_kind"]["const"] == "claim_mechanics" + assert generic["properties"]["schema_version"]["const"] == "VSTD-1" + assert generic["properties"]["receipt_kind"]["const"] == "generic_computational_run" + assert vstd2["properties"]["schema_version"]["const"] == "VSTD-2" + + +def test_long_lived_docs_reject_transient_time_state() -> None: + path = ROOT / "scripts" / "check_presentation.py" + spec = importlib.util.spec_from_file_location("check_presentation_time", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + assert module.transient_time_status_violations("TIME.md is CLEAR today") + assert module.transient_time_status_violations("TIME == OPEN") + assert module.transient_time_status_violations( + "TIME.md is a contradiction annunciator" + ) == [] + + def test_lineage_claim_gate_rejects_causal_upgrades_without_blocking_boundaries() -> None: path = ROOT / "scripts" / "check_presentation.py" spec = importlib.util.spec_from_file_location("check_presentation_lineage", path) @@ -69,9 +219,34 @@ def test_pages_artifact_serves_every_canonical_schema_id(tmp_path: Path) -> None spec.loader.exec_module(module) output = tmp_path / "site" - copied = module.build(output) + copied = module.build(output, source_ref="test-commit") assert (output / "index.html").is_file() - sources = sorted((ROOT / "receipts/schema").glob("*.json")) + assert (output / "guides.html").is_file() + assert (output / "reference.html").is_file() + assert (output / "docs/QUICKSTART.html").is_file() + assert (output / "standard/index.html").is_file() + assert (output / "standard/ARTIFACT_CONTROL.html").is_file() + assert (output / "experiments/index.html").is_file() + assert (output / "project/ROADMAP.html").is_file() + assert (output / "assets/orientation-previews.js").is_file() + coordinate = json.loads( + (output / "documentation-coordinate.json").read_text(encoding="utf-8") + ) + assert coordinate == { + "canonical_base_url": "https://timelordraps.github.io/verifier/", + "documentation_version": "1.2.0", + "normative_source": "standard/", + "release_state": "UNRELEASED_CANDIDATE", + "schema_version": 1, + "source_ref": "test-commit", + } + sources = sorted( + ( + *ROOT.joinpath("receipts/schema").glob("*.json"), + *ROOT.joinpath("standard/schemas").glob("*.json"), + ), + key=lambda path: path.name, + ) assert [path.name for path in copied] == [path.name for path in sources] for source, deployed in zip(sources, copied): assert deployed.read_bytes() == source.read_bytes() @@ -80,6 +255,247 @@ def test_pages_artifact_serves_every_canonical_schema_id(tmp_path: Path) -> None ) +def test_every_declared_document_is_rendered_with_source_aware_navigation( + tmp_path: Path, +) -> None: + path = ROOT / "scripts/build_docs.py" + spec = importlib.util.spec_from_file_location("build_docs_coverage", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + try: + spec.loader.exec_module(module) + finally: + sys.modules.pop(spec.name, None) + + output = tmp_path / "site" + output.mkdir() + written = module.build(output, source_ref="test-commit") + declared = module.documents() + assert len(written) == len(declared) + for document in declared: + target = output / Path(document.route.as_posix()) + assert target.is_file(), f"documentation omits {document.source.relative_to(ROOT)}" + + ladder = (output / "standard/index.html").read_text(encoding="utf-8") + assert 'class="doc-sidebar"' in ladder + assert "On this page" in ladder + assert '>Standard' in ladder + assert '>Specifications' not in ladder + assert 'href="VSTD-1.html"' in ladder + assert 'href="../docs/CONCEPTS_AND_PRECEDENTS.html"' in ladder + assert ( + "github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md" + not in ladder + ) + assert "/blob/test-commit/standard/LADDER.md" in ladder + assert "without changing its status" in ladder + assert "Evidence for one closure coordinate never supplies evidence for another." in ladder + assert "An UNKNOWN is never a pass" in ladder + assert 'data-orientation-preview="repository"' in ladder + assert 'data-orientation-concept="Defense in depth"' in ladder + assert 'src="../assets/orientation-previews.js" defer' in ladder + + readme = (output / "project/README.html").read_text(encoding="utf-8") + assert 'href="../docs/QUICKSTART.html"' in readme + assert 'href="../standard/index.html"' in readme + + concepts = (output / "docs/CONCEPTS_AND_PRECEDENTS.html").read_text( + encoding="utf-8" + ) + assert 'class="orientation-link"' in concepts + assert 'data-orientation-preview="repository"' in concepts + assert 'data-orientation-concept="Assurance"' in concepts + assert ( + 'data-orientation-definition="VSTD reports evidence-bounded results, ' + 'not universal confidence or institutional accreditation."' in concepts + ) + assert ( + 'data-orientation-boundary="Wikipedia orientation; not a VSTD authority"' + in concepts + ) + assert 'rel="noreferrer"' in concepts + assert 'src="../assets/orientation-previews.js" defer' in concepts + assert 'href="../reference.html#api-compute_canonical_digest"' in concepts + assert 'href="../reference.html#api-ReproducibilityLevel"' in concepts + assert 'href="../reference.html#api-DecisionCertificate"' in concepts + + quickstart = (output / "docs/QUICKSTART.html").read_text(encoding="utf-8") + assert "orientation-previews.js" not in quickstart + + +def test_orientation_previews_are_bounded_and_fail_to_ordinary_links() -> None: + source = (ROOT / "docs/CONCEPTS_AND_PRECEDENTS.md").read_text(encoding="utf-8") + script = (ROOT / "docs/assets/orientation-previews.js").read_text(encoding="utf-8") + + assert "short definition is versioned in" in source + assert "popup performs no" in source + assert "data-orientation-preview=\"repository\"" in script + assert "dataset.orientationDefinition" in script + assert "Repository definition · versioned with VSTD" in script + assert "optional external background" in script + assert "fetch(" not in script + assert "w/api.php" not in script + assert ".textContent = text" in script + + +def test_generated_api_reference_has_documented_supported_exports() -> None: + reference = (ROOT / "docs/reference.html").read_text(encoding="utf-8") + + assert "No docstring is declared for this export" not in reference + assert "Canonical grounded decision certificate (GDC) blocks" in reference + assert "canonically digested VSTD-1 claim receipt" in reference + assert "Validate one generic-run receipt" in reference + assert "Outcome vocabulary returned by the VSTD-1 claim-mechanics checker" in reference + + +def test_assembled_site_has_no_broken_internal_navigation(tmp_path: Path) -> None: + path = ROOT / "scripts/build_pages.py" + spec = importlib.util.spec_from_file_location("build_pages_links", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + output = tmp_path / "site" + module.build(output, source_ref="test-commit") + canonical = "https://timelordraps.github.io/verifier/" + for source in output.rglob("*.html"): + parser = _BuiltPageLinks() + parser.feed(source.read_text(encoding="utf-8")) + for raw in parser.links: + if raw.startswith(canonical): + relative = unquote(raw.removeprefix(canonical).split("#", 1)[0]) + target = output / relative + if not relative or relative.endswith("/"): + target /= "index.html" + elif raw.startswith(("#", "http://", "https://", "mailto:", "data:")): + continue + else: + relative = unquote(raw.split("#", 1)[0]) + target = source.parent / relative + if relative.endswith("/"): + target /= "index.html" + assert target.exists(), f"{source.relative_to(output)} links missing {raw}" + + +def test_guides_keep_repository_documentation_inside_the_site() -> None: + guides = (ROOT / "docs/guides.html").read_text(encoding="utf-8") + assert 'Standard' in guides + assert '>Specifications' not in guides + assert 'href="docs/QUICKSTART.html"' in guides + assert 'href="standard/"' in guides + assert 'href="experiments/"' in guides + assert 'href="project/ROADMAP.html"' in guides + assert "github.com/TimeLordRaps/verifier/blob/main/docs/" not in guides + assert "github.com/TimeLordRaps/verifier/blob/main/standard/" not in guides + + +def test_pages_explains_artifact_first_state_without_actor_ratings() -> None: + page = (ROOT / "docs" / "index.html").read_text(encoding="utf-8") + assert "Verify the process, not the actor." in page + assert "TRUST · FORWARD" in page + assert "ROT · CURRENT STATE" in page + assert "RUST · BACKWARD" in page + assert "cryptographic zero knowledge can enclose" in page + assert "not acronyms or actor ratings" in page + + +def test_architecture_map_names_every_published_schema() -> None: + architecture = (ROOT / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + for schema in (ROOT / "receipts" / "schema").glob("*.json"): + assert schema.name in architecture, schema.name + + +def test_conformance_gate_requires_real_scitt_cose_integration() -> None: + workflow = yaml.safe_load( + (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + ) + jobs = workflow["jobs"] + scitt_job = jobs["scitt-crypto"] + steps = "\n".join(str(step.get("run", "")) for step in scitt_job["steps"]) + assert 'pip install ".[test,scitt]"' in steps + assert "import cbor2, cryptography, scitt_cose" in steps + assert "tests/test_scitt_crypto_example.py" in steps + assert "scitt-crypto" in jobs["conformance-gate"]["needs"] + + +def test_repository_checks_do_not_self_certify_conformance() -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + guides = (ROOT / "docs" / "guides.html").read_text(encoding="utf-8") + workflow_text = (ROOT / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ) + workflow = yaml.safe_load(workflow_text) + + assert "[![Conformance]" not in readme + assert "[![Repository checks]" in readme + assert workflow["name"] == "repository-checks" + assert "trace poisoned ancestry" not in guides + assert "examples/zizk_artifact_first" in guides + + +def test_pull_requests_retain_a_commit_addressed_pages_preview() -> None: + workflow = yaml.safe_load( + (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + ) + presentation = workflow["jobs"]["presentation"] + commands = "\n".join(str(step.get("run", "")) for step in presentation["steps"]) + uploads = [ + step + for step in presentation["steps"] + if "upload-artifact@" in step.get("uses", "") + ] + + assert '--source-ref "$GITHUB_SHA"' in commands + assert uploads[0]["with"]["name"] == "pages-preview-${{ github.sha }}" + assert uploads[0]["with"]["path"] == "_site" + + +def test_codeql_is_pinned_and_required_by_the_protected_gate() -> None: + workflow = yaml.safe_load( + (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + ) + jobs = workflow["jobs"] + codeql = jobs["codeql"] + uses = [str(step.get("uses", "")) for step in codeql["steps"]] + + assert any( + item + == "github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938" + for item in uses + ) + assert any( + item + == "github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938" + for item in uses + ) + assert "codeql" in jobs["conformance-gate"]["needs"] + + +def test_branch_coverage_is_retained_without_a_global_threshold() -> None: + workflow = yaml.safe_load( + (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + ) + jobs = workflow["jobs"] + coverage = jobs["coverage"] + commands = "\n".join(str(step.get("run", "")) for step in coverage["steps"]) + uploads = [ + step for step in coverage["steps"] if "upload-artifact@" in step.get("uses", "") + ] + + assert "coverage run --branch --source=src/verifier" in commands + assert "coverage report --show-missing" in commands + assert "coverage json --pretty-print -o coverage.json" in commands + assert "coverage xml -o coverage.xml" in commands + assert "--fail-under" not in commands + assert uploads[0]["with"]["name"] == "branch-coverage-python-3.12" + assert set(uploads[0]["with"]["path"].splitlines()) == { + "coverage.json", + "coverage.xml", + } + assert "coverage" 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) @@ -98,3 +514,49 @@ def test_pages_builder_refuses_to_merge_into_existing_content(tmp_path: Path) -> else: raise AssertionError("Pages builder merged into non-empty output") assert marker.read_text(encoding="utf-8") == "keep\n" + + +def test_generated_reference_covers_commands_and_top_level_exports() -> None: + """The docs tab is generated and must list its declared live surface.""" + + path = ROOT / "scripts/build_reference.py" + spec = importlib.util.spec_from_file_location("build_reference_coverage", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + page = (ROOT / "docs/reference.html").read_text(encoding="utf-8") + assert page == module.render() + + import verifier + from verifier.runtime.public_cli import build_parser + + for command in module._walk(build_parser()): + anchor = 'id="cli-' + str(command["prog"]).replace(" ", "-") + '"' + assert anchor in page, f"reference page omits {command['prog']}" + for name in verifier.__all__: + assert f'id="api-{name}"' in page, f"reference page omits export {name}" + assert verifier.__standard__ == "VSTD-4" + assert verifier.__standard_status__ == "CANDIDATE; CONFORMANCE NOT_ESTABLISHED" + assert "VSTD-4 CANDIDATE; CONFORMANCE NOT_ESTABLISHED" in page + assert "Monotone reproduction-fidelity states" in page + + +def test_generated_reference_detects_drift() -> None: + path = ROOT / "scripts/build_reference.py" + spec = importlib.util.spec_from_file_location("build_reference_drift", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + module.PIPELINE = (( + "vstd ghost", + "A command that no longer exists.", + ("verifier.core.run:not_a_real_entry_point",), + ),) + try: + module.render() + except module.ReferenceBuildError: + pass + else: + raise AssertionError("reference build published a missing pipeline entry point") From 7d253c9eb1819a4e9adc41bdcac1bd8a5ccb4f00 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:48:34 -0700 Subject: [PATCH 12/34] Harden the VSTD 1.2.0 release boundary Reason: Bind source, packages, software bill of materials, metadata, contradiction state, cross-platform bytes, and hosted gates to the exact candidate commit. Evidence: Release procedures, deterministic builder and verifier, metadata checks, continuous-integration workflows, changelog, citation data, and adversarial release tests change together. Coordinate: verifier-standard 1.2.0 release and packaging boundary. Falsification: Source members differ from Git, rebuilt artifacts differ across platforms, TIME is not CLEAR, metadata claims release prematurely, or a protected check belongs to another commit. Compatibility: Packaging and release controls are additive; publication remains a separate maintainer action and no tag or release is created here. --- .gitattributes | 2 + .github/workflows/ci.yml | 104 ++++++++++++++-- .github/workflows/release.yml | 25 +++- .zenodo.json | 9 +- CHANGELOG.md | 163 ++++++++++++++++++++++--- CITATION.cff | 8 +- RELEASING.md | 88 ++++++++++---- scripts/check_release_boundary.py | 7 +- scripts/check_release_metadata.py | 92 ++++++++++++++ scripts/check_time_status.py | 36 ++++++ scripts/release_artifacts.py | 142 +++++++++++++++++++++- tests/test_release_artifacts.py | 196 ++++++++++++++++++++++++++++-- 12 files changed, 792 insertions(+), 80 deletions(-) create mode 100644 scripts/check_release_metadata.py create mode 100644 scripts/check_time_status.py diff --git a/.gitattributes b/.gitattributes index 58ceb9b..dcc5902 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,5 @@ *.py text eol=lf *.toml text eol=lf *.yml text eol=lf +*.cose binary +*.msgpack binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cc961f..ddcb276 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,9 @@ -name: conformance +name: repository-checks on: push: + branches: [main] + tags: ["v*"] pull_request: permissions: @@ -22,6 +24,29 @@ jobs: - run: python -m pip install ".[test]" - run: python -m pytest -q + coverage: + 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" + - run: python -m pip install ".[test]" + - name: Record bounded branch-coverage evidence + run: | + python -m coverage run --branch --source=src/verifier -m pytest -q + python -m coverage report --show-missing --skip-covered | tee -a "$GITHUB_STEP_SUMMARY" + python -m coverage json --pretty-print -o coverage.json + python -m coverage xml -o coverage.xml + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: branch-coverage-python-3.12 + path: | + coverage.json + coverage.xml + if-no-files-found: error + retention-days: 14 + stdlib-smoke: runs-on: ubuntu-latest strategy: @@ -35,6 +60,32 @@ jobs: python-version: ${{ matrix.python-version }} - run: PYTHONPATH=src python -S -c "import verifier; from verifier.core.run import load_manifest; print(verifier.__version__)" + scitt-crypto: + 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" + - run: python -m pip install ".[test,scitt]" + - name: Require the real SCITT/COSE statement and receipt path + run: | + python -c "import cbor2, cryptography, scitt_cose" + python -m pytest -q tests/test_scitt_crypto_example.py + + artifact-seal: + 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" + - run: python -m pip install ".[test,seal]" + - name: Require exact-byte freeze and finite self-closing seal behavior + run: | + python -c "import cryptography" + python -m pytest -q tests/test_artifact_control.py + release-integrity: runs-on: ${{ matrix.os }} strategy: @@ -58,7 +109,7 @@ jobs: shell: bash - run: python -m twine check dist/release-integrity/*.whl dist/release-integrity/*.tar.gz shell: bash - - run: python scripts/check_release_boundary.py dist/release-integrity/*.zip dist/release-integrity/*.whl dist/release-integrity/*.tar.gz + - run: python scripts/check_release_boundary.py dist/release-integrity/*.zip dist/release-integrity/*.whl dist/release-integrity/*.tar.gz dist/release-integrity/*.manifest.json dist/release-integrity/*.cdx.json shell: bash - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -96,12 +147,16 @@ jobs: - run: python -m pip wheel --no-cache-dir --no-deps --wheel-dir dist . - run: python -m venv /tmp/vstd-wheel - run: /tmp/vstd-wheel/bin/python -m pip install --no-deps dist/*.whl - - run: /tmp/vstd-wheel/bin/vstd demo --json - - run: /tmp/vstd-wheel/bin/vstd plan examples/generic_run/manifest.json --json - - run: /tmp/vstd-wheel/bin/vstd run examples/generic_run/manifest.json --output /tmp/vstd-receipt - - run: /tmp/vstd-wheel/bin/vstd validate /tmp/vstd-receipt - - run: /tmp/vstd-wheel/bin/vstd reproduce /tmp/vstd-receipt --rerun - - run: /tmp/vstd-wheel/bin/verifier demo --scenario honest-unknown --json + - name: Exercise the installed wheel outside the source checkout + run: | + cd /tmp + /tmp/vstd-wheel/bin/vstd demo --json + /tmp/vstd-wheel/bin/vstd plan "$GITHUB_WORKSPACE/examples/generic_run/manifest.json" --json + /tmp/vstd-wheel/bin/vstd run "$GITHUB_WORKSPACE/examples/generic_run/manifest.json" --output /tmp/vstd-receipt + /tmp/vstd-wheel/bin/vstd validate /tmp/vstd-receipt + /tmp/vstd-wheel/bin/vstd reproduce /tmp/vstd-receipt --rerun + /tmp/vstd-wheel/bin/verifier demo --scenario honest-unknown --json + /tmp/vstd-wheel/bin/python -c 'import json; from pathlib import Path; from verifier.core.checker import IndependentAuditor; receipt=json.loads(Path("/tmp/vstd-receipt/receipt.json").read_text()); hashes=(receipt["assessment_context"]["verifier"]["specification_hash"], IndependentAuditor.verifier_descriptor().specification_hash); assert all(value.startswith("sha256:") for value in hashes), hashes' presentation: runs-on: ubuntu-latest @@ -111,25 +166,54 @@ jobs: with: python-version: "3.12" - run: python scripts/check_presentation.py - - run: python scripts/build_pages.py --output _site + - run: python scripts/build_pages.py --output _site --source-ref "$GITHUB_SHA" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pages-preview-${{ github.sha }} + path: _site + if-no-files-found: error + retention-days: 14 + + codeql: + name: CodeQL (Python) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + languages: python + queries: security-extended + - uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + # This identifier remains stable because main branch protection requires it. conformance-gate: if: always() - needs: [base, stdlib-smoke, release-integrity, release-reproducibility, installed-wheel-smoke, presentation] + needs: [base, coverage, stdlib-smoke, scitt-crypto, artifact-seal, release-integrity, release-reproducibility, installed-wheel-smoke, presentation, codeql] runs-on: ubuntu-latest steps: - name: Require every declared support and artifact check env: BASE: ${{ needs.base.result }} + COVERAGE: ${{ needs.coverage.result }} STDLIB: ${{ needs.stdlib-smoke.result }} + SCITT: ${{ needs.scitt-crypto.result }} + ARTIFACT_SEAL: ${{ needs.artifact-seal.result }} RELEASE: ${{ needs.release-integrity.result }} REPRODUCIBLE: ${{ needs.release-reproducibility.result }} WHEEL: ${{ needs.installed-wheel-smoke.result }} PRESENTATION: ${{ needs.presentation.result }} + CODEQL: ${{ needs.codeql.result }} run: | test "$BASE" = success + test "$COVERAGE" = success test "$STDLIB" = success + test "$SCITT" = success + test "$ARTIFACT_SEAL" = success test "$RELEASE" = success test "$REPRODUCIBLE" = success test "$WHEEL" = success test "$PRESENTATION" = success + test "$CODEQL" = success diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1113b76..dc0671c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,9 @@ jobs: with: python-version: "3.12" + - name: Require TIME CLEAR in the exact tagged checkout + run: python scripts/check_time_status.py + - name: Require a protected-main commit and matching package version env: GH_TOKEN: ${{ github.token }} @@ -36,6 +39,9 @@ jobs: test "$VERSION" = "$PACKAGE_VERSION" test "$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs" --jq '[.check_runs[] | select(.name == "conformance-gate" and .conclusion == "success")] | length')" -ge 1 + - name: Require finalized release metadata in the exact tagged checkout + run: python scripts/check_release_metadata.py --version "${GITHUB_REF_NAME#v}" + - name: Re-run conformance on the tagged checkout run: | python -m pip install ".[test,release]" @@ -52,15 +58,17 @@ jobs: - name: Smoke-test the exact wheel run: | python -m twine check dist/*.whl dist/*.tar.gz - python scripts/check_release_boundary.py dist/*.zip dist/*.whl dist/*.tar.gz + python scripts/check_release_boundary.py dist/*.zip dist/*.whl dist/*.tar.gz dist/*.manifest.json dist/*.cdx.json python -m venv /tmp/vstd-release-wheel /tmp/vstd-release-wheel/bin/python -m pip install --no-deps dist/*.whl + cd /tmp /tmp/vstd-release-wheel/bin/vstd demo --json - /tmp/vstd-release-wheel/bin/vstd plan examples/generic_run/manifest.json --json - /tmp/vstd-release-wheel/bin/vstd run examples/generic_run/manifest.json --output /tmp/vstd-release-receipt + /tmp/vstd-release-wheel/bin/vstd plan "$GITHUB_WORKSPACE/examples/generic_run/manifest.json" --json + /tmp/vstd-release-wheel/bin/vstd run "$GITHUB_WORKSPACE/examples/generic_run/manifest.json" --output /tmp/vstd-release-receipt /tmp/vstd-release-wheel/bin/vstd validate /tmp/vstd-release-receipt /tmp/vstd-release-wheel/bin/vstd reproduce /tmp/vstd-release-receipt --rerun /tmp/vstd-release-wheel/bin/vstd hardware list --json >/dev/null + /tmp/vstd-release-wheel/bin/python -c 'import json; from pathlib import Path; from verifier.core.checker import IndependentAuditor; receipt=json.loads(Path("/tmp/vstd-release-receipt/receipt.json").read_text()); hashes=(receipt["assessment_context"]["verifier"]["specification_hash"], IndependentAuditor.verifier_descriptor().specification_hash); assert all(value.startswith("sha256:") for value in hashes), hashes' - name: Attest every published artifact uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 @@ -69,6 +77,7 @@ jobs: dist/*.zip dist/*.whl dist/*.tar.gz + dist/*.cdx.json dist/*.manifest.json - name: Write bounded release notes @@ -100,17 +109,23 @@ jobs: ' CHANGELOG.md } > release-notes.md - - name: Publish only the tested and attested artifacts + - name: Assemble a complete draft release env: GH_TOKEN: ${{ github.token }} run: | gh release create "$GITHUB_REF_NAME" \ - dist/*.zip dist/*.whl dist/*.tar.gz dist/*.manifest.json \ + dist/*.zip dist/*.whl dist/*.tar.gz dist/*.cdx.json dist/*.manifest.json \ --repo "$GITHUB_REPOSITORY" \ --verify-tag \ + --draft \ --title "VSTD ${GITHUB_REF_NAME}" \ --notes-file release-notes.md + - name: Publish the complete draft atomically + env: + GH_TOKEN: ${{ github.token }} + run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false + - name: Stage only the Python distributions for PyPI uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.zenodo.json b/.zenodo.json index 19f2dd0..acb185a 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -4,7 +4,7 @@ "name": "Roost, Tyler" } ], - "description": "A two-axis verification ladder and reference implementation for bounded claims, provenance graphs, substrate accountability, grounded refutation certificates, and computed verification depth.", + "description": "Release-candidate metadata for a verification-domain language and Python reference implementation that packages bounded computational claims with explicit evidence, checking mechanisms, limits, refutation conditions, provenance, and reproducibility information. It does not replace native domain verifiers or strengthen their results. Publication metadata is assigned only after the release exists.", "keywords": [ "verification", "provenance", @@ -13,10 +13,11 @@ "software supply chain", "accelerator accountability", "refutability", - "proof certificates" + "proof certificates", + "bounded claims" ], "license": "Apache-2.0", - "title": "VSTD: A Two-Axis Ladder for Refutable Verification", - "version": "1.1.3", + "title": "Verifier Standard (VSTD): Bounded, Refutable Evidence for Computational Claims", + "version": "1.2.0", "upload_type": "software" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e766d..82308b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,134 @@ # Changelog +> **Acronyms:** artificial intelligence (AI); Advanced Micro Devices (AMD); application programming interface (API); +> Amazon Web Services (AWS); Concise Binary Object Representation (CBOR); continuous integration (CI); +> command-line interface (CLI); conjunctive normal form (CNF); CBOR Object Signing and Encryption (COSE); +> grounded decision certificate (GDC); Hypertext Transfer Protocol Secure (HTTPS); +> Internet Engineering Task Force (IETF); JavaScript Object Notation (JSON); nondeterministic polynomial time (NP); +> reduced instruction set computer (RISC); Boolean satisfiability problem (SAT); +> Secure Hash Algorithm 256-bit (SHA-256); Secure Hash Algorithm 3 256-bit (SHA3-256); +> Supply Chain Integrity, Transparency, and Trust (SCITT); Secure Shell (SSH); +> Coordinated Universal Time (UTC); Verifier Standard (VSTD); ZIP archive format (ZIP); +> zero-identity/zero-knowledge (ZIZK). + +## 1.2.0 - UNRELEASED + +### Public surface and integrations + +- Restructure the public first-view path around one bounded project description, one + deterministic demonstration, one canonical maturity table, skeptical claim limits, + contributor routes, and release/citation boundaries; align Pages and package metadata + without changing normative or serialized-receipt semantics. +- Normalize the public architecture as a verification complex of named closure + coordinates and cumulative numbered profiles; reserve VSTD-4 rung, candidate depth, + verification order, compatibility level, and checker-cost tier for their distinct uses. +- Add experimental workflow profile 0.1 with deterministic canonicalization, strict + validation, bounded work-allocation records, additive amendments and challenges, + explicit unresolved horizons, and verdict-neutral platform events. +- Add a normalized GitHub adapter for issues, commits, workflow runs, artifacts, and + pull requests. Successful workflows and merges retain `verification_effect = NONE` + unless a separate native result is explicitly mapped through a bound VSTD receipt. +- Add `vstd experiment validate` and `vstd experiment github-events` as offline, + verdict-neutral entry points. Repository artifacts are explicitly `NOT_CHECKED` with + exit code 2 unless their root is supplied. +- Add a machine-readable schema, checked-in verdict-neutral specimen, generated + experiment index, adversarial tests, and a runnable offline example. +- Add a generated CLI/API reference page and presentation gates that reject stale + reference or experiment-index content. +- Clarify VSTD's role as a verification-domain language and interchange layer that + preserves, rather than replaces or strengthens, native verifier results. +- Add the experimental SCITT adapter, rerunnable real-COSE specimen with ephemeral keys, explicit semantic + boundary, and adversarial composition tests without claiming IETF review or payload + truth from registration. +- Surface zero-identity/zero-knowledge (ZIZK) artifact-first TRUST as governing + architecture, publish the bounded RISC Zero reference mechanism and exact recorded + public proof artifacts, and keep only unfinished mechanisms experimental while + preserving unresolved horizons and native-system authority. +- Formally distinguish TRUST as mechanism-earned forward artifact support, ROT as typed + time-indexed degradation of current admissibility, and RUST as an inverse-TRUST memetic + causal backtrace toward recorded ancestor states. None is actor-tied trust, a scalar, + or permission to infer guilt, responsibility, or causal localization from reachability. +- Present current reports, schemas, module descriptions, and examples under the full + VSTD-1 and VSTD-2 numbered-profile identifiers; remove retired partial-profile object identifiers from + active readers and add a regression preventing their return. +- Add normative artifact-control mechanism version 1 with exact-byte file/directory + freezing, SHA-256 plus SHA3-256 artifact-derived identities, observable read-only + guards, readable finite self-closing Ed25519 seals, external anchor checks, and + copy-on-write thaw descendants. Sealing is not encryption and supplies no actor trust, + semantic correctness, trusted time, or numbered VSTD profile result. +- Document multi-temporal realms, discrete and continuous coexistence, causal and + problem-space partial orders, atemporal versus temporal capsules, explicit cross-realm + mappings, and future constrained language-model transition verification without + claiming continuous mediation, inference-law implementation, or textual truth. + +### Claim boundaries and validation + +- Remove the live SimulacraBench rehearsal and its front-door promotion; the repository + never contained or reproduced the submission, hosted image, hardware, or protected + evaluation identified by that name. +- Correct generic-run wording: digest validation is an integrity check, external + references remain unattested until dereferenced and verified, same-path output + extraction is not independent verification, and unverified determinism is `UNKNOWN`. +- Publish a Pages guide index and enforce language, title, viewport, main-region, skip-link, + image-alt, labelled-navigation, generated-reference, and local-link checks in CI. +- Require CodeQL security-extended Python analysis in the protected repository-check + aggregate with only read access to content and write access to security results. +- Fail closed on malformed generic-run receipts, publish their exact schema, and dispatch + `VSTD-1` by its required receipt profile. +- Package every normative specification, verify byte identity, and smoke-test the built + wheel outside the source checkout so installed specification bindings cannot silently + become unavailable. +- Bind the bundled checker to VSTD-1, record actor and execution separation explicitly, + and never infer independent actors from a historical field name, repeated runs, or + matching results. +- Reject self-promoted independence even when every supplied status and digest agrees; + version 1.2.0 has no actor/execution evidence-binding adapter and therefore never + derives `EVIDENCED` from serialized references. +- Require the real optional SCITT/COSE cryptographic example in the protected + repository-check aggregate + rather than allowing its dependency-gated tests to disappear from the base matrix. +- Close generic-run control structures while retaining the released refutation-extension + map, make common receipt commands honor `--json`, and lock `validate` as an + integrity/profile check rather than a claim verifier. + +### Graph and conformance semantics + +- Preserve incompatible Graph assertions as evidence-linked conflict records and label + rating-derived Graph profile numbers as `CALLER_SUPPLIED` candidates with conformance `NOT_ESTABLISHED`. +- Classify the current VSTD-4 candidate-depth calculation as a structural result over + caller-supplied rung references with conformance `NOT_ESTABLISHED`; reject that + candidate at the VSTD-5 entry gate even when its candidate depth is 14. +- Label Graph 2-4 candidates consistently on first-view, documentation, command, schema, + and SCITT surfaces. Keep challenge-ledger state, degradation from status already + recorded in a Graph, and the missing challenge-to-Graph adapter distinct. + +### Release and maintainer controls + +- Mark 1.2.0 metadata as an unreleased release candidate, omit any fabricated release + date, and require the exact tagged checkout to have `TIME.md` set to `Status: CLEAR`. +- Make package/reference status explicitly say VSTD-4 candidate conformance is + `NOT_ESTABLISHED`, and require finalized release metadata in the tag workflow. +- Publish the architecture ownership map linking normative documents, runtime validators, + schemas, and conformance tests. +- Document the five-As human traversal over existing receipt, Graph, hardware, certificate, + reproduction, and SCITT machinery without adding a serialized receipt format; reject duplicate Graph + identifiers and reproduction-fidelity states inferred from declarations, matching verdicts, or + mismatching runs. +- Restore the three non-overlapping operating controls: `AGENTS.md` for automated work, + `HUMANS.md` for human five-As reasoning, and `TIME.md` for current repository + contradictions. Development may record `OPEN`; the exact tagged checkout must be + `CLEAR` before publication. +- Replace the developmental profile-numbered generic-run container with required neutral + `assessment_context`; preserve its mechanism, bound, commitment, and refutation + coordinates without carrying a VSTD-4 conformance field. + ## 1.1.3 - 2026-08-22 - Canonicalize source ZIP timestamps in UTC and remove host ZIP metadata, so the same Git coordinate produces byte-identical source archives on Windows and Linux. - Canonicalize generated wheel and source-distribution newlines, archive member order, modes, timestamps, and ownership. Rebuild wheel `RECORD` after normalization - and use compression-independent ZIP members plus a stable USTAR/gzip container. + and use compression-independent ZIP members plus a stable `ustar`/gzip container. - Normalize common HTTPS and SSH spellings of the Git origin before recording the public repository coordinate in a release manifest. - Require CI to build the complete release artifact set independently on Windows and @@ -26,7 +148,7 @@ manifests published through `v1.1.1` that bind `verifiable-standard-.zip` remain verifiable without republishing. - Record the import-package, distribution, and archive renames in - `WIRE_IDENTIFIERS.md`. No receipt wire identifier, schema `$id`, or canonical digest + `WIRE_IDENTIFIERS.md`. No receipt serialized receipt identifier, schema `$id`, or canonical digest changes. - Attribute the specifications, distribution metadata, and governance decision rights to `TimeLordRaps`. The legal name remains the copyright holder in `NOTICE`. @@ -42,7 +164,7 @@ - Rename the VSTD-2 section 7 lifecycle term `VERIFIABLE` to `GEOMETRY_INSPECTABLE` and record in `WIRE_IDENTIFIERS.md` that the section 7 vocabulary is prose-only, so - no status token reuses the maintainer's name and no wire value changes. + no status token reuses the maintainer's name and no serialized receipt value changes. - Label the reference emulator's synthetic accelerator descriptor `vendor` as `EMULATED` instead of the maintainer's name, so fabricated hardware evidence cannot read as maintainer attestation. @@ -52,7 +174,7 @@ - Correct the SimulacraBench synthetic specimen additively: unobserved private artifacts now remain `IDENTIFIED`, and the public challenge stops at - `CHALLENGED` without a founder-authored adjudication. + `CHALLENGED` without a maintainer-authored adjudication. - Require content-bound observed bytes before deriving `AVAILABLE` or `PORTABLE`; locator and retention declarations alone no longer elevate availability. - Expand the public presentation gate to reject drive-qualified paths, private @@ -62,7 +184,7 @@ ## 1.1.1 - 2026-08-22 - Replace the overview's generic maturity badges with the exact status of every - object and graph layer, so the presentation cannot imply evidence or + object and Graph numbered profile, so the presentation cannot imply evidence or implementation maturity that the specifications do not establish. - Enforce those visual labels in the presentation gate and publish canonical receipt schemas at their declared GitHub Pages `$id` routes. @@ -88,13 +210,13 @@ ## 1.0.1 - 2026-08-22 -- State explicitly that each VSTD layer requires its own evidence: layer 4 does not - supply, entail, upgrade, or repair layers 3, 2, or 1. +- State explicitly that each VSTD closure coordinate requires its own evidence: + Refutability does not supply, entail, upgrade, or repair prerequisite coordinates. - Replace unsupported Tarski, generic NP-certificate, CNF-equals-3-SAT, and physical-world co-NP claims with bounded statements tied to implemented formal languages and declared observation surfaces. -- Replace adopter-migration framing with a frozen wire-identifier and historical - project-filename registry; no external adoption is claimed. +- Replace adopter-migration framing with an exact current wire-dispatch registry; no + external adoption is claimed. - Generate source releases from exact public Git objects and publish a separate manifest binding the resolvable ref, commit, archive digest, file set, and member bytes. Line-ending equivalence is not accepted as byte identity. @@ -111,27 +233,29 @@ ## 1.0.0 - 2026-08-22 -- Redesign specification numbers as verification-depth layers: VSTD-1 through +- Redesign specification numbers as cumulative numbered profiles: VSTD-1 through VSTD-5 on the object axis and VSTD-Graph-1 through VSTD-Graph-5 on the collection axis. -- Hard-rename the historical specification paths while preserving issued receipt - wire identifiers and the `v0.1.0` and `v0.2.0` release history. -- Implement the fourteen-rung VSTD-4 refutability ladder and compute depth by - iterated satisfiability rather than accepting a declared level. +- Establish integer numbered-profile specification paths while release history remains available in + the corresponding Git tags. +- Add the fourteen-rung VSTD-4 structural calculation and compute its candidate depth by + iterated satisfiability rather than copying a declared depth. Version 1.2.0 clarifies + that its caller-supplied references do not establish VSTD-4 conformance. - Add the `VSTD4-GDC-1` grounded decision-certificate format, independent bounded checker, Horn/unit-propagation tier, width-bounded and general-resolution tiers, and evidence-bearing `UNKNOWN` results on exhaustion. - Add machine-readable refutation surfaces, precommitment envelopes, availability assessment, append-only challenge adjudication, monotonic degradation, and refutability closure. -- Compute VSTD-Graph level from membership, provenance closure, status, and edge - evidence, with a certificate explaining the next unreachable level. +- Preserve the historical `graph_level` compatibility calculation from membership, provenance closure, + status, and caller-supplied edge ratings, with a certificate explaining the next + unreachable candidate Graph profile. Version 1.2.0 labels conformance `NOT_ESTABLISHED`. - Replace fabricated conflict evidence, literal trust-boundary claims, and decorative policy certificates with checked evidence and fail-closed divergence. - Publish a draft VSTD-5 witness-corroboration interface. No independent witness implementation or interoperability claim is included. -- Move layer-specific and profile documentation under `docs/` and publish schemas - with stable layer-oriented filenames. +- Move profile-specific documentation under `docs/` and publish schemas with stable + compatibility filenames and paths. ## 0.2.0 - 2026-08-21 @@ -157,7 +281,8 @@ ## 0.1.0 - 2026-08-21 -- Publish VSTD-0.1, VSTD-DATA-0.1, and experimental VSTD-0.2. +- Publish the initial claim-mechanics, provenance-graph, and experimental + verification-geometry surfaces. - Publish zero-required-dependency receipt, provenance, geometry, and policy primitives. - Publish an optional logits-level constraint kernel with atomic dependency profiles. - Add a target-neutral public CLI for generic-run and stored VSTD-DATA receipts. diff --git a/CITATION.cff b/CITATION.cff index 1283e42..5e9e71e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,11 +1,10 @@ cff-version: 1.2.0 -message: "If you use VSTD or its reference implementation, cite this release." -title: "VSTD: A Two-Axis Ladder for Refutable Verification" +message: "This describes the Verifier Standard (VSTD) 1.2.0 release candidate; cite the published release after it exists." +title: "Verifier Standard (VSTD): Bounded, Refutable Evidence for Computational Claims" type: software authors: - name: "TimeLordRaps" -version: 1.1.3 -date-released: 2026-08-22 +version: 1.2.0 license: Apache-2.0 repository-code: "https://github.com/TimeLordRaps/verifier" keywords: @@ -17,3 +16,4 @@ keywords: - accelerator accountability - refutability - proof certificates + - bounded claims diff --git a/RELEASING.md b/RELEASING.md index 89df313..32fc556 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,12 +1,31 @@ # Release procedure +> **Acronyms:** application programming interface (API); carriage return and line feed (CRLF); digital object identifier (DOI); +> hash-based message authentication code (HMAC); line feed (LF); Secure Hash Algorithm 256-bit (SHA-256); +> Software Bill of Materials (SBOM); Coordinated Universal Time (UTC); ZIP archive format (ZIP). + Public releases are built only from a commit already present in the public repository. The release manifest is published beside the source ZIP rather than tracked inside the source tree. This avoids a self-referential commit field and lets the manifest bind an exact, publicly resolvable commit. +Development branches may record precise contradictions with [`TIME.md`](TIME.md) set to +`Status: OPEN`; normal pull-request checks do not prohibit that state. Publication is +different: the tag-triggered workflow runs `python scripts/check_time_status.py` against +the exact tagged checkout and fails unless it contains exactly one `Status: CLEAR` line. +There is no subjective override. + +The source version may be prepared as 1.2.0 while the release does not exist. During that +period, `CHANGELOG.md` says `UNRELEASED`, `CITATION.cff` identifies a release candidate and +has no `date-released`, and install instructions distinguish a source checkout from the +latest published package. Before tagging, land an explicit release-finalization change that +uses the actual publication date consistently in the changelog and citation metadata; do +not fabricate or backdate it. The tag workflow enforces this with +`python scripts/check_release_metadata.py --version ` and also refuses +release-candidate Zenodo metadata. + 1. Merge the versioned release change through the public pull-request workflow. Require - every protected conformance check on the exact candidate commit. + the protected repository-check aggregate to pass on the exact candidate commit. 2. From a clean checkout of that commit, run: ```bash @@ -17,7 +36,7 @@ exact, publicly resolvable commit. 3. Build a pre-tag candidate from the full commit SHA, not a working directory: ```bash - VERSION=1.1.3 + VERSION=1.2.0 python scripts/release_artifacts.py build \ --ref FULL_PUBLIC_COMMIT_SHA --release "$VERSION" --output-dir dist/candidate ``` @@ -29,11 +48,14 @@ exact, publicly resolvable commit. Generated packaging text is normalized to LF; wheel `RECORD` is rebuilt after normalization; ZIP metadata, tar metadata, gzip metadata, ownership, modes, and member order are canonical. The build fails unless each pair is byte-identical and both - distributions declare `verifier-standard`, version `1.1.3`, import package `verifier`, - and the frozen three console scripts. + distributions declare `verifier-standard`, version `1.2.0`, import package `verifier`, + and the frozen three console scripts. It also emits a deterministic CycloneDX 1.6 + SBOM whose components bind the source ZIP, wheel, and source distribution by SHA-256 + and byte size. The external manifest binds the SBOM digest; the SBOM does not list + itself, avoiding self-reference. - The protected conformance gate separately builds this complete artifact set on - Windows and Linux and compares every byte. Do not prepare a tag unless that + 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 cross-platform comparison passed on the exact candidate commit. 4. Run `twine check` on the candidate wheel and source distribution. Install the @@ -41,19 +63,22 @@ exact, publicly resolvable commit. `vstd hardware list --json`, and the deterministic virtual probe/verification lifecycle. Test each optional dependency profile independently; never place a test HMAC key in a committed fixture. -5. Require a zero-match boundary scan of the candidate source archive, wheel, and source - distribution for +5. Require a zero-match boundary scan of the candidate source archive, wheel, source + distribution, external manifest, and SBOM for private project names, proprietary model identifiers, local or home-directory paths, credentials, and personal email addresses. -6. Create the release tag locally at the exact tested commit. Prefer a cryptographically +6. Confirm `python scripts/check_time_status.py` passes, release-candidate metadata has + been finalized with the actual intended publication date, and then create the release + tag locally at the exact tested commit. Prefer a cryptographically signed annotated tag when the maintainer's signing key is registered and available. Rebuild using the tag coordinate. The source ZIP, wheel, and source distribution MUST - be byte-identical to the commit-coordinate candidate. The external manifest MUST - differ only where its `source.ref` changes from the full commit SHA to the tag ref, + be byte-identical to the commit-coordinate candidate. The SBOM also remains + byte-identical because it binds the resolved commit rather than the ref spelling. The + external manifest MUST differ only where its `source.ref` changes from the full commit SHA to the tag ref, plus the manifest's own resulting digest: ```bash - VERSION=1.1.3 + VERSION=1.2.0 git tag -s "v$VERSION" FULL_PUBLIC_COMMIT_SHA python scripts/release_artifacts.py build \ --ref "refs/tags/v$VERSION" --release "$VERSION" --output-dir dist/tagged @@ -62,12 +87,12 @@ exact, publicly resolvable commit. If tag signing is unavailable, an unsigned annotated tag is permitted only through `.github/workflows/release.yml`. That workflow records the GitHub tag-object verification result and reason in the release notes and MUST create GitHub/Sigstore - artifact attestations for the source ZIP, wheel, source distribution, and external - manifest. An artifact attestation is not described as a tag signature. + artifact attestations for the source ZIP, wheel, source distribution, SBOM, and + external manifest. An artifact attestation is not described as a tag signature. 7. Run the verifier independently before upload: ```bash - VERSION=1.1.3 + VERSION=1.2.0 python scripts/release_artifacts.py verify \ "dist/tagged/verifier-standard-$VERSION.manifest.json" ``` @@ -76,10 +101,12 @@ exact, publicly resolvable commit. file set and every member byte MUST match that commit. CRLF/LF equivalence is not accepted as byte identity. 8. Push the tag only after all preceding checks pass. The tag-triggered release workflow - rechecks protected-main ancestry, package version, the successful `conformance-gate`, - the full test suite, deterministic build, installed wheel, and artifact manifest. - It then attests and publishes exactly the tested source ZIP, wheel, source - distribution, and external release manifest to the GitHub release. A second job can + rechecks protected-main ancestry, package version, the successful protected + repository-check aggregate (the `conformance-gate` status context), the full test + suite, deterministic build, installed wheel, and artifact manifest. + It then attests the tested source ZIP, wheel, source distribution, SBOM, and external + release manifest. The workflow creates a draft, attaches the complete set, and only + then publishes it. A second job can access only the wheel and source distribution, requires approval in the protected `pypi` environment, and publishes them through the configured PyPI Trusted Publisher. Existing tags and release assets remain untouched; corrections are additive. @@ -89,10 +116,27 @@ exact, publicly resolvable commit. gh attestation verify PATH_TO_ASSET --repo TimeLordRaps/verifier ``` - An attestation complements but does not replace the release manifest, and it does not - turn an unsigned tag into a signed tag. + An attestation and SBOM complement but do not replace the release manifest, and neither + turns an unsigned tag into a signed tag. + + GitHub release immutability is a repository setting that applies only to future + releases. GitHub documents both the + [repository setting](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes) + and the + [versioned API](https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10#check-if-immutable-releases-are-enabled-for-a-repository). + Check the endpoint before tagging: + + ```bash + gh api -H "X-GitHub-Api-Version: 2026-03-10" \ + repos/TimeLordRaps/verifier/immutable-releases + ``` + + Require `enabled: true`. Immutability locks the published tag and attached assets and + generates a GitHub release attestation; it does not correct false metadata. Corrections, + revocations, and superseding releases remain additive. Enable the setting only after + the draft-first workflow is present on the protected release commit. 10. Let Zenodo archive the GitHub release, then record the issued DOI additively. -11. Confirm that `https://pypi.org/project/verifier-standard/1.1.3/` lists the same wheel +11. Confirm that `https://pypi.org/project/verifier-standard/1.2.0/` lists the same wheel and source-distribution SHA-256 values as the GitHub release and external manifest. PyPI ownership establishes control of the distribution coordinate only; it does not establish adoption, consensus, certification, or exclusive control of the Python diff --git a/scripts/check_release_boundary.py b/scripts/check_release_boundary.py index ef5908b..cc4d4e4 100644 --- a/scripts/check_release_boundary.py +++ b/scripts/check_release_boundary.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -"""Fail closed when a release archive contains private or secret-shaped text.""" +"""Terminology: Unicode Transformation Format, 8-bit (UTF-8); Verifier Standard (VSTD). + +Fail closed when a release archive contains private or secret-shaped text.""" from __future__ import annotations @@ -70,6 +72,9 @@ def check_artifact(path: Path, errors: list[str]) -> int: return _scan_zip(path, errors) if path.name.endswith(".tar.gz"): return _scan_tar(path, errors) + if path.name.endswith(".json"): + _scan_text(path, path.name, path.read_bytes(), errors) + return 1 raise ValueError(f"unsupported release artifact: {path}") diff --git a/scripts/check_release_metadata.py b/scripts/check_release_metadata.py new file mode 100644 index 0000000..7c38575 --- /dev/null +++ b/scripts/check_release_metadata.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Require finalized, internally consistent metadata before tag publication.""" + +from __future__ import annotations + +import argparse +from datetime import date +import json +from pathlib import Path +import re +import sys + + +ROOT = Path(__file__).resolve().parents[1] + + +def _single(pattern: str, text: str, label: str) -> str: + matches = re.findall(pattern, text, re.MULTILINE) + if len(matches) != 1: + raise ValueError(f"{label} must appear exactly once; observed {len(matches)}") + return str(matches[0]) + + +def require_finalized(root: Path, version: str) -> None: + """Reject release-candidate or inconsistent metadata for ``version``.""" + + pyproject = (root / "pyproject.toml").read_text(encoding="utf-8") + project = pyproject.split("[project]", 1) + project_text = "" if len(project) != 2 else project[1].split("\n[", 1)[0] + package_version = _single( + r'^version\s*=\s*"([^"]+)"$', project_text, "pyproject [project] version" + ) + if package_version != version: + raise ValueError( + f"release version {version} does not match package version {package_version}" + ) + + changelog = (root / "CHANGELOG.md").read_text(encoding="utf-8") + if re.search( + rf"^## {re.escape(version)} - UNRELEASED$", changelog, re.MULTILINE + ): + raise ValueError(f"CHANGELOG {version} is still UNRELEASED") + release_date = _single( + rf"^## {re.escape(version)} - (\d{{4}}-\d{{2}}-\d{{2}})$", + changelog, + f"dated CHANGELOG {version} heading", + ) + try: + date.fromisoformat(release_date) + except ValueError as exc: + raise ValueError(f"CHANGELOG release date is invalid: {release_date}") from exc + + citation = (root / "CITATION.cff").read_text(encoding="utf-8") + citation_version = _single( + r"^version:\s*([^\s]+)$", citation, "CITATION version" + ) + citation_date = _single( + r"^date-released:\s*(\d{4}-\d{2}-\d{2})$", + citation, + "CITATION date-released", + ) + if citation_version != version or citation_date != release_date: + raise ValueError( + "CITATION version/date must match the package and CHANGELOG release coordinate" + ) + if "release candidate" in citation.lower(): + raise ValueError("CITATION still describes a release candidate") + + zenodo = json.loads((root / ".zenodo.json").read_text(encoding="utf-8")) + if zenodo.get("version") != version: + raise ValueError("Zenodo version does not match the release coordinate") + description = str(zenodo.get("description", "")).lower() + if "release-candidate" in description or "after the release exists" in description: + raise ValueError("Zenodo metadata still describes an unpublished candidate") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--version", required=True) + parser.add_argument("--root", type=Path, default=ROOT) + args = parser.parse_args(argv) + try: + require_finalized(args.root, args.version) + except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: + print(f"[RELEASE METADATA BLOCKED] {exc}", file=sys.stderr) + return 1 + print(f"[RELEASE METADATA FINAL] {args.version}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_time_status.py b/scripts/check_time_status.py new file mode 100644 index 0000000..e03955d --- /dev/null +++ b/scripts/check_time_status.py @@ -0,0 +1,36 @@ +"""Fail closed unless the named TIME file has exactly ``Status: CLEAR``.""" + +from __future__ import annotations + +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[1] + + +def require_clear(path: Path) -> None: + lines = [ + line for line in path.read_text(encoding="utf-8").splitlines() + if line.startswith("Status:") + ] + if lines != ["Status: CLEAR"]: + raise ValueError( + f"release requires exactly one Status: CLEAR line; observed {lines or ['MISSING']}" + ) + + +def main(argv: list[str] | None = None) -> int: + args = sys.argv[1:] if argv is None else argv + path = Path(args[0]) if args else ROOT / "TIME.md" + try: + require_clear(path) + except (OSError, UnicodeError, ValueError) as exc: + print(f"[TIME BLOCKED] {exc}", file=sys.stderr) + return 1 + print("[TIME CLEAR] release invariant satisfied") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py index cd3a6b5..ace0685 100644 --- a/scripts/release_artifacts.py +++ b/scripts/release_artifacts.py @@ -1,4 +1,7 @@ -"""Build and verify public release artifacts from an exact public Git ref. +"""Terminology: Secure Hash Algorithm 256-bit (SHA-256); Software Bill of Materials +(SBOM); uniform resource locator (URL); Verifier Standard (VSTD); ZIP archive format (ZIP). + +Build and verify public release artifacts from an exact public Git ref. The release manifest is an artifact beside the source ZIP, not a tracked file inside the source tree. That avoids a self-referential commit hash and makes ``source_commit`` @@ -28,10 +31,11 @@ import tempfile import zipfile from pathlib import Path -from typing import Any +from typing import Any, Mapping SCHEMA_VERSION = "VSTD-PUBLIC-RELEASE-1.1" +CYCLONEDX_SPEC_VERSION = "1.6" # Archive stem for releases built from this tree. Releases up to and including v1.1.1 # were published as `verifiable-standard-.zip`; their manifests carry that # prefix and are still verified from the manifest itself. @@ -75,6 +79,95 @@ def _file_record(path: Path) -> dict[str, Any]: return {"byte_size": len(data), "sha256": _sha256(data)} +def _cyclonedx_component(filename: str, record: Mapping[str, Any]) -> dict[str, Any]: + return { + "type": "file", + "bom-ref": f"artifact:{filename}", + "name": filename, + "hashes": [{"alg": "SHA-256", "content": str(record["sha256"])}], + "properties": [{"name": "vstd:byte-size", "value": str(record["byte_size"])}], + } + + +def _cyclonedx_payload( + *, + commit: str, + epoch: str, + release: str, + artifacts: Mapping[str, Mapping[str, Any]], +) -> dict[str, Any]: + timestamp = datetime.fromtimestamp(int(epoch), timezone.utc).isoformat().replace( + "+00:00", "Z" + ) + root_ref = f"pkg:pypi/{DISTRIBUTION_NAME}@{release}" + components = [ + _cyclonedx_component(name, artifacts[name]) for name in sorted(artifacts) + ] + return { + "$schema": "https://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": CYCLONEDX_SPEC_VERSION, + "version": 1, + "metadata": { + "timestamp": timestamp, + "component": { + "type": "library", + "bom-ref": root_ref, + "name": DISTRIBUTION_NAME, + "version": release, + "purl": root_ref, + "licenses": [{"license": {"id": "Apache-2.0"}}], + "properties": [{"name": "vstd:source-commit", "value": commit}], + }, + }, + "components": components, + "dependencies": [ + { + "ref": root_ref, + "dependsOn": [component["bom-ref"] for component in components], + } + ], + } + + +def _write_cyclonedx_sbom( + destination: Path, + *, + commit: str, + epoch: str, + release: str, + artifacts: Mapping[str, Mapping[str, Any]], +) -> None: + destination.write_text( + json.dumps( + _cyclonedx_payload( + commit=commit, epoch=epoch, release=release, artifacts=artifacts + ), + indent=2, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + newline="\n", + ) + + +def _verify_cyclonedx_sbom( + path: Path, + *, + commit: str, + epoch: str, + release: str, + artifacts: Mapping[str, Mapping[str, Any]], +) -> None: + payload = json.loads(path.read_text(encoding="utf-8")) + expected = _cyclonedx_payload( + commit=commit, epoch=epoch, release=release, artifacts=artifacts + ) + if payload != expected: + raise ReleaseError("CycloneDX SBOM bytes do not match the bound release subjects") + + def _resolved_commit(repo: Path, ref: str) -> str: return _run(repo, "git", "rev-parse", f"{ref}^{{commit}}").decode().strip() @@ -522,7 +615,7 @@ def _build_sdist_once( def build_all( repo: Path, ref: str, release: str, output_dir: Path -) -> tuple[Path, Path, Path, Path]: +) -> tuple[Path, Path, Path, Path, Path]: archive, manifest_path = build_source(repo, ref, release, output_dir) commit = _resolved_commit(repo.resolve(), ref) epoch = _run(repo.resolve(), "git", "show", "-s", "--format=%ct", commit).decode().strip() @@ -550,11 +643,26 @@ def build_all( manifest = json.loads(manifest_path.read_text(encoding="utf-8")) manifest["artifacts"][wheel.name] = _file_record(wheel) manifest["artifacts"][sdist.name] = _file_record(sdist) + sbom = output_dir / f"{ARCHIVE_STEM}-{release}.cdx.json" + _write_cyclonedx_sbom( + sbom, + commit=commit, + epoch=epoch, + release=release, + artifacts=manifest["artifacts"], + ) + manifest["sbom"] = { + "filename": sbom.name, + "format": "CycloneDX", + "spec_version": CYCLONEDX_SPEC_VERSION, + "subjects": sorted(manifest["artifacts"]), + } + manifest["artifacts"][sbom.name] = _file_record(sbom) manifest_path.write_text( json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8", newline="\n" ) verify_manifest(repo, manifest_path, output_dir) - return archive, wheel, sdist, manifest_path + return archive, wheel, sdist, sbom, manifest_path def compare_artifact_directories(first: Path, second: Path) -> int: @@ -614,6 +722,29 @@ def verify_manifest(repo: Path, manifest_path: Path, artifact_dir: Path | None = if not path.is_file() or _file_record(path) != expected: raise ReleaseError(f"artifact digest or byte size mismatch: {filename}") + sbom_record = manifest.get("sbom") + if sbom_record is not None: + sbom_name = str(sbom_record.get("filename", "")) + subjects = sorted(name for name in artifacts if name != sbom_name) + if sbom_record != { + "filename": sbom_name, + "format": "CycloneDX", + "spec_version": CYCLONEDX_SPEC_VERSION, + "subjects": subjects, + }: + raise ReleaseError("release manifest SBOM binding is not canonical") + if not sbom_name.endswith(".cdx.json") or sbom_name not in artifacts: + raise ReleaseError("release manifest does not bind its declared SBOM") + _verify_cyclonedx_sbom( + artifact_dir / sbom_name, + commit=commit, + epoch=_run(repo, "git", "show", "-s", "--format=%ct", commit) + .decode() + .strip(), + release=str(manifest["release"]), + artifacts={name: artifacts[name] for name in subjects}, + ) + distribution = manifest.get("distribution") if distribution is not None: expected_distribution = { @@ -690,12 +821,13 @@ def main(argv: list[str] | None = None) -> int: print(f"[PASS] source archive: {archive}") print(f"[PASS] release manifest: {manifest}") elif args.command == "build": - archive, wheel, sdist, manifest = build_all( + archive, wheel, sdist, sbom, manifest = build_all( args.repo, args.ref, args.release, args.output_dir ) print(f"[PASS] source archive: {archive}") print(f"[PASS] reproducible wheel: {wheel}") print(f"[PASS] reproducible sdist: {sdist}") + print(f"[PASS] CycloneDX SBOM: {sbom}") print(f"[PASS] release manifest: {manifest}") elif args.command == "verify": verify_manifest(args.repo, args.manifest, args.artifact_dir) diff --git a/tests/test_release_artifacts.py b/tests/test_release_artifacts.py index 98f0cf7..b04d404 100644 --- a/tests/test_release_artifacts.py +++ b/tests/test_release_artifacts.py @@ -1,4 +1,6 @@ -"""The public source archive must bind exact, publicly resolvable Git bytes.""" +"""Terminology: Verifier Standard (VSTD); ZIP archive format (ZIP). + +The public source archive must bind exact, publicly resolvable Git bytes.""" from __future__ import annotations @@ -19,12 +21,21 @@ REPO_ROOT = Path(__file__).resolve().parents[1] SCRIPT = REPO_ROOT / "scripts" / "release_artifacts.py" RELEASE_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "release.yml" +TIME_GATE = REPO_ROOT / "scripts" / "check_time_status.py" +RELEASE_METADATA_GATE = REPO_ROOT / "scripts" / "check_release_metadata.py" SPEC = importlib.util.spec_from_file_location("vstd_release_artifacts", SCRIPT) assert SPEC is not None and SPEC.loader is not None release_artifacts = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(release_artifacts) +METADATA_SPEC = importlib.util.spec_from_file_location( + "vstd_release_metadata", RELEASE_METADATA_GATE +) +assert METADATA_SPEC is not None and METADATA_SPEC.loader is not None +release_metadata = importlib.util.module_from_spec(METADATA_SPEC) +METADATA_SPEC.loader.exec_module(release_metadata) + def test_source_release_manifest_binds_head_and_exact_archive_bytes(tmp_path: Path) -> None: result = subprocess.run( @@ -130,16 +141,16 @@ def test_repository_url_spellings_are_canonical(raw: str, expected: str) -> None def _write_raw_wheel(path: Path, *, newline: bytes, reverse: bool) -> None: - dist_info = "verifier_standard-1.1.3.dist-info" + dist_info = "verifier_standard-1.2.0.dist-info" members = [ - ("verifier/__init__.py", b'__version__ = "1.1.3"\n'), + ("verifier/__init__.py", b'__version__ = "1.2.0"\n'), ( f"{dist_info}/METADATA", newline.join( [ b"Metadata-Version: 2.4", b"Name: verifier-standard", - b"Version: 1.1.3", + b"Version: 1.2.0", b"", b"Canonical metadata.", b"", @@ -200,9 +211,9 @@ def test_wheel_normalization_removes_host_newlines_and_zip_metadata(tmp_path: Pa infos = bundle.infolist() assert all(info.create_system == 3 for info in infos) assert all(info.compress_type == zipfile.ZIP_STORED for info in infos) - metadata_name = "verifier_standard-1.1.3.dist-info/METADATA" + metadata_name = "verifier_standard-1.2.0.dist-info/METADATA" assert b"\r" not in bundle.read(metadata_name) - record_name = "verifier_standard-1.1.3.dist-info/RECORD" + record_name = "verifier_standard-1.2.0.dist-info/RECORD" rows = list(csv.reader(io.StringIO(bundle.read(record_name).decode("utf-8")))) records = {row[0]: row[1:] for row in rows} for info in infos: @@ -217,7 +228,7 @@ def test_wheel_normalization_removes_host_newlines_and_zip_metadata(tmp_path: Pa def _write_raw_sdist(path: Path, *, newline: bytes, reverse: bool) -> None: - root = "verifier_standard-1.1.3" + root = "verifier_standard-1.2.0" members = [ ( f"{root}/PKG-INFO", @@ -225,7 +236,7 @@ def _write_raw_sdist(path: Path, *, newline: bytes, reverse: bool) -> None: [ b"Metadata-Version: 2.4", b"Name: verifier-standard", - b"Version: 1.1.3", + b"Version: 1.2.0", b"", ] ), @@ -237,7 +248,7 @@ def _write_raw_sdist(path: Path, *, newline: bytes, reverse: bool) -> None: [ b"Metadata-Version: 2.4", b"Name: verifier-standard", - b"Version: 1.1.3", + b"Version: 1.2.0", b"", ] ), @@ -272,7 +283,7 @@ def test_sdist_normalization_removes_host_newlines_and_tar_metadata(tmp_path: Pa with tarfile.open(first, "r:gz") as bundle: files = {member.name: member for member in bundle.getmembers()} - root = "verifier_standard-1.1.3" + root = "verifier_standard-1.2.0" metadata = bundle.extractfile(files[f"{root}/PKG-INFO"]) assert metadata is not None and b"\r" not in metadata.read() readme = bundle.extractfile(files[f"{root}/README.md"]) @@ -295,6 +306,44 @@ def test_artifact_directory_comparison_fails_closed(tmp_path: Path) -> None: release_artifacts.compare_artifact_directories(first, second) +def test_cyclonedx_sbom_is_deterministic_bound_and_non_self_referential( + tmp_path: Path, +) -> None: + artifacts = { + "verifier-standard-1.2.0.zip": { + "byte_size": 3, + "sha256": release_artifacts._sha256(b"zip"), + }, + "verifier_standard-1.2.0-py3-none-any.whl": { + "byte_size": 5, + "sha256": release_artifacts._sha256(b"wheel"), + }, + } + arguments = { + "commit": "a" * 40, + "epoch": "1787446816", + "release": "1.2.0", + "artifacts": artifacts, + } + first = tmp_path / "first.cdx.json" + second = tmp_path / "second.cdx.json" + release_artifacts._write_cyclonedx_sbom(first, **arguments) + release_artifacts._write_cyclonedx_sbom(second, **arguments) + + assert first.read_bytes() == second.read_bytes() + payload = json.loads(first.read_text(encoding="utf-8")) + assert payload["bomFormat"] == "CycloneDX" + assert payload["specVersion"] == "1.6" + assert {component["name"] for component in payload["components"]} == set(artifacts) + assert first.name not in {component["name"] for component in payload["components"]} + release_artifacts._verify_cyclonedx_sbom(first, **arguments) + + payload["components"][0]["hashes"][0]["content"] = "0" * 64 + first.write_text(json.dumps(payload), encoding="utf-8") + with pytest.raises(release_artifacts.ReleaseError, match="bound release subjects"): + release_artifacts._verify_cyclonedx_sbom(first, **arguments) + + def test_release_notes_use_the_github_tag_object_verification() -> None: workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8") assert 'git/tags/$TAG_OBJECT' in workflow @@ -302,3 +351,130 @@ def test_release_notes_use_the_github_tag_object_verification() -> None: assert ".verification.reason" in workflow assert "SIGNED_AND_GITHUB_VERIFIED" in workflow assert 'git verify-tag "$GITHUB_REF_NAME"' not in workflow + + +def test_release_is_drafted_with_attested_sbom_before_publication() -> None: + workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8") + create = workflow.index('gh release create "$GITHUB_REF_NAME"') + publish = workflow.index('gh release edit "$GITHUB_REF_NAME"') + + assert "dist/*.cdx.json" in workflow + assert "--draft" in workflow[create:publish] + assert "--draft=false" in workflow[publish:] + assert create < publish + + +@pytest.mark.parametrize( + "status", ["OPEN", "CONFLICTED", "", "CLEAR\nStatus: CLEAR", "CLEAR\nStatus: open"] +) +def test_release_time_gate_rejects_every_non_exact_clear_state( + tmp_path: Path, status: str +) -> None: + time_file = tmp_path / "TIME.md" + time_file.write_text(f"# TIME\n\nStatus: {status}\n", encoding="utf-8") + result = subprocess.run( + [sys.executable, str(TIME_GATE), str(time_file)], + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 1 + assert "[TIME BLOCKED]" in result.stderr + + +def test_tag_release_requires_clear_time_from_the_exact_checkout(tmp_path: Path) -> None: + time_file = tmp_path / "TIME.md" + time_file.write_text("# TIME\n\nStatus: CLEAR\n", encoding="utf-8") + result = subprocess.run( + [sys.executable, str(TIME_GATE), str(time_file)], + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0 + assert "[TIME CLEAR]" in result.stdout + + workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8") + assert "Require TIME CLEAR in the exact tagged checkout" in workflow + assert "python scripts/check_time_status.py" in workflow + assert workflow.index("python scripts/check_time_status.py") < workflow.index( + "python -m pytest -q" + ) + + +def _write_final_release_metadata(root: Path) -> None: + (root / "pyproject.toml").write_text( + '[project]\nname = "verifier-standard"\nversion = "1.2.0"\n', + encoding="utf-8", + ) + (root / "CHANGELOG.md").write_text( + "# Changelog\n\n## 1.2.0 - 2026-08-26\n", encoding="utf-8" + ) + (root / "CITATION.cff").write_text( + 'cff-version: 1.2.0\nmessage: "Cite this published release."\n' + "version: 1.2.0\ndate-released: 2026-08-26\n", + encoding="utf-8", + ) + (root / ".zenodo.json").write_text( + json.dumps({"version": "1.2.0", "description": "Final publication metadata."}), + encoding="utf-8", + ) + + +@pytest.mark.parametrize( + "fault", + ( + "unreleased_changelog", + "missing_citation_date", + "mismatched_citation_date", + "candidate_citation", + "candidate_zenodo", + "package_version", + ), +) +def test_release_metadata_gate_rejects_unfinalized_or_inconsistent_state( + tmp_path: Path, fault: str +) -> None: + _write_final_release_metadata(tmp_path) + if fault == "unreleased_changelog": + path = tmp_path / "CHANGELOG.md" + path.write_text(path.read_text().replace("2026-08-26", "UNRELEASED")) + elif fault == "missing_citation_date": + path = tmp_path / "CITATION.cff" + path.write_text(path.read_text().replace("date-released: 2026-08-26\n", "")) + elif fault == "mismatched_citation_date": + path = tmp_path / "CITATION.cff" + path.write_text(path.read_text().replace("2026-08-26", "2026-08-25")) + elif fault == "candidate_citation": + path = tmp_path / "CITATION.cff" + path.write_text(path.read_text().replace("published release", "release candidate")) + elif fault == "candidate_zenodo": + path = tmp_path / ".zenodo.json" + path.write_text( + json.dumps({"version": "1.2.0", "description": "Release-candidate metadata."}) + ) + else: + path = tmp_path / "pyproject.toml" + path.write_text(path.read_text().replace("1.2.0", "1.1.3")) + + with pytest.raises(ValueError): + release_metadata.require_finalized(tmp_path, "1.2.0") + + +def test_release_metadata_gate_accepts_one_final_consistent_coordinate(tmp_path: Path) -> None: + _write_final_release_metadata(tmp_path) + release_metadata.require_finalized(tmp_path, "1.2.0") + + +def test_tag_release_contract_binds_main_version_gate_and_final_metadata() -> None: + workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8") + required = ( + 'git merge-base --is-ancestor "$GITHUB_SHA" origin/main', + 'git rev-parse "${GITHUB_REF}^{commit}"', + 'test "$VERSION" = "$PACKAGE_VERSION"', + 'commits/$GITHUB_SHA/check-runs', + 'select(.name == "conformance-gate" and .conclusion == "success")', + 'python scripts/check_release_metadata.py --version "${GITHUB_REF_NAME#v}"', + ) + for fragment in required: + assert fragment in workflow From 79ae7ef679f650bb446619eff250976f84bd60ca Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Fri, 28 Aug 2026 18:49:46 -0700 Subject: [PATCH 13/34] Restore executable proof entrypoints Reason: Preserve the recorded RISC Zero proof commands as directly runnable Unix entrypoints after the Windows history reconstruction. Evidence: Both script blobs remain byte-identical to the pre-rewrite candidate and only their executable modes are restored. Coordinate: ZIZK RISC Zero recorded-proof command surface; release 1.2.0. Falsification: Either script mode differs from the preserved candidate or direct offline invocation loses executable permission. Compatibility: File-mode repair only; no source, receipt, proof, or schema bytes change. --- examples/zizk_artifact_first/risc0/scripts/run_real_proof.sh | 0 .../zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/zizk_artifact_first/risc0/scripts/run_real_proof.sh mode change 100644 => 100755 examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh diff --git a/examples/zizk_artifact_first/risc0/scripts/run_real_proof.sh b/examples/zizk_artifact_first/risc0/scripts/run_real_proof.sh old mode 100644 new mode 100755 diff --git a/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh b/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh old mode 100644 new mode 100755 From 70706ec513eafe8b0ba359ffb38e7e9210f49ca1 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 06:19:56 -0700 Subject: [PATCH 14/34] feat: implement evidence-bound assurance mechanisms Reason: Replace previously documented absence boundaries with executable, fail-closed reference mechanisms for VSTD-4, VSTD-5, VSTD-Graph profile establishment, and additive TRUST/ROT/RUST assurance lifecycle analysis. Evidence: The implementation dispatches content-addressed evidence to named mechanism implementations with explicit trust roots and bounds. Receipt recheck paths rehash embedded bytes, rerun the mechanisms, and compare the complete derived result. Adversarial tests cover duplicate evidence and paths, challenge, revocation, staleness, supersession, conflicts, cycles, self-consumption, corroboration disagreement, incomplete witness separation, lifecycle-driven trust invalidation, diagnostic RUST, and separately earned BLAME/GUILT. Coordinate: Parent 79ae7ef679f650bb446619eff250976f84bd60ca on codex/post-1.2-professionalization for PR #27. Falsification: A forged field-only PASS, repeated evidence, caller-supplied rating, unresolved independence seam, inadmissible ancestor, changed embedded byte, replaced built-in mechanism, cycle, or replay divergence must refuse establishment or produce UNKNOWN/CONFLICTED/FAIL rather than clean assurance. Compatibility/wire impact: The historical compatibility candidate paths remain readable and retain NOT_ESTABLISHED semantics. New evidence-bound receipt alternatives and the VSTD-GRAPH-ASSURANCE-1 non-receipt log are additive. The unreleased VSTD-5 draft shape is replaced before release; no frozen released profile is silently redefined. --- receipts/schema/vstd4_receipt.json | 82 +- receipts/schema/vstd5_receipt.json | 130 +- receipts/schema/vstd_graph_receipt.json | 48 +- src/verifier/__init__.py | 80 +- src/verifier/core/depth.py | 374 +++++- src/verifier/core/evidence.py | 373 ++++++ src/verifier/core/witness.py | 550 +++++++++ src/verifier/data/__init__.py | 22 + src/verifier/data/assurance.py | 979 +++++++++++++++ src/verifier/data/graph_level.py | 309 +++++ src/verifier/layer4/challenge.py | 6 +- src/verifier/specifications/LADDER.md | 35 +- src/verifier/specifications/VSTD-4.md | 22 +- src/verifier/specifications/VSTD-5.md | 132 +- src/verifier/specifications/VSTD-Graph-2.md | 12 +- src/verifier/specifications/VSTD-Graph-3.md | 11 +- src/verifier/specifications/VSTD-Graph-4.md | 8 +- src/verifier/specifications/VSTD-Graph-5.md | 11 +- .../specifications/WIRE_IDENTIFIERS.md | 18 +- standard/LADDER.md | 35 +- standard/VSTD-4.md | 22 +- standard/VSTD-5.md | 132 +- standard/VSTD-Graph-2.md | 12 +- standard/VSTD-Graph-3.md | 11 +- standard/VSTD-Graph-4.md | 8 +- standard/VSTD-Graph-5.md | 11 +- standard/WIRE_IDENTIFIERS.md | 18 +- .../vstd-graph-assurance-1.schema.json | 108 ++ tests/test_evidence_bound_assurance.py | 1080 +++++++++++++++++ tests/test_packaged_specifications.py | 5 +- tests/test_public_api.py | 22 + tests/test_vstd_schemas.py | 50 +- 32 files changed, 4439 insertions(+), 277 deletions(-) create mode 100644 src/verifier/core/evidence.py create mode 100644 src/verifier/core/witness.py create mode 100644 src/verifier/data/assurance.py create mode 100644 standard/schemas/vstd-graph-assurance-1.schema.json create mode 100644 tests/test_evidence_bound_assurance.py diff --git a/receipts/schema/vstd4_receipt.json b/receipts/schema/vstd4_receipt.json index 81f4a64..0de6dfc 100644 --- a/receipts/schema/vstd4_receipt.json +++ b/receipts/schema/vstd4_receipt.json @@ -2,8 +2,8 @@ "$comment": "Terminology: grounded decision certificate (GDC); Verifier Standard (VSTD).", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/vstd4_receipt.json", - "title": "VSTD-4 Structural Candidate Receipt", - "description": "Legacy-compatible VSTD-4 shape. vstd4_depth is a candidate over caller-supplied rung references; conformance is NOT_ESTABLISHED. Historical receipts without conformance_status have the same non-upgrading interpretation.", + "title": "VSTD-4 Refutability Receipt", + "description": "Legacy-compatible VSTD-4 shape. Caller-supplied rung references remain a candidate with conformance NOT_ESTABLISHED. The additive EVIDENCE_BOUND form embeds exact proposition bindings and evidence bytes so registered mechanisms can be rerun; schema validity alone never establishes conformance.", "type": "object", "additionalProperties": false, "required": ["schema_version", "receipt_id", "claim_id", "binding", "vstd4_depth", "rung_evidence", "witness", "ceiling_refutation", "blocking_rungs", "status"], @@ -12,8 +12,10 @@ "receipt_id": {"type": "string", "pattern": "^VFY-4-[A-Za-z0-9._:-]+$"}, "claim_id": {"type": "string", "minLength": 1}, "binding": {"$ref": "#/$defs/binding"}, - "vstd4_depth": {"type": "integer", "minimum": 0, "maximum": 14, "description": "Structural candidate depth, not normative VSTD-4 conformance."}, - "conformance_status": {"const": "NOT_ESTABLISHED"}, + "vstd4_depth": {"type": "integer", "minimum": 0, "maximum": 14, "description": "Candidate depth unless depth_kind is EVIDENCE_BOUND and all replay checks establish the exact preconditions and rungs."}, + "depth_kind": {"enum": ["CANDIDATE", "EVIDENCE_BOUND"]}, + "conformance_status": {"enum": ["NOT_ESTABLISHED", "ESTABLISHED"]}, + "kernel_outcome": {"enum": ["ACCEPTED", "REJECTED", "REFUSED"]}, "rung_evidence": { "type": "object", "additionalProperties": false, @@ -35,18 +37,88 @@ "precommitment_envelope": {"type": "object"}, "availability": {"type": "array", "items": {"type": "object"}}, "challenge_records": {"type": "array", "items": {"type": "object"}}, - "refutability_closure": {"type": ["object", "null"]} + "refutability_closure": {"type": ["object", "null"]}, + "evidence_bindings": { + "type": "object", + "additionalProperties": false, + "required": ["prerequisites", "rungs"], + "properties": { + "prerequisites": { + "type": "object", + "additionalProperties": false, + "required": ["1", "2", "3"], + "properties": { + "1": {"$ref": "#/$defs/evidenceBinding"}, + "2": {"$ref": "#/$defs/evidenceBinding"}, + "3": {"$ref": "#/$defs/evidenceBinding"} + } + }, + "rungs": { + "type": "object", + "additionalProperties": false, + "required": ["4.1", "4.2", "4.3", "4.4", "4.5", "4.6", "4.7", "4.8", "4.9", "4.10", "4.11", "4.12", "4.13", "4.14"], + "properties": { + "4.1": {"$ref": "#/$defs/evidenceBinding"}, "4.2": {"$ref": "#/$defs/evidenceBinding"}, + "4.3": {"$ref": "#/$defs/evidenceBinding"}, "4.4": {"$ref": "#/$defs/evidenceBinding"}, + "4.5": {"$ref": "#/$defs/evidenceBinding"}, "4.6": {"$ref": "#/$defs/evidenceBinding"}, + "4.7": {"$ref": "#/$defs/evidenceBinding"}, "4.8": {"$ref": "#/$defs/evidenceBinding"}, + "4.9": {"$ref": "#/$defs/evidenceBinding"}, "4.10": {"$ref": "#/$defs/evidenceBinding"}, + "4.11": {"$ref": "#/$defs/evidenceBinding"}, "4.12": {"$ref": "#/$defs/evidenceBinding"}, + "4.13": {"$ref": "#/$defs/evidenceBinding"}, "4.14": {"$ref": "#/$defs/evidenceBinding"} + } + } + } + }, + "evidence_payloads": { + "type": "object", + "propertyNames": {"pattern": "^sha256:[0-9a-f]{64}$"}, + "additionalProperties": {"type": "string", "contentEncoding": "base64"} + } }, "allOf": [ { "if": {"properties": {"vstd4_depth": {"const": 14}}}, "then": {"properties": {"witness": {"type": "object"}, "ceiling_refutation": {"type": "null"}}}, "else": {"properties": {"ceiling_refutation": {"type": "object"}}} + }, + { + "if": {"required": ["conformance_status"], "properties": {"conformance_status": {"const": "ESTABLISHED"}}}, + "then": { + "required": ["depth_kind", "kernel_outcome", "evidence_bindings", "evidence_payloads"], + "properties": { + "vstd4_depth": {"const": 14}, + "depth_kind": {"const": "EVIDENCE_BOUND"}, + "kernel_outcome": {"const": "ACCEPTED"}, + "blocking_rungs": {"maxItems": 0}, + "witness": {"type": "object"}, + "ceiling_refutation": {"type": "null"} + } + } } ], "$defs": { "digest": {"type": "string", "pattern": "^(?:sha256:)?[0-9a-f]{64}$"}, "evidenceRef": {"type": "string", "minLength": 1, "description": "Caller-supplied reference. Schema validity does not establish retrieval, content binding, the rung proposition, or prerequisite-profile conformance."}, + "evidenceBinding": { + "type": "object", + "additionalProperties": false, + "required": ["subject_id", "predicate", "expected", "mechanism_id", "mechanism_digest", "evidence_refs", "trust_roots", "bounds", "parameters"], + "properties": { + "subject_id": {"type": "string", "minLength": 1}, + "predicate": {"type": "string", "minLength": 1}, + "expected": {}, + "mechanism_id": {"type": "string", "minLength": 1}, + "mechanism_digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}}, + "trust_roots": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + "bounds": { + "type": "object", "additionalProperties": false, + "required": ["max_evidence_items", "max_evidence_bytes"], + "properties": {"max_evidence_items": {"type": "integer", "minimum": 0}, "max_evidence_bytes": {"type": "integer", "minimum": 0}} + }, + "parameters": {"type": "object", "additionalProperties": {"type": "string"}} + } + }, "coordinate": { "type": "object", "additionalProperties": false, "required": ["subject", "predicate", "parameters"], diff --git a/receipts/schema/vstd5_receipt.json b/receipts/schema/vstd5_receipt.json index 368d3e1..b1847e4 100644 --- a/receipts/schema/vstd5_receipt.json +++ b/receipts/schema/vstd5_receipt.json @@ -1,65 +1,125 @@ { - "$comment": "Terminology: Verifier Standard (VSTD). Draft interface only; matching this schema is not VSTD-5 readiness or proof of independent corroboration.", + "$comment": "Terminology: Verifier Standard (VSTD). Witness identity is a coordinate, not computational trust.", "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/vstd5_receipt.json", - "title": "VSTD-5 Witness Corroboration Receipt (DRAFT)", - "description": "Shape for review only. entry_vstd4_depth is a declaration of the future normative precondition; the current VSTD-4 candidate cannot satisfy it, and no reference VSTD-5 acceptance path is implemented.", + "title": "VSTD-5 Witness Corroboration Receipt", + "description": "Replayable evidence-bound witness record. Schema validity establishes shape only. The reference runtime must recheck the admitted evidence-bound VSTD-4 result, identity evidence availability, every independence dimension, every corroboration mechanism, embedded evidence bytes, and the derived result.", "type": "object", "additionalProperties": false, - "required": ["schema_version", "status", "receipt_id", "claim_id", "claim_binding", "entry_vstd4_depth", "witnesses", "corroborations", "disagreements", "computed_independence"], + "required": ["schema_version", "receipt_id", "entry_vstd4", "bundle", "evidence_payloads", "result"], "properties": { - "schema_version": {"const": "VSTD-5-DRAFT"}, - "status": {"const": "DRAFT"}, + "schema_version": {"const": "VSTD-5"}, "receipt_id": {"type": "string", "pattern": "^VFY-5-[A-Za-z0-9._:-]+$"}, - "claim_id": {"type": "string", "minLength": 1}, - "claim_binding": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, - "entry_vstd4_depth": {"const": 14, "description": "Future normative VSTD-4 conformance depth. Structural candidate depth 14 is insufficient."}, - "witnesses": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/witness"}}, - "corroborations": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/corroboration"}}, - "disagreements": {"type": "array", "items": {"$ref": "#/$defs/disagreement"}}, - "computed_independence": {"enum": ["INDEPENDENT", "PARTIALLY_INDEPENDENT", "NOT_INDEPENDENT", "UNKNOWN", "CONFLICTED"]} + "entry_vstd4": { + "type": "object", + "additionalProperties": false, + "required": ["result_digest", "depth", "conformance_status", "witness_digest"], + "properties": { + "result_digest": {"$ref": "#/$defs/digest"}, + "depth": {"const": 14}, + "conformance_status": {"const": "ESTABLISHED"}, + "witness_digest": {"$ref": "#/$defs/digest"} + } + }, + "bundle": {"$ref": "#/$defs/bundle"}, + "evidence_payloads": { + "type": "object", + "propertyNames": {"pattern": "^sha256:[0-9a-f]{64}$"}, + "additionalProperties": {"type": "string", "contentEncoding": "base64"} + }, + "result": {"$ref": "#/$defs/result"} }, "$defs": { - "digest": {"type": "string", "pattern": "^(?:sha256:)?[0-9a-f]{64}$"}, + "digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "digestRef": {"type": "string", "pattern": "^(?:sha256:)?[0-9a-f]{64}$"}, + "bundle": { + "type": "object", "additionalProperties": false, + "required": ["claim_id", "declarant_id", "claim_binding_digest", "witnesses", "corroborations"], + "properties": { + "claim_id": {"type": "string", "minLength": 1}, + "declarant_id": {"type": "string", "minLength": 1}, + "claim_binding_digest": {"$ref": "#/$defs/digest"}, + "witnesses": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/witness"}}, + "corroborations": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/corroboration"}} + } + }, "witness": { "type": "object", "additionalProperties": false, - "required": ["witness_id", "identity_evidence", "independence"], + "required": ["witness_id", "identity_evidence_ref", "independence"], "properties": { "witness_id": {"type": "string", "minLength": 1}, - "identity_evidence": {"$ref": "#/$defs/digest"}, + "identity_evidence_ref": {"$ref": "#/$defs/digestRef"}, "independence": { "type": "object", "additionalProperties": false, - "required": ["shared_control", "shared_code", "shared_trust_root", "shared_evidence_source", "shared_infrastructure", "financial_dependence", "jurisdictional_dependence", "evidence"], + "required": ["control", "verdict_code", "trust_root", "evidence_source", "infrastructure", "financial_dependence", "jurisdictional_dependence"], "properties": { - "shared_control": {"$ref": "#/$defs/triState"}, "shared_code": {"$ref": "#/$defs/triState"}, - "shared_trust_root": {"$ref": "#/$defs/triState"}, "shared_evidence_source": {"$ref": "#/$defs/triState"}, - "shared_infrastructure": {"$ref": "#/$defs/triState"}, "financial_dependence": {"$ref": "#/$defs/triState"}, - "jurisdictional_dependence": {"$ref": "#/$defs/triState"}, "evidence": {"type": "array", "items": {"$ref": "#/$defs/digest"}} + "control": {"$ref": "#/$defs/dimension"}, + "verdict_code": {"$ref": "#/$defs/dimension"}, + "trust_root": {"$ref": "#/$defs/dimension"}, + "evidence_source": {"$ref": "#/$defs/dimension"}, + "infrastructure": {"$ref": "#/$defs/dimension"}, + "financial_dependence": {"$ref": "#/$defs/dimension"}, + "jurisdictional_dependence": {"$ref": "#/$defs/dimension"} } } } }, - "triState": {"enum": ["YES", "NO", "UNKNOWN"]}, - "corroboration": { + "dimension": { "type": "object", "additionalProperties": false, - "required": ["corroboration_id", "witness_id", "class", "vstd4_certificate_digest", "checker_descriptor_digest", "observed_evidence", "result", "observed_at"], + "required": ["state", "binding"], "properties": { - "corroboration_id": {"type": "string", "minLength": 1}, "witness_id": {"type": "string", "minLength": 1}, - "class": {"enum": ["PROCUREMENT", "POWER_THERMAL_ENVELOPE", "NETWORK_EGRESS", "VENDOR_TELEMETRY", "FINANCIAL_ATTESTATION", "PHYSICAL_INSPECTION"]}, - "vstd4_certificate_digest": {"$ref": "#/$defs/digest"}, "checker_descriptor_digest": {"$ref": "#/$defs/digest"}, - "observed_evidence": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/digest"}}, - "result": {"enum": ["CORROBORATED", "REFUTED", "UNKNOWN"]}, "observed_at": {"type": "string", "format": "date-time"} + "state": {"enum": ["SHARED", "SEPARATE", "UNKNOWN"]}, + "binding": {"anyOf": [{"$ref": "https://timelordraps.github.io/verifier/schemas/vstd4_receipt.json#/$defs/evidenceBinding"}, {"type": "null"}]} } }, - "disagreement": { + "corroboration": { "type": "object", "additionalProperties": false, - "required": ["disagreement_id", "corroboration_ids", "status_effect", "recorded_at"], + "required": ["corroboration_id", "witness_id", "claim_binding_digest", "vstd4_certificate_digest", "checker_descriptor_digest", "observed_evidence_refs", "result", "observed_at", "verification", "corroboration_class"], "properties": { - "disagreement_id": {"type": "string", "minLength": 1}, - "corroboration_ids": {"type": "array", "minItems": 2, "items": {"type": "string"}, "uniqueItems": true}, - "status_effect": {"enum": ["CHALLENGED", "REVOKED", "UNKNOWN", "CONFLICTED"]}, - "recorded_at": {"type": "string", "format": "date-time"} + "corroboration_id": {"type": "string", "minLength": 1}, + "witness_id": {"type": "string", "minLength": 1}, + "claim_binding_digest": {"$ref": "#/$defs/digest"}, + "vstd4_certificate_digest": {"$ref": "#/$defs/digestRef"}, + "checker_descriptor_digest": {"$ref": "#/$defs/digestRef"}, + "observed_evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/digestRef"}}, + "result": {"enum": ["CORROBORATED", "REFUTED", "UNKNOWN"]}, + "observed_at": {"type": "string", "format": "date-time"}, + "verification": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd4_receipt.json#/$defs/evidenceBinding"}, + "corroboration_class": {"type": "string", "minLength": 1} } + }, + "evaluation": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd-graph-assurance-1.schema.json#/$defs/evaluation"}, + "result": { + "type": "object", "additionalProperties": false, + "required": ["claim_id", "status", "conformance_status", "computed_independence", "independence_evaluations", "corroboration_evaluations", "disagreements", "errors", "limitations"], + "properties": { + "claim_id": {"type": "string", "minLength": 1}, + "status": {"enum": ["CORROBORATED", "REFUTED", "UNKNOWN", "CONFLICTED"]}, + "conformance_status": {"enum": ["ESTABLISHED", "NOT_ESTABLISHED"]}, + "computed_independence": {"enum": ["INDEPENDENT", "UNKNOWN"]}, + "independence_evaluations": { + "type": "array", + "items": {"type": "object", "additionalProperties": false, "required": ["witness_id", "dimension", "evaluation"], "properties": {"witness_id": {"type": "string"}, "dimension": {"type": "string"}, "evaluation": {"$ref": "#/$defs/evaluation"}}} + }, + "corroboration_evaluations": { + "type": "array", + "items": {"type": "object", "additionalProperties": false, "required": ["corroboration_id", "evaluation"], "properties": {"corroboration_id": {"type": "string"}, "evaluation": {"$ref": "#/$defs/evaluation"}}} + }, + "disagreements": {"type": "array", "items": {"type": "array", "minItems": 2, "uniqueItems": true, "items": {"type": "string"}}}, + "errors": {"type": "array", "items": {"type": "string"}}, + "limitations": {"type": "array", "items": {"type": "string"}} + }, + "allOf": [ + { + "if": {"properties": {"status": {"const": "CORROBORATED"}}, "required": ["status"]}, + "then": { + "properties": { + "conformance_status": {"const": "ESTABLISHED"}, + "computed_independence": {"const": "INDEPENDENT"} + } + } + } + ] } } } diff --git a/receipts/schema/vstd_graph_receipt.json b/receipts/schema/vstd_graph_receipt.json index eac96fb..315c194 100644 --- a/receipts/schema/vstd_graph_receipt.json +++ b/receipts/schema/vstd_graph_receipt.json @@ -165,13 +165,53 @@ "collection_id": {"type": "string", "minLength": 1}, "level": {"type": "integer", "minimum": 0, "maximum": 5}, "max_level": {"const": 5}, - "rating_basis": {"const": "CALLER_SUPPLIED"}, - "conformance_status": {"const": "NOT_ESTABLISHED"}, + "rating_basis": {"enum": ["CALLER_SUPPLIED", "MECHANISM_EVALUATED"]}, + "conformance_status": {"enum": ["NOT_ESTABLISHED", "ESTABLISHED"]}, "blocking_obligations": {"type": "array", "items": {"type": "object"}}, "witness_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, "refutation_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, - "explanation": {"type": "string"} - } + "explanation": {"type": "string"}, + "kernel_outcome": {"enum": ["ACCEPTED", "REJECTED", "REFUSED"]}, + "members": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + "binding": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd4_receipt.json#/$defs/binding"}, + "object_evaluations": { + "type": "object", + "additionalProperties": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd-graph-assurance-1.schema.json#/$defs/evaluation"} + }, + "edge_evaluations": { + "type": "object", + "additionalProperties": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd-graph-assurance-1.schema.json#/$defs/evaluation"} + }, + "binding_errors": {"type": "array", "items": {"type": "string"}}, + "evidence_bindings": { + "type": "object", + "additionalProperties": false, + "required": ["objects", "edges"], + "properties": { + "objects": {"type": "object", "additionalProperties": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd4_receipt.json#/$defs/evidenceBinding"}}, + "edges": {"type": "object", "additionalProperties": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd4_receipt.json#/$defs/evidenceBinding"}} + } + }, + "evidence_payloads": { + "type": "object", + "propertyNames": {"pattern": "^sha256:[0-9a-f]{64}$"}, + "additionalProperties": {"type": "string", "contentEncoding": "base64"} + } + }, + "allOf": [ + { + "if": {"required": ["conformance_status"], "properties": {"conformance_status": {"const": "ESTABLISHED"}}}, + "then": { + "required": ["rating_basis", "kernel_outcome", "members", "binding", "object_evaluations", "edge_evaluations", "binding_errors", "evidence_bindings", "evidence_payloads"], + "properties": { + "rating_basis": {"const": "MECHANISM_EVALUATED"}, + "level": {"minimum": 1}, + "kernel_outcome": {"const": "ACCEPTED"}, + "binding_errors": {"maxItems": 0} + } + } + } + ] } } } diff --git a/src/verifier/__init__.py b/src/verifier/__init__.py index 6df741e..ed9fd77 100644 --- a/src/verifier/__init__.py +++ b/src/verifier/__init__.py @@ -11,8 +11,8 @@ __version__ = "1.2.0" # This names the highest project-specification coordinate exposed by the package; # it is not a conformance claim. Keep the adjacent status when presenting it. -__standard__ = "VSTD-4" -__standard_status__ = "CANDIDATE; CONFORMANCE NOT_ESTABLISHED" +__standard__ = "VSTD-5" +__standard_status__ = "PROJECT SPECIFICATION; EVIDENCE-BOUND REFERENCE MECHANISM" _LAZY_EXPORTS = { "ArtifactControlError": ("verifier.artifact_control", "ArtifactControlError"), @@ -41,7 +41,53 @@ "certificate_from_canonical_bytes", ), "vstd4_depth": ("verifier.core.depth", "vstd4_depth"), + "establish_vstd4": ("verifier.core.depth", "establish_vstd4"), + "build_evidence_bound_vstd4_receipt": ( + "verifier.core.depth", + "build_evidence_bound_vstd4_receipt", + ), + "claim_binding_from_dict": ("verifier.core.depth", "claim_binding_from_dict"), + "recheck_evidence_bound_vstd4_receipt": ( + "verifier.core.depth", + "recheck_evidence_bound_vstd4_receipt", + ), "require_vstd5_entry": ("verifier.core.depth", "require_vstd5_entry"), + "BoundProposition": ("verifier.core.evidence", "BoundProposition"), + "EvidenceBounds": ("verifier.core.evidence", "EvidenceBounds"), + "EvidenceStore": ("verifier.core.evidence", "EvidenceStore"), + "EvidenceBindingError": ("verifier.core.evidence", "EvidenceBindingError"), + "MechanismDecision": ("verifier.core.evidence", "MechanismDecision"), + "MechanismOutcome": ("verifier.core.evidence", "MechanismOutcome"), + "VerificationSession": ("verifier.core.evidence", "VerificationSession"), + "WitnessBundle": ("verifier.core.witness", "WitnessBundle"), + "assess_witness_corroboration": ( + "verifier.core.witness", + "assess_witness_corroboration", + ), + "build_vstd5_receipt": ("verifier.core.witness", "build_vstd5_receipt"), + "recheck_vstd5_receipt": ("verifier.core.witness", "recheck_vstd5_receipt"), + "AssuranceLedger": ("verifier.data.assurance", "AssuranceLedger"), + "recheck_assurance_log": ( + "verifier.data.assurance", + "recheck_assurance_log", + ), + "ProvenanceHypergraph": ("verifier.data.models", "ProvenanceHypergraph"), + "establish_graph_level": ( + "verifier.data.graph_level", + "establish_graph_level", + ), + "build_evidence_bound_graph_level_record": ( + "verifier.data.graph_level", + "build_evidence_bound_graph_level_record", + ), + "graph_collection_binding_digest": ( + "verifier.data.graph_level", + "graph_collection_binding_digest", + ), + "recheck_evidence_bound_graph_level_record": ( + "verifier.data.graph_level", + "recheck_evidence_bound_graph_level_record", + ), } # Supported names remain in _LAZY_EXPORTS while deprecated. Each entry records the @@ -90,9 +136,39 @@ def __dir__() -> list[str]: certificate_from_canonical_bytes as certificate_from_canonical_bytes, ) from verifier.core.depth import ( + build_evidence_bound_vstd4_receipt as build_evidence_bound_vstd4_receipt, + claim_binding_from_dict as claim_binding_from_dict, + establish_vstd4 as establish_vstd4, + recheck_evidence_bound_vstd4_receipt as recheck_evidence_bound_vstd4_receipt, require_vstd5_entry as require_vstd5_entry, vstd4_depth as vstd4_depth, ) + from verifier.core.evidence import ( + BoundProposition as BoundProposition, + EvidenceBindingError as EvidenceBindingError, + EvidenceBounds as EvidenceBounds, + EvidenceStore as EvidenceStore, + MechanismDecision as MechanismDecision, + MechanismOutcome as MechanismOutcome, + VerificationSession as VerificationSession, + ) + from verifier.core.witness import ( + WitnessBundle as WitnessBundle, + assess_witness_corroboration as assess_witness_corroboration, + build_vstd5_receipt as build_vstd5_receipt, + recheck_vstd5_receipt as recheck_vstd5_receipt, + ) + from verifier.data.assurance import ( + AssuranceLedger as AssuranceLedger, + recheck_assurance_log as recheck_assurance_log, + ) + from verifier.data.graph_level import ( + build_evidence_bound_graph_level_record as build_evidence_bound_graph_level_record, + establish_graph_level as establish_graph_level, + graph_collection_binding_digest as graph_collection_binding_digest, + recheck_evidence_bound_graph_level_record as recheck_evidence_bound_graph_level_record, + ) + from verifier.data.models import ProvenanceHypergraph as ProvenanceHypergraph from verifier.core.receipt import ( VstdReceipt as VstdReceipt, compute_canonical_digest as compute_canonical_digest, diff --git a/src/verifier/core/depth.py b/src/verifier/core/depth.py index d5e8d91..01ada5b 100644 --- a/src/verifier/core/depth.py +++ b/src/verifier/core/depth.py @@ -1,5 +1,5 @@ -"""Terminology: conjunctive normal form (CNF); identifier (ID); unsatisfiable (UNSAT); -Verifier Standard (VSTD). +"""Terminology: conjunctive normal form (CNF); grounded decision certificate (GDC); +identifier (ID); unsatisfiable (UNSAT); Verifier Standard (VSTD). ``vstd4_depth`` -- candidate depth over caller-supplied rung references. @@ -24,10 +24,13 @@ loudly, instead of quietly certifying a sequence that no longer preserves its dependencies. -The current producer checks reference presence and rung dependencies. It does not +The compatibility producer checks reference presence and rung dependencies. It does not resolve those references, validate the propositions they allegedly establish, or check VSTD-1/2/3 preconditions. Its result is therefore a candidate with -``conformance_status = NOT_ESTABLISHED`` and cannot admit VSTD-5. +``conformance_status = NOT_ESTABLISHED`` and cannot admit VSTD-5. The separate +``establish_vstd4`` path reruns exact evidence-bound prerequisite and rung +mechanisms, then checks the structural witness before it can report established +conformance. This module *produces* certificates. It is not part of the trusted computing base; :mod:`verifier.core.kernel` checks what it emits, and the propagation @@ -42,6 +45,7 @@ from .certificate import ( CertificateHeader, ClaimBinding, + ClaimCoordinate, ClauseGrounding, CostTier, DecisionBlock, @@ -50,10 +54,22 @@ GroundedFact, Grounding, PropagationStep, + ResourceBounds, UnitPropagationProof, VariableGrounding, Verdict, + VerifierDescriptor, + canonical_digest, ) +from .evidence import ( + BoundProposition, + EvaluatedProposition, + MechanismOutcome, + EvidenceStore, + VerificationMechanism, + VerificationSession, +) +from .kernel import KernelOutcome, check as kernel_check MAX_DEPTH = 14 """Highest structural candidate depth; not sufficient for VSTD-5 entry.""" @@ -171,13 +187,88 @@ def to_dict(self) -> dict[str, object]: } -def require_vstd5_entry(result: DepthResult) -> DepthResult: +@dataclass(frozen=True) +class EvidenceBoundDepthResult: + """VSTD-4 result obtained by rerunning every bound evidence mechanism. + + The structural candidate is retained as an audit artifact. Normative + conformance is established only when the VSTD-1, VSTD-2, and VSTD-3 + preconditions and all fourteen rung propositions pass under their exact + bindings, and the candidate witness itself checks in the independent + kernel. + """ + + candidate: DepthResult + prerequisite_evaluations: tuple[tuple[int, EvaluatedProposition], ...] + rung_evaluations: tuple[tuple[str, EvaluatedProposition], ...] + binding_errors: tuple[str, ...] + kernel_outcome: str + + @property + def depth(self) -> int: + return self.candidate.depth + + @property + def witness(self) -> Optional[DecisionCertificate]: + return self.candidate.witness + + @property + def refutation(self) -> Optional[DecisionCertificate]: + return self.candidate.refutation + + @property + def blocking_rungs(self) -> tuple[str, ...]: + return self.candidate.blocking_rungs + + @property + def conformance_status(self) -> str: + if self.binding_errors or self.depth != MAX_DEPTH: + return CONFORMANCE_STATUS + if self.kernel_outcome != KernelOutcome.ACCEPTED.value: + return CONFORMANCE_STATUS + if any(not result.passed for _, result in self.prerequisite_evaluations): + return CONFORMANCE_STATUS + if any(not result.passed for _, result in self.rung_evaluations): + return CONFORMANCE_STATUS + if len(self.prerequisite_evaluations) != 3 or len(self.rung_evaluations) != MAX_DEPTH: + return CONFORMANCE_STATUS + return "ESTABLISHED" + + @property + def admits_vstd5(self) -> bool: + return self.conformance_status == "ESTABLISHED" + + def to_dict(self) -> dict[str, object]: + payload = self.candidate.to_dict() + payload.update( + { + "depth_kind": "EVIDENCE_BOUND", + "conformance_status": self.conformance_status, + "admits_vstd5": self.admits_vstd5, + "prerequisite_evaluations": { + str(profile): result.to_dict() + for profile, result in self.prerequisite_evaluations + }, + "rung_evaluations": { + rung_id: result.to_dict() + for rung_id, result in self.rung_evaluations + }, + "binding_errors": list(self.binding_errors), + "kernel_outcome": self.kernel_outcome, + } + ) + return payload + + +def require_vstd5_entry( + result: DepthResult | EvidenceBoundDepthResult, +) -> EvidenceBoundDepthResult: """Reject the current unbound candidate result at the VSTD-5 boundary. - VSTD-5 is draft, but its entry boundary is not: a structural candidate over - caller-supplied references is not normative VSTD-4 conformance. A future - evidence-binding implementation needs a distinct result type and gate; it - must not make this candidate stronger by setting another declaration field. + A structural candidate over caller-supplied references is not normative + VSTD-4 conformance. The evidence-binding implementation uses a distinct + result type and gate; it never makes this candidate stronger by setting + another declaration field. """ if result.depth != MAX_DEPTH or result.witness is None: raise VSTD5EntryError( @@ -190,10 +281,18 @@ def require_vstd5_entry(result: DepthResult) -> DepthResult: raise VSTD5EntryError( "VSTD-5 entry result carries a ceiling refutation or blocking rung" ) - raise VSTD5EntryError( - "VSTD-5 requires established VSTD-4 conformance; this structural " - f"candidate has conformance_status {result.conformance_status}" - ) + if not isinstance(result, EvidenceBoundDepthResult): + raise VSTD5EntryError( + "VSTD-5 requires established VSTD-4 conformance; this structural " + f"candidate has conformance_status {result.conformance_status}" + ) + if not result.admits_vstd5: + detail = "; ".join(result.binding_errors) or "one or more mechanisms did not pass" + raise VSTD5EntryError( + "VSTD-5 requires established VSTD-4 conformance; evidence-bound " + f"result is {result.conformance_status}: {detail}" + ) + return result # -------------------------------------------------------------------------- @@ -343,3 +442,252 @@ def vstd4_depth( refutation, blocked = _certify(1, evidence, claim_id, binding) return DepthResult(0, None, refutation, blocked) + + +def establish_vstd4( + rung_evidence: Mapping[str, BoundProposition], + *, + prerequisite_evidence: Mapping[int, BoundProposition], + session: VerificationSession, + claim_id: str, + binding: ClaimBinding, +) -> EvidenceBoundDepthResult: + """Rerun evidence mechanisms and establish VSTD-4 only if all pass. + + Expected predicates are ``vstd.object_profile.1`` through + ``vstd.object_profile.3`` and ``vstd4.rung.4.1`` through + ``vstd4.rung.4.14``. Every proposition must target ``claim_id``, expect the + Boolean value ``True``, and carry ``claim_binding_digest`` equal to the exact + :class:`ClaimBinding` used for the structural certificate. Mismatches are + excluded rather than evaluated, so field naming or neighboring evidence + cannot earn a rung. + """ + + errors: list[str] = [] + prerequisite_results: list[tuple[int, EvaluatedProposition]] = [] + rung_results: list[tuple[str, EvaluatedProposition]] = [] + exact_binding = binding.digest() + + unknown_profiles = set(prerequisite_evidence) - {1, 2, 3} + if unknown_profiles: + errors.append(f"unknown prerequisite profiles: {sorted(unknown_profiles)}") + unknown_rungs = set(rung_evidence) - set(BY_ID) + if unknown_rungs: + errors.append(f"unknown VSTD-4 rungs: {sorted(unknown_rungs)}") + + def binding_error( + proposition: BoundProposition, expected_predicate: str, label: str + ) -> Optional[str]: + if proposition.subject_id != claim_id: + return f"{label} targets {proposition.subject_id!r}, not {claim_id!r}" + if proposition.predicate != expected_predicate: + return ( + f"{label} binds predicate {proposition.predicate!r}, not " + f"{expected_predicate!r}" + ) + if proposition.expected is not True: + return f"{label} does not bind the required Boolean true proposition" + if proposition.parameters.get("claim_binding_digest") != exact_binding: + return f"{label} does not bind the exact VSTD-4 claim commitment" + return None + + for profile in (1, 2, 3): + proposition = prerequisite_evidence.get(profile) + if proposition is None: + errors.append(f"missing VSTD-{profile} prerequisite evidence") + continue + issue = binding_error(proposition, f"vstd.object_profile.{profile}", f"VSTD-{profile}") + if issue: + errors.append(issue) + continue + prerequisite_results.append((profile, session.evaluate(proposition))) + + passed_refs: dict[str, str] = {} + for rung in RUNGS: + proposition = rung_evidence.get(rung.id) + if proposition is None: + errors.append(f"missing rung {rung.id} evidence") + continue + issue = binding_error(proposition, f"vstd4.rung.{rung.id}", f"rung {rung.id}") + if issue: + errors.append(issue) + continue + result = session.evaluate(proposition) + rung_results.append((rung.id, result)) + if result.outcome is MechanismOutcome.PASS: + passed_refs[rung.id] = "sha256:" + proposition.digest() + + candidate = vstd4_depth(passed_refs, claim_id=claim_id, binding=binding) + kernel_outcome = KernelOutcome.REJECTED.value + if candidate.witness is not None: + kernel_outcome = kernel_check(candidate.witness, binding=binding).outcome.value + + return EvidenceBoundDepthResult( + candidate, + tuple(prerequisite_results), + tuple(rung_results), + tuple(errors), + kernel_outcome, + ) + + +def build_evidence_bound_vstd4_receipt( + result: EvidenceBoundDepthResult, + *, + receipt_id: str, + claim_id: str, + binding: ClaimBinding, + prerequisite_evidence: Mapping[int, BoundProposition], + rung_evidence: Mapping[str, BoundProposition], + session: VerificationSession, + status: str = "VALID", +) -> dict[str, object]: + """Serialize every input needed to rerun an evidence-bound VSTD-4 result.""" + recomputed = establish_vstd4( + rung_evidence, + prerequisite_evidence=prerequisite_evidence, + session=session, + claim_id=claim_id, + binding=binding, + ) + if canonical_digest(recomputed.to_dict()) != canonical_digest(result.to_dict()): + raise ValueError("VSTD-4 result does not match the supplied replay inputs") + all_refs = tuple( + sorted( + { + reference + for proposition in (*prerequisite_evidence.values(), *rung_evidence.values()) + for reference in proposition.evidence_refs + } + ) + ) + return { + "schema_version": "VSTD-4", + "receipt_id": receipt_id, + "claim_id": claim_id, + "binding": binding.to_dict(), + "vstd4_depth": result.depth, + "depth_kind": "EVIDENCE_BOUND", + "conformance_status": result.conformance_status, + "rung_evidence": { + rung_id: "sha256:" + proposition.digest() + for rung_id, proposition in sorted(rung_evidence.items()) + }, + "witness": None if result.witness is None else result.witness.to_dict(), + "ceiling_refutation": ( + None if result.refutation is None else result.refutation.to_dict() + ), + "blocking_rungs": list(result.blocking_rungs), + "status": status, + "kernel_outcome": result.kernel_outcome, + "evidence_bindings": { + "prerequisites": { + str(profile): proposition.to_dict() + for profile, proposition in sorted(prerequisite_evidence.items()) + }, + "rungs": { + rung_id: proposition.to_dict() + for rung_id, proposition in sorted(rung_evidence.items()) + }, + }, + "evidence_payloads": session.evidence.export_base64(all_refs), + } + + +def claim_binding_from_dict(data: Mapping[str, object]) -> ClaimBinding: + """Reconstruct the exact VSTD-4 claim binding carried by a receipt.""" + + coordinate = data["coordinate"] + verifier = data["verifier"] + bounds = data["bounds"] + if not isinstance(coordinate, Mapping) or not isinstance(verifier, Mapping) or not isinstance(bounds, Mapping): + raise ValueError("receipt ClaimBinding blocks must be objects") + return ClaimBinding( + str(data["claim"]), + ClaimCoordinate( + str(coordinate["subject"]), + str(coordinate["predicate"]), + {str(key): str(value) for key, value in dict(coordinate.get("parameters", {})).items()}, + ), + str(data["policy_root"]), + str(data["evidence_root"]), + VerifierDescriptor( + str(verifier["specification_hash"]), + str(verifier["implementation_hash"]), + str(verifier["parser_hash"]), + str(verifier.get("certificate_format", "VSTD4-GDC-1")), + str(verifier.get("format_fragment", "UP,WIDTH-K,RES")), + tuple(str(item) for item in verifier.get("dependencies", ())), + bool(verifier.get("deterministic", True)), + ), + ResourceBounds( + int(bounds["verification_cost_bound"]), + int(bounds["memory_bound"]), + int(bounds["certificate_size_bound"]), + ), + str(data.get("prior_commitment", "")), + ) + + +def recheck_evidence_bound_vstd4_receipt( + receipt: Mapping[str, object], + *, + mechanisms: Sequence[VerificationMechanism], +) -> EvidenceBoundDepthResult: + """Reconstruct evidence bytes and rerun an evidence-bound VSTD-4 receipt.""" + if receipt.get("schema_version") != "VSTD-4": + raise ValueError("not a VSTD-4 receipt") + if receipt.get("depth_kind") != "EVIDENCE_BOUND": + raise ValueError("receipt is not evidence-bound") + payloads = receipt.get("evidence_payloads") + bindings = receipt.get("evidence_bindings") + binding_data = receipt.get("binding") + if not isinstance(payloads, Mapping) or not isinstance(bindings, Mapping) or not isinstance(binding_data, Mapping): + raise ValueError("evidence-bound receipt is missing replay inputs") + store = EvidenceStore() + store.import_base64({str(key): str(value) for key, value in payloads.items()}) + session = VerificationSession(store) + for mechanism in mechanisms: + session.register(mechanism) + prerequisites_data = bindings.get("prerequisites") + rungs_data = bindings.get("rungs") + if not isinstance(prerequisites_data, Mapping) or not isinstance(rungs_data, Mapping): + raise ValueError("evidence binding maps are missing") + prerequisites = { + int(profile): BoundProposition.from_dict(proposition) + for profile, proposition in prerequisites_data.items() + if isinstance(proposition, Mapping) + } + rungs = { + str(rung_id): BoundProposition.from_dict(proposition) + for rung_id, proposition in rungs_data.items() + if isinstance(proposition, Mapping) + } + binding = claim_binding_from_dict(binding_data) + result = establish_vstd4( + rungs, + prerequisite_evidence=prerequisites, + session=session, + claim_id=str(receipt["claim_id"]), + binding=binding, + ) + observed = result.to_dict() + comparisons = { + "vstd4_depth": observed["depth"], + "conformance_status": observed["conformance_status"], + "blocking_rungs": observed["blocking_rungs"], + "kernel_outcome": observed["kernel_outcome"], + } + for field, value in comparisons.items(): + if receipt.get(field) != value: + raise ValueError(f"recomputed VSTD-4 field does not match receipt: {field}") + for field, certificate in ( + ("witness", result.witness), + ("ceiling_refutation", result.refutation), + ): + expected = receipt.get(field) + expected_digest = None if expected is None else canonical_digest(expected) + observed_digest = None if certificate is None else certificate.digest() + if expected_digest != observed_digest: + raise ValueError(f"recomputed VSTD-4 {field} does not match receipt") + return result diff --git a/src/verifier/core/evidence.py b/src/verifier/core/evidence.py new file mode 100644 index 0000000..2d4c297 --- /dev/null +++ b/src/verifier/core/evidence.py @@ -0,0 +1,373 @@ +"""Terminology: JavaScript Object Notation (JSON); Secure Hash Algorithm 256-bit +(SHA-256); Verifier Standard (VSTD). + +Evidence-bound execution for meta-verifier mechanisms. + +Serialized claims are inputs, never verdicts. :class:`VerificationSession` +resolves every content-addressed evidence item, checks its bytes, selects the +exact registered mechanism implementation, enforces the declared input bounds, +and runs that mechanism again. A caller cannot promote a declaration by +putting ``PASS`` in a field because no such field exists on +:class:`BoundProposition`. + +The session establishes only the exact proposition a mechanism checks under its +named trust roots and bounds. Registration does not make a mechanism correct, +independent, or authoritative; it makes the executable coordinate explicit and +prevents a different declared digest from substituting after the proposition was +bound. Built-in mechanisms derive that digest from their exact module bytes. An +external mechanism remains responsible for truthfully deriving its advertised +implementation digest; the session cannot infer arbitrary plugin source identity. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +import base64 +import hashlib +from pathlib import Path +import re +from typing import Any, Mapping, Protocol, Sequence + +from .certificate import canonical_digest + + +_DIGEST = re.compile(r"^(?:sha256:)?([0-9a-f]{64})$") + + +class EvidenceBindingError(ValueError): + """An evidence binding is malformed or cannot be resolved exactly.""" + + +class MechanismOutcome(str, Enum): + PASS = "PASS" + FAIL = "FAIL" + UNKNOWN = "UNKNOWN" + + +@dataclass(frozen=True) +class EvidenceBounds: + """Resource ceilings enforced before invoking a domain mechanism.""" + + max_evidence_items: int + max_evidence_bytes: int + + def __post_init__(self) -> None: + if self.max_evidence_items < 0 or self.max_evidence_bytes < 0: + raise EvidenceBindingError("evidence bounds cannot be negative") + + def to_dict(self) -> dict[str, int]: + return { + "max_evidence_items": self.max_evidence_items, + "max_evidence_bytes": self.max_evidence_bytes, + } + + +@dataclass(frozen=True) +class BoundProposition: + """Exact proposition, evidence, mechanism, trust-root, and bound binding.""" + + subject_id: str + predicate: str + expected: Any + mechanism_id: str + mechanism_digest: str + evidence_refs: tuple[str, ...] + trust_roots: tuple[str, ...] + bounds: EvidenceBounds + parameters: Mapping[str, str] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.subject_id or not self.predicate or not self.mechanism_id: + raise EvidenceBindingError( + "subject_id, predicate, and mechanism_id must not be empty" + ) + if not _DIGEST.fullmatch(self.mechanism_digest): + raise EvidenceBindingError("mechanism_digest must be a SHA-256 digest") + if not self.evidence_refs: + raise EvidenceBindingError("a bound proposition needs evidence") + normalized = tuple(_normalize_ref(item) for item in self.evidence_refs) + if len(set(normalized)) != len(normalized): + raise EvidenceBindingError( + "duplicate evidence references do not create additional support" + ) + if not self.trust_roots or any(not item for item in self.trust_roots): + raise EvidenceBindingError("at least one explicit trust root is required") + object.__setattr__(self, "evidence_refs", normalized) + object.__setattr__(self, "trust_roots", tuple(sorted(set(self.trust_roots)))) + object.__setattr__(self, "parameters", dict(sorted(self.parameters.items()))) + + def to_dict(self) -> dict[str, Any]: + return { + "subject_id": self.subject_id, + "predicate": self.predicate, + "expected": self.expected, + "mechanism_id": self.mechanism_id, + "mechanism_digest": _normalize_ref(self.mechanism_digest), + "evidence_refs": list(self.evidence_refs), + "trust_roots": list(self.trust_roots), + "bounds": self.bounds.to_dict(), + "parameters": dict(self.parameters), + } + + def digest(self) -> str: + return canonical_digest(self.to_dict()) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "BoundProposition": + bounds = data["bounds"] + return cls( + subject_id=str(data["subject_id"]), + predicate=str(data["predicate"]), + expected=data["expected"], + mechanism_id=str(data["mechanism_id"]), + mechanism_digest=str(data["mechanism_digest"]), + evidence_refs=tuple(str(item) for item in data["evidence_refs"]), + trust_roots=tuple(str(item) for item in data["trust_roots"]), + bounds=EvidenceBounds( + int(bounds["max_evidence_items"]), + int(bounds["max_evidence_bytes"]), + ), + parameters={str(key): str(value) for key, value in data.get("parameters", {}).items()}, + ) + + +@dataclass(frozen=True) +class MechanismDecision: + """One bounded mechanism result plus its exact observations.""" + + outcome: MechanismOutcome + details: str + observations: Mapping[str, Any] = field(default_factory=dict) + + +class VerificationMechanism(Protocol): + """Executable domain mechanism selected by an exact implementation digest.""" + + mechanism_id: str + mechanism_digest: str + + def evaluate( + self, binding: BoundProposition, evidence: Sequence[bytes] + ) -> MechanismDecision: + """Evaluate only ``binding`` using the already digest-checked evidence.""" + + +@dataclass(frozen=True) +class EvaluatedProposition: + """Result of executing a mechanism, not a caller-serializable verdict field.""" + + binding_digest: str + outcome: MechanismOutcome + mechanism_id: str + mechanism_digest: str + evidence_refs: tuple[str, ...] + trust_roots: tuple[str, ...] + observed_evidence_bytes: int + details: str + observations: Mapping[str, Any] = field(default_factory=dict) + + @property + def passed(self) -> bool: + return self.outcome is MechanismOutcome.PASS + + def to_dict(self) -> dict[str, Any]: + return { + "binding_digest": self.binding_digest, + "outcome": self.outcome.value, + "mechanism_id": self.mechanism_id, + "mechanism_digest": _normalize_ref(self.mechanism_digest), + "evidence_refs": list(self.evidence_refs), + "trust_roots": list(self.trust_roots), + "observed_evidence_bytes": self.observed_evidence_bytes, + "details": self.details, + "observations": dict(self.observations), + } + + +def _normalize_ref(reference: str) -> str: + matched = _DIGEST.fullmatch(reference) + if matched is None: + raise EvidenceBindingError(f"not a SHA-256 evidence reference: {reference!r}") + return "sha256:" + matched.group(1) + + +def implementation_file_digest(path: str) -> str: + """Return the SHA-256 coordinate of exact mechanism module bytes.""" + return "sha256:" + hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +class EvidenceStore: + """In-memory content-addressed evidence store with collision/fork refusal.""" + + def __init__(self) -> None: + self._payloads: dict[str, bytes] = {} + + def add(self, payload: bytes) -> str: + if not isinstance(payload, bytes): + raise TypeError("evidence payload must be bytes") + reference = "sha256:" + hashlib.sha256(payload).hexdigest() + existing = self._payloads.get(reference) + if existing is not None and existing != payload: + raise EvidenceBindingError(f"evidence digest collision at {reference}") + self._payloads[reference] = payload + return reference + + def resolve(self, reference: str) -> bytes: + normalized = _normalize_ref(reference) + try: + payload = self._payloads[normalized] + except KeyError as exc: + raise EvidenceBindingError(f"evidence is unavailable: {normalized}") from exc + observed = "sha256:" + hashlib.sha256(payload).hexdigest() + if observed != normalized: + raise EvidenceBindingError( + f"evidence bytes do not match their reference: {normalized}" + ) + return payload + + def export_base64(self, references: Sequence[str]) -> dict[str, str]: + """Export exact evidence bytes for portable, offline mechanism replay.""" + return { + _normalize_ref(reference): base64.b64encode(self.resolve(reference)).decode("ascii") + for reference in references + } + + def import_base64(self, payloads: Mapping[str, str]) -> None: + """Import a portable bundle and refuse every reference/byte mismatch.""" + for reference, encoded in payloads.items(): + try: + payload = base64.b64decode(encoded, validate=True) + except Exception as exc: + raise EvidenceBindingError( + f"invalid base64 evidence payload for {reference}" + ) from exc + observed = self.add(payload) + if observed != _normalize_ref(reference): + raise EvidenceBindingError( + f"embedded evidence does not match reference {reference}" + ) + + def __contains__(self, reference: object) -> bool: + if not isinstance(reference, str): + return False + try: + return _normalize_ref(reference) in self._payloads + except EvidenceBindingError: + return False + + +class VerificationSession: + """Resolve evidence and rerun only explicitly registered mechanisms.""" + + def __init__(self, evidence: EvidenceStore) -> None: + self.evidence = evidence + self._mechanisms: dict[str, VerificationMechanism] = {} + + def register(self, mechanism: VerificationMechanism) -> None: + if not mechanism.mechanism_id: + raise EvidenceBindingError("mechanism_id must not be empty") + digest = _normalize_ref(mechanism.mechanism_digest) + previous = self._mechanisms.get(mechanism.mechanism_id) + if previous is not None and _normalize_ref(previous.mechanism_digest) != digest: + raise EvidenceBindingError( + f"mechanism substitution refused for {mechanism.mechanism_id}" + ) + self._mechanisms[mechanism.mechanism_id] = mechanism + + def evaluate(self, binding: BoundProposition) -> EvaluatedProposition: + mechanism = self._mechanisms.get(binding.mechanism_id) + if mechanism is None: + return self._unknown(binding, "bound mechanism is not registered", 0) + if _normalize_ref(mechanism.mechanism_digest) != _normalize_ref( + binding.mechanism_digest + ): + return self._unknown(binding, "registered mechanism digest does not match", 0) + + if len(binding.evidence_refs) > binding.bounds.max_evidence_items: + return self._unknown(binding, "evidence item bound exceeded", 0) + try: + payloads = tuple(self.evidence.resolve(item) for item in binding.evidence_refs) + except EvidenceBindingError as exc: + return self._unknown(binding, str(exc), 0) + observed_bytes = sum(len(item) for item in payloads) + if observed_bytes > binding.bounds.max_evidence_bytes: + return self._unknown(binding, "evidence byte bound exceeded", observed_bytes) + + try: + decision = mechanism.evaluate(binding, payloads) + except Exception as exc: # A mechanism crash is uncertainty, not a pass. + return self._unknown( + binding, + f"mechanism execution failed: {type(exc).__name__}: {exc}", + observed_bytes, + ) + if not isinstance(decision, MechanismDecision): + return self._unknown( + binding, "mechanism returned an invalid decision object", observed_bytes + ) + return EvaluatedProposition( + binding.digest(), + decision.outcome, + binding.mechanism_id, + _normalize_ref(binding.mechanism_digest), + binding.evidence_refs, + binding.trust_roots, + observed_bytes, + decision.details, + dict(decision.observations), + ) + + @staticmethod + def _unknown( + binding: BoundProposition, details: str, observed_bytes: int + ) -> EvaluatedProposition: + return EvaluatedProposition( + binding.digest(), + MechanismOutcome.UNKNOWN, + binding.mechanism_id, + _normalize_ref(binding.mechanism_digest), + binding.evidence_refs, + binding.trust_roots, + observed_bytes, + details, + ) + + +class BytesDigestMechanism: + """Built-in mechanism for the exact proposition ``bytes.sha256 == expected``.""" + + mechanism_id = "vstd.bytes.sha256" + mechanism_digest = implementation_file_digest(__file__) + + def evaluate( + self, binding: BoundProposition, evidence: Sequence[bytes] + ) -> MechanismDecision: + if binding.predicate != "bytes.sha256" or len(evidence) != 1: + return MechanismDecision( + MechanismOutcome.UNKNOWN, + "this mechanism checks one bytes.sha256 proposition", + ) + observed = "sha256:" + hashlib.sha256(evidence[0]).hexdigest() + expected = _normalize_ref(str(binding.expected)) + outcome = MechanismOutcome.PASS if observed == expected else MechanismOutcome.FAIL + return MechanismDecision( + outcome, + f"observed {observed}; expected {expected}", + {"observed_digest": observed}, + ) + + +__all__ = [ + "BoundProposition", + "BytesDigestMechanism", + "EvidenceBindingError", + "EvidenceBounds", + "EvidenceStore", + "EvaluatedProposition", + "MechanismDecision", + "MechanismOutcome", + "VerificationMechanism", + "VerificationSession", + "implementation_file_digest", +] diff --git a/src/verifier/core/witness.py b/src/verifier/core/witness.py new file mode 100644 index 0000000..72450e8 --- /dev/null +++ b/src/verifier/core/witness.py @@ -0,0 +1,550 @@ +"""Terminology: Verifier Standard (VSTD). + +Evidence-bound VSTD-5 Witness Corroboration reference mechanism. + +Witness identifiers are coordinates, not trust. Every required separation +dimension is an exact proposition rerun by a named mechanism, and every +corroboration reruns a mechanism over content-addressed observations. Matching +names, repeated evidence, cryptographic identity, or majority count cannot +manufacture independence or corroboration. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Mapping + +from .depth import EvidenceBoundDepthResult, require_vstd5_entry +from .evidence import ( + BoundProposition, + EvidenceStore, + EvaluatedProposition, + MechanismOutcome, + VerificationSession, + VerificationMechanism, +) +from .certificate import canonical_digest + + +class IndependenceDimension(str, Enum): + CONTROL = "control" + VERDICT_CODE = "verdict_code" + TRUST_ROOT = "trust_root" + EVIDENCE_SOURCE = "evidence_source" + INFRASTRUCTURE = "infrastructure" + FINANCIAL_DEPENDENCE = "financial_dependence" + JURISDICTIONAL_DEPENDENCE = "jurisdictional_dependence" + + +class RelationshipState(str, Enum): + SHARED = "SHARED" + SEPARATE = "SEPARATE" + UNKNOWN = "UNKNOWN" + + +class CorroborationOutcome(str, Enum): + CORROBORATED = "CORROBORATED" + REFUTED = "REFUTED" + UNKNOWN = "UNKNOWN" + + +class WitnessResultStatus(str, Enum): + CORROBORATED = "CORROBORATED" + REFUTED = "REFUTED" + UNKNOWN = "UNKNOWN" + CONFLICTED = "CONFLICTED" + + +@dataclass(frozen=True) +class WitnessIdentity: + witness_id: str + identity_evidence_ref: str + + def to_dict(self) -> dict[str, str]: + return { + "witness_id": self.witness_id, + "identity_evidence_ref": self.identity_evidence_ref, + } + + +@dataclass(frozen=True) +class IndependenceAssertion: + witness_id: str + relationships: Mapping[IndependenceDimension, RelationshipState] + evidence: Mapping[IndependenceDimension, BoundProposition] + + def to_dict(self) -> dict[str, Any]: + return { + "witness_id": self.witness_id, + "dimensions": { + dimension.value: { + "state": self.relationships.get( + dimension, RelationshipState.UNKNOWN + ).value, + "binding": ( + None + if self.evidence.get(dimension) is None + else self.evidence[dimension].to_dict() + ), + } + for dimension in IndependenceDimension + }, + } + + +@dataclass(frozen=True) +class CorroborationRecord: + corroboration_id: str + witness_id: str + claim_binding_digest: str + vstd4_certificate_digest: str + checker_descriptor_digest: str + observed_evidence_refs: tuple[str, ...] + result: CorroborationOutcome + observed_at: str + verification: BoundProposition + corroboration_class: str = "GENERAL_COMPUTATIONAL_CHECK" + + def to_dict(self) -> dict[str, Any]: + return { + "corroboration_id": self.corroboration_id, + "witness_id": self.witness_id, + "claim_binding_digest": self.claim_binding_digest, + "vstd4_certificate_digest": self.vstd4_certificate_digest, + "checker_descriptor_digest": self.checker_descriptor_digest, + "observed_evidence_refs": list(self.observed_evidence_refs), + "result": self.result.value, + "observed_at": self.observed_at, + "verification": self.verification.to_dict(), + "corroboration_class": self.corroboration_class, + } + + +@dataclass(frozen=True) +class WitnessBundle: + """Claim-bound witnesses, separation assertions, and corroboration records.""" + + claim_id: str + declarant_id: str + claim_binding_digest: str + witnesses: tuple[WitnessIdentity, ...] + independence: tuple[IndependenceAssertion, ...] + corroborations: tuple[CorroborationRecord, ...] + + def to_dict(self) -> dict[str, Any]: + assertions = {item.witness_id: item for item in self.independence} + return { + "claim_id": self.claim_id, + "declarant_id": self.declarant_id, + "claim_binding_digest": self.claim_binding_digest, + "witnesses": [ + { + **witness.to_dict(), + "independence": ( + None + if assertions.get(witness.witness_id) is None + else assertions[witness.witness_id].to_dict()["dimensions"] + ), + } + for witness in self.witnesses + ], + "corroborations": [item.to_dict() for item in self.corroborations], + } + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "WitnessBundle": + witnesses: list[WitnessIdentity] = [] + assertions: list[IndependenceAssertion] = [] + for item in data.get("witnesses", ()): + witness = WitnessIdentity( + str(item["witness_id"]), str(item["identity_evidence_ref"]) + ) + witnesses.append(witness) + dimensions = item.get("independence") + if not isinstance(dimensions, Mapping): + continue + relationships: dict[IndependenceDimension, RelationshipState] = {} + evidence: dict[IndependenceDimension, BoundProposition] = {} + for dimension in IndependenceDimension: + value = dimensions.get(dimension.value, {}) + if not isinstance(value, Mapping): + continue + relationships[dimension] = RelationshipState( + value.get("state", RelationshipState.UNKNOWN.value) + ) + binding = value.get("binding") + if isinstance(binding, Mapping): + evidence[dimension] = BoundProposition.from_dict(binding) + assertions.append( + IndependenceAssertion(witness.witness_id, relationships, evidence) + ) + corroborations = tuple( + CorroborationRecord( + str(item["corroboration_id"]), + str(item["witness_id"]), + str(item["claim_binding_digest"]), + str(item["vstd4_certificate_digest"]), + str(item["checker_descriptor_digest"]), + tuple(str(ref) for ref in item["observed_evidence_refs"]), + CorroborationOutcome(item["result"]), + str(item["observed_at"]), + BoundProposition.from_dict(item["verification"]), + str(item.get("corroboration_class", "GENERAL_COMPUTATIONAL_CHECK")), + ) + for item in data.get("corroborations", ()) + ) + return cls( + str(data["claim_id"]), + str(data["declarant_id"]), + str(data["claim_binding_digest"]), + tuple(witnesses), + tuple(assertions), + corroborations, + ) + + +@dataclass(frozen=True) +class WitnessCorroborationResult: + claim_id: str + status: WitnessResultStatus + conformance_status: str + computed_independence: str + independence_evaluations: tuple[ + tuple[str, str, EvaluatedProposition], ... + ] + corroboration_evaluations: tuple[tuple[str, EvaluatedProposition], ...] + disagreements: tuple[tuple[str, ...], ...] + errors: tuple[str, ...] + limitations: tuple[str, ...] = field(default_factory=tuple) + + @property + def established(self) -> bool: + return self.conformance_status == "ESTABLISHED" + + def to_dict(self) -> dict[str, Any]: + return { + "claim_id": self.claim_id, + "status": self.status.value, + "conformance_status": self.conformance_status, + "computed_independence": self.computed_independence, + "independence_evaluations": [ + { + "witness_id": witness_id, + "dimension": dimension, + "evaluation": evaluation.to_dict(), + } + for witness_id, dimension, evaluation in self.independence_evaluations + ], + "corroboration_evaluations": [ + { + "corroboration_id": record_id, + "evaluation": evaluation.to_dict(), + } + for record_id, evaluation in self.corroboration_evaluations + ], + "disagreements": [list(group) for group in self.disagreements], + "errors": list(self.errors), + "limitations": list(self.limitations), + } + + +def assess_witness_corroboration( + entry: EvidenceBoundDepthResult, + bundle: WitnessBundle, + *, + session: VerificationSession, +) -> WitnessCorroborationResult: + """Recheck VSTD-5 entry, separation evidence, and corroboration evidence.""" + + require_vstd5_entry(entry) + errors: list[str] = [] + independence_results: list[tuple[str, str, EvaluatedProposition]] = [] + corroboration_results: list[tuple[str, EvaluatedProposition]] = [] + + if bundle.claim_id == "" or bundle.declarant_id == "": + errors.append("claim_id and declarant_id must not be empty") + if bundle.claim_binding_digest != entry.witness.header.binding: # type: ignore[union-attr] + errors.append("witness bundle does not bind the admitted VSTD-4 commitment") + + identities: dict[str, WitnessIdentity] = {} + identity_refs: set[str] = set() + for witness in bundle.witnesses: + if not witness.witness_id: + errors.append("witness_id must not be empty") + continue + if witness.witness_id == bundle.declarant_id: + errors.append(f"witness {witness.witness_id} is the declarant") + if witness.witness_id in identities: + errors.append(f"duplicate witness identifier: {witness.witness_id}") + identities[witness.witness_id] = witness + try: + identity_ref = session.evidence.add( + session.evidence.resolve(witness.identity_evidence_ref) + ) + except Exception as exc: + errors.append(f"witness {witness.witness_id} identity evidence unavailable: {exc}") + continue + if identity_ref in identity_refs: + errors.append( + f"witness {witness.witness_id} repeats another witness identity evidence" + ) + identity_refs.add(identity_ref) + + assertions: dict[str, IndependenceAssertion] = {} + for assertion in bundle.independence: + if assertion.witness_id in assertions: + errors.append(f"duplicate independence assertion: {assertion.witness_id}") + continue + assertions[assertion.witness_id] = assertion + if assertion.witness_id not in identities: + errors.append( + f"independence assertion references missing witness {assertion.witness_id}" + ) + continue + for dimension in IndependenceDimension: + state = assertion.relationships.get(dimension, RelationshipState.UNKNOWN) + if state is RelationshipState.SHARED: + errors.append( + f"witness {assertion.witness_id} shares {dimension.value} with declarant" + ) + continue + if state is RelationshipState.UNKNOWN: + errors.append( + f"witness {assertion.witness_id} has UNKNOWN {dimension.value} separation" + ) + continue + proposition = assertion.evidence.get(dimension) + if proposition is None: + errors.append( + f"witness {assertion.witness_id} has no evidence for {dimension.value}" + ) + continue + relation_subject = f"{bundle.declarant_id}->{assertion.witness_id}" + expected_predicate = f"vstd5.shared.{dimension.value}" + if ( + proposition.subject_id != relation_subject + or proposition.predicate != expected_predicate + or proposition.expected is not False + or proposition.parameters.get("claim_binding_digest") + != bundle.claim_binding_digest + ): + errors.append( + f"witness {assertion.witness_id} {dimension.value} evidence is not " + "bound to the exact negative separation proposition" + ) + continue + result = session.evaluate(proposition) + independence_results.append( + (assertion.witness_id, dimension.value, result) + ) + if not result.passed: + errors.append( + f"witness {assertion.witness_id} {dimension.value} separation " + f"was not established: {result.outcome.value}" + ) + + for witness_id in identities: + if witness_id not in assertions: + errors.append(f"witness {witness_id} has no independence assertion") + + seen_records: set[str] = set() + observed_sets: set[tuple[str, ...]] = set() + accepted_outcomes: list[tuple[str, CorroborationOutcome]] = [] + for record in bundle.corroborations: + if record.corroboration_id in seen_records: + errors.append(f"duplicate corroboration identifier: {record.corroboration_id}") + continue + seen_records.add(record.corroboration_id) + if record.witness_id not in identities: + errors.append( + f"corroboration {record.corroboration_id} references missing witness" + ) + continue + if record.claim_binding_digest != bundle.claim_binding_digest: + errors.append( + f"corroboration {record.corroboration_id} binds a neighboring claim" + ) + continue + if record.vstd4_certificate_digest.removeprefix("sha256:") != entry.witness.digest(): # type: ignore[union-attr] + errors.append( + f"corroboration {record.corroboration_id} binds a different VSTD-4 certificate" + ) + continue + unique_observations = tuple(sorted(set(record.observed_evidence_refs))) + if len(unique_observations) != len(record.observed_evidence_refs): + errors.append( + f"corroboration {record.corroboration_id} repeats evidence references" + ) + continue + if unique_observations in observed_sets: + errors.append( + f"corroboration {record.corroboration_id} duplicates another evidence set" + ) + continue + observed_sets.add(unique_observations) + expected = { + "claim_binding_digest": bundle.claim_binding_digest, + "vstd4_certificate_digest": record.vstd4_certificate_digest, + "checker_descriptor_digest": record.checker_descriptor_digest, + "result": record.result.value, + } + proposition = record.verification + if ( + proposition.subject_id != bundle.claim_id + or proposition.predicate != "vstd5.corroboration" + or proposition.expected != expected + or tuple(sorted(proposition.evidence_refs)) != unique_observations + or proposition.parameters.get("witness_id") != record.witness_id + or proposition.parameters.get("observed_at") != record.observed_at + ): + errors.append( + f"corroboration {record.corroboration_id} is not exactly bound" + ) + continue + result = session.evaluate(proposition) + corroboration_results.append((record.corroboration_id, result)) + if result.outcome is MechanismOutcome.PASS: + accepted_outcomes.append((record.corroboration_id, record.result)) + else: + errors.append( + f"corroboration {record.corroboration_id} mechanism did not pass: " + f"{result.outcome.value}" + ) + + if not bundle.witnesses: + errors.append("at least one witness is required") + if not bundle.corroborations: + errors.append("at least one corroboration is required") + corroborating_witnesses = {record.witness_id for record in bundle.corroborations} + for witness_id in identities: + if witness_id not in corroborating_witnesses: + errors.append(f"witness {witness_id} has no corroboration record") + + outcome_groups = { + outcome: tuple(record_id for record_id, item in accepted_outcomes if item is outcome) + for outcome in CorroborationOutcome + } + nonempty = [outcome for outcome, ids in outcome_groups.items() if ids] + disagreements: tuple[tuple[str, ...], ...] = () + if len(nonempty) > 1: + status = WitnessResultStatus.CONFLICTED + disagreements = (tuple(sorted(record_id for record_id, _ in accepted_outcomes)),) + elif nonempty == [CorroborationOutcome.CORROBORATED]: + status = WitnessResultStatus.CORROBORATED + elif nonempty == [CorroborationOutcome.REFUTED]: + status = WitnessResultStatus.REFUTED + else: + status = WitnessResultStatus.UNKNOWN + + expected_independence_checks = len(identities) * len(IndependenceDimension) + independence_established = ( + len(independence_results) == expected_independence_checks + and all(result.passed for _, _, result in independence_results) + and not any("separation" in error or "shares" in error for error in errors) + ) + computed_independence = "INDEPENDENT" if independence_established else "UNKNOWN" + conformance = ( + "ESTABLISHED" + if not errors and independence_established and len(corroboration_results) > 0 + else "NOT_ESTABLISHED" + ) + if status is WitnessResultStatus.CORROBORATED and conformance != "ESTABLISHED": + status = WitnessResultStatus.UNKNOWN + return WitnessCorroborationResult( + bundle.claim_id, + status, + conformance, + computed_independence, + tuple(independence_results), + tuple(corroboration_results), + disagreements, + tuple(errors), + ( + "Identity evidence identifies the witness coordinate; it does not confer trust.", + "The result is bounded to the registered mechanisms, trust roots, evidence, and bounds.", + ), + ) + + +def build_vstd5_receipt( + entry: EvidenceBoundDepthResult, + bundle: WitnessBundle, + result: WitnessCorroborationResult, + *, + receipt_id: str, + session: VerificationSession, +) -> dict[str, Any]: + """Serialize a replayable VSTD-5 receipt without treating names as trust.""" + require_vstd5_entry(entry) + recomputed = assess_witness_corroboration(entry, bundle, session=session) + if canonical_digest(recomputed.to_dict()) != canonical_digest(result.to_dict()): + raise ValueError("VSTD-5 result does not match the supplied replay inputs") + references = { + witness.identity_evidence_ref for witness in bundle.witnesses + } + for assertion in bundle.independence: + for proposition in assertion.evidence.values(): + references.update(proposition.evidence_refs) + for record in bundle.corroborations: + references.update(record.observed_evidence_refs) + references.update(record.verification.evidence_refs) + return { + "schema_version": "VSTD-5", + "receipt_id": receipt_id, + "entry_vstd4": { + "result_digest": canonical_digest(entry.to_dict()), + "depth": entry.depth, + "conformance_status": entry.conformance_status, + "witness_digest": entry.witness.digest(), # type: ignore[union-attr] + }, + "bundle": bundle.to_dict(), + "evidence_payloads": session.evidence.export_base64(tuple(sorted(references))), + "result": result.to_dict(), + } + + +def recheck_vstd5_receipt( + entry: EvidenceBoundDepthResult, + receipt: Mapping[str, Any], + *, + mechanisms: tuple[VerificationMechanism, ...], +) -> WitnessCorroborationResult: + """Import exact bytes, rerun all witness mechanisms, and compare the result.""" + require_vstd5_entry(entry) + if receipt.get("schema_version") != "VSTD-5": + raise ValueError("not a VSTD-5 receipt") + entry_record = receipt.get("entry_vstd4") + bundle_data = receipt.get("bundle") + payloads = receipt.get("evidence_payloads") + if not isinstance(entry_record, Mapping) or not isinstance(bundle_data, Mapping) or not isinstance(payloads, Mapping): + raise ValueError("VSTD-5 receipt is missing replay inputs") + if entry_record.get("result_digest") != canonical_digest(entry.to_dict()): + raise ValueError("VSTD-5 receipt references a different VSTD-4 result") + store = EvidenceStore() + store.import_base64({str(key): str(value) for key, value in payloads.items()}) + session = VerificationSession(store) + for mechanism in mechanisms: + session.register(mechanism) + result = assess_witness_corroboration( + entry, WitnessBundle.from_dict(bundle_data), session=session + ) + if canonical_digest(result.to_dict()) != canonical_digest(receipt.get("result")): + raise ValueError("recomputed VSTD-5 result does not match receipt") + return result + + +__all__ = [ + "CorroborationOutcome", + "CorroborationRecord", + "IndependenceAssertion", + "IndependenceDimension", + "RelationshipState", + "WitnessBundle", + "WitnessCorroborationResult", + "WitnessIdentity", + "WitnessResultStatus", + "assess_witness_corroboration", + "build_vstd5_receipt", + "recheck_vstd5_receipt", +] diff --git a/src/verifier/data/__init__.py b/src/verifier/data/__init__.py index cc51803..29bde2c 100644 --- a/src/verifier/data/__init__.py +++ b/src/verifier/data/__init__.py @@ -3,10 +3,22 @@ Target-neutral VSTD-Graph reference types and receipt mechanisms.""" from verifier.data.graph_level import ( + EvidenceBoundGraphLevelResult, GraphCollection, GraphLevelResult, + establish_graph_level, + graph_collection_binding_digest, graph_level, ) +from verifier.data.assurance import ( + AssuranceEvent, + AssuranceEventKind, + AssuranceLedger, + ChallengeProjectionMechanism, + DiagnosticAttribution, + DiagnosticKind, + recheck_assurance_log, +) from verifier.data.models import ( ArtifactNode, @@ -44,7 +56,17 @@ "TransformationType", "GraphCollection", "GraphLevelResult", + "EvidenceBoundGraphLevelResult", + "establish_graph_level", + "graph_collection_binding_digest", "graph_level", + "AssuranceEvent", + "AssuranceEventKind", + "AssuranceLedger", + "ChallengeProjectionMechanism", + "DiagnosticAttribution", + "DiagnosticKind", + "recheck_assurance_log", "PolicyEvaluationResult", "ProvenancePolicyVerifier", "DataIndependentAudit", diff --git a/src/verifier/data/assurance.py b/src/verifier/data/assurance.py new file mode 100644 index 0000000..5455594 --- /dev/null +++ b/src/verifier/data/assurance.py @@ -0,0 +1,979 @@ +"""Terminology: Secure Hash Algorithm 256-bit (SHA-256); Verifier Standard (VSTD). + +Executable VSTD-Graph artifact-state propagation. + +``TRUST`` is mechanism-earned forward artifact support. ``RUST`` is reverse +diagnostic traversal from a verified descendant deviation toward recorded +ancestors. ``ROT`` is typed degradation of current admissibility without +rewriting historical graph bytes. These names are formal semantic terms, not +acronyms, scalar scores, actor reputation, or references to the Rust language. + +The ledger is additive and hash chained. It can project a challenge ledger into +a current Graph view, preserve conflict resolutions as new records, deduplicate +support and reachability, compute structural RUST concentration, and perform +bounded artifact-relative diagnostic attribution. RUST reachability alone +never establishes falsity, causality, blame, guilt, or responsibility. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field, replace +from enum import Enum +from typing import Any, Iterable, Mapping, Optional + +from verifier.core.certificate import canonical_bytes, canonical_digest +from verifier.core.evidence import ( + BoundProposition, + EvidenceBounds, + EvidenceStore, + EvaluatedProposition, + MechanismDecision, + MechanismOutcome, + VerificationMechanism, + VerificationSession, + implementation_file_digest, +) +from verifier.layer4.challenge import ( + ChallengeLedger, + ChallengeOutcome, + DEGRADATION_ORDER, + most_degraded, +) + +from .models import ArtifactStatus, ConflictRecord, ProvenanceHypergraph + + +class AssuranceFlowError(ValueError): + """A requested propagation would exceed recorded topology or evidence.""" + + +class AssuranceEventKind(str, Enum): + TRUST = "TRUST" + ROT = "ROT" + RUST = "RUST" + STATUS_PROJECTION = "STATUS_PROJECTION" + CONFLICT_RESOLUTION = "CONFLICT_RESOLUTION" + CAUSAL_LOCALIZATION = "CAUSAL_LOCALIZATION" + DIAGNOSTIC_ATTRIBUTION = "DIAGNOSTIC_ATTRIBUTION" + + +class DiagnosticKind(str, Enum): + BLAME = "BLAME" + GUILT = "GUILT" + + +class ChallengeProjectionMechanism: + """Recompute one artifact's status from embedded challenge-ledger records.""" + + mechanism_id = "vstd.challenge-ledger.projection" + mechanism_digest = implementation_file_digest(__file__) + + def evaluate( + self, binding: BoundProposition, evidence: tuple[bytes, ...] + ) -> MechanismDecision: + if binding.predicate != "vstd.graph.current_status": + return MechanismDecision( + MechanismOutcome.UNKNOWN, + "this mechanism checks one projected challenge-ledger status", + ) + try: + records = [json.loads(payload.decode("utf-8")) for payload in evidence] + status, details = _status_from_challenge_records( + binding.subject_id, records + ) + except (KeyError, TypeError, ValueError, UnicodeError, json.JSONDecodeError) as exc: + return MechanismDecision( + MechanismOutcome.FAIL, + f"challenge-ledger projection evidence is invalid: {exc}", + ) + expected = ArtifactStatus(str(binding.expected)) + return MechanismDecision( + MechanismOutcome.PASS if status is expected else MechanismOutcome.FAIL, + details, + {"observed_status": status.value}, + ) + + +def _status_from_challenge_records( + subject_id: str, records: Iterable[Mapping[str, Any]] +) -> tuple[ArtifactStatus, str]: + """Independent projection over serialized records; no mutable ledger state.""" + + ordered = sorted(records, key=lambda item: int(item["sequence"])) + if len({int(item["sequence"]) for item in ordered}) != len(ordered): + raise ValueError("challenge records repeat a sequence number") + filed: dict[str, ArtifactStatus] = {} + outcomes: dict[str, ChallengeOutcome] = {} + for record in ordered: + if str(record["claim_id"]) != subject_id: + raise ValueError("challenge record targets a neighboring artifact") + kind = str(record["kind"]) + payload = record["payload"] + if not isinstance(payload, Mapping): + raise ValueError("challenge record payload is not an object") + if kind == "REFUSED": + continue + if kind == "FILED": + challenge = payload["challenge"] + admission = payload["admission"] + if not isinstance(challenge, Mapping) or not isinstance(admission, Mapping): + raise ValueError("filed challenge record is malformed") + challenge_id = str(challenge["challenge_id"]) + if challenge_id in filed: + raise ValueError("challenge identifier is repeated") + if not bool(admission["admitted"]): + raise ValueError("a FILED record carries a refused admission") + filed[challenge_id] = ArtifactStatus( + str(admission.get("resulting_status", ArtifactStatus.REVOKED.value)) + ) + continue + if kind == "ADJUDICATED": + adjudication = payload["adjudication"] + if not isinstance(adjudication, Mapping): + raise ValueError("adjudication record is malformed") + challenge_id = str(adjudication["challenge_id"]) + if challenge_id not in filed: + raise ValueError("adjudication precedes its filed challenge") + if challenge_id in outcomes: + raise ValueError("challenge has multiple adjudications") + outcomes[challenge_id] = ChallengeOutcome(str(adjudication["outcome"])) + continue + raise ValueError(f"unknown challenge record kind {kind!r}") + + confirmed = tuple( + sorted( + challenge_id + for challenge_id, outcome in outcomes.items() + if outcome is ChallengeOutcome.ACCEPTED + ) + ) + open_ids = tuple( + sorted( + challenge_id + for challenge_id in filed + if outcomes.get(challenge_id) + in (None, ChallengeOutcome.UNRESOLVED) + ) + ) + if confirmed: + return ( + most_degraded(filed[challenge_id] for challenge_id in confirmed), + f"{len(confirmed)} confirmed refutation(s); status is terminal", + ) + if open_ids: + return ( + ArtifactStatus.CHALLENGED, + f"{len(open_ids)} open credible challenge(s); an unadjudicated " + "challenge is not evidence of validity", + ) + if filed: + return ( + ArtifactStatus.VALID, + f"all {len(filed)} challenge(s) adjudicated and disproven", + ) + return ArtifactStatus.VALID, "no challenges filed" + + +@dataclass(frozen=True) +class AssuranceEvent: + sequence: int + kind: AssuranceEventKind + subject_id: str + source_ids: tuple[str, ...] + proposition: str + binding: Mapping[str, Any] + recorded_at: str + outcome: MechanismOutcome + mechanism_id: str + mechanism_digest: str + evidence_refs: tuple[str, ...] + evidence_payloads: Mapping[str, str] + trust_roots: tuple[str, ...] + details: str + previous_event_digest: str = "" + attributes: Mapping[str, Any] = field(default_factory=dict) + + def payload(self) -> dict[str, Any]: + return { + "sequence": self.sequence, + "kind": self.kind.value, + "subject_id": self.subject_id, + "source_ids": list(self.source_ids), + "proposition": self.proposition, + "binding": dict(self.binding), + "recorded_at": self.recorded_at, + "outcome": self.outcome.value, + "mechanism_id": self.mechanism_id, + "mechanism_digest": self.mechanism_digest, + "evidence_refs": list(self.evidence_refs), + "evidence_payloads": dict(self.evidence_payloads), + "trust_roots": list(self.trust_roots), + "details": self.details, + "previous_event_digest": self.previous_event_digest, + "attributes": dict(self.attributes), + } + + def digest(self) -> str: + return canonical_digest(self.payload()) + + def to_dict(self) -> dict[str, Any]: + result = self.payload() + result["event_digest"] = self.digest() + return result + + +@dataclass(frozen=True) +class ConflictResolution: + resolution_id: str + conflict_id: str + selected_value: str + recorded_at: str + evaluation: EvaluatedProposition + + def to_dict(self) -> dict[str, Any]: + return { + "resolution_id": self.resolution_id, + "conflict_id": self.conflict_id, + "selected_value": self.selected_value, + "recorded_at": self.recorded_at, + "evaluation": self.evaluation.to_dict(), + } + + +@dataclass(frozen=True) +class StructuralConcentration: + ancestor_id: str + deviating_descendants: tuple[str, ...] + + @property + def count(self) -> int: + return len(self.deviating_descendants) + + def to_dict(self) -> dict[str, Any]: + return { + "ancestor_id": self.ancestor_id, + "deviating_descendants": list(self.deviating_descendants), + "count": self.count, + "meaning": "unique diagnostic reachability roots; not causal strength", + } + + +@dataclass(frozen=True) +class DiagnosticAttribution: + kind: DiagnosticKind + ancestor_id: str + descendant_id: str + status: str + localization_event_digest: str + evaluation: Optional[EvaluatedProposition] + details: str + + def to_dict(self) -> dict[str, Any]: + return { + "kind": self.kind.value, + "ancestor_id": self.ancestor_id, + "descendant_id": self.descendant_id, + "status": self.status, + "localization_event_digest": self.localization_event_digest, + "evaluation": None if self.evaluation is None else self.evaluation.to_dict(), + "details": self.details, + } + + +class AssuranceLedger: + """Append-only current-state overlay for an immutable provenance graph.""" + + FORMAT = "VSTD-GRAPH-ASSURANCE-1" + + def __init__(self, graph: ProvenanceHypergraph) -> None: + errors = graph.validate_structure() + if errors: + raise AssuranceFlowError("invalid source graph: " + "; ".join(errors)) + if not graph.verify_acyclicity(): + raise AssuranceFlowError( + "cyclic provenance cannot carry recursive assurance propagation" + ) + self.graph = graph + self._graph_digest = canonical_digest(graph.to_dict()) + self._events: list[AssuranceEvent] = [] + self._resolutions: dict[str, ConflictResolution] = {} + + @property + def graph_digest(self) -> str: + return self._graph_digest + + def events(self) -> tuple[AssuranceEvent, ...]: + return tuple(self._events) + + def resolutions(self) -> tuple[ConflictResolution, ...]: + return tuple(self._resolutions.values()) + + def _append( + self, + *, + kind: AssuranceEventKind, + subject_id: str, + source_ids: Iterable[str], + proposition: str, + binding: Mapping[str, Any], + recorded_at: str, + evaluation: EvaluatedProposition, + evidence_payloads: Mapping[str, str], + attributes: Optional[Mapping[str, Any]] = None, + ) -> AssuranceEvent: + sources = tuple(sorted(set(source_ids))) + semantic_key = ( + kind.value, + subject_id, + sources, + proposition, + evaluation.binding_digest, + tuple(sorted((attributes or {}).items())), + ) + for event in self._events: + other_key = ( + event.kind.value, + event.subject_id, + event.source_ids, + event.proposition, + event.attributes.get("binding_digest", ""), + tuple(sorted((k, v) for k, v in event.attributes.items() if k != "binding_digest")), + ) + if semantic_key == other_key: + return event + previous = "" if not self._events else self._events[-1].digest() + combined_attributes = dict(attributes or {}) + combined_attributes["binding_digest"] = evaluation.binding_digest + event = AssuranceEvent( + len(self._events), + kind, + subject_id, + sources, + proposition, + dict(binding), + recorded_at, + evaluation.outcome, + evaluation.mechanism_id, + evaluation.mechanism_digest, + evaluation.evidence_refs, + dict(evidence_payloads), + evaluation.trust_roots, + evaluation.details, + previous, + combined_attributes, + ) + self._events.append(event) + return event + + def current_status(self, artifact_id: str) -> ArtifactStatus: + node = self.graph.artifacts.get(artifact_id) + if node is None: + return ArtifactStatus.UNKNOWN + latest_projection: Optional[ArtifactStatus] = None + rot_statuses: list[ArtifactStatus] = [] + for event in self._events: + if event.subject_id != artifact_id or event.outcome is not MechanismOutcome.PASS: + continue + if event.kind is AssuranceEventKind.STATUS_PROJECTION: + candidate = event.attributes.get("resulting_status") + if candidate is not None: + latest_projection = ArtifactStatus(str(candidate)) + elif event.kind is AssuranceEventKind.ROT: + candidate = event.attributes.get("resulting_status") + if candidate is not None: + rot_statuses.append(ArtifactStatus(str(candidate))) + statuses = [node.status, *rot_statuses] + if latest_projection is not None: + statuses.append(latest_projection) + return most_degraded(statuses) + + def impacted_descendants(self, artifact_id: str) -> tuple[str, ...]: + """Return the deduplicated recorded forward impact set, not a verdict.""" + if artifact_id not in self.graph.artifacts: + raise AssuranceFlowError(f"unknown impact origin {artifact_id}") + return tuple(sorted(self.graph.descendants((artifact_id,)) - {artifact_id})) + + def current_trust_events(self) -> tuple[AssuranceEvent, ...]: + """Return passing TRUST records still admissible under the current view.""" + unresolved_subjects = {item.subject_id for item in self.unresolved_conflicts()} + return tuple( + event + for event in self._events + if event.kind is AssuranceEventKind.TRUST + and event.outcome is MechanismOutcome.PASS + and event.subject_id not in unresolved_subjects + and all(source not in unresolved_subjects for source in event.source_ids) + and self.current_status(event.subject_id) is ArtifactStatus.VALID + and all( + self.current_status(source) is ArtifactStatus.VALID + for source in event.source_ids + ) + ) + + def unresolved_conflicts(self) -> tuple[ConflictRecord, ...]: + resolved = {item.conflict_id for item in self._resolutions.values()} + return tuple( + conflict + for conflict_id, conflict in self.graph.conflicts.items() + if conflict_id not in resolved + ) + + def materialize_current_graph(self) -> ProvenanceHypergraph: + """Create a derived current view; never mutate the historical graph.""" + current = ProvenanceHypergraph.from_dict(self.graph.to_dict()) + for artifact_id, node in tuple(current.artifacts.items()): + current.artifacts[artifact_id] = replace( + node, status=self.current_status(artifact_id) + ) + resolved = {item.conflict_id for item in self._resolutions.values()} + for conflict_id in resolved: + current.conflicts.pop(conflict_id, None) + return current + + def project_challenges( + self, + challenges: ChallengeLedger, + *, + recorded_at: str, + ) -> tuple[AssuranceEvent, ...]: + """Project challenge state into an additive current Graph overlay.""" + projected: list[AssuranceEvent] = [] + mechanism = ChallengeProjectionMechanism() + for artifact_id in sorted(self.graph.artifacts): + records = challenges.records(artifact_id) + if not records: + continue + claim_status = challenges.status(artifact_id) + store = EvidenceStore() + references = tuple( + store.add(canonical_bytes(record.to_dict())) for record in records + ) + binding = BoundProposition( + artifact_id, + "vstd.graph.current_status", + claim_status.status.value, + mechanism.mechanism_id, + mechanism.mechanism_digest, + references, + ("verifier.layer4.challenge.ChallengeLedger",), + EvidenceBounds( + len(references), + sum(len(canonical_bytes(record.to_dict())) for record in records), + ), + ) + session = VerificationSession(store) + session.register(mechanism) + event = self.record_status_projection( + artifact_id, + binding, + session=session, + recorded_at=recorded_at, + ) + if event.outcome is not MechanismOutcome.PASS: + raise AssuranceFlowError( + f"challenge projection failed for {artifact_id}: {event.details}" + ) + projected.append(event) + return tuple(projected) + + def record_status_projection( + self, + artifact_id: str, + proposition: BoundProposition, + *, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + """Record a mechanism-checked current-status projection additively.""" + if artifact_id not in self.graph.artifacts: + raise AssuranceFlowError(f"unknown status projection subject {artifact_id}") + if ( + proposition.subject_id != artifact_id + or proposition.predicate != "vstd.graph.current_status" + or proposition.mechanism_id + != ChallengeProjectionMechanism.mechanism_id + ): + raise AssuranceFlowError( + "status projection is not bound to the challenge projection mechanism" + ) + resulting_status = ArtifactStatus(str(proposition.expected)) + evaluation = session.evaluate(proposition) + return self._append( + kind=AssuranceEventKind.STATUS_PROJECTION, + subject_id=artifact_id, + source_ids=(), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={"resulting_status": resulting_status.value}, + ) + + def resolve_conflict( + self, + conflict_id: str, + selected_value: str, + proposition: BoundProposition, + *, + session: VerificationSession, + recorded_at: str, + ) -> ConflictResolution: + conflict = self.graph.conflicts.get(conflict_id) + if conflict is None: + raise AssuranceFlowError(f"unknown conflict {conflict_id}") + if conflict_id in {item.conflict_id for item in self._resolutions.values()}: + raise AssuranceFlowError(f"conflict {conflict_id} is already resolved additively") + if selected_value not in conflict.competing_values: + raise AssuranceFlowError("resolution must select one retained competing value") + if ( + proposition.subject_id != conflict.subject_id + or proposition.predicate != f"vstd.graph.resolve.{conflict.predicate}" + or proposition.expected != selected_value + or proposition.parameters.get("conflict_id") != conflict_id + ): + raise AssuranceFlowError("resolution evidence is not exactly conflict-bound") + evaluation = session.evaluate(proposition) + if not evaluation.passed: + raise AssuranceFlowError( + f"conflict remains unresolved: mechanism returned {evaluation.outcome.value}" + ) + resolution = ConflictResolution( + "resolution:" + canonical_digest( + [conflict_id, selected_value, evaluation.binding_digest] + ), + conflict_id, + selected_value, + recorded_at, + evaluation, + ) + self._resolutions[resolution.resolution_id] = resolution + self._append( + kind=AssuranceEventKind.CONFLICT_RESOLUTION, + subject_id=conflict.subject_id, + source_ids=(conflict_id,), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={ + "conflict_id": conflict_id, + "selected_value": selected_value, + "resolution_id": resolution.resolution_id, + }, + ) + return resolution + + def record_trust( + self, + target_id: str, + source_ids: Iterable[str], + proposition: BoundProposition, + *, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + sources = tuple(sorted(set(source_ids))) + if not sources: + raise AssuranceFlowError("TRUST requires at least one recorded source") + if target_id not in self.graph.artifacts: + raise AssuranceFlowError(f"unknown TRUST target {target_id}") + ancestors = self.graph.ancestors((target_id,)) - {target_id} + if any(source not in ancestors for source in sources): + raise AssuranceFlowError("TRUST sources must be recorded ancestors of target") + if self.current_status(target_id) is not ArtifactStatus.VALID or any( + self.current_status(source) is not ArtifactStatus.VALID + for source in sources + ): + raise AssuranceFlowError( + "inadmissible target or ancestry cannot provide current TRUST" + ) + unresolved_subjects = {item.subject_id for item in self.unresolved_conflicts()} + if target_id in unresolved_subjects or any(source in unresolved_subjects for source in sources): + raise AssuranceFlowError("unresolved conflict blocks clean TRUST") + expected = {"sources": list(sources), "target": target_id} + if ( + proposition.subject_id != target_id + or proposition.predicate != "vstd.graph.support" + or proposition.expected != expected + ): + raise AssuranceFlowError("TRUST proposition is not exactly topology-bound") + evaluation = session.evaluate(proposition) + return self._append( + kind=AssuranceEventKind.TRUST, + subject_id=target_id, + source_ids=sources, + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + ) + + def record_rot( + self, + artifact_id: str, + resulting_status: ArtifactStatus, + proposition: BoundProposition, + *, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + if artifact_id not in self.graph.artifacts: + raise AssuranceFlowError(f"unknown ROT subject {artifact_id}") + current = self.current_status(artifact_id) + if DEGRADATION_ORDER.index(resulting_status) <= DEGRADATION_ORDER.index(current): + raise AssuranceFlowError( + "ROT must strictly degrade current admissibility" + ) + if ( + proposition.subject_id != artifact_id + or proposition.predicate != "vstd.graph.current_status" + or proposition.expected != resulting_status.value + ): + raise AssuranceFlowError("ROT evidence is not exactly status-bound") + evaluation = session.evaluate(proposition) + return self._append( + kind=AssuranceEventKind.ROT, + subject_id=artifact_id, + source_ids=(), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={"resulting_status": resulting_status.value}, + ) + + def record_rust( + self, + descendant_id: str, + deviation: BoundProposition, + *, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + if descendant_id not in self.graph.artifacts: + raise AssuranceFlowError(f"unknown RUST origin {descendant_id}") + if ( + deviation.subject_id != descendant_id + or deviation.predicate != "vstd.graph.descendant_deviation" + or deviation.expected is not True + ): + raise AssuranceFlowError("RUST origin requires an exact deviation proposition") + evaluation = session.evaluate(deviation) + ancestors = tuple(sorted(self.graph.ancestors((descendant_id,)) - {descendant_id})) + return self._append( + kind=AssuranceEventKind.RUST, + subject_id=descendant_id, + source_ids=ancestors, + proposition=deviation.predicate, + binding=deviation.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={ + "meaning": "diagnostic reachability only", + "causal_localization": "NOT_ESTABLISHED", + }, + ) + + def rust_concentration(self) -> tuple[StructuralConcentration, ...]: + reached_by: dict[str, set[str]] = {} + for event in self._events: + if event.kind is not AssuranceEventKind.RUST or event.outcome is not MechanismOutcome.PASS: + continue + for ancestor in event.source_ids: + reached_by.setdefault(ancestor, set()).add(event.subject_id) + return tuple( + StructuralConcentration(ancestor, tuple(sorted(descendants))) + for ancestor, descendants in sorted(reached_by.items()) + ) + + def localize_cause( + self, + ancestor_id: str, + descendant_id: str, + proposition: BoundProposition, + *, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + if ancestor_id not in self.graph.ancestors((descendant_id,)) - {descendant_id}: + raise AssuranceFlowError("causal candidate is not a recorded ancestor") + rust_exists = any( + event.kind is AssuranceEventKind.RUST + and event.subject_id == descendant_id + and event.outcome is MechanismOutcome.PASS + for event in self._events + ) + if not rust_exists: + raise AssuranceFlowError("localization requires a verified descendant deviation") + expected = {"ancestor": ancestor_id, "descendant": descendant_id} + if ( + proposition.subject_id != descendant_id + or proposition.predicate != "vstd.graph.causal_localization" + or proposition.expected != expected + ): + raise AssuranceFlowError("causal localization is not exactly relation-bound") + evaluation = session.evaluate(proposition) + return self._append( + kind=AssuranceEventKind.CAUSAL_LOCALIZATION, + subject_id=descendant_id, + source_ids=(ancestor_id,), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + ) + + def diagnose( + self, + kind: DiagnosticKind, + ancestor_id: str, + descendant_id: str, + proposition: Optional[BoundProposition], + *, + session: VerificationSession, + recorded_at: str, + ) -> DiagnosticAttribution: + """Compute bounded artifact-relative BLAME or GUILT just in time. + + BLAME means only that the named artifact-relative responsibility + proposition passed. GUILT additionally names an exact violated + obligation in ``proposition.parameters['obligation']``. Neither result + concerns an actor's moral character, reputation, or general trust. + """ + localization = next( + ( + event + for event in reversed(self._events) + if event.kind is AssuranceEventKind.CAUSAL_LOCALIZATION + and event.subject_id == descendant_id + and event.source_ids == (ancestor_id,) + and event.outcome is MechanismOutcome.PASS + ), + None, + ) + if localization is None: + return DiagnosticAttribution( + kind, + ancestor_id, + descendant_id, + "NOT_ESTABLISHED", + "", + None, + "RUST reachability does not establish causal localization", + ) + if proposition is None: + return DiagnosticAttribution( + kind, + ancestor_id, + descendant_id, + "NOT_ESTABLISHED", + localization.digest(), + None, + "diagnostic attribution requires a separate exact mechanism", + ) + expected = { + "ancestor": ancestor_id, + "descendant": descendant_id, + "localization_event_digest": localization.digest(), + } + if ( + proposition.subject_id != ancestor_id + or proposition.predicate != f"vstd.graph.diagnostic.{kind.value.lower()}" + or proposition.expected != expected + ): + raise AssuranceFlowError("diagnostic proposition is not exactly relation-bound") + if kind is DiagnosticKind.GUILT and not proposition.parameters.get("obligation"): + raise AssuranceFlowError("GUILT requires an exact violated obligation") + evaluation = session.evaluate(proposition) + event = self._append( + kind=AssuranceEventKind.DIAGNOSTIC_ATTRIBUTION, + subject_id=ancestor_id, + source_ids=(descendant_id,), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={ + "diagnostic_kind": kind.value, + "localization_event_digest": localization.digest(), + "obligation": proposition.parameters.get("obligation", ""), + }, + ) + return DiagnosticAttribution( + kind, + ancestor_id, + descendant_id, + "ESTABLISHED" if evaluation.passed else "NOT_ESTABLISHED", + localization.digest(), + evaluation, + event.details, + ) + + def verify_hash_chain(self) -> bool: + previous = "" + for sequence, event in enumerate(self._events): + if event.sequence != sequence or event.previous_event_digest != previous: + return False + previous = event.digest() + return True + + def to_dict(self) -> dict[str, Any]: + return { + "schema_version": self.FORMAT, + "historical_graph_digest": self.graph_digest, + "historical_graph": self.graph.to_dict(), + "events": [event.to_dict() for event in self._events], + "conflict_resolutions": [item.to_dict() for item in self._resolutions.values()], + "current_view_digest": canonical_digest(self.materialize_current_graph().to_dict()), + } + + +def recheck_assurance_log( + payload: Mapping[str, Any], + *, + mechanisms: Iterable[VerificationMechanism], +) -> AssuranceLedger: + """Rebuild and replay a portable assurance log from its embedded bytes.""" + if payload.get("schema_version") != AssuranceLedger.FORMAT: + raise AssuranceFlowError("not a VSTD-Graph assurance log") + graph_data = payload.get("historical_graph") + events_data = payload.get("events") + if not isinstance(graph_data, Mapping) or not isinstance(events_data, list): + raise AssuranceFlowError("assurance log is missing its Graph or events") + graph = ProvenanceHypergraph.from_dict(graph_data) + if canonical_digest(graph.to_dict()) != payload.get("historical_graph_digest"): + raise AssuranceFlowError("historical Graph digest does not match embedded bytes") + ledger = AssuranceLedger(graph) + store = EvidenceStore() + for event_data in events_data: + if not isinstance(event_data, Mapping): + raise AssuranceFlowError("assurance event is not an object") + embedded = event_data.get("evidence_payloads") + if not isinstance(embedded, Mapping): + raise AssuranceFlowError("assurance event has no embedded evidence") + store.import_base64( + {str(reference): str(encoded) for reference, encoded in embedded.items()} + ) + session = VerificationSession(store) + builtin = ChallengeProjectionMechanism() + session.register(builtin) + for mechanism in mechanisms: + if mechanism.mechanism_id == builtin.mechanism_id: + raise AssuranceFlowError( + "the built-in challenge projection mechanism cannot be replaced" + ) + session.register(mechanism) + + for expected in events_data: + try: + kind = AssuranceEventKind(str(expected["kind"])) + subject_id = str(expected["subject_id"]) + source_ids = tuple(str(item) for item in expected["source_ids"]) + binding_data = expected["binding"] + if not isinstance(binding_data, Mapping): + raise TypeError("event binding is not an object") + proposition = BoundProposition.from_dict(binding_data) + recorded_at = str(expected["recorded_at"]) + attributes = expected.get("attributes", {}) + if not isinstance(attributes, Mapping): + raise TypeError("event attributes are not an object") + + if kind is AssuranceEventKind.TRUST: + event = ledger.record_trust( + subject_id, + source_ids, + proposition, + session=session, + recorded_at=recorded_at, + ) + elif kind is AssuranceEventKind.ROT: + event = ledger.record_rot( + subject_id, + ArtifactStatus(str(attributes["resulting_status"])), + proposition, + session=session, + recorded_at=recorded_at, + ) + elif kind is AssuranceEventKind.RUST: + event = ledger.record_rust( + subject_id, + proposition, + session=session, + recorded_at=recorded_at, + ) + elif kind is AssuranceEventKind.STATUS_PROJECTION: + event = ledger.record_status_projection( + subject_id, + proposition, + session=session, + recorded_at=recorded_at, + ) + elif kind is AssuranceEventKind.CONFLICT_RESOLUTION: + ledger.resolve_conflict( + str(attributes["conflict_id"]), + str(attributes["selected_value"]), + proposition, + session=session, + recorded_at=recorded_at, + ) + event = ledger.events()[-1] + elif kind is AssuranceEventKind.CAUSAL_LOCALIZATION: + if len(source_ids) != 1: + raise AssuranceFlowError( + "causal localization must name exactly one ancestor" + ) + event = ledger.localize_cause( + source_ids[0], + subject_id, + proposition, + session=session, + recorded_at=recorded_at, + ) + else: + if len(source_ids) != 1: + raise AssuranceFlowError( + "diagnostic attribution must name exactly one descendant" + ) + ledger.diagnose( + DiagnosticKind(str(attributes["diagnostic_kind"])), + subject_id, + source_ids[0], + proposition, + session=session, + recorded_at=recorded_at, + ) + event = ledger.events()[-1] + except (KeyError, TypeError, ValueError) as exc: + raise AssuranceFlowError(f"cannot replay assurance event: {exc}") from exc + if event.to_dict() != dict(expected): + raise AssuranceFlowError( + f"recomputed assurance event {event.sequence} does not match the log" + ) + + if ledger.to_dict() != dict(payload): + raise AssuranceFlowError( + "recomputed assurance state does not match the serialized log" + ) + return ledger + + +__all__ = [ + "AssuranceEvent", + "AssuranceEventKind", + "AssuranceFlowError", + "AssuranceLedger", + "ChallengeProjectionMechanism", + "ConflictResolution", + "DiagnosticAttribution", + "DiagnosticKind", + "StructuralConcentration", + "recheck_assurance_log", +] diff --git a/src/verifier/data/graph_level.py b/src/verifier/data/graph_level.py index 5530b35..64bb59d 100644 --- a/src/verifier/data/graph_level.py +++ b/src/verifier/data/graph_level.py @@ -60,8 +60,18 @@ Grounding, VariableGrounding, Verdict, + canonical_digest, ) from verifier.core.checker import MinimalIndependentDPLL +from verifier.core.evidence import ( + BoundProposition, + EvidenceStore, + EvaluatedProposition, + MechanismOutcome, + VerificationMechanism, + VerificationSession, +) +from verifier.core.depth import claim_binding_from_dict from verifier.core.kernel import check as kernel_check from verifier.core.refutation import build_horn_certificate from verifier.data.models import ArtifactStatus, ProvenanceHypergraph @@ -89,6 +99,24 @@ """ +def graph_collection_binding_digest( + graph: ProvenanceHypergraph, + *, + collection_id: str, + members: Sequence[str], + binding: ClaimBinding, +) -> str: + """Bind ratings to one Graph, member set, collection, and claim coordinate.""" + return canonical_digest( + { + "collection_id": collection_id, + "members": sorted(set(members)), + "historical_graph_digest": canonical_digest(graph.to_dict()), + "claim_binding_digest": binding.digest(), + } + ) + + class GraphEncodingError(RuntimeError): """The encoding, the solver and the direct computation do not all agree. @@ -443,6 +471,58 @@ def to_dict(self) -> dict[str, Any]: } +@dataclass(frozen=True) +class EvidenceBoundGraphLevelResult: + """Graph profile whose object and edge ratings were rerun and bound.""" + + candidate: GraphLevelResult + object_evaluations: tuple[tuple[str, EvaluatedProposition], ...] + edge_evaluations: tuple[tuple[str, EvaluatedProposition], ...] + binding_errors: tuple[str, ...] + kernel_outcome: str + + @property + def level(self) -> int: + return self.candidate.level + + @property + def conformance_status(self) -> str: + if ( + self.level < GRAPH_MIN_LEVEL + or self.binding_errors + or self.kernel_outcome != "ACCEPTED" + ): + return "NOT_ESTABLISHED" + evaluations = self.object_evaluations + self.edge_evaluations + if not evaluations or any(not result.passed for _, result in evaluations): + return "NOT_ESTABLISHED" + return "ESTABLISHED" + + @property + def rating_basis(self) -> str: + return "MECHANISM_EVALUATED" + + def to_dict(self) -> dict[str, Any]: + payload = self.candidate.to_dict() + payload.update( + { + "rating_basis": self.rating_basis, + "conformance_status": self.conformance_status, + "object_evaluations": { + subject: result.to_dict() + for subject, result in self.object_evaluations + }, + "edge_evaluations": { + subject: result.to_dict() + for subject, result in self.edge_evaluations + }, + "binding_errors": list(self.binding_errors), + "kernel_outcome": self.kernel_outcome, + } + ) + return payload + + def graph_level( graph: ProvenanceHypergraph, collection: GraphCollection, @@ -501,3 +581,232 @@ def graph_level( binding=binding, ) return GraphLevelResult(collection.collection_id, 0, None, refutation, blocked) + + +def establish_graph_level( + graph: ProvenanceHypergraph, + *, + collection_id: str, + members: Sequence[str], + object_evidence: Mapping[str, BoundProposition], + edge_evidence: Mapping[str, BoundProposition], + session: VerificationSession, + binding: ClaimBinding, +) -> EvidenceBoundGraphLevelResult: + """Rerun rating mechanisms before computing a conforming Graph profile. + + Each reachable artifact must bind ``vstd.object_profile`` and each reachable + transformation must bind ``vstd.graph_edge_profile`` to an integer in + ``1..5`` under ``parameters['collection_id']``. Missing, neighboring, + duplicate, failed, or uncertain propositions contribute rating zero and + prevent conformance; their field placement cannot promote the collection. + """ + + if not members: + raise GraphEncodingError("an evidence-bound Graph collection must have members") + normalized_members = tuple(sorted(set(members))) + closure = graph.ancestors(normalized_members) + edges = { + edge.transformation_id + for artifact_id in closure + for edge in graph.incoming_hyperedges(artifact_id) + } + errors: list[str] = [] + object_results: list[tuple[str, EvaluatedProposition]] = [] + edge_results: list[tuple[str, EvaluatedProposition]] = [] + object_levels: dict[str, int] = {} + edge_levels: dict[str, int] = {} + exact_collection_binding = graph_collection_binding_digest( + graph, + collection_id=collection_id, + members=normalized_members, + binding=binding, + ) + + extra_objects = set(object_evidence) - closure + extra_edges = set(edge_evidence) - edges + if extra_objects: + errors.append(f"object ratings outside provenance closure: {sorted(extra_objects)}") + if extra_edges: + errors.append(f"edge ratings outside provenance closure: {sorted(extra_edges)}") + + def evaluate_rating( + subject: str, + proposition: Optional[BoundProposition], + predicate: str, + sink: list[tuple[str, EvaluatedProposition]], + ) -> int: + if proposition is None: + errors.append(f"missing rating evidence for {subject}") + return 0 + if type(proposition.expected) is not int: + errors.append(f"rating for {subject} is not an integer") + return 0 + rating = proposition.expected + if not GRAPH_MIN_LEVEL <= rating <= GRAPH_MAX_LEVEL: + errors.append(f"rating for {subject} is outside 1..5") + return 0 + if ( + proposition.subject_id != subject + or proposition.predicate != predicate + or proposition.parameters.get("collection_id") != collection_id + or proposition.parameters.get("collection_binding_digest") + != exact_collection_binding + ): + errors.append(f"rating evidence for {subject} is not exactly collection-bound") + return 0 + result = session.evaluate(proposition) + sink.append((subject, result)) + if result.outcome is not MechanismOutcome.PASS: + errors.append( + f"rating mechanism for {subject} returned {result.outcome.value}" + ) + return 0 + return rating + + for artifact_id in sorted(closure): + object_levels[artifact_id] = evaluate_rating( + artifact_id, + object_evidence.get(artifact_id), + "vstd.object_profile", + object_results, + ) + for transformation_id in sorted(edges): + edge_levels[transformation_id] = evaluate_rating( + transformation_id, + edge_evidence.get(transformation_id), + "vstd.graph_edge_profile", + edge_results, + ) + + candidate = graph_level( + graph, + GraphCollection( + collection_id, + normalized_members, + object_levels, + edge_levels, + ), + binding=binding, + ) + kernel_outcome = "REJECTED" + certificate = candidate.witness or candidate.refutation + if certificate is not None: + kernel_outcome = kernel_check(certificate, binding=binding).outcome.value + return EvidenceBoundGraphLevelResult( + candidate, + tuple(object_results), + tuple(edge_results), + tuple(errors), + kernel_outcome, + ) + + +def build_evidence_bound_graph_level_record( + result: EvidenceBoundGraphLevelResult, + *, + graph: ProvenanceHypergraph, + members: Sequence[str], + binding: ClaimBinding, + object_evidence: Mapping[str, BoundProposition], + edge_evidence: Mapping[str, BoundProposition], + session: VerificationSession, +) -> dict[str, Any]: + """Serialize exact Graph rating bindings and bytes for offline replay.""" + recomputed = establish_graph_level( + graph, + collection_id=result.candidate.collection_id, + members=members, + object_evidence=object_evidence, + edge_evidence=edge_evidence, + session=session, + binding=binding, + ) + if canonical_digest(recomputed.to_dict()) != canonical_digest(result.to_dict()): + raise ValueError("Graph profile result does not match the supplied replay inputs") + all_refs = tuple( + sorted( + { + reference + for proposition in (*object_evidence.values(), *edge_evidence.values()) + for reference in proposition.evidence_refs + } + ) + ) + normalized_members = tuple(sorted(set(members))) + payload = result.to_dict() + payload.update( + { + "members": list(normalized_members), + "binding": binding.to_dict(), + "evidence_bindings": { + "objects": { + subject: proposition.to_dict() + for subject, proposition in sorted(object_evidence.items()) + }, + "edges": { + subject: proposition.to_dict() + for subject, proposition in sorted(edge_evidence.items()) + }, + }, + "evidence_payloads": session.evidence.export_base64(all_refs), + } + ) + return payload + + +def recheck_evidence_bound_graph_level_record( + graph: ProvenanceHypergraph, + record: Mapping[str, Any], + *, + mechanisms: Sequence[VerificationMechanism], +) -> EvidenceBoundGraphLevelResult: + """Rebuild the evidence store, rerun rating mechanisms, and compare result.""" + if record.get("rating_basis") != "MECHANISM_EVALUATED": + raise ValueError("Graph profile record is not mechanism-evaluated") + payloads = record.get("evidence_payloads") + bindings = record.get("evidence_bindings") + binding_data = record.get("binding") + members = record.get("members") + if ( + not isinstance(payloads, Mapping) + or not isinstance(bindings, Mapping) + or not isinstance(binding_data, Mapping) + or not isinstance(members, Sequence) + or isinstance(members, (str, bytes)) + ): + raise ValueError("evidence-bound Graph record is missing replay inputs") + store = EvidenceStore() + store.import_base64({str(key): str(value) for key, value in payloads.items()}) + session = VerificationSession(store) + for mechanism in mechanisms: + session.register(mechanism) + objects_data = bindings.get("objects") + edges_data = bindings.get("edges") + if not isinstance(objects_data, Mapping) or not isinstance(edges_data, Mapping): + raise ValueError("Graph evidence binding maps are missing") + objects = { + str(subject): BoundProposition.from_dict(proposition) + for subject, proposition in objects_data.items() + if isinstance(proposition, Mapping) + } + edges = { + str(subject): BoundProposition.from_dict(proposition) + for subject, proposition in edges_data.items() + if isinstance(proposition, Mapping) + } + result = establish_graph_level( + graph, + collection_id=str(record["collection_id"]), + members=tuple(str(item) for item in members), + object_evidence=objects, + edge_evidence=edges, + session=session, + binding=claim_binding_from_dict(binding_data), + ) + for result_field, value in result.to_dict().items(): + if record.get(result_field) != value: + raise ValueError( + f"recomputed Graph field does not match receipt: {result_field}" + ) + return result diff --git a/src/verifier/layer4/challenge.py b/src/verifier/layer4/challenge.py index 6ff158d..d1fcda0 100644 --- a/src/verifier/layer4/challenge.py +++ b/src/verifier/layer4/challenge.py @@ -7,8 +7,10 @@ verdict state is item 7 on the challenge-theater list, and until now this repository was on that list: ``ArtifactStatus.CHALLENGED`` has existed in ``verifier.data.models`` with **no producer anywhere in the tree**. This module -produces challenge-ledger claim state only; it is not an adapter that mutates or -binds that state into a VSTD-Graph artifact. +produces challenge-ledger claim state. The separate +``verifier.data.assurance.AssuranceLedger.project_challenges`` mechanism now binds +the complete serialized record set into an additive VSTD-Graph current-state view; +this module still never mutates a historical Graph artifact. The state machine:: diff --git a/src/verifier/specifications/LADDER.md b/src/verifier/specifications/LADDER.md index 08fccf2..8bb59d6 100644 --- a/src/verifier/specifications/LADDER.md +++ b/src/verifier/specifications/LADDER.md @@ -69,6 +69,7 @@ The rest of the Standard uses the following terms precisely: | **Profile axis** | One ordered family of cumulative checklists. VSTD has an object axis and a Graph axis. | Equal numbers on different axes do not identify equivalent or interchangeable results. | | **Object profile depth** | For one verification object, start at `VSTD-1` and count upward only while every required coordinate remains established. The last uninterrupted number is its depth. | Depth is a compact summary of separately established results, not a new verdict, evidence-strength rating, or permission to ignore a later established coordinate after an earlier gap. | | **Candidate Graph profile** | The greatest Graph checklist position satisfied by the current caller-supplied ratings. | The current calculation is `NOT_ESTABLISHED` because those ratings are not evidence-bound. It is not a verified Graph profile. | +| **Evidence-bound Graph profile** | The greatest Graph checklist position obtained after rerunning exact member, ancestor, and edge rating mechanisms from content-addressed evidence. | It is established only under the named mechanisms, trust roots, evidence, bounds, lifecycle view, and conflict state. | | **VSTD-4 rung** | One of the fourteen ordered refutability obligations `4.1` through `4.14`. | “Rung†names only this internal sequence, never a top-level VSTD profile. | | **Verification order** | One adjacent meta-verification order in the VSTD-2 geometry model. | The compatibility names `VerificationLayer` and `verification_layers` do not denote numbered VSTD profiles. | | **Level** | A retained word in an explicitly named external taxonomy or compatibility identifier, including `ReproducibilityLevel`, `AvailabilityLevel`, `graph_level`, and serialized Graph `level` fields. | In Graph compatibility identifiers, the value is the candidate Graph profile number; “level†is not the governing name for a VSTD profile. | @@ -140,8 +141,9 @@ can be an artifact when created, versioned, or evaluated and an actor when it ex transformation. The capitalized terms **TRUST**, **RUST**, and **ROT** are formal VSTD semantic names, not -acronyms, serialized receipt values, actor ratings, scalar scores, or references to the Rust programming -language. The same bound development graph and its time-indexed lifecycle carry three +acronyms, numbered-profile receipt verdicts, actor ratings, scalar scores, or references to +the Rust programming language. They serialize as typed event kinds only in the non-receipt +`VSTD-GRAPH-ASSURANCE-1` mechanism log. The same bound development graph and its time-indexed lifecycle carry three distinct relations: ```text @@ -192,12 +194,17 @@ intervention-level physical causality, causal localization, responsibility, or g TRUST, ROT, and RUST MUST remain separate. They do not cancel, form one scalar score, or flow in the opposite direction as inherited truth, decay, or guilt. `UNKNOWN` and `CONFLICTED` support or lineage MUST remain visible and MUST NOT become a clean signal. -This section fixes their semantic orientation and prohibited inferences; event formats, -TRUST-transfer algebra, ROT derivation and propagation, RUST concentration-independence, -and localization protocols each require their own specification and evidence. Until those -exist, the three terms are causal-provenance and lifecycle constraints, not serialized receipt values or -computable conformance results; no current VSTD runtime emits or validates a TRUST, ROT, or -RUST transfer. +`VSTD-GRAPH-ASSURANCE-1` now serializes an additive, hash-chained reference event log with +the complete historical Graph, exact proposition bindings, and embedded evidence bytes. +`AssuranceLedger` implements mechanism-earned forward TRUST, typed ROT, challenge-ledger +status projection, reverse RUST reachability, unique-descendant structural concentration, +additive conflict resolution, explicit causal localization, and bounded artifact-relative +diagnostic attribution. Duplicate paths and repeated records remain set-valued and earn no +strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded +evidence, reruns every exact mechanism, reproduces the event hash chain, and compares the +derived current view. A deployment still supplies the proposition-specific mechanisms: the +event format and dispatcher do not create a universal support-transfer algebra or infer +causality from topology. Artifact freezing and sealing are bounded mechanisms under this orientation, specified separately in [`ARTIFACT_CONTROL.md`](ARTIFACT_CONTROL.md). A verified freeze preserves @@ -289,6 +296,14 @@ a **candidate Graph profile** with those inputs, not the validity of the ratings. At a result below 5, the grounded `FAIL` certificate for profile `N+1` explains that candidate ceiling. Graph conformance additionally requires evidence-bound ratings under the applicable object and edge profiles. +`establish_graph_level` supplies that path: it rehashes embedded evidence, reruns the exact +registered rating mechanism for every member, ancestor, and reached edge, then recomputes +and kernel-checks the Graph certificate. Each rating proposition binds a digest over the +historical Graph bytes, deduplicated member set, collection identifier, and claim binding; +neighboring collection or topology evidence therefore contributes zero. Missing, +non-integer, or non-passing bindings also contribute zero and prevent conformance. Profile +zero never receives `ESTABLISHED`. The record builder recomputes before serialization, and +the rechecker preserves offline replay. --- @@ -382,7 +397,9 @@ The certificate for rung `k+1` explains a partial VSTD-4 normative depth. Only e VSTD-4 conformance at normative depth 14 admits a claim to any VSTD-5 procedure. The current reference `vstd4_depth` function instead computes a structural candidate from caller-supplied rung references, labels conformance `NOT_ESTABLISHED`, and never admits -VSTD-5. See `VSTD-4.md` for the normative rung graph and `VSTD4-GDC-1` format. +VSTD-5. `establish_vstd4` reruns exact VSTD-1/2/3 and rung bindings and may report +`EVIDENCE_BOUND` / `ESTABLISHED` only when every mechanism and the independent kernel pass. +See `VSTD-4.md` for the normative rung graph and `VSTD4-GDC-1` format. --- diff --git a/src/verifier/specifications/VSTD-4.md b/src/verifier/specifications/VSTD-4.md index bdec9af..6f90584 100644 --- a/src/verifier/specifications/VSTD-4.md +++ b/src/verifier/specifications/VSTD-4.md @@ -8,7 +8,7 @@ **Numbered profile:** VSTD-4 on the object axis; required closure coordinate: Refutability (see `LADDER.md`) **Certificate format:** `VSTD4-GDC-1` -**Status:** project specification; candidate computation implemented; evidence binding and conformance not implemented +**Status:** project specification with implemented candidate and evidence-bound reference paths **Editor:** TimeLordRaps **License:** Apache-2.0 **Date:** 2026-08-22 @@ -52,6 +52,15 @@ references, validate their rung propositions, or check VSTD-1/2/3 preconditions. result is therefore `CANDIDATE` with `conformance_status = NOT_ESTABLISHED`, including at candidate depth 14, and the reference VSTD-5 entry gate rejects it. +`verifier.core.depth.establish_vstd4` is the evidence-bound path. It requires +exact `BoundProposition` records for VSTD-1, VSTD-2, VSTD-3, and all fourteen +rungs; resolves and rehashes every embedded evidence payload; selects a registered +mechanism by identifier and implementation digest; enforces evidence byte/item +bounds; reruns the mechanism; and independently checks the resulting structural +certificate. Only the complete passing result reports `depth_kind = EVIDENCE_BOUND`, +`conformance_status = ESTABLISHED`, and admits VSTD-5. The receipt builder embeds +the bindings and evidence bytes, and the rechecker recomputes the result offline. + --- ## 2. The fourteen-rung sequence @@ -342,11 +351,12 @@ bounded checking. ## 6. Reference implementation boundary -The reference certificate producer, candidate-depth computation, and data structures are in: +The reference certificate producer, candidate/evidence-bound computations, and data structures are in: * `src/verifier/core/certificate.py` * `src/verifier/core/grounding.py` * `src/verifier/core/depth.py` +* `src/verifier/core/evidence.py` * `src/verifier/core/refutation.py` * `src/verifier/layer4/` @@ -354,9 +364,11 @@ The trusted checker is `src/verifier/core/kernel.py`. Producer modules are not part of its trusted import boundary. The kernel checks the supplied certificate, grounding, and `ClaimBinding` for internal -consistency. It does not retrieve rung references or establish the required prerequisite-profile -results. Kernel acceptance of a candidate certificate is therefore not VSTD-4 -conformance. +consistency. It does not retrieve rung references or establish prerequisite-profile +results by itself. Kernel acceptance of a candidate certificate is therefore not VSTD-4 +conformance. The evidence-bound path performs those additional checks before it can +report conformance; its result remains bounded to the registered mechanisms, trust roots, +evidence, and resource limits. No external implementation, interoperability profile, or third-party attack has yet been demonstrated for `VSTD4-GDC-1`. This implementation status MUST remain diff --git a/src/verifier/specifications/VSTD-5.md b/src/verifier/specifications/VSTD-5.md index ddbbfcb..2f47f1c 100644 --- a/src/verifier/specifications/VSTD-5.md +++ b/src/verifier/specifications/VSTD-5.md @@ -3,96 +3,120 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-5 on the object axis; required closure coordinate: Witness Corroboration (see `LADDER.md`) -**Status:** DRAFT — not implemented +**Status:** project specification with implemented evidence-bound reference mechanism **Editor:** TimeLordRaps **License:** Apache-2.0 -**Date:** 2026-08-22 +**Date:** 2026-08-29 -VSTD-5 binds a fully refutable claim to witnesses that do not share the -declarant's trust root. It is the first numbered profile that cannot be established by a -declarant acting alone. - -This document is a draft interface, not an implementation or a claim that any -independent witness exists. +VSTD-5 binds a fully refutable claim to an actually checked witness relation. It is +the first numbered object profile that a declarant acting alone cannot satisfy. +Witness identity names a coordinate; it never supplies computational trust. --- ## 1. Entry gate -Every VSTD-5 procedure MUST reject a claim unless VSTD-1/2/3 preconditions and all -VSTD-4 rung propositions have been evidence-bound and checked, establishing normative -VSTD-4 conformance at normative depth 14: - -``` -vstd4_depth(claim) == 14 -``` +Every VSTD-5 procedure MUST reject a claim unless VSTD-1, VSTD-2, and VSTD-3 +preconditions and all VSTD-4 rung propositions were evidence-bound and checked, +establishing VSTD-4 conformance at depth 14. -The gate is not satisfied by a structural candidate over caller-supplied references. -The current reference candidate reports `conformance_status = NOT_ESTABLISHED`, and -`require_vstd5_entry` rejects it even at candidate depth 14. A witness cannot -corroborate a claim whose refutability does not compose. +The compatibility `vstd4_depth` candidate never satisfies this gate. The reference +`establish_vstd4` path may satisfy it only after rerunning every exact evidence +binding and checking its depth certificate. `require_vstd5_entry` distinguishes the +two result types and fails closed. --- -## 2. Required record families +## 2. Required records -A future conforming receipt will contain: +The reference receipt contains: -* `WitnessIdentity` — the witness and the method used to bind the record to it; -* `IndependenceAssertion` — shared control, vendor, jurisdiction, funding, - infrastructure, and trust-root relationships; -* `CorroborationRecord` — what the witness independently checked, the VSTD-4 - certificate checked, observable results, time, and bounds; -* `CorroborationClass` — procurement, power/thermal envelope, network egress, - vendor telemetry, financial attestation, or physical inspection; and -* `DisagreementRecord` — conflicting observations and their effect on the claim. +* `WitnessIdentity` — witness coordinate plus content-addressed identity evidence; +* `IndependenceAssertion` — one state and evidence binding for each required + declarant/witness separation dimension; +* `CorroborationRecord` — exact VSTD-4 commitment, certificate, checker descriptor, + observations, result, time, class, and executable verification binding; +* derived disagreements — conflicting checked records retained without voting or + averaging; and +* embedded evidence bytes — enough to rehash and rerun the registered mechanisms + offline. -Independence fields MUST be evidence-bearing. A declarant's statement that a -witness is independent is not independence evidence. +Schema validity establishes only shape. `recheck_vstd5_receipt` imports and hashes +the embedded bytes, checks the admitted VSTD-4 result digest, reruns every registered +mechanism, and compares the complete derived result. --- ## 3. Independence -At minimum, an independence assertion MUST name whether declarant and witness -share: +For every declarant/witness pair, the procedure checks whether they share: 1. ownership or operational control; -2. a verdict-producing codebase; +2. verdict-producing code; 3. a verifier trust root; 4. an evidence source or telemetry provider; 5. infrastructure capable of changing the observed result; 6. financial dependence material to the corroboration; and -7. a jurisdiction or contractual relationship material to compulsion. - -`UNKNOWN` in any required independence dimension MUST cap the independence claim. +7. jurisdictional or contractual dependence material to compulsion. -> Claim independence MUST NOT exceed the independence of its weakest binding -> witness. +Every `SEPARATE` state MUST carry a `BoundProposition` for the exact negative +relationship, the admitted claim commitment, evidence references, mechanism +identifier and digest, trust roots, and bounds. `SHARED`, `UNKNOWN`, missing, +failed, or unevaluable dimensions prevent an `INDEPENDENT` result. -Independence is not manufacturable from self-report at any cryptographic -strength. +Repeated evidence, duplicate identifiers, identity keys, signatures, reputation, +and field names MUST NOT manufacture independence. The same identity evidence used +under multiple witness identifiers is rejected. --- ## 4. Corroboration and disagreement -A corroboration record MUST bind the exact VSTD-4 commitment `C`, certificate -digest, checker descriptor, observable evidence, result, and observation time. -Checking a neighbouring claim or a different commitment is not corroboration. +A corroboration mechanism MUST bind and check the exact: + +* claim commitment; +* VSTD-4 certificate digest; +* checker descriptor digest; +* witness coordinate; +* observation time; +* observation evidence bytes; and +* `CORROBORATED`, `REFUTED`, or `UNKNOWN` result. + +A record's certificate digest MUST equal the admitted evidence-bound VSTD-4 witness, +not merely a caller-selected digest repeated in both fields. Every identified witness +MUST contribute a corroboration record; dangling identities do not create plurality. -Witnesses are not votes. Conflicting witnesses MUST degrade the claim and create -an additive `DisagreementRecord`; their conclusions MUST NOT be averaged into an -apparently clean result. +A mechanism-earned negative result remains negative. Conflicting checked records +produce `CONFLICTED`; witnesses are not votes, and majority count never cleans the +conflict. A positive corroboration with any unresolved independence seam is reported as +overall `UNKNOWN`, not as independently corroborated. Reusing the same evidence set under +another corroboration identifier is rejected rather than counted twice. --- -## 5. Draft boundary +## 5. Reference algorithm + +`verifier.core.witness.assess_witness_corroboration` performs, in order: + +1. evidence-bound VSTD-4 entry validation; +2. identity-evidence availability and duplicate detection; +3. exact seven-dimension independence evaluation; +4. exact corroboration binding and mechanism execution; +5. duplicate-evidence refusal; +6. disagreement derivation; and +7. bounded result emission with all errors and limitations retained. + +`build_vstd5_receipt` serializes the replay inputs. `recheck_vstd5_receipt` reruns +them. Neither function turns an identity coordinate into trust or establishes a +fact outside the propositions checked by its registered mechanisms. + +--- -The schema `receipts/schema/vstd5_receipt.json` records the intended shape for -review. No reference witness transport, identity scheme, independence scoring -algorithm, or second-party implementation is shipped in release v1.0.0. +## 6. Current limits -The document remains `DRAFT` until VSTD-4 operating experience supplies evidence -for the final protocol. A draft schema MUST NOT be presented as VSTD-5 -conformance. +The repository ships the meta-verification mechanism and adversarial fixtures. It +does not ship or claim a real independent third-party witness, external +interoperability deployment, accreditation, or a universal way to infer real-world +separation. A deployment must supply mechanisms that actually check its evidence; +registering a mechanism names the trust boundary but does not make that mechanism +correct. diff --git a/src/verifier/specifications/VSTD-Graph-2.md b/src/verifier/specifications/VSTD-Graph-2.md index 7778283..ec4c50c 100644 --- a/src/verifier/specifications/VSTD-Graph-2.md +++ b/src/verifier/specifications/VSTD-Graph-2.md @@ -3,7 +3,7 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-Graph-2; required closure coordinate: Bounded Collection Surface (see `LADDER.md`) -**Status:** implemented candidate computation; rating-evidence binding not implemented +**Status:** project specification with implemented candidate and evidence-bound reference paths **License:** Apache-2.0 VSTD-Graph-2 closes collection-scope leakage. A collection satisfies this candidate @@ -11,8 +11,14 @@ profile only when every member and provenance ancestor is rated at object profil higher, every reachable status is admissible, and every transformation hyperedge carries profile-2 edge ratings. -`verifier.data.graph_level` computes a candidate from caller-supplied ratings and marks -conformance `NOT_ESTABLISHED`. The `FAIL` certificate for Graph profile 2 names the member, +`verifier.data.graph_level.graph_level` computes a candidate from caller-supplied ratings and marks +conformance `NOT_ESTABLISHED`. `establish_graph_level` instead reruns exact member, +ancestor, and edge rating propositions from embedded evidence through registered +mechanisms; only that path may report `MECHANISM_EVALUATED` and `ESTABLISHED`. The +rating propositions bind one digest over the exact historical Graph, deduplicated member +set, collection identifier, and Graph claim binding. A neighboring collection, topology, +or claim therefore contributes rating zero. Profile zero is never established. The +`FAIL` certificate for Graph profile 2 names the member, ancestor, status, or edge obligation that prevents admission under those inputs. It does not validate the ratings themselves. diff --git a/src/verifier/specifications/VSTD-Graph-3.md b/src/verifier/specifications/VSTD-Graph-3.md index 4f8bc70..8fc6f5f 100644 --- a/src/verifier/specifications/VSTD-Graph-3.md +++ b/src/verifier/specifications/VSTD-Graph-3.md @@ -3,7 +3,7 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-Graph-3; required closure coordinate: Accountable Provenance Closure (see `LADDER.md`) -**Status:** implemented candidate computation; rating-evidence binding not implemented +**Status:** project specification with implemented candidate and evidence-bound reference paths **License:** Apache-2.0 VSTD-Graph-3 closes unaccountable substrate across a collection. A collection @@ -15,9 +15,12 @@ The provenance closure condition is normative: rating only the selected members is insufficient. The weakest reachable ancestor or transformation caps the collection. -The reference computation consumes caller-supplied ratings and therefore reports a -candidate with conformance `NOT_ESTABLISHED`. Its certificate does not establish that a -VSTD-3 mechanism produced any supplied rating. +The compatibility computation consumes caller-supplied ratings and therefore reports a +candidate with conformance `NOT_ESTABLISHED`. `establish_graph_level` reruns a registered +mechanism over the exact evidence bytes for every member, ancestor, and transformation +rating; missing, failed, uncertain, neighboring, or out-of-closure bindings contribute +zero and prevent conformance. Every proposition also binds the exact Graph bytes, +deduplicated member set, collection identifier, and claim binding. VSTD-Graph-3 cannot establish that an outside party could refute the composed collection. That blind spot is closed by VSTD-Graph-4. diff --git a/src/verifier/specifications/VSTD-Graph-4.md b/src/verifier/specifications/VSTD-Graph-4.md index 230e3f6..c4597bc 100644 --- a/src/verifier/specifications/VSTD-Graph-4.md +++ b/src/verifier/specifications/VSTD-Graph-4.md @@ -5,7 +5,7 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-Graph-4; required closure coordinate: Refutable Transformation Closure (see `LADDER.md`) -**Status:** implemented candidate computation; rating-evidence binding not implemented +**Status:** project specification with implemented candidate and evidence-bound reference paths **License:** Apache-2.0 VSTD-Graph-4 closes non-compositional refutability. A collection satisfies this candidate @@ -17,6 +17,10 @@ Two VSTD-4 nodes connected by an unevidenced edge do not make a VSTD-Graph-4 collection. A challenge to the collection output must localize to a member, ancestor, transformation, or the composition itself. -The UNSAT certificate at the next profile is the computed explanation of the candidate +The unsatisfiable (UNSAT) certificate at the next profile is the computed explanation of the candidate ceiling over caller-supplied ratings. It does not establish Graph-4 conformance or validate the claimed `RefutabilityClosure` records. +The evidence-bound path reruns every exact rating mechanism and embeds the proposition +bindings and evidence bytes for offline replay. Those bindings commit to the exact Graph, +deduplicated members, collection identifier, and claim binding. A Graph-4 edge rating mechanism must +actually check the applicable `RefutabilityClosure`; naming one is insufficient. diff --git a/src/verifier/specifications/VSTD-Graph-5.md b/src/verifier/specifications/VSTD-Graph-5.md index 8552586..6123ef8 100644 --- a/src/verifier/specifications/VSTD-Graph-5.md +++ b/src/verifier/specifications/VSTD-Graph-5.md @@ -3,7 +3,7 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-Graph-5; required closure coordinate: Corroborated Verification Network (see `LADDER.md`) -**Status:** DRAFT profile; computation is implemented, witness protocol is not +**Status:** project specification with implemented candidate and evidence-bound reference paths **License:** Apache-2.0 VSTD-Graph-5 is the collection profile for independently corroborated members, @@ -11,9 +11,12 @@ ancestors, and transformations. The candidate-profile computation requires object and edge ratings of at least 5, provenance closure, and admissible status throughout. -Because VSTD-5 is draft, the reference implementation can compute this profile -only over externally supplied profile-5 ratings; it does not manufacture or verify -their independence. A result based on self-declared ratings is not VSTD-Graph-5 +The compatibility implementation computes this profile over externally supplied +profile-5 ratings and reports `NOT_ESTABLISHED`. The evidence-bound path can establish +it only when registered mechanisms rerun exact VSTD-5 member/ancestor ratings and +profile-5 transformation ratings from embedded evidence. Every rating is bound to the +exact Graph bytes, deduplicated member set, collection identifier, and claim binding. A result based on +self-declared ratings is not VSTD-Graph-5 conformance. Conflicting witness records are retained as conflict records and make the relevant diff --git a/src/verifier/specifications/WIRE_IDENTIFIERS.md b/src/verifier/specifications/WIRE_IDENTIFIERS.md index da77128..674c0aa 100644 --- a/src/verifier/specifications/WIRE_IDENTIFIERS.md +++ b/src/verifier/specifications/WIRE_IDENTIFIERS.md @@ -3,7 +3,7 @@ > **Acronyms:** command-line interface (CLI). **Status:** normative for current serialized-receipt dispatch -**Date:** 2026-08-27 +**Date:** 2026-08-29 A **serialized receipt identifier** is the value written into a receipt to select its exact reader and schema, principally `schema_version` plus any required profile discriminator. Standards literature often calls this a *wire identifier* or part of a *wire format*; here it means the stored JavaScript Object Notation (JSON) contract, not a network protocol. @@ -26,7 +26,7 @@ closed: | `VSTD-2.md` | `schema_version = "VSTD-2"` | | `VSTD-3.md` | `schema_version = "VSTD-3.0"` | | `VSTD-4.md` | `schema_version = "VSTD-4"` | -| `VSTD-5.md` | `schema_version = "VSTD-5-DRAFT"` | +| `VSTD-5.md` | `schema_version = "VSTD-5"` | | `VSTD-Graph-1.md` | `schema_version = "VSTD-DATA-0.1"` | VSTD-1 has two current receipt profiles: @@ -52,7 +52,7 @@ fields participate in the canonical digest. are serialized receipt values. `POST_VERIFIED`, `GEOMETRY_INSPECTABLE`, and `COMPLETELY_VERIFIED` are descriptive terms rather than receipt values. -## 2. Stored artifact-control identifiers +## 2. Stored non-receipt mechanism identifiers Artifact-control mechanism objects are stored JSON contracts, not network traffic, VSTD receipts, or new numbered profiles. They dispatch independently by: @@ -69,6 +69,13 @@ combined schema is published as [`artifact-control-1.schema.json`](https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json). These identifiers do not imply a network protocol or VSTD conformance result. +The Graph assurance event log dispatches separately as +`schema_version = "VSTD-GRAPH-ASSURANCE-1"`. Its governing behavior is +[`LADDER.md` section 1.1](LADDER.md#11-artifact-first-causal-provenance-orientation), +and its strict schema is +[`vstd-graph-assurance-1.schema.json`](https://timelordraps.github.io/verifier/schemas/vstd-graph-assurance-1.schema.json). +It is not an artifact-control object or a numbered-profile receipt. + ## 3. Import package and distribution The distribution is `verifier-standard`, the import package is `verifier`, and @@ -84,5 +91,6 @@ obsolete standard identifiers into current receipt dispatch. ## 4. Release versioning A repository release number does not claim conformance to a same-numbered VSTD profile. -VSTD-5 remains draft until its required witness mechanism and evidence binding are -implemented. +VSTD-5's reference mechanism is implemented. This project-specification status does not +claim an external witness, independent implementation, standards-body consensus, +accreditation, or interoperability deployment. diff --git a/standard/LADDER.md b/standard/LADDER.md index 08fccf2..8bb59d6 100644 --- a/standard/LADDER.md +++ b/standard/LADDER.md @@ -69,6 +69,7 @@ The rest of the Standard uses the following terms precisely: | **Profile axis** | One ordered family of cumulative checklists. VSTD has an object axis and a Graph axis. | Equal numbers on different axes do not identify equivalent or interchangeable results. | | **Object profile depth** | For one verification object, start at `VSTD-1` and count upward only while every required coordinate remains established. The last uninterrupted number is its depth. | Depth is a compact summary of separately established results, not a new verdict, evidence-strength rating, or permission to ignore a later established coordinate after an earlier gap. | | **Candidate Graph profile** | The greatest Graph checklist position satisfied by the current caller-supplied ratings. | The current calculation is `NOT_ESTABLISHED` because those ratings are not evidence-bound. It is not a verified Graph profile. | +| **Evidence-bound Graph profile** | The greatest Graph checklist position obtained after rerunning exact member, ancestor, and edge rating mechanisms from content-addressed evidence. | It is established only under the named mechanisms, trust roots, evidence, bounds, lifecycle view, and conflict state. | | **VSTD-4 rung** | One of the fourteen ordered refutability obligations `4.1` through `4.14`. | “Rung†names only this internal sequence, never a top-level VSTD profile. | | **Verification order** | One adjacent meta-verification order in the VSTD-2 geometry model. | The compatibility names `VerificationLayer` and `verification_layers` do not denote numbered VSTD profiles. | | **Level** | A retained word in an explicitly named external taxonomy or compatibility identifier, including `ReproducibilityLevel`, `AvailabilityLevel`, `graph_level`, and serialized Graph `level` fields. | In Graph compatibility identifiers, the value is the candidate Graph profile number; “level†is not the governing name for a VSTD profile. | @@ -140,8 +141,9 @@ can be an artifact when created, versioned, or evaluated and an actor when it ex transformation. The capitalized terms **TRUST**, **RUST**, and **ROT** are formal VSTD semantic names, not -acronyms, serialized receipt values, actor ratings, scalar scores, or references to the Rust programming -language. The same bound development graph and its time-indexed lifecycle carry three +acronyms, numbered-profile receipt verdicts, actor ratings, scalar scores, or references to +the Rust programming language. They serialize as typed event kinds only in the non-receipt +`VSTD-GRAPH-ASSURANCE-1` mechanism log. The same bound development graph and its time-indexed lifecycle carry three distinct relations: ```text @@ -192,12 +194,17 @@ intervention-level physical causality, causal localization, responsibility, or g TRUST, ROT, and RUST MUST remain separate. They do not cancel, form one scalar score, or flow in the opposite direction as inherited truth, decay, or guilt. `UNKNOWN` and `CONFLICTED` support or lineage MUST remain visible and MUST NOT become a clean signal. -This section fixes their semantic orientation and prohibited inferences; event formats, -TRUST-transfer algebra, ROT derivation and propagation, RUST concentration-independence, -and localization protocols each require their own specification and evidence. Until those -exist, the three terms are causal-provenance and lifecycle constraints, not serialized receipt values or -computable conformance results; no current VSTD runtime emits or validates a TRUST, ROT, or -RUST transfer. +`VSTD-GRAPH-ASSURANCE-1` now serializes an additive, hash-chained reference event log with +the complete historical Graph, exact proposition bindings, and embedded evidence bytes. +`AssuranceLedger` implements mechanism-earned forward TRUST, typed ROT, challenge-ledger +status projection, reverse RUST reachability, unique-descendant structural concentration, +additive conflict resolution, explicit causal localization, and bounded artifact-relative +diagnostic attribution. Duplicate paths and repeated records remain set-valued and earn no +strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded +evidence, reruns every exact mechanism, reproduces the event hash chain, and compares the +derived current view. A deployment still supplies the proposition-specific mechanisms: the +event format and dispatcher do not create a universal support-transfer algebra or infer +causality from topology. Artifact freezing and sealing are bounded mechanisms under this orientation, specified separately in [`ARTIFACT_CONTROL.md`](ARTIFACT_CONTROL.md). A verified freeze preserves @@ -289,6 +296,14 @@ a **candidate Graph profile** with those inputs, not the validity of the ratings. At a result below 5, the grounded `FAIL` certificate for profile `N+1` explains that candidate ceiling. Graph conformance additionally requires evidence-bound ratings under the applicable object and edge profiles. +`establish_graph_level` supplies that path: it rehashes embedded evidence, reruns the exact +registered rating mechanism for every member, ancestor, and reached edge, then recomputes +and kernel-checks the Graph certificate. Each rating proposition binds a digest over the +historical Graph bytes, deduplicated member set, collection identifier, and claim binding; +neighboring collection or topology evidence therefore contributes zero. Missing, +non-integer, or non-passing bindings also contribute zero and prevent conformance. Profile +zero never receives `ESTABLISHED`. The record builder recomputes before serialization, and +the rechecker preserves offline replay. --- @@ -382,7 +397,9 @@ The certificate for rung `k+1` explains a partial VSTD-4 normative depth. Only e VSTD-4 conformance at normative depth 14 admits a claim to any VSTD-5 procedure. The current reference `vstd4_depth` function instead computes a structural candidate from caller-supplied rung references, labels conformance `NOT_ESTABLISHED`, and never admits -VSTD-5. See `VSTD-4.md` for the normative rung graph and `VSTD4-GDC-1` format. +VSTD-5. `establish_vstd4` reruns exact VSTD-1/2/3 and rung bindings and may report +`EVIDENCE_BOUND` / `ESTABLISHED` only when every mechanism and the independent kernel pass. +See `VSTD-4.md` for the normative rung graph and `VSTD4-GDC-1` format. --- diff --git a/standard/VSTD-4.md b/standard/VSTD-4.md index bdec9af..6f90584 100644 --- a/standard/VSTD-4.md +++ b/standard/VSTD-4.md @@ -8,7 +8,7 @@ **Numbered profile:** VSTD-4 on the object axis; required closure coordinate: Refutability (see `LADDER.md`) **Certificate format:** `VSTD4-GDC-1` -**Status:** project specification; candidate computation implemented; evidence binding and conformance not implemented +**Status:** project specification with implemented candidate and evidence-bound reference paths **Editor:** TimeLordRaps **License:** Apache-2.0 **Date:** 2026-08-22 @@ -52,6 +52,15 @@ references, validate their rung propositions, or check VSTD-1/2/3 preconditions. result is therefore `CANDIDATE` with `conformance_status = NOT_ESTABLISHED`, including at candidate depth 14, and the reference VSTD-5 entry gate rejects it. +`verifier.core.depth.establish_vstd4` is the evidence-bound path. It requires +exact `BoundProposition` records for VSTD-1, VSTD-2, VSTD-3, and all fourteen +rungs; resolves and rehashes every embedded evidence payload; selects a registered +mechanism by identifier and implementation digest; enforces evidence byte/item +bounds; reruns the mechanism; and independently checks the resulting structural +certificate. Only the complete passing result reports `depth_kind = EVIDENCE_BOUND`, +`conformance_status = ESTABLISHED`, and admits VSTD-5. The receipt builder embeds +the bindings and evidence bytes, and the rechecker recomputes the result offline. + --- ## 2. The fourteen-rung sequence @@ -342,11 +351,12 @@ bounded checking. ## 6. Reference implementation boundary -The reference certificate producer, candidate-depth computation, and data structures are in: +The reference certificate producer, candidate/evidence-bound computations, and data structures are in: * `src/verifier/core/certificate.py` * `src/verifier/core/grounding.py` * `src/verifier/core/depth.py` +* `src/verifier/core/evidence.py` * `src/verifier/core/refutation.py` * `src/verifier/layer4/` @@ -354,9 +364,11 @@ The trusted checker is `src/verifier/core/kernel.py`. Producer modules are not part of its trusted import boundary. The kernel checks the supplied certificate, grounding, and `ClaimBinding` for internal -consistency. It does not retrieve rung references or establish the required prerequisite-profile -results. Kernel acceptance of a candidate certificate is therefore not VSTD-4 -conformance. +consistency. It does not retrieve rung references or establish prerequisite-profile +results by itself. Kernel acceptance of a candidate certificate is therefore not VSTD-4 +conformance. The evidence-bound path performs those additional checks before it can +report conformance; its result remains bounded to the registered mechanisms, trust roots, +evidence, and resource limits. No external implementation, interoperability profile, or third-party attack has yet been demonstrated for `VSTD4-GDC-1`. This implementation status MUST remain diff --git a/standard/VSTD-5.md b/standard/VSTD-5.md index ddbbfcb..2f47f1c 100644 --- a/standard/VSTD-5.md +++ b/standard/VSTD-5.md @@ -3,96 +3,120 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-5 on the object axis; required closure coordinate: Witness Corroboration (see `LADDER.md`) -**Status:** DRAFT — not implemented +**Status:** project specification with implemented evidence-bound reference mechanism **Editor:** TimeLordRaps **License:** Apache-2.0 -**Date:** 2026-08-22 +**Date:** 2026-08-29 -VSTD-5 binds a fully refutable claim to witnesses that do not share the -declarant's trust root. It is the first numbered profile that cannot be established by a -declarant acting alone. - -This document is a draft interface, not an implementation or a claim that any -independent witness exists. +VSTD-5 binds a fully refutable claim to an actually checked witness relation. It is +the first numbered object profile that a declarant acting alone cannot satisfy. +Witness identity names a coordinate; it never supplies computational trust. --- ## 1. Entry gate -Every VSTD-5 procedure MUST reject a claim unless VSTD-1/2/3 preconditions and all -VSTD-4 rung propositions have been evidence-bound and checked, establishing normative -VSTD-4 conformance at normative depth 14: - -``` -vstd4_depth(claim) == 14 -``` +Every VSTD-5 procedure MUST reject a claim unless VSTD-1, VSTD-2, and VSTD-3 +preconditions and all VSTD-4 rung propositions were evidence-bound and checked, +establishing VSTD-4 conformance at depth 14. -The gate is not satisfied by a structural candidate over caller-supplied references. -The current reference candidate reports `conformance_status = NOT_ESTABLISHED`, and -`require_vstd5_entry` rejects it even at candidate depth 14. A witness cannot -corroborate a claim whose refutability does not compose. +The compatibility `vstd4_depth` candidate never satisfies this gate. The reference +`establish_vstd4` path may satisfy it only after rerunning every exact evidence +binding and checking its depth certificate. `require_vstd5_entry` distinguishes the +two result types and fails closed. --- -## 2. Required record families +## 2. Required records -A future conforming receipt will contain: +The reference receipt contains: -* `WitnessIdentity` — the witness and the method used to bind the record to it; -* `IndependenceAssertion` — shared control, vendor, jurisdiction, funding, - infrastructure, and trust-root relationships; -* `CorroborationRecord` — what the witness independently checked, the VSTD-4 - certificate checked, observable results, time, and bounds; -* `CorroborationClass` — procurement, power/thermal envelope, network egress, - vendor telemetry, financial attestation, or physical inspection; and -* `DisagreementRecord` — conflicting observations and their effect on the claim. +* `WitnessIdentity` — witness coordinate plus content-addressed identity evidence; +* `IndependenceAssertion` — one state and evidence binding for each required + declarant/witness separation dimension; +* `CorroborationRecord` — exact VSTD-4 commitment, certificate, checker descriptor, + observations, result, time, class, and executable verification binding; +* derived disagreements — conflicting checked records retained without voting or + averaging; and +* embedded evidence bytes — enough to rehash and rerun the registered mechanisms + offline. -Independence fields MUST be evidence-bearing. A declarant's statement that a -witness is independent is not independence evidence. +Schema validity establishes only shape. `recheck_vstd5_receipt` imports and hashes +the embedded bytes, checks the admitted VSTD-4 result digest, reruns every registered +mechanism, and compares the complete derived result. --- ## 3. Independence -At minimum, an independence assertion MUST name whether declarant and witness -share: +For every declarant/witness pair, the procedure checks whether they share: 1. ownership or operational control; -2. a verdict-producing codebase; +2. verdict-producing code; 3. a verifier trust root; 4. an evidence source or telemetry provider; 5. infrastructure capable of changing the observed result; 6. financial dependence material to the corroboration; and -7. a jurisdiction or contractual relationship material to compulsion. - -`UNKNOWN` in any required independence dimension MUST cap the independence claim. +7. jurisdictional or contractual dependence material to compulsion. -> Claim independence MUST NOT exceed the independence of its weakest binding -> witness. +Every `SEPARATE` state MUST carry a `BoundProposition` for the exact negative +relationship, the admitted claim commitment, evidence references, mechanism +identifier and digest, trust roots, and bounds. `SHARED`, `UNKNOWN`, missing, +failed, or unevaluable dimensions prevent an `INDEPENDENT` result. -Independence is not manufacturable from self-report at any cryptographic -strength. +Repeated evidence, duplicate identifiers, identity keys, signatures, reputation, +and field names MUST NOT manufacture independence. The same identity evidence used +under multiple witness identifiers is rejected. --- ## 4. Corroboration and disagreement -A corroboration record MUST bind the exact VSTD-4 commitment `C`, certificate -digest, checker descriptor, observable evidence, result, and observation time. -Checking a neighbouring claim or a different commitment is not corroboration. +A corroboration mechanism MUST bind and check the exact: + +* claim commitment; +* VSTD-4 certificate digest; +* checker descriptor digest; +* witness coordinate; +* observation time; +* observation evidence bytes; and +* `CORROBORATED`, `REFUTED`, or `UNKNOWN` result. + +A record's certificate digest MUST equal the admitted evidence-bound VSTD-4 witness, +not merely a caller-selected digest repeated in both fields. Every identified witness +MUST contribute a corroboration record; dangling identities do not create plurality. -Witnesses are not votes. Conflicting witnesses MUST degrade the claim and create -an additive `DisagreementRecord`; their conclusions MUST NOT be averaged into an -apparently clean result. +A mechanism-earned negative result remains negative. Conflicting checked records +produce `CONFLICTED`; witnesses are not votes, and majority count never cleans the +conflict. A positive corroboration with any unresolved independence seam is reported as +overall `UNKNOWN`, not as independently corroborated. Reusing the same evidence set under +another corroboration identifier is rejected rather than counted twice. --- -## 5. Draft boundary +## 5. Reference algorithm + +`verifier.core.witness.assess_witness_corroboration` performs, in order: + +1. evidence-bound VSTD-4 entry validation; +2. identity-evidence availability and duplicate detection; +3. exact seven-dimension independence evaluation; +4. exact corroboration binding and mechanism execution; +5. duplicate-evidence refusal; +6. disagreement derivation; and +7. bounded result emission with all errors and limitations retained. + +`build_vstd5_receipt` serializes the replay inputs. `recheck_vstd5_receipt` reruns +them. Neither function turns an identity coordinate into trust or establishes a +fact outside the propositions checked by its registered mechanisms. + +--- -The schema `receipts/schema/vstd5_receipt.json` records the intended shape for -review. No reference witness transport, identity scheme, independence scoring -algorithm, or second-party implementation is shipped in release v1.0.0. +## 6. Current limits -The document remains `DRAFT` until VSTD-4 operating experience supplies evidence -for the final protocol. A draft schema MUST NOT be presented as VSTD-5 -conformance. +The repository ships the meta-verification mechanism and adversarial fixtures. It +does not ship or claim a real independent third-party witness, external +interoperability deployment, accreditation, or a universal way to infer real-world +separation. A deployment must supply mechanisms that actually check its evidence; +registering a mechanism names the trust boundary but does not make that mechanism +correct. diff --git a/standard/VSTD-Graph-2.md b/standard/VSTD-Graph-2.md index 7778283..ec4c50c 100644 --- a/standard/VSTD-Graph-2.md +++ b/standard/VSTD-Graph-2.md @@ -3,7 +3,7 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-Graph-2; required closure coordinate: Bounded Collection Surface (see `LADDER.md`) -**Status:** implemented candidate computation; rating-evidence binding not implemented +**Status:** project specification with implemented candidate and evidence-bound reference paths **License:** Apache-2.0 VSTD-Graph-2 closes collection-scope leakage. A collection satisfies this candidate @@ -11,8 +11,14 @@ profile only when every member and provenance ancestor is rated at object profil higher, every reachable status is admissible, and every transformation hyperedge carries profile-2 edge ratings. -`verifier.data.graph_level` computes a candidate from caller-supplied ratings and marks -conformance `NOT_ESTABLISHED`. The `FAIL` certificate for Graph profile 2 names the member, +`verifier.data.graph_level.graph_level` computes a candidate from caller-supplied ratings and marks +conformance `NOT_ESTABLISHED`. `establish_graph_level` instead reruns exact member, +ancestor, and edge rating propositions from embedded evidence through registered +mechanisms; only that path may report `MECHANISM_EVALUATED` and `ESTABLISHED`. The +rating propositions bind one digest over the exact historical Graph, deduplicated member +set, collection identifier, and Graph claim binding. A neighboring collection, topology, +or claim therefore contributes rating zero. Profile zero is never established. The +`FAIL` certificate for Graph profile 2 names the member, ancestor, status, or edge obligation that prevents admission under those inputs. It does not validate the ratings themselves. diff --git a/standard/VSTD-Graph-3.md b/standard/VSTD-Graph-3.md index 4f8bc70..8fc6f5f 100644 --- a/standard/VSTD-Graph-3.md +++ b/standard/VSTD-Graph-3.md @@ -3,7 +3,7 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-Graph-3; required closure coordinate: Accountable Provenance Closure (see `LADDER.md`) -**Status:** implemented candidate computation; rating-evidence binding not implemented +**Status:** project specification with implemented candidate and evidence-bound reference paths **License:** Apache-2.0 VSTD-Graph-3 closes unaccountable substrate across a collection. A collection @@ -15,9 +15,12 @@ The provenance closure condition is normative: rating only the selected members is insufficient. The weakest reachable ancestor or transformation caps the collection. -The reference computation consumes caller-supplied ratings and therefore reports a -candidate with conformance `NOT_ESTABLISHED`. Its certificate does not establish that a -VSTD-3 mechanism produced any supplied rating. +The compatibility computation consumes caller-supplied ratings and therefore reports a +candidate with conformance `NOT_ESTABLISHED`. `establish_graph_level` reruns a registered +mechanism over the exact evidence bytes for every member, ancestor, and transformation +rating; missing, failed, uncertain, neighboring, or out-of-closure bindings contribute +zero and prevent conformance. Every proposition also binds the exact Graph bytes, +deduplicated member set, collection identifier, and claim binding. VSTD-Graph-3 cannot establish that an outside party could refute the composed collection. That blind spot is closed by VSTD-Graph-4. diff --git a/standard/VSTD-Graph-4.md b/standard/VSTD-Graph-4.md index 230e3f6..c4597bc 100644 --- a/standard/VSTD-Graph-4.md +++ b/standard/VSTD-Graph-4.md @@ -5,7 +5,7 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-Graph-4; required closure coordinate: Refutable Transformation Closure (see `LADDER.md`) -**Status:** implemented candidate computation; rating-evidence binding not implemented +**Status:** project specification with implemented candidate and evidence-bound reference paths **License:** Apache-2.0 VSTD-Graph-4 closes non-compositional refutability. A collection satisfies this candidate @@ -17,6 +17,10 @@ Two VSTD-4 nodes connected by an unevidenced edge do not make a VSTD-Graph-4 collection. A challenge to the collection output must localize to a member, ancestor, transformation, or the composition itself. -The UNSAT certificate at the next profile is the computed explanation of the candidate +The unsatisfiable (UNSAT) certificate at the next profile is the computed explanation of the candidate ceiling over caller-supplied ratings. It does not establish Graph-4 conformance or validate the claimed `RefutabilityClosure` records. +The evidence-bound path reruns every exact rating mechanism and embeds the proposition +bindings and evidence bytes for offline replay. Those bindings commit to the exact Graph, +deduplicated members, collection identifier, and claim binding. A Graph-4 edge rating mechanism must +actually check the applicable `RefutabilityClosure`; naming one is insufficient. diff --git a/standard/VSTD-Graph-5.md b/standard/VSTD-Graph-5.md index 8552586..6123ef8 100644 --- a/standard/VSTD-Graph-5.md +++ b/standard/VSTD-Graph-5.md @@ -3,7 +3,7 @@ > Reader aid: [concept glossary and primary precedents](https://github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md). **Numbered profile:** VSTD-Graph-5; required closure coordinate: Corroborated Verification Network (see `LADDER.md`) -**Status:** DRAFT profile; computation is implemented, witness protocol is not +**Status:** project specification with implemented candidate and evidence-bound reference paths **License:** Apache-2.0 VSTD-Graph-5 is the collection profile for independently corroborated members, @@ -11,9 +11,12 @@ ancestors, and transformations. The candidate-profile computation requires object and edge ratings of at least 5, provenance closure, and admissible status throughout. -Because VSTD-5 is draft, the reference implementation can compute this profile -only over externally supplied profile-5 ratings; it does not manufacture or verify -their independence. A result based on self-declared ratings is not VSTD-Graph-5 +The compatibility implementation computes this profile over externally supplied +profile-5 ratings and reports `NOT_ESTABLISHED`. The evidence-bound path can establish +it only when registered mechanisms rerun exact VSTD-5 member/ancestor ratings and +profile-5 transformation ratings from embedded evidence. Every rating is bound to the +exact Graph bytes, deduplicated member set, collection identifier, and claim binding. A result based on +self-declared ratings is not VSTD-Graph-5 conformance. Conflicting witness records are retained as conflict records and make the relevant diff --git a/standard/WIRE_IDENTIFIERS.md b/standard/WIRE_IDENTIFIERS.md index da77128..674c0aa 100644 --- a/standard/WIRE_IDENTIFIERS.md +++ b/standard/WIRE_IDENTIFIERS.md @@ -3,7 +3,7 @@ > **Acronyms:** command-line interface (CLI). **Status:** normative for current serialized-receipt dispatch -**Date:** 2026-08-27 +**Date:** 2026-08-29 A **serialized receipt identifier** is the value written into a receipt to select its exact reader and schema, principally `schema_version` plus any required profile discriminator. Standards literature often calls this a *wire identifier* or part of a *wire format*; here it means the stored JavaScript Object Notation (JSON) contract, not a network protocol. @@ -26,7 +26,7 @@ closed: | `VSTD-2.md` | `schema_version = "VSTD-2"` | | `VSTD-3.md` | `schema_version = "VSTD-3.0"` | | `VSTD-4.md` | `schema_version = "VSTD-4"` | -| `VSTD-5.md` | `schema_version = "VSTD-5-DRAFT"` | +| `VSTD-5.md` | `schema_version = "VSTD-5"` | | `VSTD-Graph-1.md` | `schema_version = "VSTD-DATA-0.1"` | VSTD-1 has two current receipt profiles: @@ -52,7 +52,7 @@ fields participate in the canonical digest. are serialized receipt values. `POST_VERIFIED`, `GEOMETRY_INSPECTABLE`, and `COMPLETELY_VERIFIED` are descriptive terms rather than receipt values. -## 2. Stored artifact-control identifiers +## 2. Stored non-receipt mechanism identifiers Artifact-control mechanism objects are stored JSON contracts, not network traffic, VSTD receipts, or new numbered profiles. They dispatch independently by: @@ -69,6 +69,13 @@ combined schema is published as [`artifact-control-1.schema.json`](https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json). These identifiers do not imply a network protocol or VSTD conformance result. +The Graph assurance event log dispatches separately as +`schema_version = "VSTD-GRAPH-ASSURANCE-1"`. Its governing behavior is +[`LADDER.md` section 1.1](LADDER.md#11-artifact-first-causal-provenance-orientation), +and its strict schema is +[`vstd-graph-assurance-1.schema.json`](https://timelordraps.github.io/verifier/schemas/vstd-graph-assurance-1.schema.json). +It is not an artifact-control object or a numbered-profile receipt. + ## 3. Import package and distribution The distribution is `verifier-standard`, the import package is `verifier`, and @@ -84,5 +91,6 @@ obsolete standard identifiers into current receipt dispatch. ## 4. Release versioning A repository release number does not claim conformance to a same-numbered VSTD profile. -VSTD-5 remains draft until its required witness mechanism and evidence binding are -implemented. +VSTD-5's reference mechanism is implemented. This project-specification status does not +claim an external witness, independent implementation, standards-body consensus, +accreditation, or interoperability deployment. diff --git a/standard/schemas/vstd-graph-assurance-1.schema.json b/standard/schemas/vstd-graph-assurance-1.schema.json new file mode 100644 index 0000000..d4cc2e9 --- /dev/null +++ b/standard/schemas/vstd-graph-assurance-1.schema.json @@ -0,0 +1,108 @@ +{ + "$comment": "Terminology: Secure Hash Algorithm 256-bit (SHA-256); Verifier Standard (VSTD). TRUST, RUST, and ROT are formal semantic names, not acronyms.", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://timelordraps.github.io/verifier/schemas/vstd-graph-assurance-1.schema.json", + "title": "VSTD-Graph Assurance Event Log", + "description": "Strict shape for the additive, hash-chained reference event log. Schema validity establishes shape only. Mechanism outcomes must be replayed from the embedded evidence with the exact bound mechanism implementation.", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "historical_graph_digest", "historical_graph", "events", "conflict_resolutions", "current_view_digest"], + "properties": { + "schema_version": {"const": "VSTD-GRAPH-ASSURANCE-1"}, + "historical_graph_digest": {"$ref": "#/$defs/digest"}, + "historical_graph": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd_graph_receipt.json#/properties/hypergraph"}, + "events": { + "type": "array", + "items": {"$ref": "#/$defs/event"} + }, + "conflict_resolutions": { + "type": "array", + "items": {"$ref": "#/$defs/resolution"} + }, + "current_view_digest": {"$ref": "#/$defs/digest"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "digestRef": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "evaluation": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "outcome", "mechanism_id", "mechanism_digest", "evidence_refs", "trust_roots", "observed_evidence_bytes", "details", "observations"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "outcome": {"enum": ["PASS", "FAIL", "UNKNOWN"]}, + "mechanism_id": {"type": "string", "minLength": 1}, + "mechanism_digest": {"$ref": "#/$defs/digestRef"}, + "evidence_refs": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/digestRef"}}, + "trust_roots": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + "observed_evidence_bytes": {"type": "integer", "minimum": 0}, + "details": {"type": "string"}, + "observations": {"type": "object"} + } + }, + "event": { + "type": "object", + "additionalProperties": false, + "required": ["sequence", "kind", "subject_id", "source_ids", "proposition", "binding", "recorded_at", "outcome", "mechanism_id", "mechanism_digest", "evidence_refs", "evidence_payloads", "trust_roots", "details", "previous_event_digest", "attributes", "event_digest"], + "properties": { + "sequence": {"type": "integer", "minimum": 0}, + "kind": {"enum": ["TRUST", "ROT", "RUST", "STATUS_PROJECTION", "CONFLICT_RESOLUTION", "CAUSAL_LOCALIZATION", "DIAGNOSTIC_ATTRIBUTION"]}, + "subject_id": {"type": "string", "minLength": 1}, + "source_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + "proposition": {"type": "string", "minLength": 1}, + "binding": {"$ref": "#/$defs/binding"}, + "recorded_at": {"type": "string", "minLength": 1}, + "outcome": {"enum": ["PASS", "FAIL", "UNKNOWN"]}, + "mechanism_id": {"type": "string", "minLength": 1}, + "mechanism_digest": {"$ref": "#/$defs/digestRef"}, + "evidence_refs": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/digestRef"}}, + "evidence_payloads": { + "type": "object", + "propertyNames": {"pattern": "^sha256:[0-9a-f]{64}$"}, + "additionalProperties": {"type": "string", "contentEncoding": "base64"} + }, + "trust_roots": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + "details": {"type": "string"}, + "previous_event_digest": {"type": "string", "pattern": "^(?:[0-9a-f]{64})?$"}, + "attributes": {"type": "object"}, + "event_digest": {"$ref": "#/$defs/digest"} + } + }, + "binding": { + "type": "object", + "additionalProperties": false, + "required": ["subject_id", "predicate", "expected", "mechanism_id", "mechanism_digest", "evidence_refs", "trust_roots", "bounds", "parameters"], + "properties": { + "subject_id": {"type": "string", "minLength": 1}, + "predicate": {"type": "string", "minLength": 1}, + "expected": {}, + "mechanism_id": {"type": "string", "minLength": 1}, + "mechanism_digest": {"$ref": "#/$defs/digestRef"}, + "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/digestRef"}}, + "trust_roots": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + "bounds": { + "type": "object", + "additionalProperties": false, + "required": ["max_evidence_items", "max_evidence_bytes"], + "properties": { + "max_evidence_items": {"type": "integer", "minimum": 0}, + "max_evidence_bytes": {"type": "integer", "minimum": 0} + } + }, + "parameters": {"type": "object", "additionalProperties": {"type": "string"}} + } + }, + "resolution": { + "type": "object", + "additionalProperties": false, + "required": ["resolution_id", "conflict_id", "selected_value", "recorded_at", "evaluation"], + "properties": { + "resolution_id": {"type": "string", "minLength": 1}, + "conflict_id": {"type": "string", "minLength": 1}, + "selected_value": {"type": "string"}, + "recorded_at": {"type": "string", "minLength": 1}, + "evaluation": {"$ref": "#/$defs/evaluation"} + } + } + } +} diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py new file mode 100644 index 0000000..80a3650 --- /dev/null +++ b/tests/test_evidence_bound_assurance.py @@ -0,0 +1,1080 @@ +"""Terminology: JavaScript Object Notation (JSON); Secure Hash Algorithm 256-bit +(SHA-256); Verifier Standard (VSTD). + +Adversarial tests for evidence-bound object, Graph, lifecycle, and witness paths. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator +from referencing import Registry, Resource + +from verifier.core.certificate import ( + ClaimBinding, + ClaimCoordinate, + ResourceBounds, +) +from verifier.core.depth import ( + build_evidence_bound_vstd4_receipt, + establish_vstd4, + recheck_evidence_bound_vstd4_receipt, + require_vstd5_entry, +) +from verifier.core.evidence import ( + BoundProposition, + EvidenceBindingError, + EvidenceBounds, + EvidenceStore, + MechanismDecision, + MechanismOutcome, + VerificationSession, +) +from verifier.core.kernel import reference_descriptor +from verifier.core.witness import ( + CorroborationOutcome, + CorroborationRecord, + IndependenceAssertion, + IndependenceDimension, + RelationshipState, + WitnessBundle, + WitnessIdentity, + WitnessResultStatus, + assess_witness_corroboration, + build_vstd5_receipt, + recheck_vstd5_receipt, +) +from verifier.data.assurance import ( + AssuranceEventKind, + AssuranceFlowError, + AssuranceLedger, + ChallengeProjectionMechanism, + DiagnosticKind, + recheck_assurance_log, +) +from verifier.data.graph_level import ( + GraphCollection, + build_evidence_bound_graph_level_record, + establish_graph_level, + graph_collection_binding_digest, + graph_level, + recheck_evidence_bound_graph_level_record, +) +from verifier.data.models import ( + ArtifactNode, + ArtifactStatus, + ArtifactType, + ConflictRecord, + HyperedgePort, + ProvenanceHypergraph, + TransformationHyperedge, + TransformationType, +) +from verifier.layer4.challenge import ( + Adjudication, + Challenge, + ChallengeLedger, + ChallengeOutcome, +) +from verifier.layer4.surface import RefutationType, surface_from_types + + +class ExactFactMechanism: + """Test mechanism: compare exact JSON fact bytes with the bound proposition.""" + + mechanism_id = "test.exact-json-fact" + mechanism_digest = "sha256:" + hashlib.sha256( + b"tests.ExactFactMechanism:v1" + ).hexdigest() + + def evaluate(self, binding, evidence): + if len(evidence) != 1: + return MechanismDecision(MechanismOutcome.UNKNOWN, "one fact required") + try: + observed = json.loads(evidence[0]) + except (UnicodeDecodeError, json.JSONDecodeError): + return MechanismDecision(MechanismOutcome.FAIL, "fact is not JSON") + expected = { + "subject_id": binding.subject_id, + "predicate": binding.predicate, + "expected": binding.expected, + } + outcome = MechanismOutcome.PASS if observed == expected else MechanismOutcome.FAIL + return MechanismDecision(outcome, f"exact fact comparison: {outcome.value}") + + +def _session() -> tuple[EvidenceStore, VerificationSession]: + store = EvidenceStore() + session = VerificationSession(store) + session.register(ExactFactMechanism()) + return store, session + + +def _proposition( + store: EvidenceStore, + subject: str, + predicate: str, + expected, + *, + parameters=None, +) -> BoundProposition: + payload = json.dumps( + {"subject_id": subject, "predicate": predicate, "expected": expected}, + sort_keys=True, + separators=(",", ":"), + ).encode() + reference = store.add(payload) + return BoundProposition( + subject, + predicate, + expected, + ExactFactMechanism.mechanism_id, + ExactFactMechanism.mechanism_digest, + (reference,), + ("test:exact-fact-policy",), + EvidenceBounds(1, 20_000), + parameters or {}, + ) + + +def _binding() -> ClaimBinding: + return ClaimBinding( + "fixture claim", + ClaimCoordinate("claim:fixture", "fixture", {"scope": "test"}), + "a" * 64, + "b" * 64, + reference_descriptor(), + ResourceBounds(20_000, 20_000, 200_000), + ) + + +def _established_vstd4(store, session, *, binding=None): + binding = binding or _binding() + parameters = {"claim_binding_digest": binding.digest()} + prerequisites = { + profile: _proposition( + store, + "claim:fixture", + f"vstd.object_profile.{profile}", + True, + parameters=parameters, + ) + for profile in (1, 2, 3) + } + rungs = { + f"4.{index}": _proposition( + store, + "claim:fixture", + f"vstd4.rung.4.{index}", + True, + parameters=parameters, + ) + for index in range(1, 15) + } + return establish_vstd4( + rungs, + prerequisite_evidence=prerequisites, + session=session, + claim_id="claim:fixture", + binding=binding, + ) + + +def _graph() -> ProvenanceHypergraph: + graph = ProvenanceHypergraph() + for artifact_id in ("source", "middle", "result"): + graph.add_artifact( + ArtifactNode( + artifact_id, + artifact_id, + ArtifactType.MODEL, + hashlib.sha256(artifact_id.encode()).hexdigest(), + status=ArtifactStatus.VALID, + ) + ) + graph.add_transformation( + TransformationHyperedge( + "first", + "first", + TransformationType.EXTRACTION, + (HyperedgePort("source", "INPUT"),), + (HyperedgePort("middle", "OUTPUT"),), + {}, + {}, + {}, + ) + ) + graph.add_transformation( + TransformationHyperedge( + "second", + "second", + TransformationType.EVALUATION, + (HyperedgePort("middle", "INPUT"),), + (HyperedgePort("result", "OUTPUT"),), + {}, + {}, + {}, + ) + ) + return graph + + +def _graph_rating_evidence(graph, store, binding, *, members=("result",), rating=5): + collection_id = "collection:fixture" + parameters = { + "collection_id": collection_id, + "collection_binding_digest": graph_collection_binding_digest( + graph, + collection_id=collection_id, + members=members, + binding=binding, + ), + } + objects = { + subject: _proposition( + store, + subject, + "vstd.object_profile", + rating, + parameters=parameters, + ) + for subject in graph.artifacts + } + edges = { + subject: _proposition( + store, + subject, + "vstd.graph_edge_profile", + rating, + parameters=parameters, + ) + for subject in graph.transformations + } + return objects, edges + + +def test_serialized_pass_is_not_an_input_to_evidence_evaluation() -> None: + store, session = _session() + proposition = _proposition(store, "a", "p", True) + assert not hasattr(proposition, "outcome") + assert session.evaluate(proposition).outcome is MechanismOutcome.PASS + + missing_mechanism = BoundProposition( + "a", + "p", + True, + "not.registered", + "a" * 64, + proposition.evidence_refs, + ("test",), + EvidenceBounds(1, 1000), + ) + assert session.evaluate(missing_mechanism).outcome is MechanismOutcome.UNKNOWN + + +def test_duplicate_evidence_never_multiplies_support() -> None: + store, _session_value = _session() + reference = store.add(b"one") + with pytest.raises(EvidenceBindingError, match="duplicate evidence"): + BoundProposition( + "a", + "p", + True, + ExactFactMechanism.mechanism_id, + ExactFactMechanism.mechanism_digest, + (reference, reference), + ("test",), + EvidenceBounds(2, 100), + ) + + +def test_vstd4_can_be_established_only_by_rerunning_every_exact_binding() -> None: + store, session = _session() + result = _established_vstd4(store, session) + assert result.depth == 14 + assert result.conformance_status == "ESTABLISHED" + assert result.admits_vstd5 is True + assert require_vstd5_entry(result) is result + + +def test_neighboring_rung_evidence_cannot_establish_vstd4() -> None: + store, session = _session() + binding = _binding() + parameters = {"claim_binding_digest": binding.digest()} + prerequisites = { + profile: _proposition( + store, "claim:fixture", f"vstd.object_profile.{profile}", True, + parameters=parameters, + ) + for profile in (1, 2, 3) + } + rungs = { + f"4.{index}": _proposition( + store, + "claim:fixture", + f"vstd4.rung.4.{index}", + True, + parameters=parameters, + ) + for index in range(1, 15) + } + rungs["4.7"] = _proposition( + store, + "claim:neighbor", + "vstd4.rung.4.7", + True, + parameters=parameters, + ) + result = establish_vstd4( + rungs, + prerequisite_evidence=prerequisites, + session=session, + claim_id="claim:fixture", + binding=binding, + ) + assert result.depth == 6 + assert result.conformance_status == "NOT_ESTABLISHED" + assert any("rung 4.7 targets" in error for error in result.binding_errors) + + +def test_evidence_bound_vstd4_receipt_replays_offline_and_matches_schema() -> None: + store, session = _session() + binding = _binding() + parameters = {"claim_binding_digest": binding.digest()} + prerequisites = { + profile: _proposition( + store, "claim:fixture", f"vstd.object_profile.{profile}", True, + parameters=parameters, + ) + for profile in (1, 2, 3) + } + rungs = { + f"4.{index}": _proposition( + store, + "claim:fixture", + f"vstd4.rung.4.{index}", + True, + parameters=parameters, + ) + for index in range(1, 15) + } + result = establish_vstd4( + rungs, + prerequisite_evidence=prerequisites, + session=session, + claim_id="claim:fixture", + binding=binding, + ) + receipt = build_evidence_bound_vstd4_receipt( + result, + receipt_id="VFY-4-EVIDENCE-TEST", + claim_id="claim:fixture", + binding=binding, + prerequisite_evidence=prerequisites, + rung_evidence=rungs, + session=session, + ) + + root = Path(__file__).resolve().parents[1] + schema = json.loads((root / "receipts/schema/vstd4_receipt.json").read_text()) + certificate_schema = json.loads( + (root / "receipts/schema/vstd4_certificate.json").read_text() + ) + registry = Registry().with_resource( + certificate_schema["$id"], Resource.from_contents(certificate_schema) + ) + Draft202012Validator(schema, registry=registry).validate(receipt) + rechecked = recheck_evidence_bound_vstd4_receipt( + receipt, mechanisms=(ExactFactMechanism(),) + ) + assert rechecked.conformance_status == "ESTABLISHED" + + reference = next(iter(receipt["evidence_payloads"])) + receipt["evidence_payloads"][reference] = "bm90LXRoZS1ldmlkZW5jZQ==" + with pytest.raises(EvidenceBindingError, match="does not match"): + recheck_evidence_bound_vstd4_receipt( + receipt, mechanisms=(ExactFactMechanism(),) + ) + + +def test_graph_profile_can_be_established_from_mechanism_evaluated_ratings() -> None: + graph = _graph() + store, session = _session() + binding = _binding() + collection_parameters = { + "collection_id": "collection:fixture", + "collection_binding_digest": graph_collection_binding_digest( + graph, + collection_id="collection:fixture", + members=("result",), + binding=binding, + ), + } + objects = { + subject: _proposition( + store, + subject, + "vstd.object_profile", + 5, + parameters=collection_parameters, + ) + for subject in graph.artifacts + } + edges = { + subject: _proposition( + store, + subject, + "vstd.graph_edge_profile", + 5, + parameters=collection_parameters, + ) + for subject in graph.transformations + } + result = establish_graph_level( + graph, + collection_id="collection:fixture", + members=("result",), + object_evidence=objects, + edge_evidence=edges, + session=session, + binding=binding, + ) + assert result.level == 5 + assert result.rating_basis == "MECHANISM_EVALUATED" + assert result.conformance_status == "ESTABLISHED" + + caller_only = graph_level( + graph, + GraphCollection( + "collection:fixture", ("result",), + {item: 5 for item in graph.artifacts}, + {item: 5 for item in graph.transformations}, + ), + binding=_binding(), + ) + assert caller_only.conformance_status == "NOT_ESTABLISHED" + + record = build_evidence_bound_graph_level_record( + result, + graph=graph, + members=("result",), + binding=binding, + object_evidence=objects, + edge_evidence=edges, + session=session, + ) + root = Path(__file__).resolve().parents[1] + graph_schema = json.loads( + (root / "receipts/schema/vstd_graph_receipt.json").read_text() + ) + vstd4_schema = json.loads((root / "receipts/schema/vstd4_receipt.json").read_text()) + assurance_schema = json.loads( + (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() + ) + registry = Registry() + registry = registry.with_resource(vstd4_schema["$id"], Resource.from_contents(vstd4_schema)) + registry = registry.with_resource( + assurance_schema["$id"], Resource.from_contents(assurance_schema) + ) + Draft202012Validator( + graph_schema["properties"]["computed_graph_level"], registry=registry + ).validate(record) + invalid_zero = copy.deepcopy(record) + invalid_zero["level"] = 0 + assert list( + Draft202012Validator( + graph_schema["properties"]["computed_graph_level"], registry=registry + ).iter_errors(invalid_zero) + ) + rechecked = recheck_evidence_bound_graph_level_record( + graph, record, mechanisms=(ExactFactMechanism(),) + ) + assert rechecked.conformance_status == "ESTABLISHED" + + +def test_graph_ratings_are_bound_to_exact_collection_and_integer_type() -> None: + graph = _graph() + store, session = _session() + binding = _binding() + objects, edges = _graph_rating_evidence(graph, store, binding) + + neighboring = dict(objects) + neighboring["source"] = _proposition( + store, + "source", + "vstd.object_profile", + 5, + parameters={ + "collection_id": "collection:fixture", + "collection_binding_digest": "0" * 64, + }, + ) + result = establish_graph_level( + graph, + collection_id="collection:fixture", + members=("result",), + object_evidence=neighboring, + edge_evidence=edges, + session=session, + binding=binding, + ) + assert result.level == 0 + assert result.conformance_status == "NOT_ESTABLISHED" + assert any("not exactly collection-bound" in item for item in result.binding_errors) + + boolean_rating = dict(objects) + boolean_rating["source"] = _proposition( + store, + "source", + "vstd.object_profile", + True, + parameters=objects["source"].parameters, + ) + result = establish_graph_level( + graph, + collection_id="collection:fixture", + members=("result",), + object_evidence=boolean_rating, + edge_evidence=edges, + session=session, + binding=binding, + ) + assert result.level == 0 + assert result.conformance_status == "NOT_ESTABLISHED" + assert any("is not an integer" in item for item in result.binding_errors) + + +def test_profile_zero_never_becomes_established_conformance() -> None: + graph = _graph() + graph.artifacts["source"] = ArtifactNode( + "source", + "source", + ArtifactType.MODEL, + hashlib.sha256(b"source").hexdigest(), + status=ArtifactStatus.CHALLENGED, + ) + store, session = _session() + binding = _binding() + objects, edges = _graph_rating_evidence(graph, store, binding) + result = establish_graph_level( + graph, + collection_id="collection:fixture", + members=("result",), + object_evidence=objects, + edge_evidence=edges, + session=session, + binding=binding, + ) + assert result.level == 0 + assert result.conformance_status == "NOT_ESTABLISHED" + + +def test_challenge_projection_changes_current_admissibility_not_history() -> None: + graph = _graph() + ledger = AssuranceLedger(graph) + challenges = ChallengeLedger() + surface = surface_from_types( + ClaimCoordinate("source", "digest"), + (RefutationType.EVIDENCE_HASH_MISMATCH,), + overturning_evidence="a mismatching digest", + ) + challenges.file( + Challenge( + "challenge:1", + "source", + "certificate:1", + "digest", + RefutationType.EVIDENCE_HASH_MISMATCH, + "sha256:mismatch", + "2026-08-29T00:00:00Z", + ), + surface, + ) + events = ledger.project_challenges(challenges, recorded_at="2026-08-29T00:01:00Z") + assert events[0].kind is AssuranceEventKind.STATUS_PROJECTION + assert ledger.current_status("source") is ArtifactStatus.CHALLENGED + assert graph.artifacts["source"].status is ArtifactStatus.VALID + assert ledger.materialize_current_graph().artifacts["source"].status is ArtifactStatus.CHALLENGED + + +def test_upstream_challenge_invalidates_current_trust_and_graph_admission() -> None: + graph = _graph() + ledger = AssuranceLedger(graph) + store, session = _session() + trust = _proposition( + store, + "result", + "vstd.graph.support", + {"sources": ["source"], "target": "result"}, + ) + ledger.record_trust( + "result", + ("source",), + trust, + session=session, + recorded_at="2026-08-29T00:00:00Z", + ) + assert len(ledger.current_trust_events()) == 1 + assert ledger.impacted_descendants("source") == ("middle", "result") + + challenges = ChallengeLedger() + surface = surface_from_types( + ClaimCoordinate("source", "digest"), + (RefutationType.EVIDENCE_HASH_MISMATCH,), + overturning_evidence="a mismatching digest", + ) + challenges.file( + Challenge( + "challenge:impact", + "source", + "certificate:1", + "digest", + RefutationType.EVIDENCE_HASH_MISMATCH, + "sha256:mismatch", + "2026-08-29T00:01:00Z", + ), + surface, + ) + ledger.project_challenges(challenges, recorded_at="2026-08-29T00:02:00Z") + assert ledger.current_trust_events() == () + current = ledger.materialize_current_graph() + candidate = graph_level( + current, + GraphCollection( + "collection:fixture", + ("result",), + {item: 5 for item in current.artifacts}, + {item: 5 for item in current.transformations}, + ), + binding=_binding(), + ) + assert candidate.level == 0 + + +def test_challenge_recovery_does_not_undo_independent_rot() -> None: + graph = _graph() + ledger = AssuranceLedger(graph) + store, session = _session() + rot = _proposition( + store, + "source", + "vstd.graph.current_status", + ArtifactStatus.STALE.value, + ) + ledger.record_rot( + "source", + ArtifactStatus.STALE, + rot, + session=session, + recorded_at="2026-08-29T00:00:00Z", + ) + challenges = ChallengeLedger() + surface = surface_from_types( + ClaimCoordinate("source", "digest"), + (RefutationType.EVIDENCE_HASH_MISMATCH,), + overturning_evidence="a mismatching digest", + ) + challenges.file( + Challenge( + "challenge:recovered", + "source", + "certificate:1", + "digest", + RefutationType.EVIDENCE_HASH_MISMATCH, + "sha256:mismatch", + "2026-08-29T00:01:00Z", + ), + surface, + ) + challenges.adjudicate( + Adjudication( + "challenge:recovered", + ChallengeOutcome.REJECTED, + "counterevidence disproven", + "2026-08-29T00:02:00Z", + ) + ) + ledger.project_challenges(challenges, recorded_at="2026-08-29T00:03:00Z") + assert ledger.current_status("source") is ArtifactStatus.STALE + with pytest.raises(AssuranceFlowError, match="strictly degrade"): + ledger.record_rot( + "source", + ArtifactStatus.STALE, + rot, + session=session, + recorded_at="2026-08-29T00:04:00Z", + ) + + +def test_conflict_resolution_is_additive_and_mechanism_bound() -> None: + graph = _graph() + graph.add_conflict( + ConflictRecord( + "conflict:digest", + "source", + "content_digest", + ("sha256:a", "sha256:b"), + ("receipt:a", "receipt:b"), + ) + ) + ledger = AssuranceLedger(graph) + store, session = _session() + resolution = _proposition( + store, + "source", + "vstd.graph.resolve.content_digest", + "sha256:a", + parameters={"conflict_id": "conflict:digest"}, + ) + ledger.resolve_conflict( + "conflict:digest", + "sha256:a", + resolution, + session=session, + recorded_at="2026-08-29T00:00:00Z", + ) + assert "conflict:digest" in graph.conflicts + assert ledger.unresolved_conflicts() == () + assert ledger.materialize_current_graph().conflicts == {} + + +def test_trust_rot_and_rust_follow_direction_without_recursive_amplification() -> None: + graph = _graph() + ledger = AssuranceLedger(graph) + store, session = _session() + + trust = _proposition( + store, + "result", + "vstd.graph.support", + {"sources": ["source"], "target": "result"}, + ) + first = ledger.record_trust( + "result", ("source", "source"), trust, + session=session, recorded_at="2026-08-29T00:00:00Z", + ) + second = ledger.record_trust( + "result", ("source",), trust, + session=session, recorded_at="2026-08-29T00:00:00Z", + ) + assert first.digest() == second.digest() + assert len(ledger.events()) == 1 + + rust = _proposition(store, "result", "vstd.graph.descendant_deviation", True) + ledger.record_rust( + "result", rust, session=session, recorded_at="2026-08-29T00:01:00Z" + ) + ledger.record_rust( + "result", rust, session=session, recorded_at="2026-08-29T00:01:00Z" + ) + concentration = {item.ancestor_id: item for item in ledger.rust_concentration()} + assert concentration["source"].count == 1 + + rot = _proposition( + store, "source", "vstd.graph.current_status", ArtifactStatus.REVOKED.value + ) + ledger.record_rot( + "source", ArtifactStatus.REVOKED, rot, + session=session, recorded_at="2026-08-29T00:02:00Z", + ) + assert ledger.current_status("source") is ArtifactStatus.REVOKED + with pytest.raises(AssuranceFlowError, match="inadmissible target or ancestry"): + ledger.record_trust( + "result", ("source",), trust, + session=session, recorded_at="2026-08-29T00:03:00Z", + ) + assert ledger.verify_hash_chain() is True + + +def test_rust_requires_separate_localization_before_blame_or_guilt() -> None: + graph = _graph() + ledger = AssuranceLedger(graph) + store, session = _session() + rust = _proposition(store, "result", "vstd.graph.descendant_deviation", True) + ledger.record_rust( + "result", rust, session=session, recorded_at="2026-08-29T00:00:00Z" + ) + refused = ledger.diagnose( + DiagnosticKind.BLAME, + "source", + "result", + None, + session=session, + recorded_at="2026-08-29T00:01:00Z", + ) + assert refused.status == "NOT_ESTABLISHED" + + localization = _proposition( + store, + "result", + "vstd.graph.causal_localization", + {"ancestor": "source", "descendant": "result"}, + ) + event = ledger.localize_cause( + "source", "result", localization, + session=session, recorded_at="2026-08-29T00:02:00Z", + ) + attribution = _proposition( + store, + "source", + "vstd.graph.diagnostic.blame", + { + "ancestor": "source", + "descendant": "result", + "localization_event_digest": event.digest(), + }, + ) + result = ledger.diagnose( + DiagnosticKind.BLAME, + "source", + "result", + attribution, + session=session, + recorded_at="2026-08-29T00:03:00Z", + ) + assert result.status == "ESTABLISHED" + assert "actor" not in result.details.lower() + + guilt = _proposition( + store, + "source", + "vstd.graph.diagnostic.guilt", + { + "ancestor": "source", + "descendant": "result", + "localization_event_digest": event.digest(), + }, + ) + with pytest.raises(AssuranceFlowError, match="violated obligation"): + ledger.diagnose( + DiagnosticKind.GUILT, + "source", + "result", + guilt, + session=session, + recorded_at="2026-08-29T00:04:00Z", + ) + + +def test_assurance_event_log_is_portable_strict_and_evidence_complete() -> None: + graph = _graph() + ledger = AssuranceLedger(graph) + store, session = _session() + deviation = _proposition(store, "result", "vstd.graph.descendant_deviation", True) + ledger.record_rust( + "result", + deviation, + session=session, + recorded_at="2026-08-29T00:00:00Z", + ) + payload = ledger.to_dict() + assert payload["historical_graph"] == graph.to_dict() + assert set(payload["events"][0]["evidence_payloads"]) == set( + payload["events"][0]["evidence_refs"] + ) + + root = Path(__file__).resolve().parents[1] + schema = json.loads( + (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() + ) + graph_schema = json.loads( + (root / "receipts/schema/vstd_graph_receipt.json").read_text() + ) + registry = Registry().with_resource( + graph_schema["$id"], Resource.from_contents(graph_schema) + ) + Draft202012Validator(schema, registry=registry).validate(payload) + + restored = EvidenceStore() + restored.import_base64(payload["events"][0]["evidence_payloads"]) + assert deviation.evidence_refs[0] in restored + + rechecked = recheck_assurance_log( + payload, + mechanisms=(ExactFactMechanism(),), + ) + assert rechecked.to_dict() == payload + + tampered = copy.deepcopy(payload) + tampered["events"][0]["details"] = "caller-rewritten outcome" + with pytest.raises(AssuranceFlowError, match="does not match"): + recheck_assurance_log(tampered, mechanisms=(ExactFactMechanism(),)) + + +def test_assurance_replay_recomputes_challenge_projection() -> None: + graph = _graph() + ledger = AssuranceLedger(graph) + challenges = ChallengeLedger() + surface = surface_from_types( + ClaimCoordinate("source", "digest"), + (RefutationType.EVIDENCE_HASH_MISMATCH,), + overturning_evidence="a mismatching digest", + ) + challenges.file( + Challenge( + "challenge:replay", + "source", + "certificate:1", + "digest", + RefutationType.EVIDENCE_HASH_MISMATCH, + "sha256:mismatch", + "2026-08-29T00:00:00Z", + ), + surface, + ) + ledger.project_challenges(challenges, recorded_at="2026-08-29T00:01:00Z") + replayed = recheck_assurance_log(ledger.to_dict(), mechanisms=()) + assert replayed.current_status("source") is ArtifactStatus.CHALLENGED + + with pytest.raises(AssuranceFlowError, match="cannot be replaced"): + recheck_assurance_log( + ledger.to_dict(), mechanisms=(ChallengeProjectionMechanism(),) + ) + + +def test_assurance_ledger_refuses_recursive_cycle() -> None: + graph = _graph() + graph.add_transformation( + TransformationHyperedge( + "cycle", + "cycle", + TransformationType.EVALUATION, + (HyperedgePort("result", "INPUT"),), + (HyperedgePort("source", "OUTPUT"),), + {}, + {}, + {}, + ) + ) + with pytest.raises(AssuranceFlowError, match="cyclic provenance"): + AssuranceLedger(graph) + + +def test_vstd5_requires_every_independence_seam_and_preserves_disagreement() -> None: + store, session = _session() + entry = _established_vstd4(store, session) + binding_digest = entry.witness.header.binding # type: ignore[union-attr] + witness = WitnessIdentity("witness:one", store.add(b"identity coordinate")) + relation = "declarant:one->witness:one" + relationships = { + dimension: RelationshipState.SEPARATE for dimension in IndependenceDimension + } + independence = IndependenceAssertion( + witness.witness_id, + relationships, + { + dimension: _proposition( + store, + relation, + f"vstd5.shared.{dimension.value}", + False, + parameters={"claim_binding_digest": binding_digest}, + ) + for dimension in IndependenceDimension + }, + ) + + def corroboration(record_id, outcome, observation): + certificate_digest = entry.witness.digest() # type: ignore[union-attr] + expected = { + "claim_binding_digest": binding_digest, + "vstd4_certificate_digest": certificate_digest, + "checker_descriptor_digest": "b" * 64, + "result": outcome.value, + } + verification = _proposition( + store, + "claim:fixture", + "vstd5.corroboration", + expected, + parameters={ + "witness_id": witness.witness_id, + "observed_at": "2026-08-29T00:00:00Z", + }, + ) + # The observation is the exact fact the mechanism reruns. + return CorroborationRecord( + record_id, + witness.witness_id, + binding_digest, + certificate_digest, + "b" * 64, + verification.evidence_refs, + outcome, + "2026-08-29T00:00:00Z", + verification, + observation, + ) + + yes = corroboration("corroboration:yes", CorroborationOutcome.CORROBORATED, "TEST") + no = corroboration("corroboration:no", CorroborationOutcome.REFUTED, "TEST") + bundle = WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (independence,), + (yes, no), + ) + result = assess_witness_corroboration(entry, bundle, session=session) + assert result.conformance_status == "ESTABLISHED" + assert result.computed_independence == "INDEPENDENT" + assert result.status is WitnessResultStatus.CONFLICTED + assert result.disagreements == (("corroboration:no", "corroboration:yes"),) + + receipt = build_vstd5_receipt( + entry, + bundle, + result, + receipt_id="VFY-5-EVIDENCE-TEST", + session=session, + ) + root = Path(__file__).resolve().parents[1] + schema = json.loads((root / "receipts/schema/vstd5_receipt.json").read_text()) + vstd4_schema = json.loads((root / "receipts/schema/vstd4_receipt.json").read_text()) + assurance_schema = json.loads( + (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() + ) + registry = Registry() + registry = registry.with_resource(vstd4_schema["$id"], Resource.from_contents(vstd4_schema)) + registry = registry.with_resource( + assurance_schema["$id"], Resource.from_contents(assurance_schema) + ) + Draft202012Validator(schema, registry=registry).validate(receipt) + false_positive = copy.deepcopy(receipt) + false_positive["result"]["status"] = "CORROBORATED" + false_positive["result"]["conformance_status"] = "NOT_ESTABLISHED" + false_positive["result"]["computed_independence"] = "UNKNOWN" + assert list( + Draft202012Validator(schema, registry=registry).iter_errors(false_positive) + ) + rechecked = recheck_vstd5_receipt( + entry, receipt, mechanisms=(ExactFactMechanism(),) + ) + assert rechecked.status is WitnessResultStatus.CONFLICTED + + uncertain = IndependenceAssertion( + witness.witness_id, + {**relationships, IndependenceDimension.CONTROL: RelationshipState.UNKNOWN}, + independence.evidence, + ) + result = assess_witness_corroboration( + entry, + WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (uncertain,), + (yes,), + ), + session=session, + ) + assert result.conformance_status == "NOT_ESTABLISHED" + assert result.status is WitnessResultStatus.UNKNOWN + assert result.computed_independence == "UNKNOWN" diff --git a/tests/test_packaged_specifications.py b/tests/test_packaged_specifications.py index 7089342..2765048 100644 --- a/tests/test_packaged_specifications.py +++ b/tests/test_packaged_specifications.py @@ -30,8 +30,9 @@ def test_ladder_fixes_causal_provenance_directions_without_actor_trust() -> None assert "Memetic propagation" in ladder assert "RFC 2119" in ladder assert "RFC 8174" in ladder - assert "not serialized receipt values or\ncomputable conformance results" in ladder - assert "current VSTD runtime emits or validates a TRUST, ROT, or\nRUST transfer" in ladder + assert "serialize as typed event kinds only in the non-receipt\n`VSTD-GRAPH-ASSURANCE-1` mechanism log" in ladder + assert "`AssuranceLedger` implements mechanism-earned forward TRUST" in ladder + assert "`recheck_assurance_log` reconstructs the historical Graph" in ladder assert "MUST NOT strengthen an artifact-bound\nresult" in ladder assert "TRUST and RUST never cancel" in ladder assert "whether an actor is good, bad, reputable, or worthy of trust" in ladder diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 7d04dba..dd3b15f 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -11,17 +11,39 @@ ROOT = Path(__file__).resolve().parents[1] EXPECTED_EXPORTS = { + "AssuranceLedger", "ArtifactControlError", "ArtifactVerification", + "BoundProposition", + "EvidenceBindingError", "DecisionCertificate", + "EvidenceBounds", + "EvidenceStore", + "MechanismDecision", + "MechanismOutcome", + "ProvenanceHypergraph", "ReproducibilityLevel", + "VerificationSession", "VerificationGeometry", "VerificationVerdict", "VstdReceipt", + "WitnessBundle", + "assess_witness_corroboration", + "build_evidence_bound_graph_level_record", + "build_evidence_bound_vstd4_receipt", + "build_vstd5_receipt", "capture_run", "certificate_from_canonical_bytes", "compute_canonical_digest", + "claim_binding_from_dict", + "establish_graph_level", + "establish_vstd4", "freeze_artifact", + "graph_collection_binding_digest", + "recheck_assurance_log", + "recheck_evidence_bound_graph_level_record", + "recheck_evidence_bound_vstd4_receipt", + "recheck_vstd5_receipt", "require_vstd5_entry", "seal_artifact", "thaw_artifact", diff --git a/tests/test_vstd_schemas.py b/tests/test_vstd_schemas.py index 4ef9117..9bf403f 100644 --- a/tests/test_vstd_schemas.py +++ b/tests/test_vstd_schemas.py @@ -195,54 +195,20 @@ def test_vstd4_candidate_receipt_is_explicit_and_keeps_legacy_shape_valid() -> N assert any("not of type 'object'" in error.message for error in errors) -def test_vstd5_draft_schema_records_shape_without_establishing_entry() -> None: +def test_vstd5_schema_requires_replayable_evidence_bound_inputs() -> None: schema = _load("vstd5_receipt.json") - validator = Draft202012Validator(schema, format_checker=FormatChecker()) - assert "current VSTD-4 candidate cannot satisfy" in schema["description"] - receipt = { + assert schema["properties"]["schema_version"]["const"] == "VSTD-5" + assert "must recheck" in schema["description"] + legacy_draft = { "schema_version": "VSTD-5-DRAFT", "status": "DRAFT", "receipt_id": "VFY-5-SCHEMA-TEST", - "claim_id": "claim:schema-test", - "claim_binding": HEX, - "entry_vstd4_depth": 14, - "witnesses": [ - { - "witness_id": "witness:test", - "identity_evidence": "sha256:" + HEX, - "independence": { - "shared_control": "UNKNOWN", - "shared_code": "UNKNOWN", - "shared_trust_root": "UNKNOWN", - "shared_evidence_source": "UNKNOWN", - "shared_infrastructure": "UNKNOWN", - "financial_dependence": "UNKNOWN", - "jurisdictional_dependence": "UNKNOWN", - "evidence": [], - }, - } - ], - "corroborations": [ - { - "corroboration_id": "corroboration:test", - "witness_id": "witness:test", - "class": "PHYSICAL_INSPECTION", - "vstd4_certificate_digest": HEX, - "checker_descriptor_digest": HEX, - "observed_evidence": [HEX], - "result": "UNKNOWN", - "observed_at": "2026-08-22T12:00:00Z", - } - ], - "disagreements": [], - "computed_independence": "UNKNOWN", } - validator.validate(receipt) + assert list(Draft202012Validator(schema).iter_errors(legacy_draft)) - receipt["entry_vstd4_depth"] = 13 - errors = list(validator.iter_errors(receipt)) - assert errors - assert any("14 was expected" in error.message for error in errors) + dimension = schema["$defs"]["dimension"] + assert dimension["required"] == ["state", "binding"] + assert {"type": "null"} in dimension["properties"]["binding"]["anyOf"] def test_current_wire_identifiers_and_profile_discriminators() -> None: From ca2b9748b639d12cbdbd960d9acec2054a6c9572 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 06:20:47 -0700 Subject: [PATCH 15/34] docs: present executable assurance architecture Reason: Align every public control surface with the implemented evidence-bound VSTD-4, VSTD-5, VSTD-Graph, TRUST, ROT, RUST, BLAME, and GUILT mechanisms so the release candidate no longer describes shipped reference paths as deferred research. Evidence: README, architecture, claims, governance, human and agent guides, roadmap, change history, SCITT boundaries, Pages, generated API reference, overview assets, flagship specimens, and the artifact-first experiment index now name the exact implemented mechanism and preserve its external-evidence limits. Presentation checks enforce the resulting maturity and terminology contract. Coordinate: Parent 70706ec on codex/post-1.2-professionalization for PR #27. Falsification: A public page that calls the reference mechanism absent, treats candidate ratings as established, converts identity into trust, presents diagnostic RUST as causal blame, or claims external witnesses/interoperability must fail the presentation or semantic review boundary. Compatibility/wire impact: Documentation and generated presentation are synchronized with the additive runtime/schema work in the parent commit. Historical receipts remain readable; no released frozen wire identifier is redefined. Regenerated flagship specimens truthfully carry the current package standard-status coordinate. --- AGENTS.md | 25 ++- CHANGELOG.md | 43 +++- GOVERNANCE.md | 9 +- HUMANS.md | 9 + README.md | 46 ++-- ROADMAP.md | 28 ++- docs/API_STABILITY.md | 13 ++ docs/ARCHITECTURE.md | 80 +++---- docs/CLAIMS_AND_LIMITS.md | 31 ++- docs/assets/vstd-overview.png | Bin 89535 -> 148262 bytes docs/assets/vstd-overview.svg | 8 +- docs/index.html | 6 +- docs/reference.html | 208 +++++++++++++++++- docs/standards/SCITT_SEMANTIC_BOUNDARY.md | 10 +- docs/standards/VSTD_SCITT_CROSSWALK.md | 2 +- .../specimens/honest-unknown.json | 4 +- .../specimens/inflated-tier.json | 4 +- .../specimens/poisoned-ancestor.json | 6 +- .../specimens/wrong-artifact.json | 4 +- experiments/INDEX.md | 2 +- .../artifact_first_mechanisms/experiment.json | 30 +-- scripts/check_presentation.py | 30 +-- tests/test_presentation_surface.py | 9 +- 23 files changed, 460 insertions(+), 147 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 231bf72..8f7111e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,9 +21,12 @@ and `vstd` is the canonical command. Two independent axes: `VSTD-1..5` (object mechanics) and `VSTD-Graph-1..5` (collection dynamics). These are cumulative **numbered profiles** over named **closure coordinates**, not interchangeable layers or scalar assurance levels. Implementation status is -profile-specific: the current VSTD-4 candidate-depth and Graph 2-4 candidate-profile +profile-specific: the compatibility VSTD-4 candidate-depth and Graph candidate-profile mechanisms compute over caller-supplied references or ratings with conformance -`NOT_ESTABLISHED`; **profile 5 is DRAFT**. Object profile depth `N` holds only when +`NOT_ESTABLISHED`. Separate evidence-bound paths rerun exact registered mechanisms and +may establish VSTD-4, VSTD-5, or Graph conformance under their named evidence, trust +roots, bounds, and exact collection or claim binding. Evidence-bound Graph profile zero +remains `NOT_ESTABLISHED`. Object profile depth `N` holds only when distinct evidence passes every required coordinate in profiles 1 through `N`. A later-profile result never supplies, implies, upgrades, or repairs a prerequisite coordinate. @@ -83,14 +86,22 @@ reason, not a pass. See [`CONTRIBUTING.md`](CONTRIBUTING.md). ### Artifact-first state vocabulary -`TRUST`, `RUST`, and `ROT` are formal semantic names, not acronyms, serialized receipt values, scalar -scores, or references to the Rust programming language. `TRUST` is mechanism-earned +`TRUST`, `RUST`, and `ROT` are formal semantic names, not acronyms, scalar scores, +numbered-profile verdicts, or references to the Rust programming language. They serialize +only as typed events in `VSTD-GRAPH-ASSURANCE-1`. `TRUST` is mechanism-earned forward artifact support; `RUST` is inverse-direction diagnostic traversal from a descendant deviation toward recorded ancestors; `ROT` is typed, time-indexed degradation of current admissibility without rewriting historical evidence. Define the terms at first use in every independently readable surface and preserve their normative meaning from [`standard/LADDER.md`](standard/LADDER.md). +Assurance events are not self-authenticating status words. Portable reliance requires +`recheck_assurance_log` to reconstruct the historical Graph, rehash embedded evidence, +rerun every registered mechanism, reproduce the event chain, and compare the current view. +When upstream admissibility changes, preserve historical events, exclude no-longer-current +TRUST through `current_trust_events`, and use `impacted_descendants` only as a deduplicated +reassessment surface—not as a verdict on descendants. + Never turn identity, authorship, authorization, popularity, reputation, or actor separation into `TRUST` in computational validity. A mechanism may check an exact proposition about one of those coordinates, but that result remains adjacent to the artifact-bound process @@ -193,10 +204,12 @@ If that path is not inside this repository, prefix commands with `PYTHONPATH=src ## 4. Layout - `standard/` — normative numbered-profile documents plus `WIRE_IDENTIFIERS.md`. -- `src/verifier/core/` — receipt, checker, certificate, grounding, kernel, and the +- `src/verifier/core/` — receipt, checker, certificate, grounding, kernel, evidence-bound + mechanism execution, witness corroboration, and the generic-run capture/facade with planning, validation, inspection, reproduction, and impact modules. -- `src/verifier/constraints/`, `hardware/`, `layer4/`, `data/` — profile-specific runtime surfaces; `layer4/` is a retained module path. +- `src/verifier/constraints/`, `hardware/`, `layer4/`, `data/` — profile-specific runtime + surfaces, including additive Graph assurance propagation; `layer4/` is a retained module path. - `src/verifier/runtime/` — `public_cli.py` (every CLI entry point) and `demo.py`. - `src/verifier/specifications/` — byte-identical copies of normative spec files. - `receipts/schema/` — receipt JSON Schemas. `standard/schemas/` — strict non-receipt diff --git a/CHANGELOG.md b/CHANGELOG.md index 82308b0..7ed7447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,8 +46,8 @@ preserving unresolved horizons and native-system authority. - Formally distinguish TRUST as mechanism-earned forward artifact support, ROT as typed time-indexed degradation of current admissibility, and RUST as an inverse-TRUST memetic - causal backtrace toward recorded ancestor states. None is actor-tied trust, a scalar, - or permission to infer guilt, responsibility, or causal localization from reachability. + causal backtrace toward recorded ancestor states. None is actor-tied trust or a scalar; + reachability alone never infers guilt, responsibility, or causal localization. - Present current reports, schemas, module descriptions, and examples under the full VSTD-1 and VSTD-2 numbered-profile identifiers; remove retired partial-profile object identifiers from active readers and add a regression preventing their return. @@ -82,8 +82,9 @@ and never infer independent actors from a historical field name, repeated runs, or matching results. - Reject self-promoted independence even when every supplied status and digest agrees; - version 1.2.0 has no actor/execution evidence-binding adapter and therefore never - derives `EVIDENCED` from serialized references. + the generic-run compatibility path never derives `EVIDENCED` from serialized references. + The distinct VSTD-5 path reruns all seven separation propositions and does not upgrade + the legacy generic-run fields. - Require the real optional SCITT/COSE cryptographic example in the protected repository-check aggregate rather than allowing its dependency-gated tests to disappear from the base matrix. @@ -93,21 +94,45 @@ ### Graph and conformance semantics +- Add a zero-dependency evidence execution core that resolves and rehashes exact evidence + bytes, pins a registered mechanism implementation digest, enforces byte/item bounds, + reruns the mechanism, and preserves `PASS`, `FAIL`, or `UNKNOWN` under explicit trust roots. +- Add an evidence-bound VSTD-4 path and replayable receipt form. Compatibility + `vstd4_depth` remains a `NOT_ESTABLISHED` candidate; only exact passing VSTD-1/2/3 and + fourteen-rung mechanisms plus an accepted kernel witness admit VSTD-5. +- Implement the VSTD-5 reference mechanism and receipt: seven evidence-bound separation + dimensions, duplicate-witness/evidence refusal, exact admitted-certificate and + corroboration binding, + disagreement preservation, embedded evidence, and offline result recheck. This does not + claim a real external witness or independent implementation; a positive observation with + unresolved independence is overall `UNKNOWN`. +- Add evidence-bound Graph profile computation and replay. The compatibility `graph_level` + path remains caller-supplied; the new path reruns every member, ancestor, and reached-edge + rating mechanism bound to the exact Graph, members, collection, and claim before profile + 1–5 can report `ESTABLISHED`. Profile zero remains `NOT_ESTABLISHED`. +- Add `VSTD-GRAPH-ASSURANCE-1` and `AssuranceLedger` for hash-chained TRUST, ROT, RUST, + challenge-ledger projection, additive conflict resolution, structural RUST concentration, + explicit causal localization, and bounded artifact-relative BLAME/GUILT propositions. + Duplicate paths remain set-valued, historical graph bytes remain immutable, and topology + alone earns no causal or moral conclusion. Add complete offline event replay, current TRUST + filtering, and deduplicated descendant reassessment discovery. + - Preserve incompatible Graph assertions as evidence-linked conflict records and label rating-derived Graph profile numbers as `CALLER_SUPPLIED` candidates with conformance `NOT_ESTABLISHED`. - Classify the current VSTD-4 candidate-depth calculation as a structural result over caller-supplied rung references with conformance `NOT_ESTABLISHED`; reject that candidate at the VSTD-5 entry gate even when its candidate depth is 14. -- Label Graph 2-4 candidates consistently on first-view, documentation, command, schema, - and SCITT surfaces. Keep challenge-ledger state, degradation from status already - recorded in a Graph, and the missing challenge-to-Graph adapter distinct. +- Label compatibility Graph 2–5 candidates consistently while separately presenting the + implemented evidence-bound reference paths. Bind complete challenge-ledger state into an + additive current Graph view without mutating history. ### Release and maintainer controls - Mark 1.2.0 metadata as an unreleased release candidate, omit any fabricated release date, and require the exact tagged checkout to have `TIME.md` set to `Status: CLEAR`. -- Make package/reference status explicitly say VSTD-4 candidate conformance is - `NOT_ESTABLISHED`, and require finalized release metadata in the tag workflow. +- Make package/reference status identify VSTD-5 as the highest exposed project + specification with an evidence-bound reference mechanism, without claiming a real + independent witness, and require finalized release metadata in the tag workflow. - Publish the architecture ownership map linking normative documents, runtime validators, schemas, and conformance tests. - Document the five-As human traversal over existing receipt, Graph, hardware, certificate, diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 923b809..03819b6 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -16,9 +16,12 @@ specification. VSTD-4 ships grounded-certificate/kernel checks and separate availability, precommitment, challenge, degradation, and composition mechanisms. Its candidate-depth runtime computes only a structural candidate over caller-supplied references with conformance -`NOT_ESTABLISHED`; no mechanism binds all rung propositions and VSTD-1/2/3 preconditions -into VSTD-4 conformance. `VSTD4-GDC-1` has no demonstrated independent implementation -or external interoperability. VSTD-5 remains draft and has no shipped witness procedure. +`NOT_ESTABLISHED`. The separate evidence-bound runtime rehashes and reruns exact VSTD-1/2/3 +and fourteen-rung propositions before it may establish VSTD-4 conformance; its receipt +rechecker reproduces that result offline with the supplied mechanism implementations. +`VSTD4-GDC-1` has no demonstrated independent implementation or external interoperability. +VSTD-5 has a shipped evidence-bound reference procedure, but the repository claims no real +external witness or second implementation. ## Numbered-profile and release states diff --git a/HUMANS.md b/HUMANS.md index 0c9c89b..71a5660 100644 --- a/HUMANS.md +++ b/HUMANS.md @@ -49,6 +49,15 @@ scores, serialized receipt values, or references to the Rust programming languag | **ROT** | Typed lifecycle or dependency evidence degraded the support's current admissibility. Reassess affected dependents, but preserve the immutable historical receipt and its original result. | | **RUST** | An observed descendant deviation can be traced backward through recorded contributing ancestry. The trace identifies candidates for examination; it does not prove ancestor falsehood, guilt, responsibility, or causal localization. | +The reference `AssuranceLedger` records these as additive Graph events. Treat structural +RUST concentration as a triage count of unique deviating descendants, never causal +strength. A bounded artifact-relative `BLAME` or `GUILT` result exists only after separate +localization and attribution mechanisms pass; it never evaluates an actor's character. +For reliance, replay the portable log with `recheck_assurance_log`: a stored event word or +hash chain without successful evidence rehash and mechanism execution is not current +assurance. When upstream status changes, inspect `current_trust_events` and the deduplicated +`impacted_descendants` reassessment surface rather than deleting historical results. + Zero identity means identity contributes no verdict weight by itself. Zero knowledge means no unevidenced proposition is presumed: absent a mechanism-earned result, keep `UNKNOWN`. When a witness must remain confidential, a cryptographic zero-knowledge proof can enclose diff --git a/README.md b/README.md index cd41652..c506464 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,12 @@ would overturn it. VSTD carries those boundaries with the result. **Current boundary:** implemented reference paths cover receipts, generic computation capture, provenance graphs, verification geometry, accelerator evidence, grounded -certificate checking, reproduction, exact-byte artifact freezing, finite self-closing -seals, copy-on-write thawing, and a flagship adversarial demo. VSTD-4 candidate depth and -Graph profiles 2–5 are candidate computations with conformance `NOT_ESTABLISHED`; -VSTD-5 is not implemented. See [current maturity](#current-maturity) and +certificate checking, evidence-bound VSTD-4/VSTD-5 assessment, evidence-bound Graph +ratings, replayable additive Graph lifecycle/assurance propagation, reproduction, exact-byte artifact +freezing, finite self-closing seals, copy-on-write thawing, and a flagship adversarial demo. +Compatibility candidate paths remain `NOT_ESTABLISHED`; evidence-bound paths rerun exact +registered mechanisms and preserve their evidence, trust roots, bounds, and limitations. +No real external witness or independent implementation is claimed. See [current maturity](#current-maturity) and [claims and limits](docs/CLAIMS_AND_LIMITS.md). [Normative specifications](standard/LADDER.md) · @@ -81,7 +83,7 @@ bound: | `FAIL` | The mechanism found a checked violation, rejected certificate, or counterexample at the named surface. | Every broader interpretation is false. | | `UNKNOWN` | Available evidence, capability, or resources did not establish `PASS` or `FAIL`. | False, safe, unsupported forever, or “probably PASS.†| | `CONFLICTED` | Incompatible evidence or assertions remain explicit. | The conflict was resolved by choosing one side. | -| `NOT_ESTABLISHED` | The repository computes a candidate, but a required evidence-binding or conformance mechanism is absent. | Conformance, readiness, or a weak form of `PASS`. | +| `NOT_ESTABLISHED` | The evaluated path did not establish conformance: it may be a compatibility candidate, or required evidence, exact binding, mechanism availability, mechanism result, prerequisite, or profile floor was missing or non-passing. | Conformance, readiness, or a weak form of `PASS`. | A VSTD `PASS` never means “true in the real world†without the exact real-world proposition and observation boundary being part of the checked claim. @@ -97,18 +99,18 @@ 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 | | 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 | A grounded decision certificate (GDC) parser/kernel plus VSTD-4 candidate-depth computation | The certificate binds formula, grounding, claim, roots, and bounds; rung references and VSTD-1/2/3 preconditions are not evidence-bound by the candidate-depth runtime | `NOT_ESTABLISHED` | Rung-by-rung evidence validation, prerequisite-profile composition, and an independent checker implementation | -| VSTD-5 | Draft | Fail-closed rejection of current VSTD-4 candidates only | No witness-corroboration binding is implemented | Not implemented | Witness protocol, qualifying VSTD-4 input, distinct actors, independence evidence, and operational experience | +| 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 | | VSTD-Graph-1 | Project specification with implemented reference subset | Content-addressed artifacts, transformations, conflicts, policy queries, receipts, and recorded reachability | Binds recorded objects and edges; it does not establish real-world completeness or causality | Implemented reference subset | Independent implementation and external provenance-profile interoperability | -| VSTD-Graph-2 | Project specification | Candidate Bounded Collection Surface profile and ceiling-certificate computation | Uses caller-supplied object and edge ratings; the ratings are not validated against profile-2 evidence | `NOT_ESTABLISHED` | Rating-to-evidence validators for members, ancestors, statuses, and transformation edges | -| VSTD-Graph-3 | Project specification | Candidate Accountable Provenance Closure profile and ceiling-certificate computation | Uses caller-supplied object and edge ratings; no mechanism establishes that VSTD-3 produced them | `NOT_ESTABLISHED` | VSTD-3 rating evidence for every member, reachable ancestor, and transformation edge | -| VSTD-Graph-4 | Project specification | Candidate Refutable Transformation Closure profile and ceiling-certificate computation | Uses caller-supplied object and edge ratings; claimed refutability-closure records are not validated | `NOT_ESTABLISHED` | VSTD-4 rating evidence and validation of every reached refutability closure | -| VSTD-Graph-5 | Draft profile | Candidate Graph profile 5 can be computed from caller-supplied ratings | No independent-witness or rating-evidence binding | `NOT_ESTABLISHED` | Graph-2–4 evidence binding plus a corroborated verification-network protocol | +| VSTD-Graph-2 | Project specification with implemented reference paths | Compatibility candidate plus evidence-bound Bounded Collection Surface computation/recheck | Registered mechanisms rerun exact member, ancestor, and edge ratings bound to the Graph bytes, deduplicated members, collection, and claim | Candidate `NOT_ESTABLISHED`; evidence-bound profile 1–5 path can establish; profile zero cannot | External rating mechanisms, independent implementation, and interoperability | +| 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 | | 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 | -| zero-identity/zero-knowledge (ZIZK) artifact-first TRUST | Governing VSTD architecture in `standard/LADDER.md` section 1.1; not a separate numbered or receipt profile | Artifact-bound process semantics, forward TRUST, time-indexed ROT, and reverse diagnostic RUST | Existing mechanism-specific evidence only; identity or reputation alone, repetition, age, and topology add no assurance | Governing architectural invariant; not a separate VSTD conformance result | Event serialization, TRUST-transfer algebra, ROT derivation/propagation, RUST concentration/localization, complete trichotomy derivation, and maturation of specific optional proof backends | +| zero-identity/zero-knowledge (ZIZK) artifact-first TRUST | Governing VSTD architecture in `standard/LADDER.md` section 1.1; not a separate numbered profile | Hash-chained event serialization and offline replay, evidence-bound forward TRUST, typed ROT, challenge projection, reverse RUST, structural concentration, conflict resolution, explicit localization, and bounded diagnostic attribution | Exact Graph topology, proposition bindings, embedded evidence bytes, mechanisms, trust roots, bounds, and immutable history | Implemented reference mechanism; no universal support score or actor trust | Domain-specific transfer/localization mechanisms, independent cross-implementation replay, complete trichotomy derivation, and maturation of optional proof backends | | RISC Zero proof-carrying reference mechanism | Bounded non-normative mechanism example under the governing ZIZK architecture | Pinned prover/verifier source plus a tracked real receipt, public envelope, self-test result, and verifier command that can run network-offline after setup | Authenticates one fixed hidden-witness predicate and expected image identifier; it does not establish the witness's external truth | Native proof verified; no VSTD receipt mapping | Complete VSTD trichotomy predicate, second build, external audit, and additional proof backends | The authoritative implementation-to-specification map is @@ -160,19 +162,29 @@ record independently evidenced witness corroboration through VSTD-5. This traver not collapse the named coordinates. VSTD-Graph is the orthogonal collection axis: a bounded Graph result may be materialized as a content-addressed artifact and enter a later verification loop only with its source graph, selected surface, mechanism, lineage, losses, limitations, conflicts, -and current admissibility preserved. Current Graph-2 through Graph-5 results remain -`NOT_ESTABLISHED` candidates because their supplied ratings are not evidence-bound. +and current admissibility preserved. The compatibility `graph_level` result remains a +`NOT_ESTABLISHED` candidate. `establish_graph_level` can establish only profile 1–5 after +every required rating mechanism is rerun from exact evidence bound to the exact Graph, +member set, collection, and claim. Profile zero remains `NOT_ESTABLISHED`. The formal names **TRUST**, **ROT**, and **RUST** are semantic terms, not acronyms, actor -ratings, scalar scores, serialized receipt values, or references to the Rust programming language. +ratings, scalar scores, numbered-profile verdicts, or references to the Rust programming language. +They serialize only as typed events in `VSTD-GRAPH-ASSURANCE-1`. TRUST is mechanism-earned artifact support moving forward through checked development; ROT is typed, time-indexed degradation of current admissibility without rewriting historical evidence; RUST is the inverse-TRUST diagnostic mechanic moving backward from a descendant deviation toward recorded ancestor candidates. This memetic propagation does -not prove guilt, responsibility, falsehood, causal localization, or automatic ancestor -falsification. See [the governing +not by itself prove guilt, responsibility, falsehood, causal localization, or automatic +ancestor falsification. The reference runtime requires a separate passing localization +mechanism and an exact artifact-relative diagnostic proposition before it can emit bounded +`BLAME` or `GUILT`; those results never concern actor character. See [the governing architecture](standard/LADDER.md#11-artifact-first-causal-provenance-orientation). +`VSTD-GRAPH-ASSURANCE-1` carries the immutable historical Graph, exact event bindings, +embedded evidence bytes, event hash chain, and derived current-view digest. +`recheck_assurance_log` rehashes the evidence, reruns each exact registered mechanism, and +rejects any event or current view that does not reproduce. + This is VSTD's **ZIZK artifact-first TRUST architecture**, not an optional research profile. Zero identity means zero identity-derived verdict weight, not anonymity or the absence of identifiers. Zero knowledge means zero unevidenced knowledge is presumed: a diff --git a/ROADMAP.md b/ROADMAP.md index 05cd737..ea18d63 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -203,7 +203,7 @@ and ownership decision. ## Current experimental development tracks -This dated register records substantive work as of **2026-08-25**. A committed experiment, +This dated register records substantive work as of **2026-08-29**. A committed experiment, passing test, or generated index is not normative, released, reproduced by a distinct actor, or evidence of adoption merely because it exists. Profile manifests and the generated [`experiments/INDEX.md`](experiments/INDEX.md) are the portable experiment register when @@ -212,7 +212,7 @@ intentional experiment artifacts are present. | Track | Public artifact | Current boundary | Next gate | |---|---|---|---| | 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 | [`experiments/artifact_first_mechanisms/experiment.json`](experiments/artifact_first_mechanisms/experiment.json) | Experimental event serialization, TRUST-transfer algebra, ROT derivation/propagation, RUST concentration/localization, complete trichotomy derivation, and specific unfinished optional proof backends under the already-governing ZIZK architecture. The bounded identity evaluator and tracked RISC Zero reference mechanism are under `examples/`. | Implement and falsify each mechanism without treating the governing orientation as contingent or creating actor-tied trust. | +| 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. | ## Milestone 1 — make refutation the front door @@ -279,12 +279,24 @@ intentional experiment artifacts are present. ## Milestone 4 — challenge and degradation network -**Build** +**Implemented reference mechanism** - append-only challenge envelopes and adjudication records; -- transitive blast-radius computation over object and transformation nodes; -- freshness and availability policies for evidence that disappears; -- portable bundles for disconnected verification. +- challenge-ledger projection into an additive current Graph view; +- transitive, deduplicated descendant impact discovery; +- strictly degrading ROT status propositions without historical mutation; +- current TRUST invalidation when a required ancestor becomes inadmissible; +- portable `VSTD-GRAPH-ASSURANCE-1` logs with embedded evidence and exact offline + mechanism replay; and +- additive conflict resolution, RUST reachability/concentration, explicit localization, + and bounded artifact-relative diagnostic attribution. + +**Still supply per deployment** + +- freshness and availability mechanisms for the deployment's clocks, dependencies, and + retention boundary; +- domain mechanisms that decide whether an affected descendant actually changes status; +- external independent replay evidence and interoperable implementations. **Exit evidence** @@ -295,7 +307,9 @@ intentional experiment artifacts are present. ## Milestone 5 — corroboration without pseudo-independence -VSTD-5 remains draft until operating experience and actual outside participants exist. +The VSTD-5 meta-verification path and replayable receipt are implemented. A real +independent-witness claim remains unavailable until outside participants supply evidence +that passes every required separation and corroboration mechanism. **Exit evidence** diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md index 065a2bf..6ea80dc 100644 --- a/docs/API_STABILITY.md +++ b/docs/API_STABILITY.md @@ -22,6 +22,19 @@ The supported artifact-control exports are `freeze_artifact`, `seal_artifact`, require the optional `seal` dependency extra; importing the base package and freeze-only operations retain the zero-third-party-dependency boundary. +The supported evidence-bound construction exports are `BoundProposition`, +`EvidenceBindingError`, `EvidenceBounds`, `EvidenceStore`, `MechanismDecision`, +`MechanismOutcome`, `VerificationSession`, `WitnessBundle`, `ProvenanceHypergraph`, +`claim_binding_from_dict`, `establish_vstd4`, `assess_witness_corroboration`, +`establish_graph_level`, `graph_collection_binding_digest`, and `AssuranceLedger`. +The matching supported portable-record exports are +`build_evidence_bound_vstd4_receipt`, `recheck_evidence_bound_vstd4_receipt`, +`build_vstd5_receipt`, `recheck_vstd5_receipt`, +`build_evidence_bound_graph_level_record`, +`recheck_evidence_bound_graph_level_record`, and `recheck_assurance_log`. +Compatibility `vstd4_depth` and `graph_level`-style candidate results do not become +conformance results merely because the evidence-bound APIs also exist. + 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 09db034..b23172a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -31,17 +31,19 @@ shape only; a passing validator establishes only its named implemented checks. | 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-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 unbound candidate depth in `verifier.core.depth` | `vstd4_certificate.json`, `vstd4_receipt.json` | `test_gdc_certificate.py`, `test_vstd4_depth.py` | -| VSTD-5 | `standard/VSTD-5.md` | fail-closed candidate rejection only | `vstd5_receipt.json` | `test_vstd4_depth.py`, `test_vstd_schemas.py` | +| 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` | +| VSTD-5 | `standard/VSTD-5.md` | `verifier.core.witness` evidence-bound entry, independence, corroboration, disagreement, build, and replay | `vstd5_receipt.json` | `test_evidence_bound_assurance.py`, `test_vstd_schemas.py` | | VSTD-Graph-1 | `standard/VSTD-Graph-1.md` | `verifier.data.models`, `verifier.data.receipt` | `vstd_graph_receipt.json` | `test_public_data.py` | -| VSTD-Graph-2..5 | matching Graph documents | `verifier.data.graph_level` | `computed_graph_level` within `vstd_graph_receipt.json` | `test_graph_level.py` | -| ZIZK artifact-first TRUST/ROT/RUST | `standard/LADDER.md` section 1.1 | Governs every mechanism; bounded RISC Zero example under `examples/zizk_artifact_first/` | No separate serialized receipt identifier or profile | presentation, experiment-manifest, and ZIZK mechanism tests | +| VSTD-Graph-2..5 | matching Graph documents | `verifier.data.graph_level` candidate/evidence-bound paths | `computed_graph_level` within `vstd_graph_receipt.json` | `test_graph_level.py`, `test_evidence_bound_assurance.py` | +| ZIZK artifact-first TRUST/ROT/RUST | `standard/LADDER.md` section 1.1 | `verifier.data.assurance`; bounded RISC Zero example under `examples/zizk_artifact_first/` | `vstd-graph-assurance-1.schema.json`; not a numbered-profile receipt | assurance, presentation, experiment-manifest, and ZIZK mechanism tests | | Artifact freeze, seal, and thaw | `standard/ARTIFACT_CONTROL.md` | `verifier.artifact_control` and `vstd artifact` | `standard/schemas/artifact-control-1.schema.json`; these are mechanism objects, not receipts | `test_artifact_control.py`, public API/CLI tests | -VSTD-5 is draft. The VSTD-4 candidate-depth runtime and Graph-2 through Graph-5 compute candidates -from caller-supplied references or ratings and return -`conformance_status = NOT_ESTABLISHED`; evidence binding is not implemented. Neither -candidate is numbered-profile conformance. +Compatibility VSTD-4 and Graph paths still compute candidates from caller-supplied +references or ratings and return `conformance_status = NOT_ESTABLISHED`. Separate +evidence-bound paths resolve exact bytes, pin and rerun registered mechanisms, enforce +bounds, and recheck certificates before they can report `ESTABLISHED`. A mechanism result +is limited to its proposition, evidence, trust roots, implementation digest, and bounds; +the repository claims no external witness or independent implementation. Artifact control is an orthogonal mechanism beneath the axes. It can preserve and close an artifact used in any numbered profile, but its successful verification establishes only exact-byte @@ -110,13 +112,13 @@ assessment must examine. A materialized Graph result must retain or bind the source Graph receipt, target collection or induced subgraph, selection query, object and edge ratings with their evidence, selected -surface, lifecycle and conflict state, candidate Graph profile certificate, materialization mechanism, +surface, lifecycle and conflict state, Graph profile certificate, materialization mechanism, and declared information loss. It earns no strength from size, path count, repetition, storage, or agreement. Its result is capped by every applicable member, transformation, -mapping, substrate, refutation, witness, and materialization obligation. Because current -Graph-2 through Graph-5 ratings are caller-supplied, present materialization can establish -only the candidate computation over those ratings, not independently verified Graph -conformance. +mapping, substrate, refutation, witness, and materialization obligation. The compatibility +path establishes only its candidate computation. The evidence-bound path can establish a +Graph profile only after rerunning every required rating mechanism across the complete +closure. ## Governing ZIZK architecture and mechanism ownership @@ -156,9 +158,10 @@ Maturity attaches to mechanisms beneath that architecture: |---|---|---| | RISC Zero hidden-witness predicate | Bounded reference mechanism with tracked public proof artifacts | `examples/zizk_artifact_first/risc0/`; native verification only, no VSTD receipt mapping | | Bounded identity-disclosure evaluator | Bounded non-normative reference mechanism | `examples/zizk_artifact_first/zero_identity/`; no identity-derived trust | -| Event serialization and TRUST-transfer algebra | Experimental and unimplemented | May implement the governing direction but cannot redefine it | -| ROT derivation and cross-surface propagation | Experimental and incomplete | Current Graph admission handles some lifecycle states; no general ROT result or propagation protocol exists | -| RUST concentration and localization | Experimental and unimplemented | Diagnostic reachability only until a separately specified mechanism earns more | +| Assurance event serialization and replay | Implemented bounded reference mechanism | `VSTD-GRAPH-ASSURANCE-1` embeds the historical Graph, exact bindings, evidence bytes, a hash chain, and a current-view digest; `recheck_assurance_log` reruns every event mechanism | +| TRUST transfer | Implemented proposition-dispatch reference mechanism | `record_trust` accepts only recorded ancestors and an exact passing support proposition; current lifecycle or conflict state can invalidate the event for current use without deleting history. No universal scalar support algebra exists. | +| ROT derivation and cross-surface propagation | Implemented bounded reference mechanisms | Strictly degrading status propositions and complete challenge-ledger projections produce additive current-state overlays; the deduplicated descendant impact set is discovery, and a descendant status change still needs its own mechanism | +| RUST concentration and localization | Implemented bounded reference mechanisms | A passing descendant-deviation proposition produces deduplicated reverse reachability; concentration counts unique descendants; localization and artifact-relative diagnostic attribution require separate passing propositions | | Complete `PASS`/`FAIL`/`UNKNOWN`/`CONFLICTED` hidden-witness derivation | Experimental and unimplemented | A caller-supplied state tag is not an earned verdict | | Specific optional proof backends | Backend-specific maturity; the RISC Zero example has one recorded native proof | Optional proof machinery cannot make the governing architecture optional or establish broader VSTD conformance | @@ -233,9 +236,9 @@ score. No receipt or schema format is defined here. |---|---|---|---| | `ASSURANCE_0` | Identified evidence or a previously assessed claim, with its evidence basis, provenance, bounds, trust roots, limitations, current state, and unresolved conflicts or unknowns. | Generic-run receipts and external-evaluation evidence; `EvidenceClassification`; Graph artifacts, statuses, and `ConflictRecord`; VSTD-3 evidence sources, gaps, and claim evaluations; VSTD-4 certificates and kernel results. | There is no universal Assurance record or cross-profile scalar ordering. | | `ATTRIBUTION` | The explicit relation from evidence to the exact subject/predicate it supports, including the mapping, extraction, or transformation, scope, bounds, provenance, and information loss. | Generic-run bound-output extraction and recorded external references; Graph transformation hyperedges; VSTD-4 `ClaimCoordinate`, `ClaimBinding`, and `Grounding`; loss-sensitive SCITT coordinates. | Mapping and loss declarations remain profile-specific; a reference alone is not a checked mapping. | -| `ASSIGNMENT` | The most precise evidenced execution coordinate available: computation, execution instance, software/runtime, machine/substrate, then optional actor/operator bindings. Missing coordinates remain partial or `UNKNOWN`. | Generic-run execution and source-state records; VSTD-3 `WorkloadIdentity`, `ExecutionIdentity`, topology, device, runtime, and evidence-source records; VSTD-1 `independence_basis` for the separate independence question. | Generic actor/execution evidence binding is not implemented. Assignment alone establishes no trust, authorization, independence, or responsibility. | +| `ASSIGNMENT` | The most precise evidenced execution coordinate available: computation, execution instance, software/runtime, machine/substrate, then optional actor/operator bindings. Missing coordinates remain partial or `UNKNOWN`. | Generic-run execution and source-state records; VSTD-3 `WorkloadIdentity`, `ExecutionIdentity`, topology, device, runtime, and evidence-source records; VSTD-1 `independence_basis` for the separate independence question; generic `BoundProposition` mechanism dispatch for an exact assignment proposition. | The legacy generic-run declaration does not self-promote into an evidenced Assignment. A deployment supplies the mechanism and observations; Assignment alone establishes no trust, authorization, independence, or responsibility. | | `ASSESSMENT` | An identified verifier or mechanism evaluates one bounded proposition under the applicable input Assurance, Attribution, Assignment, specification/profile, trust roots, and bounds. It earns only the predicates it checks. | Generic validation, artifact rehash, and rerun mechanisms; VSTD-3 recomputed `ClaimEvaluation`; Graph validation and candidate-profile certificates; the VSTD-4 grounded certificate kernel; native VSTD plus native SCITT composition. | No one verifier covers every profile; mechanism results remain adjacent rather than silently merged. | -| `ASSURANCE_1` | The assessment output recorded as new evidence with complete lineage to its inputs, mechanism, proposition, and limits. It may be `PASS`, `FAIL`, `UNKNOWN`, `CONFLICTED`, or a profile-specific equivalent. | Receipts, claim evaluations, kernel results, certificates, artifact digests, Graph artifacts/hyperedges, and prior commitments can preserve and reference the output. | There is no universal recursive-loop envelope; any future wire representation requires a separate proposal. | +| `ASSURANCE_1` | The assessment output recorded as new evidence with complete lineage to its inputs, mechanism, proposition, and limits. It may be `PASS`, `FAIL`, `UNKNOWN`, `CONFLICTED`, or a profile-specific equivalent. | Receipts, claim evaluations, kernel results, certificates, artifact digests, Graph artifacts/hyperedges, prior commitments, and the replayable `VSTD-GRAPH-ASSURANCE-1` event envelope preserve and reference the output. | The Graph envelope is not a universal scalar or an automatic cross-profile cast; each later loop still names and reruns its mechanism. | First-hand and second-hand describe **provenance**, not strength. A first-hand self-observation may be weak; a second-hand certificate may be strongly bound to a narrow @@ -264,9 +267,10 @@ the child still discharges its new obligations. ROT is typed, time-indexed degra current admissibility while historical evidence remains immutable. RUST is inverse-TRUST diagnostic reachability from a downstream deviation toward recorded ancestors. Together they describe memetic causal-provenance and lifecycle behavior over one development graph. -They do not establish actor standing, guilt, responsibility, causal localization, or -automatic ancestor falsification, and no current runtime emits a general TRUST, ROT, or -RUST transfer result. +They do not establish actor standing, moral character, responsibility, or automatic +ancestor falsification. The reference runtime emits bounded TRUST, ROT, and RUST events +only after their exact mechanisms run. Causal localization and artifact-relative `BLAME` +or `GUILT` require additional exact propositions and never become actor reputation. ### Recursive-amplification falsification outcomes @@ -291,10 +295,11 @@ The historical `independent_audit` field name does not prove independence. Its `independence_basis` records actor, implementation, and runtime separation. Repeated or matching results are artifact agreement, not evidence that separate actors performed the runs; absent separation evidence is `NOT_DEMONSTRATED`. Serialized status words and -evidence-reference strings cannot self-promote that result. Because version 1.2.0 has no -actor/execution evidence-binding adapter, the bundled runtime treats supplied assertions -as no stronger than `DECLARED`, rejects receipts that label them `EVIDENCED`, and never -derives `EVIDENCED`. +evidence-reference strings cannot self-promote that result. The generic-run compatibility +path treats supplied assertions as no stronger than `DECLARED`, rejects receipts that label +them `EVIDENCED`, and never derives `EVIDENCED`. The distinct VSTD-5 reference path can +establish only its exact declarant/witness separation propositions after all seven seams are +rerun by registered mechanisms; it does not upgrade the legacy generic-run fields. Graph conflict records retain incompatible values and their evidence references without adding a scalar score or changing the frozen artifact-status vocabulary. A conflict makes @@ -307,19 +312,20 @@ question is a new assessment over the retained graph and applicable lifecycle re | Scenario | Implemented outcome | |---|---| -| An ancestor is `CHALLENGED`, `REVOKED`, or `STALE` | Candidate Graph recomputation follows the full ancestor closure and returns compatibility field `level = 0`, meaning no numbered Graph profile is satisfied. This implements a bounded ROT consequence for that query; it does not emit a general ROT result or rewrite the historical receipt. | +| An ancestor is `CHALLENGED`, `REVOKED`, or `STALE` | Candidate/evidence-bound Graph recomputation follows the full ancestor closure and returns compatibility field `level = 0`. `AssuranceLedger` also records typed ROT or projects append-only challenge-ledger state into a derived current view; historical Graph bytes remain unchanged. | | An ancestor is `SUPERSEDED` | The historical Graph candidate remains admissible by design; the stricter all-ancestors-`VALID` policy rejects it for current-use admission. Supersession does not retroactively falsify its prior lineage role. | -| Upstream evidence conflicts | A retained `ConflictRecord` blocks a clean candidate. Conflict resolution is not implemented; any future resolution must be additive and retain the competing evidence. | +| Upstream evidence conflicts | A retained `ConflictRecord` blocks a clean result. `resolve_conflict` accepts only a mechanism-passing proposition bound to the exact conflict and one retained competing value; the original conflict remains in history while the derived current view records the additive resolution. | | Evidence arrives by multiple paths or one run receipt repeats a reference | Reachability and impact sets deduplicate identifiers. Multiplicity supplies no independence or strength. | -| A descendant deviation points toward shared ancestors | Existing ancestor queries establish recorded reverse reachability only. No runtime emits RUST, measures independent concentration, or attributes causal responsibility. | -| A challenge ledger changes a claim's current status | The append-only ledger derives `CHALLENGED` or `REVOKED`, but no adapter binds that claim status into a Graph artifact. Cross-surface propagation is `NOT_ESTABLISHED`, not silently clean. | -| Later evidence is intended to clear a conflict | No conflict-resolution transition exists in version 1.2.0. Removing the old record would violate additive correction; a future mechanism must preserve it and identify what resolved it. | +| A descendant deviation points toward shared ancestors | A mechanism-passing deviation emits RUST over the deduplicated recorded ancestor set. Structural concentration counts unique deviating descendants, not paths or causal strength. Localization, BLAME, and GUILT require separate exact mechanisms. | +| A challenge ledger changes a claim's current status | `project_challenges` binds its complete append-only records into a current Graph overlay and embeds those records for replay. Existing TRUST remains historical but disappears from `current_trust_events`; `impacted_descendants` reports the deduplicated reassessment surface. It never mutates the historical graph. | +| Later evidence clears a conflict | The additive resolution retains the original competing evidence and its mechanism evaluation. Removing or rewriting the conflict remains invalid. | | Candidate calculation encounters cyclic ancestry | Rejected before candidate calculation; recursive topology cannot manufacture assurance. | -The implemented forward blast-radius query discovers recorded downstream artifacts and -generic-run receipts that require reconsideration when given an invalidated artifact. It is -a discovery mechanism, not automatic status mutation, current-admissibility adjudication, -or proof of causal influence. Automatic propagation for challenge, staleness, supersession, -conflict resolution, general TRUST transfer, ROT propagation, and RUST remains -`NOT_ESTABLISHED` until a distinct mechanism binds the lifecycle event to the exact Graph -artifact and proposition. +The forward blast-radius query remains discovery only. `AssuranceLedger` is the distinct +binding mechanism for explicit TRUST, ROT, RUST, challenge projection, and conflict +resolution. `recheck_assurance_log` reconstructs the historical Graph, rehashes every +embedded evidence item, reruns the exact registered mechanisms, reproduces the event chain, +and compares the derived current view. The ledger never infers an unrecorded edge, converts +topology into assurance, or treats RUST as causality. Domain-specific transfer and +localization propositions still require their registered mechanisms and may return +`UNKNOWN`. diff --git a/docs/CLAIMS_AND_LIMITS.md b/docs/CLAIMS_AND_LIMITS.md index 15c1905..50b7f19 100644 --- a/docs/CLAIMS_AND_LIMITS.md +++ b/docs/CLAIMS_AND_LIMITS.md @@ -37,12 +37,12 @@ checker result—not a claim of universal truth or whole-project conformance. | Reviewer question | Current answer | Mechanism and trust roots | Boundary or missing mechanism | |---|---|---|---| | What can the generic validator establish? | Stable receipt content and strict profile shape. | Canonicalization, recorded digest, profile discriminator, and bundled validator bytes. | It does not verify the recorded native claim, external evidence, actor identity, or independence. | -| What can the grounded-certificate kernel establish? | The exact `VSTD4-GDC-1` decision was accepted, rejected, or left `UNKNOWN` under its claim binding and resource bound. | Certificate bytes, formula, grounding, policy/evidence roots, verifier descriptor, and kernel. | VSTD-4 conformance is `NOT_ESTABLISHED`; rung evidence and prerequisite-profile coordinates are not bound by the candidate-depth runtime. | -| What can VSTD-Graph establish? | Stored objects, transformations, conflicts, recorded reachability, and policy results over that graph. | Content digests, Graph receipt, hyperedges, statuses, and named query or policy. | Graph profiles 2–5 are candidates from caller-supplied ratings; completeness, causality, and rating-to-evidence binding are absent. | +| What can the grounded-certificate kernel establish? | The exact `VSTD4-GDC-1` decision was accepted, rejected, or left `UNKNOWN` under its claim binding and resource bound. | Certificate bytes, formula, grounding, policy/evidence roots, verifier descriptor, and kernel. | Kernel acceptance alone is not VSTD-4 conformance. The evidence-bound path separately reruns every prerequisite/rung mechanism before it may establish 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; a thawed descendant currently matches or differs from its sealed parent. | Preserved bytes, SHA-256 and SHA3-256 commitments, read-only payload-tree guard, Ed25519 signature, artifact-derived identifiers, and any supplied external artifact/key anchor. | Read-only is not privileged access control; a seal is not encryption, correctness, trusted time, ownership, durable external archiving, or a numbered-profile result. | | 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 actor independence, automatic challenge-to-Graph propagation, VSTD-5, and unqualified provenance truth. | No current ordinary VSTD mechanism observes or validates those propositions. | Preserve `UNKNOWN`, `UNSUPPORTED`, `CONFLICTED`, or `NOT_ESTABLISHED`; do not infer a clean result. | +| 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. | Every claim below expands one of these boundaries into publishable wording and its required falsification surface. @@ -67,8 +67,9 @@ required falsification surface. | “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 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.** | Blast radius is forward reachability over the stored graph. | Challenged artifact ID and bound hypergraph. | Historical receipts were automatically mutated or that unrecorded downstream systems were found. | +| “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. | | “The compatibility API returned Graph `level = N`.†| **Not yet as a conformance claim.** | The current implementation computes candidate Graph profile `N` from caller-supplied artifact and edge ratings; `level` is the retained field name for that profile number. It labels the result `CALLER_SUPPLIED` and `NOT_ESTABLISHED`. | A structurally valid graph and explicit supplied ratings. Conformance additionally requires implemented rating-to-evidence bindings for every required profile coordinate. | The supplied ratings were independently derived, every coordinate's evidence passed, or Graph conformance was established. | +| “The evidence-bound Graph path established profile `N`.†| **Only for profile 1–5 at the exact collection and current view under the rerun mechanisms.** | Every member, ancestor, and reached edge rating binding passed; each rating binds the Graph bytes, deduplicated member set, collection, and claim; the Graph certificate checked; embedded evidence permits replay. | Exact Graph/event-log bytes, bindings, evidence, mechanism digests, trust roots, bounds, and recheck result. | Profile zero is conformance, real-world lineage is complete, topology proves causality, or another collection inherits the result. | ## Competition and scored-evaluation claims @@ -105,9 +106,10 @@ the evidence source or make the grounded claim true outside that coordinate. | “This VSTD4-GDC-1 certificate was accepted.†| The identified reference kernel accepted the exact canonical certificate under the declared claim binding, fragment, verifier, and resource bounds. | Name the certificate digest, implementation commit, claim coordinate, cost tier, bounds, and kernel result. | The underlying evidence is authentic, the policy captured every intended condition, or the claim is globally true. | | “This decision is grounded.†| Every variable and clause in the accepted certificate maps to declared subjects, predicates, values, and encoding rules whose roots are bound by the certificate. | Preserve the evidence root, policy root, grounding map, and exclusions. | Unrecorded evidence does not exist, the grounding source is independent, or the physical world is completely represented. | | “The reference implementation computed VSTD-4 candidate depth `k`.†| Caller-supplied nonempty references were structurally consistent through rungs `1..k` and, when `k < 14`, the candidate ceiling certificate blocks rung `k+1`. | State `CANDIDATE`, `conformance_status = NOT_ESTABLISHED`, the supplied references, certificates, budgets, and horizons. | The references establish their rung propositions, VSTD-1/2/3 passed, normative VSTD-4 conformance was established, or VSTD-5 entry is permitted. | +| “The evidence-bound path established VSTD-4 normative depth 14.†| Exact VSTD-1/2/3 and fourteen-rung propositions passed after evidence rehash, mechanism selection/execution, bound enforcement, and kernel checking. | Name the receipt, evidence and mechanism digests, trust roots, bounds, implementation coordinate, and recheck result. | The mechanisms are universally correct, an outside witness participated, or the claim is true beyond its exact bindings. | | “The result is refutable.†| The published result exposes a machine-checkable falsification surface and admissible counterevidence within the declared boundary. | Name that surface, the admissible counterevidence, exclusions, and decision rule. | A separate party actually attempted refutation or independently witnessed the evidence. | | “The verifier returned `UNKNOWN`.†| The declared check could not establish `PASS` or `FAIL` within the implemented fragment, available evidence, or resource bound. | Preserve the indeterminacy reason and transcript. | The proposition is false, no proof exists, or a larger bound could not decide it. | -| “The artifact is ready for VSTD-5 evaluation.†| **Not established by the current reference implementation.** | VSTD-5 requires evidence-bound VSTD-1/2/3 preconditions and normative VSTD-4 conformance at depth 14. `require_vstd5_entry` rejects the current unbound candidate. | Candidate depth 14, a `PASS` over the candidate formula, or nonempty references satisfy the gate. | +| “The artifact is ready for VSTD-5 evaluation.†| **Only after the evidence-bound VSTD-4 path establishes VSTD-4 normative depth 14.** | `require_vstd5_entry` rejects the compatibility candidate and admits only the distinct established result type. | Candidate depth 14, a `PASS` over the candidate formula, or nonempty references satisfy the gate. | The public reference implementation and its tests are one implementation. This source coordinate does not claim an external implementation, interoperability result, @@ -193,13 +195,20 @@ VSTD-4 safe template: > establish evidence authenticity, complete policy coverage, independent > witnessing, or truth outside the coordinate. -VSTD-5 draft boundary template for the current implementation: +VSTD-5 reference-result template: -> The artifact reached VSTD-4 structural candidate depth 14 with an accepted -> certificate over caller-supplied references. Conformance remains -> `NOT_ESTABLISHED`; the VSTD-5 entry gate rejected the candidate. VSTD-5 remains -> a draft specification, and no VSTD-5 readiness, conformance, or -> independent-witness claim is made. +> At receipt ``, the VSTD-5 reference mechanism admitted evidence-bound +> VSTD-4 result ``, rehashed the embedded witness evidence, and reran the +> exact seven separation and corroboration mechanisms under ``. +> It returned `` with conformance +> ``. This does not imply actor trust, an external +> witness not named by the evidence, a second implementation, or truth outside +> the checked propositions. + +A `CORROBORATED` overall result is valid only with `ESTABLISHED` conformance and +`INDEPENDENT` computed separation. If a positive observation survives but any separation +seam is unresolved, report overall `UNKNOWN`; preserve `REFUTED` and `CONFLICTED` results +rather than softening demonstrated negative evidence. ## Prohibited shortcuts diff --git a/docs/assets/vstd-overview.png b/docs/assets/vstd-overview.png index 08e738a49cd1f899a77b5514f09ef51450336ebd..a2a817cf9ebf7d433bd593a73b20477366d76c2a 100644 GIT binary patch literal 148262 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%0Akcjy8LhS()-D!#!A=LR> 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 z

    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 ZNW@J}RS?>rmJ0ztI+_L=)oKn={{tALHZuSK literal 89535 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

    A green check is not a complete explanation.

    Artifact-first semantics

    Verify the process, not the actor.

    -

    VSTD evaluates bounded validity propositions about computational processes represented by software and evidence-bearing artifacts. Identity, popularity, and reputation alone add no verdict weight. TRUST, ROT, and RUST are formal semantic names, not acronyms or actor ratings. Architectural zero knowledge presumes no unevidenced proposition; cryptographic zero knowledge can enclose a confidential witness only through a named proof system bound to the exact program and predicate.

    +

    VSTD evaluates bounded validity propositions about computational processes represented by software and evidence-bearing artifacts. Identity, popularity, and reputation alone add no verdict weight. TRUST, ROT, and RUST are formal semantic names, not acronyms or actor ratings; the reference Graph assurance log records them as evidence-bound events and can replay their embedded evidence and mechanisms offline. Architectural zero knowledge presumes no unevidenced proposition; cryptographic zero knowledge can enclose a confidential witness only through a named proof system bound to the exact program and predicate.

    TRUST · FORWARD

    Mechanism-earned support

    Exact artifact support may move through a checked transformation. It is never a rating of whether an actor is good or trustworthy.

    ROT · CURRENT STATE

    Admissibility degrades

    Typed lifecycle evidence can require reassessment without rewriting an immutable historical result. Age alone is insufficient.

    -
    RUST · BACKWARD

    Diagnostic ancestry

    A descendant deviation can trace toward recorded ancestor candidates. Reachability is not guilt, falsehood, or causal localization.

    +
    RUST · BACKWARD

    Diagnostic ancestry

    A checked descendant deviation traces over deduplicated recorded ancestors. Localization and artifact-relative BLAME or GUILT require additional exact mechanisms; reachability alone earns none of them.

    @@ -118,7 +118,7 @@

    Useful without pretending to be total.

    Current status

    Current implementation status

    -

    VSTD is a maintainer-led alpha project specification. VSTD-4 candidate depth and Graph profiles 2-5 are candidate computations with conformance NOT_ESTABLISHED; VSTD-5 is draft and not implemented. The project has no demonstrated external adoption, independent implementation, interoperability deployment, or third-party security review.

    +

    VSTD is a maintainer-led alpha project specification. Compatibility VSTD-4 and Graph paths remain NOT_ESTABLISHED candidates. Separate evidence-bound VSTD-4, VSTD-5, Graph-profile, and assurance-event paths rerun exact registered mechanisms from embedded evidence; no real external witness is claimed by this repository. The project has no demonstrated external adoption, independent implementation, interoperability deployment, or third-party security review.

    Release coordinate: this branch documents verifier-standard 1.2.0 as an unreleased candidate. Use GitHub Releases for diff --git a/docs/reference.html b/docs/reference.html index 716a9f7..366fa1f 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -34,7 +34,7 @@

    -
    Reference · verifier-standard 1.2.0 · VSTD-4 CANDIDATE; CONFORMANCE NOT_ESTABLISHED
    +
    Reference · verifier-standard 1.2.0 · VSTD-5 PROJECT SPECIFICATION; EVIDENCE-BOUND REFERENCE MECHANISM

    Inspect the whole pipeline.

    Terms used below: hash-based message authentication code (HMAC); International Organization for Standardization (ISO); JavaScript Object @@ -426,6 +426,42 @@

    ArtifactVerification class

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

    AssuranceLedger class

    +
    AssuranceLedger(graph: 'ProvenanceHypergraph') -> 'None'
    +

    Append-only current-state overlay for an immutable provenance graph.

    +

    Defined in verifier.data.assurance

    + + + + + + + + + + + + + + + + + + + +
    MethodSummary
    current_status(self, artifact_id: 'str') -> 'ArtifactStatus'
    current_trust_events(self) -> 'tuple[AssuranceEvent, ...]'Return passing TRUST records still admissible under the current view.
    diagnose(self, kind: 'DiagnosticKind', ancestor_id: 'str', descendant_id: 'str', proposition: 'Optional[BoundProposition]', *, session: 'VerificationSession', recorded_at: 'str') -> 'DiagnosticAttribution'Compute bounded artifact-relative BLAME or GUILT just in time.
    events(self) -> 'tuple[AssuranceEvent, ...]'
    impacted_descendants(self, artifact_id: 'str') -> 'tuple[str, ...]'Return the deduplicated recorded forward impact set, not a verdict.
    localize_cause(self, ancestor_id: 'str', descendant_id: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    materialize_current_graph(self) -> 'ProvenanceHypergraph'Create a derived current view; never mutate the historical graph.
    project_challenges(self, challenges: 'ChallengeLedger', *, recorded_at: 'str') -> 'tuple[AssuranceEvent, ...]'Project challenge state into an additive current Graph overlay.
    record_rot(self, artifact_id: 'str', resulting_status: 'ArtifactStatus', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    record_rust(self, descendant_id: 'str', deviation: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    record_status_projection(self, artifact_id: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Record a mechanism-checked current-status projection additively.
    record_trust(self, target_id: 'str', source_ids: 'Iterable[str]', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    resolutions(self) -> 'tuple[ConflictResolution, ...]'
    resolve_conflict(self, conflict_id: 'str', selected_value: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'ConflictResolution'
    rust_concentration(self) -> 'tuple[StructuralConcentration, ...]'
    to_dict(self) -> 'dict[str, Any]'
    unresolved_conflicts(self) -> 'tuple[ConflictRecord, ...]'
    verify_hash_chain(self) -> 'bool'
    +
    +
    +

    BoundProposition class

    +
    BoundProposition(subject_id: 'str', predicate: 'str', expected: 'Any', mechanism_id: 'str', mechanism_digest: 'str', evidence_refs: 'tuple[str, ...]', trust_roots: 'tuple[str, ...]', bounds: 'EvidenceBounds', parameters: 'Mapping[str, str]' = <factory>) -> None
    +

    Exact proposition, evidence, mechanism, trust-root, and bound binding.

    +

    Defined in verifier.core.evidence

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

    DecisionCertificate class

    DecisionCertificate(header: 'CertificateHeader', formula: 'tuple[tuple[int, ...], ...]', grounding: 'Grounding', decision: 'DecisionBlock', hints: 'dict[str, Any]' = <factory>) -> None
    @@ -437,6 +473,71 @@

    DecisionCertificate class

    without_hints(self) -> "'DecisionCertificate'"Hint-stripped form.
    +
    +

    EvidenceBindingError class

    +
    EvidenceBindingError
    +

    An evidence binding is malformed or cannot be resolved exactly.

    +

    Defined in verifier.core.evidence

    + +
    +
    +

    EvidenceBounds class

    +
    EvidenceBounds(max_evidence_items: 'int', max_evidence_bytes: 'int') -> None
    +

    Resource ceilings enforced before invoking a domain mechanism.

    +

    Defined in verifier.core.evidence

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

    EvidenceStore class

    +
    EvidenceStore() -> 'None'
    +

    In-memory content-addressed evidence store with collision/fork refusal.

    +

    Defined in verifier.core.evidence

    + + + + + +
    MethodSummary
    add(self, payload: 'bytes') -> 'str'
    export_base64(self, references: 'Sequence[str]') -> 'dict[str, str]'Export exact evidence bytes for portable, offline mechanism replay.
    import_base64(self, payloads: 'Mapping[str, str]') -> 'None'Import a portable bundle and refuse every reference/byte mismatch.
    resolve(self, reference: 'str') -> 'bytes'
    +
    +
    +

    MechanismDecision class

    +
    MechanismDecision(outcome: 'MechanismOutcome', details: 'str', observations: 'Mapping[str, Any]' = <factory>) -> None
    +

    One bounded mechanism result plus its exact observations.

    +

    Defined in verifier.core.evidence

    + +
    +
    +

    MechanismOutcome enum

    +

    Enumeration of the exported result values.

    +

    Defined in verifier.core.evidence

    +

    Members: PASS, FAIL, UNKNOWN

    +
    +
    +

    ProvenanceHypergraph class

    +
    ProvenanceHypergraph() -> 'None'
    +

    N-ary Hypergraph structure for dataset, training, and artifact lineage.

    +

    Defined in verifier.data.models

    + + + + + + + + + + + + + + + + + +
    MethodSummary
    add_artifact(self, artifact: 'ArtifactNode') -> 'str'
    add_conflict(self, conflict: 'ConflictRecord') -> 'str'
    add_contributor(self, contributor: 'ContributorSpec') -> 'str'
    add_rights(self, rights: 'RightsSpec') -> 'str'
    add_transformation(self, transform: 'TransformationHyperedge') -> 'str'
    ancestors(self, artifact_ids: 'Iterable[str]') -> 'set[str]'Backward reachability closure across transformation hyperedges.
    blast_radius(self, revoked_artifact_id: 'str') -> 'list[str]'Compute the forward blast radius of affected downstream artifacts when one node is revoked.
    compute_completeness(self) -> 'CompletenessMetrics'
    descendants(self, artifact_ids: 'Iterable[str]') -> 'set[str]'Forward reachability closure across transformation hyperedges.
    has_conflict(self, subject_id: 'str') -> 'bool'
    incoming_hyperedges(self, artifact_id: 'str') -> 'list[TransformationHyperedge]'Hyperedges that produce artifact_id as an output.
    outgoing_hyperedges(self, artifact_id: 'str') -> 'list[TransformationHyperedge]'Hyperedges that consume artifact_id as an input.
    root_sources(self) -> 'set[str]'Artifacts with zero incoming hyperedges (genesis roots).
    to_dict(self) -> 'dict[str, Any]'
    validate_structure(self) -> 'list[str]'Return deterministic errors for the implemented graph surface.
    verify_acyclicity(self, artifact_ids: 'Optional[Iterable[str]]' = None) -> 'bool'Check whether all or a selected artifact-induced subgraph contains cycles.
    +

    ReproducibilityLevel enum

    Monotone reproduction-fidelity states; class name retained for compatibility.

    @@ -455,6 +556,16 @@

    VerificationGeometry class

    validate(self) -> 'list[str]'Return structural and epistemic errors; an empty list means valid.
    +
    +

    VerificationSession class

    +
    VerificationSession(evidence: 'EvidenceStore') -> 'None'
    +

    Resolve evidence and rerun only explicitly registered mechanisms.

    +

    Defined in verifier.core.evidence

    + + + +
    MethodSummary
    evaluate(self, binding: 'BoundProposition') -> 'EvaluatedProposition'
    register(self, mechanism: 'VerificationMechanism') -> 'None'
    +

    VerificationVerdict enum

    Outcome vocabulary returned by the VSTD-1 claim-mechanics checker.

    @@ -473,6 +584,43 @@

    VstdReceipt class

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

    WitnessBundle class

    +
    WitnessBundle(claim_id: 'str', declarant_id: 'str', claim_binding_digest: 'str', witnesses: 'tuple[WitnessIdentity, ...]', independence: 'tuple[IndependenceAssertion, ...]', corroborations: 'tuple[CorroborationRecord, ...]') -> None
    +

    Claim-bound witnesses, separation assertions, and corroboration records.

    +

    Defined in verifier.core.witness

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

    assess_witness_corroboration function

    +
    assess_witness_corroboration(entry: 'EvidenceBoundDepthResult', bundle: 'WitnessBundle', *, session: 'VerificationSession') -> 'WitnessCorroborationResult'
    +

    Recheck VSTD-5 entry, separation evidence, and corroboration evidence.

    +

    Defined in verifier.core.witness

    + +
    +
    +

    build_evidence_bound_graph_level_record function

    +
    build_evidence_bound_graph_level_record(result: 'EvidenceBoundGraphLevelResult', *, graph: 'ProvenanceHypergraph', members: 'Sequence[str]', binding: 'ClaimBinding', object_evidence: 'Mapping[str, BoundProposition]', edge_evidence: 'Mapping[str, BoundProposition]', session: 'VerificationSession') -> 'dict[str, Any]'
    +

    Serialize exact Graph rating bindings and bytes for offline replay.

    +

    Defined in verifier.data.graph_level

    + +
    +
    +

    build_evidence_bound_vstd4_receipt function

    +
    build_evidence_bound_vstd4_receipt(result: 'EvidenceBoundDepthResult', *, receipt_id: 'str', claim_id: 'str', binding: 'ClaimBinding', prerequisite_evidence: 'Mapping[int, BoundProposition]', rung_evidence: 'Mapping[str, BoundProposition]', session: 'VerificationSession', status: 'str' = 'VALID') -> 'dict[str, object]'
    +

    Serialize every input needed to rerun an evidence-bound VSTD-4 result.

    +

    Defined in verifier.core.depth

    + +
    +
    +

    build_vstd5_receipt function

    +
    build_vstd5_receipt(entry: 'EvidenceBoundDepthResult', bundle: 'WitnessBundle', result: 'WitnessCorroborationResult', *, receipt_id: 'str', session: 'VerificationSession') -> 'dict[str, Any]'
    +

    Serialize a replayable VSTD-5 receipt without treating names as trust.

    +

    Defined in verifier.core.witness

    +

    capture_run function

    @@ -487,6 +635,13 @@

    certificate_from_canonical_bytes function

    Decode only the canonical JSON representation used in commitment digests.

    Defined in verifier.core.certificate

    +

    +
    +

    claim_binding_from_dict function

    +
    claim_binding_from_dict(data: 'Mapping[str, object]') -> 'ClaimBinding'
    +

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

    +

    Defined in verifier.core.depth

    +

    compute_canonical_digest function

    @@ -494,6 +649,20 @@

    compute_canonical_digest function<

    Compute SHA-256 digest of canonicalized stable payload.

    Defined in verifier.core.receipt

    +

    +
    +

    establish_graph_level function

    +
    establish_graph_level(graph: 'ProvenanceHypergraph', *, collection_id: 'str', members: 'Sequence[str]', object_evidence: 'Mapping[str, BoundProposition]', edge_evidence: 'Mapping[str, BoundProposition]', session: 'VerificationSession', binding: 'ClaimBinding') -> 'EvidenceBoundGraphLevelResult'
    +

    Rerun rating mechanisms before computing a conforming Graph profile.

    +

    Defined in verifier.data.graph_level

    + +
    +
    +

    establish_vstd4 function

    +
    establish_vstd4(rung_evidence: 'Mapping[str, BoundProposition]', *, prerequisite_evidence: 'Mapping[int, BoundProposition]', session: 'VerificationSession', claim_id: 'str', binding: 'ClaimBinding') -> 'EvidenceBoundDepthResult'
    +

    Rerun evidence mechanisms and establish VSTD-4 only if all pass.

    +

    Defined in verifier.core.depth

    +

    freeze_artifact function

    @@ -501,10 +670,45 @@

    freeze_artifact function

    Preserve exact bytes in a new guarded bundle without creating a seal.

    Defined in verifier.artifact_control

    +
    +
    +

    graph_collection_binding_digest function

    +
    graph_collection_binding_digest(graph: 'ProvenanceHypergraph', *, collection_id: 'str', members: 'Sequence[str]', binding: 'ClaimBinding') -> 'str'
    +

    Bind ratings to one Graph, member set, collection, and claim coordinate.

    +

    Defined in verifier.data.graph_level

    + +
    +
    +

    recheck_assurance_log function

    +
    recheck_assurance_log(payload: 'Mapping[str, Any]', *, mechanisms: 'Iterable[VerificationMechanism]') -> 'AssuranceLedger'
    +

    Rebuild and replay a portable assurance log from its embedded bytes.

    +

    Defined in verifier.data.assurance

    + +
    +
    +

    recheck_evidence_bound_graph_level_record function

    +
    recheck_evidence_bound_graph_level_record(graph: 'ProvenanceHypergraph', record: 'Mapping[str, Any]', *, mechanisms: 'Sequence[VerificationMechanism]') -> 'EvidenceBoundGraphLevelResult'
    +

    Rebuild the evidence store, rerun rating mechanisms, and compare result.

    +

    Defined in verifier.data.graph_level

    + +
    +
    +

    recheck_evidence_bound_vstd4_receipt function

    +
    recheck_evidence_bound_vstd4_receipt(receipt: 'Mapping[str, object]', *, mechanisms: 'Sequence[VerificationMechanism]') -> 'EvidenceBoundDepthResult'
    +

    Reconstruct evidence bytes and rerun an evidence-bound VSTD-4 receipt.

    +

    Defined in verifier.core.depth

    + +
    +
    +

    recheck_vstd5_receipt function

    +
    recheck_vstd5_receipt(entry: 'EvidenceBoundDepthResult', receipt: 'Mapping[str, Any]', *, mechanisms: 'tuple[VerificationMechanism, ...]') -> 'WitnessCorroborationResult'
    +

    Import exact bytes, rerun all witness mechanisms, and compare the result.

    +

    Defined in verifier.core.witness

    +

    require_vstd5_entry function

    -
    require_vstd5_entry(result: 'DepthResult') -> 'DepthResult'
    +
    require_vstd5_entry(result: 'DepthResult | EvidenceBoundDepthResult') -> 'EvidenceBoundDepthResult'

    Reject the current unbound candidate result at the VSTD-5 boundary.

    Defined in verifier.core.depth

    diff --git a/docs/standards/SCITT_SEMANTIC_BOUNDARY.md b/docs/standards/SCITT_SEMANTIC_BOUNDARY.md index 6f44d07..8a69f8f 100644 --- a/docs/standards/SCITT_SEMANTIC_BOUNDARY.md +++ b/docs/standards/SCITT_SEMANTIC_BOUNDARY.md @@ -47,8 +47,9 @@ VSTD numbered profiles can establish: code; - a bounded cost/memory/certificate-size ceiling and honest refusal when exceeded; - Graph lineage and blast-radius queries plus candidate degradation from statuses already - recorded in VSTD-Graph. Rating evidence and challenge-to-Graph propagation remain - `NOT_ESTABLISHED`. + recorded in VSTD-Graph. This SCITT example does not invoke the evidence-bound Graph + rating or challenge-projection mechanisms, so those results remain `NOT_ESTABLISHED` + for this example. The native solver, proof engine, signature checker, identity service, transparency log, or provenance system retains its own semantics and result. A loss-declared @@ -56,7 +57,8 @@ adapter maps that result into VSTD's verification interlingua and records the boundary around its portable composition; VSTD does not absorb or reimplement the substrate. -VSTD-5 and VSTD-Graph-5 remain draft. A later VSTD profile does not supply a missing +VSTD-5 and VSTD-Graph-5 have implemented evidence-bound reference paths, but this SCITT +example supplies neither a qualifying witness nor profile-5 rating evidence. A later VSTD profile does not supply a missing prerequisite coordinate. ## Identity, disclosure, trust, and reputation @@ -92,7 +94,7 @@ VSTD-Graph can preserve artifact history, challenges, lifecycle changes, and refutations, but the current standard does not define a scalar artifact-reputation score. The governing semantics instead distinguish TRUST, mechanism-earned artifact support; ROT, typed time-indexed degradation of current admissibility; and RUST, -inverse-TRUST diagnostic traversal toward recorded ancestors. Their unfinished transfer +inverse-TRUST diagnostic traversal toward recorded ancestors. Their reference transfer mechanisms must never overwrite a native verdict or turn actor identity, repeated registrations, signatures, observations, age, or reputation into process validity. diff --git a/docs/standards/VSTD_SCITT_CROSSWALK.md b/docs/standards/VSTD_SCITT_CROSSWALK.md index 89c85aa..9fad6c9 100644 --- a/docs/standards/VSTD_SCITT_CROSSWALK.md +++ b/docs/standards/VSTD_SCITT_CROSSWALK.md @@ -107,7 +107,7 @@ VSTD computational evidence. | Provenance graphs | VSTD-Graph records typed artifact/transformation lineage and computes candidate degradation from statuses already recorded in the Graph. | RFC 9943 correlates statements by subject; individual drafts propose object bindings and statement graphs. | Both can connect evidence about shared subjects. | SCITT core does not standardize the proposed statement-graph vocabulary; VSTD lineage is not causal proof. | Reference native SCITT statement IDs from VSTD-Graph without rewriting either graph. | | Statement graphs | VSTD-Graph has implemented graph structures, policy queries, and candidate-profile computation over caller-supplied ratings; conformance is `NOT_ESTABLISHED`. | Proposed by individual object-binding/composite drafts. | Both need explicit edge semantics and policy. | Maturity and graph objects differ. | Experimental bridge only; no claim of SCITT WG alignment. | | Dependencies | VSTD-4 can return `UNKNOWN/DEPENDENCY_UNAVAILABLE`; Graph evaluates transitive ancestors. | Composite draft proposes required statements and dependency edges. | Both surface unavailable dependencies. | SCITT core receipt validity does not settle application dependency completeness. | Preserve the native missing reason and let VSTD issue its own bounded indeterminacy certificate. | -| Revocation | The challenge ledger can derive claim state. Graph candidate computation degrades when an ancestor already records `REVOKED`. No adapter binds the first result into the second, so challenge-to-Graph propagation is `NOT_ESTABLISHED`. | RFC 9943 discusses compromised-key handling but leaves revocation strategies out of scope; individual composite draft proposes revocation statements/checks. | Both can react to invalidated evidence. | Neither SCITT core nor current VSTD supplies the missing cross-surface propagation mechanism. | Preserve each native state. A future adapter must bind the exact claim, artifact, event, and policy before a relying party changes Graph state. | +| Revocation | The challenge ledger derives append-only claim state. `AssuranceLedger.project_challenges` binds the complete record set into an additive Graph current-state view, and Graph recomputation degrades when a reached ancestor is `REVOKED`. | RFC 9943 discusses compromised-key handling but leaves revocation strategies out of scope; individual composite draft proposes revocation statements/checks. | Both can react to invalidated evidence. | VSTD's implemented projection consumes native VSTD challenge records, not SCITT revocation statements; SCITT-to-VSTD event mapping still requires an explicit application mechanism. | Preserve each native state. A SCITT application adapter must bind the exact statement, artifact, event, and policy before invoking the VSTD projection or another Graph status mechanism. | | Supersession | VSTD-Graph records `SUPERSEDED` without automatically making the older node inadmissible. | RFC 9943 permits later same-issuer/same-subject statements to supersede earlier ones; selection is relying-party policy. | Both preserve history. | Neither makes “newer†automatically “truerâ€; policy consequences differ. | Normalize `SUPERSEDED` without upgrading; require explicit current-evidence policy. | | Conflicts | VSTD preserves `CONFLICTED` where defined and graph blockers. | RFC 9943 allows conflicting issuers; individual composite draft proposes `conflict`. | Both refuse silent reconciliation. | SCITT core delegates issuer selection; VSTD may express a bounded conflict result. | Preserve `CONFLICTED` as distinct from UNKNOWN and FAIL. | | Freshness | VSTD bounds and evidence can include time/freshness; stale Graph artifacts are inadmissible to candidate Graph profiles. | Receipt state is true when issued; keys/policies can change; application policies determine freshness. SCRAPI can issue fresh receipts. | Both require time-indexed trust coordinates. | Inclusion is historical; it does not establish current payload validity. | Carry registration time, policy, key/VDS, and freshness decision separately. | diff --git a/examples/flagship_demo/specimens/honest-unknown.json b/examples/flagship_demo/specimens/honest-unknown.json index a978cd7..1fbfc7c 100644 --- a/examples/flagship_demo/specimens/honest-unknown.json +++ b/examples/flagship_demo/specimens/honest-unknown.json @@ -30,7 +30,7 @@ "format_fragment": "UP,WIDTH-K,RES", "implementation_hash": "sha256:a7d3c7b3123e9932395eff82f231c53e5b701888abbd203a47f155644cd10a64", "parser_hash": "sha256:c657e7777a850e584b7c010f8f7ff778b5269507db3eb0b2ac362a7cdac4cd67", - "specification_hash": "sha256:bce4666ddbcf73f460432990045b16e5197c3b03a1db5db096ba3220ae313205" + "specification_hash": "sha256:e1eb5bea41c03b0cfc58c9af5cc23d47b1128253c504c8d3674f87bf6da7ed52" } }, "certificate": { @@ -157,7 +157,7 @@ ] }, "header": { - "binding": "7a334822c49b2a8685e47b1dc05dabdead1ba980e3b907632cc9e0546db8b828", + "binding": "a249fa8e24aed0815a9787a6e96000fcba043d7e07fef11c4f9d64ff9f08bf86", "clause_count": 3, "format": "VSTD4-GDC-1", "literal_count": 4, diff --git a/examples/flagship_demo/specimens/inflated-tier.json b/examples/flagship_demo/specimens/inflated-tier.json index 3400606..2583be4 100644 --- a/examples/flagship_demo/specimens/inflated-tier.json +++ b/examples/flagship_demo/specimens/inflated-tier.json @@ -30,7 +30,7 @@ "format_fragment": "UP,WIDTH-K,RES", "implementation_hash": "sha256:a7d3c7b3123e9932395eff82f231c53e5b701888abbd203a47f155644cd10a64", "parser_hash": "sha256:c657e7777a850e584b7c010f8f7ff778b5269507db3eb0b2ac362a7cdac4cd67", - "specification_hash": "sha256:bce4666ddbcf73f460432990045b16e5197c3b03a1db5db096ba3220ae313205" + "specification_hash": "sha256:e1eb5bea41c03b0cfc58c9af5cc23d47b1128253c504c8d3674f87bf6da7ed52" } }, "certificate": { @@ -154,7 +154,7 @@ ] }, "header": { - "binding": "7a334822c49b2a8685e47b1dc05dabdead1ba980e3b907632cc9e0546db8b828", + "binding": "a249fa8e24aed0815a9787a6e96000fcba043d7e07fef11c4f9d64ff9f08bf86", "clause_count": 3, "format": "VSTD4-GDC-1", "literal_count": 4, diff --git a/examples/flagship_demo/specimens/poisoned-ancestor.json b/examples/flagship_demo/specimens/poisoned-ancestor.json index 258ac64..284af57 100644 --- a/examples/flagship_demo/specimens/poisoned-ancestor.json +++ b/examples/flagship_demo/specimens/poisoned-ancestor.json @@ -30,7 +30,7 @@ "format_fragment": "UP,WIDTH-K,RES", "implementation_hash": "sha256:a7d3c7b3123e9932395eff82f231c53e5b701888abbd203a47f155644cd10a64", "parser_hash": "sha256:c657e7777a850e584b7c010f8f7ff778b5269507db3eb0b2ac362a7cdac4cd67", - "specification_hash": "sha256:bce4666ddbcf73f460432990045b16e5197c3b03a1db5db096ba3220ae313205" + "specification_hash": "sha256:e1eb5bea41c03b0cfc58c9af5cc23d47b1128253c504c8d3674f87bf6da7ed52" } }, "collection": { @@ -65,7 +65,7 @@ "level": 0, "max_level": 5, "rating_basis": "CALLER_SUPPLIED", - "refutation_digest": "d3f0af5c1aba3e79079cd017f54bea0472f7e08d3b65601084eca220303f3f49", + "refutation_digest": "022ac984bda6a4e63f65ba8e294f1fabdecb1660e70fe6442cb4819a27434d84", "witness_digest": null }, "hypergraph": { @@ -592,7 +592,7 @@ ] }, "header": { - "binding": "2e395dbb80e179a5aa41bf847816635c633615a27e107bce8c67a039c4494c88", + "binding": "d17ad10464fab5b474fc36cc5ce9a9b2ba54a3aa081ccb0e8feceea68bf1d7b6", "clause_count": 17, "format": "VSTD4-GDC-1", "literal_count": 25, diff --git a/examples/flagship_demo/specimens/wrong-artifact.json b/examples/flagship_demo/specimens/wrong-artifact.json index e38c907..82d915f 100644 --- a/examples/flagship_demo/specimens/wrong-artifact.json +++ b/examples/flagship_demo/specimens/wrong-artifact.json @@ -30,7 +30,7 @@ "format_fragment": "UP,WIDTH-K,RES", "implementation_hash": "sha256:a7d3c7b3123e9932395eff82f231c53e5b701888abbd203a47f155644cd10a64", "parser_hash": "sha256:c657e7777a850e584b7c010f8f7ff778b5269507db3eb0b2ac362a7cdac4cd67", - "specification_hash": "sha256:bce4666ddbcf73f460432990045b16e5197c3b03a1db5db096ba3220ae313205" + "specification_hash": "sha256:e1eb5bea41c03b0cfc58c9af5cc23d47b1128253c504c8d3674f87bf6da7ed52" } }, "certificate": { @@ -162,7 +162,7 @@ ] }, "header": { - "binding": "7a334822c49b2a8685e47b1dc05dabdead1ba980e3b907632cc9e0546db8b828", + "binding": "a249fa8e24aed0815a9787a6e96000fcba043d7e07fef11c4f9d64ff9f08bf86", "clause_count": 3, "format": "VSTD4-GDC-1", "literal_count": 4, diff --git a/experiments/INDEX.md b/experiments/INDEX.md index 6639035..f2d5f6b 100644 --- a/experiments/INDEX.md +++ b/experiments/INDEX.md @@ -14,7 +14,7 @@ PYTHONPATH=src python scripts/build_experiment_index.py --check | Experiment | State | Question | Publication | Open horizons | Manifest | |---|---|---|---|---:|---| -| experiment-artifact-first-mechanisms | RUNNING | Which bounded event serialization, TRUST (mechanism-earned forward artifact support) transfer algebra, ROT (typed time-indexed current-admissibility degradation) derivation and propagation, RUST (inverse-TRUST diagnostic backtrace) concentration and localization rules, and hidden-witness trichotomy mechanisms can implement the governing artifact-first causal-provenance orientation without actor reputation, scalar cancellation, causal-localization overclaim, or making that orientation contingent on the study? | CANDIDATE | 6 | [`experiments/artifact_first_mechanisms/experiment.json`](artifact_first_mechanisms/experiment.json)
    `sha256:98f1aaf7d5d7c8fa3cea8e3f58ce93c50111fbd1e41bf5e49ead8276b438e9dc` | +| experiment-artifact-first-mechanisms | RUNNING | Which remaining domain-specific TRUST (mechanism-earned forward artifact support) transfer rules, ROT (typed time-indexed current-admissibility degradation) policies, RUST (inverse-TRUST diagnostic backtrace) localization mechanisms, hidden-witness trichotomy mechanisms, and independent implementations can extend the shipped reference event/dispatch substrate without actor reputation, scalar cancellation, causal-localization overclaim, or making the governing orientation contingent on the study? | CANDIDATE | 6 | [`experiments/artifact_first_mechanisms/experiment.json`](artifact_first_mechanisms/experiment.json)
    `sha256:1994591c49cccc8e99bc5233564b5b8eeed3810c6859f4e8c36f41d4797c4639` | | experiment-github-verdict-neutrality | COMPLETED | Does the GitHub adapter preserve successful workflow and merge states without converting them into a VSTD verdict? | INTERNAL | 1 | [`experiments/github_verdict_neutrality/experiment.json`](github_verdict_neutrality/experiment.json)
    `sha256:3b98310d35c20e7099d242e2c655e4bf8dc62d91298adc04e4dc2f56f2f79d89` | Platform events, including successful workflows and merges, retain diff --git a/experiments/artifact_first_mechanisms/experiment.json b/experiments/artifact_first_mechanisms/experiment.json index 82e2023..d1aed82 100644 --- a/experiments/artifact_first_mechanisms/experiment.json +++ b/experiments/artifact_first_mechanisms/experiment.json @@ -7,7 +7,7 @@ "experiment": { "id": "experiment-artifact-first-mechanisms", "title": "Mechanism completion under VSTD's governing ZIZK artifact-first architecture", - "question": "Which bounded event serialization, TRUST (mechanism-earned forward artifact support) transfer algebra, ROT (typed time-indexed current-admissibility degradation) derivation and propagation, RUST (inverse-TRUST diagnostic backtrace) concentration and localization rules, and hidden-witness trichotomy mechanisms can implement the governing artifact-first causal-provenance orientation without actor reputation, scalar cancellation, causal-localization overclaim, or making that orientation contingent on the study?", + "question": "Which remaining domain-specific TRUST (mechanism-earned forward artifact support) transfer rules, ROT (typed time-indexed current-admissibility degradation) policies, RUST (inverse-TRUST diagnostic backtrace) localization mechanisms, hidden-witness trichotomy mechanisms, and independent implementations can extend the shipped reference event/dispatch substrate without actor reputation, scalar cancellation, causal-localization overclaim, or making the governing orientation contingent on the study?", "state": "RUNNING", "started_at": "2026-08-23T00:00:00Z" }, @@ -66,8 +66,8 @@ "recorded_at": "2026-08-24T00:00:00Z", "artifact_id": "artifact-round2-design", "limitations": [ - "This experimental workflow manifest records only unfinished mechanisms and their evidence; it does not classify VSTD's governing ZIZK artifact-first architecture as experimental.", - "Round 2 is a design synthesis rather than a completed trustless protocol.", + "This experimental workflow manifest records remaining domain mechanisms and external evidence; it does not classify VSTD's governing ZIZK artifact-first architecture or the shipped reference event/dispatch substrate as experimental.", + "Round 2 remains a design synthesis for the domain-independent transfer algebra and external protocol, not a denial of the bounded reference implementation.", "The bounded identity-disclosure reference evaluator is semantic and does not itself provide cryptographic anonymity or unlinkability." ] }, @@ -335,8 +335,8 @@ "observation-zk-proof", "observation-identity-boundary" ], - "decision": "Treat standard/LADDER.md section 1.1 as the controlling semantic invariant and confine Round 2 to candidate operational mechanics for TRUST transfer, ROT derivation/propagation, and RUST backtrace/concentration.", - "reason": "The architecture is normative; only event serialization, TRUST-transfer algebra, ROT derivation and propagation, RUST concentration and localization, complete trichotomy derivation, and specific unfinished optional proof backends remain experimental.", + "decision": "Treat standard/LADDER.md section 1.1 and VSTD-GRAPH-ASSURANCE-1 as the controlling semantic and implemented reference boundaries; confine Round 2 to domain-specific TRUST transfer, ROT derivation/propagation, RUST backtrace/concentration and localization mechanisms, independent cross-implementation replay, and remaining proof research.", + "reason": "The architecture and bounded reference event/dispatch mechanisms are implemented; the complete domain-independent transfer algebra, complete trichotomy derivation, external independent replay, and specific unfinished optional proof backends remain experimental.", "action_ids": [ "action-reverification-synthesis" ], @@ -358,7 +358,7 @@ { "id": "amendment-actor-artifact-rust-correction", "recorded_at": "2026-08-25T00:00:00Z", - "reason": "Correct the object-only framing and distinguish the normative causal-provenance orientation from its experimental event, transfer, RUST, trichotomy, and optional proof-backend mechanisms: artifact support propagates ancestor-to-descendant, while RUST memetically backtraces descendant-to-ancestor without becoming actor reputation, scalar cancellation, causal localization, or guilt.", + "reason": "Correct the object-only framing and distinguish the normative causal-provenance orientation and implemented reference events from remaining domain transfer, localization, trichotomy, and optional proof-backend research: artifact support propagates ancestor-to-descendant, while RUST memetically backtraces descendant-to-ancestor without becoming actor reputation, scalar cancellation, causal localization, or guilt.", "supersedes": [ "amendment-round2-design" ], @@ -379,8 +379,8 @@ { "id": "horizon-operational-protocol", "status": "UNKNOWN", - "description": "End-to-end trustless reverification protocol", - "reason": "Round 2 records a design substrate; no complete operational protocol or independent implementation is included." + "description": "Independent end-to-end deployment of the reference reverification protocol", + "reason": "The repository ships a bounded operational reference substrate; no external independent implementation or deployment evidence is included." }, { "id": "horizon-anonymity", @@ -403,20 +403,20 @@ { "id": "horizon-rot-current-admissibility", "status": "UNKNOWN", - "description": "Typed ROT derivation and dependency-aware current-admissibility propagation", - "reason": "Some Graph queries enforce bounded lifecycle consequences, but no general ROT result, cross-surface binder, or propagation protocol is implemented." + "description": "Domain-specific ROT policies beyond the reference dispatcher", + "reason": "AssuranceLedger implements typed ROT and challenge-ledger projection without rewriting history; production lifecycle mechanisms and cross-implementation evidence remain unavailable." }, { "id": "horizon-rust-memetic-backtrace", "status": "UNKNOWN", - "description": "Typed backward RUST transfer, concentration, and localization over bound ancestry", - "reason": "The child-to-parent transfer states and concentration boundary are design-only; no event ledger, propagation implementation, or localization experiment is included." + "description": "Independent domain localization beyond structural RUST concentration", + "reason": "AssuranceLedger implements typed backward RUST, deduplicated structural concentration, and explicit localization dispatch; no external intervention mechanism or independent replay is included." }, { "id": "horizon-forward-artifact-trust", "status": "UNKNOWN", - "description": "Scoped forward TRUST transfer through developmental claim space", - "reason": "The design requires meet-like bounded support and child-local obligations, but no transfer algebra or implementation is included." + "description": "Complete domain-independent TRUST transfer algebra", + "reason": "AssuranceLedger implements evidence-bound forward TRUST over recorded ancestry; no universal algebra can replace the domain mechanism that checks each exact support proposition." } ], "publication": { @@ -432,5 +432,5 @@ }, "workflow_events": [], "interventions": [], - "manifest_digest": "sha256:98f1aaf7d5d7c8fa3cea8e3f58ce93c50111fbd1e41bf5e49ead8276b438e9dc" + "manifest_digest": "sha256:1994591c49cccc8e99bc5233564b5b8eeed3810c6859f4e8c36f41d4797c4639" } diff --git a/scripts/check_presentation.py b/scripts/check_presentation.py index 71ea31d..10f265b 100644 --- a/scripts/check_presentation.py +++ b/scripts/check_presentation.py @@ -106,20 +106,20 @@ "VSTD-1": "Implemented reference subset", "VSTD-2": "Implemented vertical slice", "VSTD-3": "Implemented reference surface", - "VSTD-4": "`NOT_ESTABLISHED`", - "VSTD-5": "Not implemented", + "VSTD-4": "Candidate path `NOT_ESTABLISHED`; evidence-bound path can establish conformance", + "VSTD-5": "Mechanism can establish a bounded result; a positive observation with unresolved independence remains overall `UNKNOWN`; no repository claim of a real independent witness", "VSTD-Graph-1": "Implemented reference subset", - "VSTD-Graph-2": "`NOT_ESTABLISHED`", - "VSTD-Graph-3": "`NOT_ESTABLISHED`", - "VSTD-Graph-4": "`NOT_ESTABLISHED`", - "VSTD-Graph-5": "`NOT_ESTABLISHED`", + "VSTD-Graph-2": "Candidate `NOT_ESTABLISHED`; evidence-bound profile 1–5 path can establish; profile zero cannot", + "VSTD-Graph-3": "Candidate `NOT_ESTABLISHED`; evidence-bound path can establish", + "VSTD-Graph-4": "Candidate `NOT_ESTABLISHED`; evidence-bound path can establish", + "VSTD-Graph-5": "Candidate `NOT_ESTABLISHED`; evidence-bound path can establish", "Generic run": "Implemented VSTD-1 profile", "Experimental workflow": "No VSTD conformance claim", "Supply Chain Integrity, Transparency, and Trust (SCITT) interoperability": ( "VSTD-4 remains `NOT_ESTABLISHED`" ), "zero-identity/zero-knowledge (ZIZK) artifact-first TRUST": ( - "Governing architectural invariant; not a separate VSTD conformance result" + "Implemented reference mechanism; no universal support score or actor trust" ), "RISC Zero proof-carrying reference mechanism": ( "Native proof verified; no VSTD receipt mapping" @@ -418,8 +418,8 @@ def check_claim_boundaries(errors: list[str]) -> None: errors.append(f"{relative} uses reputation-centric founder-maintained wording") if "`vstd` is the canonical cross-platform CLI name" not in wire: errors.append("WIRE_IDENTIFIERS.md does not preserve the CLI compatibility rule") - if "VSTD-4 CANDIDATE; CONFORMANCE NOT_ESTABLISHED" not in reference: - errors.append("generated reference does not bound its VSTD-4 implementation status") + if "VSTD-5 PROJECT SPECIFICATION; EVIDENCE-BOUND REFERENCE MECHANISM" not in reference: + errors.append("generated reference does not report its VSTD-5 mechanism status") if "reproducible COSE specimen" in changelog or "reproducible specimen" in roadmap: errors.append("SCITT ephemeral-key specimen is described as byte-reproducible") if "ephemeral-key COSE artifacts" not in scitt_demo: @@ -505,13 +505,13 @@ def check_visual_assets(errors: list[str]) -> None: "vstd-1": "REF. SUBSET", "vstd-2": "EXPERIMENTAL", "vstd-3": "IMPLEMENTED", - "vstd-4": "CANDIDATE", - "vstd-5": "DRAFT", + "vstd-4": "REF. MECH.", + "vstd-5": "REF. MECH.", "graph-1": "REF. SUBSET", - "graph-2": "CANDIDATE", - "graph-3": "CANDIDATE", - "graph-4": "CANDIDATE", - "graph-5": "DRAFT", + "graph-2": "REF. MECH.", + "graph-3": "REF. MECH.", + "graph-4": "REF. MECH.", + "graph-5": "REF. MECH.", } observed_status = { element.attrib["data-profile"]: "".join(element.itertext()).strip() diff --git a/tests/test_presentation_surface.py b/tests/test_presentation_surface.py index d250660..0f3728f 100644 --- a/tests/test_presentation_surface.py +++ b/tests/test_presentation_surface.py @@ -536,9 +536,12 @@ def test_generated_reference_covers_commands_and_top_level_exports() -> None: assert anchor in page, f"reference page omits {command['prog']}" for name in verifier.__all__: assert f'id="api-{name}"' in page, f"reference page omits export {name}" - assert verifier.__standard__ == "VSTD-4" - assert verifier.__standard_status__ == "CANDIDATE; CONFORMANCE NOT_ESTABLISHED" - assert "VSTD-4 CANDIDATE; CONFORMANCE NOT_ESTABLISHED" in page + assert verifier.__standard__ == "VSTD-5" + assert ( + verifier.__standard_status__ + == "PROJECT SPECIFICATION; EVIDENCE-BOUND REFERENCE MECHANISM" + ) + assert "VSTD-5 PROJECT SPECIFICATION; EVIDENCE-BOUND REFERENCE MECHANISM" in page assert "Monotone reproduction-fidelity states" in page From 2eba5bdee2103aea0bd2ad8f39d81cb1f0eb3eca Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 06:25:56 -0700 Subject: [PATCH 16/34] fix: stabilize enum reference text on Python 3.10 Reason: Python 3.10 supplies the inherited Enum documentation string `An enumeration.` for an undocumented str/Enum class, while later interpreters expose a different inherited string. That made the generated API reference interpreter-dependent. Evidence: MechanismOutcome now declares its own exact documentation string. The regression assertion binds that declaration, and Python 3.10 now passes the generated-reference and full presentation checks locally. Forty-four adjacent assurance and presentation tests pass after the repair. Coordinate: Parent ca2b9748b639d12cbdbd960d9acec2054a6c9572 on codex/post-1.2-professionalization for PR #27; hosted run 33254844371 exposed the defect only in base Python 3.10. Falsification: Running scripts/build_reference.py --check or scripts/check_presentation.py under Python 3.10 must reproduce the committed docs/reference.html exactly. Any inherited interpreter-specific Enum summary is a failure. Compatibility/wire impact: No receipt, schema, runtime verdict, or public callable changes. This adds an explicit class docstring and a regression assertion so generated documentation is stable across the advertised Python 3.10 through 3.13 range. --- src/verifier/core/evidence.py | 2 ++ tests/test_evidence_bound_assurance.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/verifier/core/evidence.py b/src/verifier/core/evidence.py index 2d4c297..fcafcd7 100644 --- a/src/verifier/core/evidence.py +++ b/src/verifier/core/evidence.py @@ -40,6 +40,8 @@ class EvidenceBindingError(ValueError): class MechanismOutcome(str, Enum): + """Enumeration of the exported result values.""" + PASS = "PASS" FAIL = "FAIL" UNKNOWN = "UNKNOWN" diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py index 80a3650..292cdb7 100644 --- a/tests/test_evidence_bound_assurance.py +++ b/tests/test_evidence_bound_assurance.py @@ -259,6 +259,7 @@ def _graph_rating_evidence(graph, store, binding, *, members=("result",), rating def test_serialized_pass_is_not_an_input_to_evidence_evaluation() -> None: + assert MechanismOutcome.__doc__ == "Enumeration of the exported result values." store, session = _session() proposition = _proposition(store, "a", "p", True) assert not hasattr(proposition, "outcome") From 24bdba9d72fb5e6269ebaa153ce61e5915cbcd27 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 07:52:10 -0700 Subject: [PATCH 17/34] Bind TRUST dependencies and witness independence Reason: Close two semantic-review blockers and make BLAME/GUILT's distinction mechanism-bound rather than field-derived. Evidence: The prior runtime retained descendant TRUST after an intermediate artifact was revoked, and duplicate witness identity could still yield computed_independence=INDEPENDENT. Both counterexamples were reproduced at 2eba5bd before this change. Coordinate: VSTD-GRAPH-ASSURANCE-1 TRUST/conflict events; unreleased VSTD-5 witness result; artifact-relative diagnostic attribution. Falsification: Reject the change if TRUST can skip a transformation, remain current after a required artifact/edge degrades or conflicts, or if duplicate/missing/reused witness identity or independence assertions can yield INDEPENDENT. Reject GUILT if its exact violated obligation is not part of the mechanism-checked proposition. Compatibility and wire impact: This intentionally revises unreleased VSTD-GRAPH-ASSURANCE-1 and VSTD-5 shapes and the unreleased record_trust call surface before 1.2.0. No released receipt identifier is silently redefined. Historical Graph bytes and recorded events remain immutable; current eligibility is derived additively. --- CHANGELOG.md | 19 +- HUMANS.md | 4 +- README.md | 8 +- docs/ARCHITECTURE.md | 18 +- docs/index.html | 4 +- docs/reference.html | 5 +- receipts/schema/vstd5_receipt.json | 29 +- src/verifier/core/witness.py | 114 +++-- src/verifier/data/assurance.py | 279 ++++++++++- src/verifier/specifications/LADDER.md | 28 +- standard/LADDER.md | 28 +- .../vstd-graph-assurance-1.schema.json | 45 +- tests/test_evidence_bound_assurance.py | 454 +++++++++++++++++- 13 files changed, 924 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ed7447..7034ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,20 +102,25 @@ fourteen-rung mechanisms plus an accepted kernel witness admit VSTD-5. - Implement the VSTD-5 reference mechanism and receipt: seven evidence-bound separation dimensions, duplicate-witness/evidence refusal, exact admitted-certificate and - corroboration binding, - disagreement preservation, embedded evidence, and offline result recheck. This does not + corroboration binding, typed binding/identity/separation/corroboration errors, + disagreement preservation, embedded evidence, and offline result recheck. Independence + fails closed on any identity or separation defect without parsing error-message text. This does not claim a real external witness or independent implementation; a positive observation with unresolved independence is overall `UNKNOWN`. - Add evidence-bound Graph profile computation and replay. The compatibility `graph_level` path remains caller-supplied; the new path reruns every member, ancestor, and reached-edge rating mechanism bound to the exact Graph, members, collection, and claim before profile 1–5 can report `ESTABLISHED`. Profile zero remains `NOT_ESTABLISHED`. -- Add `VSTD-GRAPH-ASSURANCE-1` and `AssuranceLedger` for hash-chained TRUST, ROT, RUST, - challenge-ledger projection, additive conflict resolution, structural RUST concentration, +- Add `VSTD-GRAPH-ASSURANCE-1` and `AssuranceLedger` for hash-chained edge-local TRUST, ROT, RUST, + challenge-ledger projection, additive conflict declaration/resolution, structural RUST concentration, explicit causal localization, and bounded artifact-relative BLAME/GUILT propositions. - Duplicate paths remain set-valued, historical graph bytes remain immutable, and topology - alone earns no causal or moral conclusion. Add complete offline event replay, current TRUST - filtering, and deduplicated descendant reassessment discovery. + Each TRUST event binds one exact transformation, its inputs/output, the historical Graph, + and prerequisite TRUST events; current eligibility recursively fails closed when any bound + dependency degrades or conflicts. Duplicate paths remain set-valued, historical graph bytes + remain immutable, and topology alone earns no causal or moral conclusion. BLAME establishes + bounded responsibility or material contribution; GUILT additionally binds an exact violated + obligation and is not BLAME in the opposite direction. Add complete offline event replay, + current TRUST filtering, and deduplicated descendant reassessment discovery. - Preserve incompatible Graph assertions as evidence-linked conflict records and label rating-derived Graph profile numbers as `CALLER_SUPPLIED` candidates with conformance `NOT_ESTABLISHED`. diff --git a/HUMANS.md b/HUMANS.md index 71a5660..e9f3210 100644 --- a/HUMANS.md +++ b/HUMANS.md @@ -52,7 +52,9 @@ scores, serialized receipt values, or references to the Rust programming languag The reference `AssuranceLedger` records these as additive Graph events. Treat structural RUST concentration as a triage count of unique deviating descendants, never causal strength. A bounded artifact-relative `BLAME` or `GUILT` result exists only after separate -localization and attribution mechanisms pass; it never evaluates an actor's character. +localization and attribution mechanisms pass. BLAME establishes responsibility or material +contribution for the exact deviation; GUILT is the stronger obligation-relative result and +is not BLAME flowing in the opposite direction. Neither evaluates an actor's character. For reliance, replay the portable log with `recheck_assurance_log`: a stored event word or hash chain without successful evidence rehash and mechanism execution is not current assurance. When upstream status changes, inspect `current_trust_events` and the deduplicated diff --git a/README.md b/README.md index c506464..f7cfe51 100644 --- a/README.md +++ b/README.md @@ -170,14 +170,18 @@ member set, collection, and claim. Profile zero remains `NOT_ESTABLISHED`. The formal names **TRUST**, **ROT**, and **RUST** are semantic terms, not acronyms, actor ratings, scalar scores, numbered-profile verdicts, or references to the Rust programming language. They serialize only as typed events in `VSTD-GRAPH-ASSURANCE-1`. -TRUST is mechanism-earned artifact support moving forward through checked development; +TRUST is mechanism-earned artifact support moving forward edge by edge through checked +development, with each event binding one exact transformation, its inputs and output, the +historical Graph digest, and any prerequisite TRUST events; ROT is typed, time-indexed degradation of current admissibility without rewriting historical evidence; RUST is the inverse-TRUST diagnostic mechanic moving backward from a descendant deviation toward recorded ancestor candidates. This memetic propagation does not by itself prove guilt, responsibility, falsehood, causal localization, or automatic ancestor falsification. The reference runtime requires a separate passing localization mechanism and an exact artifact-relative diagnostic proposition before it can emit bounded -`BLAME` or `GUILT`; those results never concern actor character. See [the governing +`BLAME` or `GUILT`. BLAME establishes bounded responsibility or material contribution; +GUILT is not its opposite, but the stronger combined result that additionally establishes +an exact violated obligation. Neither result concerns actor character. See [the governing architecture](standard/LADDER.md#11-artifact-first-causal-provenance-orientation). `VSTD-GRAPH-ASSURANCE-1` carries the immutable historical Graph, exact event bindings, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b23172a..033adad 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -159,7 +159,7 @@ Maturity attaches to mechanisms beneath that architecture: | RISC Zero hidden-witness predicate | Bounded reference mechanism with tracked public proof artifacts | `examples/zizk_artifact_first/risc0/`; native verification only, no VSTD receipt mapping | | Bounded identity-disclosure evaluator | Bounded non-normative reference mechanism | `examples/zizk_artifact_first/zero_identity/`; no identity-derived trust | | Assurance event serialization and replay | Implemented bounded reference mechanism | `VSTD-GRAPH-ASSURANCE-1` embeds the historical Graph, exact bindings, evidence bytes, a hash chain, and a current-view digest; `recheck_assurance_log` reruns every event mechanism | -| TRUST transfer | Implemented proposition-dispatch reference mechanism | `record_trust` accepts only recorded ancestors and an exact passing support proposition; current lifecycle or conflict state can invalidate the event for current use without deleting history. No universal scalar support algebra exists. | +| TRUST transfer | Implemented edge-local proposition-dispatch reference mechanism | `record_trust` binds one exact transformation, its complete inputs and output, the historical Graph digest, and the prerequisite TRUST event for every derived input. Recursive current-admissibility checking excludes the route if any required event, artifact, or transformation degrades or conflicts, without deleting history. No universal scalar support algebra exists. | | ROT derivation and cross-surface propagation | Implemented bounded reference mechanisms | Strictly degrading status propositions and complete challenge-ledger projections produce additive current-state overlays; the deduplicated descendant impact set is discovery, and a descendant status change still needs its own mechanism | | RUST concentration and localization | Implemented bounded reference mechanisms | A passing descendant-deviation proposition produces deduplicated reverse reachability; concentration counts unique descendants; localization and artifact-relative diagnostic attribution require separate passing propositions | | Complete `PASS`/`FAIL`/`UNKNOWN`/`CONFLICTED` hidden-witness derivation | Experimental and unimplemented | A caller-supplied state tag is not an earned verdict | @@ -270,7 +270,9 @@ they describe memetic causal-provenance and lifecycle behavior over one developm They do not establish actor standing, moral character, responsibility, or automatic ancestor falsification. The reference runtime emits bounded TRUST, ROT, and RUST events only after their exact mechanisms run. Causal localization and artifact-relative `BLAME` -or `GUILT` require additional exact propositions and never become actor reputation. +or `GUILT` require additional exact propositions. BLAME establishes bounded responsibility +or material contribution; GUILT is not directionally opposite, but additionally binds and +checks an exact violated obligation. Neither becomes actor reputation. ### Recursive-amplification falsification outcomes @@ -299,7 +301,9 @@ evidence-reference strings cannot self-promote that result. The generic-run comp path treats supplied assertions as no stronger than `DECLARED`, rejects receipts that label them `EVIDENCED`, and never derives `EVIDENCED`. The distinct VSTD-5 reference path can establish only its exact declarant/witness separation propositions after all seven seams are -rerun by registered mechanisms; it does not upgrade the legacy generic-run fields. +rerun by registered mechanisms. Typed binding, identity, separation, and corroboration +errors keep `computed_independence` fail-closed; no error-message text is interpreted as a +semantic category. This path does not upgrade the legacy generic-run fields. Graph conflict records retain incompatible values and their evidence references without adding a scalar score or changing the frozen artifact-status vocabulary. A conflict makes @@ -314,16 +318,16 @@ question is a new assessment over the retained graph and applicable lifecycle re |---|---| | An ancestor is `CHALLENGED`, `REVOKED`, or `STALE` | Candidate/evidence-bound Graph recomputation follows the full ancestor closure and returns compatibility field `level = 0`. `AssuranceLedger` also records typed ROT or projects append-only challenge-ledger state into a derived current view; historical Graph bytes remain unchanged. | | An ancestor is `SUPERSEDED` | The historical Graph candidate remains admissible by design; the stricter all-ancestors-`VALID` policy rejects it for current-use admission. Supersession does not retroactively falsify its prior lineage role. | -| Upstream evidence conflicts | A retained `ConflictRecord` blocks a clean result. `resolve_conflict` accepts only a mechanism-passing proposition bound to the exact conflict and one retained competing value; the original conflict remains in history while the derived current view records the additive resolution. | +| Upstream evidence conflicts | A retained `ConflictRecord` or a mechanism-established `CONFLICT_DECLARATION` event blocks every dependent edge-local TRUST route. `resolve_conflict` accepts only a mechanism-passing proposition bound to the exact conflict and one retained competing value; the original conflict remains in history while the derived current view records the additive resolution. | | Evidence arrives by multiple paths or one run receipt repeats a reference | Reachability and impact sets deduplicate identifiers. Multiplicity supplies no independence or strength. | | A descendant deviation points toward shared ancestors | A mechanism-passing deviation emits RUST over the deduplicated recorded ancestor set. Structural concentration counts unique deviating descendants, not paths or causal strength. Localization, BLAME, and GUILT require separate exact mechanisms. | -| A challenge ledger changes a claim's current status | `project_challenges` binds its complete append-only records into a current Graph overlay and embeds those records for replay. Existing TRUST remains historical but disappears from `current_trust_events`; `impacted_descendants` reports the deduplicated reassessment surface. It never mutates the historical graph. | +| A challenge ledger changes a claim's current status | `project_challenges` binds its complete append-only records into a current Graph overlay and embeds those records for replay. Existing TRUST remains historical, while recursively dependent events disappear from `current_trust_events`; `impacted_descendants` reports the deduplicated reassessment surface. It never mutates the historical graph. | | Later evidence clears a conflict | The additive resolution retains the original competing evidence and its mechanism evaluation. Removing or rewriting the conflict remains invalid. | | Candidate calculation encounters cyclic ancestry | Rejected before candidate calculation; recursive topology cannot manufacture assurance. | The forward blast-radius query remains discovery only. `AssuranceLedger` is the distinct -binding mechanism for explicit TRUST, ROT, RUST, challenge projection, and conflict -resolution. `recheck_assurance_log` reconstructs the historical Graph, rehashes every +binding mechanism for explicit edge-local TRUST, ROT, RUST, challenge projection, and +conflict declaration/resolution. `recheck_assurance_log` reconstructs the historical Graph, rehashes every embedded evidence item, reruns the exact registered mechanisms, reproduces the event chain, and compares the derived current view. The ledger never infers an unrecorded edge, converts topology into assurance, or treats RUST as causality. Domain-specific transfer and diff --git a/docs/index.html b/docs/index.html index 2265d73..09640bd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -80,9 +80,9 @@

    A green check is not a complete explanation.

    Verify the process, not the actor.

    VSTD evaluates bounded validity propositions about computational processes represented by software and evidence-bearing artifacts. Identity, popularity, and reputation alone add no verdict weight. TRUST, ROT, and RUST are formal semantic names, not acronyms or actor ratings; the reference Graph assurance log records them as evidence-bound events and can replay their embedded evidence and mechanisms offline. Architectural zero knowledge presumes no unevidenced proposition; cryptographic zero knowledge can enclose a confidential witness only through a named proof system bound to the exact program and predicate.

    -
    TRUST · FORWARD

    Mechanism-earned support

    Exact artifact support may move through a checked transformation. It is never a rating of whether an actor is good or trustworthy.

    +
    TRUST · FORWARD

    Mechanism-earned support

    Exact artifact support moves edge by edge through a checked transformation bound to its inputs, output, Graph, and prerequisite support. It is never an actor rating.

    ROT · CURRENT STATE

    Admissibility degrades

    Typed lifecycle evidence can require reassessment without rewriting an immutable historical result. Age alone is insufficient.

    -
    RUST · BACKWARD

    Diagnostic ancestry

    A checked descendant deviation traces over deduplicated recorded ancestors. Localization and artifact-relative BLAME or GUILT require additional exact mechanisms; reachability alone earns none of them.

    +
    RUST · BACKWARD

    Diagnostic ancestry

    A checked descendant deviation traces over deduplicated recorded ancestors. BLAME requires localized responsibility; GUILT additionally requires an exact violated obligation. Neither follows from reachability alone.

    diff --git a/docs/reference.html b/docs/reference.html index 366fa1f..302483c 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -433,17 +433,18 @@

    AssuranceLedger class

    Defined in verifier.data.assurance

    - + + - + diff --git a/receipts/schema/vstd5_receipt.json b/receipts/schema/vstd5_receipt.json index b1847e4..117daf5 100644 --- a/receipts/schema/vstd5_receipt.json +++ b/receipts/schema/vstd5_receipt.json @@ -91,7 +91,7 @@ "evaluation": {"$ref": "https://timelordraps.github.io/verifier/schemas/vstd-graph-assurance-1.schema.json#/$defs/evaluation"}, "result": { "type": "object", "additionalProperties": false, - "required": ["claim_id", "status", "conformance_status", "computed_independence", "independence_evaluations", "corroboration_evaluations", "disagreements", "errors", "limitations"], + "required": ["claim_id", "status", "conformance_status", "computed_independence", "independence_evaluations", "corroboration_evaluations", "disagreements", "binding_errors", "identity_errors", "separation_errors", "corroboration_errors", "errors", "limitations"], "properties": { "claim_id": {"type": "string", "minLength": 1}, "status": {"enum": ["CORROBORATED", "REFUTED", "UNKNOWN", "CONFLICTED"]}, @@ -106,6 +106,10 @@ "items": {"type": "object", "additionalProperties": false, "required": ["corroboration_id", "evaluation"], "properties": {"corroboration_id": {"type": "string"}, "evaluation": {"$ref": "#/$defs/evaluation"}}} }, "disagreements": {"type": "array", "items": {"type": "array", "minItems": 2, "uniqueItems": true, "items": {"type": "string"}}}, + "binding_errors": {"type": "array", "items": {"type": "string"}}, + "identity_errors": {"type": "array", "items": {"type": "string"}}, + "separation_errors": {"type": "array", "items": {"type": "string"}}, + "corroboration_errors": {"type": "array", "items": {"type": "string"}}, "errors": {"type": "array", "items": {"type": "string"}}, "limitations": {"type": "array", "items": {"type": "string"}} }, @@ -118,6 +122,29 @@ "computed_independence": {"const": "INDEPENDENT"} } } + }, + { + "if": {"properties": {"computed_independence": {"const": "INDEPENDENT"}}, "required": ["computed_independence"]}, + "then": { + "properties": { + "binding_errors": {"maxItems": 0}, + "identity_errors": {"maxItems": 0}, + "separation_errors": {"maxItems": 0} + } + } + }, + { + "if": {"properties": {"conformance_status": {"const": "ESTABLISHED"}}, "required": ["conformance_status"]}, + "then": { + "properties": { + "computed_independence": {"const": "INDEPENDENT"}, + "binding_errors": {"maxItems": 0}, + "identity_errors": {"maxItems": 0}, + "separation_errors": {"maxItems": 0}, + "corroboration_errors": {"maxItems": 0}, + "errors": {"maxItems": 0} + } + } } ] } diff --git a/src/verifier/core/witness.py b/src/verifier/core/witness.py index 72450e8..f4cac2e 100644 --- a/src/verifier/core/witness.py +++ b/src/verifier/core/witness.py @@ -215,13 +215,26 @@ class WitnessCorroborationResult: ] corroboration_evaluations: tuple[tuple[str, EvaluatedProposition], ...] disagreements: tuple[tuple[str, ...], ...] - errors: tuple[str, ...] + binding_errors: tuple[str, ...] + identity_errors: tuple[str, ...] + separation_errors: tuple[str, ...] + corroboration_errors: tuple[str, ...] limitations: tuple[str, ...] = field(default_factory=tuple) @property def established(self) -> bool: return self.conformance_status == "ESTABLISHED" + @property + def errors(self) -> tuple[str, ...]: + """Return all errors without using message text as decision input.""" + return ( + *self.binding_errors, + *self.identity_errors, + *self.separation_errors, + *self.corroboration_errors, + ) + def to_dict(self) -> dict[str, Any]: return { "claim_id": self.claim_id, @@ -244,6 +257,10 @@ def to_dict(self) -> dict[str, Any]: for record_id, evaluation in self.corroboration_evaluations ], "disagreements": [list(group) for group in self.disagreements], + "binding_errors": list(self.binding_errors), + "identity_errors": list(self.identity_errors), + "separation_errors": list(self.separation_errors), + "corroboration_errors": list(self.corroboration_errors), "errors": list(self.errors), "limitations": list(self.limitations), } @@ -258,35 +275,46 @@ def assess_witness_corroboration( """Recheck VSTD-5 entry, separation evidence, and corroboration evidence.""" require_vstd5_entry(entry) - errors: list[str] = [] + binding_errors: list[str] = [] + identity_errors: list[str] = [] + separation_errors: list[str] = [] + corroboration_errors: list[str] = [] independence_results: list[tuple[str, str, EvaluatedProposition]] = [] corroboration_results: list[tuple[str, EvaluatedProposition]] = [] - if bundle.claim_id == "" or bundle.declarant_id == "": - errors.append("claim_id and declarant_id must not be empty") + if bundle.claim_id == "": + binding_errors.append("claim_id must not be empty") + if bundle.declarant_id == "": + identity_errors.append("declarant_id must not be empty") if bundle.claim_binding_digest != entry.witness.header.binding: # type: ignore[union-attr] - errors.append("witness bundle does not bind the admitted VSTD-4 commitment") + binding_errors.append( + "witness bundle does not bind the admitted VSTD-4 commitment" + ) identities: dict[str, WitnessIdentity] = {} identity_refs: set[str] = set() for witness in bundle.witnesses: if not witness.witness_id: - errors.append("witness_id must not be empty") + identity_errors.append("witness_id must not be empty") continue if witness.witness_id == bundle.declarant_id: - errors.append(f"witness {witness.witness_id} is the declarant") + identity_errors.append(f"witness {witness.witness_id} is the declarant") if witness.witness_id in identities: - errors.append(f"duplicate witness identifier: {witness.witness_id}") + identity_errors.append( + f"duplicate witness identifier: {witness.witness_id}" + ) identities[witness.witness_id] = witness try: identity_ref = session.evidence.add( session.evidence.resolve(witness.identity_evidence_ref) ) except Exception as exc: - errors.append(f"witness {witness.witness_id} identity evidence unavailable: {exc}") + identity_errors.append( + f"witness {witness.witness_id} identity evidence unavailable: {exc}" + ) continue if identity_ref in identity_refs: - errors.append( + identity_errors.append( f"witness {witness.witness_id} repeats another witness identity evidence" ) identity_refs.add(identity_ref) @@ -294,29 +322,31 @@ def assess_witness_corroboration( assertions: dict[str, IndependenceAssertion] = {} for assertion in bundle.independence: if assertion.witness_id in assertions: - errors.append(f"duplicate independence assertion: {assertion.witness_id}") + separation_errors.append( + f"duplicate independence assertion: {assertion.witness_id}" + ) continue assertions[assertion.witness_id] = assertion if assertion.witness_id not in identities: - errors.append( + separation_errors.append( f"independence assertion references missing witness {assertion.witness_id}" ) continue for dimension in IndependenceDimension: state = assertion.relationships.get(dimension, RelationshipState.UNKNOWN) if state is RelationshipState.SHARED: - errors.append( + separation_errors.append( f"witness {assertion.witness_id} shares {dimension.value} with declarant" ) continue if state is RelationshipState.UNKNOWN: - errors.append( + separation_errors.append( f"witness {assertion.witness_id} has UNKNOWN {dimension.value} separation" ) continue proposition = assertion.evidence.get(dimension) if proposition is None: - errors.append( + separation_errors.append( f"witness {assertion.witness_id} has no evidence for {dimension.value}" ) continue @@ -329,7 +359,7 @@ def assess_witness_corroboration( or proposition.parameters.get("claim_binding_digest") != bundle.claim_binding_digest ): - errors.append( + separation_errors.append( f"witness {assertion.witness_id} {dimension.value} evidence is not " "bound to the exact negative separation proposition" ) @@ -339,46 +369,50 @@ def assess_witness_corroboration( (assertion.witness_id, dimension.value, result) ) if not result.passed: - errors.append( + separation_errors.append( f"witness {assertion.witness_id} {dimension.value} separation " f"was not established: {result.outcome.value}" ) for witness_id in identities: if witness_id not in assertions: - errors.append(f"witness {witness_id} has no independence assertion") + separation_errors.append( + f"witness {witness_id} has no independence assertion" + ) seen_records: set[str] = set() observed_sets: set[tuple[str, ...]] = set() accepted_outcomes: list[tuple[str, CorroborationOutcome]] = [] for record in bundle.corroborations: if record.corroboration_id in seen_records: - errors.append(f"duplicate corroboration identifier: {record.corroboration_id}") + corroboration_errors.append( + f"duplicate corroboration identifier: {record.corroboration_id}" + ) continue seen_records.add(record.corroboration_id) if record.witness_id not in identities: - errors.append( + corroboration_errors.append( f"corroboration {record.corroboration_id} references missing witness" ) continue if record.claim_binding_digest != bundle.claim_binding_digest: - errors.append( + corroboration_errors.append( f"corroboration {record.corroboration_id} binds a neighboring claim" ) continue if record.vstd4_certificate_digest.removeprefix("sha256:") != entry.witness.digest(): # type: ignore[union-attr] - errors.append( + corroboration_errors.append( f"corroboration {record.corroboration_id} binds a different VSTD-4 certificate" ) continue unique_observations = tuple(sorted(set(record.observed_evidence_refs))) if len(unique_observations) != len(record.observed_evidence_refs): - errors.append( + corroboration_errors.append( f"corroboration {record.corroboration_id} repeats evidence references" ) continue if unique_observations in observed_sets: - errors.append( + corroboration_errors.append( f"corroboration {record.corroboration_id} duplicates another evidence set" ) continue @@ -398,7 +432,7 @@ def assess_witness_corroboration( or proposition.parameters.get("witness_id") != record.witness_id or proposition.parameters.get("observed_at") != record.observed_at ): - errors.append( + corroboration_errors.append( f"corroboration {record.corroboration_id} is not exactly bound" ) continue @@ -407,19 +441,21 @@ def assess_witness_corroboration( if result.outcome is MechanismOutcome.PASS: accepted_outcomes.append((record.corroboration_id, record.result)) else: - errors.append( + corroboration_errors.append( f"corroboration {record.corroboration_id} mechanism did not pass: " f"{result.outcome.value}" ) if not bundle.witnesses: - errors.append("at least one witness is required") + identity_errors.append("at least one witness is required") if not bundle.corroborations: - errors.append("at least one corroboration is required") + corroboration_errors.append("at least one corroboration is required") corroborating_witnesses = {record.witness_id for record in bundle.corroborations} for witness_id in identities: if witness_id not in corroborating_witnesses: - errors.append(f"witness {witness_id} has no corroboration record") + corroboration_errors.append( + f"witness {witness_id} has no corroboration record" + ) outcome_groups = { outcome: tuple(record_id for record_id, item in accepted_outcomes if item is outcome) @@ -439,14 +475,23 @@ def assess_witness_corroboration( expected_independence_checks = len(identities) * len(IndependenceDimension) independence_established = ( - len(independence_results) == expected_independence_checks + bool(identities) + and len(bundle.witnesses) == len(identities) + and len(assertions) == len(identities) + and len(independence_results) == expected_independence_checks and all(result.passed for _, _, result in independence_results) - and not any("separation" in error or "shares" in error for error in errors) + and not binding_errors + and not identity_errors + and not separation_errors ) computed_independence = "INDEPENDENT" if independence_established else "UNKNOWN" conformance = ( "ESTABLISHED" - if not errors and independence_established and len(corroboration_results) > 0 + if ( + independence_established + and not corroboration_errors + and len(corroboration_results) > 0 + ) else "NOT_ESTABLISHED" ) if status is WitnessResultStatus.CORROBORATED and conformance != "ESTABLISHED": @@ -459,7 +504,10 @@ def assess_witness_corroboration( tuple(independence_results), tuple(corroboration_results), disagreements, - tuple(errors), + tuple(binding_errors), + tuple(identity_errors), + tuple(separation_errors), + tuple(corroboration_errors), ( "Identity evidence identifies the witness coordinate; it does not confer trust.", "The result is bounded to the registered mechanisms, trust roots, evidence, and bounds.", diff --git a/src/verifier/data/assurance.py b/src/verifier/data/assurance.py index 5455594..9aa400d 100644 --- a/src/verifier/data/assurance.py +++ b/src/verifier/data/assurance.py @@ -53,6 +53,7 @@ class AssuranceEventKind(str, Enum): ROT = "ROT" RUST = "RUST" STATUS_PROJECTION = "STATUS_PROJECTION" + CONFLICT_DECLARATION = "CONFLICT_DECLARATION" CONFLICT_RESOLUTION = "CONFLICT_RESOLUTION" CAUSAL_LOCALIZATION = "CAUSAL_LOCALIZATION" DIAGNOSTIC_ATTRIBUTION = "DIAGNOSTIC_ATTRIBUTION" @@ -294,9 +295,10 @@ def __init__(self, graph: ProvenanceHypergraph) -> None: raise AssuranceFlowError( "cyclic provenance cannot carry recursive assurance propagation" ) - self.graph = graph - self._graph_digest = canonical_digest(graph.to_dict()) + self.graph = ProvenanceHypergraph.from_dict(graph.to_dict()) + self._graph_digest = canonical_digest(self.graph.to_dict()) self._events: list[AssuranceEvent] = [] + self._conflicts: dict[str, ConflictRecord] = dict(self.graph.conflicts) self._resolutions: dict[str, ConflictResolution] = {} @property @@ -395,33 +397,119 @@ def impacted_descendants(self, artifact_id: str) -> tuple[str, ...]: return tuple(sorted(self.graph.descendants((artifact_id,)) - {artifact_id})) def current_trust_events(self) -> tuple[AssuranceEvent, ...]: - """Return passing TRUST records still admissible under the current view.""" + """Return recursively current edge-local TRUST records.""" + if canonical_digest(self.graph.to_dict()) != self.graph_digest: + return () unresolved_subjects = {item.subject_id for item in self.unresolved_conflicts()} - return tuple( - event + trust_events = { + event.digest(): event for event in self._events if event.kind is AssuranceEventKind.TRUST - and event.outcome is MechanismOutcome.PASS - and event.subject_id not in unresolved_subjects - and all(source not in unresolved_subjects for source in event.source_ids) - and self.current_status(event.subject_id) is ArtifactStatus.VALID - and all( - self.current_status(source) is ArtifactStatus.VALID - for source in event.source_ids + } + memo: dict[str, bool] = {} + + def is_current(event_digest: str, visiting: set[str]) -> bool: + cached = memo.get(event_digest) + if cached is not None: + return cached + if event_digest in visiting: + memo[event_digest] = False + return False + event = trust_events.get(event_digest) + if event is None or event.outcome is not MechanismOutcome.PASS: + memo[event_digest] = False + return False + visiting.add(event_digest) + try: + transformation_id = str(event.attributes["transformation_id"]) + historical_graph_digest = str( + event.attributes["historical_graph_digest"] + ) + attribute_inputs = tuple(str(item) for item in event.attributes["inputs"]) + attribute_output = str(event.attributes["output"]) + prerequisite_digests = tuple( + str(item) + for item in event.attributes["prerequisite_trust_event_digests"] + ) + transform = self.graph.transformations[transformation_id] + except (KeyError, TypeError): + memo[event_digest] = False + return False + + exact_inputs = tuple(sorted({port.artifact_id for port in transform.inputs})) + output_ids = {port.artifact_id for port in transform.outputs} + expected = { + "historical_graph_digest": self.graph_digest, + "inputs": list(exact_inputs), + "output": event.subject_id, + "prerequisite_trust_event_digests": list(prerequisite_digests), + "transformation_id": transformation_id, + } + required_prerequisite_targets = { + source + for source in exact_inputs + if self.graph.incoming_hyperedges(source) + } + prerequisite_targets: list[str] = [] + valid = ( + historical_graph_digest == self.graph_digest + and attribute_inputs == exact_inputs + and attribute_output == event.subject_id + and event.source_ids == exact_inputs + and event.subject_id in output_ids + and transform.status == "COMPLETED" + and event.subject_id not in unresolved_subjects + and transformation_id not in unresolved_subjects + and all(source not in unresolved_subjects for source in exact_inputs) + and self.current_status(event.subject_id) is ArtifactStatus.VALID + and all( + self.current_status(source) is ArtifactStatus.VALID + for source in exact_inputs + ) + and event.binding.get("subject_id") == event.subject_id + and event.binding.get("predicate") == "vstd.graph.support" + and event.binding.get("expected") == expected + and len(set(prerequisite_digests)) == len(prerequisite_digests) ) + if valid: + for prerequisite_digest in prerequisite_digests: + prerequisite = trust_events.get(prerequisite_digest) + if ( + prerequisite is None + or prerequisite.sequence >= event.sequence + or not is_current(prerequisite_digest, visiting) + ): + valid = False + break + prerequisite_targets.append(prerequisite.subject_id) + if valid: + valid = ( + len(prerequisite_targets) == len(required_prerequisite_targets) + and set(prerequisite_targets) == required_prerequisite_targets + ) + memo[event_digest] = valid + return valid + + return tuple( + event + for event_digest, event in trust_events.items() + if is_current(event_digest, set()) ) def unresolved_conflicts(self) -> tuple[ConflictRecord, ...]: resolved = {item.conflict_id for item in self._resolutions.values()} return tuple( conflict - for conflict_id, conflict in self.graph.conflicts.items() + for conflict_id, conflict in self._conflicts.items() if conflict_id not in resolved ) def materialize_current_graph(self) -> ProvenanceHypergraph: """Create a derived current view; never mutate the historical graph.""" current = ProvenanceHypergraph.from_dict(self.graph.to_dict()) + for conflict_id, conflict in self._conflicts.items(): + if conflict_id not in current.conflicts: + current.add_conflict(conflict) for artifact_id, node in tuple(current.artifacts.items()): current.artifacts[artifact_id] = replace( node, status=self.current_status(artifact_id) @@ -511,6 +599,62 @@ def record_status_projection( attributes={"resulting_status": resulting_status.value}, ) + def record_conflict( + self, + conflict: ConflictRecord, + proposition: BoundProposition, + *, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + """Add mechanism-established current conflict evidence without rewriting Graph.""" + if ( + conflict.subject_id not in self.graph.artifacts + and conflict.subject_id not in self.graph.transformations + ): + raise AssuranceFlowError( + f"unknown conflict subject {conflict.subject_id}" + ) + if ( + not conflict.conflict_id + or not conflict.predicate + or len(conflict.competing_values) < 2 + or len(set(conflict.competing_values)) != len(conflict.competing_values) + or not conflict.evidence_refs + or len(set(conflict.evidence_refs)) != len(conflict.evidence_refs) + ): + raise AssuranceFlowError( + "conflict requires an identifier, predicate, distinct competing values, " + "and distinct evidence references" + ) + prior = self._conflicts.get(conflict.conflict_id) + if prior is not None: + raise AssuranceFlowError( + f"conflict identifier {conflict.conflict_id} is already bound" + ) + expected = conflict.to_dict() + if ( + proposition.subject_id != conflict.subject_id + or proposition.predicate != "vstd.graph.conflict" + or proposition.expected != expected + ): + raise AssuranceFlowError("conflict evidence is not exactly record-bound") + evaluation = session.evaluate(proposition) + event = self._append( + kind=AssuranceEventKind.CONFLICT_DECLARATION, + subject_id=conflict.subject_id, + source_ids=conflict.evidence_refs, + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={"conflict": conflict.to_dict()}, + ) + if evaluation.outcome is MechanismOutcome.PASS: + self._conflicts[conflict.conflict_id] = conflict + return event + def resolve_conflict( self, conflict_id: str, @@ -520,7 +664,7 @@ def resolve_conflict( session: VerificationSession, recorded_at: str, ) -> ConflictResolution: - conflict = self.graph.conflicts.get(conflict_id) + conflict = self._conflicts.get(conflict_id) if conflict is None: raise AssuranceFlowError(f"unknown conflict {conflict_id}") if conflict_id in {item.conflict_id for item in self._resolutions.values()}: @@ -572,28 +716,77 @@ def record_trust( source_ids: Iterable[str], proposition: BoundProposition, *, + transformation_id: str, + prerequisite_trust_event_digests: Iterable[str] = (), session: VerificationSession, recorded_at: str, ) -> AssuranceEvent: sources = tuple(sorted(set(source_ids))) if not sources: raise AssuranceFlowError("TRUST requires at least one recorded source") + if canonical_digest(self.graph.to_dict()) != self.graph_digest: + raise AssuranceFlowError("historical Graph changed after ledger creation") if target_id not in self.graph.artifacts: raise AssuranceFlowError(f"unknown TRUST target {target_id}") - ancestors = self.graph.ancestors((target_id,)) - {target_id} - if any(source not in ancestors for source in sources): - raise AssuranceFlowError("TRUST sources must be recorded ancestors of target") + transform = self.graph.transformations.get(transformation_id) + if transform is None: + raise AssuranceFlowError(f"unknown TRUST transformation {transformation_id}") + exact_inputs = tuple(sorted({port.artifact_id for port in transform.inputs})) + if sources != exact_inputs: + raise AssuranceFlowError( + "TRUST sources must equal the exact transformation input set" + ) + if target_id not in {port.artifact_id for port in transform.outputs}: + raise AssuranceFlowError( + "TRUST target must be an output of the bound transformation" + ) + if transform.status != "COMPLETED": + raise AssuranceFlowError("incomplete transformation cannot provide TRUST") if self.current_status(target_id) is not ArtifactStatus.VALID or any( self.current_status(source) is not ArtifactStatus.VALID for source in sources ): raise AssuranceFlowError( - "inadmissible target or ancestry cannot provide current TRUST" + "inadmissible target or transformation input cannot provide current TRUST" ) unresolved_subjects = {item.subject_id for item in self.unresolved_conflicts()} - if target_id in unresolved_subjects or any(source in unresolved_subjects for source in sources): + if ( + target_id in unresolved_subjects + or transformation_id in unresolved_subjects + or any(source in unresolved_subjects for source in sources) + ): raise AssuranceFlowError("unresolved conflict blocks clean TRUST") - expected = {"sources": list(sources), "target": target_id} + + prerequisite_digests = tuple(sorted(set(prerequisite_trust_event_digests))) + current_by_digest = { + event.digest(): event for event in self.current_trust_events() + } + required_prerequisite_targets = { + source for source in sources if self.graph.incoming_hyperedges(source) + } + prerequisite_targets: list[str] = [] + for event_digest in prerequisite_digests: + prerequisite = current_by_digest.get(event_digest) + if prerequisite is None: + raise AssuranceFlowError( + "TRUST prerequisite is not a current passing TRUST event" + ) + prerequisite_targets.append(prerequisite.subject_id) + if ( + len(prerequisite_targets) != len(required_prerequisite_targets) + or set(prerequisite_targets) != required_prerequisite_targets + ): + raise AssuranceFlowError( + "TRUST requires exactly one current prerequisite for each derived input" + ) + + expected = { + "historical_graph_digest": self.graph_digest, + "inputs": list(sources), + "output": target_id, + "prerequisite_trust_event_digests": list(prerequisite_digests), + "transformation_id": transformation_id, + } if ( proposition.subject_id != target_id or proposition.predicate != "vstd.graph.support" @@ -610,6 +803,13 @@ def record_trust( recorded_at=recorded_at, evaluation=evaluation, evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={ + "historical_graph_digest": self.graph_digest, + "inputs": list(sources), + "output": target_id, + "prerequisite_trust_event_digests": list(prerequisite_digests), + "transformation_id": transformation_id, + }, ) def record_rot( @@ -743,9 +943,10 @@ def diagnose( """Compute bounded artifact-relative BLAME or GUILT just in time. BLAME means only that the named artifact-relative responsibility - proposition passed. GUILT additionally names an exact violated - obligation in ``proposition.parameters['obligation']``. Neither result - concerns an actor's moral character, reputation, or general trust. + proposition passed. GUILT is not its directional opposite: it checks + that same localized responsibility together with an exact violated + obligation. Neither result concerns an actor's moral character, + reputation, or general trust. """ localization = next( ( @@ -778,19 +979,22 @@ def diagnose( None, "diagnostic attribution requires a separate exact mechanism", ) + obligation = proposition.parameters.get("obligation", "") + if kind is DiagnosticKind.GUILT and not obligation: + raise AssuranceFlowError("GUILT requires an exact violated obligation") expected = { "ancestor": ancestor_id, "descendant": descendant_id, "localization_event_digest": localization.digest(), } + if kind is DiagnosticKind.GUILT: + expected["violated_obligation"] = obligation if ( proposition.subject_id != ancestor_id or proposition.predicate != f"vstd.graph.diagnostic.{kind.value.lower()}" or proposition.expected != expected ): raise AssuranceFlowError("diagnostic proposition is not exactly relation-bound") - if kind is DiagnosticKind.GUILT and not proposition.parameters.get("obligation"): - raise AssuranceFlowError("GUILT requires an exact violated obligation") evaluation = session.evaluate(proposition) event = self._append( kind=AssuranceEventKind.DIAGNOSTIC_ATTRIBUTION, @@ -804,7 +1008,7 @@ def diagnose( attributes={ "diagnostic_kind": kind.value, "localization_event_digest": localization.digest(), - "obligation": proposition.parameters.get("obligation", ""), + "obligation": obligation, }, ) return DiagnosticAttribution( @@ -891,6 +1095,13 @@ def recheck_assurance_log( subject_id, source_ids, proposition, + transformation_id=str(attributes["transformation_id"]), + prerequisite_trust_event_digests=tuple( + str(item) + for item in attributes[ + "prerequisite_trust_event_digests" + ] + ), session=session, recorded_at=recorded_at, ) @@ -916,6 +1127,22 @@ def recheck_assurance_log( session=session, recorded_at=recorded_at, ) + elif kind is AssuranceEventKind.CONFLICT_DECLARATION: + conflict_data = attributes["conflict"] + if not isinstance(conflict_data, Mapping): + raise TypeError("conflict declaration is not an object") + event = ledger.record_conflict( + ConflictRecord( + str(conflict_data["conflict_id"]), + str(conflict_data["subject_id"]), + str(conflict_data["predicate"]), + tuple(str(item) for item in conflict_data["competing_values"]), + tuple(str(item) for item in conflict_data["evidence_refs"]), + ), + proposition, + session=session, + recorded_at=recorded_at, + ) elif kind is AssuranceEventKind.CONFLICT_RESOLUTION: ledger.resolve_conflict( str(attributes["conflict_id"]), diff --git a/src/verifier/specifications/LADDER.md b/src/verifier/specifications/LADDER.md index 8bb59d6..0f3a8bb 100644 --- a/src/verifier/specifications/LADDER.md +++ b/src/verifier/specifications/LADDER.md @@ -168,6 +168,14 @@ averaged, voted, or converted into actor standing. Every child MUST still discha new predicates, transformations, boundaries, and evidence obligations. A declared trust root is an explicit dependency and stopping boundary, not actor TRUST. +The reference event mechanism realizes that rule edge by edge. Each TRUST event binds the +historical Graph digest, one exact transformation, its complete input artifact set, one +output artifact, and the exact prerequisite TRUST event for every derived input. A +descendant event is current only while every recursively required event, input, output, +and transformation remains admissible and free of unresolved conflict. Alternate or +duplicate paths remain distinct recorded routes; their count supplies no added strength +or witness independence. + **ROT** is typed, time-indexed degradation of the current admissibility of recorded TRUST. It requires exact lifecycle or dependency evidence, such as expiry under a declared freshness bound, `STALE`, `CHALLENGED`, `REVOKED`, `SUPERSEDED`, or an invalidated required @@ -186,6 +194,16 @@ observation, falsehood, or causal responsibility; localization requires addition intervention, ablation, independently bound execution evidence, or an equivalent declared mechanism. +**BLAME** and **GUILT** are bounded artifact-relative diagnostic results, not opposite +directions on the Graph. BLAME requires a named mechanism to establish that an exact +artifact bears responsibility for or materially contributed to an exact localized +deviation. GUILT is the stronger combined proposition: the same localized responsibility +plus an exact obligation that the artifact violated. Thus GUILT contains a responsibility +component, while BLAME alone establishes no obligation or violation. Neither term concerns +actor morality, character, identity, or reputation. Exoneration, obligation satisfaction, +or not-guilty conclusions require their own exact propositions and mechanisms; absent such +evidence the result remains `UNKNOWN`. + The word *causal* is required here for recorded developmental and provenance causality: the graph states which artifacts and transformations produced later claim architecture. Propagation across those causal-provenance edges does not by itself establish @@ -196,11 +214,11 @@ flow in the opposite direction as inherited truth, decay, or guilt. `UNKNOWN` an `CONFLICTED` support or lineage MUST remain visible and MUST NOT become a clean signal. `VSTD-GRAPH-ASSURANCE-1` now serializes an additive, hash-chained reference event log with the complete historical Graph, exact proposition bindings, and embedded evidence bytes. -`AssuranceLedger` implements mechanism-earned forward TRUST, typed ROT, challenge-ledger -status projection, reverse RUST reachability, unique-descendant structural concentration, -additive conflict resolution, explicit causal localization, and bounded artifact-relative -diagnostic attribution. Duplicate paths and repeated records remain set-valued and earn no -strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded +`AssuranceLedger` implements mechanism-earned forward TRUST edge by edge, typed ROT, +challenge-ledger status projection, reverse RUST reachability, unique-descendant structural +concentration, additive conflict declaration and resolution, explicit causal localization, +and bounded artifact-relative diagnostic attribution. Duplicate paths and repeated records +remain set-valued and earn no strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded evidence, reruns every exact mechanism, reproduces the event hash chain, and compares the derived current view. A deployment still supplies the proposition-specific mechanisms: the event format and dispatcher do not create a universal support-transfer algebra or infer diff --git a/standard/LADDER.md b/standard/LADDER.md index 8bb59d6..0f3a8bb 100644 --- a/standard/LADDER.md +++ b/standard/LADDER.md @@ -168,6 +168,14 @@ averaged, voted, or converted into actor standing. Every child MUST still discha new predicates, transformations, boundaries, and evidence obligations. A declared trust root is an explicit dependency and stopping boundary, not actor TRUST. +The reference event mechanism realizes that rule edge by edge. Each TRUST event binds the +historical Graph digest, one exact transformation, its complete input artifact set, one +output artifact, and the exact prerequisite TRUST event for every derived input. A +descendant event is current only while every recursively required event, input, output, +and transformation remains admissible and free of unresolved conflict. Alternate or +duplicate paths remain distinct recorded routes; their count supplies no added strength +or witness independence. + **ROT** is typed, time-indexed degradation of the current admissibility of recorded TRUST. It requires exact lifecycle or dependency evidence, such as expiry under a declared freshness bound, `STALE`, `CHALLENGED`, `REVOKED`, `SUPERSEDED`, or an invalidated required @@ -186,6 +194,16 @@ observation, falsehood, or causal responsibility; localization requires addition intervention, ablation, independently bound execution evidence, or an equivalent declared mechanism. +**BLAME** and **GUILT** are bounded artifact-relative diagnostic results, not opposite +directions on the Graph. BLAME requires a named mechanism to establish that an exact +artifact bears responsibility for or materially contributed to an exact localized +deviation. GUILT is the stronger combined proposition: the same localized responsibility +plus an exact obligation that the artifact violated. Thus GUILT contains a responsibility +component, while BLAME alone establishes no obligation or violation. Neither term concerns +actor morality, character, identity, or reputation. Exoneration, obligation satisfaction, +or not-guilty conclusions require their own exact propositions and mechanisms; absent such +evidence the result remains `UNKNOWN`. + The word *causal* is required here for recorded developmental and provenance causality: the graph states which artifacts and transformations produced later claim architecture. Propagation across those causal-provenance edges does not by itself establish @@ -196,11 +214,11 @@ flow in the opposite direction as inherited truth, decay, or guilt. `UNKNOWN` an `CONFLICTED` support or lineage MUST remain visible and MUST NOT become a clean signal. `VSTD-GRAPH-ASSURANCE-1` now serializes an additive, hash-chained reference event log with the complete historical Graph, exact proposition bindings, and embedded evidence bytes. -`AssuranceLedger` implements mechanism-earned forward TRUST, typed ROT, challenge-ledger -status projection, reverse RUST reachability, unique-descendant structural concentration, -additive conflict resolution, explicit causal localization, and bounded artifact-relative -diagnostic attribution. Duplicate paths and repeated records remain set-valued and earn no -strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded +`AssuranceLedger` implements mechanism-earned forward TRUST edge by edge, typed ROT, +challenge-ledger status projection, reverse RUST reachability, unique-descendant structural +concentration, additive conflict declaration and resolution, explicit causal localization, +and bounded artifact-relative diagnostic attribution. Duplicate paths and repeated records +remain set-valued and earn no strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded evidence, reruns every exact mechanism, reproduces the event hash chain, and compares the derived current view. A deployment still supplies the proposition-specific mechanisms: the event format and dispatcher do not create a universal support-transfer algebra or infer diff --git a/standard/schemas/vstd-graph-assurance-1.schema.json b/standard/schemas/vstd-graph-assurance-1.schema.json index d4cc2e9..32cb250 100644 --- a/standard/schemas/vstd-graph-assurance-1.schema.json +++ b/standard/schemas/vstd-graph-assurance-1.schema.json @@ -46,7 +46,7 @@ "required": ["sequence", "kind", "subject_id", "source_ids", "proposition", "binding", "recorded_at", "outcome", "mechanism_id", "mechanism_digest", "evidence_refs", "evidence_payloads", "trust_roots", "details", "previous_event_digest", "attributes", "event_digest"], "properties": { "sequence": {"type": "integer", "minimum": 0}, - "kind": {"enum": ["TRUST", "ROT", "RUST", "STATUS_PROJECTION", "CONFLICT_RESOLUTION", "CAUSAL_LOCALIZATION", "DIAGNOSTIC_ATTRIBUTION"]}, + "kind": {"enum": ["TRUST", "ROT", "RUST", "STATUS_PROJECTION", "CONFLICT_DECLARATION", "CONFLICT_RESOLUTION", "CAUSAL_LOCALIZATION", "DIAGNOSTIC_ATTRIBUTION"]}, "subject_id": {"type": "string", "minLength": 1}, "source_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "proposition": {"type": "string", "minLength": 1}, @@ -66,6 +66,49 @@ "previous_event_digest": {"type": "string", "pattern": "^(?:[0-9a-f]{64})?$"}, "attributes": {"type": "object"}, "event_digest": {"$ref": "#/$defs/digest"} + }, + "allOf": [ + { + "if": {"properties": {"kind": {"const": "TRUST"}}, "required": ["kind"]}, + "then": {"properties": {"attributes": {"$ref": "#/$defs/trustAttributes"}}} + }, + { + "if": {"properties": {"kind": {"const": "CONFLICT_DECLARATION"}}, "required": ["kind"]}, + "then": {"properties": {"attributes": {"$ref": "#/$defs/conflictAttributes"}}} + } + ] + }, + "trustAttributes": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "historical_graph_digest", "inputs", "output", "prerequisite_trust_event_digests", "transformation_id"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "historical_graph_digest": {"$ref": "#/$defs/digest"}, + "inputs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + "output": {"type": "string", "minLength": 1}, + "prerequisite_trust_event_digests": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/digest"}}, + "transformation_id": {"type": "string", "minLength": 1} + } + }, + "conflictAttributes": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "conflict"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "conflict": { + "type": "object", + "additionalProperties": false, + "required": ["conflict_id", "subject_id", "predicate", "competing_values", "evidence_refs"], + "properties": { + "conflict_id": {"type": "string", "minLength": 1}, + "subject_id": {"type": "string", "minLength": 1}, + "predicate": {"type": "string", "minLength": 1}, + "competing_values": {"type": "array", "minItems": 2, "uniqueItems": true, "items": {"type": "string"}}, + "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string"}} + } + } } }, "binding": { diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py index 292cdb7..f7217a0 100644 --- a/tests/test_evidence_bound_assurance.py +++ b/tests/test_evidence_bound_assurance.py @@ -258,6 +258,123 @@ def _graph_rating_evidence(graph, store, binding, *, members=("result",), rating return objects, edges +def _trust_proposition( + store: EvidenceStore, + ledger: AssuranceLedger, + transformation_id: str, + target_id: str, + prerequisite_digests=(), +) -> BoundProposition: + transform = ledger.graph.transformations[transformation_id] + inputs = sorted({port.artifact_id for port in transform.inputs}) + prerequisites = sorted(set(prerequisite_digests)) + return _proposition( + store, + target_id, + "vstd.graph.support", + { + "historical_graph_digest": ledger.graph_digest, + "inputs": inputs, + "output": target_id, + "prerequisite_trust_event_digests": prerequisites, + "transformation_id": transformation_id, + }, + ) + + +def _record_trust_chain( + ledger: AssuranceLedger, + store: EvidenceStore, + session: VerificationSession, +): + first_binding = _trust_proposition(store, ledger, "first", "middle") + first = ledger.record_trust( + "middle", + ("source",), + first_binding, + transformation_id="first", + session=session, + recorded_at="2026-08-29T00:00:00Z", + ) + second_binding = _trust_proposition( + store, ledger, "second", "result", (first.digest(),) + ) + second = ledger.record_trust( + "result", + ("middle",), + second_binding, + transformation_id="second", + prerequisite_trust_event_digests=(first.digest(),), + session=session, + recorded_at="2026-08-29T00:00:01Z", + ) + return first, second + + +def _witness_components( + store: EvidenceStore, + entry, + witness_id: str, + *, + declarant_id: str = "declarant:one", + identity_evidence_ref: str | None = None, +): + binding_digest = entry.witness.header.binding + identity_ref = identity_evidence_ref or store.add( + f"identity coordinate:{witness_id}".encode() + ) + witness = WitnessIdentity(witness_id, identity_ref) + relation = f"{declarant_id}->{witness_id}" + relationships = { + dimension: RelationshipState.SEPARATE for dimension in IndependenceDimension + } + assertion = IndependenceAssertion( + witness_id, + relationships, + { + dimension: _proposition( + store, + relation, + f"vstd5.shared.{dimension.value}", + False, + parameters={"claim_binding_digest": binding_digest}, + ) + for dimension in IndependenceDimension + }, + ) + checker_digest = hashlib.sha256(witness_id.encode()).hexdigest() + certificate_digest = entry.witness.digest() + expected = { + "claim_binding_digest": binding_digest, + "vstd4_certificate_digest": certificate_digest, + "checker_descriptor_digest": checker_digest, + "result": CorroborationOutcome.CORROBORATED.value, + } + verification = _proposition( + store, + "claim:fixture", + "vstd5.corroboration", + expected, + parameters={ + "witness_id": witness_id, + "observed_at": "2026-08-29T00:00:00Z", + }, + ) + corroboration = CorroborationRecord( + f"corroboration:{witness_id}", + witness_id, + binding_digest, + certificate_digest, + checker_digest, + verification.evidence_refs, + CorroborationOutcome.CORROBORATED, + "2026-08-29T00:00:00Z", + verification, + "TEST", + ) + return witness, assertion, corroboration + + def test_serialized_pass_is_not_an_input_to_evidence_evaluation() -> None: assert MechanismOutcome.__doc__ == "Enumeration of the exported result values." store, session = _session() @@ -607,20 +724,28 @@ def test_upstream_challenge_invalidates_current_trust_and_graph_admission() -> N graph = _graph() ledger = AssuranceLedger(graph) store, session = _session() - trust = _proposition( - store, - "result", - "vstd.graph.support", - {"sources": ["source"], "target": "result"}, + first_binding = _trust_proposition(store, ledger, "first", "middle") + first = ledger.record_trust( + "middle", + ("source",), + first_binding, + transformation_id="first", + session=session, + recorded_at="2026-08-29T00:00:00Z", + ) + second_binding = _trust_proposition( + store, ledger, "second", "result", (first.digest(),) ) ledger.record_trust( "result", - ("source",), - trust, + ("middle",), + second_binding, + transformation_id="second", + prerequisite_trust_event_digests=(first.digest(),), session=session, - recorded_at="2026-08-29T00:00:00Z", + recorded_at="2026-08-29T00:00:01Z", ) - assert len(ledger.current_trust_events()) == 1 + assert len(ledger.current_trust_events()) == 2 assert ledger.impacted_descendants("source") == ("middle", "result") challenges = ChallengeLedger() @@ -749,18 +874,15 @@ def test_trust_rot_and_rust_follow_direction_without_recursive_amplification() - ledger = AssuranceLedger(graph) store, session = _session() - trust = _proposition( - store, - "result", - "vstd.graph.support", - {"sources": ["source"], "target": "result"}, - ) + trust = _trust_proposition(store, ledger, "first", "middle") first = ledger.record_trust( - "result", ("source", "source"), trust, + "middle", ("source", "source"), trust, + transformation_id="first", session=session, recorded_at="2026-08-29T00:00:00Z", ) second = ledger.record_trust( - "result", ("source",), trust, + "middle", ("source",), trust, + transformation_id="first", session=session, recorded_at="2026-08-29T00:00:00Z", ) assert first.digest() == second.digest() @@ -784,14 +906,158 @@ def test_trust_rot_and_rust_follow_direction_without_recursive_amplification() - session=session, recorded_at="2026-08-29T00:02:00Z", ) assert ledger.current_status("source") is ArtifactStatus.REVOKED - with pytest.raises(AssuranceFlowError, match="inadmissible target or ancestry"): + with pytest.raises(AssuranceFlowError, match="inadmissible target or transformation input"): ledger.record_trust( - "result", ("source",), trust, + "middle", ("source",), trust, + transformation_id="first", session=session, recorded_at="2026-08-29T00:03:00Z", ) assert ledger.verify_hash_chain() is True +def test_trust_cannot_jump_over_an_unbound_transformation() -> None: + ledger = AssuranceLedger(_graph()) + store, session = _session() + direct = _proposition( + store, + "result", + "vstd.graph.support", + { + "historical_graph_digest": ledger.graph_digest, + "inputs": ["source"], + "output": "result", + "prerequisite_trust_event_digests": [], + "transformation_id": "second", + }, + ) + with pytest.raises(AssuranceFlowError, match="exact transformation input set"): + ledger.record_trust( + "result", + ("source",), + direct, + transformation_id="second", + session=session, + recorded_at="2026-08-29T00:00:00Z", + ) + + missing_prerequisite = _trust_proposition( + store, ledger, "second", "result" + ) + with pytest.raises(AssuranceFlowError, match="exactly one current prerequisite"): + ledger.record_trust( + "result", + ("middle",), + missing_prerequisite, + transformation_id="second", + session=session, + recorded_at="2026-08-29T00:00:01Z", + ) + + +@pytest.mark.parametrize( + "status", + ( + ArtifactStatus.CHALLENGED, + ArtifactStatus.STALE, + ArtifactStatus.REVOKED, + ArtifactStatus.SUPERSEDED, + ), +) +def test_intermediate_degradation_excludes_but_does_not_rewrite_trust( + status: ArtifactStatus, +) -> None: + ledger = AssuranceLedger(_graph()) + store, session = _session() + first, second = _record_trust_chain(ledger, store, session) + historical_digests = (first.digest(), second.digest()) + rot = _proposition( + store, "middle", "vstd.graph.current_status", status.value + ) + ledger.record_rot( + "middle", + status, + rot, + session=session, + recorded_at="2026-08-29T00:01:00Z", + ) + + assert ledger.current_trust_events() == () + assert tuple(event.digest() for event in ledger.events()[:2]) == historical_digests + assert all(event.outcome is MechanismOutcome.PASS for event in ledger.events()[:2]) + + +@pytest.mark.parametrize("conflict_subject", ("middle", "first")) +def test_dependency_conflict_invalidates_and_additive_resolution_restores_trust( + conflict_subject: str, +) -> None: + ledger = AssuranceLedger(_graph()) + store, session = _session() + first, second = _record_trust_chain(ledger, store, session) + assert tuple(event.digest() for event in ledger.current_trust_events()) == ( + first.digest(), + second.digest(), + ) + + conflict = ConflictRecord( + f"conflict:{conflict_subject}", + conflict_subject, + "current_dependency_state", + ("candidate:a", "candidate:b"), + ("evidence:a", "evidence:b"), + ) + conflict_binding = _proposition( + store, + conflict_subject, + "vstd.graph.conflict", + conflict.to_dict(), + ) + ledger.record_conflict( + conflict, + conflict_binding, + session=session, + recorded_at="2026-08-29T00:01:00Z", + ) + assert ledger.current_trust_events() == () + assert len(ledger.events()) == 3 + + resolution = _proposition( + store, + conflict_subject, + "vstd.graph.resolve.current_dependency_state", + "candidate:a", + parameters={"conflict_id": conflict.conflict_id}, + ) + ledger.resolve_conflict( + conflict.conflict_id, + "candidate:a", + resolution, + session=session, + recorded_at="2026-08-29T00:02:00Z", + ) + assert tuple(event.digest() for event in ledger.current_trust_events()) == ( + first.digest(), + second.digest(), + ) + root = Path(__file__).resolve().parents[1] + schema = json.loads( + (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() + ) + graph_schema = json.loads( + (root / "receipts/schema/vstd_graph_receipt.json").read_text() + ) + registry = Registry().with_resource( + graph_schema["$id"], Resource.from_contents(graph_schema) + ) + Draft202012Validator(schema, registry=registry).validate(ledger.to_dict()) + replayed = recheck_assurance_log( + ledger.to_dict(), mechanisms=(ExactFactMechanism(),) + ) + assert tuple(event.digest() for event in replayed.current_trust_events()) == ( + first.digest(), + second.digest(), + ) + + def test_rust_requires_separate_localization_before_blame_or_guilt() -> None: graph = _graph() ledger = AssuranceLedger(graph) @@ -861,6 +1127,30 @@ def test_rust_requires_separate_localization_before_blame_or_guilt() -> None: recorded_at="2026-08-29T00:04:00Z", ) + established_guilt = _proposition( + store, + "source", + "vstd.graph.diagnostic.guilt", + { + "ancestor": "source", + "descendant": "result", + "localization_event_digest": event.digest(), + "violated_obligation": "obligation:preserve-result-integrity", + }, + parameters={"obligation": "obligation:preserve-result-integrity"}, + ) + guilt_result = ledger.diagnose( + DiagnosticKind.GUILT, + "source", + "result", + established_guilt, + session=session, + recorded_at="2026-08-29T00:05:00Z", + ) + assert guilt_result.status == "ESTABLISHED" + assert guilt_result.evaluation is not None + assert guilt_result.evaluation.passed is True + def test_assurance_event_log_is_portable_strict_and_evidence_complete() -> None: graph = _graph() @@ -1054,6 +1344,17 @@ def corroboration(record_id, outcome, observation): assert list( Draft202012Validator(schema, registry=registry).iter_errors(false_positive) ) + false_independence = copy.deepcopy(receipt) + false_independence["result"]["conformance_status"] = "NOT_ESTABLISHED" + false_independence["result"]["identity_errors"] = [ + "duplicate witness identifier: witness:one" + ] + false_independence["result"]["errors"] = list( + false_independence["result"]["identity_errors"] + ) + assert list( + Draft202012Validator(schema, registry=registry).iter_errors(false_independence) + ) rechecked = recheck_vstd5_receipt( entry, receipt, mechanisms=(ExactFactMechanism(),) ) @@ -1079,3 +1380,118 @@ def corroboration(record_id, outcome, observation): assert result.conformance_status == "NOT_ESTABLISHED" assert result.status is WitnessResultStatus.UNKNOWN assert result.computed_independence == "UNKNOWN" + + +def test_computed_independence_fails_closed_on_identity_and_assertion_errors() -> None: + store, session = _session() + entry = _established_vstd4(store, session) + binding_digest = entry.witness.header.binding # type: ignore[union-attr] + witness, assertion, corroboration = _witness_components( + store, entry, "witness:one" + ) + + duplicate_witness = assess_witness_corroboration( + entry, + WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness, witness), + (assertion,), + (corroboration,), + ), + session=session, + ) + assert duplicate_witness.computed_independence == "UNKNOWN" + assert any("duplicate witness identifier" in item for item in duplicate_witness.identity_errors) + + shared_identity = store.add(b"shared identity evidence") + first = _witness_components( + store, entry, "witness:first", identity_evidence_ref=shared_identity + ) + second = _witness_components( + store, entry, "witness:second", identity_evidence_ref=shared_identity + ) + repeated_identity = assess_witness_corroboration( + entry, + WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (first[0], second[0]), + (first[1], second[1]), + (first[2], second[2]), + ), + session=session, + ) + assert repeated_identity.computed_independence == "UNKNOWN" + assert any("repeats another witness identity" in item for item in repeated_identity.identity_errors) + + missing = _witness_components( + store, + entry, + "witness:missing", + identity_evidence_ref="sha256:" + "0" * 64, + ) + missing_identity = assess_witness_corroboration( + entry, + WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (missing[0],), + (missing[1],), + (missing[2],), + ), + session=session, + ) + assert missing_identity.computed_independence == "UNKNOWN" + assert any("identity evidence unavailable" in item for item in missing_identity.identity_errors) + + declarant = _witness_components( + store, entry, "declarant:one", declarant_id="declarant:one" + ) + reused_declarant = assess_witness_corroboration( + entry, + WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (declarant[0],), + (declarant[1],), + (declarant[2],), + ), + session=session, + ) + assert reused_declarant.computed_independence == "UNKNOWN" + assert any("is the declarant" in item for item in reused_declarant.identity_errors) + + missing_assertion = assess_witness_corroboration( + entry, + WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (), + (corroboration,), + ), + session=session, + ) + assert missing_assertion.computed_independence == "UNKNOWN" + assert any("no independence assertion" in item for item in missing_assertion.separation_errors) + + duplicate_assertion = assess_witness_corroboration( + entry, + WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (assertion, assertion), + (corroboration,), + ), + session=session, + ) + assert duplicate_assertion.computed_independence == "UNKNOWN" + assert any("duplicate independence assertion" in item for item in duplicate_assertion.separation_errors) From 8893ff6f3fe2d82e05c15472c0d391b706fdb2b4 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 08:53:30 -0700 Subject: [PATCH 18/34] Bind deviations and replay assurance Reason: Close three semantic-review blockers without weakening UNKNOWN, conflict retention, or immutable historical evidence. Evidence: At parent 24bdba9d, BLAME could be established without selecting one of two descendant deviations; resolving a status conflict to REVOKED restored dependent TRUST while leaving the historical VALID state visible; and a duplicate VSTD-5 independence assertion was collapsed by receipt serialization so the emitted receipt failed its own rechecker. Coordinate: VSTD-GRAPH-ASSURANCE-1 causal localization, conflict-resolution current-state overlay, edge-local TRUST filtering, and the unreleased VSTD-5 witness bundle/receipt. Falsification: Reject this change if localization can omit or substitute its passing RUST event and deviation binding; if selecting REVOKED or FAILED can restore current TRUST; if arbitrary conflict adjudication implies admissibility; or if any emitted duplicate, orphan, missing, reused-identity, or negative witness input fails exact build-to-recheck replay. Compatibility and wire impact: This intentionally revises the unreleased VSTD-GRAPH-ASSURANCE-1 localization shape and localize_cause call, plus the unreleased VSTD-5 bundle shape, before 1.2.0. VSTD-Graph-1 receipts remain immutable and own no resolution transition; the separate non-receipt assurance overlay retains historical conflict and resolution evidence. No released receipt identifier is silently redefined. --- CHANGELOG.md | 10 +- HUMANS.md | 6 +- README.md | 7 +- docs/ARCHITECTURE.md | 11 +- docs/reference.html | 8 +- receipts/schema/vstd5_receipt.json | 16 +- src/verifier/core/witness.py | 21 +- src/verifier/data/assurance.py | 147 +++++- src/verifier/specifications/LADDER.md | 14 +- src/verifier/specifications/VSTD-5.md | 18 +- src/verifier/specifications/VSTD-Graph-1.md | 8 +- standard/LADDER.md | 14 +- standard/VSTD-5.md | 18 +- standard/VSTD-Graph-1.md | 8 +- .../vstd-graph-assurance-1.schema.json | 29 ++ tests/test_evidence_bound_assurance.py | 426 +++++++++++++++++- 16 files changed, 675 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7034ea2..fe23964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,7 +104,10 @@ dimensions, duplicate-witness/evidence refusal, exact admitted-certificate and corroboration binding, typed binding/identity/separation/corroboration errors, disagreement preservation, embedded evidence, and offline result recheck. Independence - fails closed on any identity or separation defect without parsing error-message text. This does not + fails closed on any identity or separation defect without parsing error-message text. + Witness identities and assertions serialize separately and in order, so duplicate, + orphan, missing, and reused-identity error inputs remain replayable instead of collapsing + during receipt construction. This does not claim a real external witness or independent implementation; a positive observation with unresolved independence is overall `UNKNOWN`. - Add evidence-bound Graph profile computation and replay. The compatibility `graph_level` @@ -119,7 +122,10 @@ dependency degrades or conflicts. Duplicate paths remain set-valued, historical graph bytes remain immutable, and topology alone earns no causal or moral conclusion. BLAME establishes bounded responsibility or material contribution; GUILT additionally binds an exact violated - obligation and is not BLAME in the opposite direction. Add complete offline event replay, + obligation and is not BLAME in the opposite direction. Localization binds one exact passing + RUST event and descendant-deviation proposition. Status-conflict resolution projects the + selected state into current admissibility; arbitrary resolved predicates remain blocked. + Add complete offline event replay, current TRUST filtering, and deduplicated descendant reassessment discovery. - Preserve incompatible Graph assertions as evidence-linked conflict records and label diff --git a/HUMANS.md b/HUMANS.md index e9f3210..531337b 100644 --- a/HUMANS.md +++ b/HUMANS.md @@ -54,7 +54,11 @@ RUST concentration as a triage count of unique deviating descendants, never caus strength. A bounded artifact-relative `BLAME` or `GUILT` result exists only after separate localization and attribution mechanisms pass. BLAME establishes responsibility or material contribution for the exact deviation; GUILT is the stronger obligation-relative result and -is not BLAME flowing in the opposite direction. Neither evaluates an actor's character. +is not BLAME flowing in the opposite direction. The localization must name the exact passing +RUST event and descendant-deviation binding; sharing a descendant identifier is insufficient. +Neither evaluates an actor's character. A resolved conflict restores current TRUST only when +its selected value has a checked admissible status consequence; selecting an arbitrary value +does not establish admissibility. For reliance, replay the portable log with `recheck_assurance_log`: a stored event word or hash chain without successful evidence rehash and mechanism execution is not current assurance. When upstream status changes, inspect `current_trust_events` and the deduplicated diff --git a/README.md b/README.md index f7cfe51..1d26594 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,8 @@ historical evidence; RUST is the inverse-TRUST diagnostic mechanic moving backwa descendant deviation toward recorded ancestor candidates. This memetic propagation does not by itself prove guilt, responsibility, falsehood, causal localization, or automatic ancestor falsification. The reference runtime requires a separate passing localization -mechanism and an exact artifact-relative diagnostic proposition before it can emit bounded +mechanism bound to one exact passing RUST event and descendant-deviation proposition before +it can emit a bounded `BLAME` or `GUILT`. BLAME establishes bounded responsibility or material contribution; GUILT is not its opposite, but the stronger combined result that additionally establishes an exact violated obligation. Neither result concerns actor character. See [the governing @@ -187,7 +188,9 @@ architecture](standard/LADDER.md#11-artifact-first-causal-provenance-orientation `VSTD-GRAPH-ASSURANCE-1` carries the immutable historical Graph, exact event bindings, embedded evidence bytes, event hash chain, and derived current-view digest. `recheck_assurance_log` rehashes the evidence, reruns each exact registered mechanism, and -rejects any event or current view that does not reproduce. +rejects any event or current view that does not reproduce. Conflict adjudication and current +admissibility are separate: a selected status affects the current artifact or transformation +state, while resolving an arbitrary predicate cannot silently restore TRUST. This is VSTD's **ZIZK artifact-first TRUST architecture**, not an optional research profile. Zero identity means zero identity-derived verdict weight, not anonymity or the diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 033adad..d410fec 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -303,7 +303,10 @@ them `EVIDENCED`, and never derives `EVIDENCED`. The distinct VSTD-5 reference p establish only its exact declarant/witness separation propositions after all seven seams are rerun by registered mechanisms. Typed binding, identity, separation, and corroboration errors keep `computed_independence` fail-closed; no error-message text is interpreted as a -semantic category. This path does not upgrade the legacy generic-run fields. +semantic category. Witness identities and independence assertions serialize as separate +ordered arrays, so duplicates, orphan assertions, and missing cardinality survive receipt +build and replay rather than disappearing inside a keyed map. This path does not upgrade the +legacy generic-run fields. Graph conflict records retain incompatible values and their evidence references without adding a scalar score or changing the frozen artifact-status vocabulary. A conflict makes @@ -318,11 +321,11 @@ question is a new assessment over the retained graph and applicable lifecycle re |---|---| | An ancestor is `CHALLENGED`, `REVOKED`, or `STALE` | Candidate/evidence-bound Graph recomputation follows the full ancestor closure and returns compatibility field `level = 0`. `AssuranceLedger` also records typed ROT or projects append-only challenge-ledger state into a derived current view; historical Graph bytes remain unchanged. | | An ancestor is `SUPERSEDED` | The historical Graph candidate remains admissible by design; the stricter all-ancestors-`VALID` policy rejects it for current-use admission. Supersession does not retroactively falsify its prior lineage role. | -| Upstream evidence conflicts | A retained `ConflictRecord` or a mechanism-established `CONFLICT_DECLARATION` event blocks every dependent edge-local TRUST route. `resolve_conflict` accepts only a mechanism-passing proposition bound to the exact conflict and one retained competing value; the original conflict remains in history while the derived current view records the additive resolution. | +| Upstream evidence conflicts | A retained `ConflictRecord` or a mechanism-established `CONFLICT_DECLARATION` event blocks every dependent edge-local TRUST route. `resolve_conflict` accepts only a mechanism-passing proposition bound to the exact conflict and one retained competing value. A status resolution projects the selected artifact or transformation state: only `VALID` / `COMPLETED` can restore the route. An arbitrary resolved predicate remains admissibility-blocking because value adjudication does not establish support effect. The original conflict and resolution evidence remain historical. | | Evidence arrives by multiple paths or one run receipt repeats a reference | Reachability and impact sets deduplicate identifiers. Multiplicity supplies no independence or strength. | -| A descendant deviation points toward shared ancestors | A mechanism-passing deviation emits RUST over the deduplicated recorded ancestor set. Structural concentration counts unique deviating descendants, not paths or causal strength. Localization, BLAME, and GUILT require separate exact mechanisms. | +| A descendant deviation points toward shared ancestors | A mechanism-passing deviation emits RUST over the deduplicated recorded ancestor set. Structural concentration counts unique deviating descendants, not paths or causal strength. Localization selects and binds one exact passing RUST event, its descendant-deviation binding digest, and an ancestor contained in that event. BLAME and GUILT bind that localization event and require separate exact mechanisms. | | A challenge ledger changes a claim's current status | `project_challenges` binds its complete append-only records into a current Graph overlay and embeds those records for replay. Existing TRUST remains historical, while recursively dependent events disappear from `current_trust_events`; `impacted_descendants` reports the deduplicated reassessment surface. It never mutates the historical graph. | -| Later evidence clears a conflict | The additive resolution retains the original competing evidence and its mechanism evaluation. Removing or rewriting the conflict remains invalid. | +| Later evidence adjudicates a conflict | The additive resolution retains the original competing evidence and its mechanism evaluation. VSTD-Graph-1 receipts remain immutable; the separate assurance overlay owns the resolution and current-state projection. Removing or rewriting historical evidence remains invalid. | | Candidate calculation encounters cyclic ancestry | Rejected before candidate calculation; recursive topology cannot manufacture assurance. | The forward blast-radius query remains discovery only. `AssuranceLedger` is the distinct diff --git a/docs/reference.html b/docs/reference.html index 302483c..f143ac3 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -432,12 +432,14 @@

    AssuranceLedger class

    Append-only current-state overlay for an immutable provenance graph.

    Defined in verifier.data.assurance

    MethodSummary
    current_status(self, artifact_id: 'str') -> 'ArtifactStatus'
    current_trust_events(self) -> 'tuple[AssuranceEvent, ...]'Return passing TRUST records still admissible under the current view.
    current_trust_events(self) -> 'tuple[AssuranceEvent, ...]'Return recursively current edge-local TRUST records.
    diagnose(self, kind: 'DiagnosticKind', ancestor_id: 'str', descendant_id: 'str', proposition: 'Optional[BoundProposition]', *, session: 'VerificationSession', recorded_at: 'str') -> 'DiagnosticAttribution'Compute bounded artifact-relative BLAME or GUILT just in time.
    events(self) -> 'tuple[AssuranceEvent, ...]'
    impacted_descendants(self, artifact_id: 'str') -> 'tuple[str, ...]'Return the deduplicated recorded forward impact set, not a verdict.
    localize_cause(self, ancestor_id: 'str', descendant_id: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    materialize_current_graph(self) -> 'ProvenanceHypergraph'Create a derived current view; never mutate the historical graph.
    project_challenges(self, challenges: 'ChallengeLedger', *, recorded_at: 'str') -> 'tuple[AssuranceEvent, ...]'Project challenge state into an additive current Graph overlay.
    record_conflict(self, conflict: 'ConflictRecord', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Add mechanism-established current conflict evidence without rewriting Graph.
    record_rot(self, artifact_id: 'str', resulting_status: 'ArtifactStatus', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    record_rust(self, descendant_id: 'str', deviation: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    record_status_projection(self, artifact_id: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Record a mechanism-checked current-status projection additively.
    record_trust(self, target_id: 'str', source_ids: 'Iterable[str]', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    record_trust(self, target_id: 'str', source_ids: 'Iterable[str]', proposition: 'BoundProposition', *, transformation_id: 'str', prerequisite_trust_event_digests: 'Iterable[str]' = (), session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    resolutions(self) -> 'tuple[ConflictResolution, ...]'
    resolve_conflict(self, conflict_id: 'str', selected_value: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'ConflictResolution'
    rust_concentration(self) -> 'tuple[StructuralConcentration, ...]'
    + + - + @@ -446,7 +448,7 @@

    AssuranceLedger class

    - + @@ -589,7 +591,7 @@

    VstdReceipt class

    WitnessBundle class

    WitnessBundle(claim_id: 'str', declarant_id: 'str', claim_binding_digest: 'str', witnesses: 'tuple[WitnessIdentity, ...]', independence: 'tuple[IndependenceAssertion, ...]', corroborations: 'tuple[CorroborationRecord, ...]') -> None
    -

    Claim-bound witnesses, separation assertions, and corroboration records.

    +

    Claim-bound identities, ordered separation assertions, and corroborations.

    Defined in verifier.core.witness

    MethodSummary
    admissibility_blocking_conflicts(self) -> 'tuple[ConflictRecord, ...]'Return conflicts whose effect still blocks a clean TRUST route.
    current_status(self, artifact_id: 'str') -> 'ArtifactStatus'
    current_transformation_status(self, transformation_id: 'str') -> 'str'Return a transformation's additive current status projection.
    current_trust_events(self) -> 'tuple[AssuranceEvent, ...]'Return recursively current edge-local TRUST records.
    diagnose(self, kind: 'DiagnosticKind', ancestor_id: 'str', descendant_id: 'str', proposition: 'Optional[BoundProposition]', *, session: 'VerificationSession', recorded_at: 'str') -> 'DiagnosticAttribution'Compute bounded artifact-relative BLAME or GUILT just in time.
    events(self) -> 'tuple[AssuranceEvent, ...]'
    impacted_descendants(self, artifact_id: 'str') -> 'tuple[str, ...]'Return the deduplicated recorded forward impact set, not a verdict.
    localize_cause(self, ancestor_id: 'str', descendant_id: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    localize_cause(self, ancestor_id: 'str', descendant_id: 'str', proposition: 'BoundProposition', *, rust_event_digest: 'str', session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Bind one ancestor to one exact passing descendant-deviation event.
    materialize_current_graph(self) -> 'ProvenanceHypergraph'Create a derived current view; never mutate the historical graph.
    project_challenges(self, challenges: 'ChallengeLedger', *, recorded_at: 'str') -> 'tuple[AssuranceEvent, ...]'Project challenge state into an additive current Graph overlay.
    record_conflict(self, conflict: 'ConflictRecord', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Add mechanism-established current conflict evidence without rewriting Graph.
    record_status_projection(self, artifact_id: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Record a mechanism-checked current-status projection additively.
    record_trust(self, target_id: 'str', source_ids: 'Iterable[str]', proposition: 'BoundProposition', *, transformation_id: 'str', prerequisite_trust_event_digests: 'Iterable[str]' = (), session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'
    resolutions(self) -> 'tuple[ConflictResolution, ...]'
    resolve_conflict(self, conflict_id: 'str', selected_value: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'ConflictResolution'
    resolve_conflict(self, conflict_id: 'str', selected_value: 'str', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'ConflictResolution'Adjudicate one value without equating selection with admissibility.
    rust_concentration(self) -> 'tuple[StructuralConcentration, ...]'
    to_dict(self) -> 'dict[str, Any]'
    unresolved_conflicts(self) -> 'tuple[ConflictRecord, ...]'
    diff --git a/receipts/schema/vstd5_receipt.json b/receipts/schema/vstd5_receipt.json index 117daf5..3f2328b 100644 --- a/receipts/schema/vstd5_receipt.json +++ b/receipts/schema/vstd5_receipt.json @@ -34,22 +34,30 @@ "digestRef": {"type": "string", "pattern": "^(?:sha256:)?[0-9a-f]{64}$"}, "bundle": { "type": "object", "additionalProperties": false, - "required": ["claim_id", "declarant_id", "claim_binding_digest", "witnesses", "corroborations"], + "required": ["claim_id", "declarant_id", "claim_binding_digest", "witnesses", "independence_assertions", "corroborations"], "properties": { "claim_id": {"type": "string", "minLength": 1}, "declarant_id": {"type": "string", "minLength": 1}, "claim_binding_digest": {"$ref": "#/$defs/digest"}, "witnesses": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/witness"}}, + "independence_assertions": {"type": "array", "items": {"$ref": "#/$defs/independenceAssertion"}}, "corroborations": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/corroboration"}} } }, "witness": { "type": "object", "additionalProperties": false, - "required": ["witness_id", "identity_evidence_ref", "independence"], + "required": ["witness_id", "identity_evidence_ref"], "properties": { "witness_id": {"type": "string", "minLength": 1}, - "identity_evidence_ref": {"$ref": "#/$defs/digestRef"}, - "independence": { + "identity_evidence_ref": {"$ref": "#/$defs/digestRef"} + } + }, + "independenceAssertion": { + "type": "object", "additionalProperties": false, + "required": ["witness_id", "dimensions"], + "properties": { + "witness_id": {"type": "string", "minLength": 1}, + "dimensions": { "type": "object", "additionalProperties": false, "required": ["control", "verdict_code", "trust_root", "evidence_source", "infrastructure", "financial_dependence", "jurisdictional_dependence"], "properties": { diff --git a/src/verifier/core/witness.py b/src/verifier/core/witness.py index f4cac2e..6f0e70c 100644 --- a/src/verifier/core/witness.py +++ b/src/verifier/core/witness.py @@ -123,7 +123,7 @@ def to_dict(self) -> dict[str, Any]: @dataclass(frozen=True) class WitnessBundle: - """Claim-bound witnesses, separation assertions, and corroboration records.""" + """Claim-bound identities, ordered separation assertions, and corroborations.""" claim_id: str declarant_id: str @@ -133,21 +133,13 @@ class WitnessBundle: corroborations: tuple[CorroborationRecord, ...] def to_dict(self) -> dict[str, Any]: - assertions = {item.witness_id: item for item in self.independence} return { "claim_id": self.claim_id, "declarant_id": self.declarant_id, "claim_binding_digest": self.claim_binding_digest, - "witnesses": [ - { - **witness.to_dict(), - "independence": ( - None - if assertions.get(witness.witness_id) is None - else assertions[witness.witness_id].to_dict()["dimensions"] - ), - } - for witness in self.witnesses + "witnesses": [witness.to_dict() for witness in self.witnesses], + "independence_assertions": [ + item.to_dict() for item in self.independence ], "corroborations": [item.to_dict() for item in self.corroborations], } @@ -161,7 +153,8 @@ def from_dict(cls, data: Mapping[str, Any]) -> "WitnessBundle": str(item["witness_id"]), str(item["identity_evidence_ref"]) ) witnesses.append(witness) - dimensions = item.get("independence") + for item in data.get("independence_assertions", ()): + dimensions = item.get("dimensions") if not isinstance(dimensions, Mapping): continue relationships: dict[IndependenceDimension, RelationshipState] = {} @@ -177,7 +170,7 @@ def from_dict(cls, data: Mapping[str, Any]) -> "WitnessBundle": if isinstance(binding, Mapping): evidence[dimension] = BoundProposition.from_dict(binding) assertions.append( - IndependenceAssertion(witness.witness_id, relationships, evidence) + IndependenceAssertion(str(item["witness_id"]), relationships, evidence) ) corroborations = tuple( CorroborationRecord( diff --git a/src/verifier/data/assurance.py b/src/verifier/data/assurance.py index 9aa400d..6f12a16 100644 --- a/src/verifier/data/assurance.py +++ b/src/verifier/data/assurance.py @@ -388,8 +388,57 @@ def current_status(self, artifact_id: str) -> ArtifactStatus: statuses = [node.status, *rot_statuses] if latest_projection is not None: statuses.append(latest_projection) + for resolution in self._resolutions.values(): + conflict = self._conflicts[resolution.conflict_id] + if conflict.subject_id == artifact_id and conflict.predicate == "status": + statuses.append(ArtifactStatus(resolution.selected_value)) return most_degraded(statuses) + def current_transformation_status(self, transformation_id: str) -> str: + """Return a transformation's additive current status projection.""" + transform = self.graph.transformations.get(transformation_id) + if transform is None: + return "UNKNOWN" + if transform.status != "COMPLETED": + return transform.status + for resolution in self._resolutions.values(): + conflict = self._conflicts[resolution.conflict_id] + if ( + conflict.subject_id == transformation_id + and conflict.predicate == "status" + and resolution.selected_value != "COMPLETED" + ): + return resolution.selected_value + return "COMPLETED" + + def admissibility_blocking_conflicts(self) -> tuple[ConflictRecord, ...]: + """Return conflicts whose effect still blocks a clean TRUST route. + + A passing resolution decides which retained value prevailed. It restores + admissibility only when the conflict is exactly about ``status`` and the + selected current state is itself admissible. Arbitrary resolved fields + remain blockers because adjudicating a value does not establish its + effect on edge-local support. + """ + resolutions = { + item.conflict_id: item for item in self._resolutions.values() + } + blocking: list[ConflictRecord] = [] + for conflict_id, conflict in self._conflicts.items(): + resolution = resolutions.get(conflict_id) + if resolution is None: + blocking.append(conflict) + continue + if conflict.predicate != "status": + blocking.append(conflict) + continue + if conflict.subject_id in self.graph.artifacts: + if self.current_status(conflict.subject_id) is not ArtifactStatus.VALID: + blocking.append(conflict) + elif self.current_transformation_status(conflict.subject_id) != "COMPLETED": + blocking.append(conflict) + return tuple(blocking) + def impacted_descendants(self, artifact_id: str) -> tuple[str, ...]: """Return the deduplicated recorded forward impact set, not a verdict.""" if artifact_id not in self.graph.artifacts: @@ -400,7 +449,9 @@ def current_trust_events(self) -> tuple[AssuranceEvent, ...]: """Return recursively current edge-local TRUST records.""" if canonical_digest(self.graph.to_dict()) != self.graph_digest: return () - unresolved_subjects = {item.subject_id for item in self.unresolved_conflicts()} + blocked_subjects = { + item.subject_id for item in self.admissibility_blocking_conflicts() + } trust_events = { event.digest(): event for event in self._events @@ -457,10 +508,10 @@ def is_current(event_digest: str, visiting: set[str]) -> bool: and attribute_output == event.subject_id and event.source_ids == exact_inputs and event.subject_id in output_ids - and transform.status == "COMPLETED" - and event.subject_id not in unresolved_subjects - and transformation_id not in unresolved_subjects - and all(source not in unresolved_subjects for source in exact_inputs) + and self.current_transformation_status(transformation_id) == "COMPLETED" + and event.subject_id not in blocked_subjects + and transformation_id not in blocked_subjects + and all(source not in blocked_subjects for source in exact_inputs) and self.current_status(event.subject_id) is ArtifactStatus.VALID and all( self.current_status(source) is ArtifactStatus.VALID @@ -514,9 +565,15 @@ def materialize_current_graph(self) -> ProvenanceHypergraph: current.artifacts[artifact_id] = replace( node, status=self.current_status(artifact_id) ) - resolved = {item.conflict_id for item in self._resolutions.values()} - for conflict_id in resolved: - current.conflicts.pop(conflict_id, None) + for transformation_id, transform in tuple(current.transformations.items()): + current.transformations[transformation_id] = replace( + transform, + status=self.current_transformation_status(transformation_id), + ) + for resolution in self._resolutions.values(): + conflict = self._conflicts[resolution.conflict_id] + if conflict.predicate == "status": + current.conflicts.pop(resolution.conflict_id, None) return current def project_challenges( @@ -664,6 +721,7 @@ def resolve_conflict( session: VerificationSession, recorded_at: str, ) -> ConflictResolution: + """Adjudicate one value without equating selection with admissibility.""" conflict = self._conflicts.get(conflict_id) if conflict is None: raise AssuranceFlowError(f"unknown conflict {conflict_id}") @@ -671,6 +729,18 @@ def resolve_conflict( raise AssuranceFlowError(f"conflict {conflict_id} is already resolved additively") if selected_value not in conflict.competing_values: raise AssuranceFlowError("resolution must select one retained competing value") + if conflict.predicate == "status": + if conflict.subject_id in self.graph.artifacts: + try: + ArtifactStatus(selected_value) + except ValueError as exc: + raise AssuranceFlowError( + "artifact status resolution must select a defined status" + ) from exc + elif not selected_value: + raise AssuranceFlowError( + "transformation status resolution must not be empty" + ) if ( proposition.subject_id != conflict.subject_id or proposition.predicate != f"vstd.graph.resolve.{conflict.predicate}" @@ -740,7 +810,7 @@ def record_trust( raise AssuranceFlowError( "TRUST target must be an output of the bound transformation" ) - if transform.status != "COMPLETED": + if self.current_transformation_status(transformation_id) != "COMPLETED": raise AssuranceFlowError("incomplete transformation cannot provide TRUST") if self.current_status(target_id) is not ArtifactStatus.VALID or any( self.current_status(source) is not ArtifactStatus.VALID @@ -749,13 +819,17 @@ def record_trust( raise AssuranceFlowError( "inadmissible target or transformation input cannot provide current TRUST" ) - unresolved_subjects = {item.subject_id for item in self.unresolved_conflicts()} + blocked_subjects = { + item.subject_id for item in self.admissibility_blocking_conflicts() + } if ( - target_id in unresolved_subjects - or transformation_id in unresolved_subjects - or any(source in unresolved_subjects for source in sources) + target_id in blocked_subjects + or transformation_id in blocked_subjects + or any(source in blocked_subjects for source in sources) ): - raise AssuranceFlowError("unresolved conflict blocks clean TRUST") + raise AssuranceFlowError( + "conflict without an admissible current-state consequence blocks clean TRUST" + ) prerequisite_digests = tuple(sorted(set(prerequisite_trust_event_digests))) current_by_digest = { @@ -898,20 +972,35 @@ def localize_cause( descendant_id: str, proposition: BoundProposition, *, + rust_event_digest: str, session: VerificationSession, recorded_at: str, ) -> AssuranceEvent: + """Bind one ancestor to one exact passing descendant-deviation event.""" if ancestor_id not in self.graph.ancestors((descendant_id,)) - {descendant_id}: raise AssuranceFlowError("causal candidate is not a recorded ancestor") - rust_exists = any( - event.kind is AssuranceEventKind.RUST - and event.subject_id == descendant_id - and event.outcome is MechanismOutcome.PASS - for event in self._events + rust_event = next( + (event for event in self._events if event.digest() == rust_event_digest), + None, ) - if not rust_exists: - raise AssuranceFlowError("localization requires a verified descendant deviation") - expected = {"ancestor": ancestor_id, "descendant": descendant_id} + if ( + rust_event is None + or rust_event.kind is not AssuranceEventKind.RUST + or rust_event.outcome is not MechanismOutcome.PASS + or rust_event.subject_id != descendant_id + or ancestor_id not in rust_event.source_ids + ): + raise AssuranceFlowError( + "localization requires the exact passing RUST event for this " + "descendant and ancestor" + ) + deviation_binding_digest = str(rust_event.attributes["binding_digest"]) + expected = { + "ancestor": ancestor_id, + "descendant": descendant_id, + "rust_event_digest": rust_event_digest, + "deviation_binding_digest": deviation_binding_digest, + } if ( proposition.subject_id != descendant_id or proposition.predicate != "vstd.graph.causal_localization" @@ -928,6 +1017,10 @@ def localize_cause( recorded_at=recorded_at, evaluation=evaluation, evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={ + "rust_event_digest": rust_event_digest, + "deviation_binding_digest": deviation_binding_digest, + }, ) def diagnose( @@ -948,6 +1041,11 @@ def diagnose( obligation. Neither result concerns an actor's moral character, reputation, or general trust. """ + requested_localization_digest = "" + if proposition is not None and isinstance(proposition.expected, Mapping): + requested_localization_digest = str( + proposition.expected.get("localization_event_digest", "") + ) localization = next( ( event @@ -956,6 +1054,10 @@ def diagnose( and event.subject_id == descendant_id and event.source_ids == (ancestor_id,) and event.outcome is MechanismOutcome.PASS + and ( + not requested_localization_digest + or event.digest() == requested_localization_digest + ) ), None, ) @@ -1161,6 +1263,7 @@ def recheck_assurance_log( source_ids[0], subject_id, proposition, + rust_event_digest=str(attributes["rust_event_digest"]), session=session, recorded_at=recorded_at, ) diff --git a/src/verifier/specifications/LADDER.md b/src/verifier/specifications/LADDER.md index 0f3a8bb..60dbdca 100644 --- a/src/verifier/specifications/LADDER.md +++ b/src/verifier/specifications/LADDER.md @@ -172,7 +172,11 @@ The reference event mechanism realizes that rule edge by edge. Each TRUST event historical Graph digest, one exact transformation, its complete input artifact set, one output artifact, and the exact prerequisite TRUST event for every derived input. A descendant event is current only while every recursively required event, input, output, -and transformation remains admissible and free of unresolved conflict. Alternate or +and transformation remains admissible and free of an admissibility-blocking conflict. A +status-conflict resolution projects its selected state into the current view: `VALID` or +`COMPLETED` may restore the affected route, while `REVOKED`, `FAILED`, or another +inadmissible state cannot. Resolving an arbitrary predicate selects a retained value but +does not establish its admissibility effect, so the route remains blocked. Alternate or duplicate paths remain distinct recorded routes; their count supplies no added strength or witness independence. @@ -194,6 +198,10 @@ observation, falsehood, or causal responsibility; localization requires addition intervention, ablation, independently bound execution evidence, or an equivalent declared mechanism. +Reference causal localization MUST select one exact passing RUST event, bind that event's +digest and the exact descendant-deviation proposition digest, confirm the selected artifact +is among that event's recorded ancestors, and preserve those coordinates through replay. + **BLAME** and **GUILT** are bounded artifact-relative diagnostic results, not opposite directions on the Graph. BLAME requires a named mechanism to establish that an exact artifact bears responsibility for or materially contributed to an exact localized @@ -202,7 +210,9 @@ plus an exact obligation that the artifact violated. Thus GUILT contains a respo component, while BLAME alone establishes no obligation or violation. Neither term concerns actor morality, character, identity, or reputation. Exoneration, obligation satisfaction, or not-guilty conclusions require their own exact propositions and mechanisms; absent such -evidence the result remains `UNKNOWN`. +evidence the result remains `UNKNOWN`. BLAME and GUILT bind the causal-localization event +digest; that event transitively binds the selected RUST event and exact deviation, so neither +result can float across two deviations on the same descendant. The word *causal* is required here for recorded developmental and provenance causality: the graph states which artifacts and transformations produced later claim architecture. diff --git a/src/verifier/specifications/VSTD-5.md b/src/verifier/specifications/VSTD-5.md index 2f47f1c..acdd75e 100644 --- a/src/verifier/specifications/VSTD-5.md +++ b/src/verifier/specifications/VSTD-5.md @@ -32,8 +32,10 @@ two result types and fails closed. The reference receipt contains: * `WitnessIdentity` — witness coordinate plus content-addressed identity evidence; -* `IndependenceAssertion` — one state and evidence binding for each required - declarant/witness separation dimension; +* an ordered `independence_assertions` array — every supplied + `IndependenceAssertion`, including duplicates, orphan references, and missing + cardinality as an empty or incomplete array, so negative assessment inputs are not + collapsed during serialization; * `CorroborationRecord` — exact VSTD-4 commitment, certificate, checker descriptor, observations, result, time, class, and executable verification binding; * derived disagreements — conflicting checked records retained without voting or @@ -43,7 +45,9 @@ The reference receipt contains: Schema validity establishes only shape. `recheck_vstd5_receipt` imports and hashes the embedded bytes, checks the admitted VSTD-4 result digest, reruns every registered -mechanism, and compares the complete derived result. +mechanism, and compares the complete derived result. Every receipt emitted by +`build_vstd5_receipt`, including `UNKNOWN` / `NOT_ESTABLISHED` error receipts, MUST +preserve the exact error-producing input and recheck identically. --- @@ -106,9 +110,11 @@ another corroboration identifier is rejected rather than counted twice. 6. disagreement derivation; and 7. bounded result emission with all errors and limitations retained. -`build_vstd5_receipt` serializes the replay inputs. `recheck_vstd5_receipt` reruns -them. Neither function turns an identity coordinate into trust or establishes a -fact outside the propositions checked by its registered mechanisms. +`build_vstd5_receipt` serializes witness identities and independence assertions as +separate ordered arrays so duplicate and orphan assertions survive round trip. +`recheck_vstd5_receipt` reruns them. Neither function turns an identity coordinate +into trust or establishes a fact outside the propositions checked by its registered +mechanisms. --- diff --git a/src/verifier/specifications/VSTD-Graph-1.md b/src/verifier/specifications/VSTD-Graph-1.md index 06396d9..b2787bb 100644 --- a/src/verifier/specifications/VSTD-Graph-1.md +++ b/src/verifier/specifications/VSTD-Graph-1.md @@ -81,9 +81,11 @@ outputs. The edge records ancestry; it does not by itself establish causal influ - `evidence_refs` retains at least two evidence records rather than selecting a winner. A conflict record does not mutate the frozen artifact-status vocabulary. It makes the -subject inadmissible to a clean candidate Graph profile. The current reference implementation -has no conflict-resolution transition; later resolution must be additive and must retain the -competing evidence. +subject inadmissible to a clean candidate Graph profile. The VSTD-Graph-1 receipt has no +conflict-resolution transition and remains immutable. The separate non-receipt +`VSTD-GRAPH-ASSURANCE-1` overlay can record additive, mechanism-checked resolution while +retaining the competing evidence. A selected status is projected into that overlay's current +view; resolving any other predicate does not by itself establish a clean admissibility effect. --- diff --git a/standard/LADDER.md b/standard/LADDER.md index 0f3a8bb..60dbdca 100644 --- a/standard/LADDER.md +++ b/standard/LADDER.md @@ -172,7 +172,11 @@ The reference event mechanism realizes that rule edge by edge. Each TRUST event historical Graph digest, one exact transformation, its complete input artifact set, one output artifact, and the exact prerequisite TRUST event for every derived input. A descendant event is current only while every recursively required event, input, output, -and transformation remains admissible and free of unresolved conflict. Alternate or +and transformation remains admissible and free of an admissibility-blocking conflict. A +status-conflict resolution projects its selected state into the current view: `VALID` or +`COMPLETED` may restore the affected route, while `REVOKED`, `FAILED`, or another +inadmissible state cannot. Resolving an arbitrary predicate selects a retained value but +does not establish its admissibility effect, so the route remains blocked. Alternate or duplicate paths remain distinct recorded routes; their count supplies no added strength or witness independence. @@ -194,6 +198,10 @@ observation, falsehood, or causal responsibility; localization requires addition intervention, ablation, independently bound execution evidence, or an equivalent declared mechanism. +Reference causal localization MUST select one exact passing RUST event, bind that event's +digest and the exact descendant-deviation proposition digest, confirm the selected artifact +is among that event's recorded ancestors, and preserve those coordinates through replay. + **BLAME** and **GUILT** are bounded artifact-relative diagnostic results, not opposite directions on the Graph. BLAME requires a named mechanism to establish that an exact artifact bears responsibility for or materially contributed to an exact localized @@ -202,7 +210,9 @@ plus an exact obligation that the artifact violated. Thus GUILT contains a respo component, while BLAME alone establishes no obligation or violation. Neither term concerns actor morality, character, identity, or reputation. Exoneration, obligation satisfaction, or not-guilty conclusions require their own exact propositions and mechanisms; absent such -evidence the result remains `UNKNOWN`. +evidence the result remains `UNKNOWN`. BLAME and GUILT bind the causal-localization event +digest; that event transitively binds the selected RUST event and exact deviation, so neither +result can float across two deviations on the same descendant. The word *causal* is required here for recorded developmental and provenance causality: the graph states which artifacts and transformations produced later claim architecture. diff --git a/standard/VSTD-5.md b/standard/VSTD-5.md index 2f47f1c..acdd75e 100644 --- a/standard/VSTD-5.md +++ b/standard/VSTD-5.md @@ -32,8 +32,10 @@ two result types and fails closed. The reference receipt contains: * `WitnessIdentity` — witness coordinate plus content-addressed identity evidence; -* `IndependenceAssertion` — one state and evidence binding for each required - declarant/witness separation dimension; +* an ordered `independence_assertions` array — every supplied + `IndependenceAssertion`, including duplicates, orphan references, and missing + cardinality as an empty or incomplete array, so negative assessment inputs are not + collapsed during serialization; * `CorroborationRecord` — exact VSTD-4 commitment, certificate, checker descriptor, observations, result, time, class, and executable verification binding; * derived disagreements — conflicting checked records retained without voting or @@ -43,7 +45,9 @@ The reference receipt contains: Schema validity establishes only shape. `recheck_vstd5_receipt` imports and hashes the embedded bytes, checks the admitted VSTD-4 result digest, reruns every registered -mechanism, and compares the complete derived result. +mechanism, and compares the complete derived result. Every receipt emitted by +`build_vstd5_receipt`, including `UNKNOWN` / `NOT_ESTABLISHED` error receipts, MUST +preserve the exact error-producing input and recheck identically. --- @@ -106,9 +110,11 @@ another corroboration identifier is rejected rather than counted twice. 6. disagreement derivation; and 7. bounded result emission with all errors and limitations retained. -`build_vstd5_receipt` serializes the replay inputs. `recheck_vstd5_receipt` reruns -them. Neither function turns an identity coordinate into trust or establishes a -fact outside the propositions checked by its registered mechanisms. +`build_vstd5_receipt` serializes witness identities and independence assertions as +separate ordered arrays so duplicate and orphan assertions survive round trip. +`recheck_vstd5_receipt` reruns them. Neither function turns an identity coordinate +into trust or establishes a fact outside the propositions checked by its registered +mechanisms. --- diff --git a/standard/VSTD-Graph-1.md b/standard/VSTD-Graph-1.md index 06396d9..b2787bb 100644 --- a/standard/VSTD-Graph-1.md +++ b/standard/VSTD-Graph-1.md @@ -81,9 +81,11 @@ outputs. The edge records ancestry; it does not by itself establish causal influ - `evidence_refs` retains at least two evidence records rather than selecting a winner. A conflict record does not mutate the frozen artifact-status vocabulary. It makes the -subject inadmissible to a clean candidate Graph profile. The current reference implementation -has no conflict-resolution transition; later resolution must be additive and must retain the -competing evidence. +subject inadmissible to a clean candidate Graph profile. The VSTD-Graph-1 receipt has no +conflict-resolution transition and remains immutable. The separate non-receipt +`VSTD-GRAPH-ASSURANCE-1` overlay can record additive, mechanism-checked resolution while +retaining the competing evidence. A selected status is projected into that overlay's current +view; resolving any other predicate does not by itself establish a clean admissibility effect. --- diff --git a/standard/schemas/vstd-graph-assurance-1.schema.json b/standard/schemas/vstd-graph-assurance-1.schema.json index 32cb250..08dbea3 100644 --- a/standard/schemas/vstd-graph-assurance-1.schema.json +++ b/standard/schemas/vstd-graph-assurance-1.schema.json @@ -75,6 +75,14 @@ { "if": {"properties": {"kind": {"const": "CONFLICT_DECLARATION"}}, "required": ["kind"]}, "then": {"properties": {"attributes": {"$ref": "#/$defs/conflictAttributes"}}} + }, + { + "if": {"properties": {"kind": {"const": "CONFLICT_RESOLUTION"}}, "required": ["kind"]}, + "then": {"properties": {"attributes": {"$ref": "#/$defs/resolutionAttributes"}}} + }, + { + "if": {"properties": {"kind": {"const": "CAUSAL_LOCALIZATION"}}, "required": ["kind"]}, + "then": {"properties": {"attributes": {"$ref": "#/$defs/localizationAttributes"}}} } ] }, @@ -111,6 +119,27 @@ } } }, + "resolutionAttributes": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "conflict_id", "selected_value", "resolution_id"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "conflict_id": {"type": "string", "minLength": 1}, + "selected_value": {"type": "string"}, + "resolution_id": {"type": "string", "minLength": 1} + } + }, + "localizationAttributes": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "rust_event_digest", "deviation_binding_digest"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "rust_event_digest": {"$ref": "#/$defs/digest"}, + "deviation_binding_digest": {"$ref": "#/$defs/digest"} + } + }, "binding": { "type": "object", "additionalProperties": false, diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py index f7217a0..63e4f2d 100644 --- a/tests/test_evidence_bound_assurance.py +++ b/tests/test_evidence_bound_assurance.py @@ -7,6 +7,7 @@ from __future__ import annotations import copy +from dataclasses import replace import hashlib import json from pathlib import Path @@ -837,7 +838,7 @@ def test_challenge_recovery_does_not_undo_independent_rot() -> None: ) -def test_conflict_resolution_is_additive_and_mechanism_bound() -> None: +def test_non_status_conflict_resolution_remains_admissibility_blocking() -> None: graph = _graph() graph.add_conflict( ConflictRecord( @@ -866,7 +867,10 @@ def test_conflict_resolution_is_additive_and_mechanism_bound() -> None: ) assert "conflict:digest" in graph.conflicts assert ledger.unresolved_conflicts() == () - assert ledger.materialize_current_graph().conflicts == {} + assert tuple( + item.conflict_id for item in ledger.admissibility_blocking_conflicts() + ) == ("conflict:digest",) + assert "conflict:digest" in ledger.materialize_current_graph().conflicts def test_trust_rot_and_rust_follow_direction_without_recursive_amplification() -> None: @@ -987,7 +991,7 @@ def test_intermediate_degradation_excludes_but_does_not_rewrite_trust( @pytest.mark.parametrize("conflict_subject", ("middle", "first")) -def test_dependency_conflict_invalidates_and_additive_resolution_restores_trust( +def test_non_status_resolution_does_not_manufacture_current_trust( conflict_subject: str, ) -> None: ledger = AssuranceLedger(_graph()) @@ -1034,10 +1038,17 @@ def test_dependency_conflict_invalidates_and_additive_resolution_restores_trust( session=session, recorded_at="2026-08-29T00:02:00Z", ) - assert tuple(event.digest() for event in ledger.current_trust_events()) == ( - first.digest(), - second.digest(), - ) + assert ledger.current_trust_events() == () + assert conflict.conflict_id in ledger.materialize_current_graph().conflicts + with pytest.raises(AssuranceFlowError, match="admissible current-state consequence"): + ledger.record_trust( + "middle", + ("source",), + _trust_proposition(store, ledger, "first", "middle"), + transformation_id="first", + session=session, + recorded_at="2026-08-29T00:02:01Z", + ) root = Path(__file__).resolve().parents[1] schema = json.loads( (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() @@ -1052,18 +1063,91 @@ def test_dependency_conflict_invalidates_and_additive_resolution_restores_trust( replayed = recheck_assurance_log( ledger.to_dict(), mechanisms=(ExactFactMechanism(),) ) - assert tuple(event.digest() for event in replayed.current_trust_events()) == ( - first.digest(), - second.digest(), + assert replayed.current_trust_events() == () + assert conflict.conflict_id in replayed.materialize_current_graph().conflicts + + +@pytest.mark.parametrize( + ("conflict_subject", "selected_value", "expected_current"), + ( + ("middle", "VALID", True), + ("middle", "REVOKED", False), + ("first", "COMPLETED", True), + ("first", "FAILED", False), + ), +) +def test_status_resolution_projects_current_admissibility_and_replays( + conflict_subject: str, + selected_value: str, + expected_current: bool, +) -> None: + ledger = AssuranceLedger(_graph()) + store, session = _session() + first, second = _record_trust_chain(ledger, store, session) + competing = ( + ("VALID", "REVOKED") + if conflict_subject == "middle" + else ("COMPLETED", "FAILED") + ) + conflict = ConflictRecord( + f"conflict:{conflict_subject}-status", + conflict_subject, + "status", + competing, + ("evidence:admissible", "evidence:inadmissible"), + ) + ledger.record_conflict( + conflict, + _proposition(store, conflict_subject, "vstd.graph.conflict", conflict.to_dict()), + session=session, + recorded_at="2026-08-29T00:01:00Z", + ) + assert ledger.current_trust_events() == () + ledger.resolve_conflict( + conflict.conflict_id, + selected_value, + _proposition( + store, + conflict_subject, + "vstd.graph.resolve.status", + selected_value, + parameters={"conflict_id": conflict.conflict_id}, + ), + session=session, + recorded_at="2026-08-29T00:02:00Z", ) + expected_digests = (first.digest(), second.digest()) if expected_current else () + assert tuple(event.digest() for event in ledger.current_trust_events()) == expected_digests + if conflict_subject == "middle": + assert ledger.current_status("middle").value == selected_value + else: + assert ledger.current_transformation_status("first") == selected_value + assert conflict.conflict_id not in ledger.materialize_current_graph().conflicts + + serialized = ledger.to_dict() + assert any( + event["kind"] == AssuranceEventKind.CONFLICT_DECLARATION.value + and event["attributes"]["conflict"]["conflict_id"] == conflict.conflict_id + for event in serialized["events"] + ) + assert serialized["conflict_resolutions"][0]["conflict_id"] == conflict.conflict_id + replayed = recheck_assurance_log( + serialized, mechanisms=(ExactFactMechanism(),) + ) + assert tuple(event.digest() for event in replayed.current_trust_events()) == expected_digests + if conflict_subject == "middle": + assert replayed.current_status("middle").value == selected_value + else: + assert replayed.current_transformation_status("first") == selected_value + def test_rust_requires_separate_localization_before_blame_or_guilt() -> None: graph = _graph() ledger = AssuranceLedger(graph) store, session = _session() rust = _proposition(store, "result", "vstd.graph.descendant_deviation", True) - ledger.record_rust( + rust_event = ledger.record_rust( "result", rust, session=session, recorded_at="2026-08-29T00:00:00Z" ) refused = ledger.diagnose( @@ -1080,10 +1164,16 @@ def test_rust_requires_separate_localization_before_blame_or_guilt() -> None: store, "result", "vstd.graph.causal_localization", - {"ancestor": "source", "descendant": "result"}, + { + "ancestor": "source", + "descendant": "result", + "rust_event_digest": rust_event.digest(), + "deviation_binding_digest": rust.digest(), + }, ) event = ledger.localize_cause( "source", "result", localization, + rust_event_digest=rust_event.digest(), session=session, recorded_at="2026-08-29T00:02:00Z", ) attribution = _proposition( @@ -1152,6 +1242,218 @@ def test_rust_requires_separate_localization_before_blame_or_guilt() -> None: assert guilt_result.evaluation.passed is True +def test_localization_selects_one_exact_passing_deviation() -> None: + ledger = AssuranceLedger(_graph()) + store, session = _session() + first_deviation = _proposition( + store, + "result", + "vstd.graph.descendant_deviation", + True, + parameters={"deviation_id": "D1"}, + ) + second_deviation = _proposition( + store, + "result", + "vstd.graph.descendant_deviation", + True, + parameters={"deviation_id": "D2"}, + ) + first_rust = ledger.record_rust( + "result", + first_deviation, + session=session, + recorded_at="2026-08-29T00:00:00Z", + ) + second_rust = ledger.record_rust( + "result", + second_deviation, + session=session, + recorded_at="2026-08-29T00:01:00Z", + ) + + with pytest.raises(AssuranceFlowError, match="exact passing RUST event"): + ledger.localize_cause( + "source", + "result", + _proposition( + store, + "result", + "vstd.graph.causal_localization", + { + "ancestor": "source", + "descendant": "result", + "rust_event_digest": "0" * 64, + "deviation_binding_digest": first_deviation.digest(), + }, + ), + rust_event_digest="0" * 64, + session=session, + recorded_at="2026-08-29T00:02:00Z", + ) + + neighboring_rust = ledger.record_rust( + "middle", + _proposition( + store, + "middle", + "vstd.graph.descendant_deviation", + True, + parameters={"deviation_id": "neighbor"}, + ), + session=session, + recorded_at="2026-08-29T00:03:00Z", + ) + with pytest.raises(AssuranceFlowError, match="exact passing RUST event"): + ledger.localize_cause( + "source", + "result", + _proposition( + store, + "result", + "vstd.graph.causal_localization", + { + "ancestor": "source", + "descendant": "result", + "rust_event_digest": neighboring_rust.digest(), + "deviation_binding_digest": str( + neighboring_rust.attributes["binding_digest"] + ), + }, + ), + rust_event_digest=neighboring_rust.digest(), + session=session, + recorded_at="2026-08-29T00:04:00Z", + ) + + failed_reference = store.add( + json.dumps( + { + "subject_id": "result", + "predicate": "vstd.graph.descendant_deviation", + "expected": False, + }, + sort_keys=True, + separators=(",", ":"), + ).encode() + ) + failed_deviation = replace(first_deviation, evidence_refs=(failed_reference,)) + failed_rust = ledger.record_rust( + "result", + failed_deviation, + session=session, + recorded_at="2026-08-29T00:05:00Z", + ) + assert failed_rust.outcome is MechanismOutcome.FAIL + with pytest.raises(AssuranceFlowError, match="exact passing RUST event"): + ledger.localize_cause( + "source", + "result", + _proposition( + store, + "result", + "vstd.graph.causal_localization", + { + "ancestor": "source", + "descendant": "result", + "rust_event_digest": failed_rust.digest(), + "deviation_binding_digest": failed_deviation.digest(), + }, + ), + rust_event_digest=failed_rust.digest(), + session=session, + recorded_at="2026-08-29T00:06:00Z", + ) + + missing_ancestor_ledger = AssuranceLedger(_graph()) + missing_ancestor_store, missing_ancestor_session = _session() + complete_rust = missing_ancestor_ledger.record_rust( + "result", + _proposition( + missing_ancestor_store, + "result", + "vstd.graph.descendant_deviation", + True, + ), + session=missing_ancestor_session, + recorded_at="2026-08-29T00:06:01Z", + ) + missing_ancestor_event = replace(complete_rust, source_ids=("middle",)) + missing_ancestor_ledger._events[0] = missing_ancestor_event + with pytest.raises(AssuranceFlowError, match="exact passing RUST event"): + missing_ancestor_ledger.localize_cause( + "source", + "result", + _proposition( + missing_ancestor_store, + "result", + "vstd.graph.causal_localization", + { + "ancestor": "source", + "descendant": "result", + "rust_event_digest": missing_ancestor_event.digest(), + "deviation_binding_digest": str( + missing_ancestor_event.attributes["binding_digest"] + ), + }, + ), + rust_event_digest=missing_ancestor_event.digest(), + session=missing_ancestor_session, + recorded_at="2026-08-29T00:06:02Z", + ) + + localization = ledger.localize_cause( + "source", + "result", + _proposition( + store, + "result", + "vstd.graph.causal_localization", + { + "ancestor": "source", + "descendant": "result", + "rust_event_digest": first_rust.digest(), + "deviation_binding_digest": first_deviation.digest(), + }, + ), + rust_event_digest=first_rust.digest(), + session=session, + recorded_at="2026-08-29T00:07:00Z", + ) + assert localization.attributes["rust_event_digest"] == first_rust.digest() + assert localization.attributes["deviation_binding_digest"] == first_deviation.digest() + assert second_rust.digest() not in json.dumps(localization.to_dict()) + + blame = _proposition( + store, + "source", + "vstd.graph.diagnostic.blame", + { + "ancestor": "source", + "descendant": "result", + "localization_event_digest": localization.digest(), + }, + ) + assert ledger.diagnose( + DiagnosticKind.BLAME, + "source", + "result", + blame, + session=session, + recorded_at="2026-08-29T00:08:00Z", + ).status == "ESTABLISHED" + + tampered = copy.deepcopy(ledger.to_dict()) + localization_record = next( + event + for event in tampered["events"] + if event["kind"] == AssuranceEventKind.CAUSAL_LOCALIZATION.value + ) + localization_record["attributes"]["rust_event_digest"] = second_rust.digest() + with pytest.raises(AssuranceFlowError): + recheck_assurance_log(tampered, mechanisms=(ExactFactMechanism(),)) + + def test_assurance_event_log_is_portable_strict_and_evidence_complete() -> None: graph = _graph() ledger = AssuranceLedger(graph) @@ -1495,3 +1797,103 @@ def test_computed_independence_fails_closed_on_identity_and_assertion_errors() - ) assert duplicate_assertion.computed_independence == "UNKNOWN" assert any("duplicate independence assertion" in item for item in duplicate_assertion.separation_errors) + + +def test_vstd5_receipts_preserve_noncanonical_replay_inputs() -> None: + store, session = _session() + entry = _established_vstd4(store, session) + binding_digest = entry.witness.header.binding # type: ignore[union-attr] + witness, assertion, corroboration = _witness_components( + store, entry, "witness:one" + ) + shared_identity = store.add(b"shared identity evidence for replay") + first = _witness_components( + store, + entry, + "witness:first", + identity_evidence_ref=shared_identity, + ) + second = _witness_components( + store, + entry, + "witness:second", + identity_evidence_ref=shared_identity, + ) + orphan = replace(assertion, witness_id="witness:orphan") + bundles = { + "duplicate-assertion": WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (assertion, assertion), + (corroboration,), + ), + "orphan-assertion": WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (assertion, orphan), + (corroboration,), + ), + "duplicate-witness": WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness, witness), + (assertion,), + (corroboration,), + ), + "missing-assertion": WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (), + (corroboration,), + ), + "reused-identity": WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (first[0], second[0]), + (first[1], second[1]), + (first[2], second[2]), + ), + } + root = Path(__file__).resolve().parents[1] + schema = json.loads((root / "receipts/schema/vstd5_receipt.json").read_text()) + vstd4_schema = json.loads( + (root / "receipts/schema/vstd4_receipt.json").read_text() + ) + assurance_schema = json.loads( + (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() + ) + registry = Registry() + registry = registry.with_resource( + vstd4_schema["$id"], Resource.from_contents(vstd4_schema) + ) + registry = registry.with_resource( + assurance_schema["$id"], Resource.from_contents(assurance_schema) + ) + validator = Draft202012Validator(schema, registry=registry) + + for name, bundle in bundles.items(): + result = assess_witness_corroboration(entry, bundle, session=session) + assert result.conformance_status == "NOT_ESTABLISHED", name + receipt = build_vstd5_receipt( + entry, + bundle, + result, + receipt_id=f"VFY-5-{name.upper()}", + session=session, + ) + validator.validate(receipt) + assert len(receipt["bundle"]["independence_assertions"]) == len( + bundle.independence + ) + rechecked = recheck_vstd5_receipt( + entry, receipt, mechanisms=(ExactFactMechanism(),) + ) + assert rechecked.to_dict() == result.to_dict(), name From 31810f661156c3a3390c1a2f0e7194b826a40ed0 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 10:06:47 -0700 Subject: [PATCH 19/34] Enforce strict portable receipt boundaries Reason: Close the remaining VSTD-5 semantic-review blocker: assessment of malformed inputs is diagnostic, while a supported receipt builder and rechecker must never name a schema-invalid object as a portable receipt. Evidence: At parent 8893ff6f, empty witness or corroboration sets, empty claim/declarant/witness identifiers, and invalid receipt identifiers could be emitted and replayed even though vstd5_receipt.json rejected them. Artifact and transformation identifiers could also overlap, leaving the Graph assurance subject coordinate ambiguous. Mechanism: Add a zero-dependency structural and verdict-evidence gate shared by build_vstd5_receipt and recheck_vstd5_receipt; retain permissive UNKNOWN / NOT_ESTABLISHED assessment; keep representable duplicate, orphan, missing, reused-identity, and disagreement receipts replayable; require globally disjoint Graph object identifiers; and align the Standard, architecture, stability, and release-candidate surfaces. Falsification: Reject this change if the builder returns any tested schema-invalid receipt, if the rechecker accepts malformed external shape before mechanism replay, if valid positive or representable negative receipts fail strict schema validation and exact replay, or if an artifact/transformation identifier collision survives Graph validation. Compatibility and wire impact: The strict VSTD-5 schema is unchanged. This repairs the supported unreleased builder/rechecker contract before 1.2.0 and narrows portable admission without weakening malformed-input diagnostics. Graph identifiers become globally disjoint because the existing assurance subject field is untyped. Historical receipt bytes remain unchanged; no released profile identifier is redefined. --- CHANGELOG.md | 9 +- README.md | 14 +- docs/API_STABILITY.md | 6 + docs/ARCHITECTURE.md | 13 +- src/verifier/core/witness.py | 277 +++++++++++++++++++- src/verifier/data/models.py | 15 ++ src/verifier/specifications/LADDER.md | 17 +- src/verifier/specifications/VSTD-5.md | 23 +- src/verifier/specifications/VSTD-Graph-1.md | 5 + standard/LADDER.md | 17 +- standard/VSTD-5.md | 23 +- standard/VSTD-Graph-1.md | 5 + tests/test_evidence_bound_assurance.py | 164 +++++++++--- tests/test_public_data.py | 34 +++ 14 files changed, 550 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe23964..af2b1fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,7 +107,10 @@ fails closed on any identity or separation defect without parsing error-message text. Witness identities and assertions serialize separately and in order, so duplicate, orphan, missing, and reused-identity error inputs remain replayable instead of collapsing - during receipt construction. This does not + during receipt construction. Keep permissive malformed-input assessment distinct from + portable receipt admission: the builder now raises unless the strict schema and complete + verdict-material evidence coverage hold, and the rechecker applies the same zero-dependency + gate before replay. This does not claim a real external witness or independent implementation; a positive observation with unresolved independence is overall `UNKNOWN`. - Add evidence-bound Graph profile computation and replay. The compatibility `graph_level` @@ -125,6 +128,10 @@ obligation and is not BLAME in the opposite direction. Localization binds one exact passing RUST event and descendant-deviation proposition. Status-conflict resolution projects the selected state into current admissibility; arbitrary resolved predicates remain blocked. + The current runtime has no general non-status admissibility-effect mechanism. RUST follows + historically recorded contributing ancestry even when current lifecycle state excludes a + route from TRUST. Artifact and transformation identifiers are globally disjoint so an + assurance `subject_id` cannot ambiguously name both. Add complete offline event replay, current TRUST filtering, and deduplicated descendant reassessment discovery. diff --git a/README.md b/README.md index 1d26594..28d987c 100644 --- a/README.md +++ b/README.md @@ -175,9 +175,10 @@ development, with each event binding one exact transformation, its inputs and ou historical Graph digest, and any prerequisite TRUST events; ROT is typed, time-indexed degradation of current admissibility without rewriting historical evidence; RUST is the inverse-TRUST diagnostic mechanic moving backward from a -descendant deviation toward recorded ancestor candidates. This memetic propagation does -not by itself prove guilt, responsibility, falsehood, causal localization, or automatic -ancestor falsification. The reference runtime requires a separate passing localization +descendant deviation through historically recorded contributing ancestry. Current +revocation or conflict can remove a route from current TRUST without erasing that diagnostic +history. This memetic propagation does not by itself prove guilt, responsibility, falsehood, +causal localization, or automatic ancestor falsification. The reference runtime requires a separate passing localization mechanism bound to one exact passing RUST event and descendant-deviation proposition before it can emit a bounded `BLAME` or `GUILT`. BLAME establishes bounded responsibility or material contribution; @@ -190,7 +191,9 @@ embedded evidence bytes, event hash chain, and derived current-view digest. `recheck_assurance_log` rehashes the evidence, reruns each exact registered mechanism, and rejects any event or current view that does not reproduce. Conflict adjudication and current admissibility are separate: a selected status affects the current artifact or transformation -state, while resolving an arbitrary predicate cannot silently restore TRUST. +state, while resolving an arbitrary predicate cannot silently restore TRUST. No general +non-status admissibility-effect mechanism is currently implemented, so that route remains +blocked. This is VSTD's **ZIZK artifact-first TRUST architecture**, not an optional research profile. Zero identity means zero identity-derived verdict weight, not anonymity or the @@ -299,7 +302,8 @@ reference](https://timelordraps.github.io/verifier/reference.html). - [VSTD-1 receipts](standard/VSTD-1.md) carry claim coordinates, evidence, checker results, trust boundaries, and reproducibility information. - [VSTD-Graph-1](standard/VSTD-Graph-1.md) records content-addressed artifacts, - many-to-many transformations, conflicts, and bounded downstream reachability. + many-to-many transformations, globally disjoint artifact/transformation identifiers, + conflicts, and bounded downstream reachability. - [`VSTD4-GDC-1`](standard/VSTD-4.md) binds a decision certificate to a formula, grounding, claim coordinate, verifier descriptor, roots, and resource bounds. diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md index 6ea80dc..d8d984f 100644 --- a/docs/API_STABILITY.md +++ b/docs/API_STABILITY.md @@ -35,6 +35,12 @@ 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. +`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. +`recheck_vstd5_receipt` applies the same zero-dependency structural gate before replay and +does not accept a schema-invalid assessment object as a portable receipt. + 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 d410fec..9893e9f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -305,12 +305,17 @@ rerun by registered mechanisms. Typed binding, identity, separation, and corrobo errors keep `computed_independence` fail-closed; no error-message text is interpreted as a semantic category. Witness identities and independence assertions serialize as separate ordered arrays, so duplicates, orphan assertions, and missing cardinality survive receipt -build and replay rather than disappearing inside a keyed map. This path does not upgrade the +build and replay rather than disappearing inside a keyed map. Permissive assessment remains +separate from portable-record admission: the builder raises unless the result inhabits the +strict VSTD-5 schema and embeds every verdict-material evidence byte, and the rechecker +enforces the same zero-dependency shape gate before replay. This path does not upgrade the legacy generic-run fields. Graph conflict records retain incompatible values and their evidence references without adding a scalar score or changing the frozen artifact-status vocabulary. A conflict makes -the subject inadmissible to a clean candidate Graph profile. +the subject inadmissible to a clean candidate Graph profile. Artifact and transformation +identifiers are globally disjoint, so the conflict overlay's `subject_id` cannot ambiguously +name both kinds of Graph object. ### Recursive current-state audit @@ -321,9 +326,9 @@ question is a new assessment over the retained graph and applicable lifecycle re |---|---| | An ancestor is `CHALLENGED`, `REVOKED`, or `STALE` | Candidate/evidence-bound Graph recomputation follows the full ancestor closure and returns compatibility field `level = 0`. `AssuranceLedger` also records typed ROT or projects append-only challenge-ledger state into a derived current view; historical Graph bytes remain unchanged. | | An ancestor is `SUPERSEDED` | The historical Graph candidate remains admissible by design; the stricter all-ancestors-`VALID` policy rejects it for current-use admission. Supersession does not retroactively falsify its prior lineage role. | -| Upstream evidence conflicts | A retained `ConflictRecord` or a mechanism-established `CONFLICT_DECLARATION` event blocks every dependent edge-local TRUST route. `resolve_conflict` accepts only a mechanism-passing proposition bound to the exact conflict and one retained competing value. A status resolution projects the selected artifact or transformation state: only `VALID` / `COMPLETED` can restore the route. An arbitrary resolved predicate remains admissibility-blocking because value adjudication does not establish support effect. The original conflict and resolution evidence remain historical. | +| Upstream evidence conflicts | A retained `ConflictRecord` or a mechanism-established `CONFLICT_DECLARATION` event blocks every dependent edge-local TRUST route. `resolve_conflict` accepts only a mechanism-passing proposition bound to the exact conflict and one retained competing value. A status resolution projects the selected artifact or transformation state: only `VALID` / `COMPLETED` can restore the route. An arbitrary resolved predicate remains admissibility-blocking because value adjudication does not establish support effect; no general non-status admissibility-effect mechanism is implemented. The original conflict and resolution evidence remain historical. | | Evidence arrives by multiple paths or one run receipt repeats a reference | Reachability and impact sets deduplicate identifiers. Multiplicity supplies no independence or strength. | -| A descendant deviation points toward shared ancestors | A mechanism-passing deviation emits RUST over the deduplicated recorded ancestor set. Structural concentration counts unique deviating descendants, not paths or causal strength. Localization selects and binds one exact passing RUST event, its descendant-deviation binding digest, and an ancestor contained in that event. BLAME and GUILT bind that localization event and require separate exact mechanisms. | +| A descendant deviation points toward shared ancestors | A mechanism-passing deviation emits RUST over the deduplicated historically recorded contributing ancestor set. Current revocation or conflict does not erase diagnostic history. Structural concentration counts unique deviating descendants, not paths or causal strength. Localization selects and binds one exact passing RUST event, its descendant-deviation binding digest, and an ancestor contained in that event. BLAME and GUILT bind that localization event and require separate exact mechanisms. | | A challenge ledger changes a claim's current status | `project_challenges` binds its complete append-only records into a current Graph overlay and embeds those records for replay. Existing TRUST remains historical, while recursively dependent events disappear from `current_trust_events`; `impacted_descendants` reports the deduplicated reassessment surface. It never mutates the historical graph. | | Later evidence adjudicates a conflict | The additive resolution retains the original competing evidence and its mechanism evaluation. VSTD-Graph-1 receipts remain immutable; the separate assurance overlay owns the resolution and current-state projection. Removing or rewriting historical evidence remains invalid. | | Candidate calculation encounters cyclic ancestry | Rejected before candidate calculation; recursive topology cannot manufacture assurance. | diff --git a/src/verifier/core/witness.py b/src/verifier/core/witness.py index 6f0e70c..82cd124 100644 --- a/src/verifier/core/witness.py +++ b/src/verifier/core/witness.py @@ -1,4 +1,5 @@ -"""Terminology: Verifier Standard (VSTD). +"""Terminology: identifier (ID); Request for Comments (RFC); Secure Hash Algorithm +256-bit (SHA-256); Verifier Standard (VSTD). Evidence-bound VSTD-5 Witness Corroboration reference mechanism. @@ -12,7 +13,9 @@ from __future__ import annotations from dataclasses import dataclass, field +from datetime import datetime from enum import Enum +import re from typing import Any, Mapping from .depth import EvidenceBoundDepthResult, require_vstd5_entry @@ -259,6 +262,271 @@ def to_dict(self) -> dict[str, Any]: } +_RECEIPT_ID = re.compile(r"^VFY-5-[A-Za-z0-9._:-]+$") +_RAW_DIGEST = re.compile(r"^[0-9a-f]{64}$") +_DIGEST_REF = re.compile(r"^(?:sha256:)?[0-9a-f]{64}$") +_PREFIXED_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +_DATE_TIME = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$" +) + + +def _shape_error(path: str, message: str) -> None: + raise ValueError(f"invalid VSTD-5 receipt shape at {path}: {message}") + + +def _object(value: Any, path: str, keys: set[str]) -> Mapping[str, Any]: + if not isinstance(value, Mapping) or set(value) != keys: + _shape_error(path, f"must be an object with exactly {sorted(keys)}") + return value + + +def _array(value: Any, path: str, minimum: int = 0) -> list[Any]: + if not isinstance(value, list) or len(value) < minimum: + _shape_error(path, f"must be an array with at least {minimum} item(s)") + return value + + +def _text(value: Any, path: str, nonempty: bool = False) -> str: + if not isinstance(value, str) or (nonempty and not value): + _shape_error(path, "must be a nonempty string" if nonempty else "must be a string") + return value + + +def _sha(value: Any, path: str, form: str = "raw") -> str: + value = _text(value, path) + pattern = {"raw": _RAW_DIGEST, "ref": _DIGEST_REF, "prefixed": _PREFIXED_DIGEST}[form] + if pattern.fullmatch(value) is None: + _shape_error(path, f"must be a {form} SHA-256 digest") + return value + + +def _strings( + value: Any, + path: str, + minimum: int = 0, + *, + unique: bool = False, + nonempty: bool = False, +) -> list[str]: + values = _array(value, path, minimum) + for index, item in enumerate(values): + _text(item, f"{path}[{index}]", nonempty) + if unique and len(set(values)) != len(values): + _shape_error(path, "must not contain duplicates") + return values + + +def _evidence_refs( + value: Any, + path: str, + required_payloads: set[str], + minimum: int = 0, + *, + prefixed: bool = False, +) -> list[str]: + references = _array(value, path, minimum) + normalized: list[str] = [] + for index, reference in enumerate(references): + reference = _sha(reference, f"{path}[{index}]", "prefixed" if prefixed else "ref") + normalized.append("sha256:" + reference.removeprefix("sha256:")) + if len(set(normalized)) != len(normalized): + _shape_error(path, "must not contain duplicates") + required_payloads.update(normalized) + return normalized + + +def _binding_shape(value: Any, path: str, required_payloads: set[str]) -> None: + binding = _object( + value, + path, + { + "subject_id", "predicate", "expected", "mechanism_id", "mechanism_digest", + "evidence_refs", "trust_roots", "bounds", "parameters", + }, + ) + for field_name in ("subject_id", "predicate", "mechanism_id"): + _text(binding[field_name], f"{path}.{field_name}", True) + _sha(binding["mechanism_digest"], f"{path}.mechanism_digest", "prefixed") + _evidence_refs(binding["evidence_refs"], f"{path}.evidence_refs", required_payloads, 1, prefixed=True) + _strings(binding["trust_roots"], f"{path}.trust_roots", 1, unique=True, nonempty=True) + bounds = _object( + binding["bounds"], f"{path}.bounds", {"max_evidence_items", "max_evidence_bytes"} + ) + if any(type(bounds[name]) is not int or bounds[name] < 0 for name in bounds): + _shape_error(f"{path}.bounds", "values must be nonnegative integers") + parameters = binding["parameters"] + if not isinstance(parameters, Mapping) or any( + not isinstance(key, str) or not isinstance(item, str) for key, item in parameters.items() + ): + _shape_error(f"{path}.parameters", "must map strings to strings") + + +def _evaluation_shape(value: Any, path: str) -> None: + evaluation = _object( + value, + path, + { + "binding_digest", "outcome", "mechanism_id", "mechanism_digest", + "evidence_refs", "trust_roots", "observed_evidence_bytes", "details", + "observations", + }, + ) + _sha(evaluation["binding_digest"], f"{path}.binding_digest") + if evaluation["outcome"] not in {item.value for item in MechanismOutcome}: + _shape_error(f"{path}.outcome", "is not a mechanism outcome") + _text(evaluation["mechanism_id"], f"{path}.mechanism_id", True) + _sha(evaluation["mechanism_digest"], f"{path}.mechanism_digest", "ref") + references: set[str] = set() + _evidence_refs(evaluation["evidence_refs"], f"{path}.evidence_refs", references) + _strings(evaluation["trust_roots"], f"{path}.trust_roots", 1, unique=True, nonempty=True) + if type(evaluation["observed_evidence_bytes"]) is not int or evaluation["observed_evidence_bytes"] < 0: + _shape_error(f"{path}.observed_evidence_bytes", "must be a nonnegative integer") + _text(evaluation["details"], f"{path}.details") + if not isinstance(evaluation["observations"], Mapping): + _shape_error(f"{path}.observations", "must be an object") + + +def _result_shape(value: Any, path: str) -> None: + result = _object( + value, + path, + { + "claim_id", "status", "conformance_status", "computed_independence", + "independence_evaluations", "corroboration_evaluations", "disagreements", + "binding_errors", "identity_errors", "separation_errors", + "corroboration_errors", "errors", "limitations", + }, + ) + _text(result["claim_id"], f"{path}.claim_id", True) + if result["status"] not in {item.value for item in WitnessResultStatus}: + _shape_error(f"{path}.status", "is not a witness result") + if result["conformance_status"] not in {"ESTABLISHED", "NOT_ESTABLISHED"}: + _shape_error(f"{path}.conformance_status", "is not a conformance status") + if result["computed_independence"] not in {"INDEPENDENT", "UNKNOWN"}: + _shape_error(f"{path}.computed_independence", "is not an independence result") + for field_name, id_name in ( + ("independence_evaluations", "witness_id"), + ("corroboration_evaluations", "corroboration_id"), + ): + for index, item in enumerate(_array(result[field_name], f"{path}.{field_name}")): + keys = {id_name, "evaluation"} + if field_name == "independence_evaluations": + keys.add("dimension") + item = _object(item, f"{path}.{field_name}[{index}]", keys) + _text(item[id_name], f"{path}.{field_name}[{index}].{id_name}") + if "dimension" in item: + _text(item["dimension"], f"{path}.{field_name}[{index}].dimension") + _evaluation_shape(item["evaluation"], f"{path}.{field_name}[{index}].evaluation") + for index, group in enumerate(_array(result["disagreements"], f"{path}.disagreements")): + _strings(group, f"{path}.disagreements[{index}]", 2, unique=True) + error_fields = ( + "binding_errors", "identity_errors", "separation_errors", + "corroboration_errors", "errors", "limitations", + ) + errors = {name: _strings(result[name], f"{path}.{name}") for name in error_fields} + if result["status"] == "CORROBORATED" and ( + result["conformance_status"] != "ESTABLISHED" + or result["computed_independence"] != "INDEPENDENT" + ): + _shape_error(path, "CORROBORATED requires established independent evidence") + if result["computed_independence"] == "INDEPENDENT" and any( + errors[name] for name in error_fields[:3] + ): + _shape_error(path, "INDEPENDENT cannot retain binding, identity, or separation errors") + if result["conformance_status"] == "ESTABLISHED" and ( + result["computed_independence"] != "INDEPENDENT" + or any(errors[name] for name in error_fields[:-1]) + ): + _shape_error(path, "ESTABLISHED cannot retain conformance errors") + + +def _validate_vstd5_receipt_shape(receipt: Mapping[str, Any]) -> None: + """Enforce the published receipt profile without adding a schema dependency.""" + + receipt = _object( + receipt, + "$", + {"schema_version", "receipt_id", "entry_vstd4", "bundle", "evidence_payloads", "result"}, + ) + if receipt["schema_version"] != "VSTD-5": + _shape_error("$.schema_version", "must equal VSTD-5") + if _RECEIPT_ID.fullmatch(_text(receipt["receipt_id"], "$.receipt_id")) is None: + _shape_error("$.receipt_id", "must match the VFY-5 identifier grammar") + entry = _object( + receipt["entry_vstd4"], + "$.entry_vstd4", + {"result_digest", "depth", "conformance_status", "witness_digest"}, + ) + _sha(entry["result_digest"], "$.entry_vstd4.result_digest") + _sha(entry["witness_digest"], "$.entry_vstd4.witness_digest") + if entry["depth"] != 14 or entry["conformance_status"] != "ESTABLISHED": + _shape_error("$.entry_vstd4", "must identify an established depth-14 VSTD-4 result") + bundle = _object( + receipt["bundle"], + "$.bundle", + {"claim_id", "declarant_id", "claim_binding_digest", "witnesses", "independence_assertions", "corroborations"}, + ) + _text(bundle["claim_id"], "$.bundle.claim_id", True) + _text(bundle["declarant_id"], "$.bundle.declarant_id", True) + _sha(bundle["claim_binding_digest"], "$.bundle.claim_binding_digest") + required_payloads: set[str] = set() + for index, item in enumerate(_array(bundle["witnesses"], "$.bundle.witnesses", 1)): + witness = _object(item, f"$.bundle.witnesses[{index}]", {"witness_id", "identity_evidence_ref"}) + _text(witness["witness_id"], f"$.bundle.witnesses[{index}].witness_id", True) + _evidence_refs([witness["identity_evidence_ref"]], f"$.bundle.witnesses[{index}].identity_evidence_ref", required_payloads, 1) + dimensions = {item.value for item in IndependenceDimension} + for index, item in enumerate(_array(bundle["independence_assertions"], "$.bundle.independence_assertions")): + path = f"$.bundle.independence_assertions[{index}]" + assertion = _object(item, path, {"witness_id", "dimensions"}) + _text(assertion["witness_id"], f"{path}.witness_id", True) + records = _object(assertion["dimensions"], f"{path}.dimensions", dimensions) + for dimension, value in records.items(): + coordinate = f"{path}.dimensions.{dimension}" + record = _object(value, coordinate, {"state", "binding"}) + if record["state"] not in {item.value for item in RelationshipState}: + _shape_error(f"{coordinate}.state", "is not a relationship state") + if record["binding"] is not None: + _binding_shape(record["binding"], f"{coordinate}.binding", required_payloads) + for index, item in enumerate(_array(bundle["corroborations"], "$.bundle.corroborations", 1)): + path = f"$.bundle.corroborations[{index}]" + record = _object( + item, + path, + { + "corroboration_id", "witness_id", "claim_binding_digest", + "vstd4_certificate_digest", "checker_descriptor_digest", + "observed_evidence_refs", "result", "observed_at", "verification", + "corroboration_class", + }, + ) + for name in ("corroboration_id", "witness_id", "corroboration_class"): + _text(record[name], f"{path}.{name}", True) + _sha(record["claim_binding_digest"], f"{path}.claim_binding_digest") + _sha(record["vstd4_certificate_digest"], f"{path}.vstd4_certificate_digest", "ref") + _sha(record["checker_descriptor_digest"], f"{path}.checker_descriptor_digest", "ref") + _evidence_refs(record["observed_evidence_refs"], f"{path}.observed_evidence_refs", required_payloads, 1) + if record["result"] not in {item.value for item in CorroborationOutcome}: + _shape_error(f"{path}.result", "is not a corroboration outcome") + observed_at = _text(record["observed_at"], f"{path}.observed_at") + try: + if _DATE_TIME.fullmatch(observed_at) is None: + raise ValueError + datetime.fromisoformat(observed_at.replace("Z", "+00:00")) + except ValueError: + _shape_error(f"{path}.observed_at", "must be an RFC 3339 date-time") + _binding_shape(record["verification"], f"{path}.verification", required_payloads) + payloads = receipt["evidence_payloads"] + if not isinstance(payloads, Mapping): + _shape_error("$.evidence_payloads", "must be an object") + for reference, encoded in payloads.items(): + _sha(reference, "$.evidence_payloads key", "prefixed") + _text(encoded, f"$.evidence_payloads.{reference}") + missing = sorted(required_payloads - set(payloads)) + if missing: + _shape_error("$.evidence_payloads", f"is missing verdict-material bytes for {missing}") + _result_shape(receipt["result"], "$.result") + def assess_witness_corroboration( entry: EvidenceBoundDepthResult, bundle: WitnessBundle, @@ -530,7 +798,7 @@ def build_vstd5_receipt( for record in bundle.corroborations: references.update(record.observed_evidence_refs) references.update(record.verification.evidence_refs) - return { + receipt = { "schema_version": "VSTD-5", "receipt_id": receipt_id, "entry_vstd4": { @@ -543,6 +811,8 @@ def build_vstd5_receipt( "evidence_payloads": session.evidence.export_base64(tuple(sorted(references))), "result": result.to_dict(), } + _validate_vstd5_receipt_shape(receipt) + return receipt def recheck_vstd5_receipt( @@ -553,8 +823,7 @@ def recheck_vstd5_receipt( ) -> WitnessCorroborationResult: """Import exact bytes, rerun all witness mechanisms, and compare the result.""" require_vstd5_entry(entry) - if receipt.get("schema_version") != "VSTD-5": - raise ValueError("not a VSTD-5 receipt") + _validate_vstd5_receipt_shape(receipt) entry_record = receipt.get("entry_vstd4") bundle_data = receipt.get("bundle") payloads = receipt.get("evidence_payloads") diff --git a/src/verifier/data/models.py b/src/verifier/data/models.py index 5bec45c..08bb55f 100644 --- a/src/verifier/data/models.py +++ b/src/verifier/data/models.py @@ -268,9 +268,19 @@ def _add_unique(collection: dict[str, Any], identifier: str, value: Any) -> str: return identifier def add_artifact(self, artifact: ArtifactNode) -> str: + if artifact.artifact_id in self.transformations: + raise ValueError( + "artifact and transformation identifiers must be disjoint: " + f"{artifact.artifact_id}" + ) return self._add_unique(self.artifacts, artifact.artifact_id, artifact) def add_transformation(self, transform: TransformationHyperedge) -> str: + if transform.transformation_id in self.artifacts: + raise ValueError( + "artifact and transformation identifiers must be disjoint: " + f"{transform.transformation_id}" + ) return self._add_unique( self.transformations, transform.transformation_id, transform ) @@ -352,6 +362,11 @@ def validate_structure(self) -> list[str]: captures every real-world input or transformation. """ errors: list[str] = [] + for identifier in sorted(set(self.artifacts) & set(self.transformations)): + errors.append( + "artifact and transformation identifiers must be disjoint: " + f"{identifier}" + ) digest_pattern = re.compile(r"^[0-9a-fA-F]{64}$") for artifact_id, artifact in sorted(self.artifacts.items()): diff --git a/src/verifier/specifications/LADDER.md b/src/verifier/specifications/LADDER.md index 60dbdca..a87175d 100644 --- a/src/verifier/specifications/LADDER.md +++ b/src/verifier/specifications/LADDER.md @@ -176,9 +176,10 @@ and transformation remains admissible and free of an admissibility-blocking conf status-conflict resolution projects its selected state into the current view: `VALID` or `COMPLETED` may restore the affected route, while `REVOKED`, `FAILED`, or another inadmissible state cannot. Resolving an arbitrary predicate selects a retained value but -does not establish its admissibility effect, so the route remains blocked. Alternate or -duplicate paths remain distinct recorded routes; their count supplies no added strength -or witness independence. +does not establish its admissibility effect, so the route remains blocked. The current +reference runtime implements no general non-status admissibility-effect mechanism. +Alternate or duplicate paths remain distinct recorded routes; their count supplies no +added strength or witness independence. **ROT** is typed, time-indexed degradation of the current admissibility of recorded TRUST. It requires exact lifecycle or dependency evidence, such as expiry under a declared @@ -190,10 +191,12 @@ requires its named policy or mechanism. **RUST** is the inverse-TRUST diagnostic mechanic: a typed trace created by an observed descendant deviation from a declared expectation. It moves child-to-parent only through -recorded admissible creation, input, or transformation paths. The inverse is directional -and diagnostic, not arithmetic: TRUST and RUST never cancel. Distinct comparable -backtraces may concentrate on a shared ancestor and prioritize it for falsification or -diagnostic examination. Transferred RUST establishes ancestral reachability, not direct +historically recorded contributing creation, input, or transformation paths. Current +revocation, challenge, staleness, or conflict can remove a route from current TRUST without +erasing it from historical diagnostic ancestry. The inverse is directional and diagnostic, +not arithmetic: TRUST and RUST never cancel. Distinct comparable backtraces may concentrate +on a shared ancestor and prioritize it for falsification or diagnostic examination. +Transferred RUST establishes ancestral reachability, not current admissibility, direct observation, falsehood, or causal responsibility; localization requires additional intervention, ablation, independently bound execution evidence, or an equivalent declared mechanism. diff --git a/src/verifier/specifications/VSTD-5.md b/src/verifier/specifications/VSTD-5.md index acdd75e..718e952 100644 --- a/src/verifier/specifications/VSTD-5.md +++ b/src/verifier/specifications/VSTD-5.md @@ -45,9 +45,15 @@ The reference receipt contains: Schema validity establishes only shape. `recheck_vstd5_receipt` imports and hashes the embedded bytes, checks the admitted VSTD-4 result digest, reruns every registered -mechanism, and compares the complete derived result. Every receipt emitted by -`build_vstd5_receipt`, including `UNKNOWN` / `NOT_ESTABLISHED` error receipts, MUST -preserve the exact error-producing input and recheck identically. +mechanism, and compares the complete derived result. Assessment and receipt construction +are separate boundaries: `assess_witness_corroboration` may diagnose an arbitrary malformed +or incomplete bundle as `UNKNOWN` / `NOT_ESTABLISHED`, but that assessment object is not +thereby a VSTD-5 receipt. `build_vstd5_receipt` MUST fail before returning unless the object +inhabits the strict receipt schema, contains at least one witness and corroboration, and +embeds every verdict-material evidence byte. Every receipt it does emit, including a +representable `UNKNOWN` / `NOT_ESTABLISHED` error receipt, MUST preserve the exact +error-producing input and recheck identically. `recheck_vstd5_receipt` MUST enforce that +strict shape and evidence coverage before mechanism replay. --- @@ -111,10 +117,13 @@ another corroboration identifier is rejected rather than counted twice. 7. bounded result emission with all errors and limitations retained. `build_vstd5_receipt` serializes witness identities and independence assertions as -separate ordered arrays so duplicate and orphan assertions survive round trip. -`recheck_vstd5_receipt` reruns them. Neither function turns an identity coordinate -into trust or establishes a fact outside the propositions checked by its registered -mechanisms. +separate ordered arrays so representable duplicate, orphan, reused-identity, and missing +assertion failures survive round trip. It refuses empty witness/corroboration collections, +empty required identifiers, invalid receipt identifiers, malformed nested records, and +missing verdict-material bytes rather than naming them receipts. `recheck_vstd5_receipt` +applies the same zero-dependency structural gate before replay. Neither function turns an +identity coordinate into trust or establishes a fact outside the propositions checked by +its registered mechanisms. --- diff --git a/src/verifier/specifications/VSTD-Graph-1.md b/src/verifier/specifications/VSTD-Graph-1.md index b2787bb..b5d2801 100644 --- a/src/verifier/specifications/VSTD-Graph-1.md +++ b/src/verifier/specifications/VSTD-Graph-1.md @@ -76,6 +76,9 @@ outputs. The edge records ancestry; it does not by itself establish causal influ bounded CNF with its minimal DPLL implementation; general SMT is not implemented. ### 2.6 Conflict Records ($\mathcal{X}$) +- Artifact and transformation identifiers share one globally disjoint subject namespace. + An identifier collision is structurally invalid because an untyped `subject_id` must name + exactly one kind of Graph object. - `conflict_id`, `subject_id`, and `predicate` identify the disputed coordinate. - `competing_values` retains at least two incompatible values. - `evidence_refs` retains at least two evidence records rather than selecting a winner. @@ -86,6 +89,8 @@ conflict-resolution transition and remains immutable. The separate non-receipt `VSTD-GRAPH-ASSURANCE-1` overlay can record additive, mechanism-checked resolution while retaining the competing evidence. A selected status is projected into that overlay's current view; resolving any other predicate does not by itself establish a clean admissibility effect. +No general non-status admissibility-effect mechanism is implemented in the current reference +runtime, so such a conflict remains blocking. --- diff --git a/standard/LADDER.md b/standard/LADDER.md index 60dbdca..a87175d 100644 --- a/standard/LADDER.md +++ b/standard/LADDER.md @@ -176,9 +176,10 @@ and transformation remains admissible and free of an admissibility-blocking conf status-conflict resolution projects its selected state into the current view: `VALID` or `COMPLETED` may restore the affected route, while `REVOKED`, `FAILED`, or another inadmissible state cannot. Resolving an arbitrary predicate selects a retained value but -does not establish its admissibility effect, so the route remains blocked. Alternate or -duplicate paths remain distinct recorded routes; their count supplies no added strength -or witness independence. +does not establish its admissibility effect, so the route remains blocked. The current +reference runtime implements no general non-status admissibility-effect mechanism. +Alternate or duplicate paths remain distinct recorded routes; their count supplies no +added strength or witness independence. **ROT** is typed, time-indexed degradation of the current admissibility of recorded TRUST. It requires exact lifecycle or dependency evidence, such as expiry under a declared @@ -190,10 +191,12 @@ requires its named policy or mechanism. **RUST** is the inverse-TRUST diagnostic mechanic: a typed trace created by an observed descendant deviation from a declared expectation. It moves child-to-parent only through -recorded admissible creation, input, or transformation paths. The inverse is directional -and diagnostic, not arithmetic: TRUST and RUST never cancel. Distinct comparable -backtraces may concentrate on a shared ancestor and prioritize it for falsification or -diagnostic examination. Transferred RUST establishes ancestral reachability, not direct +historically recorded contributing creation, input, or transformation paths. Current +revocation, challenge, staleness, or conflict can remove a route from current TRUST without +erasing it from historical diagnostic ancestry. The inverse is directional and diagnostic, +not arithmetic: TRUST and RUST never cancel. Distinct comparable backtraces may concentrate +on a shared ancestor and prioritize it for falsification or diagnostic examination. +Transferred RUST establishes ancestral reachability, not current admissibility, direct observation, falsehood, or causal responsibility; localization requires additional intervention, ablation, independently bound execution evidence, or an equivalent declared mechanism. diff --git a/standard/VSTD-5.md b/standard/VSTD-5.md index acdd75e..718e952 100644 --- a/standard/VSTD-5.md +++ b/standard/VSTD-5.md @@ -45,9 +45,15 @@ The reference receipt contains: Schema validity establishes only shape. `recheck_vstd5_receipt` imports and hashes the embedded bytes, checks the admitted VSTD-4 result digest, reruns every registered -mechanism, and compares the complete derived result. Every receipt emitted by -`build_vstd5_receipt`, including `UNKNOWN` / `NOT_ESTABLISHED` error receipts, MUST -preserve the exact error-producing input and recheck identically. +mechanism, and compares the complete derived result. Assessment and receipt construction +are separate boundaries: `assess_witness_corroboration` may diagnose an arbitrary malformed +or incomplete bundle as `UNKNOWN` / `NOT_ESTABLISHED`, but that assessment object is not +thereby a VSTD-5 receipt. `build_vstd5_receipt` MUST fail before returning unless the object +inhabits the strict receipt schema, contains at least one witness and corroboration, and +embeds every verdict-material evidence byte. Every receipt it does emit, including a +representable `UNKNOWN` / `NOT_ESTABLISHED` error receipt, MUST preserve the exact +error-producing input and recheck identically. `recheck_vstd5_receipt` MUST enforce that +strict shape and evidence coverage before mechanism replay. --- @@ -111,10 +117,13 @@ another corroboration identifier is rejected rather than counted twice. 7. bounded result emission with all errors and limitations retained. `build_vstd5_receipt` serializes witness identities and independence assertions as -separate ordered arrays so duplicate and orphan assertions survive round trip. -`recheck_vstd5_receipt` reruns them. Neither function turns an identity coordinate -into trust or establishes a fact outside the propositions checked by its registered -mechanisms. +separate ordered arrays so representable duplicate, orphan, reused-identity, and missing +assertion failures survive round trip. It refuses empty witness/corroboration collections, +empty required identifiers, invalid receipt identifiers, malformed nested records, and +missing verdict-material bytes rather than naming them receipts. `recheck_vstd5_receipt` +applies the same zero-dependency structural gate before replay. Neither function turns an +identity coordinate into trust or establishes a fact outside the propositions checked by +its registered mechanisms. --- diff --git a/standard/VSTD-Graph-1.md b/standard/VSTD-Graph-1.md index b2787bb..b5d2801 100644 --- a/standard/VSTD-Graph-1.md +++ b/standard/VSTD-Graph-1.md @@ -76,6 +76,9 @@ outputs. The edge records ancestry; it does not by itself establish causal influ bounded CNF with its minimal DPLL implementation; general SMT is not implemented. ### 2.6 Conflict Records ($\mathcal{X}$) +- Artifact and transformation identifiers share one globally disjoint subject namespace. + An identifier collision is structurally invalid because an untyped `subject_id` must name + exactly one kind of Graph object. - `conflict_id`, `subject_id`, and `predicate` identify the disputed coordinate. - `competing_values` retains at least two incompatible values. - `evidence_refs` retains at least two evidence records rather than selecting a winner. @@ -86,6 +89,8 @@ conflict-resolution transition and remains immutable. The separate non-receipt `VSTD-GRAPH-ASSURANCE-1` overlay can record additive, mechanism-checked resolution while retaining the competing evidence. A selected status is projected into that overlay's current view; resolving any other predicate does not by itself establish a clean admissibility effect. +No general non-status admissibility-effect mechanism is implemented in the current reference +runtime, so such a conflict remains blocking. --- diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py index 63e4f2d..bc212f7 100644 --- a/tests/test_evidence_bound_assurance.py +++ b/tests/test_evidence_bound_assurance.py @@ -376,6 +376,23 @@ def _witness_components( return witness, assertion, corroboration +def _vstd5_schema_validator() -> Draft202012Validator: + root = Path(__file__).resolve().parents[1] + schema = json.loads((root / "receipts/schema/vstd5_receipt.json").read_text()) + vstd4_schema = json.loads( + (root / "receipts/schema/vstd4_receipt.json").read_text() + ) + assurance_schema = json.loads( + (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() + ) + registry = Registry().with_resource( + vstd4_schema["$id"], Resource.from_contents(vstd4_schema) + ).with_resource( + assurance_schema["$id"], Resource.from_contents(assurance_schema) + ) + return Draft202012Validator(schema, registry=registry) + + def test_serialized_pass_is_not_an_input_to_evidence_evaluation() -> None: assert MechanismOutcome.__doc__ == "Enumeration of the exported result values." store, session = _session() @@ -1627,24 +1644,14 @@ def corroboration(record_id, outcome, observation): receipt_id="VFY-5-EVIDENCE-TEST", session=session, ) - root = Path(__file__).resolve().parents[1] - schema = json.loads((root / "receipts/schema/vstd5_receipt.json").read_text()) - vstd4_schema = json.loads((root / "receipts/schema/vstd4_receipt.json").read_text()) - assurance_schema = json.loads( - (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() - ) - registry = Registry() - registry = registry.with_resource(vstd4_schema["$id"], Resource.from_contents(vstd4_schema)) - registry = registry.with_resource( - assurance_schema["$id"], Resource.from_contents(assurance_schema) - ) - Draft202012Validator(schema, registry=registry).validate(receipt) + validator = _vstd5_schema_validator() + validator.validate(receipt) false_positive = copy.deepcopy(receipt) false_positive["result"]["status"] = "CORROBORATED" false_positive["result"]["conformance_status"] = "NOT_ESTABLISHED" false_positive["result"]["computed_independence"] = "UNKNOWN" assert list( - Draft202012Validator(schema, registry=registry).iter_errors(false_positive) + validator.iter_errors(false_positive) ) false_independence = copy.deepcopy(receipt) false_independence["result"]["conformance_status"] = "NOT_ESTABLISHED" @@ -1655,7 +1662,7 @@ def corroboration(record_id, outcome, observation): false_independence["result"]["identity_errors"] ) assert list( - Draft202012Validator(schema, registry=registry).iter_errors(false_independence) + validator.iter_errors(false_independence) ) rechecked = recheck_vstd5_receipt( entry, receipt, mechanisms=(ExactFactMechanism(),) @@ -1862,22 +1869,7 @@ def test_vstd5_receipts_preserve_noncanonical_replay_inputs() -> None: (first[2], second[2]), ), } - root = Path(__file__).resolve().parents[1] - schema = json.loads((root / "receipts/schema/vstd5_receipt.json").read_text()) - vstd4_schema = json.loads( - (root / "receipts/schema/vstd4_receipt.json").read_text() - ) - assurance_schema = json.loads( - (root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text() - ) - registry = Registry() - registry = registry.with_resource( - vstd4_schema["$id"], Resource.from_contents(vstd4_schema) - ) - registry = registry.with_resource( - assurance_schema["$id"], Resource.from_contents(assurance_schema) - ) - validator = Draft202012Validator(schema, registry=registry) + validator = _vstd5_schema_validator() for name, bundle in bundles.items(): result = assess_witness_corroboration(entry, bundle, session=session) @@ -1897,3 +1889,115 @@ def test_vstd5_receipts_preserve_noncanonical_replay_inputs() -> None: entry, receipt, mechanisms=(ExactFactMechanism(),) ) assert rechecked.to_dict() == result.to_dict(), name + + +def test_vstd5_builder_returns_only_strict_replayable_receipts() -> None: + store, session = _session() + entry = _established_vstd4(store, session) + binding_digest = entry.witness.header.binding # type: ignore[union-attr] + witness, assertion, corroboration = _witness_components( + store, entry, "witness:one" + ) + valid = WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (assertion,), + (corroboration,), + ) + result = assess_witness_corroboration(entry, valid, session=session) + assert result.status is WitnessResultStatus.CORROBORATED + receipt = build_vstd5_receipt( + entry, valid, result, receipt_id="VFY-5-STRICT", session=session + ) + _vstd5_schema_validator().validate(receipt) + assert recheck_vstd5_receipt( + entry, receipt, mechanisms=(ExactFactMechanism(),) + ).to_dict() == result.to_dict() + + invalid_bundles = { + "no-witnesses": WitnessBundle( + "claim:fixture", "declarant:one", binding_digest, (), (), () + ), + "no-corroborations": replace(valid, corroborations=()), + "empty-claim": replace(valid, claim_id=""), + "empty-declarant": replace(valid, declarant_id=""), + "empty-witness": replace( + valid, witnesses=(replace(witness, witness_id=""),) + ), + } + for name, bundle in invalid_bundles.items(): + result = assess_witness_corroboration(entry, bundle, session=session) + with pytest.raises(ValueError, match="invalid VSTD-5 receipt shape"): + build_vstd5_receipt( + entry, + bundle, + result, + receipt_id=f"VFY-5-{name.upper()}", + session=session, + ) + + for receipt_id in ("", "invalid id"): + with pytest.raises(ValueError, match="receipt_id"): + build_vstd5_receipt( + entry, + valid, + assess_witness_corroboration(entry, valid, session=session), + receipt_id=receipt_id, + session=session, + ) + + +def test_vstd5_rechecker_refuses_external_shape_and_payload_defects() -> None: + store, session = _session() + entry = _established_vstd4(store, session) + binding_digest = entry.witness.header.binding # type: ignore[union-attr] + witness, assertion, corroboration = _witness_components( + store, entry, "witness:one" + ) + bundle = WitnessBundle( + "claim:fixture", + "declarant:one", + binding_digest, + (witness,), + (assertion,), + (corroboration,), + ) + receipt = build_vstd5_receipt( + entry, + bundle, + assess_witness_corroboration(entry, bundle, session=session), + receipt_id="VFY-5-EXTERNAL", + session=session, + ) + malformed = [] + for coordinate, value in ( + (("receipt_id",), "invalid id"), + (("bundle", "claim_id"), ""), + (("bundle", "witnesses"), []), + (("bundle", "corroborations"), []), + (("bundle", "witnesses", 0, "witness_id"), ""), + ): + candidate = copy.deepcopy(receipt) + target = candidate + for part in coordinate[:-1]: + target = target[part] + target[coordinate[-1]] = value + malformed.append(candidate) + extra = copy.deepcopy(receipt) + extra["result"]["unexpected"] = True + malformed.append(extra) + + validator = _vstd5_schema_validator() + for candidate in malformed: + assert list(validator.iter_errors(candidate)) + with pytest.raises(ValueError, match="invalid VSTD-5 receipt shape"): + recheck_vstd5_receipt(entry, candidate, mechanisms=()) + + missing_payload = copy.deepcopy(receipt) + missing_payload["evidence_payloads"].pop( + next(iter(missing_payload["evidence_payloads"])) + ) + with pytest.raises(ValueError, match="missing verdict-material bytes"): + recheck_vstd5_receipt(entry, missing_payload, mechanisms=()) diff --git a/tests/test_public_data.py b/tests/test_public_data.py index 15e918a..d161dbd 100644 --- a/tests/test_public_data.py +++ b/tests/test_public_data.py @@ -242,6 +242,40 @@ def test_duplicate_graph_identifier_cannot_replace_recorded_evidence() -> None: assert graph.artifacts["artifact:duplicate"] is original +def test_artifact_and_transformation_identifiers_are_globally_disjoint() -> None: + graph = ProvenanceHypergraph() + graph.add_artifact( + ArtifactNode( + artifact_id="shared:id", + label="artifact", + artifact_type=ArtifactType.RAW_SOURCE_FILE, + content_digest="a" * 64, + ) + ) + collision = TransformationHyperedge( + transformation_id="shared:id", + label="transformation", + transformation_type=TransformationType.EVALUATION, + inputs=(HyperedgePort("shared:id", "INPUT"),), + outputs=(HyperedgePort("shared:id", "OUTPUT"),), + software_provenance={}, + parameters={}, + execution_environment={}, + ) + with pytest.raises(ValueError, match="identifiers must be disjoint"): + graph.add_transformation(collision) + + reverse = ProvenanceHypergraph() + reverse.add_transformation(collision) + with pytest.raises(ValueError, match="identifiers must be disjoint"): + reverse.add_artifact(graph.artifacts["shared:id"]) + + graph.transformations[collision.transformation_id] = collision + assert graph.validate_structure()[0] == ( + "artifact and transformation identifiers must be disjoint: shared:id" + ) + + def test_completeness_rejects_non_hex_digest() -> None: graph = ProvenanceHypergraph() graph.add_artifact( From c7a02814df63c8505958c0703e292f1d696a468f Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 11:14:55 -0700 Subject: [PATCH 20/34] Bind portable semantics and preserve frozen readers Reason: Close two semantic-review blockers without weakening the strict VSTD-5 shape gate or retroactively narrowing the frozen VSTD-DATA-0.1 reader. Evidence: At parent 31810f66, a schema-valid VSTD-5 receipt could relabel entry_vstd4.witness_digest or corroboration_class and still replay as CORROBORATED / ESTABLISHED. The same parent also rejected a historical VSTD-DATA-0.1 payload whose artifact and transformation collections each used the same identifier even though the frozen schema and v1.1.3 reader admitted separate namespaces. Mechanism: Compare every carried VSTD-4 entry coordinate with the admitted entry; make corroboration_class part of the exact mechanism-checked proposition; retain separate frozen artifact/transformation namespaces during decoding and receipt replay; keep direct new construction strict; and reject cross-kind overlap at evidence-bound Graph establishment and AssuranceLedger boundaries. Falsification: Reject this change if either schema-valid cross-field relabeling retains an established replay; if within-kind duplicates can replace recorded evidence; if a frozen two-namespace receipt cannot validate and reproduce; or if an overlapping historical graph can enter evidence-bound establishment or assurance propagation. Compatibility and wire impact: No schema or serialized identifier changes. VSTD-5 is unreleased and its existing fields now receive the semantics their names claim. VSTD-DATA-0.1 historical bytes regain the v1.1.3 two-namespace reader behavior, while stricter additive mechanisms remain fail-closed. Compatibility graph_level remains caller-supplied and NOT_ESTABLISHED. --- CHANGELOG.md | 9 +- README.md | 6 +- docs/API_STABILITY.md | 11 +- docs/ARCHITECTURE.md | 14 ++- docs/profiles/vstd-3/compatibility.md | 5 +- docs/reference.html | 2 +- src/verifier/core/witness.py | 23 ++-- src/verifier/data/graph_level.py | 9 ++ src/verifier/data/models.py | 109 +++++++++++------- src/verifier/data/receipt.py | 4 +- src/verifier/specifications/VSTD-5.md | 15 ++- src/verifier/specifications/VSTD-Graph-1.md | 12 +- .../specifications/WIRE_IDENTIFIERS.md | 6 + standard/VSTD-5.md | 15 ++- standard/VSTD-Graph-1.md | 12 +- standard/WIRE_IDENTIFIERS.md | 6 + tests/test_evidence_bound_assurance.py | 74 ++++++++++++ tests/test_public_data.py | 81 +++++++++++++ 18 files changed, 336 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af2b1fc..9df3416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,7 +110,8 @@ during receipt construction. Keep permissive malformed-input assessment distinct from portable receipt admission: the builder now raises unless the strict schema and complete verdict-material evidence coverage hold, and the rechecker applies the same zero-dependency - gate before replay. This does not + gate before replay. The rechecker also compares the complete carried VSTD-4 entry, and + `corroboration_class` is mechanism-bound rather than relabelable metadata. This does not claim a real external witness or independent implementation; a positive observation with unresolved independence is overall `UNKNOWN`. - Add evidence-bound Graph profile computation and replay. The compatibility `graph_level` @@ -130,8 +131,10 @@ selected state into current admissibility; arbitrary resolved predicates remain blocked. The current runtime has no general non-status admissibility-effect mechanism. RUST follows historically recorded contributing ancestry even when current lifecycle state excludes a - route from TRUST. Artifact and transformation identifiers are globally disjoint so an - assurance `subject_id` cannot ambiguously name both. + route from TRUST. New construction, evidence-bound Graph establishment, and assurance + propagation require globally disjoint artifact/transformation identifiers so an untyped + `subject_id` cannot ambiguously name both; the frozen `VSTD-DATA-0.1` reader retains its + original two namespaces. Add complete offline event replay, current TRUST filtering, and deduplicated descendant reassessment discovery. diff --git a/README.md b/README.md index 28d987c..f98f9a2 100644 --- a/README.md +++ b/README.md @@ -302,8 +302,10 @@ reference](https://timelordraps.github.io/verifier/reference.html). - [VSTD-1 receipts](standard/VSTD-1.md) carry claim coordinates, evidence, checker results, trust boundaries, and reproducibility information. - [VSTD-Graph-1](standard/VSTD-Graph-1.md) records content-addressed artifacts, - many-to-many transformations, globally disjoint artifact/transformation identifiers, - conflicts, and bounded downstream reachability. + many-to-many transformations, conflicts, and bounded downstream reachability. Its frozen + reader preserves separate historical artifact/transformation namespaces; new construction + plus evidence-bound establishment and assurance propagation require global cross-kind + disjointness. - [`VSTD4-GDC-1`](standard/VSTD-4.md) binds a decision certificate to a formula, grounding, claim coordinate, verifier descriptor, roots, and resource bounds. diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md index d8d984f..e7d299d 100644 --- a/docs/API_STABILITY.md +++ b/docs/API_STABILITY.md @@ -39,7 +39,16 @@ conformance results merely because the evidence-bound APIs also exist. 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. `recheck_vstd5_receipt` applies the same zero-dependency structural gate before replay and -does not accept a schema-invalid assessment object as a portable receipt. +does not accept a schema-invalid assessment object as a portable receipt. It also compares +the complete carried VSTD-4 entry and mechanism-checks `corroboration_class`; schema-valid +field relabeling cannot retain an established replay result. + +`ProvenanceHypergraph.from_dict` retains the frozen `VSTD-DATA-0.1` two-namespace reader: +one identifier may occur once as an artifact and once as a transformation. Direct `add_*` +construction and default structural validation are stricter and globally disjoint. Such a +historical overlap remains readable but cannot enter evidence-bound Graph establishment or +assurance mechanisms. The compatibility candidate computation retains its historical scope +and remains `NOT_ESTABLISHED`. Direct imports from `verifier.core`, `verifier.data`, `verifier.hardware`, other subpackages, or underscore-prefixed names are internal unless another published policy diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9893e9f..608a01e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -308,14 +308,18 @@ ordered arrays, so duplicates, orphan assertions, and missing cardinality surviv build and replay rather than disappearing inside a keyed map. Permissive assessment remains separate from portable-record admission: the builder raises unless the result inhabits the strict VSTD-5 schema and embeds every verdict-material evidence byte, and the rechecker -enforces the same zero-dependency shape gate before replay. This path does not upgrade the -legacy generic-run fields. +enforces the same zero-dependency shape gate before replay. It also compares every redundant +VSTD-4 entry coordinate and treats `corroboration_class` as part of the mechanism-checked +expected proposition, so relabeling either cannot retain an established result. This path +does not upgrade the legacy generic-run fields. Graph conflict records retain incompatible values and their evidence references without adding a scalar score or changing the frozen artifact-status vocabulary. A conflict makes -the subject inadmissible to a clean candidate Graph profile. Artifact and transformation -identifiers are globally disjoint, so the conflict overlay's `subject_id` cannot ambiguously -name both kinds of Graph object. +the subject inadmissible to a clean candidate Graph profile. The frozen `VSTD-DATA-0.1` +reader retains its historical separate artifact/transformation namespaces. Direct new +construction, evidence-bound Graph establishment, and the assurance overlay require global +cross-kind disjointness, so an untyped `subject_id` cannot ambiguously name both kinds of +Graph object. ### Recursive current-state audit diff --git a/docs/profiles/vstd-3/compatibility.md b/docs/profiles/vstd-3/compatibility.md index ad6a2b4..caf904f 100644 --- a/docs/profiles/vstd-3/compatibility.md +++ b/docs/profiles/vstd-3/compatibility.md @@ -15,7 +15,10 @@ only its emulator-scoped claims. - `VSTD-1` claim-mechanics and generic-run receipts retain their separate required `receipt_kind` values. -- `VSTD-DATA-0.1` hypergraphs remain readable as historical VSTD-Graph-1 receipts. +- `VSTD-DATA-0.1` hypergraphs remain readable as historical VSTD-Graph-1 receipts, including + their original separate artifact and transformation identifier namespaces. Direct new + construction, evidence-bound Graph establishment, and current Graph assurance require + global cross-kind disjointness. - `VSTD-2` geometry remains a separate verification-surface receipt. - The public `validate`, `inspect`, `reproduce`, `data`, and `impact` commands retain their earlier behavior. diff --git a/docs/reference.html b/docs/reference.html index f143ac3..146c637 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -537,7 +537,7 @@

    ProvenanceHypergraph class

    - +
    MethodSummary
    to_dict(self) -> 'dict[str, Any]'
    outgoing_hyperedges(self, artifact_id: 'str') -> 'list[TransformationHyperedge]'Hyperedges that consume artifact_id as an input.
    root_sources(self) -> 'set[str]'Artifacts with zero incoming hyperedges (genesis roots).
    to_dict(self) -> 'dict[str, Any]'
    validate_structure(self) -> 'list[str]'Return deterministic errors for the implemented graph surface.
    validate_structure(self, *, allow_legacy_identifier_overlap: 'bool' = False) -> 'list[str]'Return deterministic errors for the implemented graph surface.
    verify_acyclicity(self, artifact_ids: 'Optional[Iterable[str]]' = None) -> 'bool'Check whether all or a selected artifact-induced subgraph contains cycles.
    diff --git a/src/verifier/core/witness.py b/src/verifier/core/witness.py index 82cd124..7f94879 100644 --- a/src/verifier/core/witness.py +++ b/src/verifier/core/witness.py @@ -682,6 +682,7 @@ def assess_witness_corroboration( "claim_binding_digest": bundle.claim_binding_digest, "vstd4_certificate_digest": record.vstd4_certificate_digest, "checker_descriptor_digest": record.checker_descriptor_digest, + "corroboration_class": record.corroboration_class, "result": record.result.value, } proposition = record.verification @@ -776,6 +777,17 @@ def assess_witness_corroboration( ) +def _vstd5_entry_record(entry: EvidenceBoundDepthResult) -> dict[str, Any]: + """Return every redundant VSTD-4 coordinate carried by a VSTD-5 receipt.""" + + return { + "result_digest": canonical_digest(entry.to_dict()), + "depth": entry.depth, + "conformance_status": entry.conformance_status, + "witness_digest": entry.witness.digest(), # type: ignore[union-attr] + } + + def build_vstd5_receipt( entry: EvidenceBoundDepthResult, bundle: WitnessBundle, @@ -801,12 +813,7 @@ def build_vstd5_receipt( receipt = { "schema_version": "VSTD-5", "receipt_id": receipt_id, - "entry_vstd4": { - "result_digest": canonical_digest(entry.to_dict()), - "depth": entry.depth, - "conformance_status": entry.conformance_status, - "witness_digest": entry.witness.digest(), # type: ignore[union-attr] - }, + "entry_vstd4": _vstd5_entry_record(entry), "bundle": bundle.to_dict(), "evidence_payloads": session.evidence.export_base64(tuple(sorted(references))), "result": result.to_dict(), @@ -829,8 +836,8 @@ def recheck_vstd5_receipt( payloads = receipt.get("evidence_payloads") if not isinstance(entry_record, Mapping) or not isinstance(bundle_data, Mapping) or not isinstance(payloads, Mapping): raise ValueError("VSTD-5 receipt is missing replay inputs") - if entry_record.get("result_digest") != canonical_digest(entry.to_dict()): - raise ValueError("VSTD-5 receipt references a different VSTD-4 result") + if dict(entry_record) != _vstd5_entry_record(entry): + raise ValueError("VSTD-5 receipt references an inconsistent VSTD-4 entry") store = EvidenceStore() store.import_base64({str(key): str(value) for key, value in payloads.items()}) session = VerificationSession(store) diff --git a/src/verifier/data/graph_level.py b/src/verifier/data/graph_level.py index 64bb59d..dd9e2e1 100644 --- a/src/verifier/data/graph_level.py +++ b/src/verifier/data/graph_level.py @@ -604,6 +604,15 @@ def establish_graph_level( if not members: raise GraphEncodingError("an evidence-bound Graph collection must have members") + identifier_overlap = sorted( + set(graph.artifacts) & set(graph.transformations) + ) + if identifier_overlap: + raise GraphEncodingError( + "evidence-bound Graph establishment requires globally disjoint " + "artifact and transformation identifiers: " + + ", ".join(identifier_overlap) + ) normalized_members = tuple(sorted(set(members))) closure = graph.ancestors(normalized_members) edges = { diff --git a/src/verifier/data/models.py b/src/verifier/data/models.py index 08bb55f..5213306 100644 --- a/src/verifier/data/models.py +++ b/src/verifier/data/models.py @@ -355,18 +355,23 @@ def root_sources(self) -> set[str]: roots.add(art_id) return roots - def validate_structure(self) -> list[str]: + def validate_structure( + self, *, allow_legacy_identifier_overlap: bool = False + ) -> list[str]: """Return deterministic errors for the implemented graph surface. This validates the stored representation. It does not prove that the graph - captures every real-world input or transformation. + captures every real-world input or transformation. The compatibility flag + preserves the two identifier namespaces of frozen ``VSTD-DATA-0.1`` bytes; + new construction and assurance mechanisms keep the stricter default. """ errors: list[str] = [] - for identifier in sorted(set(self.artifacts) & set(self.transformations)): - errors.append( - "artifact and transformation identifiers must be disjoint: " - f"{identifier}" - ) + if not allow_legacy_identifier_overlap: + for identifier in sorted(set(self.artifacts) & set(self.transformations)): + errors.append( + "artifact and transformation identifiers must be disjoint: " + f"{identifier}" + ) digest_pattern = re.compile(r"^[0-9a-fA-F]{64}$") for artifact_id, artifact in sorted(self.artifacts.items()): @@ -545,7 +550,19 @@ def to_dict(self) -> dict[str, Any]: } @classmethod - def from_dict(cls, data: Mapping[str, Any]) -> "ProvenanceHypergraph": + def from_dict( + cls, + data: Mapping[str, Any], + *, + allow_legacy_identifier_overlap: bool = True, + ) -> "ProvenanceHypergraph": + """Decode stored Graph bytes without rewriting their identifier semantics. + + Frozen ``VSTD-DATA-0.1`` used separate artifact and transformation + namespaces. The default therefore retains cross-kind overlap while still + refusing duplicates inside either collection. Pass ``False`` for a strict + new-mechanism decoder; direct ``add_*`` construction is always strict. + """ g = cls() for c_data in data.get("contributors", []): g.add_contributor(ContributorSpec(**c_data)) @@ -572,25 +589,29 @@ def from_dict(cls, data: Mapping[str, Any]) -> "ProvenanceHypergraph": ) ) for a_data in data.get("artifacts", []): - g.add_artifact( - ArtifactNode( - artifact_id=a_data["artifact_id"], - label=a_data["label"], - artifact_type=ArtifactType(a_data["artifact_type"]), - content_digest=a_data["content_digest"], - byte_size=a_data.get("byte_size", 0), - record_count=a_data.get("record_count"), - mime_type=a_data.get("mime_type", "application/octet-stream"), - metadata_digest=a_data.get("metadata_digest", ""), - provenance_digest=a_data.get("provenance_digest", ""), - status=ArtifactStatus(a_data.get("status", "UNKNOWN")), - evidence_class=EvidenceClassification(a_data.get("evidence_class", "DECLARED")), - rights_id=a_data.get("rights_id"), - contributor_id=a_data.get("contributor_id"), - storage_uris=tuple(a_data.get("storage_uris", ())), - attributes=a_data.get("attributes", {}), - ) + artifact = ArtifactNode( + artifact_id=a_data["artifact_id"], + label=a_data["label"], + artifact_type=ArtifactType(a_data["artifact_type"]), + content_digest=a_data["content_digest"], + byte_size=a_data.get("byte_size", 0), + record_count=a_data.get("record_count"), + mime_type=a_data.get("mime_type", "application/octet-stream"), + metadata_digest=a_data.get("metadata_digest", ""), + provenance_digest=a_data.get("provenance_digest", ""), + status=ArtifactStatus(a_data.get("status", "UNKNOWN")), + evidence_class=EvidenceClassification( + a_data.get("evidence_class", "DECLARED") + ), + rights_id=a_data.get("rights_id"), + contributor_id=a_data.get("contributor_id"), + storage_uris=tuple(a_data.get("storage_uris", ())), + attributes=a_data.get("attributes", {}), ) + if allow_legacy_identifier_overlap: + g._add_unique(g.artifacts, artifact.artifact_id, artifact) + else: + g.add_artifact(artifact) for t_data in data.get("transformations", []): inputs = tuple( HyperedgePort(artifact_id=p["artifact_id"], role=p["role"]) @@ -600,18 +621,28 @@ def from_dict(cls, data: Mapping[str, Any]) -> "ProvenanceHypergraph": HyperedgePort(artifact_id=p["artifact_id"], role=p["role"]) for p in t_data.get("outputs", []) ) - g.add_transformation( - TransformationHyperedge( - transformation_id=t_data["transformation_id"], - label=t_data["label"], - transformation_type=TransformationType(t_data["transformation_type"]), - inputs=inputs, - outputs=outputs, - software_provenance=t_data.get("software_provenance", {}), - parameters=t_data.get("parameters", {}), - execution_environment=t_data.get("execution_environment", {}), - evidence_class=EvidenceClassification(t_data.get("evidence_class", "DECLARED")), - status=t_data.get("status", "COMPLETED"), - ) + transformation = TransformationHyperedge( + transformation_id=t_data["transformation_id"], + label=t_data["label"], + transformation_type=TransformationType( + t_data["transformation_type"] + ), + inputs=inputs, + outputs=outputs, + software_provenance=t_data.get("software_provenance", {}), + parameters=t_data.get("parameters", {}), + execution_environment=t_data.get("execution_environment", {}), + evidence_class=EvidenceClassification( + t_data.get("evidence_class", "DECLARED") + ), + status=t_data.get("status", "COMPLETED"), ) + if allow_legacy_identifier_overlap: + g._add_unique( + g.transformations, + transformation.transformation_id, + transformation, + ) + else: + g.add_transformation(transformation) return g diff --git a/src/verifier/data/receipt.py b/src/verifier/data/receipt.py index 1c460bd..429caf8 100644 --- a/src/verifier/data/receipt.py +++ b/src/verifier/data/receipt.py @@ -278,7 +278,9 @@ def validate_data_receipt(receipt_path_or_dir: Path) -> int: except (KeyError, TypeError, ValueError) as exc: print(f"[FAIL] Cannot parse hypergraph: {exc}", file=sys.stderr) return 1 - graph_errors.extend(hypergraph.validate_structure()) + graph_errors.extend( + hypergraph.validate_structure(allow_legacy_identifier_overlap=True) + ) target_artifact_id = data.get("dataset_spec", {}).get("target_artifact_id") if target_artifact_id not in hypergraph.artifacts: diff --git a/src/verifier/specifications/VSTD-5.md b/src/verifier/specifications/VSTD-5.md index 718e952..5f00ba4 100644 --- a/src/verifier/specifications/VSTD-5.md +++ b/src/verifier/specifications/VSTD-5.md @@ -44,8 +44,9 @@ The reference receipt contains: offline. Schema validity establishes only shape. `recheck_vstd5_receipt` imports and hashes -the embedded bytes, checks the admitted VSTD-4 result digest, reruns every registered -mechanism, and compares the complete derived result. Assessment and receipt construction +the embedded bytes, compares every carried VSTD-4 entry coordinate—including the result +digest and witness digest—with the admitted entry, reruns every registered mechanism, and +compares the complete derived result. Assessment and receipt construction are separate boundaries: `assess_witness_corroboration` may diagnose an arbitrary malformed or incomplete bundle as `UNKNOWN` / `NOT_ESTABLISHED`, but that assessment object is not thereby a VSTD-5 receipt. `build_vstd5_receipt` MUST fail before returning unless the object @@ -87,6 +88,7 @@ A corroboration mechanism MUST bind and check the exact: * claim commitment; * VSTD-4 certificate digest; * checker descriptor digest; +* corroboration class; * witness coordinate; * observation time; * observation evidence bytes; and @@ -95,6 +97,8 @@ A corroboration mechanism MUST bind and check the exact: A record's certificate digest MUST equal the admitted evidence-bound VSTD-4 witness, not merely a caller-selected digest repeated in both fields. Every identified witness MUST contribute a corroboration record; dangling identities do not create plurality. +`corroboration_class` is part of the mechanism-checked expected proposition, not declared +metadata and not an assurance-bearing label by itself. A mechanism-earned negative result remains negative. Conflicting checked records produce `CONFLICTED`; witnesses are not votes, and majority count never cleans the @@ -121,9 +125,10 @@ separate ordered arrays so representable duplicate, orphan, reused-identity, and assertion failures survive round trip. It refuses empty witness/corroboration collections, empty required identifiers, invalid receipt identifiers, malformed nested records, and missing verdict-material bytes rather than naming them receipts. `recheck_vstd5_receipt` -applies the same zero-dependency structural gate before replay. Neither function turns an -identity coordinate into trust or establishes a fact outside the propositions checked by -its registered mechanisms. +applies the same zero-dependency structural gate, rejects any inconsistent redundant VSTD-4 +entry coordinate, and mechanism-checks the corroboration class before accepting exact replay. +Neither function turns an identity coordinate into trust or establishes a fact outside the +propositions checked by its registered mechanisms. --- diff --git a/src/verifier/specifications/VSTD-Graph-1.md b/src/verifier/specifications/VSTD-Graph-1.md index b5d2801..f7ac14a 100644 --- a/src/verifier/specifications/VSTD-Graph-1.md +++ b/src/verifier/specifications/VSTD-Graph-1.md @@ -65,6 +65,15 @@ outputs. The edge records ancestry; it does not by itself establish causal influ - `parameters`: Exact hyperparameter dictionary, filter criteria, or random seeds. - `execution_environment`: Python runtime, host OS, hardware acceleration class, timestamp. +The frozen `VSTD-DATA-0.1` serialization defines artifact and transformation identifiers +inside separate collections; historical readers therefore retain a payload in which one +string occurs once in each collection. Direct new construction, evidence-bound Graph +establishment, and `VSTD-GRAPH-ASSURANCE-1` require the two sets to be globally +disjoint because current evidence maps and the assurance overlay's `subject_id` do not carry +an artifact/transformation kind. A historical overlap is readable and reproducible as +recorded lineage but is inadmissible to those stricter current mechanisms. This compatibility +rule does not let duplicates within either collection replace recorded evidence. + ### 2.3 Contributor Nodes ($\mathcal{C}$) - `contributor_id`, `name`, `contributor_type` (`INDIVIDUAL`, `ORGANIZATION`, `MODEL_GENERATOR`, `AUTOMATED_SYSTEM`), `uri`. @@ -76,9 +85,6 @@ outputs. The edge records ancestry; it does not by itself establish causal influ bounded CNF with its minimal DPLL implementation; general SMT is not implemented. ### 2.6 Conflict Records ($\mathcal{X}$) -- Artifact and transformation identifiers share one globally disjoint subject namespace. - An identifier collision is structurally invalid because an untyped `subject_id` must name - exactly one kind of Graph object. - `conflict_id`, `subject_id`, and `predicate` identify the disputed coordinate. - `competing_values` retains at least two incompatible values. - `evidence_refs` retains at least two evidence records rather than selecting a winner. diff --git a/src/verifier/specifications/WIRE_IDENTIFIERS.md b/src/verifier/specifications/WIRE_IDENTIFIERS.md index 674c0aa..b3d7768 100644 --- a/src/verifier/specifications/WIRE_IDENTIFIERS.md +++ b/src/verifier/specifications/WIRE_IDENTIFIERS.md @@ -29,6 +29,12 @@ closed: | `VSTD-5.md` | `schema_version = "VSTD-5"` | | `VSTD-Graph-1.md` | `schema_version = "VSTD-DATA-0.1"` | +The frozen `VSTD-DATA-0.1` reader preserves its original separate artifact and +transformation identifier namespaces. New Graph construction, evidence-bound Graph +establishment, and the separate `VSTD-GRAPH-ASSURANCE-1` mechanism require global +cross-kind disjointness; that stricter admission rule does not retroactively narrow which +historical `VSTD-DATA-0.1` bytes can be decoded and replayed. + VSTD-1 has two current receipt profiles: | `receipt_kind` | Schema | Meaning | diff --git a/standard/VSTD-5.md b/standard/VSTD-5.md index 718e952..5f00ba4 100644 --- a/standard/VSTD-5.md +++ b/standard/VSTD-5.md @@ -44,8 +44,9 @@ The reference receipt contains: offline. Schema validity establishes only shape. `recheck_vstd5_receipt` imports and hashes -the embedded bytes, checks the admitted VSTD-4 result digest, reruns every registered -mechanism, and compares the complete derived result. Assessment and receipt construction +the embedded bytes, compares every carried VSTD-4 entry coordinate—including the result +digest and witness digest—with the admitted entry, reruns every registered mechanism, and +compares the complete derived result. Assessment and receipt construction are separate boundaries: `assess_witness_corroboration` may diagnose an arbitrary malformed or incomplete bundle as `UNKNOWN` / `NOT_ESTABLISHED`, but that assessment object is not thereby a VSTD-5 receipt. `build_vstd5_receipt` MUST fail before returning unless the object @@ -87,6 +88,7 @@ A corroboration mechanism MUST bind and check the exact: * claim commitment; * VSTD-4 certificate digest; * checker descriptor digest; +* corroboration class; * witness coordinate; * observation time; * observation evidence bytes; and @@ -95,6 +97,8 @@ A corroboration mechanism MUST bind and check the exact: A record's certificate digest MUST equal the admitted evidence-bound VSTD-4 witness, not merely a caller-selected digest repeated in both fields. Every identified witness MUST contribute a corroboration record; dangling identities do not create plurality. +`corroboration_class` is part of the mechanism-checked expected proposition, not declared +metadata and not an assurance-bearing label by itself. A mechanism-earned negative result remains negative. Conflicting checked records produce `CONFLICTED`; witnesses are not votes, and majority count never cleans the @@ -121,9 +125,10 @@ separate ordered arrays so representable duplicate, orphan, reused-identity, and assertion failures survive round trip. It refuses empty witness/corroboration collections, empty required identifiers, invalid receipt identifiers, malformed nested records, and missing verdict-material bytes rather than naming them receipts. `recheck_vstd5_receipt` -applies the same zero-dependency structural gate before replay. Neither function turns an -identity coordinate into trust or establishes a fact outside the propositions checked by -its registered mechanisms. +applies the same zero-dependency structural gate, rejects any inconsistent redundant VSTD-4 +entry coordinate, and mechanism-checks the corroboration class before accepting exact replay. +Neither function turns an identity coordinate into trust or establishes a fact outside the +propositions checked by its registered mechanisms. --- diff --git a/standard/VSTD-Graph-1.md b/standard/VSTD-Graph-1.md index b5d2801..f7ac14a 100644 --- a/standard/VSTD-Graph-1.md +++ b/standard/VSTD-Graph-1.md @@ -65,6 +65,15 @@ outputs. The edge records ancestry; it does not by itself establish causal influ - `parameters`: Exact hyperparameter dictionary, filter criteria, or random seeds. - `execution_environment`: Python runtime, host OS, hardware acceleration class, timestamp. +The frozen `VSTD-DATA-0.1` serialization defines artifact and transformation identifiers +inside separate collections; historical readers therefore retain a payload in which one +string occurs once in each collection. Direct new construction, evidence-bound Graph +establishment, and `VSTD-GRAPH-ASSURANCE-1` require the two sets to be globally +disjoint because current evidence maps and the assurance overlay's `subject_id` do not carry +an artifact/transformation kind. A historical overlap is readable and reproducible as +recorded lineage but is inadmissible to those stricter current mechanisms. This compatibility +rule does not let duplicates within either collection replace recorded evidence. + ### 2.3 Contributor Nodes ($\mathcal{C}$) - `contributor_id`, `name`, `contributor_type` (`INDIVIDUAL`, `ORGANIZATION`, `MODEL_GENERATOR`, `AUTOMATED_SYSTEM`), `uri`. @@ -76,9 +85,6 @@ outputs. The edge records ancestry; it does not by itself establish causal influ bounded CNF with its minimal DPLL implementation; general SMT is not implemented. ### 2.6 Conflict Records ($\mathcal{X}$) -- Artifact and transformation identifiers share one globally disjoint subject namespace. - An identifier collision is structurally invalid because an untyped `subject_id` must name - exactly one kind of Graph object. - `conflict_id`, `subject_id`, and `predicate` identify the disputed coordinate. - `competing_values` retains at least two incompatible values. - `evidence_refs` retains at least two evidence records rather than selecting a winner. diff --git a/standard/WIRE_IDENTIFIERS.md b/standard/WIRE_IDENTIFIERS.md index 674c0aa..b3d7768 100644 --- a/standard/WIRE_IDENTIFIERS.md +++ b/standard/WIRE_IDENTIFIERS.md @@ -29,6 +29,12 @@ closed: | `VSTD-5.md` | `schema_version = "VSTD-5"` | | `VSTD-Graph-1.md` | `schema_version = "VSTD-DATA-0.1"` | +The frozen `VSTD-DATA-0.1` reader preserves its original separate artifact and +transformation identifier namespaces. New Graph construction, evidence-bound Graph +establishment, and the separate `VSTD-GRAPH-ASSURANCE-1` mechanism require global +cross-kind disjointness; that stricter admission rule does not retroactively narrow which +historical `VSTD-DATA-0.1` bytes can be decoded and replayed. + VSTD-1 has two current receipt profiles: | `receipt_kind` | Schema | Meaning | diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py index bc212f7..0a268dc 100644 --- a/tests/test_evidence_bound_assurance.py +++ b/tests/test_evidence_bound_assurance.py @@ -59,6 +59,7 @@ recheck_assurance_log, ) from verifier.data.graph_level import ( + GraphEncodingError, GraphCollection, build_evidence_bound_graph_level_record, establish_graph_level, @@ -349,6 +350,7 @@ def _witness_components( "claim_binding_digest": binding_digest, "vstd4_certificate_digest": certificate_digest, "checker_descriptor_digest": checker_digest, + "corroboration_class": "TEST", "result": CorroborationOutcome.CORROBORATED.value, } verification = _proposition( @@ -633,6 +635,23 @@ def test_graph_profile_can_be_established_from_mechanism_evaluated_ratings() -> assert rechecked.conformance_status == "ESTABLISHED" +def test_evidence_bound_graph_refuses_frozen_cross_kind_identifier_overlap() -> None: + graph = _graph() + graph.transformations["source"] = replace( + graph.transformations["first"], transformation_id="source" + ) + with pytest.raises(GraphEncodingError, match="globally disjoint"): + establish_graph_level( + graph, + collection_id="collection:fixture", + members=("result",), + object_evidence={}, + edge_evidence={}, + session=_session()[1], + binding=_binding(), + ) + + def test_graph_ratings_are_bound_to_exact_collection_and_integer_type() -> None: graph = _graph() store, session = _session() @@ -1595,6 +1614,7 @@ def corroboration(record_id, outcome, observation): "claim_binding_digest": binding_digest, "vstd4_certificate_digest": certificate_digest, "checker_descriptor_digest": "b" * 64, + "corroboration_class": "TEST", "result": outcome.value, } verification = _proposition( @@ -2001,3 +2021,57 @@ def test_vstd5_rechecker_refuses_external_shape_and_payload_defects() -> None: ) with pytest.raises(ValueError, match="missing verdict-material bytes"): recheck_vstd5_receipt(entry, missing_payload, mechanisms=()) + + +def test_vstd5_rechecker_refuses_schema_valid_cross_field_contradictions() -> None: + store, session = _session() + entry = _established_vstd4(store, session) + witness, assertion, corroboration = _witness_components( + store, entry, "witness:one" + ) + bundle = WitnessBundle( + "claim:fixture", + "declarant:one", + entry.witness.header.binding, # type: ignore[union-attr] + (witness,), + (assertion,), + (corroboration,), + ) + result = assess_witness_corroboration(entry, bundle, session=session) + receipt = build_vstd5_receipt( + entry, + bundle, + result, + receipt_id="VFY-5-CROSS-FIELD", + session=session, + ) + validator = _vstd5_schema_validator() + + for field_name in ("result_digest", "witness_digest"): + candidate = copy.deepcopy(receipt) + candidate["entry_vstd4"][field_name] = "0" * 64 + validator.validate(candidate) + with pytest.raises(ValueError, match="inconsistent VSTD-4 entry"): + recheck_vstd5_receipt( + entry, candidate, mechanisms=(ExactFactMechanism(),) + ) + + relabeled = copy.deepcopy(receipt) + relabeled["bundle"]["corroborations"][0][ + "corroboration_class" + ] = "UNIVERSAL_FORMAL_PROOF" + validator.validate(relabeled) + with pytest.raises(ValueError, match="recomputed VSTD-5 result"): + recheck_vstd5_receipt( + entry, relabeled, mechanisms=(ExactFactMechanism(),) + ) + + relabeled_bundle = WitnessBundle.from_dict(relabeled["bundle"]) + relabeled_result = assess_witness_corroboration( + entry, relabeled_bundle, session=session + ) + assert relabeled_result.status is WitnessResultStatus.UNKNOWN + assert relabeled_result.conformance_status == "NOT_ESTABLISHED" + assert relabeled_result.corroboration_errors == ( + "corroboration corroboration:witness:one is not exactly bound", + ) diff --git a/tests/test_public_data.py b/tests/test_public_data.py index d161dbd..267c336 100644 --- a/tests/test_public_data.py +++ b/tests/test_public_data.py @@ -5,9 +5,11 @@ from __future__ import annotations import json +from dataclasses import replace from pathlib import Path import pytest +from jsonschema import Draft202012Validator from verifier.core.checker import VerificationVerdict from verifier.core.provenance import GitProvenance, ProvenanceRecord, RuntimeEnvironment @@ -22,6 +24,7 @@ TransformationHyperedge, TransformationType, ) +from verifier.data.assurance import AssuranceFlowError, AssuranceLedger from verifier.data.policy import ProvenancePolicyVerifier from verifier.data.receipt import ( DataIndependentAudit, @@ -276,6 +279,84 @@ def test_artifact_and_transformation_identifiers_are_globally_disjoint() -> None ) +def test_frozen_graph_reader_preserves_separate_identifier_namespaces( + tmp_path: Path, capsys +) -> None: + artifact = ArtifactNode( + artifact_id="artifact:input", + label="input", + artifact_type=ArtifactType.RAW_SOURCE_FILE, + content_digest="a" * 64, + ) + output = ArtifactNode( + artifact_id="artifact:output", + label="output", + artifact_type=ArtifactType.EVALUATION_REPORT, + content_digest="b" * 64, + ) + transformation = TransformationHyperedge( + transformation_id="artifact:input", + label="historical overlapping identifier", + transformation_type=TransformationType.EVALUATION, + inputs=(HyperedgePort("artifact:input", "INPUT"),), + outputs=(HyperedgePort("artifact:output", "OUTPUT"),), + software_provenance={}, + parameters={}, + execution_environment={}, + ) + payload = { + "artifacts": [artifact.to_dict(), output.to_dict()], + "transformations": [transformation.to_dict()], + "contributors": [], + "rights": [], + "conflicts": [], + } + graph_schema = json.loads( + ( + Path(__file__).resolve().parents[1] + / "receipts/schema/vstd_graph_receipt.json" + ).read_text() + )["properties"]["hypergraph"] + Draft202012Validator(graph_schema).validate(payload) + + restored = ProvenanceHypergraph.from_dict(payload) + assert restored.to_dict() == payload + assert restored.validate_structure( + allow_legacy_identifier_overlap=True + ) == [] + assert restored.validate_structure()[0] == ( + "artifact and transformation identifiers must be disjoint: artifact:input" + ) + with pytest.raises(ValueError, match="identifiers must be disjoint"): + ProvenanceHypergraph.from_dict( + payload, allow_legacy_identifier_overlap=False + ) + duplicate_artifact = { + **payload, + "artifacts": [*payload["artifacts"], artifact.to_dict()], + } + with pytest.raises(ValueError, match="duplicate graph identifier"): + ProvenanceHypergraph.from_dict(duplicate_artifact) + with pytest.raises(AssuranceFlowError, match="invalid source graph"): + AssuranceLedger(restored) + + receipt = _receipt() + receipt.hypergraph = restored + receipt.completeness_metrics = restored.compute_completeness() + receipt.independent_audit = replace( + receipt.independent_audit, + acyclic_hypergraph=True, + integrity_passed=True, + root_sources_count=1, + terminal_outputs_count=1, + transformations_count=1, + ) + receipt.save_to_directory(tmp_path) + assert validate_data_receipt(tmp_path) == 0 + assert "[VALIDATION OK]" in capsys.readouterr().out + assert reproduce_data_receipt(tmp_path) == 0 + + def test_completeness_rejects_non_hex_digest() -> None: graph = ProvenanceHypergraph() graph.add_artifact( From 3cb54359451833e0c8984c78d3f2de6307f1282d Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 12:13:35 -0700 Subject: [PATCH 21/34] fix(vstd5): bind bundles to admitted claim identity Retain the claim_id used by evidence-bound VSTD-4 establishment and require VSTD-5 witness bundles to use that exact identifier. A shared binding or certificate digest no longer permits a neighboring claim ID to acquire an established corroboration result. Keep claim identity distinct from ClaimBinding.coordinate.subject, preserve representable UNKNOWN receipts, and cover name-only, mechanism-consistent relabeling, replay tampering, and offline replay. --- docs/API_STABILITY.md | 5 +- docs/ARCHITECTURE.md | 6 +- src/verifier/core/depth.py | 3 + src/verifier/core/witness.py | 4 + src/verifier/specifications/VSTD-5.md | 8 +- standard/VSTD-5.md | 8 +- tests/test_evidence_bound_assurance.py | 107 +++++++++++++++++++++++++ 7 files changed, 135 insertions(+), 6 deletions(-) diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md index e7d299d..5942751 100644 --- a/docs/API_STABILITY.md +++ b/docs/API_STABILITY.md @@ -40,8 +40,9 @@ result. The supported `build_vstd5_receipt` boundary is stricter: it either rais an object satisfying the published receipt shape with all verdict-material evidence bytes. `recheck_vstd5_receipt` applies the same zero-dependency structural gate before replay and does not accept a schema-invalid assessment object as a portable receipt. It also compares -the complete carried VSTD-4 entry and mechanism-checks `corroboration_class`; schema-valid -field relabeling cannot retain an established replay result. +the complete carried VSTD-4 entry, requires the bundle `claim_id` to equal the admitted +VSTD-4 claim identifier, and mechanism-checks `corroboration_class`; schema-valid field +relabeling cannot retain an established replay result. `ProvenanceHypergraph.from_dict` retains the frozen `VSTD-DATA-0.1` two-namespace reader: one identifier may occur once as an artifact and once as a transformation. Direct `add_*` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 608a01e..90c783b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -310,8 +310,10 @@ separate from portable-record admission: the builder raises unless the result in strict VSTD-5 schema and embeds every verdict-material evidence byte, and the rechecker enforces the same zero-dependency shape gate before replay. It also compares every redundant VSTD-4 entry coordinate and treats `corroboration_class` as part of the mechanism-checked -expected proposition, so relabeling either cannot retain an established result. This path -does not upgrade the legacy generic-run fields. +expected proposition. The admitted evidence-bound VSTD-4 result retains its exact `claim_id`, +and VSTD-5 requires the bundle to use that identifier; a shared binding or certificate digest +cannot turn a neighboring identifier into an alias. Thus relabeling any of these coordinates +cannot retain an established result. This path does not upgrade the legacy generic-run fields. Graph conflict records retain incompatible values and their evidence references without adding a scalar score or changing the frozen artifact-status vocabulary. A conflict makes diff --git a/src/verifier/core/depth.py b/src/verifier/core/depth.py index 01ada5b..1eb0ede 100644 --- a/src/verifier/core/depth.py +++ b/src/verifier/core/depth.py @@ -203,6 +203,7 @@ class EvidenceBoundDepthResult: rung_evaluations: tuple[tuple[str, EvaluatedProposition], ...] binding_errors: tuple[str, ...] kernel_outcome: str + claim_id: str @property def depth(self) -> int: @@ -255,6 +256,7 @@ def to_dict(self) -> dict[str, object]: }, "binding_errors": list(self.binding_errors), "kernel_outcome": self.kernel_outcome, + "claim_id": self.claim_id, } ) return payload @@ -528,6 +530,7 @@ def binding_error( tuple(rung_results), tuple(errors), kernel_outcome, + claim_id, ) diff --git a/src/verifier/core/witness.py b/src/verifier/core/witness.py index 7f94879..8372d16 100644 --- a/src/verifier/core/witness.py +++ b/src/verifier/core/witness.py @@ -545,6 +545,10 @@ def assess_witness_corroboration( if bundle.claim_id == "": binding_errors.append("claim_id must not be empty") + if bundle.claim_id != entry.claim_id: + binding_errors.append( + "witness bundle claim_id does not match the admitted VSTD-4 claim_id" + ) if bundle.declarant_id == "": identity_errors.append("declarant_id must not be empty") if bundle.claim_binding_digest != entry.witness.header.binding: # type: ignore[union-attr] diff --git a/src/verifier/specifications/VSTD-5.md b/src/verifier/specifications/VSTD-5.md index 5f00ba4..1d8cbb3 100644 --- a/src/verifier/specifications/VSTD-5.md +++ b/src/verifier/specifications/VSTD-5.md @@ -20,6 +20,12 @@ Every VSTD-5 procedure MUST reject a claim unless VSTD-1, VSTD-2, and VSTD-3 preconditions and all VSTD-4 rung propositions were evidence-bound and checked, establishing VSTD-4 conformance at depth 14. +The VSTD-5 bundle `claim_id` MUST equal the exact `claim_id` admitted by that +evidence-bound VSTD-4 result. A shared claim-binding or certificate digest does not +establish that a neighboring identifier is an alias. Any future identifier mapping +would require its own bounded proposition and mechanism; the reference mechanism does +not implement such aliases. + The compatibility `vstd4_depth` candidate never satisfies this gate. The reference `establish_vstd4` path may satisfy it only after rerunning every exact evidence binding and checking its depth certificate. `require_vstd5_entry` distinguishes the @@ -112,7 +118,7 @@ another corroboration identifier is rejected rather than counted twice. `verifier.core.witness.assess_witness_corroboration` performs, in order: -1. evidence-bound VSTD-4 entry validation; +1. evidence-bound VSTD-4 entry and exact claim-identifier validation; 2. identity-evidence availability and duplicate detection; 3. exact seven-dimension independence evaluation; 4. exact corroboration binding and mechanism execution; diff --git a/standard/VSTD-5.md b/standard/VSTD-5.md index 5f00ba4..1d8cbb3 100644 --- a/standard/VSTD-5.md +++ b/standard/VSTD-5.md @@ -20,6 +20,12 @@ Every VSTD-5 procedure MUST reject a claim unless VSTD-1, VSTD-2, and VSTD-3 preconditions and all VSTD-4 rung propositions were evidence-bound and checked, establishing VSTD-4 conformance at depth 14. +The VSTD-5 bundle `claim_id` MUST equal the exact `claim_id` admitted by that +evidence-bound VSTD-4 result. A shared claim-binding or certificate digest does not +establish that a neighboring identifier is an alias. Any future identifier mapping +would require its own bounded proposition and mechanism; the reference mechanism does +not implement such aliases. + The compatibility `vstd4_depth` candidate never satisfies this gate. The reference `establish_vstd4` path may satisfy it only after rerunning every exact evidence binding and checking its depth certificate. `require_vstd5_entry` distinguishes the @@ -112,7 +118,7 @@ another corroboration identifier is rejected rather than counted twice. `verifier.core.witness.assess_witness_corroboration` performs, in order: -1. evidence-bound VSTD-4 entry validation; +1. evidence-bound VSTD-4 entry and exact claim-identifier validation; 2. identity-evidence availability and duplicate detection; 3. exact seven-dimension independence evaluation; 4. exact corroboration binding and mechanism execution; diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py index 0a268dc..9c80e8b 100644 --- a/tests/test_evidence_bound_assurance.py +++ b/tests/test_evidence_bound_assurance.py @@ -530,6 +530,7 @@ def test_evidence_bound_vstd4_receipt_replays_offline_and_matches_schema() -> No rechecked = recheck_evidence_bound_vstd4_receipt( receipt, mechanisms=(ExactFactMechanism(),) ) + assert rechecked.claim_id == "claim:fixture" assert rechecked.conformance_status == "ESTABLISHED" reference = next(iter(receipt["evidence_payloads"])) @@ -1969,6 +1970,112 @@ def test_vstd5_builder_returns_only_strict_replayable_receipts() -> None: ) +def test_vstd5_claim_id_must_match_the_admitted_vstd4_claim() -> None: + store, session = _session() + entry = _established_vstd4(store, session) + witness, assertion, corroboration = _witness_components( + store, entry, "witness:one" + ) + valid = WitnessBundle( + entry.claim_id, + "declarant:one", + entry.witness.header.binding, # type: ignore[union-attr] + (witness,), + (assertion,), + (corroboration,), + ) + positive = assess_witness_corroboration(entry, valid, session=session) + assert positive.status is WitnessResultStatus.CORROBORATED + assert positive.conformance_status == "ESTABLISHED" + + name_only = assess_witness_corroboration( + entry, replace(valid, claim_id="claim:neighbor"), session=session + ) + assert name_only.status is WitnessResultStatus.UNKNOWN + assert name_only.conformance_status == "NOT_ESTABLISHED" + assert ( + "witness bundle claim_id does not match the admitted VSTD-4 claim_id" + in name_only.binding_errors + ) + + neighbor_verification = _proposition( + store, + "claim:neighbor", + "vstd5.corroboration", + dict(corroboration.verification.expected), + parameters=dict(corroboration.verification.parameters), + ) + neighbor_record = replace( + corroboration, + observed_evidence_refs=neighbor_verification.evidence_refs, + verification=neighbor_verification, + ) + neighbor = replace( + valid, + claim_id="claim:neighbor", + corroborations=(neighbor_record,), + ) + result = assess_witness_corroboration(entry, neighbor, session=session) + assert result.status is WitnessResultStatus.UNKNOWN + assert result.conformance_status == "NOT_ESTABLISHED" + assert result.binding_errors == ( + "witness bundle claim_id does not match the admitted VSTD-4 claim_id", + ) + + receipt = build_vstd5_receipt( + entry, + neighbor, + result, + receipt_id="VFY-5-NEIGHBOR-CLAIM", + session=session, + ) + _vstd5_schema_validator().validate(receipt) + assert recheck_vstd5_receipt( + entry, receipt, mechanisms=(ExactFactMechanism(),) + ).to_dict() == result.to_dict() + + positive_receipt = build_vstd5_receipt( + entry, + valid, + positive, + receipt_id="VFY-5-CLAIM-TAMPER", + session=session, + ) + positive_receipt["bundle"]["claim_id"] = "claim:neighbor" + _vstd5_schema_validator().validate(positive_receipt) + with pytest.raises(ValueError, match="recomputed VSTD-5 result"): + recheck_vstd5_receipt( + entry, positive_receipt, mechanisms=(ExactFactMechanism(),) + ) + + +def test_vstd5_claim_id_is_distinct_from_the_claim_coordinate_subject() -> None: + store, session = _session() + binding = replace( + _binding(), + coordinate=ClaimCoordinate( + "artifact:coordinate-subject", "fixture", {"scope": "test"} + ), + ) + entry = _established_vstd4(store, session, binding=binding) + witness, assertion, corroboration = _witness_components( + store, entry, "witness:one" + ) + bundle = WitnessBundle( + "claim:fixture", + "declarant:one", + entry.witness.header.binding, # type: ignore[union-attr] + (witness,), + (assertion,), + (corroboration,), + ) + result = assess_witness_corroboration(entry, bundle, session=session) + assert entry.claim_id == "claim:fixture" + assert entry.claim_id != binding.coordinate.subject + assert result.status is WitnessResultStatus.CORROBORATED + assert result.conformance_status == "ESTABLISHED" + + def test_vstd5_rechecker_refuses_external_shape_and_payload_defects() -> None: store, session = _session() entry = _established_vstd4(store, session) From 5d0e9a6032f3444de3ba63ec3921755c23406949 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 17:59:49 -0700 Subject: [PATCH 22/34] docs(time): record GUILT composition contradiction --- TIME.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/TIME.md b/TIME.md index 297804c..0cf253a 100644 --- a/TIME.md +++ b/TIME.md @@ -1,6 +1,6 @@ # TIME -Status: CLEAR +Status: OPEN TIME is the live repository-contradiction annunciator. Its status is repository process metadata, not Verifier Standard (VSTD) receipt vocabulary. A live entry belongs here only @@ -13,7 +13,30 @@ escalation, see [`HUMANS.md`](HUMANS.md). ## Live contradictions -None. +### GUILT composition can exceed separately earned components + +- **Implementation coordinate:** + `src/verifier/data/assurance.py::AssuranceLedger.diagnose` +- **Current implementation proposition:** one combined + `vstd.graph.diagnostic.guilt` evaluation can establish `GUILT` after causal + localization when an obligation string is repeated consistently in the proposition. +- **Governing proposition:** `GUILT` requires separately bound, passing evaluations for + responsibility or material contribution, obligation applicability, and violation of + that same obligation. A compound mechanism may perform all three checks but must emit + three separately bound component evaluations. +- **Smallest reproduced counterexample:** record a passing `RUST` event and causal + localization, omit `BLAME` or another responsibility-component result, omit obligation + applicability and violation results, then pass one exact-fact GUILT proposition carrying + a decorative `violated_obligation` string. The current runtime returns `ESTABLISHED`. +- **Affected surfaces:** Graph assurance runtime and replay, the + `VSTD-GRAPH-ASSURANCE-1` schema, normative and packaged Ladder text, architecture and + claim-boundary documentation, generated API reference, tests, changelog, and pull-request + review map. +- **Repair gate:** bind the final GUILT result to exact passing responsibility, + applicability, and violation component digests; reject missing, failed, unknown, + malformed, duplicate, conflicting, or neighboring components; embed and replay every + component's evidence; then complete focused and full validation before returning TIME to + `Status: CLEAR`. When a contradiction is open, change the status to `Status: OPEN` and record the exact coordinates, both incompatible claims, evidence for each side, and affected behavior. An From 5eb38f3d3f9b6bfaa4ab9664d21f49b6df695a6c Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sat, 29 Aug 2026 18:39:18 -0700 Subject: [PATCH 23/34] fix(assurance): compose GUILT from bound components --- CHANGELOG.md | 12 +- HUMANS.md | 14 +- TIME.md | 27 +- docs/ARCHITECTURE.md | 11 +- docs/CLAIMS_AND_LIMITS.md | 5 + docs/reference.html | 18 +- src/verifier/__init__.py | 2 + src/verifier/core/evidence.py | 131 +++ src/verifier/data/__init__.py | 2 + src/verifier/data/assurance.py | 826 +++++++++++++++++- src/verifier/specifications/LADDER.md | 38 +- standard/LADDER.md | 38 +- .../vstd-graph-assurance-1.schema.json | 100 ++- tests/test_evidence_bound_assurance.py | 31 +- tests/test_guilt_composition.py | 568 ++++++++++++ tests/test_public_api.py | 1 + 16 files changed, 1716 insertions(+), 108 deletions(-) create mode 100644 tests/test_guilt_composition.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9df3416..6756004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,9 +125,15 @@ and prerequisite TRUST events; current eligibility recursively fails closed when any bound dependency degrades or conflicts. Duplicate paths remain set-valued, historical graph bytes remain immutable, and topology alone earns no causal or moral conclusion. BLAME establishes - bounded responsibility or material contribution; GUILT additionally binds an exact violated - obligation and is not BLAME in the opposite direction. Localization binds one exact passing - RUST event and descendant-deviation proposition. Status-conflict resolution projects the + bounded responsibility or material contribution. GUILT is not BLAME in the opposite + direction: it composes separately bound responsibility, exact scoped-obligation + applicability, and same-obligation violation components, then binds their exact event + digests. One compound mechanism may emit all three component evaluations in one invocation; + an opaque combined pass or decorative obligation string remains `NOT_ESTABLISHED`. + Localization binds one exact passing RUST event and descendant-deviation proposition. + Neither result establishes actor morality, reputation, automatic legal liability, + innocence, exoneration, obligation satisfaction, or absence of hidden contributors. + Status-conflict resolution projects the selected state into current admissibility; arbitrary resolved predicates remain blocked. The current runtime has no general non-status admissibility-effect mechanism. RUST follows historically recorded contributing ancestry even when current lifecycle state excludes a diff --git a/HUMANS.md b/HUMANS.md index 531337b..394ac8a 100644 --- a/HUMANS.md +++ b/HUMANS.md @@ -53,10 +53,16 @@ The reference `AssuranceLedger` records these as additive Graph events. Treat st RUST concentration as a triage count of unique deviating descendants, never causal strength. A bounded artifact-relative `BLAME` or `GUILT` result exists only after separate localization and attribution mechanisms pass. BLAME establishes responsibility or material -contribution for the exact deviation; GUILT is the stronger obligation-relative result and -is not BLAME flowing in the opposite direction. The localization must name the exact passing -RUST event and descendant-deviation binding; sharing a descendant identifier is insufficient. -Neither evaluates an actor's character. A resolved conflict restores current TRUST only when +contribution for the exact deviation. GUILT is not BLAME flowing in the opposite direction: +it requires separately bound passing responsibility, exact obligation-applicability, and +same-obligation violation components, then a final evaluation binding all three component +digests. One compound mechanism may perform the three checks in one invocation only when it +emits three separately bound evaluations. A label such as `violated_obligation`, even repeated +consistently, earns nothing by itself. The localization must name the exact passing RUST event +and descendant-deviation binding; sharing a descendant identifier is insufficient. Neither +result evaluates an actor's character, reputation, social standing, or automatic legal +liability. Missing GUILT is not innocence or exoneration, and does not prove obligation +satisfaction or absence of hidden contributors. A resolved conflict restores current TRUST only when its selected value has a checked admissible status consequence; selecting an arbitrary value does not establish admissibility. For reliance, replay the portable log with `recheck_assurance_log`: a stored event word or diff --git a/TIME.md b/TIME.md index 0cf253a..297804c 100644 --- a/TIME.md +++ b/TIME.md @@ -1,6 +1,6 @@ # TIME -Status: OPEN +Status: CLEAR TIME is the live repository-contradiction annunciator. Its status is repository process metadata, not Verifier Standard (VSTD) receipt vocabulary. A live entry belongs here only @@ -13,30 +13,7 @@ escalation, see [`HUMANS.md`](HUMANS.md). ## Live contradictions -### GUILT composition can exceed separately earned components - -- **Implementation coordinate:** - `src/verifier/data/assurance.py::AssuranceLedger.diagnose` -- **Current implementation proposition:** one combined - `vstd.graph.diagnostic.guilt` evaluation can establish `GUILT` after causal - localization when an obligation string is repeated consistently in the proposition. -- **Governing proposition:** `GUILT` requires separately bound, passing evaluations for - responsibility or material contribution, obligation applicability, and violation of - that same obligation. A compound mechanism may perform all three checks but must emit - three separately bound component evaluations. -- **Smallest reproduced counterexample:** record a passing `RUST` event and causal - localization, omit `BLAME` or another responsibility-component result, omit obligation - applicability and violation results, then pass one exact-fact GUILT proposition carrying - a decorative `violated_obligation` string. The current runtime returns `ESTABLISHED`. -- **Affected surfaces:** Graph assurance runtime and replay, the - `VSTD-GRAPH-ASSURANCE-1` schema, normative and packaged Ladder text, architecture and - claim-boundary documentation, generated API reference, tests, changelog, and pull-request - review map. -- **Repair gate:** bind the final GUILT result to exact passing responsibility, - applicability, and violation component digests; reject missing, failed, unknown, - malformed, duplicate, conflicting, or neighboring components; embed and replay every - component's evidence; then complete focused and full validation before returning TIME to - `Status: CLEAR`. +None. When a contradiction is open, change the status to `Status: OPEN` and record the exact coordinates, both incompatible claims, evidence for each side, and affected behavior. An diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 90c783b..7db082b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -161,7 +161,7 @@ Maturity attaches to mechanisms beneath that architecture: | Assurance event serialization and replay | Implemented bounded reference mechanism | `VSTD-GRAPH-ASSURANCE-1` embeds the historical Graph, exact bindings, evidence bytes, a hash chain, and a current-view digest; `recheck_assurance_log` reruns every event mechanism | | TRUST transfer | Implemented edge-local proposition-dispatch reference mechanism | `record_trust` binds one exact transformation, its complete inputs and output, the historical Graph digest, and the prerequisite TRUST event for every derived input. Recursive current-admissibility checking excludes the route if any required event, artifact, or transformation degrades or conflicts, without deleting history. No universal scalar support algebra exists. | | ROT derivation and cross-surface propagation | Implemented bounded reference mechanisms | Strictly degrading status propositions and complete challenge-ledger projections produce additive current-state overlays; the deduplicated descendant impact set is discovery, and a descendant status change still needs its own mechanism | -| RUST concentration and localization | Implemented bounded reference mechanisms | A passing descendant-deviation proposition produces deduplicated reverse reachability; concentration counts unique descendants; localization and artifact-relative diagnostic attribution require separate passing propositions | +| RUST concentration, localization, and diagnostic attribution | Implemented bounded reference mechanisms | A passing descendant-deviation proposition produces deduplicated reverse reachability; concentration counts unique descendants; localization and BLAME require separate passing propositions. GUILT additionally composes exact responsibility, obligation-applicability, and obligation-violation components; an opaque obligation label cannot establish it. | | Complete `PASS`/`FAIL`/`UNKNOWN`/`CONFLICTED` hidden-witness derivation | Experimental and unimplemented | A caller-supplied state tag is not an earned verdict | | Specific optional proof backends | Backend-specific maturity; the RISC Zero example has one recorded native proof | Optional proof machinery cannot make the governing architecture optional or establish broader VSTD conformance | @@ -271,8 +271,13 @@ They do not establish actor standing, moral character, responsibility, or automa ancestor falsification. The reference runtime emits bounded TRUST, ROT, and RUST events only after their exact mechanisms run. Causal localization and artifact-relative `BLAME` or `GUILT` require additional exact propositions. BLAME establishes bounded responsibility -or material contribution; GUILT is not directionally opposite, but additionally binds and -checks an exact violated obligation. Neither becomes actor reputation. +or material contribution. GUILT is not directionally opposite: it composes three separately +bound passing evaluations for responsibility, applicability of an exact scoped obligation, +and violation of that same obligation relative to the same localized deviation. The final +evaluation binds all three component digests. One compound mechanism may emit those three +evaluations in one invocation, but one opaque combined result cannot substitute for them. +Neither becomes actor reputation, moral character, automatic legal liability, or social +scoring; missing GUILT does not establish innocence or exoneration. ### Recursive-amplification falsification outcomes diff --git a/docs/CLAIMS_AND_LIMITS.md b/docs/CLAIMS_AND_LIMITS.md index 50b7f19..01af7b4 100644 --- a/docs/CLAIMS_AND_LIMITS.md +++ b/docs/CLAIMS_AND_LIMITS.md @@ -68,6 +68,7 @@ required falsification 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 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. | | “The compatibility API returned Graph `level = N`.†| **Not yet as a conformance claim.** | The current implementation computes candidate Graph profile `N` from caller-supplied artifact and edge ratings; `level` is the retained field name for that profile number. It labels the result `CALLER_SUPPLIED` and `NOT_ESTABLISHED`. | A structurally valid graph and explicit supplied ratings. Conformance additionally requires implemented rating-to-evidence bindings for every required profile coordinate. | The supplied ratings were independently derived, every coordinate's evidence passed, or Graph conformance was established. | | “The evidence-bound Graph path established profile `N`.†| **Only for profile 1–5 at the exact collection and current view under the rerun mechanisms.** | Every member, ancestor, and reached edge rating binding passed; each rating binds the Graph bytes, deduplicated member set, collection, and claim; the Graph certificate checked; embedded evidence permits replay. | Exact Graph/event-log bytes, bindings, evidence, mechanism digests, trust roots, bounds, and recheck result. | Profile zero is conformance, real-world lineage is complete, topology proves causality, or another collection inherits the result. | @@ -231,5 +232,9 @@ Do not publish any of these without the missing qualification: - “All compute was accounted for†without complete-mediation evidence for every path in the named governed boundary. - “No undeclared compute occurred†from a device, host, provider, or fleet receipt. +- “GUILT†from a decorative obligation string, an opaque combined `PASS`, graph placement, + actor identity, role, ownership, reputation, or a violation that does not bind the same + artifact, scoped obligation, and localized deviation. +- “No GUILT means innocence, exoneration, obligation satisfaction, or no hidden contributor.†- “Sealed means encrypted, immutable, correct, externally archived, or continuously guarded.†diff --git a/docs/reference.html b/docs/reference.html index 146c637..303abd2 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -433,10 +433,15 @@

    AssuranceLedger class

    Defined in verifier.data.assurance

    + - + + + + + @@ -517,6 +522,16 @@

    MechanismOutcome enum

    Defined in verifier.core.evidence

    Members: PASS, FAIL, UNKNOWN

    +
    +

    ObligationCoordinate class

    +
    ObligationCoordinate(obligation_id: 'str' = '', content_digest: 'str' = '', scope: 'Mapping[str, str]' = <factory>, assumptions: 'tuple[str, ...]' = (), exclusions: 'tuple[str, ...]' = ()) -> None
    +

    Exact technical obligation and the declared scope in which it applies.

    +

    Defined in verifier.data.assurance

    +
    MethodSummary
    admissibility_blocking_conflicts(self) -> 'tuple[ConflictRecord, ...]'Return conflicts whose effect still blocks a clean TRUST route.
    compose_guilt(self, ancestor_id: 'str', descendant_id: 'str', obligation: 'ObligationCoordinate', proposition: 'BoundProposition', *, localization_event_digest: 'str', responsibility_component_digest: 'str', applicability_component_digest: 'str', violation_component_digest: 'str', session: 'VerificationSession', recorded_at: 'str') -> 'DiagnosticAttribution'Compose technical GUILT from three exact, separately earned components.
    current_status(self, artifact_id: 'str') -> 'ArtifactStatus'
    current_transformation_status(self, transformation_id: 'str') -> 'str'Return a transformation's additive current status projection.
    current_trust_events(self) -> 'tuple[AssuranceEvent, ...]'Return recursively current edge-local TRUST records.
    diagnose(self, kind: 'DiagnosticKind', ancestor_id: 'str', descendant_id: 'str', proposition: 'Optional[BoundProposition]', *, session: 'VerificationSession', recorded_at: 'str') -> 'DiagnosticAttribution'Compute bounded artifact-relative BLAME or GUILT just in time.
    diagnose(self, kind: 'DiagnosticKind', ancestor_id: 'str', descendant_id: 'str', proposition: 'Optional[BoundProposition]', *, session: 'VerificationSession', recorded_at: 'str') -> 'DiagnosticAttribution'Compute BLAME, or fail closed for legacy opaque GUILT calls.
    establish_guilt_components(self, ancestor_id: 'str', descendant_id: 'str', obligation: 'ObligationCoordinate', responsibility_proposition: 'BoundProposition', applicability_proposition: 'BoundProposition', violation_proposition: 'BoundProposition', *, localization_event_digest: 'str', session: 'VerificationSession', recorded_at: 'str') -> 'tuple[AssuranceEvent, AssuranceEvent, AssuranceEvent]'Run one compound mechanism and retain three separately bound results.
    establish_obligation_applicability(self, artifact_id: 'str', obligation: 'ObligationCoordinate', proposition: 'BoundProposition', *, session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Evaluate whether one exact obligation applies to one exact artifact.
    establish_obligation_violation(self, ancestor_id: 'str', descendant_id: 'str', obligation: 'ObligationCoordinate', proposition: 'BoundProposition', *, localization_event_digest: 'str', applicability_component_digest: 'str', session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Evaluate violation after exact applicability and deviation localization.
    establish_responsibility(self, ancestor_id: 'str', descendant_id: 'str', proposition: 'BoundProposition', *, localization_event_digest: 'str', session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Evaluate one separately bound material-contribution component.
    events(self) -> 'tuple[AssuranceEvent, ...]'
    impacted_descendants(self, artifact_id: 'str') -> 'tuple[str, ...]'Return the deduplicated recorded forward impact set, not a verdict.
    localize_cause(self, ancestor_id: 'str', descendant_id: 'str', proposition: 'BoundProposition', *, rust_event_digest: 'str', session: 'VerificationSession', recorded_at: 'str') -> 'AssuranceEvent'Bind one ancestor to one exact passing descendant-deviation event.
    + + +
    MethodSummary
    digest(self) -> 'str'
    to_dict(self) -> 'dict[str, Any]'
    +

    ProvenanceHypergraph class

    ProvenanceHypergraph() -> 'None'
    @@ -566,6 +581,7 @@

    VerificationSession class

    Defined in verifier.core.evidence

    +
    MethodSummary
    evaluate(self, binding: 'BoundProposition') -> 'EvaluatedProposition'
    evaluate_compound(self, bindings: 'Sequence[BoundProposition]') -> 'tuple[EvaluatedProposition, ...]'Run one compound mechanism invocation over separately bound propositions.
    register(self, mechanism: 'VerificationMechanism') -> 'None'
    diff --git a/src/verifier/__init__.py b/src/verifier/__init__.py index ed9fd77..31ffced 100644 --- a/src/verifier/__init__.py +++ b/src/verifier/__init__.py @@ -67,6 +67,7 @@ "build_vstd5_receipt": ("verifier.core.witness", "build_vstd5_receipt"), "recheck_vstd5_receipt": ("verifier.core.witness", "recheck_vstd5_receipt"), "AssuranceLedger": ("verifier.data.assurance", "AssuranceLedger"), + "ObligationCoordinate": ("verifier.data.assurance", "ObligationCoordinate"), "recheck_assurance_log": ( "verifier.data.assurance", "recheck_assurance_log", @@ -160,6 +161,7 @@ def __dir__() -> list[str]: ) from verifier.data.assurance import ( AssuranceLedger as AssuranceLedger, + ObligationCoordinate as ObligationCoordinate, recheck_assurance_log as recheck_assurance_log, ) from verifier.data.graph_level import ( diff --git a/src/verifier/core/evidence.py b/src/verifier/core/evidence.py index fcafcd7..be1cb63 100644 --- a/src/verifier/core/evidence.py +++ b/src/verifier/core/evidence.py @@ -320,6 +320,137 @@ def evaluate(self, binding: BoundProposition) -> EvaluatedProposition: dict(decision.observations), ) + def evaluate_compound( + self, bindings: Sequence[BoundProposition] + ) -> tuple[EvaluatedProposition, ...]: + """Run one compound mechanism invocation over separately bound propositions. + + Every returned evaluation retains its own binding digest, evidence + references, trust roots, bounds, and outcome. A mechanism without an + explicit ``evaluate_compound`` entry point cannot use this path; three + ordinary evaluations are not relabeled as one compound invocation. + """ + + items = tuple(bindings) + if not items: + raise EvidenceBindingError( + "compound evaluation requires at least one bound proposition" + ) + mechanism_id = items[0].mechanism_id + mechanism_digest = _normalize_ref(items[0].mechanism_digest) + if any( + item.mechanism_id != mechanism_id + or _normalize_ref(item.mechanism_digest) != mechanism_digest + for item in items + ): + return tuple( + self._unknown( + item, + "compound propositions do not bind one exact mechanism", + 0, + ) + for item in items + ) + mechanism = self._mechanisms.get(mechanism_id) + if mechanism is None: + return tuple( + self._unknown(item, "bound mechanism is not registered", 0) + for item in items + ) + if _normalize_ref(mechanism.mechanism_digest) != mechanism_digest: + return tuple( + self._unknown( + item, "registered mechanism digest does not match", 0 + ) + for item in items + ) + evaluator = getattr(mechanism, "evaluate_compound", None) + if not callable(evaluator): + return tuple( + self._unknown( + item, + "bound mechanism has no compound evaluation entry point", + 0, + ) + for item in items + ) + + evidence_sets: list[tuple[bytes, ...]] = [] + observed_sizes: list[int] = [] + for item in items: + if len(item.evidence_refs) > item.bounds.max_evidence_items: + return tuple( + self._unknown( + other, + "compound evidence item bound exceeded before invocation", + 0, + ) + for other in items + ) + try: + payloads = tuple( + self.evidence.resolve(reference) + for reference in item.evidence_refs + ) + except EvidenceBindingError as exc: + return tuple( + self._unknown( + other, + f"compound evidence resolution failed: {exc}", + 0, + ) + for other in items + ) + observed_bytes = sum(len(payload) for payload in payloads) + if observed_bytes > item.bounds.max_evidence_bytes: + return tuple( + self._unknown( + other, + "compound evidence byte bound exceeded before invocation", + observed_bytes if other is item else 0, + ) + for other in items + ) + evidence_sets.append(payloads) + observed_sizes.append(observed_bytes) + + try: + decisions = tuple(evaluator(items, tuple(evidence_sets))) + except Exception as exc: + return tuple( + self._unknown( + item, + f"compound mechanism execution failed: {type(exc).__name__}: {exc}", + observed_sizes[index], + ) + for index, item in enumerate(items) + ) + if len(decisions) != len(items) or any( + not isinstance(decision, MechanismDecision) for decision in decisions + ): + return tuple( + self._unknown( + item, + "compound mechanism did not return one decision per binding", + observed_sizes[index], + ) + for index, item in enumerate(items) + ) + return tuple( + EvaluatedProposition( + item.digest(), + decision.outcome, + item.mechanism_id, + _normalize_ref(item.mechanism_digest), + item.evidence_refs, + item.trust_roots, + observed_sizes[index], + decision.details, + dict(decision.observations), + ) + for index, (item, decision) in enumerate(zip(items, decisions)) + ) + @staticmethod def _unknown( binding: BoundProposition, details: str, observed_bytes: int diff --git a/src/verifier/data/__init__.py b/src/verifier/data/__init__.py index 29bde2c..cd87778 100644 --- a/src/verifier/data/__init__.py +++ b/src/verifier/data/__init__.py @@ -17,6 +17,7 @@ ChallengeProjectionMechanism, DiagnosticAttribution, DiagnosticKind, + ObligationCoordinate, recheck_assurance_log, ) @@ -66,6 +67,7 @@ "ChallengeProjectionMechanism", "DiagnosticAttribution", "DiagnosticKind", + "ObligationCoordinate", "recheck_assurance_log", "PolicyEvaluationResult", "ProvenancePolicyVerifier", diff --git a/src/verifier/data/assurance.py b/src/verifier/data/assurance.py index 6f12a16..1120a70 100644 --- a/src/verifier/data/assurance.py +++ b/src/verifier/data/assurance.py @@ -18,6 +18,7 @@ from __future__ import annotations import json +import re from dataclasses import dataclass, field, replace from enum import Enum from typing import Any, Iterable, Mapping, Optional @@ -56,6 +57,9 @@ class AssuranceEventKind(str, Enum): CONFLICT_DECLARATION = "CONFLICT_DECLARATION" CONFLICT_RESOLUTION = "CONFLICT_RESOLUTION" CAUSAL_LOCALIZATION = "CAUSAL_LOCALIZATION" + RESPONSIBILITY_COMPONENT = "RESPONSIBILITY_COMPONENT" + OBLIGATION_APPLICABILITY = "OBLIGATION_APPLICABILITY" + OBLIGATION_VIOLATION = "OBLIGATION_VIOLATION" DIAGNOSTIC_ATTRIBUTION = "DIAGNOSTIC_ATTRIBUTION" @@ -64,6 +68,78 @@ class DiagnosticKind(str, Enum): GUILT = "GUILT" +_DIGEST_REF = re.compile(r"^sha256:[0-9a-f]{64}$") + + +@dataclass(frozen=True) +class ObligationCoordinate: + """Exact technical obligation and the declared scope in which it applies. + + An identifier or content digest names the obligation. ``scope`` carries + every material time, realm, jurisdiction, contract, policy, or version + coordinate. Assumptions and exclusions remain part of the exact coordinate; + the evaluating proposition separately binds its evidence, mechanism, trust + roots, and resource bounds. + """ + + obligation_id: str = "" + content_digest: str = "" + scope: Mapping[str, str] = field(default_factory=dict) + assumptions: tuple[str, ...] = () + exclusions: tuple[str, ...] = () + + def __post_init__(self) -> None: + if not self.obligation_id and not self.content_digest: + raise AssuranceFlowError( + "an obligation coordinate requires an identifier or content digest" + ) + if self.content_digest and not _DIGEST_REF.fullmatch(self.content_digest): + raise AssuranceFlowError( + "obligation content_digest must be a sha256 content reference" + ) + if not self.scope: + raise AssuranceFlowError("an obligation coordinate requires declared scope") + normalized_scope: dict[str, str] = {} + for key, value in self.scope.items(): + if not isinstance(key, str) or not key or not isinstance(value, str) or not value: + raise AssuranceFlowError( + "obligation scope keys and values must be nonempty strings" + ) + normalized_scope[key] = value + if any(not isinstance(item, str) or not item for item in self.assumptions): + raise AssuranceFlowError("obligation assumptions must be nonempty strings") + if any(not isinstance(item, str) or not item for item in self.exclusions): + raise AssuranceFlowError("obligation exclusions must be nonempty strings") + object.__setattr__(self, "scope", dict(sorted(normalized_scope.items()))) + object.__setattr__(self, "assumptions", tuple(sorted(set(self.assumptions)))) + object.__setattr__(self, "exclusions", tuple(sorted(set(self.exclusions)))) + + def to_dict(self) -> dict[str, Any]: + return { + "obligation_id": self.obligation_id, + "content_digest": self.content_digest, + "scope": dict(self.scope), + "assumptions": list(self.assumptions), + "exclusions": list(self.exclusions), + } + + def digest(self) -> str: + return canonical_digest(self.to_dict()) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "ObligationCoordinate": + scope = data.get("scope") + if not isinstance(scope, Mapping): + raise AssuranceFlowError("obligation scope is not an object") + return cls( + obligation_id=str(data.get("obligation_id", "")), + content_digest=str(data.get("content_digest", "")), + scope={str(key): str(value) for key, value in scope.items()}, + assumptions=tuple(str(item) for item in data.get("assumptions", ())), + exclusions=tuple(str(item) for item in data.get("exclusions", ())), + ) + + class ChallengeProjectionMechanism: """Recompute one artifact's status from embedded challenge-ledger records.""" @@ -269,9 +345,13 @@ class DiagnosticAttribution: localization_event_digest: str evaluation: Optional[EvaluatedProposition] details: str + obligation_coordinate: Optional[ObligationCoordinate] = None + responsibility_component_digest: str = "" + applicability_component_digest: str = "" + violation_component_digest: str = "" def to_dict(self) -> dict[str, Any]: - return { + result = { "kind": self.kind.value, "ancestor_id": self.ancestor_id, "descendant_id": self.descendant_id, @@ -280,6 +360,15 @@ def to_dict(self) -> dict[str, Any]: "evaluation": None if self.evaluation is None else self.evaluation.to_dict(), "details": self.details, } + if self.obligation_coordinate is not None: + result["obligation_coordinate"] = self.obligation_coordinate.to_dict() + if self.responsibility_component_digest: + result["responsibility_component_digest"] = self.responsibility_component_digest + if self.applicability_component_digest: + result["applicability_component_digest"] = self.applicability_component_digest + if self.violation_component_digest: + result["violation_component_digest"] = self.violation_component_digest + return result class AssuranceLedger: @@ -1023,6 +1112,529 @@ def localize_cause( }, ) + def _event_by_digest(self, event_digest: str) -> Optional[AssuranceEvent]: + return next( + (event for event in self._events if event.digest() == event_digest), + None, + ) + + def _require_localization( + self, + ancestor_id: str, + descendant_id: str, + localization_event_digest: str, + ) -> AssuranceEvent: + localization = self._event_by_digest(localization_event_digest) + if ( + localization is None + or localization.kind is not AssuranceEventKind.CAUSAL_LOCALIZATION + or localization.outcome is not MechanismOutcome.PASS + or localization.subject_id != descendant_id + or localization.source_ids != (ancestor_id,) + ): + raise AssuranceFlowError( + "component requires the exact passing localization event for this " + "ancestor and descendant" + ) + return localization + + @staticmethod + def _unestablished_guilt( + ancestor_id: str, + descendant_id: str, + localization_event_digest: str, + details: str, + *, + obligation: Optional[ObligationCoordinate] = None, + responsibility_component_digest: str = "", + applicability_component_digest: str = "", + violation_component_digest: str = "", + ) -> DiagnosticAttribution: + return DiagnosticAttribution( + DiagnosticKind.GUILT, + ancestor_id, + descendant_id, + "NOT_ESTABLISHED", + localization_event_digest, + None, + details, + obligation, + responsibility_component_digest, + applicability_component_digest, + violation_component_digest, + ) + + def establish_responsibility( + self, + ancestor_id: str, + descendant_id: str, + proposition: BoundProposition, + *, + localization_event_digest: str, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + """Evaluate one separately bound material-contribution component.""" + + localization = self._require_localization( + ancestor_id, descendant_id, localization_event_digest + ) + expected = { + "ancestor_id": ancestor_id, + "descendant_id": descendant_id, + "localization_event_digest": localization_event_digest, + "rust_event_digest": str(localization.attributes["rust_event_digest"]), + "deviation_binding_digest": str( + localization.attributes["deviation_binding_digest"] + ), + } + if ( + proposition.subject_id != ancestor_id + or proposition.predicate != "vstd.graph.responsibility" + or proposition.expected != expected + ): + raise AssuranceFlowError( + "responsibility component is not exactly artifact/deviation-bound" + ) + evaluation = session.evaluate(proposition) + return self._append( + kind=AssuranceEventKind.RESPONSIBILITY_COMPONENT, + subject_id=ancestor_id, + source_ids=(descendant_id,), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes=expected, + ) + + def establish_obligation_applicability( + self, + artifact_id: str, + obligation: ObligationCoordinate, + proposition: BoundProposition, + *, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + """Evaluate whether one exact obligation applies to one exact artifact.""" + + if artifact_id not in self.graph.artifacts: + raise AssuranceFlowError(f"unknown obligation subject {artifact_id}") + expected = { + "artifact_id": artifact_id, + "obligation_coordinate": obligation.to_dict(), + } + if ( + proposition.subject_id != artifact_id + or proposition.predicate != "vstd.graph.obligation_applicability" + or proposition.expected != expected + ): + raise AssuranceFlowError( + "obligation applicability is not exactly artifact/scope-bound" + ) + evaluation = session.evaluate(proposition) + return self._append( + kind=AssuranceEventKind.OBLIGATION_APPLICABILITY, + subject_id=artifact_id, + source_ids=(), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes=expected, + ) + + def establish_obligation_violation( + self, + ancestor_id: str, + descendant_id: str, + obligation: ObligationCoordinate, + proposition: BoundProposition, + *, + localization_event_digest: str, + applicability_component_digest: str, + session: VerificationSession, + recorded_at: str, + ) -> AssuranceEvent: + """Evaluate violation after exact applicability and deviation localization.""" + + localization = self._require_localization( + ancestor_id, descendant_id, localization_event_digest + ) + applicability = self._event_by_digest(applicability_component_digest) + if ( + applicability is None + or applicability.kind is not AssuranceEventKind.OBLIGATION_APPLICABILITY + or applicability.outcome is not MechanismOutcome.PASS + or applicability.subject_id != ancestor_id + or applicability.source_ids + or applicability.attributes.get("obligation_coordinate") + != obligation.to_dict() + ): + raise AssuranceFlowError( + "violation requires the exact passing applicability component" + ) + expected = { + "artifact_id": ancestor_id, + "descendant_id": descendant_id, + "localization_event_digest": localization_event_digest, + "rust_event_digest": str(localization.attributes["rust_event_digest"]), + "deviation_binding_digest": str( + localization.attributes["deviation_binding_digest"] + ), + "obligation_coordinate": obligation.to_dict(), + "applicability_binding_digest": str( + applicability.attributes["binding_digest"] + ), + } + if ( + proposition.subject_id != ancestor_id + or proposition.predicate != "vstd.graph.obligation_violation" + or proposition.expected != expected + ): + raise AssuranceFlowError( + "obligation violation is not exactly obligation/deviation-bound" + ) + evaluation = session.evaluate(proposition) + return self._append( + kind=AssuranceEventKind.OBLIGATION_VIOLATION, + subject_id=ancestor_id, + source_ids=(descendant_id,), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={ + **expected, + "applicability_component_digest": applicability_component_digest, + }, + ) + + def establish_guilt_components( + self, + ancestor_id: str, + descendant_id: str, + obligation: ObligationCoordinate, + responsibility_proposition: BoundProposition, + applicability_proposition: BoundProposition, + violation_proposition: BoundProposition, + *, + localization_event_digest: str, + session: VerificationSession, + recorded_at: str, + ) -> tuple[AssuranceEvent, AssuranceEvent, AssuranceEvent]: + """Run one compound mechanism and retain three separately bound results.""" + + localization = self._require_localization( + ancestor_id, descendant_id, localization_event_digest + ) + responsibility_expected = { + "ancestor_id": ancestor_id, + "descendant_id": descendant_id, + "localization_event_digest": localization_event_digest, + "rust_event_digest": str(localization.attributes["rust_event_digest"]), + "deviation_binding_digest": str( + localization.attributes["deviation_binding_digest"] + ), + } + applicability_expected = { + "artifact_id": ancestor_id, + "obligation_coordinate": obligation.to_dict(), + } + violation_expected = { + "artifact_id": ancestor_id, + "descendant_id": descendant_id, + "localization_event_digest": localization_event_digest, + "rust_event_digest": str(localization.attributes["rust_event_digest"]), + "deviation_binding_digest": str( + localization.attributes["deviation_binding_digest"] + ), + "obligation_coordinate": obligation.to_dict(), + "applicability_binding_digest": applicability_proposition.digest(), + } + required = ( + ( + responsibility_proposition, + "vstd.graph.responsibility", + responsibility_expected, + ), + ( + applicability_proposition, + "vstd.graph.obligation_applicability", + applicability_expected, + ), + ( + violation_proposition, + "vstd.graph.obligation_violation", + violation_expected, + ), + ) + if any( + proposition.subject_id != ancestor_id + or proposition.predicate != predicate + or proposition.expected != expected + for proposition, predicate, expected in required + ): + raise AssuranceFlowError( + "compound GUILT components are not separately and exactly bound" + ) + group_payload = { + "component_bindings": [ + responsibility_proposition.to_dict(), + applicability_proposition.to_dict(), + violation_proposition.to_dict(), + ], + "component_roles": [ + "RESPONSIBILITY", + "OBLIGATION_APPLICABILITY", + "OBLIGATION_VIOLATION", + ], + } + compound_group_digest = canonical_digest(group_payload) + evaluations = session.evaluate_compound( + ( + responsibility_proposition, + applicability_proposition, + violation_proposition, + ) + ) + responsibility = self._append( + kind=AssuranceEventKind.RESPONSIBILITY_COMPONENT, + subject_id=ancestor_id, + source_ids=(descendant_id,), + proposition=responsibility_proposition.predicate, + binding=responsibility_proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluations[0], + evidence_payloads=session.evidence.export_base64( + evaluations[0].evidence_refs + ), + attributes={ + **responsibility_expected, + "compound_group_digest": compound_group_digest, + "compound_component_index": 0, + }, + ) + applicability = self._append( + kind=AssuranceEventKind.OBLIGATION_APPLICABILITY, + subject_id=ancestor_id, + source_ids=(), + proposition=applicability_proposition.predicate, + binding=applicability_proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluations[1], + evidence_payloads=session.evidence.export_base64( + evaluations[1].evidence_refs + ), + attributes={ + **applicability_expected, + "compound_group_digest": compound_group_digest, + "compound_component_index": 1, + }, + ) + violation = self._append( + kind=AssuranceEventKind.OBLIGATION_VIOLATION, + subject_id=ancestor_id, + source_ids=(descendant_id,), + proposition=violation_proposition.predicate, + binding=violation_proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluations[2], + evidence_payloads=session.evidence.export_base64( + evaluations[2].evidence_refs + ), + attributes={ + **violation_expected, + "applicability_component_digest": applicability.digest(), + "compound_group_digest": compound_group_digest, + "compound_component_index": 2, + }, + ) + return responsibility, applicability, violation + + def compose_guilt( + self, + ancestor_id: str, + descendant_id: str, + obligation: ObligationCoordinate, + proposition: BoundProposition, + *, + localization_event_digest: str, + responsibility_component_digest: str, + applicability_component_digest: str, + violation_component_digest: str, + session: VerificationSession, + recorded_at: str, + ) -> DiagnosticAttribution: + """Compose technical GUILT from three exact, separately earned components. + + GUILT is artifact-relative. It does not establish moral character, actor + reputation, automatic legal liability, innocence, exoneration, obligation + satisfaction, or absence of hidden contributors. + """ + + component_digests = ( + responsibility_component_digest, + applicability_component_digest, + violation_component_digest, + ) + if any(not item for item in component_digests) or len(set(component_digests)) != 3: + return self._unestablished_guilt( + ancestor_id, + descendant_id, + localization_event_digest, + "GUILT requires three distinct component event digests", + obligation=obligation, + responsibility_component_digest=responsibility_component_digest, + applicability_component_digest=applicability_component_digest, + violation_component_digest=violation_component_digest, + ) + try: + self._require_localization( + ancestor_id, descendant_id, localization_event_digest + ) + except AssuranceFlowError as exc: + return self._unestablished_guilt( + ancestor_id, + descendant_id, + localization_event_digest, + str(exc), + obligation=obligation, + responsibility_component_digest=responsibility_component_digest, + applicability_component_digest=applicability_component_digest, + violation_component_digest=violation_component_digest, + ) + + responsibility = self._event_by_digest(responsibility_component_digest) + responsibility_matches = False + if responsibility is not None and responsibility.outcome is MechanismOutcome.PASS: + responsibility_matches = ( + responsibility.subject_id == ancestor_id + and responsibility.source_ids == (descendant_id,) + and responsibility.attributes.get("localization_event_digest") + == localization_event_digest + and ( + responsibility.kind is AssuranceEventKind.RESPONSIBILITY_COMPONENT + or ( + responsibility.kind + is AssuranceEventKind.DIAGNOSTIC_ATTRIBUTION + and responsibility.attributes.get("diagnostic_kind") + == DiagnosticKind.BLAME.value + ) + ) + ) + if not responsibility_matches: + return self._unestablished_guilt( + ancestor_id, + descendant_id, + localization_event_digest, + "responsibility component is missing, non-passing, or coordinate-mismatched", + obligation=obligation, + responsibility_component_digest=responsibility_component_digest, + applicability_component_digest=applicability_component_digest, + violation_component_digest=violation_component_digest, + ) + + applicability = self._event_by_digest(applicability_component_digest) + if not ( + applicability is not None + and applicability.kind is AssuranceEventKind.OBLIGATION_APPLICABILITY + and applicability.outcome is MechanismOutcome.PASS + and applicability.subject_id == ancestor_id + and not applicability.source_ids + and applicability.attributes.get("obligation_coordinate") + == obligation.to_dict() + ): + return self._unestablished_guilt( + ancestor_id, + descendant_id, + localization_event_digest, + "applicability component is missing, non-passing, or coordinate-mismatched", + obligation=obligation, + responsibility_component_digest=responsibility_component_digest, + applicability_component_digest=applicability_component_digest, + violation_component_digest=violation_component_digest, + ) + + violation = self._event_by_digest(violation_component_digest) + if not ( + violation is not None + and violation.kind is AssuranceEventKind.OBLIGATION_VIOLATION + and violation.outcome is MechanismOutcome.PASS + and violation.subject_id == ancestor_id + and violation.source_ids == (descendant_id,) + and violation.attributes.get("localization_event_digest") + == localization_event_digest + and violation.attributes.get("obligation_coordinate") + == obligation.to_dict() + and violation.attributes.get("applicability_component_digest") + == applicability_component_digest + ): + return self._unestablished_guilt( + ancestor_id, + descendant_id, + localization_event_digest, + "violation component is missing, non-passing, or coordinate-mismatched", + obligation=obligation, + responsibility_component_digest=responsibility_component_digest, + applicability_component_digest=applicability_component_digest, + violation_component_digest=violation_component_digest, + ) + + expected = { + "ancestor_id": ancestor_id, + "descendant_id": descendant_id, + "localization_event_digest": localization_event_digest, + "obligation_coordinate": obligation.to_dict(), + "responsibility_component_digest": responsibility_component_digest, + "applicability_component_digest": applicability_component_digest, + "violation_component_digest": violation_component_digest, + } + if ( + proposition.subject_id != ancestor_id + or proposition.predicate != "vstd.graph.diagnostic.guilt" + or proposition.expected != expected + ): + raise AssuranceFlowError( + "GUILT proposition is not exactly component-composition-bound" + ) + evaluation = session.evaluate(proposition) + event = self._append( + kind=AssuranceEventKind.DIAGNOSTIC_ATTRIBUTION, + subject_id=ancestor_id, + source_ids=(descendant_id,), + proposition=proposition.predicate, + binding=proposition.to_dict(), + recorded_at=recorded_at, + evaluation=evaluation, + evidence_payloads=session.evidence.export_base64(evaluation.evidence_refs), + attributes={ + "diagnostic_kind": DiagnosticKind.GUILT.value, + "localization_event_digest": localization_event_digest, + "obligation_coordinate": obligation.to_dict(), + "responsibility_component_digest": responsibility_component_digest, + "applicability_component_digest": applicability_component_digest, + "violation_component_digest": violation_component_digest, + }, + ) + return DiagnosticAttribution( + DiagnosticKind.GUILT, + ancestor_id, + descendant_id, + "ESTABLISHED" if evaluation.passed else "NOT_ESTABLISHED", + localization_event_digest, + evaluation, + event.details, + obligation, + responsibility_component_digest, + applicability_component_digest, + violation_component_digest, + ) + def diagnose( self, kind: DiagnosticKind, @@ -1033,13 +1645,12 @@ def diagnose( session: VerificationSession, recorded_at: str, ) -> DiagnosticAttribution: - """Compute bounded artifact-relative BLAME or GUILT just in time. + """Compute BLAME, or fail closed for legacy opaque GUILT calls. BLAME means only that the named artifact-relative responsibility - proposition passed. GUILT is not its directional opposite: it checks - that same localized responsibility together with an exact violated - obligation. Neither result concerns an actor's moral character, - reputation, or general trust. + proposition passed. GUILT must use :meth:`compose_guilt`; one opaque + proposition cannot substitute for separately bound responsibility, + applicability, and violation evaluations. """ requested_localization_digest = "" if proposition is not None and isinstance(proposition.expected, Mapping): @@ -1081,16 +1692,19 @@ def diagnose( None, "diagnostic attribution requires a separate exact mechanism", ) - obligation = proposition.parameters.get("obligation", "") - if kind is DiagnosticKind.GUILT and not obligation: - raise AssuranceFlowError("GUILT requires an exact violated obligation") + if kind is DiagnosticKind.GUILT: + return self._unestablished_guilt( + ancestor_id, + descendant_id, + localization.digest(), + "opaque GUILT evaluation is insufficient; separately establish " + "responsibility, obligation applicability, and obligation violation", + ) expected = { "ancestor": ancestor_id, "descendant": descendant_id, "localization_event_digest": localization.digest(), } - if kind is DiagnosticKind.GUILT: - expected["violated_obligation"] = obligation if ( proposition.subject_id != ancestor_id or proposition.predicate != f"vstd.graph.diagnostic.{kind.value.lower()}" @@ -1110,7 +1724,6 @@ def diagnose( attributes={ "diagnostic_kind": kind.value, "localization_event_digest": localization.digest(), - "obligation": obligation, }, ) return DiagnosticAttribution( @@ -1178,7 +1791,11 @@ def recheck_assurance_log( ) session.register(mechanism) - for expected in events_data: + replayed_compound_positions: set[int] = set() + for event_position, expected in enumerate(events_data): + if event_position in replayed_compound_positions: + continue + compound_replayed = False try: kind = AssuranceEventKind(str(expected["kind"])) subject_id = str(expected["subject_id"]) @@ -1267,22 +1884,196 @@ def recheck_assurance_log( session=session, recorded_at=recorded_at, ) - else: + elif kind is AssuranceEventKind.RESPONSIBILITY_COMPONENT: if len(source_ids) != 1: raise AssuranceFlowError( - "diagnostic attribution must name exactly one descendant" + "responsibility component must name exactly one descendant" ) - ledger.diagnose( - DiagnosticKind(str(attributes["diagnostic_kind"])), + if "compound_group_digest" in attributes: + if event_position + 2 >= len(events_data): + raise AssuranceFlowError( + "compound component group is incomplete" + ) + applicability_expected = events_data[event_position + 1] + violation_expected = events_data[event_position + 2] + if not isinstance(applicability_expected, Mapping) or not isinstance( + violation_expected, Mapping + ): + raise TypeError("compound component event is not an object") + applicability_attributes = applicability_expected.get( + "attributes", {} + ) + violation_attributes = violation_expected.get("attributes", {}) + if not isinstance(applicability_attributes, Mapping) or not isinstance( + violation_attributes, Mapping + ): + raise TypeError("compound component attributes are not objects") + compound_group_digest = str(attributes["compound_group_digest"]) + if ( + int(attributes.get("compound_component_index", -1)) != 0 + or applicability_expected.get("kind") + != AssuranceEventKind.OBLIGATION_APPLICABILITY.value + or violation_expected.get("kind") + != AssuranceEventKind.OBLIGATION_VIOLATION.value + or applicability_attributes.get("compound_group_digest") + != compound_group_digest + or violation_attributes.get("compound_group_digest") + != compound_group_digest + or int( + applicability_attributes.get( + "compound_component_index", -1 + ) + ) + != 1 + or int( + violation_attributes.get("compound_component_index", -1) + ) + != 2 + or applicability_expected.get("recorded_at") != recorded_at + or violation_expected.get("recorded_at") != recorded_at + ): + raise AssuranceFlowError( + "compound component group order or coordinate is invalid" + ) + applicability_binding = applicability_expected.get("binding") + violation_binding = violation_expected.get("binding") + obligation_data = applicability_attributes.get( + "obligation_coordinate" + ) + if ( + not isinstance(applicability_binding, Mapping) + or not isinstance(violation_binding, Mapping) + or not isinstance(obligation_data, Mapping) + ): + raise TypeError("compound component binding is not an object") + compound_events = ledger.establish_guilt_components( + subject_id, + source_ids[0], + ObligationCoordinate.from_dict(obligation_data), + proposition, + BoundProposition.from_dict(applicability_binding), + BoundProposition.from_dict(violation_binding), + localization_event_digest=str( + attributes["localization_event_digest"] + ), + session=session, + recorded_at=recorded_at, + ) + expected_group = ( + expected, + applicability_expected, + violation_expected, + ) + for recomputed, recorded in zip( + compound_events, expected_group + ): + if recomputed.to_dict() != dict(recorded): + raise AssuranceFlowError( + "recomputed compound component event does not match the log" + ) + replayed_compound_positions.update( + (event_position + 1, event_position + 2) + ) + event = compound_events[0] + compound_replayed = True + else: + event = ledger.establish_responsibility( + subject_id, + source_ids[0], + proposition, + localization_event_digest=str( + attributes["localization_event_digest"] + ), + session=session, + recorded_at=recorded_at, + ) + elif kind is AssuranceEventKind.OBLIGATION_APPLICABILITY: + if source_ids: + raise AssuranceFlowError( + "obligation applicability cannot name a neighboring source" + ) + obligation_data = attributes["obligation_coordinate"] + if not isinstance(obligation_data, Mapping): + raise TypeError("obligation coordinate is not an object") + event = ledger.establish_obligation_applicability( + subject_id, + ObligationCoordinate.from_dict(obligation_data), + proposition, + session=session, + recorded_at=recorded_at, + ) + elif kind is AssuranceEventKind.OBLIGATION_VIOLATION: + if len(source_ids) != 1: + raise AssuranceFlowError( + "obligation violation must name exactly one descendant" + ) + obligation_data = attributes["obligation_coordinate"] + if not isinstance(obligation_data, Mapping): + raise TypeError("obligation coordinate is not an object") + event = ledger.establish_obligation_violation( subject_id, source_ids[0], + ObligationCoordinate.from_dict(obligation_data), proposition, + localization_event_digest=str( + attributes["localization_event_digest"] + ), + applicability_component_digest=str( + attributes["applicability_component_digest"] + ), session=session, recorded_at=recorded_at, ) + elif kind is AssuranceEventKind.DIAGNOSTIC_ATTRIBUTION: + if len(source_ids) != 1: + raise AssuranceFlowError( + "diagnostic attribution must name exactly one descendant" + ) + diagnostic_kind = DiagnosticKind(str(attributes["diagnostic_kind"])) + if diagnostic_kind is DiagnosticKind.GUILT: + obligation_data = attributes["obligation_coordinate"] + if not isinstance(obligation_data, Mapping): + raise TypeError("obligation coordinate is not an object") + result = ledger.compose_guilt( + subject_id, + source_ids[0], + ObligationCoordinate.from_dict(obligation_data), + proposition, + localization_event_digest=str( + attributes["localization_event_digest"] + ), + responsibility_component_digest=str( + attributes["responsibility_component_digest"] + ), + applicability_component_digest=str( + attributes["applicability_component_digest"] + ), + violation_component_digest=str( + attributes["violation_component_digest"] + ), + session=session, + recorded_at=recorded_at, + ) + if result.evaluation is None: + raise AssuranceFlowError( + "recorded GUILT event cannot satisfy component composition" + ) + else: + ledger.diagnose( + diagnostic_kind, + subject_id, + source_ids[0], + proposition, + session=session, + recorded_at=recorded_at, + ) event = ledger.events()[-1] + else: # pragma: no cover - exhaustive enum guard + raise AssuranceFlowError(f"unsupported assurance event kind {kind.value}") except (KeyError, TypeError, ValueError) as exc: raise AssuranceFlowError(f"cannot replay assurance event: {exc}") from exc + if compound_replayed: + continue if event.to_dict() != dict(expected): raise AssuranceFlowError( f"recomputed assurance event {event.sequence} does not match the log" @@ -1304,6 +2095,7 @@ def recheck_assurance_log( "ConflictResolution", "DiagnosticAttribution", "DiagnosticKind", + "ObligationCoordinate", "StructuralConcentration", "recheck_assurance_log", ] diff --git a/src/verifier/specifications/LADDER.md b/src/verifier/specifications/LADDER.md index a87175d..202db3d 100644 --- a/src/verifier/specifications/LADDER.md +++ b/src/verifier/specifications/LADDER.md @@ -208,14 +208,22 @@ is among that event's recorded ancestors, and preserve those coordinates through **BLAME** and **GUILT** are bounded artifact-relative diagnostic results, not opposite directions on the Graph. BLAME requires a named mechanism to establish that an exact artifact bears responsibility for or materially contributed to an exact localized -deviation. GUILT is the stronger combined proposition: the same localized responsibility -plus an exact obligation that the artifact violated. Thus GUILT contains a responsibility -component, while BLAME alone establishes no obligation or violation. Neither term concerns -actor morality, character, identity, or reputation. Exoneration, obligation satisfaction, -or not-guilty conclusions require their own exact propositions and mechanisms; absent such -evidence the result remains `UNKNOWN`. BLAME and GUILT bind the causal-localization event -digest; that event transitively binds the selected RUST event and exact deviation, so neither -result can float across two deviations on the same descendant. +deviation. GUILT requires three separately bound passing components whose coordinates agree: +(1) responsibility or material contribution by exact artifact A for exact localized +deviation D; (2) applicability to A of exact obligation O under its declared scope, +assumptions, exclusions, roots, and bounds; and (3) violation by A of that same O relative to +that same D and applicable scope. The final GUILT proposition MUST bind the exact obligation +coordinate, causal-localization event digest, and all three component digests. A single +compound mechanism MAY check the components in one invocation only when it emits three +separately bound evaluations; one opaque combined result or nonempty obligation string is +insufficient. An existing passing BLAME event can supply the responsibility component only +when its exact event digest and all coordinates match. Neither term concerns actor morality, +character, identity, reputation, automatic legal liability, or social scoring. Exoneration, +innocence, obligation satisfaction, absence of hidden contributors, or not-guilty conclusions +require their own exact propositions and mechanisms; a missing component remains `UNKNOWN`, +not evidence of the opposite. The localization event transitively binds the selected RUST +event and exact deviation, so neither result can float across two deviations on the same +descendant. The word *causal* is required here for recorded developmental and provenance causality: the graph states which artifacts and transformations produced later claim architecture. @@ -230,12 +238,14 @@ the complete historical Graph, exact proposition bindings, and embedded evidence `AssuranceLedger` implements mechanism-earned forward TRUST edge by edge, typed ROT, challenge-ledger status projection, reverse RUST reachability, unique-descendant structural concentration, additive conflict declaration and resolution, explicit causal localization, -and bounded artifact-relative diagnostic attribution. Duplicate paths and repeated records -remain set-valued and earn no strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded -evidence, reruns every exact mechanism, reproduces the event hash chain, and compares the -derived current view. A deployment still supplies the proposition-specific mechanisms: the -event format and dispatcher do not create a universal support-transfer algebra or infer -causality from topology. +separately bound responsibility/applicability/violation components, and component-composed +artifact-relative GUILT. Duplicate paths and repeated records remain set-valued and earn no +strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded +evidence, reruns every exact component mechanism—including one-invocation compound groups as +such—reproduces the event hash chain, and compares the derived current view. A deployment +still supplies the proposition-specific mechanisms: the event format and dispatcher do not +establish real-world obligation applicability, legal culpability, a universal support-transfer +algebra, or causality from topology. Artifact freezing and sealing are bounded mechanisms under this orientation, specified separately in [`ARTIFACT_CONTROL.md`](ARTIFACT_CONTROL.md). A verified freeze preserves diff --git a/standard/LADDER.md b/standard/LADDER.md index a87175d..202db3d 100644 --- a/standard/LADDER.md +++ b/standard/LADDER.md @@ -208,14 +208,22 @@ is among that event's recorded ancestors, and preserve those coordinates through **BLAME** and **GUILT** are bounded artifact-relative diagnostic results, not opposite directions on the Graph. BLAME requires a named mechanism to establish that an exact artifact bears responsibility for or materially contributed to an exact localized -deviation. GUILT is the stronger combined proposition: the same localized responsibility -plus an exact obligation that the artifact violated. Thus GUILT contains a responsibility -component, while BLAME alone establishes no obligation or violation. Neither term concerns -actor morality, character, identity, or reputation. Exoneration, obligation satisfaction, -or not-guilty conclusions require their own exact propositions and mechanisms; absent such -evidence the result remains `UNKNOWN`. BLAME and GUILT bind the causal-localization event -digest; that event transitively binds the selected RUST event and exact deviation, so neither -result can float across two deviations on the same descendant. +deviation. GUILT requires three separately bound passing components whose coordinates agree: +(1) responsibility or material contribution by exact artifact A for exact localized +deviation D; (2) applicability to A of exact obligation O under its declared scope, +assumptions, exclusions, roots, and bounds; and (3) violation by A of that same O relative to +that same D and applicable scope. The final GUILT proposition MUST bind the exact obligation +coordinate, causal-localization event digest, and all three component digests. A single +compound mechanism MAY check the components in one invocation only when it emits three +separately bound evaluations; one opaque combined result or nonempty obligation string is +insufficient. An existing passing BLAME event can supply the responsibility component only +when its exact event digest and all coordinates match. Neither term concerns actor morality, +character, identity, reputation, automatic legal liability, or social scoring. Exoneration, +innocence, obligation satisfaction, absence of hidden contributors, or not-guilty conclusions +require their own exact propositions and mechanisms; a missing component remains `UNKNOWN`, +not evidence of the opposite. The localization event transitively binds the selected RUST +event and exact deviation, so neither result can float across two deviations on the same +descendant. The word *causal* is required here for recorded developmental and provenance causality: the graph states which artifacts and transformations produced later claim architecture. @@ -230,12 +238,14 @@ the complete historical Graph, exact proposition bindings, and embedded evidence `AssuranceLedger` implements mechanism-earned forward TRUST edge by edge, typed ROT, challenge-ledger status projection, reverse RUST reachability, unique-descendant structural concentration, additive conflict declaration and resolution, explicit causal localization, -and bounded artifact-relative diagnostic attribution. Duplicate paths and repeated records -remain set-valued and earn no strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded -evidence, reruns every exact mechanism, reproduces the event hash chain, and compares the -derived current view. A deployment still supplies the proposition-specific mechanisms: the -event format and dispatcher do not create a universal support-transfer algebra or infer -causality from topology. +separately bound responsibility/applicability/violation components, and component-composed +artifact-relative GUILT. Duplicate paths and repeated records remain set-valued and earn no +strength. `recheck_assurance_log` reconstructs the historical Graph, rehashes the embedded +evidence, reruns every exact component mechanism—including one-invocation compound groups as +such—reproduces the event hash chain, and compares the derived current view. A deployment +still supplies the proposition-specific mechanisms: the event format and dispatcher do not +establish real-world obligation applicability, legal culpability, a universal support-transfer +algebra, or causality from topology. Artifact freezing and sealing are bounded mechanisms under this orientation, specified separately in [`ARTIFACT_CONTROL.md`](ARTIFACT_CONTROL.md). A verified freeze preserves diff --git a/standard/schemas/vstd-graph-assurance-1.schema.json b/standard/schemas/vstd-graph-assurance-1.schema.json index 08dbea3..ab17cce 100644 --- a/standard/schemas/vstd-graph-assurance-1.schema.json +++ b/standard/schemas/vstd-graph-assurance-1.schema.json @@ -46,7 +46,7 @@ "required": ["sequence", "kind", "subject_id", "source_ids", "proposition", "binding", "recorded_at", "outcome", "mechanism_id", "mechanism_digest", "evidence_refs", "evidence_payloads", "trust_roots", "details", "previous_event_digest", "attributes", "event_digest"], "properties": { "sequence": {"type": "integer", "minimum": 0}, - "kind": {"enum": ["TRUST", "ROT", "RUST", "STATUS_PROJECTION", "CONFLICT_DECLARATION", "CONFLICT_RESOLUTION", "CAUSAL_LOCALIZATION", "DIAGNOSTIC_ATTRIBUTION"]}, + "kind": {"enum": ["TRUST", "ROT", "RUST", "STATUS_PROJECTION", "CONFLICT_DECLARATION", "CONFLICT_RESOLUTION", "CAUSAL_LOCALIZATION", "RESPONSIBILITY_COMPONENT", "OBLIGATION_APPLICABILITY", "OBLIGATION_VIOLATION", "DIAGNOSTIC_ATTRIBUTION"]}, "subject_id": {"type": "string", "minLength": 1}, "source_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "proposition": {"type": "string", "minLength": 1}, @@ -83,6 +83,22 @@ { "if": {"properties": {"kind": {"const": "CAUSAL_LOCALIZATION"}}, "required": ["kind"]}, "then": {"properties": {"attributes": {"$ref": "#/$defs/localizationAttributes"}}} + }, + { + "if": {"properties": {"kind": {"const": "RESPONSIBILITY_COMPONENT"}}, "required": ["kind"]}, + "then": {"properties": {"attributes": {"$ref": "#/$defs/responsibilityAttributes"}}} + }, + { + "if": {"properties": {"kind": {"const": "OBLIGATION_APPLICABILITY"}}, "required": ["kind"]}, + "then": {"properties": {"attributes": {"$ref": "#/$defs/applicabilityAttributes"}}} + }, + { + "if": {"properties": {"kind": {"const": "OBLIGATION_VIOLATION"}}, "required": ["kind"]}, + "then": {"properties": {"attributes": {"$ref": "#/$defs/violationAttributes"}}} + }, + { + "if": {"properties": {"kind": {"const": "DIAGNOSTIC_ATTRIBUTION"}}, "required": ["kind"]}, + "then": {"properties": {"attributes": {"$ref": "#/$defs/diagnosticAttributes"}}} } ] }, @@ -140,6 +156,88 @@ "deviation_binding_digest": {"$ref": "#/$defs/digest"} } }, + "obligationCoordinate": { + "type": "object", + "additionalProperties": false, + "required": ["obligation_id", "content_digest", "scope", "assumptions", "exclusions"], + "properties": { + "obligation_id": {"type": "string"}, + "content_digest": {"type": "string", "pattern": "^(?:sha256:[0-9a-f]{64})?$"}, + "scope": {"type": "object", "minProperties": 1, "propertyNames": {"minLength": 1}, "additionalProperties": {"type": "string", "minLength": 1}}, + "assumptions": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, + "exclusions": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}} + }, + "anyOf": [ + {"properties": {"obligation_id": {"type": "string", "minLength": 1}}}, + {"properties": {"content_digest": {"$ref": "#/$defs/digestRef"}}} + ] + }, + "responsibilityAttributes": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "ancestor_id", "descendant_id", "localization_event_digest", "rust_event_digest", "deviation_binding_digest"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "ancestor_id": {"type": "string", "minLength": 1}, + "descendant_id": {"type": "string", "minLength": 1}, + "localization_event_digest": {"$ref": "#/$defs/digest"}, + "rust_event_digest": {"$ref": "#/$defs/digest"}, + "deviation_binding_digest": {"$ref": "#/$defs/digest"}, + "compound_group_digest": {"$ref": "#/$defs/digest"}, + "compound_component_index": {"const": 0} + }, + "dependentRequired": {"compound_group_digest": ["compound_component_index"], "compound_component_index": ["compound_group_digest"]} + }, + "applicabilityAttributes": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "artifact_id", "obligation_coordinate"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "artifact_id": {"type": "string", "minLength": 1}, + "obligation_coordinate": {"$ref": "#/$defs/obligationCoordinate"}, + "compound_group_digest": {"$ref": "#/$defs/digest"}, + "compound_component_index": {"const": 1} + }, + "dependentRequired": {"compound_group_digest": ["compound_component_index"], "compound_component_index": ["compound_group_digest"]} + }, + "violationAttributes": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "artifact_id", "descendant_id", "localization_event_digest", "rust_event_digest", "deviation_binding_digest", "obligation_coordinate", "applicability_binding_digest", "applicability_component_digest"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "artifact_id": {"type": "string", "minLength": 1}, + "descendant_id": {"type": "string", "minLength": 1}, + "localization_event_digest": {"$ref": "#/$defs/digest"}, + "rust_event_digest": {"$ref": "#/$defs/digest"}, + "deviation_binding_digest": {"$ref": "#/$defs/digest"}, + "obligation_coordinate": {"$ref": "#/$defs/obligationCoordinate"}, + "applicability_binding_digest": {"$ref": "#/$defs/digest"}, + "applicability_component_digest": {"$ref": "#/$defs/digest"}, + "compound_group_digest": {"$ref": "#/$defs/digest"}, + "compound_component_index": {"const": 2} + }, + "dependentRequired": {"compound_group_digest": ["compound_component_index"], "compound_component_index": ["compound_group_digest"]} + }, + "diagnosticAttributes": { + "type": "object", + "additionalProperties": false, + "required": ["binding_digest", "diagnostic_kind", "localization_event_digest"], + "properties": { + "binding_digest": {"$ref": "#/$defs/digest"}, + "diagnostic_kind": {"enum": ["BLAME", "GUILT"]}, + "localization_event_digest": {"$ref": "#/$defs/digest"}, + "obligation_coordinate": {"$ref": "#/$defs/obligationCoordinate"}, + "responsibility_component_digest": {"$ref": "#/$defs/digest"}, + "applicability_component_digest": {"$ref": "#/$defs/digest"}, + "violation_component_digest": {"$ref": "#/$defs/digest"} + }, + "allOf": [{ + "if": {"properties": {"diagnostic_kind": {"const": "GUILT"}}, "required": ["diagnostic_kind"]}, + "then": {"required": ["obligation_coordinate", "responsibility_component_digest", "applicability_component_digest", "violation_component_digest"]} + }] + }, "binding": { "type": "object", "additionalProperties": false, diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py index 9c80e8b..25066d0 100644 --- a/tests/test_evidence_bound_assurance.py +++ b/tests/test_evidence_bound_assurance.py @@ -1234,27 +1234,7 @@ def test_rust_requires_separate_localization_before_blame_or_guilt() -> None: assert result.status == "ESTABLISHED" assert "actor" not in result.details.lower() - guilt = _proposition( - store, - "source", - "vstd.graph.diagnostic.guilt", - { - "ancestor": "source", - "descendant": "result", - "localization_event_digest": event.digest(), - }, - ) - with pytest.raises(AssuranceFlowError, match="violated obligation"): - ledger.diagnose( - DiagnosticKind.GUILT, - "source", - "result", - guilt, - session=session, - recorded_at="2026-08-29T00:04:00Z", - ) - - established_guilt = _proposition( + opaque_guilt = _proposition( store, "source", "vstd.graph.diagnostic.guilt", @@ -1270,13 +1250,12 @@ def test_rust_requires_separate_localization_before_blame_or_guilt() -> None: DiagnosticKind.GUILT, "source", "result", - established_guilt, + opaque_guilt, session=session, - recorded_at="2026-08-29T00:05:00Z", + recorded_at="2026-08-29T00:04:00Z", ) - assert guilt_result.status == "ESTABLISHED" - assert guilt_result.evaluation is not None - assert guilt_result.evaluation.passed is True + assert guilt_result.status == "NOT_ESTABLISHED" + assert guilt_result.evaluation is None def test_localization_selects_one_exact_passing_deviation() -> None: diff --git a/tests/test_guilt_composition.py b/tests/test_guilt_composition.py new file mode 100644 index 0000000..3a5a060 --- /dev/null +++ b/tests/test_guilt_composition.py @@ -0,0 +1,568 @@ +"""Adversarial tests for component-earned artifact-relative GUILT.""" + +from __future__ import annotations + +import copy +from dataclasses import replace +import hashlib +import json +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator +from referencing import Registry, Resource + +from verifier.core.evidence import ( + BoundProposition, + EvidenceBindingError, + EvidenceBounds, + EvidenceStore, + MechanismDecision, + MechanismOutcome, + VerificationSession, +) +from verifier.data.assurance import ( + AssuranceFlowError, + AssuranceLedger, + DiagnosticKind, + ObligationCoordinate, + recheck_assurance_log, +) +from verifier.data.models import ( + ArtifactNode, + ArtifactStatus, + ArtifactType, + HyperedgePort, + ProvenanceHypergraph, + TransformationHyperedge, + TransformationType, +) + + +class ExactFactMechanism: + mechanism_id = "test.guilt-exact-fact" + mechanism_digest = "sha256:" + hashlib.sha256(b"guilt-exact-fact:v1").hexdigest() + + def evaluate(self, binding, evidence): + if len(evidence) != 1: + return MechanismDecision(MechanismOutcome.UNKNOWN, "one fact required") + try: + observed = json.loads(evidence[0]) + except (UnicodeDecodeError, json.JSONDecodeError): + return MechanismDecision(MechanismOutcome.FAIL, "invalid fact") + expected = { + "subject_id": binding.subject_id, + "predicate": binding.predicate, + "expected": binding.expected, + } + outcome = MechanismOutcome.PASS if observed == expected else MechanismOutcome.FAIL + return MechanismDecision(outcome, f"exact fact: {outcome.value}") + + +class CompoundExactFactMechanism(ExactFactMechanism): + mechanism_id = "test.guilt-compound-facts" + mechanism_digest = "sha256:" + hashlib.sha256(b"guilt-compound-facts:v1").hexdigest() + + def __init__(self): + self.compound_calls = 0 + self.ordinary_calls = 0 + + def evaluate(self, binding, evidence): + self.ordinary_calls += 1 + return MechanismDecision(MechanismOutcome.UNKNOWN, "compound entry point required") + + def evaluate_compound(self, bindings, evidence_sets): + self.compound_calls += 1 + return tuple( + super(CompoundExactFactMechanism, self).evaluate(binding, evidence) + for binding, evidence in zip(bindings, evidence_sets) + ) + + +class GuiltRig: + def __init__(self): + graph = ProvenanceHypergraph() + for artifact_id in ("source", "middle", "result"): + graph.add_artifact( + ArtifactNode( + artifact_id, + artifact_id, + ArtifactType.MODEL, + hashlib.sha256(artifact_id.encode()).hexdigest(), + status=ArtifactStatus.VALID, + ) + ) + for identity, source, target in ( + ("first", "source", "middle"), + ("second", "middle", "result"), + ): + graph.add_transformation( + TransformationHyperedge( + identity, + identity, + TransformationType.EVALUATION, + (HyperedgePort(source, "INPUT"),), + (HyperedgePort(target, "OUTPUT"),), + {}, + {}, + {}, + ) + ) + self.ledger = AssuranceLedger(graph) + self.store = EvidenceStore() + self.session = VerificationSession(self.store) + self.session.register(ExactFactMechanism()) + self.obligation = ObligationCoordinate( + obligation_id="obligation:integrity", + content_digest="sha256:" + "a" * 64, + scope={"policy": "fixture-v1", "realm": "test", "version": "1"}, + assumptions=("fixture policy governs source",), + exclusions=("no legal-liability conclusion",), + ) + self.rust, self.localization = self.localize("source", "D1", minute=0) + + def proposition( + self, + subject, + predicate, + expected, + *, + outcome=MechanismOutcome.PASS, + mechanism=ExactFactMechanism, + parameters=None, + ): + payload = json.dumps( + {"subject_id": subject, "predicate": predicate, "expected": expected}, + sort_keys=True, + separators=(",", ":"), + ).encode() + reference = self.store.add(payload) + proposition = BoundProposition( + subject, + predicate, + expected, + mechanism.mechanism_id, + mechanism.mechanism_digest, + (reference,), + ("test:guilt-policy",), + EvidenceBounds(1, 20_000), + parameters or {}, + ) + if outcome is MechanismOutcome.UNKNOWN: + return replace(proposition, bounds=EvidenceBounds(0, 20_000)) + if outcome is MechanismOutcome.FAIL: + wrong = self.store.add( + json.dumps( + {"subject_id": subject, "predicate": predicate, "expected": "neighbor"}, + sort_keys=True, + separators=(",", ":"), + ).encode() + ) + return replace(proposition, evidence_refs=(wrong,)) + return proposition + + def localize(self, ancestor, deviation_id, *, minute): + deviation = self.proposition( + "result", + "vstd.graph.descendant_deviation", + True, + parameters={"deviation_id": deviation_id}, + ) + rust = self.ledger.record_rust( + "result", + deviation, + session=self.session, + recorded_at=f"2026-08-29T10:{minute:02d}:00Z", + ) + expected = { + "ancestor": ancestor, + "descendant": "result", + "rust_event_digest": rust.digest(), + "deviation_binding_digest": deviation.digest(), + } + localization = self.ledger.localize_cause( + ancestor, + "result", + self.proposition("result", "vstd.graph.causal_localization", expected), + rust_event_digest=rust.digest(), + session=self.session, + recorded_at=f"2026-08-29T10:{minute + 1:02d}:00Z", + ) + return rust, localization + + def responsibility(self, *, ancestor="source", localization=None, outcome=MechanismOutcome.PASS): + localization = localization or self.localization + expected = { + "ancestor_id": ancestor, + "descendant_id": "result", + "localization_event_digest": localization.digest(), + "rust_event_digest": localization.attributes["rust_event_digest"], + "deviation_binding_digest": localization.attributes["deviation_binding_digest"], + } + return self.ledger.establish_responsibility( + ancestor, + "result", + self.proposition( + ancestor, "vstd.graph.responsibility", expected, outcome=outcome + ), + localization_event_digest=localization.digest(), + session=self.session, + recorded_at="2026-08-29T10:10:00Z", + ) + + def applicability(self, *, artifact="source", obligation=None, outcome=MechanismOutcome.PASS): + obligation = obligation or self.obligation + expected = { + "artifact_id": artifact, + "obligation_coordinate": obligation.to_dict(), + } + proposition = self.proposition( + artifact, + "vstd.graph.obligation_applicability", + expected, + outcome=outcome, + ) + event = self.ledger.establish_obligation_applicability( + artifact, + obligation, + proposition, + session=self.session, + recorded_at="2026-08-29T10:11:00Z", + ) + return proposition, event + + def violation( + self, + applicability, + *, + artifact="source", + obligation=None, + localization=None, + outcome=MechanismOutcome.PASS, + ): + obligation = obligation or self.obligation + localization = localization or self.localization + expected = { + "artifact_id": artifact, + "descendant_id": "result", + "localization_event_digest": localization.digest(), + "rust_event_digest": localization.attributes["rust_event_digest"], + "deviation_binding_digest": localization.attributes["deviation_binding_digest"], + "obligation_coordinate": obligation.to_dict(), + "applicability_binding_digest": applicability.attributes["binding_digest"], + } + return self.ledger.establish_obligation_violation( + artifact, + "result", + obligation, + self.proposition( + artifact, "vstd.graph.obligation_violation", expected, outcome=outcome + ), + localization_event_digest=localization.digest(), + applicability_component_digest=applicability.digest(), + session=self.session, + recorded_at="2026-08-29T10:12:00Z", + ) + + def compose(self, responsibility=None, applicability=None, violation=None, *, digests=None): + component_digests = digests or ( + responsibility.digest() if responsibility else "1" * 64, + applicability.digest() if applicability else "2" * 64, + violation.digest() if violation else "3" * 64, + ) + expected = { + "ancestor_id": "source", + "descendant_id": "result", + "localization_event_digest": self.localization.digest(), + "obligation_coordinate": self.obligation.to_dict(), + "responsibility_component_digest": component_digests[0], + "applicability_component_digest": component_digests[1], + "violation_component_digest": component_digests[2], + } + return self.ledger.compose_guilt( + "source", + "result", + self.obligation, + self.proposition("source", "vstd.graph.diagnostic.guilt", expected), + localization_event_digest=self.localization.digest(), + responsibility_component_digest=component_digests[0], + applicability_component_digest=component_digests[1], + violation_component_digest=component_digests[2], + session=self.session, + recorded_at="2026-08-29T10:13:00Z", + ) + + def passing_components(self): + responsibility = self.responsibility() + _, applicability = self.applicability() + violation = self.violation(applicability) + return responsibility, applicability, violation + + +def test_opaque_or_incomplete_components_never_establish_guilt(): + rig = GuiltRig() + decorative = rig.proposition( + "source", + "vstd.graph.diagnostic.guilt", + { + "ancestor": "source", + "descendant": "result", + "localization_event_digest": rig.localization.digest(), + "violated_obligation": "obligation:decorative", + }, + parameters={"obligation": "obligation:decorative"}, + ) + opaque = rig.ledger.diagnose( + DiagnosticKind.GUILT, + "source", + "result", + decorative, + session=rig.session, + recorded_at="2026-08-29T10:09:00Z", + ) + assert opaque.status == "NOT_ESTABLISHED" and opaque.evaluation is None + + responsibility = rig.responsibility() + assert rig.compose(responsibility).status == "NOT_ESTABLISHED" + _, applicability = rig.applicability() + assert rig.compose(responsibility, applicability).status == "NOT_ESTABLISHED" + assert not any(event.attributes.get("diagnostic_kind") == "GUILT" for event in rig.ledger.events()) + + +@pytest.mark.parametrize( + ("component", "outcome"), + [ + ("responsibility", MechanismOutcome.FAIL), + ("responsibility", MechanismOutcome.UNKNOWN), + ("applicability", MechanismOutcome.FAIL), + ("applicability", MechanismOutcome.UNKNOWN), + ("violation", MechanismOutcome.FAIL), + ("violation", MechanismOutcome.UNKNOWN), + ], +) +def test_fail_or_unknown_component_never_composes(component, outcome): + rig = GuiltRig() + responsibility = rig.responsibility( + outcome=outcome if component == "responsibility" else MechanismOutcome.PASS + ) + _, applicability = rig.applicability( + outcome=outcome if component == "applicability" else MechanismOutcome.PASS + ) + violation = None + if applicability.outcome is MechanismOutcome.PASS: + violation = rig.violation( + applicability, + outcome=outcome if component == "violation" else MechanismOutcome.PASS, + ) + result = rig.compose(responsibility, applicability, violation) + assert result.status == "NOT_ESTABLISHED" and result.evaluation is None + + +def test_neighboring_artifact_obligation_deviation_or_localization_cannot_compose(): + rig = GuiltRig() + responsibility, applicability, _ = rig.passing_components() + _, neighbor_app = rig.applicability(artifact="middle") + assert rig.compose(responsibility, neighbor_app).status == "NOT_ESTABLISHED" + + other = ObligationCoordinate( + obligation_id="obligation:neighbor", + scope={"policy": "neighbor", "realm": "test"}, + ) + _, other_app = rig.applicability(obligation=other) + assert rig.compose(responsibility, other_app).status == "NOT_ESTABLISHED" + + _, middle_localization = rig.localize("middle", "D-middle", minute=20) + _, middle_app = rig.applicability(artifact="middle") + middle_violation = rig.violation( + middle_app, artifact="middle", localization=middle_localization + ) + assert rig.compose(responsibility, applicability, middle_violation).status == "NOT_ESTABLISHED" + + _, second_localization = rig.localize("source", "D2", minute=30) + second_violation = rig.violation(applicability, localization=second_localization) + assert rig.compose(responsibility, applicability, second_violation).status == "NOT_ESTABLISHED" + + other_violation = rig.violation(other_app, obligation=other) + assert rig.compose(responsibility, applicability, other_violation).status == "NOT_ESTABLISHED" + + +def test_exact_components_and_existing_blame_can_each_supply_responsibility(): + rig = GuiltRig() + responsibility, applicability, violation = rig.passing_components() + direct = rig.compose(responsibility, applicability, violation) + assert direct.status == "ESTABLISHED" + + second = GuiltRig() + blame_expected = { + "ancestor": "source", + "descendant": "result", + "localization_event_digest": second.localization.digest(), + } + blame = second.ledger.diagnose( + DiagnosticKind.BLAME, + "source", + "result", + second.proposition("source", "vstd.graph.diagnostic.blame", blame_expected), + session=second.session, + recorded_at="2026-08-29T10:10:00Z", + ) + assert blame.status == "ESTABLISHED" + blame_event = second.ledger.events()[-1] + _, second_applicability = second.applicability() + second_violation = second.violation(second_applicability) + assert second.compose(blame_event, second_applicability, second_violation).status == "ESTABLISHED" + + +def test_one_compound_invocation_emits_three_bound_results_and_replays_once(): + rig = GuiltRig() + compound = CompoundExactFactMechanism() + rig.session.register(compound) + responsibility_expected = { + "ancestor_id": "source", + "descendant_id": "result", + "localization_event_digest": rig.localization.digest(), + "rust_event_digest": rig.localization.attributes["rust_event_digest"], + "deviation_binding_digest": rig.localization.attributes["deviation_binding_digest"], + } + applicability_expected = { + "artifact_id": "source", + "obligation_coordinate": rig.obligation.to_dict(), + } + responsibility_proposition = rig.proposition( + "source", "vstd.graph.responsibility", responsibility_expected, + mechanism=CompoundExactFactMechanism, + ) + applicability_proposition = rig.proposition( + "source", "vstd.graph.obligation_applicability", applicability_expected, + mechanism=CompoundExactFactMechanism, + ) + violation_expected = { + "artifact_id": "source", + "descendant_id": "result", + "localization_event_digest": rig.localization.digest(), + "rust_event_digest": rig.localization.attributes["rust_event_digest"], + "deviation_binding_digest": rig.localization.attributes["deviation_binding_digest"], + "obligation_coordinate": rig.obligation.to_dict(), + "applicability_binding_digest": applicability_proposition.digest(), + } + violation_proposition = rig.proposition( + "source", "vstd.graph.obligation_violation", violation_expected, + mechanism=CompoundExactFactMechanism, + ) + components = rig.ledger.establish_guilt_components( + "source", + "result", + rig.obligation, + responsibility_proposition, + applicability_proposition, + violation_proposition, + localization_event_digest=rig.localization.digest(), + session=rig.session, + recorded_at="2026-08-29T10:10:00Z", + ) + assert compound.compound_calls == 1 and compound.ordinary_calls == 0 + assert all(event.outcome is MechanismOutcome.PASS for event in components) + assert len({event.attributes["binding_digest"] for event in components}) == 3 + assert rig.compose(*components).status == "ESTABLISHED" + + replay_mechanism = CompoundExactFactMechanism() + payload = rig.ledger.to_dict() + replayed = recheck_assurance_log( + payload, mechanisms=(ExactFactMechanism(), replay_mechanism) + ) + assert replayed.to_dict() == payload + assert replay_mechanism.compound_calls == 1 and replay_mechanism.ordinary_calls == 0 + + +def _passing_payload(): + rig = GuiltRig() + components = rig.passing_components() + result = rig.compose(*components) + assert result.status == "ESTABLISHED" + return rig, components, result, rig.ledger.to_dict() + + +def test_schema_and_replay_accept_exact_component_log(): + _, _, _, payload = _passing_payload() + root = Path(__file__).resolve().parents[1] + schema = json.loads((root / "standard/schemas/vstd-graph-assurance-1.schema.json").read_text()) + graph_schema = json.loads((root / "receipts/schema/vstd_graph_receipt.json").read_text()) + registry = Registry().with_resource(graph_schema["$id"], Resource.from_contents(graph_schema)) + Draft202012Validator(schema, registry=registry).validate(payload) + assert recheck_assurance_log(payload, mechanisms=(ExactFactMechanism(),)).to_dict() == payload + + +def test_duplicate_component_references_do_not_manufacture_strength(): + rig = GuiltRig() + responsibility = rig.responsibility() + duplicate = responsibility.digest() + result = rig.compose(digests=(duplicate, duplicate, duplicate)) + assert result.status == "NOT_ESTABLISHED" and "distinct" in result.details + + +@pytest.mark.parametrize( + "field", + ["responsibility_component_digest", "applicability_component_digest", "violation_component_digest"], +) +def test_replay_refuses_changed_component_digest(field): + _, _, _, payload = _passing_payload() + guilt = next(event for event in payload["events"] if event["attributes"].get("diagnostic_kind") == "GUILT") + guilt["attributes"][field] = "f" * 64 + with pytest.raises(AssuranceFlowError): + recheck_assurance_log(payload, mechanisms=(ExactFactMechanism(),)) + + +@pytest.mark.parametrize( + ("kind", "path", "value"), + [ + ("OBLIGATION_APPLICABILITY", ("artifact_id",), "middle"), + ("OBLIGATION_APPLICABILITY", ("obligation_coordinate", "obligation_id"), "obligation:neighbor"), + ("OBLIGATION_APPLICABILITY", ("obligation_coordinate", "scope", "policy"), "neighbor"), + ("OBLIGATION_VIOLATION", ("descendant_id",), "middle"), + ("OBLIGATION_VIOLATION", ("localization_event_digest",), "e" * 64), + ], +) +def test_replay_refuses_changed_artifact_obligation_scope_deviation_or_localization(kind, path, value): + _, _, _, payload = _passing_payload() + event = next(item for item in payload["events"] if item["kind"] == kind) + target = event["attributes"] + for key in path[:-1]: + target = target[key] + target[path[-1]] = value + with pytest.raises(AssuranceFlowError): + recheck_assurance_log(payload, mechanisms=(ExactFactMechanism(),)) + + +def test_replay_refuses_changed_evidence_outcome_or_event_order(): + _, _, _, original = _passing_payload() + evidence = copy.deepcopy(original) + applicability = next(event for event in evidence["events"] if event["kind"] == "OBLIGATION_APPLICABILITY") + reference = next(iter(applicability["evidence_payloads"])) + applicability["evidence_payloads"][reference] = "bmVpZ2hib3I=" + with pytest.raises(EvidenceBindingError): + recheck_assurance_log(evidence, mechanisms=(ExactFactMechanism(),)) + + outcome = copy.deepcopy(original) + next(event for event in outcome["events"] if event["kind"] == "OBLIGATION_VIOLATION")["outcome"] = "FAIL" + with pytest.raises(AssuranceFlowError): + recheck_assurance_log(outcome, mechanisms=(ExactFactMechanism(),)) + + order = copy.deepcopy(original) + first = next(index for index, event in enumerate(order["events"]) if event["kind"] == "RESPONSIBILITY_COMPONENT") + second = next(index for index, event in enumerate(order["events"]) if event["kind"] == "OBLIGATION_APPLICABILITY") + order["events"][first], order["events"][second] = order["events"][second], order["events"][first] + with pytest.raises(AssuranceFlowError): + recheck_assurance_log(order, mechanisms=(ExactFactMechanism(),)) + + +def test_result_never_renders_innocence_morality_reputation_or_legal_liability(): + _, _, result, _ = _passing_payload() + rendered = json.dumps(result.to_dict(), sort_keys=True).lower() + for prohibited in ( + "innocence", + "moral culpability", + "actor reputation", + "general actor trust", + "legal liability", + ): + assert prohibited not in rendered diff --git a/tests/test_public_api.py b/tests/test_public_api.py index dd3b15f..37d58df 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -21,6 +21,7 @@ "EvidenceStore", "MechanismDecision", "MechanismOutcome", + "ObligationCoordinate", "ProvenanceHypergraph", "ReproducibilityLevel", "VerificationSession", From 11f243c84da292594b222f9ebcd544563273d1e4 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sun, 30 Aug 2026 11:57:45 -0700 Subject: [PATCH 24/34] docs(time): record thaw-lineage verification contradiction --- TIME.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/TIME.md b/TIME.md index 297804c..a1c49b3 100644 --- a/TIME.md +++ b/TIME.md @@ -1,6 +1,6 @@ # TIME -Status: CLEAR +Status: OPEN TIME is the live repository-contradiction annunciator. Its status is repository process metadata, not Verifier Standard (VSTD) receipt vocabulary. A live entry belongs here only @@ -13,7 +13,30 @@ escalation, see [`HUMANS.md`](HUMANS.md). ## Live contradictions -None. +### Thaw lineage can be manufactured from a self-consistent sidecar + +- **Implementation coordinate:** + `src/verifier/artifact_control/__init__.py::thawed_artifact_status` accepts a + `VSTD-ARTIFACT-THAW-1` sidecar without an actual parent bundle. It recomputes the + sidecar's self-derived `thaw_id` and compares the descendant only with the recorded + `parent_artifact_id`. +- **Governing Standard proposition:** `standard/ARTIFACT_CONTROL.md` states that thaw + requires a cleanly verified sealed parent, records that parent's exact artifact, + content, freeze, and seal identifiers, and uses `THAWED_CLEAN` for a descendant that + still matches the parent's initial artifact identity. +- **Public-reference proposition:** the generated `vstd artifact status` reference says + it compares a thawed descendant with its sealed parent identity. +- **Counterexample:** a completely fabricated sidecar with no parent bundle can return + `THAWED_CLEAN`. A legitimate sidecar can also have its `parent_content_id`, + `parent_freeze_id`, and `parent_seal_ids` replaced and its unkeyed `thaw_id` + recomputed while still returning `THAWED_CLEAN`. +- **Contradiction:** a self-derived `thaw_id` establishes only internal field agreement. + It cannot establish that an actual sealed parent exists, that its coordinates are + genuine, or that the historical copy operation occurred. +- **Required repair gate:** established `THAWED_CLEAN` status must require an actual + supplied parent bundle, clean seal verification, exact agreement with every recorded + parent coordinate, and descendant comparison using authoritative parent metadata. + Sidecar-only agreement must remain `NOT_ESTABLISHED` or fail closed. When a contradiction is open, change the status to `Status: OPEN` and record the exact coordinates, both incompatible claims, evidence for each side, and affected behavior. An From 3f98446fcf5a2cbd028aab7416e4778f1ed0b700 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sun, 30 Aug 2026 12:20:54 -0700 Subject: [PATCH 25/34] fix(artifact-control): verify thaw lineage against sealed parent --- .github/workflows/ci.yml | 5 + CHANGELOG.md | 5 + README.md | 9 +- TIME.md | 27 +- docs/API_STABILITY.md | 7 + docs/ARCHITECTURE.md | 6 +- docs/CLAIMS_AND_LIMITS.md | 3 +- docs/index.html | 1 + docs/reference.html | 9 +- src/verifier/artifact_control/__init__.py | 194 +++++++- .../artifact-control-1.schema.json | 3 +- src/verifier/runtime/public_cli.py | 26 +- .../specifications/ARTIFACT_CONTROL.md | 28 +- standard/ARTIFACT_CONTROL.md | 28 +- .../schemas/artifact-control-1.schema.json | 3 +- tests/test_artifact_control.py | 441 +++++++++++++++++- tests/test_public_api.py | 13 + 17 files changed, 735 insertions(+), 73 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddcb276..cb961e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,6 +157,11 @@ jobs: /tmp/vstd-wheel/bin/vstd reproduce /tmp/vstd-receipt --rerun /tmp/vstd-wheel/bin/verifier demo --scenario honest-unknown --json /tmp/vstd-wheel/bin/python -c 'import json; from pathlib import Path; from verifier.core.checker import IndependentAuditor; receipt=json.loads(Path("/tmp/vstd-receipt/receipt.json").read_text()); hashes=(receipt["assessment_context"]["verifier"]["specification_hash"], IndependentAuditor.verifier_descriptor().specification_hash); assert all(value.startswith("sha256:") for value in hashes), hashes' + printf 'installed-wheel-artifact\n' > /tmp/vstd-wheel-artifact.bin + /tmp/vstd-wheel/bin/vstd artifact freeze /tmp/vstd-wheel-artifact.bin /tmp/vstd-wheel-artifact --json + /tmp/vstd-wheel/bin/vstd artifact verify /tmp/vstd-wheel-artifact --freeze-only --json + /tmp/vstd-wheel/bin/python -c 'import inspect; from verifier import thawed_artifact_status; assert "parent_bundle" in inspect.signature(thawed_artifact_status).parameters' + /tmp/vstd-wheel/bin/vstd artifact status --help | grep -- --parent-bundle presentation: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 6756004..0389076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,11 @@ ### Claim boundaries and validation +- Require `thawed_artifact_status` and `vstd artifact status` to verify an actual supplied, + cleanly sealed parent and every recorded parent coordinate before returning + `THAWED_CLEAN` or `THAWED_DIRTY`. Sidecar-only agreement is now `NOT_ESTABLISHED`; even a + verified current match does not authenticate the historical copy operation or external + parent continuity. - Remove the live SimulacraBench rehearsal and its front-door promotion; the repository never contained or reproduced the submission, hosted image, hardware, or protected evaluation identified by that name. diff --git a/README.md b/README.md index f98f9a2..a8cb945 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ vstd artifact verify ARTIFACT.vstd --freeze-only vstd artifact seal ARTIFACT.vstd --private-key ed25519-private.pem vstd artifact verify ARTIFACT.vstd --expected-artifact-id EXPECTED_ID vstd artifact thaw ARTIFACT.vstd MUTABLE_COPY -vstd artifact status MUTABLE_COPY +vstd artifact status MUTABLE_COPY --parent-bundle ARTIFACT.vstd ``` The finite seal signs the complete envelope with its signature and identifier fields @@ -254,7 +254,12 @@ manifest/log coordinate is still required to detect whole-bundle substitution. A freeze or seal establishes bounded integrity and closure only—not correctness, freshness, ownership, authorization, trusted time, external preservation, or actor trust. Thaw is copy-on-write: it creates a mutable descendant and leaves the sealed parent -unchanged. See the normative +unchanged. Later `THAWED_CLEAN` or `THAWED_DIRTY` status requires that actual parent bundle, +clean seal verification, and exact agreement with every sidecar parent coordinate. Without +the parent, sidecar agreement remains `NOT_ESTABLISHED`. The sidecar's unkeyed hash does not +prove that the historical copy occurred. A supplied parent establishes only internal +consistency unless an expected artifact/key identifier or separately verified external log +also supplies continuity. See the normative [artifact-control mechanism](standard/ARTIFACT_CONTROL.md) and the architectural [realm/time-capsule model](docs/REALMS_AND_TIME_CAPSULES.md). diff --git a/TIME.md b/TIME.md index a1c49b3..297804c 100644 --- a/TIME.md +++ b/TIME.md @@ -1,6 +1,6 @@ # TIME -Status: OPEN +Status: CLEAR TIME is the live repository-contradiction annunciator. Its status is repository process metadata, not Verifier Standard (VSTD) receipt vocabulary. A live entry belongs here only @@ -13,30 +13,7 @@ escalation, see [`HUMANS.md`](HUMANS.md). ## Live contradictions -### Thaw lineage can be manufactured from a self-consistent sidecar - -- **Implementation coordinate:** - `src/verifier/artifact_control/__init__.py::thawed_artifact_status` accepts a - `VSTD-ARTIFACT-THAW-1` sidecar without an actual parent bundle. It recomputes the - sidecar's self-derived `thaw_id` and compares the descendant only with the recorded - `parent_artifact_id`. -- **Governing Standard proposition:** `standard/ARTIFACT_CONTROL.md` states that thaw - requires a cleanly verified sealed parent, records that parent's exact artifact, - content, freeze, and seal identifiers, and uses `THAWED_CLEAN` for a descendant that - still matches the parent's initial artifact identity. -- **Public-reference proposition:** the generated `vstd artifact status` reference says - it compares a thawed descendant with its sealed parent identity. -- **Counterexample:** a completely fabricated sidecar with no parent bundle can return - `THAWED_CLEAN`. A legitimate sidecar can also have its `parent_content_id`, - `parent_freeze_id`, and `parent_seal_ids` replaced and its unkeyed `thaw_id` - recomputed while still returning `THAWED_CLEAN`. -- **Contradiction:** a self-derived `thaw_id` establishes only internal field agreement. - It cannot establish that an actual sealed parent exists, that its coordinates are - genuine, or that the historical copy operation occurred. -- **Required repair gate:** established `THAWED_CLEAN` status must require an actual - supplied parent bundle, clean seal verification, exact agreement with every recorded - parent coordinate, and descendant comparison using authoritative parent metadata. - Sidecar-only agreement must remain `NOT_ESTABLISHED` or fail closed. +None. When a contradiction is open, change the status to `Status: OPEN` and record the exact coordinates, both incompatible claims, evidence for each side, and affected behavior. An diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md index 5942751..fb33e8b 100644 --- a/docs/API_STABILITY.md +++ b/docs/API_STABILITY.md @@ -22,6 +22,13 @@ The supported artifact-control exports are `freeze_artifact`, `seal_artifact`, require the optional `seal` dependency extra; importing the base package and freeze-only operations retain the zero-third-party-dependency boundary. +`thawed_artifact_status` treats a `VSTD-ARTIFACT-THAW-1` sidecar as unkeyed lineage +metadata. Without `parent_bundle`, it returns `NOT_ESTABLISHED` even when descendant bytes +agree with the sidecar's recorded identifier. `THAWED_CLEAN` or `THAWED_DIRTY` requires an +actual supplied parent that verifies as cleanly sealed and matches every recorded parent +coordinate. Optional expected artifact and key identifiers add external-anchor checks. +The result still does not authenticate the historical copy operation. + The supported evidence-bound construction exports are `BoundProposition`, `EvidenceBindingError`, `EvidenceBounds`, `EvidenceStore`, `MechanismDecision`, `MechanismOutcome`, `VerificationSession`, `WitnessBundle`, `ProvenanceHypergraph`, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7db082b..a4b7013 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -49,7 +49,11 @@ Artifact control is an orthogonal mechanism beneath the axes. It can preserve an an artifact used in any numbered profile, but its successful verification establishes only exact-byte integrity, structural closure, and any separately supplied external anchor. It cannot supply a numbered-profile result, semantic correctness, encryption, trusted time, actor trust, or -continuous temporal mediation. +continuous temporal mediation. A thaw sidecar is additive lineage metadata, not parent +authentication: sidecar-only agreement remains `NOT_ESTABLISHED`. Current clean or dirty +status requires an actual supplied, cleanly sealed parent whose artifact, content, freeze, +seal, kind, and media-type coordinates match. Even that comparison does not authenticate +the historical copy operation. ## Verification complex and profile satisfaction diff --git a/docs/CLAIMS_AND_LIMITS.md b/docs/CLAIMS_AND_LIMITS.md index 01af7b4..836ca94 100644 --- a/docs/CLAIMS_AND_LIMITS.md +++ b/docs/CLAIMS_AND_LIMITS.md @@ -40,7 +40,7 @@ checker result—not a claim of universal truth or whole-project conformance. | What can the grounded-certificate kernel establish? | The exact `VSTD4-GDC-1` decision was accepted, rejected, or left `UNKNOWN` under its claim binding and resource bound. | Certificate bytes, formula, grounding, policy/evidence roots, verifier descriptor, and kernel. | Kernel acceptance alone is not VSTD-4 conformance. The evidence-bound path separately reruns every prerequisite/rung mechanism before it may establish 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; a thawed descendant currently matches or differs from its sealed parent. | Preserved bytes, SHA-256 and SHA3-256 commitments, read-only payload-tree guard, Ed25519 signature, artifact-derived identifiers, and any supplied external artifact/key anchor. | Read-only is not privileged access control; a seal is not encryption, correctness, trusted time, ownership, durable external archiving, or a numbered-profile result. | +| 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, and any supplied external artifact/key anchor. | 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. | | 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. | @@ -55,6 +55,7 @@ required falsification surface. | “These observed bytes match this SHA-256 digest.†| **Yes, conditionally.** | A named mechanism can hash accessible bytes at an observation time and compare them with the recorded digest. | The bytes, hashing mechanism, observation time, expected digest, comparison result. | The bytes came from the claimed source, existed before observation, are uncontaminated, or are legally usable. | | “This artifact is frozen.†| **Yes, after freeze verification passes.** | The current regular-file bytes, portable paths, manifest identifiers, and read-only payload-tree guard recompute. | Complete bundle, passing `vstd artifact verify --freeze-only`, and the exact mechanism version. | Privileged mutation is impossible, an external archive retained the artifact, the artifact is correct, or a signature exists. | | “This artifact is sealed.†| **Yes, after seal verification passes.** | The carried Ed25519 key verifies the finite signature closure and the seal identifier closes the signature-bearing envelope. | Passing seal verification plus an expected artifact/key coordinate when whole-bundle substitution is in scope. | Encryption, secrecy, ownership, authorization, trusted time, semantic correctness, continuous custody, actor trust, or a numbered VSTD profile result. | +| “This thawed descendant currently matches this sealed parent.†| **Yes, only when that actual parent is supplied and verifies.** | The parent must be cleanly `SEALED`; every sidecar parent coordinate and recorded seal must agree; and the descendant identity is recomputed from authoritative parent kind and media type. | Descendant, strict thaw sidecar, supplied parent bundle, passing parent verification, exact coordinate comparison, and external artifact/key anchor when continuity is required. | The sidecar proves its own history, `thaw_artifact` was independently observed, or the supplied parent has external continuity when no external coordinate was checked. Sidecar-only agreement remains `NOT_ESTABLISHED`. | | “VSTD-Graph records this lineage graph.†| **Yes.** | The receipt binds the stored artifact nodes, transformation edges, roles, statuses, and declarations. Historical Graph-1 receipts retain the serialized receipt identifier `VSTD-DATA-0.1`. | Valid receipt and structurally valid hypergraph. | The graph contains every real-world input or transformation. | | “This is the complete provenance of the model or dataset.†| **No, unless completeness is independently evidenced for the declared boundary.** | A graph cannot infer hidden inputs, pre-observation history, out-of-band processing, or missing instrumentation. | Independent coverage evidence for every declared boundary plus explicit horizons outside it. | That a high coverage summary proves complete real-world lineage. | | “This transformation actually ran and produced this output.†| **Only with execution evidence.** | Software, parameters, and environment fields are declarations until a run trace, rerun, attestation, or equivalent evidence binds execution to the output. | Identified inputs and outputs, execution trace or rerun, software identity, parameters, environment, and evidence classification. | Recording a script name or commit proves execution. | diff --git a/docs/index.html b/docs/index.html index 09640bd..0151f66 100644 --- a/docs/index.html +++ b/docs/index.html @@ -119,6 +119,7 @@

    Useful without pretending to be total.

    Current status

    Current implementation status

    VSTD is a maintainer-led alpha project specification. Compatibility VSTD-4 and Graph paths remain NOT_ESTABLISHED candidates. Separate evidence-bound VSTD-4, VSTD-5, Graph-profile, and assurance-event paths rerun exact registered mechanisms from embedded evidence; no real external witness is claimed by this repository. The project has no demonstrated external adoption, independent implementation, interoperability deployment, or third-party security review.

    +

    Artifact-control boundary: freeze preserves exact bytes and sealing is not encryption. A thaw sidecar alone is unkeyed metadata and remains NOT_ESTABLISHED; clean or dirty status requires the actual supplied parent to verify as sealed with every recorded parent coordinate matching. This current comparison does not authenticate the historical copy operation or external continuity.

    Release coordinate: this branch documents verifier-standard 1.2.0 as an unreleased candidate. Use GitHub Releases for diff --git a/docs/reference.html b/docs/reference.html index 303abd2..02c9be7 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -231,10 +231,13 @@

    vstd artifact thaw

    vstd artifact status

    -

    Compare a thawed descendant with its sealed parent identity.

    +

    Compare a descendant with recorded sidecar metadata, or verify current equality against a supplied sealed parent.

    + + +
    ArgumentKindMeaning
    artifactpositional
    --recordoptional
    --parent-bundleoptionalActual frozen parent bundle required to establish THAWED_CLEAN or THAWED_DIRTY.
    --expected-artifact-idoptional
    --expected-key-idoptional
    --jsonoptional
    @@ -748,8 +751,8 @@

    thaw_artifact function

    thawed_artifact_status function

    -
    thawed_artifact_status(artifact: 'str | Path', thaw_record: 'str | Path | None' = None) -> 'dict[str, Any]'
    -

    Compare a mutable descendant with its sealed parent's initial identity.

    +
    thawed_artifact_status(artifact: 'str | Path', thaw_record: 'str | Path | None' = None, *, parent_bundle: 'str | Path | None' = None, expected_artifact_id: 'str | None' = None, expected_key_id: 'str | None' = None) -> 'dict[str, Any]'
    +

    Assess current descendant equality without authenticating the historical copy.

    Defined in verifier.artifact_control

    diff --git a/src/verifier/artifact_control/__init__.py b/src/verifier/artifact_control/__init__.py index c500005..cb23fb6 100644 --- a/src/verifier/artifact_control/__init__.py +++ b/src/verifier/artifact_control/__init__.py @@ -841,7 +841,13 @@ def thaw_artifact( record["thaw_id"] = _identity("vstd-thaw-1", _canonical_bytes(record)) try: _write_json(record_path, record) - status = thawed_artifact_status(destination_path, record_path) + status = thawed_artifact_status( + destination_path, + record_path, + parent_bundle=bundle_path, + expected_artifact_id=expected_artifact_id, + expected_key_id=expected_key_id, + ) if status["state"] != "THAWED_CLEAN": raise ArtifactControlError("thawed descendant did not match the sealed parent") except Exception: @@ -856,9 +862,21 @@ def thaw_artifact( def thawed_artifact_status( - artifact: str | Path, thaw_record: str | Path | None = None + artifact: str | Path, + thaw_record: str | Path | None = None, + *, + parent_bundle: str | Path | None = None, + expected_artifact_id: str | None = None, + expected_key_id: str | None = None, ) -> dict[str, Any]: - """Compare a mutable descendant with its sealed parent's initial identity.""" + """Assess current descendant equality without authenticating the historical copy. + + A sidecar alone can report only agreement with its own recorded metadata. A + ``THAWED_CLEAN`` or ``THAWED_DIRTY`` result requires an actual supplied parent + bundle whose freeze and seals verify and whose exact coordinates match the + sidecar. Even that result does not prove that the historical copy operation was + independently observed. + """ artifact_path = Path(artifact).resolve() record_path = ( @@ -883,30 +901,164 @@ def thawed_artifact_status( ) if record["schema_version"] != THAW_SCHEMA: raise ArtifactControlError(f"unsupported thaw schema {record['schema_version']!r}") + identifier_fields = { + "parent_artifact_id": "vstd-artifact-1:", + "parent_content_id": "vstd-content-1:", + "parent_freeze_id": "vstd-freeze-1:", + "thaw_id": "vstd-thaw-1:", + } + for name, prefix in identifier_fields.items(): + value = record[name] + if ( + not isinstance(value, str) + or not value.startswith(prefix) + or _DUAL_ID.fullmatch(value) is None + ): + raise ArtifactControlError(f"thaw record {name} is invalid") + seal_ids = record["parent_seal_ids"] + if not isinstance(seal_ids, list) or not seal_ids: + raise ArtifactControlError("thaw record parent_seal_ids must be a nonempty array") + if not all( + isinstance(value, str) + and value.startswith("vstd-seal-1:") + and _DUAL_ID.fullmatch(value) is not None + for value in seal_ids + ): + raise ArtifactControlError("thaw record parent_seal_ids contains an invalid seal ID") + if len(set(seal_ids)) != len(seal_ids): + raise ArtifactControlError("thaw record parent_seal_ids must not contain duplicates") + if record["artifact_kind"] not in {"file", "directory"}: + raise ArtifactControlError("thaw record artifact_kind is invalid") + if not isinstance(record["media_type"], str) or not record["media_type"]: + raise ArtifactControlError("thaw record media_type must be a nonempty string") stable = {key: record[key] for key in record if key != "thaw_id"} if record["thaw_id"] != _identity("vstd-thaw-1", _canonical_bytes(stable)): raise ArtifactControlError("thaw_id does not close the thaw record") - observed_kind = "file" if artifact_path.is_file() else "directory" if artifact_path.is_dir() else "" - if observed_kind != record["artifact_kind"]: - state = "THAWED_DIRTY" - observed_id = None - else: - entries = _source_entries(artifact_path) - observed_id = _identity( - "vstd-artifact-1", - _canonical_bytes(_descriptor(observed_kind, record["media_type"], entries)), - ) - state = ( - "THAWED_CLEAN" - if observed_id == record["parent_artifact_id"] - else "THAWED_DIRTY" - ) - return { - "state": state, + + observed_kind = ( + "file" + if artifact_path.is_file() + else "directory" + if artifact_path.is_dir() + else "" + ) + + recorded_observed_id: str | None = None + try: + if observed_kind == record["artifact_kind"]: + recorded_observed_id = _identity( + "vstd-artifact-1", + _canonical_bytes( + _descriptor( + observed_kind, + record["media_type"], + _source_entries(artifact_path), + ) + ), + ) + except ArtifactControlError: + recorded_observed_id = None + recorded_identity_match = recorded_observed_id == record["parent_artifact_id"] + + result: dict[str, Any] = { + "state": "NOT_ESTABLISHED", + "lineage_state": "NOT_ESTABLISHED", + "recorded_identity_match": recorded_identity_match, + "verified_parent_identity_match": None, + "identity_basis": "SIDECAR_RECORDED_METADATA", + "parent_verification_state": "NOT_CHECKED", + "external_anchor_state": "NOT_CHECKED", + "historical_operation": "NOT_ESTABLISHED", "parent_artifact_id": record["parent_artifact_id"], - "observed_artifact_id": observed_id, + "observed_artifact_id": recorded_observed_id, "thaw_id": record["thaw_id"], + "errors": [], + "warnings": [ + "sidecar agreement does not establish an actual sealed parent or historical thaw operation" + ], } + if parent_bundle is None: + return result + + parent_path = Path(parent_bundle).resolve() + parent_verification = verify_frozen_artifact( + parent_path, + expected_artifact_id=expected_artifact_id, + expected_key_id=expected_key_id, + require_seal=True, + ) + result["parent_verification_state"] = parent_verification.state + result["external_anchor_state"] = parent_verification.external_anchor + result["errors"] = list(parent_verification.errors) + if not parent_verification.sealed: + result["state"] = "FAIL" + result["errors"].append( + "supplied parent bundle is not cleanly sealed" + ) + return result + + parent_freeze = _load_freeze(parent_path) + coordinate_errors: list[str] = [] + comparisons = ( + ("parent_artifact_id", parent_verification.artifact_id), + ("parent_content_id", parent_verification.content_id), + ("parent_freeze_id", parent_verification.freeze_id), + ("artifact_kind", parent_freeze["artifact_kind"]), + ("media_type", parent_freeze["media_type"]), + ) + for name, expected in comparisons: + if record[name] != expected: + coordinate_errors.append( + f"thaw record {name} does not match the supplied sealed parent" + ) + valid_parent_seals = set(parent_verification.valid_seal_ids) + missing_seals = sorted(set(seal_ids) - valid_parent_seals) + if missing_seals: + coordinate_errors.append( + "thaw record names a seal that is not valid on the supplied parent: " + + ", ".join(missing_seals) + ) + + authoritative_observed_id: str | None = None + try: + if observed_kind == parent_freeze["artifact_kind"]: + authoritative_observed_id = _identity( + "vstd-artifact-1", + _canonical_bytes( + _descriptor( + observed_kind, + parent_freeze["media_type"], + _source_entries(artifact_path), + ) + ), + ) + except ArtifactControlError: + authoritative_observed_id = None + + result["identity_basis"] = "VERIFIED_PARENT_METADATA" + result["observed_artifact_id"] = authoritative_observed_id + result["verified_parent_identity_match"] = ( + authoritative_observed_id == parent_verification.artifact_id + ) + result["warnings"] = [ + "current equality does not prove that the historical thaw operation was independently observed" + ] + if parent_verification.external_anchor == "NOT_CHECKED": + result["warnings"].append( + "supplied parent is internally consistent; external continuity was not checked" + ) + if coordinate_errors: + result["state"] = "FAIL" + result["errors"].extend(coordinate_errors) + return result + + result["lineage_state"] = "PARENT_COORDINATES_ESTABLISHED" + result["state"] = ( + "THAWED_CLEAN" + if result["verified_parent_identity_match"] + else "THAWED_DIRTY" + ) + return result __all__ = [ diff --git a/src/verifier/artifact_control/artifact-control-1.schema.json b/src/verifier/artifact_control/artifact-control-1.schema.json index fa1b943..7812d5c 100644 --- a/src/verifier/artifact_control/artifact-control-1.schema.json +++ b/src/verifier/artifact_control/artifact-control-1.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json", "title": "Verifier Standard (VSTD) artifact-control mechanism formats", - "description": "Strict shapes for freeze manifests, finite self-closing seal envelopes, and copy-on-write thaw lineage records. These objects are mechanism formats, not VSTD receipts or numbered-profile conformance claims.", + "description": "Strict shapes for freeze manifests, finite self-closing seal envelopes, and copy-on-write thaw lineage records. Shape-valid thaw metadata does not verify a parent or historical copy operation. These objects are mechanism formats, not VSTD receipts or numbered-profile conformance claims.", "oneOf": [ {"$ref": "#/$defs/freeze"}, {"$ref": "#/$defs/seal"}, @@ -105,6 +105,7 @@ }, "thaw": { "type": "object", + "description": "Unkeyed lineage metadata whose self-derived thaw_id establishes field agreement only. Established clean or dirty status requires separate verification against the actual supplied sealed parent.", "additionalProperties": false, "required": ["schema_version", "parent_artifact_id", "parent_content_id", "parent_freeze_id", "parent_seal_ids", "artifact_kind", "media_type", "thaw_id"], "properties": { diff --git a/src/verifier/runtime/public_cli.py b/src/verifier/runtime/public_cli.py index e9d83de..d68fdfc 100644 --- a/src/verifier/runtime/public_cli.py +++ b/src/verifier/runtime/public_cli.py @@ -324,10 +324,20 @@ def build_parser() -> argparse.ArgumentParser: thaw_parser.add_argument("--json", action="store_true") status_parser = artifact_commands.add_parser( - "status", help="Compare a thawed descendant with its sealed parent identity." + "status", + help=( + "Compare a descendant with recorded sidecar metadata, or verify current " + "equality against a supplied sealed parent." + ), ) status_parser.add_argument("artifact") status_parser.add_argument("--record") + status_parser.add_argument( + "--parent-bundle", + help="Actual frozen parent bundle required to establish THAWED_CLEAN or THAWED_DIRTY.", + ) + status_parser.add_argument("--expected-artifact-id") + status_parser.add_argument("--expected-key-id") status_parser.add_argument("--json", action="store_true") add_experiment_parsers(subparsers) add_vstd3_parsers(subparsers) @@ -507,9 +517,19 @@ def _handle_artifact_command(args: argparse.Namespace) -> int: output = {"state": "THAWED_CLEAN", **result} _print_artifact_result(output, args.json) return 0 - result = thawed_artifact_status(args.artifact, args.record) + result = thawed_artifact_status( + args.artifact, + args.record, + parent_bundle=args.parent_bundle, + expected_artifact_id=args.expected_artifact_id, + expected_key_id=args.expected_key_id, + ) _print_artifact_result(result, args.json) - return 0 if result["state"] == "THAWED_CLEAN" else 1 + if result["state"] == "THAWED_CLEAN": + return 0 + if result["state"] == "NOT_ESTABLISHED": + return 2 + return 1 def main(argv: list[str] | None = None) -> int: diff --git a/src/verifier/specifications/ARTIFACT_CONTROL.md b/src/verifier/specifications/ARTIFACT_CONTROL.md index 9bdd333..48a2043 100644 --- a/src/verifier/specifications/ARTIFACT_CONTROL.md +++ b/src/verifier/specifications/ARTIFACT_CONTROL.md @@ -18,7 +18,7 @@ encryption format, archival service, correctness proof, or actor reputation syst |---|---|---| | **Freeze** | The bundle's current regular-file bytes and portable paths match its manifest, and its guarded payload tree is read-only. | Durable external preservation, privileged-write prevention, correctness, freshness, or a cryptographic signer. | | **Seal** | A carried public key verifies a signature over the exact freeze closure, and the seal identifier closes the signature-bearing envelope. | Encryption, secrecy, ownership, authorization, trusted time, signer reputation, or protection against whole-bundle substitution. | -| **Thaw** | A new mutable descendant initially matched a clean sealed parent and retained a lineage sidecar. | Mutation of the parent, continued equality after thaw, or a sealed descendant. | +| **Thaw** | The creation operation copied a clean sealed parent into a new mutable descendant and emitted a lineage sidecar. Later `THAWED_CLEAN` status establishes current equality only when the actual supplied parent verifies and every recorded parent coordinate agrees. | Authentication of the historical copy operation, mutation of the parent, continued equality after thaw, or a sealed descendant. | Sealing and encryption are independent. Version 1 seals are readable and authenticated; they do not encrypt any byte. A future encrypted container MUST still identify a separate @@ -109,10 +109,26 @@ path and emits a `VSTD-ARTIFACT-THAW-1` sidecar beside the descendant. The sidec the parent artifact, content, freeze, and seal identifiers. It is lineage metadata, not a seal. The parent remains unchanged. -`THAWED_CLEAN` means the descendant still has the parent's initial artifact identity. -`THAWED_DIRTY` means it no longer does. To produce a new frozen artifact, freeze the -descendant into a new bundle and bind the sealed parent through `lineage`. This is an -additive state transition; no operation edits or erases the parent. +A sidecar's self-derived `thaw_id` establishes only internal agreement among its fields. +Sidecar-only status is `NOT_ESTABLISHED`, even when current descendant bytes match the +recorded artifact identifier. `THAWED_CLEAN` requires the actual supplied parent bundle to +verify as cleanly `SEALED`; its artifact, content, freeze, artifact-kind, and media-type +coordinates must equal the sidecar; and every sidecar seal identifier must remain valid on +that parent. Later additional valid parent seals are permitted. A conflicted parent or any +coordinate mismatch fails closed. Authoritative parent metadata—not sidecar metadata—is +used for the established descendant comparison. + +`THAWED_CLEAN` means the current descendant matches that supplied, cleanly sealed parent. +`THAWED_DIRTY` means the verified parent coordinates still agree but the descendant no +longer does. Neither result proves that a verifier independently observed or authenticated +the historical copy operation. That claim requires a separately signed, logged, attested, +or otherwise mechanism-checked event. Without an expected artifact identifier, expected +key identifier, or separately verified external log coordinate, a supplied parent proves +internal parent consistency rather than external continuity. + +To produce a new frozen artifact, freeze the descendant into a new bundle and bind the +sealed parent through `lineage`. This is an additive state transition; no operation edits +or erases the parent. `bound_contexts` similarly binds the artifact identifiers of clean sealed context bundles. It does not interpret or validate their subject matter. A sealed realm descriptor, @@ -128,7 +144,7 @@ checks it. | `SEALED` | Freeze, guards, and at least one seal verified with no contradictory seal. | | `CONFLICTED` | Valid and invalid seal evidence coexist. | | `FAIL` | A checked structural, byte, guard, seal, or external-anchor condition failed. | -| `THAWED_CLEAN` / `THAWED_DIRTY` | A mutable descendant currently matches or differs from its recorded parent identity. | +| `THAWED_CLEAN` / `THAWED_DIRTY` | With an actual cleanly verified supplied parent whose exact recorded coordinates agree, a mutable descendant currently matches or differs from that parent. Historical execution of the copy remains `NOT_ESTABLISHED`. | A clean freeze or seal can earn bounded **TRUST** in integrity and closure. It earns no support for semantic correctness. Freezing does not stop **ROT** caused by staleness, diff --git a/standard/ARTIFACT_CONTROL.md b/standard/ARTIFACT_CONTROL.md index 9bdd333..48a2043 100644 --- a/standard/ARTIFACT_CONTROL.md +++ b/standard/ARTIFACT_CONTROL.md @@ -18,7 +18,7 @@ encryption format, archival service, correctness proof, or actor reputation syst |---|---|---| | **Freeze** | The bundle's current regular-file bytes and portable paths match its manifest, and its guarded payload tree is read-only. | Durable external preservation, privileged-write prevention, correctness, freshness, or a cryptographic signer. | | **Seal** | A carried public key verifies a signature over the exact freeze closure, and the seal identifier closes the signature-bearing envelope. | Encryption, secrecy, ownership, authorization, trusted time, signer reputation, or protection against whole-bundle substitution. | -| **Thaw** | A new mutable descendant initially matched a clean sealed parent and retained a lineage sidecar. | Mutation of the parent, continued equality after thaw, or a sealed descendant. | +| **Thaw** | The creation operation copied a clean sealed parent into a new mutable descendant and emitted a lineage sidecar. Later `THAWED_CLEAN` status establishes current equality only when the actual supplied parent verifies and every recorded parent coordinate agrees. | Authentication of the historical copy operation, mutation of the parent, continued equality after thaw, or a sealed descendant. | Sealing and encryption are independent. Version 1 seals are readable and authenticated; they do not encrypt any byte. A future encrypted container MUST still identify a separate @@ -109,10 +109,26 @@ path and emits a `VSTD-ARTIFACT-THAW-1` sidecar beside the descendant. The sidec the parent artifact, content, freeze, and seal identifiers. It is lineage metadata, not a seal. The parent remains unchanged. -`THAWED_CLEAN` means the descendant still has the parent's initial artifact identity. -`THAWED_DIRTY` means it no longer does. To produce a new frozen artifact, freeze the -descendant into a new bundle and bind the sealed parent through `lineage`. This is an -additive state transition; no operation edits or erases the parent. +A sidecar's self-derived `thaw_id` establishes only internal agreement among its fields. +Sidecar-only status is `NOT_ESTABLISHED`, even when current descendant bytes match the +recorded artifact identifier. `THAWED_CLEAN` requires the actual supplied parent bundle to +verify as cleanly `SEALED`; its artifact, content, freeze, artifact-kind, and media-type +coordinates must equal the sidecar; and every sidecar seal identifier must remain valid on +that parent. Later additional valid parent seals are permitted. A conflicted parent or any +coordinate mismatch fails closed. Authoritative parent metadata—not sidecar metadata—is +used for the established descendant comparison. + +`THAWED_CLEAN` means the current descendant matches that supplied, cleanly sealed parent. +`THAWED_DIRTY` means the verified parent coordinates still agree but the descendant no +longer does. Neither result proves that a verifier independently observed or authenticated +the historical copy operation. That claim requires a separately signed, logged, attested, +or otherwise mechanism-checked event. Without an expected artifact identifier, expected +key identifier, or separately verified external log coordinate, a supplied parent proves +internal parent consistency rather than external continuity. + +To produce a new frozen artifact, freeze the descendant into a new bundle and bind the +sealed parent through `lineage`. This is an additive state transition; no operation edits +or erases the parent. `bound_contexts` similarly binds the artifact identifiers of clean sealed context bundles. It does not interpret or validate their subject matter. A sealed realm descriptor, @@ -128,7 +144,7 @@ checks it. | `SEALED` | Freeze, guards, and at least one seal verified with no contradictory seal. | | `CONFLICTED` | Valid and invalid seal evidence coexist. | | `FAIL` | A checked structural, byte, guard, seal, or external-anchor condition failed. | -| `THAWED_CLEAN` / `THAWED_DIRTY` | A mutable descendant currently matches or differs from its recorded parent identity. | +| `THAWED_CLEAN` / `THAWED_DIRTY` | With an actual cleanly verified supplied parent whose exact recorded coordinates agree, a mutable descendant currently matches or differs from that parent. Historical execution of the copy remains `NOT_ESTABLISHED`. | A clean freeze or seal can earn bounded **TRUST** in integrity and closure. It earns no support for semantic correctness. Freezing does not stop **ROT** caused by staleness, diff --git a/standard/schemas/artifact-control-1.schema.json b/standard/schemas/artifact-control-1.schema.json index fa1b943..7812d5c 100644 --- a/standard/schemas/artifact-control-1.schema.json +++ b/standard/schemas/artifact-control-1.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://timelordraps.github.io/verifier/schemas/artifact-control-1.schema.json", "title": "Verifier Standard (VSTD) artifact-control mechanism formats", - "description": "Strict shapes for freeze manifests, finite self-closing seal envelopes, and copy-on-write thaw lineage records. These objects are mechanism formats, not VSTD receipts or numbered-profile conformance claims.", + "description": "Strict shapes for freeze manifests, finite self-closing seal envelopes, and copy-on-write thaw lineage records. Shape-valid thaw metadata does not verify a parent or historical copy operation. These objects are mechanism formats, not VSTD receipts or numbered-profile conformance claims.", "oneOf": [ {"$ref": "#/$defs/freeze"}, {"$ref": "#/$defs/seal"}, @@ -105,6 +105,7 @@ }, "thaw": { "type": "object", + "description": "Unkeyed lineage metadata whose self-derived thaw_id establishes field agreement only. Established clean or dirty status requires separate verification against the actual supplied sealed parent.", "additionalProperties": false, "required": ["schema_version", "parent_artifact_id", "parent_content_id", "parent_freeze_id", "parent_seal_ids", "artifact_kind", "media_type", "thaw_id"], "properties": { diff --git a/tests/test_artifact_control.py b/tests/test_artifact_control.py index d884924..f638f85 100644 --- a/tests/test_artifact_control.py +++ b/tests/test_artifact_control.py @@ -6,12 +6,14 @@ from __future__ import annotations import base64 +import hashlib import json import stat from pathlib import Path import pytest +import verifier.artifact_control as artifact_control_module from verifier.artifact_control import ( ArtifactControlError, freeze_artifact, @@ -59,6 +61,44 @@ def _seal_path(bundle: Path) -> Path: return next((bundle / "seals").glob("*.json")) +def _dual_id(kind: str, payload: bytes) -> str: + return ( + f"vstd-{kind}-1:sha256:{hashlib.sha256(payload).hexdigest()}:" + f"sha3-256:{hashlib.sha3_256(payload).hexdigest()}" + ) + + +def _fake_dual_id(kind: str, digit: str = "0") -> str: + return f"vstd-{kind}-1:sha256:{digit * 64}:sha3-256:{digit * 64}" + + +def _reclose_thaw_record(path: Path, **changes: object) -> dict[str, object]: + record = json.loads(path.read_text(encoding="utf-8")) + record.update(changes) + stable = {key: record[key] for key in record if key != "thaw_id"} + canonical = json.dumps( + stable, + ensure_ascii=False, + allow_nan=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + record["thaw_id"] = _dual_id("thaw", canonical) + path.write_text( + json.dumps(record, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + newline="\n", + ) + return record + + +def _thawed_file(tmp_path: Path, name: str = "thaw") -> tuple[Path, Path, Path]: + bundle, _ = _sealed_file(tmp_path, name) + descendant = tmp_path / f"{name}-descendant.bin" + record = thaw_artifact(bundle, descendant) + return bundle, descendant, Path(str(record["record_path"])) + + def test_freeze_preserves_exact_file_bytes_without_claiming_a_seal(tmp_path: Path) -> None: source = tmp_path / "source.bin" source.write_bytes(b"\x00\r\n\xff") @@ -251,9 +291,18 @@ def test_thaw_is_copy_on_write_and_dirtying_is_observable(tmp_path: Path) -> Non record = thaw_artifact(bundle, descendant) assert descendant.read_bytes() == parent_before - assert thawed_artifact_status(descendant)["state"] == "THAWED_CLEAN" + sidecar_only = thawed_artifact_status(descendant) + assert sidecar_only["state"] == "NOT_ESTABLISHED" + assert sidecar_only["recorded_identity_match"] is True + assert sidecar_only["lineage_state"] == "NOT_ESTABLISHED" + assert thawed_artifact_status( + descendant, parent_bundle=bundle + )["state"] == "THAWED_CLEAN" descendant.write_bytes(b"changed") - assert thawed_artifact_status(descendant)["state"] == "THAWED_DIRTY" + assert thawed_artifact_status(descendant)["state"] == "NOT_ESTABLISHED" + assert thawed_artifact_status( + descendant, parent_bundle=bundle + )["state"] == "THAWED_DIRTY" assert (bundle / "payload").read_bytes() == parent_before assert record["parent_artifact_id"] == verify_frozen_artifact(bundle).artifact_id @@ -284,6 +333,370 @@ def test_sealed_parent_and_context_bindings_are_explicit_and_deduplicated( ) +def test_fabricated_thaw_sidecar_without_parent_never_establishes_lineage( + tmp_path: Path, +) -> None: + artifact = tmp_path / "fabricated.bin" + artifact.write_bytes(b"fabricated") + probe = tmp_path / "probe" + manifest = freeze_artifact(artifact, probe, media_type="application/x-fabricated") + record_path = tmp_path / "fabricated.bin.vstd-thaw.json" + record_path.write_text( + json.dumps( + { + "schema_version": "VSTD-ARTIFACT-THAW-1", + "parent_artifact_id": manifest["artifact_id"], + "parent_content_id": _fake_dual_id("content"), + "parent_freeze_id": _fake_dual_id("freeze"), + "parent_seal_ids": [_fake_dual_id("seal")], + "artifact_kind": "file", + "media_type": "application/x-fabricated", + "thaw_id": _fake_dual_id("thaw"), + } + ), + encoding="utf-8", + ) + _reclose_thaw_record(record_path) + + result = thawed_artifact_status(artifact, record_path) + + assert result["state"] == "NOT_ESTABLISHED" + assert result["recorded_identity_match"] is True + assert result["verified_parent_identity_match"] is None + assert result["lineage_state"] == "NOT_ESTABLISHED" + assert result["historical_operation"] == "NOT_ESTABLISHED" + + +@pytest.mark.parametrize( + ("field", "replacement"), + ( + ("parent_content_id", _fake_dual_id("content")), + ("parent_freeze_id", _fake_dual_id("freeze")), + ("parent_seal_ids", [_fake_dual_id("seal")]), + ), +) +def test_reclosed_false_parent_coordinates_do_not_establish_clean_lineage( + tmp_path: Path, field: str, replacement: object +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, field) + _reclose_thaw_record(record_path, **{field: replacement}) + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert result["state"] == "FAIL" + assert result["lineage_state"] == "NOT_ESTABLISHED" + assert result["historical_operation"] == "NOT_ESTABLISHED" + + +def test_fabricated_parent_artifact_and_matching_descendant_need_actual_parent( + tmp_path: Path, +) -> None: + _, descendant, record_path = _thawed_file(tmp_path, "fabricated-parent") + descendant.write_bytes(b"neighboring fabricated descendant") + probe = tmp_path / "fabricated-parent-probe" + manifest = freeze_artifact( + descendant, probe, media_type="application/x-test" + ) + _reclose_thaw_record(record_path, parent_artifact_id=manifest["artifact_id"]) + + result = thawed_artifact_status(descendant, record_path) + + assert result["recorded_identity_match"] is True + assert result["state"] == "NOT_ESTABLISHED" + assert result["parent_verification_state"] == "NOT_CHECKED" + + +def test_neighboring_sealed_parent_bundle_is_refused(tmp_path: Path) -> None: + _, descendant, record_path = _thawed_file(tmp_path, "original-parent") + neighbor, _ = _sealed_file(tmp_path, "neighbor-parent") + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=neighbor + ) + + assert result["state"] == "FAIL" + assert any("seal" in error for error in result["errors"]) + + +def test_recorded_seal_must_be_valid_on_supplied_parent(tmp_path: Path) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "invalid-recorded-seal") + _reclose_thaw_record(record_path, parent_seal_ids=[_fake_dual_id("seal", "1")]) + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert result["state"] == "FAIL" + assert any("not valid" in error for error in result["errors"]) + + +def test_later_additional_valid_parent_seal_preserves_thaw_lineage( + tmp_path: Path, +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "additional-seal") + original = json.loads(record_path.read_text(encoding="utf-8"))["parent_seal_ids"] + seal_artifact(bundle, _private_key(tmp_path / "additional-seal-later.pem")) + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert result["state"] == "THAWED_CLEAN" + assert set(original).issubset(verify_frozen_artifact(bundle).valid_seal_ids) + + +def test_unsealed_parent_cannot_establish_thaw_lineage(tmp_path: Path) -> None: + source = tmp_path / "unsealed-parent.bin" + source.write_bytes(b"unsealed") + unsealed = tmp_path / "unsealed-parent" + freeze_artifact(source, unsealed, media_type="application/x-test") + _, descendant, record_path = _thawed_file(tmp_path, "sealed-origin") + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=unsealed + ) + + assert result["state"] == "FAIL" + assert result["parent_verification_state"] == "NOT_ESTABLISHED" + + +def test_conflicted_parent_cannot_establish_thaw_lineage(tmp_path: Path) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "conflicted-parent") + invalid = bundle / "seals" / "invalid.json" + invalid.write_text("{}\n", encoding="utf-8") + invalid.chmod(invalid.stat().st_mode & ~stat.S_IWUSR) + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert result["state"] == "FAIL" + assert result["parent_verification_state"] == "CONFLICTED" + + +@pytest.mark.parametrize( + ("anchor_name", "anchor_value"), + ( + ("expected_artifact_id", _fake_dual_id("artifact")), + ("expected_key_id", "vstd-seal-key-1:sha256:" + "0" * 64), + ), +) +def test_parent_external_anchor_mismatch_refuses_thaw_lineage( + tmp_path: Path, anchor_name: str, anchor_value: str +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, anchor_name) + + result = thawed_artifact_status( + descendant, + record_path, + parent_bundle=bundle, + **{anchor_name: anchor_value}, + ) + + assert result["state"] == "FAIL" + assert result["external_anchor_state"] == "MISMATCH" + + +def test_verified_parent_distinguishes_clean_and_dirty_without_claiming_history( + tmp_path: Path, +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "verified-parent") + + clean = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + descendant.write_bytes(b"dirty descendant") + dirty = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert clean["state"] == "THAWED_CLEAN" + assert clean["lineage_state"] == "PARENT_COORDINATES_ESTABLISHED" + assert clean["verified_parent_identity_match"] is True + assert dirty["state"] == "THAWED_DIRTY" + assert dirty["lineage_state"] == "PARENT_COORDINATES_ESTABLISHED" + assert dirty["verified_parent_identity_match"] is False + assert clean["historical_operation"] == dirty["historical_operation"] == "NOT_ESTABLISHED" + assert any("historical" in warning for warning in clean["warnings"]) + + +@pytest.mark.parametrize( + ("field", "replacement"), + (("artifact_kind", "directory"), ("media_type", "application/x-neighbor")), +) +def test_sidecar_kind_and_media_type_must_match_authoritative_parent_metadata( + tmp_path: Path, field: str, replacement: str +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, field) + _reclose_thaw_record(record_path, **{field: replacement}) + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert result["state"] == "FAIL" + assert result["identity_basis"] == "VERIFIED_PARENT_METADATA" + assert any(field in error for error in result["errors"]) + + +def test_status_check_does_not_modify_parent_bytes_seals_or_modes(tmp_path: Path) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "parent-immutability") + + def snapshot() -> list[tuple[str, bytes | None, int]]: + return [ + ( + path.relative_to(bundle).as_posix(), + path.read_bytes() if path.is_file() else None, + stat.S_IMODE(path.stat().st_mode), + ) + for path in sorted(bundle.rglob("*"), key=lambda item: item.as_posix()) + ] + + before = snapshot() + result = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + after = snapshot() + + assert result["state"] == "THAWED_CLEAN" + assert after == before + + +@pytest.mark.parametrize( + "mutation", + ( + "schema", + "identifier", + "empty_seals", + "invalid_seal", + "duplicate_seal", + "kind", + "media_type", + "thaw_id", + ), +) +def test_malformed_thaw_sidecar_fields_fail_closed( + tmp_path: Path, mutation: str +) -> None: + _, descendant, record_path = _thawed_file(tmp_path, f"malformed-{mutation}") + record = json.loads(record_path.read_text(encoding="utf-8")) + if mutation == "schema": + record["schema_version"] = "UNKNOWN" + elif mutation == "identifier": + record["parent_content_id"] = "not-an-identifier" + elif mutation == "empty_seals": + record["parent_seal_ids"] = [] + elif mutation == "invalid_seal": + record["parent_seal_ids"] = ["not-a-seal"] + elif mutation == "duplicate_seal": + record["parent_seal_ids"] = record["parent_seal_ids"] * 2 + elif mutation == "kind": + record["artifact_kind"] = "device" + elif mutation == "media_type": + record["media_type"] = "" + else: + record["thaw_id"] = _fake_dual_id("thaw") + record_path.write_text(json.dumps(record), encoding="utf-8") + + with pytest.raises(ArtifactControlError): + thawed_artifact_status(descendant, record_path) + + +def test_verified_parent_kind_change_is_dirty_not_clean(tmp_path: Path) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "kind-change") + descendant.unlink() + descendant.mkdir() + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert result["state"] == "THAWED_DIRTY" + assert result["verified_parent_identity_match"] is False + + +def test_symlink_descendant_cannot_match_recorded_or_verified_parent( + tmp_path: Path, +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "symlink-descendant") + target = tmp_path / "symlink-target.bin" + target.write_bytes(descendant.read_bytes()) + descendant.unlink() + try: + descendant.symlink_to(target) + except OSError as exc: + pytest.skip(f"symlink creation is unavailable: {exc}") + + sidecar_only = thawed_artifact_status(descendant, record_path) + verified = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert sidecar_only["state"] == "NOT_ESTABLISHED" + assert sidecar_only["recorded_identity_match"] is False + assert verified["state"] == "THAWED_DIRTY" + assert verified["verified_parent_identity_match"] is False + + +def test_unreadable_descendant_inventory_cannot_match_any_identity( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "unreadable-descendant") + original = artifact_control_module._source_entries + + def refuse_descendant(path: Path) -> list[dict[str, object]]: + if path.resolve() == descendant.resolve(): + raise ArtifactControlError("simulated unsupported descendant") + return original(path) + + monkeypatch.setattr(artifact_control_module, "_source_entries", refuse_descendant) + + sidecar_only = thawed_artifact_status(descendant, record_path) + verified = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert sidecar_only["recorded_identity_match"] is False + assert verified["state"] == "THAWED_DIRTY" + assert verified["verified_parent_identity_match"] is False + + +def test_matching_external_parent_anchors_are_reported_without_claiming_history( + tmp_path: Path, +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "matching-anchors") + parent = verify_frozen_artifact(bundle) + + result = thawed_artifact_status( + descendant, + record_path, + parent_bundle=bundle, + expected_artifact_id=parent.artifact_id, + expected_key_id=parent.key_ids[0], + ) + + assert result["state"] == "THAWED_CLEAN" + assert result["external_anchor_state"] == "ARTIFACT_AND_KEY_MATCHED" + assert not any("external continuity was not checked" in item for item in result["warnings"]) + assert result["historical_operation"] == "NOT_ESTABLISHED" + + +def test_parent_mutation_after_thaw_refuses_current_lineage(tmp_path: Path) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "mutated-parent") + payload = bundle / "payload" + _writable(payload) + payload.write_bytes(b"mutated parent") + + result = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert result["state"] == "FAIL" + assert result["parent_verification_state"] == "FAIL" + + def test_unknown_bundle_or_manifest_fields_fail_closed(tmp_path: Path) -> None: bundle, _ = _sealed_file(tmp_path) (bundle / "surprise.txt").write_text("not part of the format", encoding="utf-8") @@ -343,6 +756,28 @@ def test_public_cli_exposes_the_complete_artifact_lifecycle( ["artifact", "thaw", str(bundle), str(descendant), "--json"] ) == 0 assert json.loads(capsys.readouterr().out)["state"] == "THAWED_CLEAN" + assert main(["artifact", "status", str(descendant), "--json"]) == 2 + assert json.loads(capsys.readouterr().out)["state"] == "NOT_ESTABLISHED" + assert main( + [ + "artifact", + "status", + str(descendant), + "--parent-bundle", + str(bundle), + "--json", + ] + ) == 0 + assert json.loads(capsys.readouterr().out)["state"] == "THAWED_CLEAN" descendant.write_bytes(b"dirty") - assert main(["artifact", "status", str(descendant), "--json"]) == 1 + assert main( + [ + "artifact", + "status", + str(descendant), + "--parent-bundle", + str(bundle), + "--json", + ] + ) == 1 assert json.loads(capsys.readouterr().out)["state"] == "THAWED_DIRTY" diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 37d58df..da8ccfd 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -2,6 +2,7 @@ from __future__ import annotations +import inspect from pathlib import Path import pytest @@ -62,6 +63,18 @@ def test_supported_top_level_exports_are_explicit_and_resolvable() -> None: assert getattr(verifier, name) is not None +def test_thaw_status_public_api_requires_explicit_parent_evidence_for_establishment() -> None: + parameters = inspect.signature(verifier.thawed_artifact_status).parameters + assert tuple(parameters) == ( + "artifact", + "thaw_record", + "parent_bundle", + "expected_artifact_id", + "expected_key_id", + ) + assert parameters["parent_bundle"].kind is inspect.Parameter.KEYWORD_ONLY + + def test_deprecation_registry_warns_without_replacing_the_export( monkeypatch: pytest.MonkeyPatch, ) -> None: From 19e00bdb75527f6580de59752c1378691798356e Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sun, 30 Aug 2026 12:35:50 -0700 Subject: [PATCH 26/34] fix(artifact-control): preserve descendant symlink identity --- src/verifier/artifact_control/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/verifier/artifact_control/__init__.py b/src/verifier/artifact_control/__init__.py index cb23fb6..3f20d1f 100644 --- a/src/verifier/artifact_control/__init__.py +++ b/src/verifier/artifact_control/__init__.py @@ -878,7 +878,7 @@ def thawed_artifact_status( independently observed. """ - artifact_path = Path(artifact).resolve() + artifact_path = Path(artifact).absolute() record_path = ( Path(thaw_record).resolve() if thaw_record is not None From 6b8fe413c1155f877efb2b557cb3ad51342872f1 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sun, 30 Aug 2026 14:34:54 -0700 Subject: [PATCH 27/34] fix(artifact-control): preserve lexical creation paths --- CHANGELOG.md | 4 + docs/API_STABILITY.md | 6 + docs/ARCHITECTURE.md | 5 +- docs/CLAIMS_AND_LIMITS.md | 4 +- docs/reference.html | 4 +- src/verifier/artifact_control/__init__.py | 108 ++++-- src/verifier/runtime/public_cli.py | 9 +- .../specifications/ARTIFACT_CONTROL.md | 11 +- standard/ARTIFACT_CONTROL.md | 11 +- tests/test_artifact_control.py | 329 ++++++++++++++++++ 10 files changed, 454 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0389076..5b0cc3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,10 @@ `THAWED_CLEAN` or `THAWED_DIRTY`. Sidecar-only agreement is now `NOT_ESTABLISHED`; even a verified current match does not authenticate the historical copy operation or external parent continuity. +- Preserve final filesystem-entry identity during artifact creation: freeze refuses + symbolic-link sources, and bundle, thaw-descendant, and sidecar outputs refuse every + preexisting lexical entry, including dangling symbolic links, without claiming universal + race-free filesystem security. - Remove the live SimulacraBench rehearsal and its front-door promotion; the repository never contained or reproduced the submission, hosted image, hardware, or protected evaluation identified by that name. diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md index fb33e8b..6edeb9d 100644 --- a/docs/API_STABILITY.md +++ b/docs/API_STABILITY.md @@ -22,6 +22,12 @@ The supported artifact-control exports are `freeze_artifact`, `seal_artifact`, require the optional `seal` dependency extra; importing the base package and freeze-only operations retain the zero-third-party-dependency boundary. +`freeze_artifact` classifies the supplied final source entry before dereferencing it and +refuses symbolic links. New freeze bundles, thaw descendants, and generated thaw sidecars +require absent lexical destination entries, including refusal of dangling symbolic links. +This fail-closed creation contract does not promise universal race-free filesystem security +against concurrent privileged replacement. + `thawed_artifact_status` treats a `VSTD-ARTIFACT-THAW-1` sidecar as unkeyed lineage metadata. Without `parent_bundle`, it returns `NOT_ESTABLISHED` even when descendant bytes agree with the sidecar's recorded identifier. `THAWED_CLEAN` or `THAWED_DIRTY` requires an diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a4b7013..3a846a0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -53,7 +53,10 @@ continuous temporal mediation. A thaw sidecar is additive lineage metadata, not authentication: sidecar-only agreement remains `NOT_ESTABLISHED`. Current clean or dirty status requires an actual supplied, cleanly sealed parent whose artifact, content, freeze, seal, kind, and media-type coordinates match. Even that comparison does not authenticate -the historical copy operation. +the historical copy operation. Creation paths preserve the caller-supplied final entry +through classification: freeze refuses symbolic-link sources, while bundle, descendant, +and sidecar creation require an absent lexical destination. These checks do not claim +universal race-free filesystem security against concurrent privileged replacement. ## Verification complex and profile satisfaction diff --git a/docs/CLAIMS_AND_LIMITS.md b/docs/CLAIMS_AND_LIMITS.md index 836ca94..ee0d4dc 100644 --- a/docs/CLAIMS_AND_LIMITS.md +++ b/docs/CLAIMS_AND_LIMITS.md @@ -40,7 +40,7 @@ checker result—not a claim of universal truth or whole-project conformance. | What can the grounded-certificate kernel establish? | The exact `VSTD4-GDC-1` decision was accepted, rejected, or left `UNKNOWN` under its claim binding and resource bound. | Certificate bytes, formula, grounding, policy/evidence roots, verifier descriptor, and kernel. | Kernel acceptance alone is not VSTD-4 conformance. The evidence-bound path separately reruns every prerequisite/rung mechanism before it may establish 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, and any supplied external artifact/key anchor. | 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. | +| 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, and fail-closed final-entry classification for supported creation paths. | 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, and path checks do not establish universal race-free filesystem security. | | 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. | @@ -53,7 +53,7 @@ required falsification surface. |---|---|---|---|---| | “This receipt's stable content has not changed.†| **Yes, after validation passes.** | The validator recomputes the canonical digest over the specified stable fields and compares it with the recorded digest. | Receipt bytes, canonicalization version, recorded digest, passing validator result. | The statements inside the receipt are true or authentic. | | “These observed bytes match this SHA-256 digest.†| **Yes, conditionally.** | A named mechanism can hash accessible bytes at an observation time and compare them with the recorded digest. | The bytes, hashing mechanism, observation time, expected digest, comparison result. | The bytes came from the claimed source, existed before observation, are uncontaminated, or are legally usable. | -| “This artifact is frozen.†| **Yes, after freeze verification passes.** | The current regular-file bytes, portable paths, manifest identifiers, and read-only payload-tree guard recompute. | Complete bundle, passing `vstd artifact verify --freeze-only`, and the exact mechanism version. | Privileged mutation is impossible, an external archive retained the artifact, the artifact is correct, or a signature exists. | +| “This artifact is frozen.†| **Yes, after freeze verification passes.** | The supplied source was an accepted ordinary file or directory; the current regular-file bytes, portable paths, manifest identifiers, and read-only payload-tree guard recompute. | Complete bundle, passing `vstd artifact verify --freeze-only`, and the exact mechanism version. | A symbolic-link source was frozen as its target, privileged mutation is impossible, an external archive retained the artifact, the artifact is correct, or a signature exists. | | “This artifact is sealed.†| **Yes, after seal verification passes.** | The carried Ed25519 key verifies the finite signature closure and the seal identifier closes the signature-bearing envelope. | Passing seal verification plus an expected artifact/key coordinate when whole-bundle substitution is in scope. | Encryption, secrecy, ownership, authorization, trusted time, semantic correctness, continuous custody, actor trust, or a numbered VSTD profile result. | | “This thawed descendant currently matches this sealed parent.†| **Yes, only when that actual parent is supplied and verifies.** | The parent must be cleanly `SEALED`; every sidecar parent coordinate and recorded seal must agree; and the descendant identity is recomputed from authoritative parent kind and media type. | Descendant, strict thaw sidecar, supplied parent bundle, passing parent verification, exact coordinate comparison, and external artifact/key anchor when continuity is required. | The sidecar proves its own history, `thaw_artifact` was independently observed, or the supplied parent has external continuity when no external coordinate was checked. Sidecar-only agreement remains `NOT_ESTABLISHED`. | | “VSTD-Graph records this lineage graph.†| **Yes.** | The receipt binds the stored artifact nodes, transformation edges, roles, statuses, and declarations. Historical Graph-1 receipts retain the serialized receipt identifier `VSTD-DATA-0.1`. | Valid receipt and structurally valid hypergraph. | The graph contains every real-world input or transformation. | diff --git a/docs/reference.html b/docs/reference.html index 02c9be7..a64118f 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -188,7 +188,7 @@

    vstd artifact

    vstd artifact freeze

    -

    Copy exact file or directory bytes into a guarded artifact bundle.

    +

    Copy exact ordinary file or directory bytes into a new guarded bundle; symbolic-link sources are refused.

    @@ -220,7 +220,7 @@

    vstd artifact verify

    vstd artifact thaw

    -

    Copy a clean sealed parent into a mutable descendant.

    +

    Copy a clean sealed parent into a new, lexically absent mutable descendant.

    ArgumentKindMeaning
    sourcepositional
    bundlepositional
    diff --git a/src/verifier/artifact_control/__init__.py b/src/verifier/artifact_control/__init__.py index 3f20d1f..2721826 100644 --- a/src/verifier/artifact_control/__init__.py +++ b/src/verifier/artifact_control/__init__.py @@ -18,6 +18,7 @@ import base64 import hashlib import json +import os import re import shutil import stat @@ -178,6 +179,52 @@ def _write_json(path: Path, value: Mapping[str, Any]) -> None: ) +def _write_json_exclusive(path: Path, value: Mapping[str, Any]) -> None: + created = False + try: + with path.open("x", encoding="utf-8", newline="\n") as stream: + created = True + stream.write( + json.dumps( + value, ensure_ascii=False, allow_nan=False, indent=2, sort_keys=True + ) + + "\n" + ) + except Exception: + if created: + _remove_created_entry(path) + raise + + +def _absolute_lexical(path: str | Path) -> Path: + """Resolve parent directories but preserve the final filesystem entry.""" + + absolute = Path(os.path.abspath(os.fspath(path))) + return absolute.parent.resolve() / absolute.name + + +def _lexists(path: Path) -> bool: + """Report whether the lexical entry exists, including a dangling symbolic link.""" + + return os.path.lexists(path) + + +def _require_absent(path: Path, label: str) -> None: + if _lexists(path): + raise ArtifactControlError(f"{label} already exists: {path}") + + +def _remove_created_entry(path: Path) -> None: + """Remove one invocation-owned lexical entry without traversing a replacement link.""" + + if path.is_symlink(): + path.unlink() + elif path.is_dir(): + shutil.rmtree(path, ignore_errors=True) + elif _lexists(path): + path.unlink() + + def _relative_posix(path: Path, root: Path) -> str: return path.relative_to(root).as_posix() @@ -478,12 +525,12 @@ def freeze_artifact( ) -> dict[str, Any]: """Preserve exact bytes in a new guarded bundle without creating a seal.""" - source_path = Path(source).resolve() - bundle_path = Path(bundle).resolve() + source_path = _absolute_lexical(source) + bundle_path = _absolute_lexical(bundle) if not isinstance(media_type, str) or not media_type: raise ArtifactControlError("media_type must be a nonempty string") - if bundle_path.exists(): - raise ArtifactControlError(f"freeze bundle already exists: {bundle_path}") + _require_absent(bundle_path, "freeze bundle") + entries = _source_entries(source_path) if source_path.is_dir(): try: bundle_path.relative_to(source_path) @@ -507,8 +554,7 @@ def freeze_artifact( if len(set(lineage)) != len(lineage) or len(set(contexts)) != len(contexts): raise ArtifactControlError("duplicate parent or context bundles do not add assurance") - kind = "file" if source_path.is_file() else "directory" if source_path.is_dir() else "" - entries = _source_entries(source_path) + kind = "file" if source_path.is_file() else "directory" descriptor = _descriptor(kind, media_type, entries) artifact_id = _identity("vstd-artifact-1", _canonical_bytes(descriptor)) content_id = _identity( @@ -801,7 +847,7 @@ def thaw_artifact( """Create a mutable descendant from a cleanly sealed frozen artifact.""" bundle_path = Path(bundle).resolve() - destination_path = Path(destination).resolve() + destination_path = _absolute_lexical(destination) result = verify_frozen_artifact( bundle_path, expected_artifact_id=expected_artifact_id, @@ -812,23 +858,14 @@ def thaw_artifact( raise ArtifactControlError( f"thaw requires a cleanly sealed artifact; observed {result.state}" ) - if destination_path.exists(): - raise ArtifactControlError(f"thaw destination already exists: {destination_path}") + _require_absent(destination_path, "thaw destination") record_path = destination_path.with_name(destination_path.name + ".vstd-thaw.json") - if record_path.exists(): - raise ArtifactControlError(f"thaw record already exists: {record_path}") + _require_absent(record_path, "thaw record") freeze = _load_freeze(bundle_path) payload = bundle_path / "payload" destination_path.parent.mkdir(parents=True, exist_ok=True) - if freeze["artifact_kind"] == "file": - shutil.copyfile(payload, destination_path) - _make_writable(destination_path) - else: - shutil.copytree(payload, destination_path) - for path in destination_path.rglob("*"): - if path.is_file() or path.is_dir(): - _make_writable(path) - _make_writable(destination_path) + destination_created = False + record_created = False record: dict[str, Any] = { "schema_version": THAW_SCHEMA, "parent_artifact_id": result.artifact_id, @@ -840,7 +877,24 @@ def thaw_artifact( } record["thaw_id"] = _identity("vstd-thaw-1", _canonical_bytes(record)) try: - _write_json(record_path, record) + if freeze["artifact_kind"] == "file": + with ( + payload.open("rb") as source_stream, + destination_path.open("xb") as target_stream, + ): + destination_created = True + shutil.copyfileobj(source_stream, target_stream) + _make_writable(destination_path) + else: + destination_path.mkdir() + destination_created = True + shutil.copytree(payload, destination_path, dirs_exist_ok=True) + for path in destination_path.rglob("*"): + if path.is_file() or path.is_dir(): + _make_writable(path) + _make_writable(destination_path) + _write_json_exclusive(record_path, record) + record_created = True status = thawed_artifact_status( destination_path, record_path, @@ -851,12 +905,10 @@ def thaw_artifact( if status["state"] != "THAWED_CLEAN": raise ArtifactControlError("thawed descendant did not match the sealed parent") except Exception: - if record_path.exists(): - record_path.unlink() - if destination_path.is_dir(): - shutil.rmtree(destination_path, ignore_errors=True) - elif destination_path.exists(): - destination_path.unlink() + if record_created: + _remove_created_entry(record_path) + if destination_created: + _remove_created_entry(destination_path) raise return {**record, "record_path": str(record_path)} @@ -878,7 +930,7 @@ def thawed_artifact_status( independently observed. """ - artifact_path = Path(artifact).absolute() + artifact_path = _absolute_lexical(artifact) record_path = ( Path(thaw_record).resolve() if thaw_record is not None diff --git a/src/verifier/runtime/public_cli.py b/src/verifier/runtime/public_cli.py index d68fdfc..275fb9a 100644 --- a/src/verifier/runtime/public_cli.py +++ b/src/verifier/runtime/public_cli.py @@ -285,7 +285,11 @@ def build_parser() -> argparse.ArgumentParser: dest="artifact_command", required=True ) freeze_parser = artifact_commands.add_parser( - "freeze", help="Copy exact file or directory bytes into a guarded artifact bundle." + "freeze", + help=( + "Copy exact ordinary file or directory bytes into a new guarded bundle; " + "symbolic-link sources are refused." + ), ) freeze_parser.add_argument("source") freeze_parser.add_argument("bundle") @@ -315,7 +319,8 @@ def build_parser() -> argparse.ArgumentParser: verify_parser.add_argument("--json", action="store_true") thaw_parser = artifact_commands.add_parser( - "thaw", help="Copy a clean sealed parent into a mutable descendant." + "thaw", + help="Copy a clean sealed parent into a new, lexically absent mutable descendant.", ) thaw_parser.add_argument("bundle") thaw_parser.add_argument("destination") diff --git a/src/verifier/specifications/ARTIFACT_CONTROL.md b/src/verifier/specifications/ARTIFACT_CONTROL.md index 48a2043..1cfbf1f 100644 --- a/src/verifier/specifications/ARTIFACT_CONTROL.md +++ b/src/verifier/specifications/ARTIFACT_CONTROL.md @@ -42,6 +42,13 @@ allocation, and filesystem-specific metadata are outside version 1. The portable read-only guard is an observable tripwire, not an access-control boundary against a privileged writer. +Freeze classifies the caller-supplied final source entry before dereferencing it, so a +symbolic link cannot inherit its target's artifact identity. A new bundle, thaw descendant, +or generated thaw sidecar requires an absent lexical destination: an existing file, +directory, special object, symbolic link, or dangling symbolic link refuses creation. +Exclusive file and sidecar creation narrows replacement races, but version 1 does not claim +universal race-free filesystem security against a concurrent privileged process. + “Portable†means slash-normalized relative path representation. Case sensitivity, Unicode normalization, reserved names, and path-length limits remain properties of the host filesystem; version 1 does not claim that every valid source tree can be materialized @@ -107,7 +114,9 @@ evidence. Thaw requires a cleanly verified seal. It copies the parent payload to a new writable path and emits a `VSTD-ARTIFACT-THAW-1` sidecar beside the descendant. The sidecar records the parent artifact, content, freeze, and seal identifiers. It is lineage metadata, not a -seal. The parent remains unchanged. +seal. The requested descendant and sidecar paths must both be lexically absent; thaw never +uses a preexisting symbolic link as permission to create or label its target. The parent +remains unchanged. A sidecar's self-derived `thaw_id` establishes only internal agreement among its fields. Sidecar-only status is `NOT_ESTABLISHED`, even when current descendant bytes match the diff --git a/standard/ARTIFACT_CONTROL.md b/standard/ARTIFACT_CONTROL.md index 48a2043..1cfbf1f 100644 --- a/standard/ARTIFACT_CONTROL.md +++ b/standard/ARTIFACT_CONTROL.md @@ -42,6 +42,13 @@ allocation, and filesystem-specific metadata are outside version 1. The portable read-only guard is an observable tripwire, not an access-control boundary against a privileged writer. +Freeze classifies the caller-supplied final source entry before dereferencing it, so a +symbolic link cannot inherit its target's artifact identity. A new bundle, thaw descendant, +or generated thaw sidecar requires an absent lexical destination: an existing file, +directory, special object, symbolic link, or dangling symbolic link refuses creation. +Exclusive file and sidecar creation narrows replacement races, but version 1 does not claim +universal race-free filesystem security against a concurrent privileged process. + “Portable†means slash-normalized relative path representation. Case sensitivity, Unicode normalization, reserved names, and path-length limits remain properties of the host filesystem; version 1 does not claim that every valid source tree can be materialized @@ -107,7 +114,9 @@ evidence. Thaw requires a cleanly verified seal. It copies the parent payload to a new writable path and emits a `VSTD-ARTIFACT-THAW-1` sidecar beside the descendant. The sidecar records the parent artifact, content, freeze, and seal identifiers. It is lineage metadata, not a -seal. The parent remains unchanged. +seal. The requested descendant and sidecar paths must both be lexically absent; thaw never +uses a preexisting symbolic link as permission to create or label its target. The parent +remains unchanged. A sidecar's self-derived `thaw_id` establishes only internal agreement among its fields. Sidecar-only status is `NOT_ESTABLISHED`, even when current descendant bytes match the diff --git a/tests/test_artifact_control.py b/tests/test_artifact_control.py index f638f85..44306bb 100644 --- a/tests/test_artifact_control.py +++ b/tests/test_artifact_control.py @@ -8,6 +8,7 @@ import base64 import hashlib import json +import os import stat from pathlib import Path @@ -99,6 +100,13 @@ def _thawed_file(tmp_path: Path, name: str = "thaw") -> tuple[Path, Path, Path]: return bundle, descendant, Path(str(record["record_path"])) +def _symlink_or_skip(link: Path, target: Path, *, target_is_directory: bool = False) -> None: + try: + link.symlink_to(target, target_is_directory=target_is_directory) + except OSError as exc: + pytest.skip(f"symlink creation is unavailable: {exc}") + + def test_freeze_preserves_exact_file_bytes_without_claiming_a_seal(tmp_path: Path) -> None: source = tmp_path / "source.bin" source.write_bytes(b"\x00\r\n\xff") @@ -134,6 +142,67 @@ def test_freeze_preserves_directory_paths_files_and_empty_directories(tmp_path: assert verify_frozen_artifact(bundle, require_seal=False).freeze_valid +@pytest.mark.parametrize("target_kind", ("file", "directory", "dangling")) +def test_freeze_refuses_top_level_source_symlink_without_creating_bundle( + tmp_path: Path, target_kind: str +) -> None: + target = tmp_path / "target" + if target_kind == "file": + target.write_bytes(b"target") + elif target_kind == "directory": + target.mkdir() + (target / "value").write_bytes(b"target") + link = tmp_path / "source-link" + _symlink_or_skip(link, target, target_is_directory=target_kind == "directory") + bundle = tmp_path / "bundle" + + with pytest.raises(ArtifactControlError, match="symbolic links"): + freeze_artifact(link, bundle) + + assert link.is_symlink() + assert not os.path.lexists(bundle) + + +def test_freeze_refuses_nested_symlink_without_leaving_partial_bundle(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "ordinary").write_bytes(b"ordinary") + _symlink_or_skip(source / "nested-link", tmp_path / "absent") + bundle = tmp_path / "bundle" + + with pytest.raises(ArtifactControlError, match="symbolic links"): + freeze_artifact(source, bundle) + + assert not os.path.lexists(bundle) + + +@pytest.mark.parametrize("target_kind", ("file", "directory", "dangling")) +def test_freeze_refuses_symlink_bundle_destination_without_mutating_target( + tmp_path: Path, target_kind: str +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + target = tmp_path / "target" + if target_kind == "file": + target.write_bytes(b"unchanged") + elif target_kind == "directory": + target.mkdir() + (target / "unchanged").write_bytes(b"unchanged") + bundle = tmp_path / "bundle-link" + _symlink_or_skip(bundle, target, target_is_directory=target_kind == "directory") + + with pytest.raises(ArtifactControlError, match="already exists"): + freeze_artifact(source, bundle) + + assert bundle.is_symlink() + if target_kind == "file": + assert target.read_bytes() == b"unchanged" + elif target_kind == "directory": + assert (target / "unchanged").read_bytes() == b"unchanged" + else: + assert not target.exists() + + def test_payload_mutation_and_guard_removal_fail_closed(tmp_path: Path) -> None: bundle, _ = _sealed_file(tmp_path) payload = bundle / "payload" @@ -307,6 +376,266 @@ def test_thaw_is_copy_on_write_and_dirtying_is_observable(tmp_path: Path) -> Non assert record["parent_artifact_id"] == verify_frozen_artifact(bundle).artifact_id +@pytest.mark.parametrize("target_kind", ("file", "directory", "dangling")) +def test_thaw_refuses_symlink_destination_without_mutating_target( + tmp_path: Path, target_kind: str +) -> None: + bundle, _ = _sealed_file(tmp_path, f"destination-{target_kind}") + target = tmp_path / "target" + if target_kind == "file": + target.write_bytes(b"unchanged") + elif target_kind == "directory": + target.mkdir() + (target / "unchanged").write_bytes(b"unchanged") + destination = tmp_path / "descendant-link" + _symlink_or_skip( + destination, target, target_is_directory=target_kind == "directory" + ) + + with pytest.raises(ArtifactControlError, match="already exists"): + thaw_artifact(bundle, destination) + + assert destination.is_symlink() + assert not os.path.lexists(Path(str(destination) + ".vstd-thaw.json")) + assert not os.path.lexists(Path(str(target) + ".vstd-thaw.json")) + if target_kind == "file": + assert target.read_bytes() == b"unchanged" + elif target_kind == "directory": + assert (target / "unchanged").read_bytes() == b"unchanged" + else: + assert not target.exists() + + +@pytest.mark.parametrize("destination_kind", ("file", "directory")) +def test_thaw_refuses_existing_ordinary_destination( + tmp_path: Path, destination_kind: str +) -> None: + bundle, _ = _sealed_file(tmp_path, f"existing-{destination_kind}") + destination = tmp_path / "existing" + if destination_kind == "file": + destination.write_bytes(b"unchanged") + else: + destination.mkdir() + + with pytest.raises(ArtifactControlError, match="already exists"): + thaw_artifact(bundle, destination) + + assert destination.is_file() if destination_kind == "file" else destination.is_dir() + + +@pytest.mark.parametrize("target_kind", ("file", "dangling")) +def test_thaw_refuses_symlink_record_destination_without_mutating_target( + tmp_path: Path, target_kind: str +) -> None: + bundle, _ = _sealed_file(tmp_path, f"record-{target_kind}") + destination = tmp_path / "descendant" + record_path = Path(str(destination) + ".vstd-thaw.json") + target = tmp_path / "record-target" + if target_kind == "file": + target.write_bytes(b"unchanged") + _symlink_or_skip(record_path, target) + + with pytest.raises(ArtifactControlError, match="already exists"): + thaw_artifact(bundle, destination) + + assert not os.path.lexists(destination) + assert record_path.is_symlink() + if target_kind == "file": + assert target.read_bytes() == b"unchanged" + else: + assert not target.exists() + + +def test_thaw_directory_succeeds_and_mutation_becomes_dirty(tmp_path: Path) -> None: + source = tmp_path / "source" + (source / "empty").mkdir(parents=True) + (source / "value").write_bytes(b"value") + bundle = tmp_path / "bundle" + freeze_artifact(source, bundle) + seal_artifact(bundle, _private_key(tmp_path / "key.pem")) + destination = tmp_path / "descendant" + + record = thaw_artifact(bundle, destination) + record_path = Path(str(record["record_path"])) + clean = thawed_artifact_status(destination, record_path, parent_bundle=bundle) + (destination / "value").write_bytes(b"changed") + dirty = thawed_artifact_status(destination, record_path, parent_bundle=bundle) + + assert (destination / "empty").is_dir() + assert clean["state"] == "THAWED_CLEAN" + assert dirty["state"] == "THAWED_DIRTY" + + +def test_failed_thaw_cleanup_unlinks_replacement_symlink_without_touching_target( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + bundle, _ = _sealed_file(tmp_path, "cleanup") + destination = tmp_path / "descendant" + record_path = Path(str(destination) + ".vstd-thaw.json") + replacement_target = tmp_path / "replacement-target" + replacement_target.write_bytes(b"unchanged") + + def fail_after_creation(*args: object, **kwargs: object) -> dict[str, object]: + destination.unlink() + _symlink_or_skip(destination, replacement_target) + raise ArtifactControlError("simulated post-copy refusal") + + monkeypatch.setattr( + artifact_control_module, "thawed_artifact_status", fail_after_creation + ) + + with pytest.raises(ArtifactControlError, match="simulated post-copy refusal"): + thaw_artifact(bundle, destination) + + assert not os.path.lexists(destination) + assert not os.path.lexists(record_path) + assert replacement_target.read_bytes() == b"unchanged" + + +def test_exclusive_sidecar_write_removes_a_partial_file( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + target = tmp_path / "record.json" + original = json.dumps + + def fail_serialization(*args: object, **kwargs: object) -> str: + if kwargs.get("indent") == 2: + raise RuntimeError("simulated serialization failure") + return original(*args, **kwargs) + + monkeypatch.setattr(artifact_control_module.json, "dumps", fail_serialization) + + with pytest.raises(RuntimeError, match="simulated serialization failure"): + artifact_control_module._write_json_exclusive(target, {"value": 1}) + + assert not os.path.lexists(target) + + +def test_exclusive_sidecar_write_never_removes_a_preexisting_file(tmp_path: Path) -> None: + target = tmp_path / "record.json" + target.write_bytes(b"preexisting") + + with pytest.raises(FileExistsError): + artifact_control_module._write_json_exclusive(target, {"value": 1}) + + assert target.read_bytes() == b"preexisting" + + +def test_cleanup_tolerates_an_already_absent_invocation_entry(tmp_path: Path) -> None: + missing = tmp_path / "already-absent" + + artifact_control_module._remove_created_entry(missing) + + assert not os.path.lexists(missing) + + +def test_failed_sidecar_creation_cleans_the_created_descendant( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + bundle, _ = _sealed_file(tmp_path, "sidecar-cleanup") + destination = tmp_path / "descendant" + + def refuse_sidecar(*args: object, **kwargs: object) -> None: + raise ArtifactControlError("simulated sidecar refusal") + + monkeypatch.setattr(artifact_control_module, "_write_json_exclusive", refuse_sidecar) + + with pytest.raises(ArtifactControlError, match="simulated sidecar refusal"): + thaw_artifact(bundle, destination) + + assert not os.path.lexists(destination) + assert not os.path.lexists(Path(str(destination) + ".vstd-thaw.json")) + + +def test_raced_file_destination_is_not_deleted_when_exclusive_creation_refuses_it( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + bundle, _ = _sealed_file(tmp_path, "raced-destination") + destination = tmp_path / "descendant" + original_open = Path.open + + def raced_open(path: Path, mode: str = "r", *args: object, **kwargs: object): + if path == destination and mode == "xb": + path.write_bytes(b"raced") + raise FileExistsError("simulated concurrent destination") + return original_open(path, mode, *args, **kwargs) + + monkeypatch.setattr(Path, "open", raced_open) + + with pytest.raises(FileExistsError, match="concurrent destination"): + thaw_artifact(bundle, destination) + + assert destination.read_bytes() == b"raced" + assert not os.path.lexists(Path(str(destination) + ".vstd-thaw.json")) + + +def test_post_copy_nonclean_status_removes_created_descendant_and_sidecar( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + bundle, _ = _sealed_file(tmp_path, "nonclean-post-copy") + destination = tmp_path / "descendant" + + monkeypatch.setattr( + artifact_control_module, + "thawed_artifact_status", + lambda *args, **kwargs: {"state": "THAWED_DIRTY"}, + ) + + with pytest.raises(ArtifactControlError, match="did not match"): + thaw_artifact(bundle, destination) + + assert not os.path.lexists(destination) + assert not os.path.lexists(Path(str(destination) + ".vstd-thaw.json")) + + +def test_failed_directory_post_check_cleans_only_created_entries( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source" + (source / "empty").mkdir(parents=True) + bundle = tmp_path / "bundle" + freeze_artifact(source, bundle) + seal_artifact(bundle, _private_key(tmp_path / "directory-cleanup.pem")) + destination = tmp_path / "descendant" + + def refuse_status(*args: object, **kwargs: object) -> dict[str, object]: + raise ArtifactControlError("simulated directory post-check refusal") + + monkeypatch.setattr(artifact_control_module, "thawed_artifact_status", refuse_status) + + with pytest.raises(ArtifactControlError, match="post-check refusal"): + thaw_artifact(bundle, destination) + + assert not os.path.lexists(destination) + assert not os.path.lexists(Path(str(destination) + ".vstd-thaw.json")) + + +def test_directory_thaw_rejects_a_link_injected_during_copy( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "value").write_bytes(b"value") + bundle = tmp_path / "bundle" + freeze_artifact(source, bundle) + seal_artifact(bundle, _private_key(tmp_path / "injected-link.pem")) + destination = tmp_path / "descendant" + original_copytree = artifact_control_module.shutil.copytree + + def inject_link(*args: object, **kwargs: object) -> Path: + copied = original_copytree(*args, **kwargs) + _symlink_or_skip(destination / "injected", tmp_path / "absent") + return copied + + monkeypatch.setattr(artifact_control_module.shutil, "copytree", inject_link) + + with pytest.raises(ArtifactControlError, match="did not match"): + thaw_artifact(bundle, destination) + + assert not os.path.lexists(destination) + assert not os.path.lexists(Path(str(destination) + ".vstd-thaw.json")) + + def test_sealed_parent_and_context_bindings_are_explicit_and_deduplicated( tmp_path: Path, ) -> None: From f2c7438ce9169912f1eed3fa33ccfa99f42ffedd Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sun, 30 Aug 2026 15:43:10 -0700 Subject: [PATCH 28/34] docs(time): record linked bundle-member contradiction --- TIME.md | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/TIME.md b/TIME.md index 297804c..5140c16 100644 --- a/TIME.md +++ b/TIME.md @@ -1,6 +1,6 @@ # TIME -Status: CLEAR +Status: OPEN TIME is the live repository-contradiction annunciator. Its status is repository process metadata, not Verifier Standard (VSTD) receipt vocabulary. A live entry belongs here only @@ -13,7 +13,38 @@ escalation, see [`HUMANS.md`](HUMANS.md). ## Live contradictions -None. +### Internal authoritative bundle members can inherit linked external bytes + +At head `6b8fe413c1155f877efb2b557cb3ad51342872f1` and tree +`b67052abe008319e730b7fe2c0cfedfc3614e6f3`, the normative bundle boundary and +runtime behavior disagree: + +- `standard/ARTIFACT_CONTROL.md` requires symbolic links and special filesystem + objects to fail closed. Internal `bundle/freeze.json`, `bundle/seals`, + `bundle/seals/*.json`, `bundle/payload`, and its inventoried descendants are + authoritative bundle members whose lexical final entries must have the required + ordinary object type; they cannot inherit bytes from symbolic-link targets. +- `_load_freeze` and its `_read_json_object` call parse `bundle/freeze.json` after + dereference; `verify_frozen_artifact` separately reads those dereferenced bytes; + `_seal_file_paths` accepts linked regular-file targets; and `seal_artifact` uses + target-following existing-entry handling. + +On Windows Subsystem for Linux 2 (WSL2) over ext4, replacing an otherwise valid +`freeze.json` with a link to its external original produced `SEALED`. Replacing an +otherwise valid seal envelope with a link to its external original also produced +`SEALED`, despite both authoritative bytes residing outside the represented bundle. + +This does not change the accepted read-only alias policy for the caller-supplied outer +parent-bundle path or explicit thaw-record path: those aliases may resolve because the +actual bytes, seals, anchors, and bindings are subsequently verified. The contradiction +is limited to authoritative entries inside the bundle. + +Repair gate: introduce role-specific lexical classification and strict reading for +internal manifests, the seals container, and seal files; preserve payload refusal and +outer read-only aliases; add Linux-effective regressions proving linked or dangling +internal members fail while ordinary freeze, seal, conflict, thaw, and alias behavior +remains intact; then complete the repository validation gates before returning TIME to +`CLEAR`. When a contradiction is open, change the status to `Status: OPEN` and record the exact coordinates, both incompatible claims, evidence for each side, and affected behavior. An From b6d97647a0d3e2b91f0bce8fdf67bf1fb869229d Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Sun, 30 Aug 2026 16:23:29 -0700 Subject: [PATCH 29/34] fix(artifact-control): reject linked internal bundle members --- CHANGELOG.md | 4 + TIME.md | 35 +- docs/API_STABILITY.md | 7 + docs/ARCHITECTURE.md | 6 + docs/CLAIMS_AND_LIMITS.md | 2 +- src/verifier/artifact_control/__init__.py | 220 +++-- .../specifications/ARTIFACT_CONTROL.md | 13 + standard/ARTIFACT_CONTROL.md | 13 + tests/test_artifact_control.py | 776 +++++++++++++++++- 9 files changed, 989 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b0cc3f..6b4b272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,10 @@ symbolic-link sources, and bundle, thaw-descendant, and sidecar outputs refuse every preexisting lexical entry, including dangling symbolic links, without claiming universal race-free filesystem security. +- Require authoritative freeze-manifest, payload, seals-container, and seal-envelope + members to have ordinary lexical types; linked external or in-bundle targets cannot lend + bytes to bundle closure, while verified outer read aliases and ordinary hard-link + byte-and-path semantics remain explicitly distinct. - Remove the live SimulacraBench rehearsal and its front-door promotion; the repository never contained or reproduced the submission, hosted image, hardware, or protected evaluation identified by that name. diff --git a/TIME.md b/TIME.md index 5140c16..297804c 100644 --- a/TIME.md +++ b/TIME.md @@ -1,6 +1,6 @@ # TIME -Status: OPEN +Status: CLEAR TIME is the live repository-contradiction annunciator. Its status is repository process metadata, not Verifier Standard (VSTD) receipt vocabulary. A live entry belongs here only @@ -13,38 +13,7 @@ escalation, see [`HUMANS.md`](HUMANS.md). ## Live contradictions -### Internal authoritative bundle members can inherit linked external bytes - -At head `6b8fe413c1155f877efb2b557cb3ad51342872f1` and tree -`b67052abe008319e730b7fe2c0cfedfc3614e6f3`, the normative bundle boundary and -runtime behavior disagree: - -- `standard/ARTIFACT_CONTROL.md` requires symbolic links and special filesystem - objects to fail closed. Internal `bundle/freeze.json`, `bundle/seals`, - `bundle/seals/*.json`, `bundle/payload`, and its inventoried descendants are - authoritative bundle members whose lexical final entries must have the required - ordinary object type; they cannot inherit bytes from symbolic-link targets. -- `_load_freeze` and its `_read_json_object` call parse `bundle/freeze.json` after - dereference; `verify_frozen_artifact` separately reads those dereferenced bytes; - `_seal_file_paths` accepts linked regular-file targets; and `seal_artifact` uses - target-following existing-entry handling. - -On Windows Subsystem for Linux 2 (WSL2) over ext4, replacing an otherwise valid -`freeze.json` with a link to its external original produced `SEALED`. Replacing an -otherwise valid seal envelope with a link to its external original also produced -`SEALED`, despite both authoritative bytes residing outside the represented bundle. - -This does not change the accepted read-only alias policy for the caller-supplied outer -parent-bundle path or explicit thaw-record path: those aliases may resolve because the -actual bytes, seals, anchors, and bindings are subsequently verified. The contradiction -is limited to authoritative entries inside the bundle. - -Repair gate: introduce role-specific lexical classification and strict reading for -internal manifests, the seals container, and seal files; preserve payload refusal and -outer read-only aliases; add Linux-effective regressions proving linked or dangling -internal members fail while ordinary freeze, seal, conflict, thaw, and alias behavior -remains intact; then complete the repository validation gates before returning TIME to -`CLEAR`. +None. When a contradiction is open, change the status to `Status: OPEN` and record the exact coordinates, both incompatible claims, evidence for each side, and affected behavior. An diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md index 6edeb9d..58cdc34 100644 --- a/docs/API_STABILITY.md +++ b/docs/API_STABILITY.md @@ -28,6 +28,13 @@ require absent lexical destination entries, including refusal of dangling symbol This fail-closed creation contract does not promise universal race-free filesystem security against concurrent privileged replacement. +Verification also requires authoritative internal bundle members to have their ordinary +lexical file or directory type. The freeze manifest, payload, seals container, and seal +envelopes cannot inherit authoritative bytes through symbolic links or supported +reparse-point aliases. This does not change the accepted read-only alias behavior of an +outer parent-bundle or explicit thaw-record argument. Ordinary hard links remain regular +file byte-and-path semantics rather than an exclusive-inode claim. + `thawed_artifact_status` treats a `VSTD-ARTIFACT-THAW-1` sidecar as unkeyed lineage metadata. Without `parent_bundle`, it returns `NOT_ESTABLISHED` even when descendant bytes agree with the sidecar's recorded identifier. `THAWED_CLEAN` or `THAWED_DIRTY` requires an diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3a846a0..c145c5d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -57,6 +57,12 @@ the historical copy operation. Creation paths preserve the caller-supplied final through classification: freeze refuses symbolic-link sources, while bundle, descendant, and sidecar creation require an absent lexical destination. These checks do not claim universal race-free filesystem security against concurrent privileged replacement. +Authoritative members inside a bundle are stricter: the freeze manifest, payload, seals +container, and seal envelopes must have their required ordinary lexical types and cannot +borrow bytes through symbolic links or supported reparse-point aliases. That internal +closure rule does not reject an outer parent-bundle or explicit thaw-record read alias, +whose resolved bytes and bindings are verified. Ordinary hard links remain byte-and-path +objects rather than claims of exclusive inode ownership. ## Verification complex and profile satisfaction diff --git a/docs/CLAIMS_AND_LIMITS.md b/docs/CLAIMS_AND_LIMITS.md index ee0d4dc..084de29 100644 --- a/docs/CLAIMS_AND_LIMITS.md +++ b/docs/CLAIMS_AND_LIMITS.md @@ -40,7 +40,7 @@ checker result—not a claim of universal truth or whole-project conformance. | What can the grounded-certificate kernel establish? | The exact `VSTD4-GDC-1` decision was accepted, rejected, or left `UNKNOWN` under its claim binding and resource bound. | Certificate bytes, formula, grounding, policy/evidence roots, verifier descriptor, and kernel. | Kernel acceptance alone is not VSTD-4 conformance. The evidence-bound path separately reruns every prerequisite/rung mechanism before it may establish 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, and fail-closed final-entry classification for supported creation paths. | 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, and path checks do not establish universal race-free filesystem security. | +| 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 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. | diff --git a/src/verifier/artifact_control/__init__.py b/src/verifier/artifact_control/__init__.py index 2721826..b5c68f8 100644 --- a/src/verifier/artifact_control/__init__.py +++ b/src/verifier/artifact_control/__init__.py @@ -143,7 +143,7 @@ def _strict_object( return value -def _read_json_object(path: Path, label: str) -> dict[str, Any]: +def _parse_json_object(value: str | bytes, label: str, path: Path) -> dict[str, Any]: def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]: result: dict[str, Any] = {} for key, value in pairs: @@ -156,18 +156,26 @@ def reject_constant(value: str) -> None: raise ArtifactControlError(f"{label} contains non-finite number {value}") try: - value = json.loads( - path.read_text(encoding="utf-8"), + parsed = json.loads( + value, object_pairs_hook=reject_duplicates, parse_constant=reject_constant, ) - except OSError as exc: - raise ArtifactControlError(f"cannot read {label}: {path}") from exc - except json.JSONDecodeError as exc: + except (json.JSONDecodeError, UnicodeDecodeError) as exc: raise ArtifactControlError(f"{label} is not readable JSON: {path}") from exc - if not isinstance(value, dict): + if not isinstance(parsed, dict): raise ArtifactControlError(f"{label} must contain one JSON object") - return value + return parsed + + +def _read_json_object(path: Path, label: str) -> dict[str, Any]: + """Read a generic JSON coordinate, retaining accepted read-only alias behavior.""" + + try: + value = path.read_text(encoding="utf-8") + except OSError as exc: + raise ArtifactControlError(f"cannot read {label}: {path}") from exc + return _parse_json_object(value, label, path) def _write_json(path: Path, value: Mapping[str, Any]) -> None: @@ -209,6 +217,63 @@ def _lexists(path: Path) -> bool: return os.path.lexists(path) +def _is_link_like(entry: os.stat_result) -> bool: + """Identify symbolic links and Windows reparse-point aliases from lexical metadata.""" + + if stat.S_ISLNK(entry.st_mode): + return True + reparse_flag = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0) + return bool(getattr(entry, "st_file_attributes", 0) & reparse_flag) + + +def _internal_entry_stat(path: Path, label: str, expected: str) -> os.stat_result: + """Require one authoritative bundle member to have an ordinary lexical type.""" + + try: + entry = path.lstat() + except OSError as exc: + raise ArtifactControlError(f"cannot inspect {label}: {path}") from exc + if _is_link_like(entry): + raise ArtifactControlError(f"{label} must not be a symbolic link or reparse point") + matches = stat.S_ISREG(entry.st_mode) if expected == "file" else stat.S_ISDIR(entry.st_mode) + if not matches: + raise ArtifactControlError(f"{label} must be an ordinary {expected}: {path}") + return entry + + +def _read_internal_regular_bytes(path: Path, label: str) -> bytes: + """Capture one ordinary internal file snapshot without following its final entry.""" + + before = _internal_entry_stat(path, label, "file") + flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_CLOEXEC", 0) + flags |= getattr(os, "O_NOFOLLOW", 0) + try: + descriptor = os.open(path, flags) + except OSError as exc: + raise ArtifactControlError(f"cannot open {label}: {path}") from exc + try: + opened = os.fstat(descriptor) + if not stat.S_ISREG(opened.st_mode) or ( + opened.st_dev, + opened.st_ino, + ) != (before.st_dev, before.st_ino): + raise ArtifactControlError(f"{label} changed during lexical classification") + blocks: list[bytes] = [] + while True: + block = os.read(descriptor, 1024 * 1024) + if not block: + break + blocks.append(block) + return b"".join(blocks) + finally: + os.close(descriptor) + + +def _read_internal_json_object(path: Path, label: str) -> tuple[dict[str, Any], bytes]: + raw = _read_internal_regular_bytes(path, label) + return _parse_json_object(raw, label, path), raw + + def _require_absent(path: Path, label: str) -> None: if _lexists(path): raise ArtifactControlError(f"{label} already exists: {path}") @@ -217,11 +282,22 @@ def _require_absent(path: Path, label: str) -> None: def _remove_created_entry(path: Path) -> None: """Remove one invocation-owned lexical entry without traversing a replacement link.""" - if path.is_symlink(): - path.unlink() - elif path.is_dir(): + try: + entry = path.lstat() + except FileNotFoundError: + return + if _is_link_like(entry): + if stat.S_ISDIR(entry.st_mode) and not stat.S_ISLNK(entry.st_mode): + path.rmdir() + else: + path.unlink() + elif stat.S_ISDIR(entry.st_mode): shutil.rmtree(path, ignore_errors=True) - elif _lexists(path): + else: + try: + path.chmod(entry.st_mode | stat.S_IWUSR) + except OSError: + pass path.unlink() @@ -230,24 +306,37 @@ def _relative_posix(path: Path, root: Path) -> str: def _source_entries(source: Path) -> list[dict[str, Any]]: - if source.is_symlink(): - raise ArtifactControlError("symbolic links are not accepted as frozen artifacts") - if source.is_file(): + try: + source_entry = source.lstat() + except OSError as exc: + raise ArtifactControlError(f"cannot inspect artifact source: {source}") from exc + if _is_link_like(source_entry): + raise ArtifactControlError( + "symbolic links or reparse-point aliases are not accepted as frozen artifacts" + ) + if stat.S_ISREG(source_entry.st_mode): digests, size = _digests_file(source) return [{"kind": "file", "path": ".", "byte_size": size, "digests": digests}] - if not source.is_dir(): + if not stat.S_ISDIR(source_entry.st_mode): raise ArtifactControlError("artifact source must be a regular file or directory") entries: list[dict[str, Any]] = [] for path in sorted(source.rglob("*"), key=lambda item: item.relative_to(source).as_posix()): - if path.is_symlink(): + try: + entry = path.lstat() + except OSError as exc: raise ArtifactControlError( - f"symbolic links are not accepted as frozen artifacts: {_relative_posix(path, source)}" + f"cannot inspect frozen artifact entry: {_relative_posix(path, source)}" + ) from exc + if _is_link_like(entry): + raise ArtifactControlError( + "symbolic links or reparse-point aliases are not accepted as frozen artifacts: " + f"{_relative_posix(path, source)}" ) relative = _relative_posix(path, source) - if path.is_dir(): + if stat.S_ISDIR(entry.st_mode): entries.append({"kind": "directory", "path": relative}) - elif path.is_file(): + elif stat.S_ISREG(entry.st_mode): digests, size = _digests_file(path) entries.append( { @@ -288,7 +377,7 @@ def _make_writable(path: Path) -> None: def _is_read_only(path: Path) -> bool: - return not bool(path.stat().st_mode & (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)) + return not bool(path.lstat().st_mode & (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)) def _set_bundle_guard(bundle: Path) -> None: @@ -371,8 +460,10 @@ def _validate_entries(entries: Any) -> list[dict[str, Any]]: return validated -def _load_freeze(bundle: Path) -> dict[str, Any]: - freeze = _read_json_object(bundle / "freeze.json", "freeze manifest") +def _load_freeze_snapshot(bundle: Path) -> tuple[dict[str, Any], bytes]: + freeze, freeze_bytes = _read_internal_json_object( + bundle / "freeze.json", "freeze manifest" + ) _strict_object( freeze, { @@ -437,7 +528,11 @@ def _load_freeze(bundle: Path) -> dict[str, Any]: ) if mechanism != _MECHANISM: raise ArtifactControlError("freeze.mechanism is unsupported") - return freeze + return freeze, freeze_bytes + + +def _load_freeze(bundle: Path) -> dict[str, Any]: + return _load_freeze_snapshot(bundle)[0] def _stable_freeze(freeze: Mapping[str, Any]) -> dict[str, Any]: @@ -447,11 +542,9 @@ def _stable_freeze(freeze: Mapping[str, Any]) -> dict[str, Any]: def _observed_bundle_entries(bundle: Path, kind: str) -> list[dict[str, Any]]: payload = bundle / "payload" if kind == "file": - if not payload.is_file() or payload.is_symlink(): - raise ArtifactControlError("frozen file payload is missing or not a regular file") + _internal_entry_stat(payload, "frozen file payload", "file") else: - if not payload.is_dir() or payload.is_symlink(): - raise ArtifactControlError("frozen directory payload is missing or not a directory") + _internal_entry_stat(payload, "frozen directory payload", "directory") return _source_entries(payload) @@ -505,14 +598,15 @@ def _seal_payload( def _seal_file_paths(bundle: Path) -> list[Path]: seals = bundle / "seals" - if not seals.exists(): + if not _lexists(seals): return [] - if not seals.is_dir() or seals.is_symlink(): - raise ArtifactControlError("seals must be a regular directory") - unexpected = [path for path in seals.iterdir() if not path.is_file() or path.suffix != ".json"] - if unexpected: - raise ArtifactControlError("seals directory contains an unsupported entry") - return sorted(seals.glob("*.json"), key=lambda path: path.name) + _internal_entry_stat(seals, "seals container", "directory") + paths = sorted(seals.iterdir(), key=lambda path: path.name) + for path in paths: + if path.suffix != ".json": + raise ArtifactControlError("seals directory contains an unsupported entry") + _internal_entry_stat(path, f"seal member {path.name}", "file") + return paths def freeze_artifact( @@ -608,8 +702,7 @@ def seal_artifact(bundle: str | Path, private_key: str | Path) -> dict[str, Any] verification = verify_frozen_artifact(bundle_path, require_seal=False) if verification.state not in {"FROZEN_UNSEALED", "SEALED"}: raise ArtifactControlError("artifact must be cleanly frozen before it can be sealed") - freeze = _load_freeze(bundle_path) - freeze_bytes = (bundle_path / "freeze.json").read_bytes() + freeze, freeze_bytes = _load_freeze_snapshot(bundle_path) _, serialization, Ed25519PrivateKey, _ = _seal_dependencies() key_bytes = Path(private_key).read_bytes() @@ -639,21 +732,45 @@ def seal_artifact(bundle: str | Path, private_key: str | Path) -> dict[str, Any] ) seals = bundle_path / "seals" - seals.mkdir(exist_ok=True) + seals_created = False + if _lexists(seals): + _internal_entry_stat(seals, "seals container", "directory") + else: + try: + seals.mkdir() + seals_created = True + except FileExistsError: + _internal_entry_stat(seals, "seals container", "directory") filename = hashlib.sha256(_canonical_bytes(envelope)).hexdigest() + ".json" target = seals / filename - if target.exists(): - if _read_json_object(target, "seal") != envelope: - raise ArtifactControlError("existing seal filename contains different bytes") + target_created = False + try: + if _lexists(target): + existing, _ = _read_internal_json_object(target, "seal") + if existing != envelope: + raise ArtifactControlError("existing seal filename contains different bytes") + return envelope + _write_json_exclusive(target, envelope) + target_created = True + _make_read_only(target) + final = verify_frozen_artifact(bundle_path, require_seal=True) + if not final.sealed or envelope["seal_id"] not in final.valid_seal_ids: + raise ArtifactControlError( + f"seal did not produce a cleanly sealed artifact; observed {final.state}" + ) return envelope - _write_json(target, envelope) - _make_read_only(target) - final = verify_frozen_artifact(bundle_path, require_seal=True) - if not final.sealed or envelope["seal_id"] not in final.valid_seal_ids: - raise ArtifactControlError( - f"seal did not produce a cleanly sealed artifact; observed {final.state}" - ) - return envelope + except Exception: + if target_created: + _remove_created_entry(target) + if seals_created: + try: + if _is_link_like(seals.lstat()): + _remove_created_entry(seals) + else: + seals.rmdir() + except OSError: + pass + raise def _verify_seal( @@ -735,7 +852,7 @@ def verify_frozen_artifact( "artifact bundle contains unsupported top-level entries: " + ", ".join(unexpected) ) - freeze = _load_freeze(bundle_path) + freeze, freeze_bytes = _load_freeze_snapshot(bundle_path) artifact_id = freeze["artifact_id"] content_id = freeze["content_id"] freeze_id = freeze["freeze_id"] @@ -760,10 +877,9 @@ def verify_frozen_artifact( freeze_valid = not freeze_errors guard_errors.extend(_guard_errors(bundle_path)) guard_valid = not guard_errors - freeze_bytes = (bundle_path / "freeze.json").read_bytes() for seal_path in _seal_file_paths(bundle_path): try: - seal = _read_json_object(seal_path, "seal") + seal, _ = _read_internal_json_object(seal_path, "seal") seal_id, key_id = _verify_seal(seal, freeze, freeze_bytes) valid_seals.append(seal_id) key_ids.append(key_id) diff --git a/src/verifier/specifications/ARTIFACT_CONTROL.md b/src/verifier/specifications/ARTIFACT_CONTROL.md index 1cfbf1f..772455a 100644 --- a/src/verifier/specifications/ARTIFACT_CONTROL.md +++ b/src/verifier/specifications/ARTIFACT_CONTROL.md @@ -49,6 +49,19 @@ directory, special object, symbolic link, or dangling symbolic link refuses crea Exclusive file and sidecar creation narrows replacement races, but version 1 does not claim universal race-free filesystem security against a concurrent privileged process. +Authoritative entries inside a bundle have a stricter role-specific boundary. +`freeze.json` and every `seals/*.json` member must be lexical ordinary files; `seals` and a +directory payload must be lexical ordinary directories; and a file payload must be a +lexical ordinary file. An internal symbolic link or supported reparse-point alias fails +structurally even when its target is byte-identical, correctly signed, read-only, or inside +the same bundle. Manifest parsing and closure use one captured ordinary-file byte snapshot. +This differs from a caller-supplied outer parent-bundle path or explicit thaw-record path, +which may be a read-only alias because the resolved bytes and every applicable seal, anchor, +and binding are subsequently verified. Version 1 identifies accepted ordinary files by +portable path and bytes, not exclusive inode ownership; hard-link identity remains outside +its claims. Mount, network-filesystem, and concurrent-replacement behavior remains bounded +by the host and does not establish universal alias resistance. + “Portable†means slash-normalized relative path representation. Case sensitivity, Unicode normalization, reserved names, and path-length limits remain properties of the host filesystem; version 1 does not claim that every valid source tree can be materialized diff --git a/standard/ARTIFACT_CONTROL.md b/standard/ARTIFACT_CONTROL.md index 1cfbf1f..772455a 100644 --- a/standard/ARTIFACT_CONTROL.md +++ b/standard/ARTIFACT_CONTROL.md @@ -49,6 +49,19 @@ directory, special object, symbolic link, or dangling symbolic link refuses crea Exclusive file and sidecar creation narrows replacement races, but version 1 does not claim universal race-free filesystem security against a concurrent privileged process. +Authoritative entries inside a bundle have a stricter role-specific boundary. +`freeze.json` and every `seals/*.json` member must be lexical ordinary files; `seals` and a +directory payload must be lexical ordinary directories; and a file payload must be a +lexical ordinary file. An internal symbolic link or supported reparse-point alias fails +structurally even when its target is byte-identical, correctly signed, read-only, or inside +the same bundle. Manifest parsing and closure use one captured ordinary-file byte snapshot. +This differs from a caller-supplied outer parent-bundle path or explicit thaw-record path, +which may be a read-only alias because the resolved bytes and every applicable seal, anchor, +and binding are subsequently verified. Version 1 identifies accepted ordinary files by +portable path and bytes, not exclusive inode ownership; hard-link identity remains outside +its claims. Mount, network-filesystem, and concurrent-replacement behavior remains bounded +by the host and does not establish universal alias resistance. + “Portable†means slash-normalized relative path representation. Case sensitivity, Unicode normalization, reserved names, and path-length limits remain properties of the host filesystem; version 1 does not claim that every valid source tree can be materialized diff --git a/tests/test_artifact_control.py b/tests/test_artifact_control.py index 44306bb..0ee4d5c 100644 --- a/tests/test_artifact_control.py +++ b/tests/test_artifact_control.py @@ -1,6 +1,7 @@ """Adversarial tests for exact-byte freezing and finite self-closing seals. -Terminology: Privacy-Enhanced Mail (PEM); Verifier Standard (VSTD). +Terminology: JavaScript Object Notation (JSON); Privacy-Enhanced Mail (PEM); +Verifier Standard (VSTD). """ from __future__ import annotations @@ -11,6 +12,7 @@ import os import stat from pathlib import Path +from types import SimpleNamespace import pytest @@ -107,6 +109,15 @@ def _symlink_or_skip(link: Path, target: Path, *, target_is_directory: bool = Fa pytest.skip(f"symlink creation is unavailable: {exc}") +def _fifo_or_skip(path: Path) -> None: + if not hasattr(os, "mkfifo"): + pytest.skip("first-in, first-out special objects are unavailable") + try: + os.mkfifo(path) + except OSError as exc: + pytest.skip(f"first-in, first-out special-object creation is unavailable: {exc}") + + def test_freeze_preserves_exact_file_bytes_without_claiming_a_seal(tmp_path: Path) -> None: source = tmp_path / "source.bin" source.write_bytes(b"\x00\r\n\xff") @@ -203,6 +214,171 @@ def test_freeze_refuses_symlink_bundle_destination_without_mutating_target( assert not target.exists() +def test_linked_external_freeze_manifest_is_structural_failure(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path, "linked-external-freeze") + freeze_path = bundle / "freeze.json" + external = tmp_path / "external-freeze.json" + _writable(freeze_path) + freeze_path.replace(external) + external.chmod(external.stat().st_mode & ~stat.S_IWUSR) + _symlink_or_skip(freeze_path, external) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert freeze_path.is_symlink() + assert not bool(external.stat().st_mode & stat.S_IWUSR) + assert any("freeze manifest must not" in error for error in result.errors) + + +def test_linked_in_bundle_freeze_manifest_fails_even_with_identical_bytes( + tmp_path: Path, +) -> None: + bundle, _ = _sealed_file(tmp_path, "linked-in-bundle-freeze") + freeze_path = bundle / "freeze.json" + target = bundle / "seals" / "freeze-manifest-copy" + _writable(freeze_path) + freeze_path.replace(target) + target.chmod(target.stat().st_mode & ~stat.S_IWUSR) + _symlink_or_skip(freeze_path, target) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("freeze manifest must not" in error for error in result.errors) + + +def test_dangling_freeze_manifest_link_is_structural_failure(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path, "dangling-freeze") + freeze_path = bundle / "freeze.json" + _writable(freeze_path) + freeze_path.unlink() + _symlink_or_skip(freeze_path, tmp_path / "absent-freeze.json") + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("freeze manifest must not" in error for error in result.errors) + + +@pytest.mark.parametrize("replacement", ("directory", "fifo")) +def test_nonregular_freeze_manifest_is_structural_failure( + tmp_path: Path, replacement: str +) -> None: + bundle, _ = _sealed_file(tmp_path, f"nonregular-freeze-{replacement}") + freeze_path = bundle / "freeze.json" + _writable(freeze_path) + freeze_path.unlink() + if replacement == "directory": + freeze_path.mkdir() + else: + _fifo_or_skip(freeze_path) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("freeze manifest must be an ordinary file" in error for error in result.errors) + + +def test_missing_and_nonobject_freeze_manifests_fail_closed(tmp_path: Path) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + missing_bundle = tmp_path / "missing-bundle" + freeze_artifact(source, missing_bundle) + freeze_path = missing_bundle / "freeze.json" + _writable(freeze_path) + freeze_path.unlink() + assert verify_frozen_artifact(missing_bundle, require_seal=False).state == "FAIL" + + nonobject_bundle = tmp_path / "nonobject-bundle" + freeze_artifact(source, nonobject_bundle) + freeze_path = nonobject_bundle / "freeze.json" + _writable(freeze_path) + freeze_path.write_text("[]\n", encoding="utf-8") + result = verify_frozen_artifact(nonobject_bundle, require_seal=False) + assert result.state == "FAIL" + assert any("one JSON object" in error for error in result.errors) + + +def test_internal_snapshot_fails_if_open_or_identity_changes_after_classification( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + freeze_artifact(source, bundle) + freeze_path = bundle / "freeze.json" + original_open = artifact_control_module.os.open + + def refuse_open(path: object, flags: int) -> int: + if Path(path) == freeze_path: + raise OSError("simulated no-follow refusal") + return original_open(path, flags) + + monkeypatch.setattr(artifact_control_module.os, "open", refuse_open) + assert verify_frozen_artifact(bundle, require_seal=False).state == "FAIL" + monkeypatch.setattr(artifact_control_module.os, "open", original_open) + + original_fstat = artifact_control_module.os.fstat + + def changed_fstat(descriptor: int) -> SimpleNamespace: + observed = original_fstat(descriptor) + return SimpleNamespace( + st_mode=observed.st_mode, + st_dev=observed.st_dev, + st_ino=observed.st_ino + 1, + ) + + monkeypatch.setattr(artifact_control_module.os, "fstat", changed_fstat) + result = verify_frozen_artifact(bundle, require_seal=False) + assert result.state == "FAIL" + assert any("changed during lexical classification" in error for error in result.errors) + + +def test_missing_generic_json_alias_reports_read_failure(tmp_path: Path) -> None: + with pytest.raises(ArtifactControlError, match="cannot read"): + artifact_control_module._read_json_object(tmp_path / "absent.json", "record") + + +def test_missing_and_special_sources_fail_without_creating_bundle(tmp_path: Path) -> None: + missing_bundle = tmp_path / "missing-bundle" + with pytest.raises(ArtifactControlError, match="cannot inspect artifact source"): + freeze_artifact(tmp_path / "absent", missing_bundle) + assert not os.path.lexists(missing_bundle) + + special = tmp_path / "special" + _fifo_or_skip(special) + special_bundle = tmp_path / "special-bundle" + with pytest.raises(ArtifactControlError, match="regular file or directory"): + freeze_artifact(special, special_bundle) + assert not os.path.lexists(special_bundle) + + +def test_nested_special_or_uninspectable_source_entry_fails_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source" + source.mkdir() + special = source / "special" + _fifo_or_skip(special) + with pytest.raises(ArtifactControlError, match="special filesystem object"): + freeze_artifact(source, tmp_path / "special-bundle") + + special.unlink() + ordinary = source / "ordinary" + ordinary.write_bytes(b"ordinary") + original_lstat = Path.lstat + + def refuse_entry(path: Path): + if path == ordinary: + raise OSError("simulated entry race") + return original_lstat(path) + + monkeypatch.setattr(Path, "lstat", refuse_entry) + with pytest.raises(ArtifactControlError, match="cannot inspect frozen artifact entry"): + freeze_artifact(source, tmp_path / "uninspectable-bundle") + + def test_payload_mutation_and_guard_removal_fail_closed(tmp_path: Path) -> None: bundle, _ = _sealed_file(tmp_path) payload = bundle / "payload" @@ -217,6 +393,21 @@ def test_payload_mutation_and_guard_removal_fail_closed(tmp_path: Path) -> None: assert any("payload" in error or "inventory" in error for error in result.errors) +def test_linked_payload_member_remains_structural_failure(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path, "linked-payload") + payload = bundle / "payload" + external = tmp_path / "external-payload" + _writable(payload) + payload.replace(external) + external.chmod(external.stat().st_mode & ~stat.S_IWUSR) + _symlink_or_skip(payload, external) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("payload must not" in error for error in result.errors) + + def test_directory_addition_requires_breaking_the_tree_guard_and_fails(tmp_path: Path) -> None: source = tmp_path / "source" source.mkdir() @@ -346,6 +537,527 @@ def test_valid_and_invalid_seals_remain_conflicted(tmp_path: Path) -> None: seal_artifact(bundle, _private_key(tmp_path / "other.pem")) +@pytest.mark.parametrize("target_location", ("external", "internal")) +def test_linked_seals_container_is_structural_failure( + tmp_path: Path, target_location: str +) -> None: + if target_location == "external": + bundle, _ = _sealed_file(tmp_path, "external-seals-container") + target = tmp_path / "external-seals" + else: + source = tmp_path / "directory-source" + (source / "seal-store").mkdir(parents=True) + (source / "value").write_bytes(b"value") + bundle = tmp_path / "internal-seals-container" + freeze_artifact(source, bundle) + seal_artifact(bundle, _private_key(tmp_path / "internal-seals.pem")) + target = bundle / "payload" / "seal-store" + _writable(bundle / "payload") + _writable(target) + target.rmdir() + seals = bundle / "seals" + if target_location == "external": + seals.replace(target) + else: + target.mkdir() + for seal_path in seals.iterdir(): + seal_path.replace(target / seal_path.name) + seals.rmdir() + _symlink_or_skip(seals, target, target_is_directory=True) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("seals container must not" in error for error in result.errors) + + +def test_dangling_seals_container_link_is_structural_failure(tmp_path: Path) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "dangling-seals-container" + freeze_artifact(source, bundle) + _symlink_or_skip(bundle / "seals", tmp_path / "absent-seals", target_is_directory=True) + + result = verify_frozen_artifact(bundle, require_seal=False) + + assert result.state == "FAIL" + assert any("seals container must not" in error for error in result.errors) + + +@pytest.mark.parametrize("replacement", ("file", "fifo")) +def test_non_directory_seals_container_is_structural_failure( + tmp_path: Path, replacement: str +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / f"non-directory-seals-{replacement}" + freeze_artifact(source, bundle) + seals = bundle / "seals" + if replacement == "file": + seals.write_bytes(b"not a directory") + else: + _fifo_or_skip(seals) + + result = verify_frozen_artifact(bundle, require_seal=False) + + assert result.state == "FAIL" + assert any("seals container must be an ordinary directory" in error for error in result.errors) + + +def test_linked_external_seal_member_is_structural_failure(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path, "external-seal-member") + seal_path = _seal_path(bundle) + external = tmp_path / "external-seal.json" + _writable(seal_path) + seal_path.replace(external) + external.chmod(external.stat().st_mode & ~stat.S_IWUSR) + _symlink_or_skip(seal_path, external) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert seal_path.is_symlink() + assert not bool(external.stat().st_mode & stat.S_IWUSR) + assert any("seal member" in error and "must not" in error for error in result.errors) + + +def test_linked_internal_identical_seal_member_is_structural_failure(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path, "internal-seal-member") + seal_path = _seal_path(bundle) + target = seal_path.with_name("identical-target.json") + target.write_bytes(seal_path.read_bytes()) + target.chmod(target.stat().st_mode & ~stat.S_IWUSR) + _writable(seal_path) + seal_path.unlink() + _symlink_or_skip(seal_path, target) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("seal member" in error and "must not" in error for error in result.errors) + + +def test_dangling_seal_member_link_is_structural_failure(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path, "dangling-seal-member") + seal_path = _seal_path(bundle) + _writable(seal_path) + seal_path.unlink() + _symlink_or_skip(seal_path, tmp_path / "absent-seal.json") + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("seal member" in error and "must not" in error for error in result.errors) + + +@pytest.mark.parametrize("replacement", ("directory", "fifo")) +def test_nonregular_seal_member_is_structural_failure( + tmp_path: Path, replacement: str +) -> None: + bundle, _ = _sealed_file(tmp_path, f"nonregular-seal-{replacement}") + seal_path = _seal_path(bundle) + _writable(seal_path) + seal_path.unlink() + if replacement == "directory": + seal_path.mkdir() + else: + _fifo_or_skip(seal_path) + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("seal member" in error for error in result.errors) + + +def test_unexpected_non_json_seal_member_is_structural_failure(tmp_path: Path) -> None: + bundle, _ = _sealed_file(tmp_path, "unexpected-seal-member") + (bundle / "seals" / "unexpected.txt").write_bytes(b"unexpected") + + result = verify_frozen_artifact(bundle) + + assert result.state == "FAIL" + assert any("unsupported entry" in error for error in result.errors) + + +def test_absent_seals_container_preserves_zero_seal_semantics(tmp_path: Path) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "unsealed" + freeze_artifact(source, bundle) + + assert not os.path.lexists(bundle / "seals") + assert verify_frozen_artifact(bundle, require_seal=False).state == "FROZEN_UNSEALED" + assert verify_frozen_artifact(bundle, require_seal=True).state == "NOT_ESTABLISHED" + + +def test_seal_creation_refuses_existing_linked_valid_target_without_mutating_it( + tmp_path: Path, +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + seal_artifact(bundle, key) + target = _seal_path(bundle) + external = tmp_path / "external-envelope.json" + _writable(target) + target.replace(external) + external.chmod(external.stat().st_mode & ~stat.S_IWUSR) + before = external.read_bytes() + _symlink_or_skip(target, external) + + with pytest.raises(ArtifactControlError, match="cleanly frozen"): + seal_artifact(bundle, key) + + assert target.is_symlink() + assert external.read_bytes() == before + + +@pytest.mark.parametrize("target_state", ("dangling", "different")) +def test_seal_creation_refuses_nonordinary_or_different_deterministic_target( + tmp_path: Path, target_state: str +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"same source") + key = _private_key(tmp_path / "key.pem") + probe = tmp_path / "probe" + freeze_artifact(source, probe) + seal_artifact(probe, key) + filename = _seal_path(probe).name + + bundle = tmp_path / "bundle" + freeze_artifact(source, bundle) + seals = bundle / "seals" + seals.mkdir() + target = seals / filename + if target_state == "dangling": + external = tmp_path / "absent-envelope.json" + _symlink_or_skip(target, external) + else: + target.write_bytes(b"{}") + + with pytest.raises(ArtifactControlError, match="cleanly frozen"): + seal_artifact(bundle, key) + + if target_state == "dangling": + assert target.is_symlink() + assert not external.exists() + else: + assert target.read_bytes() == b"{}" + + +@pytest.mark.parametrize("target_state", ("directory", "dangling")) +def test_seal_creation_refuses_linked_seals_container_without_writing_through_it( + tmp_path: Path, target_state: str +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + target = tmp_path / "seals-target" + if target_state == "directory": + target.mkdir() + seals = bundle / "seals" + _symlink_or_skip(seals, target, target_is_directory=True) + + with pytest.raises(ArtifactControlError, match="cleanly frozen"): + seal_artifact(bundle, key) + + assert seals.is_symlink() + if target_state == "directory": + assert list(target.iterdir()) == [] + else: + assert not target.exists() + + +def test_seal_creation_deduplicates_only_an_ordinary_identical_envelope( + tmp_path: Path, +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + + first = seal_artifact(bundle, key) + path = _seal_path(bundle) + before = path.read_bytes() + second = seal_artifact(bundle, key) + + assert second == first + assert path.read_bytes() == before + assert not path.is_symlink() + + +def test_seal_creation_recovers_from_directory_creation_race( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + seals = bundle / "seals" + original_mkdir = Path.mkdir + + def raced_mkdir(path: Path, *args: object, **kwargs: object) -> None: + if path == seals: + original_mkdir(path) + raise FileExistsError("simulated directory race") + original_mkdir(path, *args, **kwargs) + + monkeypatch.setattr(Path, "mkdir", raced_mkdir) + + envelope = seal_artifact(bundle, key) + + assert verify_frozen_artifact(bundle).state == "SEALED" + assert envelope["seal_id"] in verify_frozen_artifact(bundle).valid_seal_ids + + +def test_seal_creation_compares_existing_ordinary_target_before_reuse( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"same source") + key = _private_key(tmp_path / "key.pem") + probe = tmp_path / "probe" + freeze_artifact(source, probe) + seal_artifact(probe, key) + filename = _seal_path(probe).name + + bundle = tmp_path / "bundle" + freeze_artifact(source, bundle) + seals = bundle / "seals" + seals.mkdir() + target = seals / filename + target.write_text("{}\n", encoding="utf-8") + original_verify = artifact_control_module.verify_frozen_artifact + first = True + + def allow_initial_inspection(*args: object, **kwargs: object): + nonlocal first + if first: + first = False + return artifact_control_module.ArtifactVerification( + state="FROZEN_UNSEALED", + artifact_id=None, + content_id=None, + freeze_id=None, + freeze_valid=True, + guard_valid=True, + valid_seal_ids=(), + key_ids=(), + external_anchor="NOT_CHECKED", + errors=(), + warnings=(), + ) + return original_verify(*args, **kwargs) + + monkeypatch.setattr( + artifact_control_module, "verify_frozen_artifact", allow_initial_inspection + ) + + with pytest.raises(ArtifactControlError, match="different bytes"): + seal_artifact(bundle, key) + + assert target.read_text(encoding="utf-8") == "{}\n" + + +def test_seal_creation_failure_before_target_ownership_removes_created_container( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + + def refuse_write(*args: object, **kwargs: object) -> None: + raise ArtifactControlError("simulated exclusive-write refusal") + + monkeypatch.setattr(artifact_control_module, "_write_json_exclusive", refuse_write) + + with pytest.raises(ArtifactControlError, match="exclusive-write refusal"): + seal_artifact(bundle, key) + + assert not os.path.lexists(bundle / "seals") + + +def test_failed_seal_creation_removes_only_created_target_and_empty_container( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + original_verify = artifact_control_module.verify_frozen_artifact + calls = 0 + + def fail_final_verification(*args: object, **kwargs: object): + nonlocal calls + calls += 1 + if calls == 1: + return original_verify(*args, **kwargs) + return artifact_control_module.ArtifactVerification( + state="FAIL", + artifact_id=None, + content_id=None, + freeze_id=None, + freeze_valid=False, + guard_valid=False, + valid_seal_ids=(), + key_ids=(), + external_anchor="NOT_CHECKED", + errors=("simulated final failure",), + warnings=(), + ) + + monkeypatch.setattr( + artifact_control_module, "verify_frozen_artifact", fail_final_verification + ) + + with pytest.raises(ArtifactControlError, match="did not produce"): + seal_artifact(bundle, key) + + assert not os.path.lexists(bundle / "seals") + + +def test_seal_cleanup_unlinks_replacement_link_without_touching_target( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + external = tmp_path / "replacement-target" + external.write_bytes(b"unchanged") + original_make_read_only = artifact_control_module._make_read_only + + def replace_then_fail(path: Path) -> None: + if path.parent.name == "seals": + path.unlink() + _symlink_or_skip(path, external) + raise ArtifactControlError("simulated seal cleanup") + original_make_read_only(path) + + monkeypatch.setattr(artifact_control_module, "_make_read_only", replace_then_fail) + + with pytest.raises(ArtifactControlError, match="simulated seal cleanup"): + seal_artifact(bundle, key) + + assert external.read_bytes() == b"unchanged" + assert not os.path.lexists(bundle / "seals") + + +def test_seal_cleanup_unlinks_replacement_container_alias_without_traversal( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + external = tmp_path / "replacement-container" + external.mkdir() + marker = external / "unchanged" + marker.write_bytes(b"unchanged") + original_make_read_only = artifact_control_module._make_read_only + + def replace_container_then_fail(path: Path) -> None: + if path.parent.name == "seals": + path.unlink() + path.parent.rmdir() + _symlink_or_skip(path.parent, external, target_is_directory=True) + raise ArtifactControlError("simulated container replacement") + original_make_read_only(path) + + monkeypatch.setattr( + artifact_control_module, "_make_read_only", replace_container_then_fail + ) + + with pytest.raises(ArtifactControlError, match="container replacement"): + seal_artifact(bundle, key) + + assert marker.read_bytes() == b"unchanged" + assert not os.path.lexists(bundle / "seals") + + +def test_cleanup_handles_reparse_directory_branch_and_chmod_refusal( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + directory = tmp_path / "junction-shaped" + directory.mkdir() + monkeypatch.setattr(artifact_control_module, "_is_link_like", lambda entry: True) + artifact_control_module._remove_created_entry(directory) + assert not directory.exists() + + monkeypatch.undo() + file_path = tmp_path / "ordinary" + file_path.write_bytes(b"ordinary") + original_chmod = Path.chmod + + def refuse_chmod(path: Path, mode: int) -> None: + if path == file_path: + raise OSError("simulated chmod refusal") + original_chmod(path, mode) + + monkeypatch.setattr(Path, "chmod", refuse_chmod) + artifact_control_module._remove_created_entry(file_path) + assert not file_path.exists() + + +def test_seal_cleanup_preserves_precise_failure_when_empty_container_cannot_be_removed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"source") + bundle = tmp_path / "bundle" + key = _private_key(tmp_path / "key.pem") + freeze_artifact(source, bundle) + original_verify = artifact_control_module.verify_frozen_artifact + original_rmdir = Path.rmdir + calls = 0 + + def fail_final_verification(*args: object, **kwargs: object): + nonlocal calls + calls += 1 + if calls == 1: + return original_verify(*args, **kwargs) + return artifact_control_module.ArtifactVerification( + state="FAIL", + artifact_id=None, + content_id=None, + freeze_id=None, + freeze_valid=False, + guard_valid=False, + valid_seal_ids=(), + key_ids=(), + external_anchor="NOT_CHECKED", + errors=("simulated final failure",), + warnings=(), + ) + + def refuse_seals_rmdir(path: Path) -> None: + if path == bundle / "seals": + raise OSError("simulated inaccessible cleanup path") + original_rmdir(path) + + monkeypatch.setattr( + artifact_control_module, "verify_frozen_artifact", fail_final_verification + ) + monkeypatch.setattr(Path, "rmdir", refuse_seals_rmdir) + + with pytest.raises(ArtifactControlError, match="did not produce"): + seal_artifact(bundle, key) + + assert (bundle / "seals").is_dir() + assert list((bundle / "seals").iterdir()) == [] + + def test_thaw_is_copy_on_write_and_dirtying_is_observable(tmp_path: Path) -> None: unsealed_source = tmp_path / "unsealed.bin" unsealed_source.write_bytes(b"unsealed") @@ -1012,6 +1724,68 @@ def test_matching_external_parent_anchors_are_reported_without_claiming_history( assert result["historical_operation"] == "NOT_ESTABLISHED" +def test_outer_parent_bundle_alias_remains_an_accepted_read_only_coordinate( + tmp_path: Path, +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "outer-parent-alias") + alias = tmp_path / "parent-alias" + _symlink_or_skip(alias, bundle, target_is_directory=True) + + verification = verify_frozen_artifact(alias) + status = thawed_artifact_status( + descendant, record_path, parent_bundle=alias + ) + + assert verification.state == "SEALED" + assert status["state"] == "THAWED_CLEAN" + assert status["historical_operation"] == "NOT_ESTABLISHED" + + +def test_explicit_thaw_record_alias_remains_readable_without_authenticating_history( + tmp_path: Path, +) -> None: + bundle, descendant, record_path = _thawed_file(tmp_path, "record-alias") + external = tmp_path / "external-record.json" + record_path.replace(external) + _symlink_or_skip(record_path, external) + + status = thawed_artifact_status( + descendant, record_path, parent_bundle=bundle + ) + + assert status["state"] == "THAWED_CLEAN" + assert status["historical_operation"] == "NOT_ESTABLISHED" + + +def test_hard_linked_internal_json_members_remain_regular_file_semantics( + tmp_path: Path, +) -> None: + bundle, _ = _sealed_file(tmp_path, "hard-linked-members") + freeze_path = bundle / "freeze.json" + seal_path = _seal_path(bundle) + external_freeze = tmp_path / "hard-freeze.json" + external_seal = tmp_path / "hard-seal.json" + _writable(freeze_path) + _writable(seal_path) + freeze_path.replace(external_freeze) + seal_path.replace(external_seal) + try: + os.link(external_freeze, freeze_path) + os.link(external_seal, seal_path) + except OSError as exc: + pytest.skip(f"hard-link creation is unavailable: {exc}") + external_freeze.chmod(external_freeze.stat().st_mode & ~stat.S_IWUSR) + external_seal.chmod(external_seal.stat().st_mode & ~stat.S_IWUSR) + + result = verify_frozen_artifact(bundle) + + assert result.state == "SEALED" + assert not freeze_path.is_symlink() + assert not seal_path.is_symlink() + assert freeze_path.stat().st_ino == external_freeze.stat().st_ino + assert seal_path.stat().st_ino == external_seal.stat().st_ino + + def test_parent_mutation_after_thaw_refuses_current_lineage(tmp_path: Path) -> None: bundle, descendant, record_path = _thawed_file(tmp_path, "mutated-parent") payload = bundle / "payload" From e9d2b13eb22342934789bf94ee894bb5faed6d98 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Mon, 31 Aug 2026 09:27:02 -0700 Subject: [PATCH 30/34] test(vstd5): pin claim identity in entry digest --- tests/test_evidence_bound_assurance.py | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_evidence_bound_assurance.py b/tests/test_evidence_bound_assurance.py index 25066d0..c30d293 100644 --- a/tests/test_evidence_bound_assurance.py +++ b/tests/test_evidence_bound_assurance.py @@ -20,6 +20,7 @@ ClaimBinding, ClaimCoordinate, ResourceBounds, + canonical_digest, ) from verifier.core.depth import ( build_evidence_bound_vstd4_receipt, @@ -2028,6 +2029,37 @@ def test_vstd5_claim_id_must_match_the_admitted_vstd4_claim() -> None: ) +def test_vstd5_entry_digest_binds_the_admitted_vstd4_claim_id() -> None: + store, session = _session() + entry = _established_vstd4(store, session) + neighbor = replace(entry, claim_id="claim:neighbor") + entry_digest = canonical_digest(entry.to_dict()) + neighbor_digest = canonical_digest(neighbor.to_dict()) + assert neighbor_digest != entry_digest + + witness, assertion, corroboration = _witness_components( + store, entry, "witness:one" + ) + bundle = WitnessBundle( + entry.claim_id, + "declarant:one", + entry.witness.header.binding, # type: ignore[union-attr] + (witness,), + (assertion,), + (corroboration,), + ) + result = assess_witness_corroboration(entry, bundle, session=session) + receipt = build_vstd5_receipt( + entry, + bundle, + result, + receipt_id="VFY-5-ENTRY-CLAIM-DIGEST", + session=session, + ) + assert receipt["entry_vstd4"]["result_digest"] == entry_digest + assert receipt["entry_vstd4"]["result_digest"] != neighbor_digest + + def test_vstd5_claim_id_is_distinct_from_the_claim_coordinate_subject() -> None: store, session = _session() binding = replace( From 1159839d3a1cb51726cbb87b46eace1ff58d1943 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Mon, 31 Aug 2026 10:02:52 -0700 Subject: [PATCH 31/34] fix(zizk): bind recorded proof to tracked image --- CHANGELOG.md | 3 + README.md | 2 +- docs/ARCHITECTURE.md | 4 +- .../risc0/CLAIM_BOUNDARY.md | 5 + examples/zizk_artifact_first/risc0/README.md | 24 ++- .../risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md | 77 ++++--- .../zizk_artifact_first/risc0/THREAT_MODEL.md | 14 +- .../risc0/recorded-proof/public.json | 188 +++++++++--------- .../risc0/recorded-proof/receipt.msgpack | Bin 301811 -> 301835 bytes .../risc0/scripts/verify_recorded_proof.sh | 14 +- experiments/INDEX.md | 2 +- .../artifact_first_mechanisms/experiment.json | 8 +- tests/test_zizk_artifact_first.py | 10 +- 13 files changed, 196 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b4b272..2a79d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,9 @@ architecture, publish the bounded RISC Zero reference mechanism and exact recorded public proof artifacts, and keep only unfinished mechanisms experimental while preserving unresolved horizons and native-system authority. +- Bind the recorded RISC Zero proof to the image produced from the tracked guest and + locked toolchain in the governed offline verifier, rather than accepting source/proof + correspondence from a neighboring historical image identifier. - Formally distinguish TRUST as mechanism-earned forward artifact support, ROT as typed time-indexed degradation of current admissibility, and RUST as an inverse-TRUST memetic causal backtrace toward recorded ancestor states. None is actor-tied trust or a scalar; diff --git a/README.md b/README.md index a8cb945..e732ba8 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ or a second implementation. | 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 | | zero-identity/zero-knowledge (ZIZK) artifact-first TRUST | Governing VSTD architecture in `standard/LADDER.md` section 1.1; not a separate numbered profile | Hash-chained event serialization and offline replay, evidence-bound forward TRUST, typed ROT, challenge projection, reverse RUST, structural concentration, conflict resolution, explicit localization, and bounded diagnostic attribution | Exact Graph topology, proposition bindings, embedded evidence bytes, mechanisms, trust roots, bounds, and immutable history | Implemented reference mechanism; no universal support score or actor trust | Domain-specific transfer/localization mechanisms, independent cross-implementation replay, complete trichotomy derivation, and maturation of optional proof backends | -| RISC Zero proof-carrying reference mechanism | Bounded non-normative mechanism example under the governing ZIZK architecture | Pinned prover/verifier source plus a tracked real receipt, public envelope, self-test result, and verifier command that can run network-offline after setup | Authenticates one fixed hidden-witness predicate and expected image identifier; it does not establish the witness's external truth | Native proof verified; no VSTD receipt mapping | Complete VSTD trichotomy predicate, second build, external audit, and additional proof backends | +| RISC Zero proof-carrying reference mechanism | Bounded non-normative mechanism example under the governing ZIZK architecture | Pinned prover/verifier source plus a tracked real receipt, public envelope, self-test result, and network-offline command that requires the tracked guest build and recorded proof to share one image identifier | Authenticates one fixed hidden-witness predicate and expected image identifier; it does not establish the witness's external truth | Native proof verified; no VSTD receipt mapping | Independent build host, external audit, complete VSTD trichotomy predicate, and additional proof backends | The authoritative implementation-to-specification map is [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Normative meaning remains under diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c145c5d..45ea4ba 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,7 +1,7 @@ # Verifier Standard (VSTD) conformance architecture > **Acronyms:** application programming interface (API); Boolean satisfiability problem (SAT); command-line interface (CLI); -> JavaScript Object Notation (JSON); reduced instruction set computer (RISC); +> identifier (ID); JavaScript Object Notation (JSON); reduced instruction set computer (RISC); > Supply Chain Integrity, Transparency, and Trust (SCITT); Verifier Standard (VSTD); > zero-identity/zero-knowledge (ZIZK). @@ -169,7 +169,7 @@ Maturity attaches to mechanisms beneath that architecture: | Mechanism surface | Current status | Ownership boundary | |---|---|---| -| RISC Zero hidden-witness predicate | Bounded reference mechanism with tracked public proof artifacts | `examples/zizk_artifact_first/risc0/`; native verification only, no VSTD receipt mapping | +| RISC Zero hidden-witness predicate | Bounded reference mechanism with tracked public proof artifacts and a governed tracked-source-build/image-ID equality gate | `examples/zizk_artifact_first/risc0/`; native verification only, no VSTD receipt mapping or independent build host | | Bounded identity-disclosure evaluator | Bounded non-normative reference mechanism | `examples/zizk_artifact_first/zero_identity/`; no identity-derived trust | | Assurance event serialization and replay | Implemented bounded reference mechanism | `VSTD-GRAPH-ASSURANCE-1` embeds the historical Graph, exact bindings, evidence bytes, a hash chain, and a current-view digest; `recheck_assurance_log` reruns every event mechanism | | TRUST transfer | Implemented edge-local proposition-dispatch reference mechanism | `record_trust` binds one exact transformation, its complete inputs and output, the historical Graph digest, and the prerequisite TRUST event for every derived input. Recursive current-admissibility checking excludes the route if any required event, artifact, or transformation degrades or conflicts, without deleting history. No universal scalar support algebra exists. | diff --git a/examples/zizk_artifact_first/risc0/CLAIM_BOUNDARY.md b/examples/zizk_artifact_first/risc0/CLAIM_BOUNDARY.md index 96c8a61..8059d8a 100644 --- a/examples/zizk_artifact_first/risc0/CLAIM_BOUNDARY.md +++ b/examples/zizk_artifact_first/risc0/CLAIM_BOUNDARY.md @@ -17,6 +17,11 @@ The concrete verified statement is: > predicate and was bound to the journal's subject, policy, challenge, threshold, and > salted evidence commitment. +The repository's governed offline command additionally builds the tracked guest with +the locked toolchain and refuses the receipt unless that build's image ID equals the +recorded image ID. Proof verification alone binds an image identifier; this separate +comparison is what connects the tracked source-facing build to that identifier. + The zero-knowledge basis is the selected protocol and implementation, not merely the absence of witness text from JSON. The artifact scan is an additional serialization check, not a proof of zero knowledge. diff --git a/examples/zizk_artifact_first/risc0/README.md b/examples/zizk_artifact_first/risc0/README.md index 8c71df2..936bd7a 100644 --- a/examples/zizk_artifact_first/risc0/README.md +++ b/examples/zizk_artifact_first/risc0/README.md @@ -130,8 +130,8 @@ The exact non-secret artifacts from the recorded run are tracked under | Artifact | Bytes | Secure Hash Algorithm 256-bit (SHA-256) | |---|---:|---| -| `receipt.msgpack` | 301811 | `5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df` | -| `public.json` | 2575 | `6324c3c5d77ea4df4034f61131059289d5228f190d69e34c59bd7416fa9ac823` | +| `receipt.msgpack` | 301835 | `04813c4757ba4efbdad9d51d50d7402f3a98f6c23e53b9b58cce8af12ef9caa2` | +| `public.json` | 2590 | `188098e6ba1ac940475f15e0a4304ff08d678d98a9ed708dbe41dc6dde596b76` | | `self-test-results.json` | 377 | `e4c1bff21fb6161221276157fa96af6661af8635da35970ba12e462881f2c6fe` | The private witness and salt are not tracked and are not required for verification. @@ -142,24 +142,28 @@ this command from this directory: ./scripts/verify_recorded_proof.sh ``` -The script executes this direct verifier command: +The script first builds the tracked guest with the locked toolchain and requires its +image ID to equal the recorded proof's image ID. It then executes the direct verifier: ```bash export PATH="$HOME/.risc0/bin:$HOME/.cargo/bin:$PATH" export CARGO_TARGET_DIR="${HOME}/.cache/vstd-zk-target" export RISC0_DEV_MODE=0 +EXPECTED_IMAGE_ID="91df751f5764f81ba4995994afb43e87928dc32d23c81799c767794c27eabcff" +ACTUAL_IMAGE_ID="$(cargo run --locked --release -q -p vstd-zk-host -- image-id)" +test "${ACTUAL_IMAGE_ID}" = "${EXPECTED_IMAGE_ID}" cargo run --locked --release -p vstd-zk-host -- \ verify recorded-proof/receipt.msgpack recorded-proof/public.json \ - e1e9bf4f68ef60ff9af6b50e144082bc475cc20cab47e8187201153da597dcd8 + "${EXPECTED_IMAGE_ID}" ``` The final argument is the exact RISC Zero guest image identifier recorded by the -public envelope and independently pinned by this repository. It is an explicit -program trust coordinate, not actor identity or reputation. Omitting it verifies -newly produced artifacts against the guest image built by the current checkout; -supplying it permits offline verification of this immutable historical receipt -without silently substituting the current build's image identifier. To require Cargo to -use only an already populated local cache, run +public envelope. The preceding comparison binds that identifier to the program built +from this checkout's tracked source and lock files. It is an explicit program trust +coordinate, not actor identity or reputation. The host verifier can separately accept an +explicit historical image ID, but that direct operation alone does not establish that the +current source builds the historical program. To require Cargo to use only an already +populated local cache, run `CARGO_NET_OFFLINE=true ./scripts/verify_recorded_proof.sh`. The expected successful output is: diff --git a/examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md b/examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md index 06d2cf8..1872826 100644 --- a/examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md +++ b/examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md @@ -6,20 +6,23 @@ > Verifier Standard (VSTD); Windows Subsystem for Linux 2 (WSL2); zero-identity/zero-knowledge (ZIZK); > zero-knowledge virtual machine (zkVM). -**Date:** 2026-08-23 -**Status:** completed recorded run; non-secret proof artifacts tracked as a bounded -reference mechanism; no VSTD receipt mapping +**Original run:** 2026-08-23 +**Recorded artifact refresh:** 2026-08-31 +**Status:** completed real-proof run refreshed against the tracked guest image; non-secret +proof artifacts tracked as a bounded reference mechanism; no VSTD receipt mapping ## Repository coordinates - Repository: `TimeLordRaps/verifier` -- Immutable base: `598c545be3833d6d81bb7e252ca5837f3bb2a449` -- Branch: `codex/zizk-zero-knowledge` -- Worktree: isolated worktree named `zizk-zk-codex`; its machine-specific absolute - path is intentionally excluded from this public report -- Primary worktree modified: no +- Original development base: `598c545be3833d6d81bb7e252ca5837f3bb2a449` +- Regeneration source commit: `e9d2b13eb22342934789bf94ee894bb5faed6d98` +- Regeneration source tree: `6fcf0a4d391815f7218a61fb32ca6bac0b71df63` +- Branch: `codex/post-1.2-professionalization` +- Worktree: the pull-request worktree; its machine-specific absolute path is + intentionally excluded from this public report - Existing serialized receipt identifiers modified: no -- Push, pull request, merge, tag, release, or publication performed: no +- The proof command performed no Git or network publication action. No merge, tag, + release, or publication was performed. ## Selected proof system @@ -30,7 +33,7 @@ Exactly one proof system was selected and used: | SDK and verifier | RISC Zero zkVM `3.0.6` | | Receipt kind | composite STARK | | Program trust coordinate | RISC Zero image ID | -| Image ID | `e1e9bf4f68ef60ff9af6b50e144082bc475cc20cab47e8187201153da597dcd8` | +| Image ID | `91df751f5764f81ba4995994afb43e87928dc32d23c81799c767794c27eabcff` | | Tool manager | `rzup 0.5.0` | | Prover executable | `r0vm 3.0.6` | | Guest build tool | `cargo-risczero 3.0.6` | @@ -111,34 +114,43 @@ PASS cargo build --locked --release -p vstd-zk-host PASS + +two clean target directories: +vstd-zk-host image-id +91df751f5764f81ba4995994afb43e87928dc32d23c81799c767794c27eabcff ``` Real proof plus automated negative cases: ```text -RISC0_DEV_MODE=0 vstd-zk-host self-test local-artifacts/recorded-final +RISC0_DEV_MODE=0 CARGO_NET_OFFLINE=true \ + ./scripts/run_real_proof.sh PASS -elapsed wall time: 6.10 seconds -maximum resident set: 1,214,664 KiB ``` Offline verifier invocation without the witness: ```text -RISC0_DEV_MODE=0 vstd-zk-host verify receipt.msgpack public.json +CARGO_NET_OFFLINE=true ./scripts/verify_recorded_proof.sh PASS -elapsed wall time: 0.10 seconds -maximum resident set: 5,632 KiB ``` Repository validation: ```text python -m pytest -q -258 passed, 3 skipped +552 passed, 38 skipped + +python scripts/build_experiment_index.py --check +[EXPERIMENT INDEX OK] manifests and repository artifacts verified + +python scripts/build_reference.py --check +[REFERENCE OK] docs/reference.html matches the implementation python scripts/check_presentation.py -[PRESENTATION OK] links, versions, boundaries, paths, and visual assets +[PRESENTATION OK] links, accessibility, versions, boundaries, paths, maturity, transient +status, visual assets, generated reference, experiment index, acronym expansion, and +structural terminology python -m compileall -q src scripts PASS @@ -168,24 +180,26 @@ All ten recorded Boolean checks were `true`. The exact receipt, public envelope, and self-test result are tracked under [`recorded-proof/`](recorded-proof/) so a consumer can verify the recorded run rather than -only generating a new proof. The ephemeral private witness and salt remain excluded. +only generating a new proof. The governed offline command first requires the locked build +of the tracked guest to reproduce the recorded image ID. The ephemeral private witness +and salt remain excluded. | Artifact | Bytes | SHA-256 | |---|---:|---| -| `receipt.msgpack` | 301811 | `5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df` | -| `public.json` | 2575 | `6324c3c5d77ea4df4034f61131059289d5228f190d69e34c59bd7416fa9ac823` | +| `receipt.msgpack` | 301835 | `04813c4757ba4efbdad9d51d50d7402f3a98f6c23e53b9b58cce8af12ef9caa2` | +| `public.json` | 2590 | `188098e6ba1ac940475f15e0a4304ff08d678d98a9ed708dbe41dc6dde596b76` | | `self-test-results.json` | 377 | `e4c1bff21fb6161221276157fa96af6661af8635da35970ba12e462881f2c6fe` | -| `corrupted-receipt.msgpack` | 301811 | `389117e63a429e55c3f3616b9cbf2339fb1c99b48712ef7a3e5f5f16b32b6d81` | -| `tampered-journal.msgpack` | 301811 | `4e443f5084b8665a7185e6e4e62fd72eed5130673b3d7bfde488cdc6c405555c` | -| `mutated-public.json` | 2575 | `c2a056b71b2019daa8ac9f3aefcb4c2cc28a1b56ef97f8c61947bf37c5f2b7b9` | -| `transplanted-public.json` | 2575 | `95cf0d97e20777e2ad49234b7b27074dc8931ee7de215632a7d22a6b5466f2b6` | +| `corrupted-receipt.msgpack` | 301835 | `a37fbceb5cd234a991deb8c530d29551534fe04be881defa716d9fdcf1cbcf1f` | +| `tampered-journal.msgpack` | 301835 | `e210702b95557d76d4a2c285b14c738f4ddb7160017e92ae082dd6d9ef48b4f8` | +| `mutated-public.json` | 2590 | `c04651e41f50682bdae40049bdd73084d1ac2a98816f8e051ace7ed3ca24d360` | +| `transplanted-public.json` | 2590 | `997ac9107ab917838517acddbaf2bf2d01ad074e4c08b01e251b1bb3d7dc1db7` | ## Unresolved assumptions - The selected cryptographic implementation and transitive dependencies were not independently audited in this work. -- The image ID was produced once in this environment; a second independent build has not - yet corroborated it. +- Two clean builds in the same recorded WSL2 environment produced the same image ID. A + build on an independent host and an independent implementation remain unavailable. - The host, compiler, installer, and operating system remain trusted for witness secrecy. - The experiment does not establish constant-time or side-channel-resistant proving. - The challenge is cryptographically bound, but challenge issuance, expiry, uniqueness, @@ -197,10 +211,11 @@ only generating a new proof. The ephemeral private witness and salt remain exclu ## Public claims currently justified -The local evidence justifies saying that the bounded RISC Zero 3.0.6 reference mechanism produced -and re-verified within the reference program a real composite STARK receipt for one bounded -hidden-witness predicate, with the recorded negative cases rejected. It does not establish -distinct prover/verifier actors. +The local evidence justifies saying that the bounded RISC Zero 3.0.6 reference mechanism +produced and re-verified a real composite STARK receipt for one bounded hidden-witness +predicate, with the recorded negative cases rejected, and that two clean builds under the +same recorded local environment produced its recorded image ID. It does not establish an +independent build environment, implementation, or distinct prover/verifier actors. It also supports keeping VSTD core disclosure-neutral: this result demonstrates one optional privacy mechanism without requiring or invalidating full-disclosure receipts. diff --git a/examples/zizk_artifact_first/risc0/THREAT_MODEL.md b/examples/zizk_artifact_first/risc0/THREAT_MODEL.md index b59a1f1..8cfba7a 100644 --- a/examples/zizk_artifact_first/risc0/THREAT_MODEL.md +++ b/examples/zizk_artifact_first/risc0/THREAT_MODEL.md @@ -19,13 +19,15 @@ threshold, predicate result, and salted evidence commitment are not secrets. Acceptance depends on all of the following: -1. the expected RISC Zero image ID obtained from the reviewed guest ELF; +1. the expected RISC Zero image ID reproduced from the tracked guest source and locked + toolchain by the governed offline command; 2. RISC Zero zkVM 3.0.6 verification code and its proof-system parameters; 3. the pinned Rust sources and `Cargo.lock`; 4. SHA-256 collision and preimage resistance for the public digests; 5. correct public-statement comparison after receipt verification; and -6. a verifier obtaining the expected image ID independently rather than trusting an - unbound metadata field supplied by the prover. +6. the governed source-build comparison or another verifier obtaining the expected image + ID independently rather than trusting an unbound metadata field supplied by the + prover. The composite STARK uses transparent public setup. Its non-interactive security relies on the proof system's Fiat-Shamir construction and its documented hash assumptions. @@ -84,9 +86,9 @@ receipts. ### Supply chain Version pins and a committed lock file constrain dependencies but do not independently -audit every transitive crate, compiler binary, installer, or build host. Reproducing an -image ID on another trusted build host is useful evidence, not supplied here as an -independent implementation. +audit every transitive crate, compiler binary, installer, or build host. Two clean builds +under the same recorded Windows Subsystem for Linux 2 environment reproduced the image +ID; a build on another trusted host and an independent implementation remain unavailable. ### Semantic overreach diff --git a/examples/zizk_artifact_first/risc0/recorded-proof/public.json b/examples/zizk_artifact_first/risc0/recorded-proof/public.json index 3e845cc..16e1bc5 100644 --- a/examples/zizk_artifact_first/risc0/recorded-proof/public.json +++ b/examples/zizk_artifact_first/risc0/recorded-proof/public.json @@ -1,9 +1,9 @@ { "experiment_profile": "ZIZK-VSTD-ZK-EXPERIMENT-0.1", "proof_system": "risc0-zkvm-3.0.6-composite-stark", - "image_id": "e1e9bf4f68ef60ff9af6b50e144082bc475cc20cab47e8187201153da597dcd8", - "receipt_sha256": "5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df", - "receipt_size": 301811, + "image_id": "91df751f5764f81ba4995994afb43e87928dc32d23c81799c767794c27eabcff", + "receipt_sha256": "04813c4757ba4efbdad9d51d50d7402f3a98f6c23e53b9b58cce8af12ef9caa2", + "receipt_size": 301835, "journal": { "profile_digest": [ 95, @@ -74,38 +74,38 @@ 33 ], "subject_digest": [ - 22, - 122, - 99, - 204, - 97, - 34, - 89, - 53, - 32, - 92, - 242, - 247, - 10, - 210, - 88, - 172, - 220, - 231, - 224, - 30, - 26, - 196, - 131, - 46, - 214, + 15, + 27, + 245, 182, - 32, - 56, - 55, - 236, - 22, - 203 + 140, + 250, + 168, + 118, + 49, + 177, + 132, + 61, + 122, + 52, + 67, + 176, + 80, + 20, + 6, + 114, + 246, + 141, + 33, + 166, + 94, + 91, + 230, + 232, + 144, + 225, + 222, + 195 ], "policy_digest": [ 75, @@ -142,73 +142,73 @@ 95 ], "challenge": [ - 70, - 25, - 227, - 207, - 4, - 53, - 122, - 247, - 196, - 116, - 55, - 94, - 192, - 15, - 67, + 225, + 150, 94, - 13, - 20, - 211, - 145, - 35, + 113, + 151, + 146, + 163, + 246, + 56, + 62, + 239, + 168, + 129, + 100, + 213, + 168, + 45, + 135, + 142, + 10, + 221, + 172, + 125, + 16, + 32, + 183, + 56, + 228, 230, - 197, - 120, - 234, - 38, - 157, - 44, - 156, - 99, - 136, - 154 + 67, + 10, + 173 ], "threshold": 70, "evidence_commitment": [ - 197, - 74, - 14, - 209, - 241, - 162, - 135, - 66, - 59, - 124, - 229, - 70, - 93, - 53, - 243, + 233, + 126, + 33, + 208, + 156, + 20, + 122, + 55, + 35, + 170, + 125, + 169, + 60, + 246, + 6, + 43, + 38, + 186, + 172, + 112, + 173, 147, - 120, - 47, - 41, + 163, + 165, + 219, + 117, + 22, + 212, + 215, 34, - 80, - 125, - 63, - 61, - 137, - 137, - 80, - 249, - 27, - 95, - 80, - 95 + 24, + 33 ], "predicate_satisfied": true } diff --git a/examples/zizk_artifact_first/risc0/recorded-proof/receipt.msgpack b/examples/zizk_artifact_first/risc0/recorded-proof/receipt.msgpack index e058835b952bde43ec21e6bf5be847d483a9c949..852ab292aaa4cb35ef03a3475c521860d49ec2ff 100644 GIT binary patch literal 301835 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

    m9wH4EB)zen>2H2!Pe2_5Cm~ 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%0AM-2fYh7ic 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

      gZw?sse@8nb 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 Qe>g~GQkAsy|8xoeHzm_X*#H0l literal 301811 zcmeFZRd^L!v~3+F9^y(|A?~h(xVyW%2X}YZjk~+Mdjo>IvvG%w^Ur%9&)a?dzrz#B zw^psH%B(T_XuXd$YP8XH=wTFZv5YALIzE30p9nKEKoOIcCvowvSH z693~LUH|_5L&Tp|a@hQ_7>CoJJ=y5~T{?lFKT~^C@b~F98va~!#p}Ku!;Xl*nu-s->Dq{QY$y^3T`La%H{+W)^~E*f$BXUa zguiQu!(WIIR}xt!4s~Utc-yfb;&qd}N%#}mE-pLL{r_BqIx!P+KGXEs%oo`WTlOHM z7Iz(2&wFiHEo@qXk8Xo6(I@u~|KHdB?|u2-b@0E>$N%r@fy=6IHpEm6noL9g!$mm$ zmOO{n@!UAvq<_UDKTXaW=h#E1(P^~qgO_!^3r>rZu43TfpN-w}Ln$;~&s$0D(YTd( zoa>y9#PwnKDK(z_je4y!oA59%A4E&PuaR^-Rdhyu`ihH4UmU#`&-;D$pp^2mD*Z9UPs9E~jiF#B&)9um0zbMYr=z;R@t9^*B-x5u? z?lX?yd*$yFtP0l_)6M_XLhMTpEF@gNv<9agx;?4!@YJ9}bx0@r+Af=k%OVB2Ta|xF zcxx+%^B>uTq*^ES!=dcQBE0rWUPJ5gVhNINP144Adt(*KPu{f>@o`r@U4J#HQvGSz z0BkcPlZajPEP<#I{^3{$Y8oT?c}6$7x|D3Dbo0;GSZ!46idUD|`=nR9eWFg?W<83T zjo;CePs}8)cbGPnDN!ld=9JwgWWK{b(xO65C|G^dn$Y=r`FLR zC6kGkg33x9{`h@EXP?o0N}t;wr=hv`2{M{vEilY?3dBkOY+tOV4&8#RR9HRU!!KD; zvY`0~u3C{ncsaD4C32!!JsI=nxQn}SC>pzAX5Wdkq; zwu_`EO%YgkCjZ1Eeu*aWQZrwmA@gZARbQu_!}9i&DC~M2zd?`DR~4~69jQz0sZ}X> z_B<3%+Z?Ai#D@;?$9taDb1cuBbiqMqUM8A5<`2W=iu_Wv`sK=yuz!OA4nOoxQ`+a~ z15{$ho})3t#2M2qF`}U{Tv`w~TT0_5>C7m`2IJw(+#iZ?nTf!)+8jhNQGmZll^2RTHOg zm_;97hQ*L^Cn-O&?=iiGy*Y|(rM)Tzv(lR=sTp$tw*`HllhD-tIF4y@bu^urqK(b_ z1Aj1IcQTh&<@$Vz^`kIJ3yA|V48AZ4G0VQGhmBmxM?9`Pn1_N{>M0DHBle(^l6ITQ{-d*ri#f3bZ->jn2&up5 zh|&5TZRj??I81G0_*+!slnrIAm?NDWPrau$9XFb^S41WK)d7(k?lepV-lkX@g`0jwM&V zWBK;LcU+HYG!VT_>4Uh%zyBlj{JVol&w2kH?Fo82(dqowLa%d^7NZyZU@($rcIP3z z?p7+M{mMd+e3<(LsoOIvu|3y5fQXitx5*zL`Ua7jy5}bc zQ|7cWhO9D2Su78BHzv1hartdD$2x+Qq=_`)xxX4kZLislkH=n%GD?2`uqNTg ztXs&A|F??V4viHkIKDbd)tw<7B-$HA(7I&Rd_qT#9Y)8}?PG{r|Evm^0}uOQP;Qq- z#_`Q-$@FdLMXzP24Ny(#9*W`h|K?IQseBx{dIc9qpFHz9CS!(NLhZ%#LAV6PNntSf zU^5+aZ*{@@+n_HK2X^dlWNLb4QJ>SQ<6E(N*|dOe6D;mg z+WS~Og%Jx?aW~mH4wF99gXtdBXiHs2&TMo>|2#{?d57sl%}D!1g`J-*s(n4jQ4ss@ z35t4q|4V4>osD=^=qM8TH{Xq{lq0niHLlu+WbCGPlx;{0r1tUV9P+(>HDjUnY!xm099__iIbcuZ!Z8h?Jp3g>N@dyX}18oVa}R_#Th zKy@u{Y4@&BJ;h0jA_=JhXf3?E6t832`b6#Pp+kb}zrFA(F!CaGfA}WU@2k}ldLvYW zLKFAvv=3S7fZN718}YxB=t1PAiYP34FWHOaf0dgEX<8Ca*BbnBD*I-ETXSSK<)=c& z5dC54I@|;No5={6+e$#%m}U5Vskw-y{lArHhx%P5)}?<68LOH+k?$<)Nx^Wx^8^o> zr9-NGaULo=bJMYYbo~YLWj>Fw>ObrpZjYSLlCV3zk=jiM?umzZ=}B_BxNM@Nu=x!Z z9{UvWJfPJ;<f+XT!u=6DcYtkwRu&G>&ovVmvHbZ%Fgos@6p;WBEzo0(#``DA~hhbIq5rn{p9 z-P6~MrsK`QgJ?A+sH0tzJsd;ZbDQwglYEJ>!SzQNdKAu~#70{hvH^onoy6S6Gh z03pr!*%S=id=i;k?z-sQ4~`?DYxl8)M|7WvTR`7_sO4Xaz-`F#Tp?G7%2DGQF3x$2 z^KP>Dz1~Y}=ZXSqkKb*kWx=>&%mQ<(#2?u#go2#PG8%vFQXy+*S1bIRe7B(-bg=`S zkQv`_3%Vsu>Cv;kNClXTp&|aYA>9Yv7pP>2t1)Jg`@d3C9XXelH#`5Kr>S}kiD&x^ z!ed&y5cxjNrGy=E`$L=AspELGtZhQ^)tOMt^XAV%Q)h!462m(0(r$ZQmzq`0^mWCMjuj zROW0&=JwyuI7GS6K*H$-n zHAm;zK{=#%ZBM|qXX9$xb-ub|@m5-cy2<^&k`z9~5AO;67GcnDxHXv@>c$W+U)+dI zml`?j^Ls8Qr=agte7i3#A%C6qM3Tl;OrY?;CVS!}Vk<~ZT98h1s&gBDK9iIP^>&_x zx6!KuwCAaKqxr0>=qs)c97u+#k{0eN>7vq1-r5hnf7Qf_H8`??fYtZ<5mlz?kNvwd z^*Fc9+Cz2!SIbDw(33#cw{s?X^M4MdT7ezFWRQNU&|j)TZSx!>aGt2Xxoy z_JG*>3?-e{jsK_JCM0lT5T0kA=h7CcnoQ)&KUUPJ92|zZkDUj#_jjG6XNFr&*-EiBWR9PGgOnSalW1~&XGu{Xty#F( zjJZq7v}bqGuQCs$;@^`8NX@%{g~}(dkCUY~x@!W4$K74g-}r!PvIOO(tKVVh6z=d?Jav+DD6QHO&TsI<9+@dn{)pRiES~kofW6 zSBzT!xg|RGyq&ZakDiLrzA{C!4c?w6eY(^pl!C9tk+}bNe_TgP>(G6)=2&WVLsnBc zDlCzV3&x5l8YY@K_O;WsnlFJ9+o&mm7@b`&yo2Ug&{W4t8BRS8DO zD?f`QBS}7(h||GG&{3EYi}t0@HpFgPwt$G9Q(KU0+#5?tWXLF@Cr_-W?4rXM(pG&8 zr7XHx9cjNWSCQGjw4czLr*@Lo^~Eh5e%~n~+G1@t0*^cRVX}WyC@s^~jA>l;={_cF zoh-%gPG7_7+2Zb4zRA_X_wAgOl#I9+jE>Gp(Idq9uE+b+_2ojMu6#zR{a*k{}A%~Bs;JVKE z9f5BrjiGta=xAcUFV7_Y(*tEuC1vG_$jjJ={m+EUG#Q_>N1>PI6k`67_(9}pr9*TX zI`SNTD~4YrE;MioxwCw1$?LRLM^pBDFw#$|Z&PIZ_#=Mqe>S7)x%ECt)6=(8pxvi| zCXX3$ct#ffMRrVP5luGxb`ub7Wr<{--C$xo`()F;GERlyve%h}h0oB!@5HW9f~y8~ zV!LCFXviAOevx>0o)TtzCC<@<=N;6Cycvl^-KG&Vb>?lvxNn`{Sc_{f5-@eZStORu z)Io8l=3lJ(96p5eR}~fNuO8lxqlV=*yc+IV(Ps2`2*uLkbalT~cSYT|vj)@sS%-+& zKKM3W4t(53*qF0-2~lZwBGsY%0a}^Hmx(^2JczPWGA81VJG)>Yz3dd)KAQK4ADw#u zh4x1u=z3%7H`=bPzk*5m###7$98-bvm3|ipO~}4W#hd~C$haMTiEe*}+Thx~+cb2> zpBsV%%{H?bGt?BE}qZI1W9iy%i zdf5GOpM`mkX>YM{Q9naO_Iow7)IN&Mz3A9;+#2WI$8f;Ld8GWCk|5gq9_LU!=Kqw| z0gGIzjR`WPTl1Oobe(QO=onJ$IV0Xo8KptW8N%6@$$H9w6=8a zqyALTTA~JB_M<$!|5frlyrR*VZTSR)>&o**F?#u$psr^F30mJKPs)zhg{Y2J?~Zk! zDL3ir@Xu+K+&7eA^K}49IhB1xU5`eoKBl;@?R5#$SEpy=X&ab~r-sWX@<*ir8thv|c@4j^ zY&wZDiSwvfxw;z)O8?A5$E4pB6d&DNfTW8`3)#WzLvZvNwUE4RZ7)zw)>uL}r)9r! zJeYkFS?Tfn2sYm_jKt-0lxZ2|pNh8Z4K2L%hxEYNZtyyMSHzDZDdM{w@h1(pp>!hW zE*=A3q?1rt5>COQ|0-!Ru((FfxZDvT3DdNzgQ*EkwnHPhPJ+VkpRZHv{W69`1OEWgV(r~YuQ1>4f_n5@ zO7-aV=de+f{zd&`-E8#tuKh|;>k}Pp0$)Z@J0Wl=K?_v`4XU!3hRw&9i$q0xDIP|9 z$!cU~Jd?y$`o;xJ7g~Nr^=qGUEWI54(2o3Shi!k~&2*j9kdL?Y5F_lbp6ZE3)-7Ku zT6f3dnN^TT+q0dqDEqa2`M>ujYj56E3TqdhAlS`h9==ZT0!*c!sG+^$ayIoI1E*qE zJkpMi&j~IxeTj5I?y$WQ=}!u}kpD^jD{k965{Y)3SSpb4+vhmHdB2_RJ{p4A%C7lC ztm&PX6!mfp!Ec=2LTr`npJ04$!Eiz%v`!QME7O&fm4la}eewPs)YK36AfV*@dP=ob zPZDHz?l*};7Js6!zVjtMlcwy)W1xg2UP+Z2_|1w6#=5HSY?Lw`o{PspH4 zmgO0`v@ZEZSNYHTaa#DF5n(fqYfzwUKbi=)=XX#n{QVTm$QV^r!p2>~?(u^IG@tcZ zNU*e05Ap&N1@byyu>q~mn&Z$luiuM=-}29tUllEBNcrGBXb(@HDcZKFQ%ESz^`Ug| zF%Oh1EiK5|E+~=rC8#4acIvyE@5iM7r#hlO42#6C-)jfrmm2jW`KZ+c3a#q;(x5)t9`}JGU1*ai6sE{c z@#I%qToepF!gDr>(>mu9aHYe6kUw2-V6cATd16AQin_4EavVj!w`P+#VCxAw3cn@L z)qi#gM(@9`M03)jEaGl8hEbxVu1aNiel5}x`l2L+T{}Z{W7QWN=M3;eYU;Kaf{(7g zO;lE71B&0&-;wmx^)M=ZKB`l*x80ibm6pj!^vqgEpv=o@7%n${K(XX)Ni-%Ux6r+` z^f;NO*Pr;xS%{m--Puv*9>xN$x`_I#iTI?yW*Q5_;Q&P&l))j_8Fq zClUVp!2*h92b~~lK}}Px}3es3p_=1;>>Z zkWohS5fxqb_>q4*|2+Ero!8PHTwg}Zsv26wEmoNTKF{tw@=;ZA5a}XLlkG>t90pxmz!)V&4u%`s;!RNL~oA#lXKL5QRh& z9c23at4hzahvt#Jd(=Im;)8CZJEZe9mPebX;(zhZdSNA9K8fN#CX2E5IXMiAs09y+ zXl@@zw|C#Blc1aQf$$o!93#)C*kPU!=Z8mW=RMJIg_cs8IsE{^OAFr;YBJMI1W@J= zrdYGba=K0~C?}?u-xc!n96w6yyE zP?LOLiN%sYC)}TZSEAwl#wMH&>S>{4Gy6CS=X;f6=u*>-=t_kYst?r};+l0>7?`K> zW*{TED4G^q3wsQw>}jLd)5#a{d3Im$yXVcT2pq7pnEXB85^=s?dx@0GBL|>xS3I+U#f7hz4MQ2;*G!J&wB65I1^#9Z-$O(0zl6 zvZ|KAX|taqSznJOpe4~;22&klK_q2XXH!>M`U$f1|JmrBomY=~nS~?Pc6~+dTz&o@vW?EklNYgF znc($PlW;rqIvb5;)fRM2p41cj7jLbEczYx~Sliz}@n7HHRlw`FE@(*P?w~FE#2ZwL zpAQ$_)^`=GG}Eoo9kJ{-P0}$=lz0hqJ1Bc;Hjyo!EtF+^l0^RdfeolT{d_^(z2i^C zRSqgZeb^81+n^(bv`o(gTooj<(CVesj^7>4RYXqR(vzl@sa7Ztzjm6AuSL)B?i)It z=0`mTP-81|jY92r1MzG=w1bSa+b)EdS7l;1Yh@Ab>je0gIdbnfk-C#!Q8dd1 zq+#9yL%eq#8c#_3zA_BVO*i1t_+dE4!P8TT%bq3r(Y@H=W>yhOKuF#Mdd-RMNo7cK z9z9krpMu8w`ECMPe;-P2{j<%Y`EUM>-eUU+!a%yPm()-VA)eN~*-FWS;#4F9N0gFv zJXV;bXQQ>Lkd3n?=aAe?5|3X?LUqvFvq(#gUrlw(ZU;PGCJHa={pM=qW-M7i_Ur-W zIG*-cFWj8}PT>2-!4bcq7nc!r_eBI!-Aw!8SNEeU?$Up|oL3Xl$-37fx_S*!$M5Bw zgGkvd(WRpHqR1eeseC}$r;^{KIoC9i|NHY?q?aw&5tfR7*;x z33>d(7}Zg!MI;YasX(TG%{bb2PN>7SJVyr?EAM)2Lbaz+lWpcnK#YGBhDQVq^g5Qm zM$rH2+&C@9`@#=!w;mk0io?hIK(%HSkkX>`^4MD-z45<9o^&fmn?_4EZ zw%vjV+l32>UOL$b%TDul(!CUekbQ1?h*tYApUDcd3dU{dx#RR~tKLo9~2-AI;si!4p;hhNZrV6hw>%XSMuFMn$nKIb1BQCIiVgbj|cNP=N(0%SbLl`wD0&uC7EJ8lb z|2=KKD|7LDH@%YhOC?(LaxpQ-SJI#dVUELkh&;(x3-s1MK7z`Z2PUNIX$8{l_U1m6 z^z|1yKl|+$8f#h}Q}9^*E|KR1uv5I~+>XS!mR=NZd%u_ln+y9;*1!4}bE6ygsctg4 zL7>W{PZ)=XZ$@#hxI>Md?k{QXu6#`RN;Mx*8rMVEGudsc$mf7GnHpz`NeEd~K+TH> zV+dWfdOU_>{}$39F*h4~pUJDK?bYak+CDRDY@=o=W4|*~8HqV1*~F$C-AUEQdoQTz zs~AJV`rVb-s|=b+N~+Q{vh?!XiT`h^BdW@iB`B9P_7^p3vJ7>91`ZIG$ktY2x$f_V z_J&_$s4bA`NsC)S6QS2HY7)Aj`361DJ-Lm+3;9k;v6d$r=q9a8!eWdAt{6>A%7v>Q@?5LnY&3Ry4P zQ50=>P(gOfh)-C_Bxd4w-Te*~-)CG#!gY+u&4w(RgX_(j9;mH}ltFc3#4CZzZr(%X z=C&Tl91J>uXZVu;$VuBBh-^T!aL&U{`4hf$sul?bc5f-GOI=8pv!A6pow1m~PVj-b@6~9sG=~Sp&RiN=oeF>UV2Hinjr?QY> zcc&;!Gky)lV@>*a+%?xcCiGj)D>4kn?W1+%#-aEmsw^QcU{#%v2zly+S7$xLwRChZ zQuKy?CT+>XFpSo0{zZzE7FN2XKxaeIWYAh#?cHe~N%%Yl4EL_iEF1oB$~Lo-xDU; z&s!EmYaV|>_u&Uhs126;Kx|x4I2B7K2|D`EvqU7j{1i{mzH`gaTYWnhe`lB8H1>V^ zh;a9s7bsObKcQ;f;7jB?ty)8Ntfc`-$B($9Wbf%mapW~GecqU3y%d*x0h<-mMjbXp{Op;lk?2E}8Sf1o<|U(r$@_FaTaq|G-1 zmY7!3_St+E`8Q7#5S5fVhpxf1d~sZ)agE}{NI~P=t=!0vdbbvZ4OjMJ>1o?U&~5K= z$U9c%;(BfBC_K^@sgg6^YZKm?|LG8wyz&^1ItRB2Z?di{u1AxFTRr=}4!zdzu_1D? zsG>%FP2Z!S-sfLzzpV?PWT*fcsiqDi&c`a-n8A02{YWjLWWGSw$ zUPMlSjV>ii*Y2am(fczgb9)}da`oLyBq*mF6102TETk^Vf5+CT`6^*b(qZ)6`OjVg zm(H6-sOj}ObS$QiMem732O;}TA45v}RzC4V-+#sMzTc=O8CvB>dTDn%G9{nt zk!OCbjrPJvx2TM;l@MFex)TDB&;AGbHCqo7KT`6L@Gq8#8gbKnEHRz&9<+LG7QN26 z?D5nubQ9sHy;Tw-Gp{RfOU2A2QitB{i*flFCF1Y3MbfGjG>VeY7(3FY4;HmfOW`10 z3om`eZk5hDv}QyP!}jlC8`{!cWJC($$#v2m%6-H?GF6t+Tj8fk>vpFT$w@IoNL%;& zEkVjtj*zp#T$NNqXC=aJ)kNW>vQ3bi$gC5j^l6%p^Nkll#4~KJD4>!j#l3gPCg@Rc z6ai5)&LLxa;w^d^W51#}tM^JQTf3|!=F!&zI{t~P6VSn28jpTGPm$rHvW(1&1MEe% zVEz@{JlpjNOt|tJn;AoQVo{znmw;vGwhG(m(nFFvwtk~_uF`bO>POB+eexbv!Cd}} zpeSdoIR%IG3^54$e35Rpf@GRH4mCg`yygsLC5nTHEtvIFxDj*9h?8>(RP0fbugDueLbSVs-v36a zbciLHli%1-Ak+6YRVJqbsrr%POHq$^XJ|3H_>rWIB>_Z!k=skdjv2qGv^u#L8z@M&$Msc+MnECP6=fG{GLwvj8k9n@E;XJZTR`Vn0b8(q1U200{F%h3p4Oe z%yr~#syvSpJ`i_Y)XISS7!iXc|Q!aBNaJ{?ZotpV|bR-8ycV^^H$y0?vyz++al zCzXC9B1PtLcr>kkiY7v#3<;wxDt9rS_Bz9Ho_E565VHj}G)-8MPSnf|7w}ec)1ZsV zq8u#KGx{Oh_lY02z6J}aTI%b8UFh3;=+B*WkmmUp&f<0XEn z!bs>h(E?M`ZdN31H*gh!qu=sW>`rvUy~Ls$`b|+|ksNa34{lZ~Ut*T`KnC}l#x+!* zTik)fvb`Iy@%b@{1f3}j1kb)7Eh_qsWfbRIO{G>{=L7W{C#<33+!SY8=el-7_3uIv zp-LS%9lf#zJE`z;If8br^-N+5qbyKSD%7OX_u@pV-n7pkY;fYg1dhJ(0MnzzX_!P= z3tJ&%RSoi|Tsn#E=k^%Mu>BHLc|7PM_Ky#uCK{%-3mafs9i?*D574;!-7*wS!aB%b z)9H)K6k9n=WR}$-p=rHGI4yYx$V*x_m{NnJ7Z?`L8bj&!1A6H9Q9Fdk+B=8o$lYIv z=I&YbBy08ZqIAk1Wr}80RpMi)EPQ{HITomInc$A{pZq-0dW=sHGU~o?bJQn`%=e4x zFkHVbjUY+eF$|yUt~c=bcm6eUS1g)OO;qJ4e4Ov}CH%$0ImF8KeTdgZku8-!v-Jte zmjuD{b6%cB7xiW4_;i2hjf~RxSJ)ek8H`%jtqEJPi%;|-HYX*Qu4})RVjfc?L)_dR$5FIuzCg$MVA>{dD%+>t7G1aT+EeG^i-eq9gS}zKF}mN>>Kja z?ezuL^14Em=i2R58uY!7!if2~=t>LB?WXNE8I|m1e^8$|Ob^|?j|BHL{reDy$^8?M z8h=9{rL0Q_Xm~JZICW9;MDD%m*DZA4o3B7qORWgc^%qXj)OxQKv%!_+WS)^+O2EqN z5u)LEGL(uJ3ytu-F+WYnH-kY~{%&(ZLDwLgwCov?NJU*ef%L>JS12ELYcV}H6dooq zqUo5Z=u?}Bwa)K~=hxgT)JWYC-Ppb*3i!P@H^=IBakdC--F7CR=|v14iU+z-60z(7 z701mMB0XPc5E?7z|DvdD%6XhGE$yUx@_<#isw5OqI63tZh4b>(QGQT_hMK%b{t>|u zjXa?fHLp{)V&+@)OJ*bzv(hCAxou`Q2vH9|fZou*F?cRe6p}7gAGcW zOB}KNHTNW%6Eek8-c{d&f=g4hss8zX6aKs1F43l)5sl}A(RVTbGySoMhlwDYscqm8 zLLM&{p}Ld$9vB`o60l)Qv0x}CZPp_@=hq9ge%6d2?7v^7*hQPvqS$Y)NdD=Mj3>y% z>I0=4f_-r?41Pq<^1);2n3mK9t&8#BurpY(osh1R6_7e2y%AHJnPph5St%0NF&`sH zoFd9xX!4(#bYD}|gXDT)6t~p&-A?QMS*M8X*1DARxu14XD0g@=ExB$!r6=^W<+RG{C@oz7jSP(^ zTBKYO2ClSgTr4pTht~@gKBX9wvHj1Gu5ww8^6_teB02TMLNfR6&_((C_8e?v+)j~t z_l7@qfm2r!_s`5OWJT}Vf%As(!KD9LGgXZI$aztBL-#zMtQQbQqMx^zov>MjQJF{< zwagyaA!34lV`;8tD{ShK1IlJ1O+GP;|m_NLHkUC?tE||>~$lbCt z`!uThD^}pMIj4#2WHBt^Ws;DL`hR01Q1mz;d?f$H0&I<2B_rg_rT&7GXI!Un%-3+F z<*eV56K}l)A0=xwRL9B7(EUixB3h$9*kTmBYCIixo`xf%An}QWAAN5UAis77e)FS5 ztYk^m8+0BPwV=0ETdauiYAy0-m#5MaJXVC-wMt)Ow9Bb4*^w6B6fLnkK-H6;Vz_4b z;_fKB=G-E`{X`wk!@l3g&;IKSg7J<4Xf+9*|>aX{wU4F|&JJc%UseSjNr*Hh(aaZp<;ayhKr5*`;&)UT(W$XBp)W5$828+L& zP+8u4BtcCv#YCF+JT4|R-Uyj8L*ST-{(~;z>wI&9$UtlZOAoX|sv=J?I!|2}K zCB>+ZF2$$ci19rAoM9xGinLLtqH(2g{7buFpl4%9sF~IlS_4Litix%Y#TZLmxJl}A z5hb#BPsv8FbXad3n#^xdHPL+sk#4e?$TuIIA|fiIjcKwRWJW{b<{QX|7WG4=M0GWZ zr_Y{3Y5W0o5>Ds~X1-fiB&7B^pC_@gt8j&OK3s(5>7QX_^p5CH<(CWB#iLew8aG4L zJM^4csg1VNfmHOj1noloU`i;NQy&UK|4pO?jGdM>3c);Y2^IRz-H=(A;EMW}IV&*m z5?M3zgR()y=_HwpDZKgbah4zR2tRB65V1Y|BgSW1R~4YuX#5@x1(y_Im|T{jrnNLmm5346n@N?C?WC%Y!0I*cT5n*KkgS1i%i<^GWs3Ni7;cIsY{F`Ff^U3P z!aOB4Mpy;|T`3>>QW2@c{%24<{dpAejz_ald!V(5(#-xMA3vhsR-~i;`9#_f^8_jr zN>5-^u(TN?x2#QgrOlHNI<~tcsw<_N(K3oSPu7=zJm+!+z^ z>$%*JnwbnKp<+{w8xf^f=b*5>M-*n6849$$ zYkG>Jok1nddSc_VK0PIY+}>q6_`4-P!Eeuf4`S;~hf(2hZ72H95%+Pd>a!I2LzBlL z7w%|Oj5X#YLVt5e|v2+X*vCNiLV^UF`Df`Eam~IRjp~+nY8a2`a3vv*?PToo zjS_J0S|YV$XWC#}w#AeDQ>}U!9r9KoC9lekzzL?C3B0K>lA4aI`WU6HmLpnSvAakB z&HM+03zLqb^gv{ljSu%;LB~Lq=}2XM5SrnGffKn?bCgh6rhFN<7w6^>VD)DV;THxz zBG{?$8X*p0hp4N*m4Tw(eJ#;`jgiJ^>VZBe9zSnS*Uk>XKtuM-ruIQ-8{zGHWQjec z`j;@%uNheW`n-;ykr#NTQU?a_5gc%E1!QYeLXqDk+( z-a*sO;U7`+Q~4JMsb$MCANjF6U6!{zv^|LSFA2M;+H%Mnw_P^>QXXV}0go|zL>-ziP>9s#?eEE+ zo4J7Q=09U-KG2y?=+U)~cpOQTp-b|;bM!92SZ$p?U;f+wBr2Yp+ejK2<$H(l-6K8148ZrkjEecE#+Fs9{=0PO;b?E$>VI z0`CF%A6jlJ{L5>%34V3d2!sE&Po$(H`w;RDr?tfNm5|*vea{q%mTb=*x<&jEXtGiA z5(;mp?n7&|!X$JLzg|j$-_KmKA0HLmC;oRlChBunp}8qKK#ZNdX%hk7hC^tVZhD0A z&#Q45B^sX;vCf3485ewj#qA9b> zN8m97F(Nj5Kr1GRGGaih%;qiiUHjh?C$1yf>(;|!XnK^Q94>G6oT01Es{JU=A2$-| zw7iF;sEK(f;~qy#NEjt{ag9_z;V+c#97Wml(`i_Ei(!-Cg0Oh3p7(i#>V=gqVmfir zHZmuy(jwV1vr%}*hD#}WY4DU@ACp($zbN1q>4|@2P)=NBhSBPGb=Z#^7mwc0X{vhv0n-azBnZ^}ripUzJ3R$Nb$L(W{-`n3?Ea~M z-NxZA7<-neV;;MrhMJkDn@IdScodn#YymNTvRxXyDF$joLfnF*vEe8=ubU@cWg=_iAV0wA$;z!MN}@! z*^Sxr;XVYfmi>xE#ERi0J+7UC!N|?RAz!{HP2^FwuMwgxyp-UUT@D0oy&=w7$1hg& z8gk`0U!K21jODv`!y;Lt0LeQ-Z#a%=cckQysRu?|e+td;afCWK|88$2JfP`3QC)hs zqSg6uH>T6xi}`DtqVXb5U(*Z!(@kzv4k_D$TcNOs)9>Htht;mf!6dB=lqGZIu!lq@ zHx`O7DM<1sG)siQ>t*J=r~d2mEuy>@{zLe&D`T+E-@hNn6Qe%T^zhviB93I15o?iP zg3j2V1BCVwV#0L6@q-lKv=V${`qgd17#VPkxZws7q8Y8Yf_>3if1*}gbQ2lffp5rl z6yMumV{o9Irb~)qk&{|_xO!S{=vgp0Ip(Z9JoMXh7Rm}L04#i(Z_zmiM@&9A* ztfIP3!*)$~cXxMpcXxMpr;1_%VuFeR*omU3D2jrLsDN=Rii!cEfPsn$DvIuB_ThK3 z*FOK|z%{ehjD!DpfA4e0^}wF{9ziYsF4RF7`@2ZU{^)Ii`wx+mNa2nB1!Ijh)I2)a zasfH%WtQOT3|59ip;HU#|@!k4B zlr~wULi&opUl_4(I0VDbLG9pDp3eq@BF|>zFKRi1$i(R%h^aoz0m&fiM#H8-0<<=`XgG9MlN3i}4@YU&V7?uGW3^^Z^% zKb4K>IMzr=_!v={?ZZ<$NR6)HgA+#~1K8y5iXgtOM}wsCg|%R6^CG)&=?+p?4_}Ca zfuGrNcsdr@BQ}7t26DF}I+4sMPTh+0EG!7c_>xHf08-FDMmf3?9;Bj3T1kU%D z$TwL3Y7^MrQt?T(bie`_;k zb*Kp{XKiL7W}o(9F9jmy9*8assW&m2p@u;6$Tie$+W&pgW3Kv6BD z2}(K}k03JQj4+tQcCCN`TgMCJF*kCNn6y#{HqRQ_QNYj7fvixQ8mLZ{OTi{oWG&3^ zZ@mrf7c7cMsgU+Zz!}>u5Kx(P0K&vmU$BZf4y}9NXl6fj2zV}9mL!k4ns&%{~Dx+UvPvR*J36_oL5^6 z?{+_DQaM|t5pA;g4ocOQ`jOgL{|DK7etIEr*t-*H`>e>U%lkcrz;_c{5mRsQ1zB8$ z@`yEAd>CwxAC4l>dATC8{8m4vhv8-YS!4tpm6GRReQ+e6)mjUUvb5+RZ3VsDnm4MSki4gK8k(W^rEWztf zLdaT$sB7sPHDPZ3eF2@M*=PwZ z%RW1#8cRq*x$V{fY#h%~>HO%oT8Mm5T?REb9|{|lsV)L@LcJU!nqIP_Ox39h>Wt!k zFcLD-L|kgE0VEHoY=M(m`~XblWp^R3YdjJf%?@&qcC9ypkI2b(7>Y;Ffph7yLa6Pq zJ_ineE?Wq`TS}cySs#GG>Lw#)o=>nqOUh{;DdL=@`pt{0ft^K393I&^E>sQ{Sp&A; zJl7$$ad8cW&h+P?ct>d=Qf4T;#MdJu4EIz%y3zhT^a!Fig>AsUa}Na*+q#{=mvJ@; z0+au#Oh()H!Q&7){c)E&Pk}8Zn!pMy$GqWd@*)|!o63zLA>qh}f*;$-Q(rIA30<|% zcVOKY+=3AO-A|CPWt9$8uYH?EguJ>Q@=g=MA#?0*FH&t^b3?FR!3X9=9;+#!xAih4 zHg0}`VCx$j5VrerKJ-;P`oN=FQ%`ok8jba4Yx@zP&sPfWw|!%dip&MeV- z0ya&RZt!B#i9_7B;A%KqjjZA4Mabkyz5~Vj zP1fMNW~2hifBWO%|MFHEGL2g9L+9VaAjquoXM(leC>MC83qlaInObJ)rb#gn3T8=# z!YSK22;R1)bFz1t7LsSG9wBO-PZbG+!B=5<`}OaJ^$0H=ouxkf_MU#g3Q3Q z&*m^ZxHcSt?b@BC2=ZX-MP@~M4VY{m$HM$V=T5Ns>AFFDfXxuB))q>XmaiP4`vJQj zlKEX)V6o$%4WuLAY9Zm$!2rmWzjmiy+LKRk6(*A~GTh_|B~3^!cl+~++_aqJ8ss
    1. {item}
    2. " for item in items) + f"", + f"<{tag}{start_attribute}>" + + "".join(f"
    3. {item}
    4. " for item in items) + + f"", index, ) diff --git a/tests/test_presentation_surface.py b/tests/test_presentation_surface.py index 887a9fe..01c54ae 100644 --- a/tests/test_presentation_surface.py +++ b/tests/test_presentation_surface.py @@ -290,6 +290,11 @@ def test_every_declared_document_is_rendered_with_source_aware_navigation( 'Concept guide and ' 'intellectual precedents' in ladder ) + releasing = (output / "project/RELEASING.html").read_text(encoding="utf-8") + assert '
      1. Run ' in releasing + assert '
      2. Confirm python scripts/check_time_status.py passes' in releasing + assert '
        1. Push the tag ' in releasing + assert '
          1. Let Zenodo ' in releasing assert ( 'concept guide' in ladder

      $MK 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�Q$JZw)GXkY$?zU_(cEL;dAOMn74`*h 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 +BgtCxRfG@H-8TW%-mvm;Ce`<=ijnou_&E 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 diff --git a/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh b/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh index a75ab97..2fa08be 100755 --- a/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh +++ b/examples/zizk_artifact_first/risc0/scripts/verify_recorded_proof.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Terminology: reduced instruction set computer (RISC); RISC Zero (RISC0); +# Terminology: identifier (ID); reduced instruction set computer (RISC); RISC Zero (RISC0); # Verifier Standard (VSTD). set -euo pipefail @@ -11,6 +11,16 @@ export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-${HOME}/.cache/vstd-zk-target}" export RISC0_DEV_MODE=0 cd "${MECHANISM_DIR}" +EXPECTED_IMAGE_ID="91df751f5764f81ba4995994afb43e87928dc32d23c81799c767794c27eabcff" +ACTUAL_IMAGE_ID="$( + cargo run --locked --release -q -p vstd-zk-host -- image-id +)" +if [[ "${ACTUAL_IMAGE_ID}" != "${EXPECTED_IMAGE_ID}" ]]; then + printf 'FAIL: tracked guest image ID %s differs from recorded proof image ID %s\n' \ + "${ACTUAL_IMAGE_ID}" "${EXPECTED_IMAGE_ID}" >&2 + exit 1 +fi + cargo run --locked --release -p vstd-zk-host -- \ verify recorded-proof/receipt.msgpack recorded-proof/public.json \ - e1e9bf4f68ef60ff9af6b50e144082bc475cc20cab47e8187201153da597dcd8 + "${EXPECTED_IMAGE_ID}" diff --git a/experiments/INDEX.md b/experiments/INDEX.md index f2d5f6b..4299675 100644 --- a/experiments/INDEX.md +++ b/experiments/INDEX.md @@ -14,7 +14,7 @@ PYTHONPATH=src python scripts/build_experiment_index.py --check | Experiment | State | Question | Publication | Open horizons | Manifest | |---|---|---|---|---:|---| -| experiment-artifact-first-mechanisms | RUNNING | Which remaining domain-specific TRUST (mechanism-earned forward artifact support) transfer rules, ROT (typed time-indexed current-admissibility degradation) policies, RUST (inverse-TRUST diagnostic backtrace) localization mechanisms, hidden-witness trichotomy mechanisms, and independent implementations can extend the shipped reference event/dispatch substrate without actor reputation, scalar cancellation, causal-localization overclaim, or making the governing orientation contingent on the study? | CANDIDATE | 6 | [`experiments/artifact_first_mechanisms/experiment.json`](artifact_first_mechanisms/experiment.json)
      `sha256:1994591c49cccc8e99bc5233564b5b8eeed3810c6859f4e8c36f41d4797c4639` | +| experiment-artifact-first-mechanisms | RUNNING | Which remaining domain-specific TRUST (mechanism-earned forward artifact support) transfer rules, ROT (typed time-indexed current-admissibility degradation) policies, RUST (inverse-TRUST diagnostic backtrace) localization mechanisms, hidden-witness trichotomy mechanisms, and independent implementations can extend the shipped reference event/dispatch substrate without actor reputation, scalar cancellation, causal-localization overclaim, or making the governing orientation contingent on the study? | CANDIDATE | 6 | [`experiments/artifact_first_mechanisms/experiment.json`](artifact_first_mechanisms/experiment.json)
      `sha256:81ce95f54a1ca98cd9fb897cf0568329aff03d5cb7334ffd367c915c2f6835dc` | | experiment-github-verdict-neutrality | COMPLETED | Does the GitHub adapter preserve successful workflow and merge states without converting them into a VSTD verdict? | INTERNAL | 1 | [`experiments/github_verdict_neutrality/experiment.json`](github_verdict_neutrality/experiment.json)
      `sha256:3b98310d35c20e7099d242e2c655e4bf8dc62d91298adc04e4dc2f56f2f79d89` | Platform events, including successful workflows and merges, retain diff --git a/experiments/artifact_first_mechanisms/experiment.json b/experiments/artifact_first_mechanisms/experiment.json index d1aed82..63a17da 100644 --- a/experiments/artifact_first_mechanisms/experiment.json +++ b/experiments/artifact_first_mechanisms/experiment.json @@ -76,21 +76,21 @@ "id": "artifact-zk-report", "role": "round-1-zero-knowledge-report", "media_type": "text/markdown", - "digest": "sha256:ea53f18c4bc46a26a311ebf7ba6d5a44b924d18f3f5b51ce3fabb99f3fe0a4c3", + "digest": "sha256:a3b0cdc18e81216cedd3adcaaa65119d20cd755eaa530e2a5d9219d26eda1758", "locator": "repo:examples/zizk_artifact_first/risc0/ROUND1_ZERO_KNOWLEDGE_REPORT.md" }, { "id": "artifact-zk-receipt", "role": "recorded-risc0-proof-receipt", "media_type": "application/msgpack", - "digest": "sha256:5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df", + "digest": "sha256:04813c4757ba4efbdad9d51d50d7402f3a98f6c23e53b9b58cce8af12ef9caa2", "locator": "repo:examples/zizk_artifact_first/risc0/recorded-proof/receipt.msgpack" }, { "id": "artifact-zk-public-envelope", "role": "recorded-risc0-public-envelope", "media_type": "application/json", - "digest": "sha256:6324c3c5d77ea4df4034f61131059289d5228f190d69e34c59bd7416fa9ac823", + "digest": "sha256:188098e6ba1ac940475f15e0a4304ff08d678d98a9ed708dbe41dc6dde596b76", "locator": "repo:examples/zizk_artifact_first/risc0/recorded-proof/public.json" }, { @@ -432,5 +432,5 @@ }, "workflow_events": [], "interventions": [], - "manifest_digest": "sha256:1994591c49cccc8e99bc5233564b5b8eeed3810c6859f4e8c36f41d4797c4639" + "manifest_digest": "sha256:81ce95f54a1ca98cd9fb897cf0568329aff03d5cb7334ffd367c915c2f6835dc" } diff --git a/tests/test_zizk_artifact_first.py b/tests/test_zizk_artifact_first.py index 5d54b00..cc2ae72 100644 --- a/tests/test_zizk_artifact_first.py +++ b/tests/test_zizk_artifact_first.py @@ -1,4 +1,4 @@ -"""Terminology: Verifier Standard (VSTD).""" +"""Terminology: identifier (ID); Verifier Standard (VSTD).""" from __future__ import annotations @@ -105,8 +105,8 @@ def test_private_inputs_are_excluded_and_public_proof_artifacts_are_versioned() def test_recorded_public_proof_artifact_hashes_match_the_reported_run() -> None: expected = { - "receipt.msgpack": "5fd33b0fbf6b54e34d4dd19c5ff068a8f82bacacc21881b5fa2cc5c0a90090df", - "public.json": "6324c3c5d77ea4df4034f61131059289d5228f190d69e34c59bd7416fa9ac823", + "receipt.msgpack": "04813c4757ba4efbdad9d51d50d7402f3a98f6c23e53b9b58cce8af12ef9caa2", + "public.json": "188098e6ba1ac940475f15e0a4304ff08d678d98a9ed708dbe41dc6dde596b76", "self-test-results.json": "e4c1bff21fb6161221276157fa96af6661af8635da35970ba12e462881f2c6fe", } for name, digest in expected.items(): @@ -114,7 +114,7 @@ def test_recorded_public_proof_artifact_hashes_match_the_reported_run() -> None: assert hashlib.sha256(artifact.read_bytes()).hexdigest() == digest -def test_recorded_verification_pins_the_historical_program_trust_coordinate() -> None: +def test_recorded_verification_binds_the_tracked_guest_to_the_proof() -> None: public = json.loads( (MECHANISM / "recorded-proof" / "public.json").read_text(encoding="utf-8") ) @@ -125,6 +125,8 @@ def test_recorded_verification_pins_the_historical_program_trust_coordinate() -> host = (MECHANISM / "host" / "src" / "main.rs").read_text(encoding="utf-8") assert expected_image_id in script + assert "cargo run --locked --release -q -p vstd-zk-host -- image-id" in script + assert 'if [[ "${ACTUAL_IMAGE_ID}" != "${EXPECTED_IMAGE_ID}" ]]; then' in script assert "verify recorded-proof/receipt.msgpack recorded-proof/public.json" in script assert "let trusted_id = expected_id.unwrap_or_else(method_id);" in host assert "trusted_id != method_id()" not in host From 833b8ea086ee0ccec10737090e68054be0e4cc31 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Tue, 1 Sep 2026 03:26:37 -0700 Subject: [PATCH 32/34] fix(docs): preserve formatted link labels --- scripts/build_docs.py | 4 ++-- tests/test_presentation_surface.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/build_docs.py b/scripts/build_docs.py index 5cd2296..74580a1 100644 --- a/scripts/build_docs.py +++ b/scripts/build_docs.py @@ -329,8 +329,8 @@ def render_link(match: re.Match[str]) -> str: lambda match: f'
      {match.group(1)}', value, ) - for index, rendered in enumerate(tokens): - value = value.replace(f"\x00{index}\x00", rendered) + for index in reversed(range(len(tokens))): + value = value.replace(f"\x00{index}\x00", tokens[index]) return value @staticmethod diff --git a/tests/test_presentation_surface.py b/tests/test_presentation_surface.py index 0f3728f..887a9fe 100644 --- a/tests/test_presentation_surface.py +++ b/tests/test_presentation_surface.py @@ -276,6 +276,8 @@ def test_every_declared_document_is_rendered_with_source_aware_navigation( for document in declared: target = output / Path(document.route.as_posix()) assert target.is_file(), f"documentation omits {document.source.relative_to(ROOT)}" + for target in written: + assert "\x00" not in target.read_text(encoding="utf-8") ladder = (output / "standard/index.html").read_text(encoding="utf-8") assert 'class="doc-sidebar"' in ladder @@ -284,6 +286,14 @@ def test_every_declared_document_is_rendered_with_source_aware_navigation( assert '>Specifications' not in ladder assert 'href="VSTD-1.html"' in ladder assert 'href="../docs/CONCEPTS_AND_PRECEDENTS.html"' in ladder + assert ( + 'Concept guide and ' + 'intellectual precedents' in ladder + ) + assert ( + 'concept guide' + in ladder + ) assert ( "github.com/TimeLordRaps/verifier/blob/main/docs/CONCEPTS_AND_PRECEDENTS.md" not in ladder From e31bd77b532e8ab7e11005981edd4113a4f4f971 Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Tue, 1 Sep 2026 03:47:43 -0700 Subject: [PATCH 33/34] fix(release): require immutable publication --- .github/workflows/release.yml | 9 ++++++++ CHANGELOG.md | 3 +++ RELEASING.md | 40 ++++++++++++++++++--------------- tests/test_release_artifacts.py | 5 +++++ 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc0671c..ab0a403 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,15 @@ jobs: test "$VERSION" = "$PACKAGE_VERSION" test "$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs" --jq '[.check_runs[] | select(.name == "conformance-gate" and .conclusion == "success")] | length')" -ge 1 + - name: Require immutable GitHub releases before publication + env: + GH_TOKEN: ${{ github.token }} + run: | + test "$(gh api \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + "repos/$GITHUB_REPOSITORY/immutable-releases" \ + --jq '.enabled')" = true + - name: Require finalized release metadata in the exact tagged checkout run: python scripts/check_release_metadata.py --version "${GITHUB_REF_NAME#v}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a79d2e..125f048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,6 +173,9 @@ - Mark 1.2.0 metadata as an unreleased release candidate, omit any fabricated release date, and require the exact tagged checkout to have `TIME.md` set to `Status: CLEAR`. +- Move the immutable-release setting check before tag creation in the documented release + sequence and enforce it again in the tag workflow, so a disabled setting stops + publication rather than producing a mutable release. - Make package/reference status identify VSTD-5 as the highest exposed project specification with an evidence-bound reference mechanism, without claiming a real independent witness, and require finalized release metadata in the tag workflow. diff --git a/RELEASING.md b/RELEASING.md index 32fc556..0fa1384 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -69,7 +69,26 @@ release-candidate Zenodo metadata. credentials, and personal email addresses. 6. Confirm `python scripts/check_time_status.py` passes, release-candidate metadata has been finalized with the actual intended publication date, and then create the release - tag locally at the exact tested commit. Prefer a cryptographically + tag locally at the exact tested commit. Before creating the tag, require GitHub release + immutability to be enabled: + + ```bash + gh api -H "X-GitHub-Api-Version: 2026-03-10" \ + repos/TimeLordRaps/verifier/immutable-releases + ``` + + The response MUST contain `"enabled": true`. GitHub release immutability is a + repository setting that applies only to future releases; GitHub documents both the + [repository setting](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes) + and the + [versioned API](https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10#check-if-immutable-releases-are-enabled-for-a-repository). + The tag workflow repeats this check and stops before publication when the setting is + disabled. Immutability locks the published tag and attached assets and generates a + GitHub release attestation; it does not correct false metadata. Corrections, + revocations, and superseding releases remain additive. Enable the setting only after + the draft-first workflow is present on the protected release commit. + + Prefer a cryptographically signed annotated tag when the maintainer's signing key is registered and available. Rebuild using the tag coordinate. The source ZIP, wheel, and source distribution MUST be byte-identical to the commit-coordinate candidate. The SBOM also remains @@ -103,7 +122,8 @@ release-candidate Zenodo metadata. 8. Push the tag only after all preceding checks pass. The tag-triggered release workflow rechecks protected-main ancestry, package version, the successful protected repository-check aggregate (the `conformance-gate` status context), the full test - suite, deterministic build, installed wheel, and artifact manifest. + suite, immutable-release setting, deterministic build, installed wheel, and artifact + manifest. It then attests the tested source ZIP, wheel, source distribution, SBOM, and external release manifest. The workflow creates a draft, attaches the complete set, and only then publishes it. A second job can @@ -119,22 +139,6 @@ release-candidate Zenodo metadata. An attestation and SBOM complement but do not replace the release manifest, and neither turns an unsigned tag into a signed tag. - GitHub release immutability is a repository setting that applies only to future - releases. GitHub documents both the - [repository setting](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes) - and the - [versioned API](https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10#check-if-immutable-releases-are-enabled-for-a-repository). - Check the endpoint before tagging: - - ```bash - gh api -H "X-GitHub-Api-Version: 2026-03-10" \ - repos/TimeLordRaps/verifier/immutable-releases - ``` - - Require `enabled: true`. Immutability locks the published tag and attached assets and - generates a GitHub release attestation; it does not correct false metadata. Corrections, - revocations, and superseding releases remain additive. Enable the setting only after - the draft-first workflow is present on the protected release commit. 10. Let Zenodo archive the GitHub release, then record the issued DOI additively. 11. Confirm that `https://pypi.org/project/verifier-standard/1.2.0/` lists the same wheel and source-distribution SHA-256 values as the GitHub release and external manifest. diff --git a/tests/test_release_artifacts.py b/tests/test_release_artifacts.py index b04d404..7b54aca 100644 --- a/tests/test_release_artifacts.py +++ b/tests/test_release_artifacts.py @@ -474,7 +474,12 @@ def test_tag_release_contract_binds_main_version_gate_and_final_metadata() -> No 'test "$VERSION" = "$PACKAGE_VERSION"', 'commits/$GITHUB_SHA/check-runs', 'select(.name == "conformance-gate" and .conclusion == "success")', + 'repos/$GITHUB_REPOSITORY/immutable-releases', + "--jq '.enabled')\" = true", 'python scripts/check_release_metadata.py --version "${GITHUB_REF_NAME#v}"', ) for fragment in required: assert fragment in workflow + assert workflow.index('repos/$GITHUB_REPOSITORY/immutable-releases') < workflow.index( + 'gh release create "$GITHUB_REF_NAME"' + ) From dbd94c6abe0722b9ffb60ff92ec1c4f5b65f0c7d Mon Sep 17 00:00:00 2001 From: TimeLordRaps Date: Tue, 1 Sep 2026 04:03:54 -0700 Subject: [PATCH 34/34] fix(docs): preserve procedure numbering --- CHANGELOG.md | 2 ++ scripts/build_docs.py | 6 +++++- tests/test_presentation_surface.py | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 125f048..52a4d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,8 @@ extraction is not independent verification, and unverified determinism is `UNKNOWN`. - Publish a Pages guide index and enforce language, title, viewport, main-region, skip-link, image-alt, labelled-navigation, generated-reference, and local-link checks in CI. +- Preserve explicit ordered-list starting numbers in generated Pages so procedures split + by code blocks retain their source step numbers instead of restarting at one. - Require CodeQL security-extended Python analysis in the protected repository-check aggregate with only read access to content and write access to security results. - Fail closed on malformed generic-run receipts, publish their exact schema, and dispatch diff --git a/scripts/build_docs.py b/scripts/build_docs.py index 74580a1..2ea6d7b 100644 --- a/scripts/build_docs.py +++ b/scripts/build_docs.py @@ -368,6 +368,8 @@ def _render_list(self, lines: list[str], index: int) -> tuple[str, int]: base_indent = len(first.group(1).replace("\t", " ")) ordered = first.group(2)[0].isdigit() tag = "ol" if ordered else "ul" + start = int(first.group(2)[:-1]) if ordered else 1 + start_attribute = f' start="{start}"' if ordered and start != 1 else "" items: list[str] = [] while index < len(lines): match = self.LIST_ITEM.match(lines[index]) @@ -401,7 +403,9 @@ def _render_list(self, lines: list[str], index: int) -> tuple[str, int]: break items.append(self.inline(" ".join(parts)) + "".join(nested)) return ( - f"<{tag}>" + "".join(f"

    ArgumentKindMeaning
    bundlepositional
    destinationpositional

    &u6sC}zIlN2 zg>ZCFDQelb(&Mn{UG6IL=g>4_HO0ewsK;JlTM}xrE7kvDBWT8=zH;tP$i$%5t z-)Az;vVF;#c2ZD9=f;&R$)t61NhZG8NSzzls`HsX%6~<3!@m3@77?Y^DvMUQS)!_G zN|0^&6HTg%DMIffcHN}yti^Tf?DIQBF2>#(kzW3MyZ)vStho^@%U3qNcYV>l4dwd< z7mGcHMECBiV;f_aryDlxWh>9`MmdfPYlJVxR+>Z7$Q(`PI9*4VB`=>=kJjzIS|-bE z)#MO>xDNc08LB76|8z)k-zBJmk9$2J^zOKmUc`M*{cD>QO$9-{#kttY#EvMQ7%@s$ z^zD69Dxe=ON%h@rXOpccbaV_OdX)62f-3I{zotQ4vNQVl4wTdd46b_z9o)+rB$D^C zAuYVRc$WOU`j^tSXif4^>^vvglTY`>m-J=d-`)%ywA9G~{#l=(BrvmhH2RG}J{y4t zxkUGJx<`LFx#rX{5V5l~km=>!!QAy9K1{S-1YrKKq>gT+e&GAN<0- z=9puSF=mbNTg&KAZ}RgB#$2d!^n(?tkVAA;SsLY%3c zqGB9(zsOjiT6!q)7vV0k{i>2T`v#G#9lq4a#Nx;n2lZ`qCi1>xss+kIzzH%z_!pZG zpaw1X<)F_3ZN`geYB~q`QV(+(F;{er*E|PH3G~ z)l6{2lf{x_Oo9m+c0(7~?zhINl=xk{Ez3{QObJ>|nC zms#1RuIY}HKv)b8L9C4CCYFX{tp@#fxAMK#l-8TC@Mgg#(w-rZHG!l>Tm?gcA2uJT zUpIHi`ob6Yl~nLxkm4&5-2{>1_IdY+$~)TsvY_EAXzu$YHitqYxp`b#*q7l%`thGH zGXY=}qEQa*Y%5vH(uI$>p52S%*^+nG4;Q|vo4l6^R12>q6FxiqQ>}$bCn#fkYx|eG zqd0-bXBIJ*nZxt4r&?!fHrIKdlsS>`p|cL3AISn1l70M0Q9N^p#ae=^rpNu)9g5;; z+Pefp8@w%_@>GK4HyW2{KC5ElE0y#$i|vMZQmHAaY33Y6E}oSml?p4Oi0ylACU zzUJ%wU&r?_(tRp=eP;~F?Dn|i&do^DzmgH#=V%J}1mkGrVrzJ>^!I-qa=(S1JFL|j zcp{$j)^yP=l`&f^Kj27@UR$4+Y_@Opx;Bj4W!fzIw+Dqn#!U_x&Y!IJb(T4r`tYVv zRr`HZ8A%g^49pp4ntARnI$Ef-D-a}!3Fi<-X-of5z30)*drC&y+}pZNm+Xb)bsss` zz`*?}wJrnAp=m$8aehoTzbWjZqAI%;5>vHvP|V&I_?p?fFFX=h8Fm|SOR>=bAq&2b z_v+ln+qi8_ZWx{)$Cf>_Mi>akL!$$T^{-ck8T^db)btGDxrcHXesl7@dXy;X zI@ztp1HxnBn?{gmU-pee6_TGTifpY0`dhhAc>GJG#&sJQy_-kFiVUCqwpK3}s%_Hp zPQFqS_O|=CEx+e}5{I^*IIRA8?CJgUyX` zcRMz7ELVjl{7eZMwX1vCl4M_t=`G-!XFV!IXT3q5dLt#Teh+9coHPn;Tvkg--v&!hE*G5VofS{-amc_=Re`Z4;w!i^hG%$We^I`?uj#h9k3H` zfv9s)r+<*59|_2Py+xeD6Y3l)$XGSB>yQDp#7U+otQADmr#sKkQ)~KRgoLpBZ>wvI?>^~F}beUoy8EY{1E(2 zX~4H1&0Jw1g?+IusI`p7ZW1nm#(39iJF>PBCYNR-!U!FdJctJqYs5{x8#WmsIi^K^ z24FwEW^1v8b3VIhGf_P}zLHUlVu+#zE+4VLw@Jd4T;t~wPN#H#6%DR^3Xxnx%Y;Tf z2J9GC?;EIxPv|k`N8^^yV{TeP z4R$5oHYXo%Ab;Z{dH0T=6TS>d#Mowy=`Bv&ev_ZpszWE5(ec#yxuFRSUD`%FaJ#@B zbr504s3o-Mz71@Uh&H~H7K((7(I>EMO7&?>4( zFPTT-P&k0!&PHE6|JBe(T*=R~Cmi`?V3u}esK~})kIe0at{sq`YuL0YeG}$H+oCgW zQ$kkOp=>zLpA1jp@Hi*t#Powd8J~oI-FuFDOl~=X`+dxd@AavmKsfyv=}U7HKetFj zkTn7#;umrrM3`9j3|1kYTm*E+{uzJNCT28!B@#SS1SqHgqzJyDeRd>@yyZYdVSHW1 z*`SGuH?Jhsv0&vSQR1x0-JA|i*GSc7bk%fc<_^nLlE#icyQ8eksO32SFha}8yyP8w zCiF;w)A!TWwGdnWydWtRFgRI-w-&?kmIprf5(J5dB6Q$NPhZdiJuI% z4|+`e;^0dHZCY`Qy7_OPCsPGBFW!OEfEa;@wc=xl`J2-=9v9HdaWluu9Y(b91>^lD zq<|vUd%23X_jTHGCq?tlXGM>5&@@}z<$@5o+!rqeENye9*9MUjk-+&qA3MOJH|adk z)TfG1W5Y}S2O%Hw5Q_EaAPuph5G!-SKjZfv7O|b%33LT(@C+zMjP!_GZLQi(%?>7|~LV zUx9yM>|J)leIOzm{W*F1gVeDS>RpZw3Xu@vJ~TvMjd{nN;vLDrslO@_(%BuacBHTZ zCKpVDw~+dy4?Rd18{L{wm@`Q6nFiO9*v<7hW|4ec`TY_5d1Q%VIZ*+=CUyUOwh;3) z6NVWAq!3w=z#~g`y7J->iuc|0C{)Lpe8cPZyFKy9(AnXi6cU1SCrL#uaPTOZU)CO- z4NZEwMK`{qUP0W;!*E(Vi*;y^NwxZU%)Q)j`CRsTOHLfb$>P72cc*@IH+lD}`bS&O zS6-vz*4Kxa8@4xs1SikPK9@~eQKfBoj|=+02o+8kI^&q;@8T=&-mS^|SUIzWngN&p zkp`nzBMt1!3jvReSP*V|sQwBjy1g4~TrZ^h36j^1qTyqA(!|%=3c57Dh`8-W(@IGN zA}fVL#<2zLTU19AFWuoiTTqFNp?>DO;}9q;)dG3m)BBRxb%U3(B0VZRE6lus)|SMJx# z0Pi4cDWo=EXXar7dF8(I(%8WL$76aF5(M{CsYP$Ra|y%X5lmW%mj8E_8&*N0idfg`ZG`#g-w7e65;nfb*;G3x_m`PS7L2 zUz_i8F`xGSt;%zFJl7d%c%#|iaife}Xp7cx|0>>!d8l;QQFRpyASG07WNoZ1DU|Z{+tou1CwsX88 z(7kZHn#UebSa@ziXZIcjBO-GPzRZeAF~ChIzrS|ydVR1GvsoGUZg-2+sAsE9Lap`V zc1Ga+uIbOu)ucJaht)97+$`r$>tR%yTjn=HrAL<7AIH3oMcX<)DfT_kZKADTVzOAYShl2f?@HM=u}^S{#yiJRk5uHf7&E@TY+Fw@ zR&Tq+6CyoNW<_QuYQ$?U)X97KQf^I!dCdJK|5ZzQ=RQMQS^9?rMtveBzn^aCcTzP{ z(O3L=jEd{Pv~9v={Uw|&L0!06=~ncEs2b7!J0tARb=q#Z6<-nb7tSbM~+`}MMCPI&y2mi#!x4ts>3L(g(m|hLW605*2Mk6hR^gjhQAPQ*NmvQ@?6G7``>f?GDpc;R z`u6mleQ&v09lAYg)t+9`eV`L+x-m^Me>OgDy+Oa#JNxM5F>&R~P5tGDy!n4L6MK~;-9M@bJGcQtYa3c0EcYt51s29affL%r4 z3fEo6c6TznufpMQXk&~A2`OaI&8foSR5|HEF*wWFxq>x6e}e4s2I++(txM@@mX3|2Z`CLkpqEx_QL;o@FD! z*w~gPk6hl^D9+Z{6p4oA5p~O3Tk-vBTw^8xK%ghR5=M?#dCAkl7-^^~!x7jT+|vnL zxeu)L4wNGGNC+k-lHq1%rOp^LQH&Jd2`PijD7%^!2ZHs$T|w5>a{)D_vmXzIu8wgT zvd}i&5;}%h&`jrht9gC5ZT*qwdklhf;6BT_H7F4(gCKXmp22y%uG!@1&$i%29bm*s zFH8&MehcV4y7z4TjF=G=An`jvD=~+;cJ|u=FwfDXpfvW@KN<&AgjZngSHUc$>|6W#Y z4F9(f^)QBkBA$9eyL({s17StNX1Q{ z_WN$=jkHc>NM%1BGR^hB3{}e7Ot%b5>OXOTj7GSDqj**Vo|VmPVBZ|1M`TO@q@nWj z0MZPG-{v&>K~($?wT=xL*?YUf1phTQhQjJHRiO^K0uo;RUp$A#5XL~4LRc};8iDs$MiYV) zu1Ijjq_dLYK`(5t$>joacx&jFcVWN*4ul;*>)1mAM`6sGKVRhlKN{d{_+KnQYMo^v zs(`DetPbL`JdHsfpQa(;Hb8r6ja?~&%30^8D)7*%?quP5EL45=2`Ub>>}>(HVxw4y~>SJrvxL$KNZTI$=ECm5U(!6M{;k9BxJ z^Qd@@$gd!5PrUyngiUl)dhh_o=DL4$LK*C1*|v+otD@)?7@Q+P+}Hmh216s#vN4Ro zZzdK$GW>t2!=}9{z3$pX>yIrtt}C}jQ)sgJQN!%>=DdtSTESkI(O+w&p@dHqL$Ti3 z6NZw4Z9dazgdCv6hNo=vu>o3kiE|2$gr8bY=kvH-;Pb>!uuav5D-t@`wa0=$zyv{a z*6q-F2ca275Ak=vcpt(Tu32&LjWWX z^W~2YI+tn42oNQ}9Q!wy^^zMiX3keT!2fkqol$yA__Ls+3e*q~HnXOiprlHT=*&t) zMvfbucmMBDW(yAD?@W~AFdeTnuLLE9L?pjf-W==#f{e!)LsNa9R}S1>mMf<dVqpwsATEE?g6zxx@6h`v+_Xf%PT_^j7#YVi3jZr) z7ewuE+LL`*@&7a4DwX}O4Qdlj1OHi57@80n8E!Ib332ffM$Fl1#q%x#fF8sL7kKhN3D#gR zSIS|3R|{uF>k~6~W>l2Js>PSf-jO##9R?EO7w87jqBh-Ci2 zL`aE(=HYl24y~b$q%2!z#?2hzo>+Fr9EqAUa#6dSpz$~Tnq5spL)PeN4yw0DHaEsx zmXmYf&vIyvijI6GfcktGgK>#prjPwC8ni+Pajn)}r*4A)ttycul##UD21L5AWEz~>zGR(8_=^CdX&qq^ z21@wt-ghr}aOxx4mC^x%MTUc_mttdQ5l@4ub?(~WSUpgRMK69Bmv=UJJYx}*>rB&7 z17;3n4XR$4D10b&c+36mdAPneMjr*oOHB|hSw;_RpVn4fs&eq?BAoekEsA-OwY>!5 zC;16+L(#VAGAKyCFF@0M76Ts8%v{<5?WAw5AD>%fEl%w_py2ZOn|6YdisjV#(#v&= zwy^;zlTZyS9V0oT6km<`LG?0WCq18t6l&#;B7zw{CAN?QYHLB%FGZCIXT3AZke9Sr z=>!B7WOYSJ4@$wVc+17#VPEHFr&CqFUZ}#w!+aa$l1vs^}PmWol8!2r{5!^SGGxsmsp>^1NZ$YIo)+fXJnK#)v62ea2e0DJBSvCar8 z;3u&gkv{`do{EyG;KeYAk>TV^+(%%|uG-&}dMT}=>g`^U9ANm5j$|{c6#r1M6B~DCNyHV=p?40!x}qE>w5Y+Px2REgP)PvwQ}mp zezmLE@3(MN*YKeslk*`QFsbE$j+|UYUk@R1u#gCm!bf1!@*1*rCB(L;0h(#?r66Z^ zlPsE}Fg6=hFKoCrMk4zGt;m9%o6|cNTXRXP~jG~hNLuHK875fGgy z8z_Xr_nnJr*b>1<2mI1U<034&Bk6nZuKy%>GXlpst=m;lF28v!Jmj!744aoB5B3Nt zzbKk+?a?Yaadk+pT)mJ*QaqBU&Nq{vU7fuPw``$Nm(dcG`b&ZB4?eaDSw)Q-aAo>@ z0aG++8xO!60tX^d6LMg!B^kwdlGz=hBC?e7TSfUPg&!F}H?p{JHW20!dS2K0A1H4m zMWn?w@`c^et%T`2zi%q6TYYQN1Db}O&f`1n(|?!4HCf?+K51H^mys5e(J^AEQ8)}j zv#ig3VWHW)-SZVa==1fj{?2AKM+vk-fOa9!ORWgy*IuILV{f7`-G$*{Zzgo|9yJq< zfj@gcdS=$~+LBBQFkGN}<=gFXHR+Vq2JdN+nuEo@*rO#I*Ja1G50PG8*qfM4>Y>SC z3+wSk7sw`d7};Z49*v_7#0VfTvvuw(j$hAaH*6iyt=mvH6@a^rm?s1ju9U-Fz9?C6 z0M)beVY!Ul!;;+2?Nls?{1g)h&#pQ#Ljlt-Ht@}P{rMrmY;i3ZwMQ92KP$T<(ESo`}1tFaetzl&U6f>|od59HbB$-ACZ$)uO^Dh603(;F&mARNqA^i;y?B6M*?c z4aY>rDUrqSjqHicM!10#J6u>SU>c3YkNiB9n^eRse=dXnP1QUT@J{q1#1n}O-?A}y z6iDoSZQ)taG>K zJ+-(D0>1TIM49L15zREr_1w(6dG30g1?%WTw~cgkHpBjq`>dG#W7=eNx(=tjZg4wt zMILL)Ro)sxHF5Y!<>}+zK!0Gz{sL$~Ag;orByYgBXob=AzPi#f{-&HTo3Z1fd$oOU zY~{?`cT!f?%bu%l2E-`>=o9!#jxZZ9dM5;nr7#9`t?PW3w$msW2d4-X0ZhiB9(N=f zX&D5|?*{$RFQ_4fPdM-?KK^id#!7L%YwlxngWK2;i9hK&rb{6PH4Nut-y~@KsI8gx zu%;*`-uv#qQ`%hPEla>^QTHf7-v_IoC#6YGmo0%`v_}-bg?1{@)44$K_6-(WD4VrX z9o6{xI%cTdt8o zA#$G}h96I8O^UPD*ZDOc&*@Gs5zay|Wd@puD)th8w~|&KDcI-L=<&RZlxYwTS;kN0 z_z^Og)(%=45*9D`O=X%PVtE!Oq{(+8AEjQb6M=WM34gmiauXR%&VE3_<`#aNa;C_1 z2dlF?uTg2QB-{gkZ@5#|x?h?(LX|Z@1)d1Y;jno*$xlu!zN>mR>6^~17dW;bdb+Jy ze=6G284Lf^p$Rj04r5*I7n_Jv3(q;eS zxm>HXwR}&nQ$I&D+9F1%q)Kt-d-be(dR*z17w`D?~_6?zkGNq6K%X+d(s!2 z%^1vCpGo(!i0E{^`P$1$q+#;&y2%Me-ERz!Ta2tO`z zrQ~87uNl)d(=ocN2dr3*GzQ0{X}Z;%D#=-;=-bznC*8wr96_CnY>ap`qp=Cu4|dz- zyoYivur|J|-aHUu+4+ec`nKs0yj62VNyEQXV#KeYz={x6{j1;{DRiV!D<|uT4NQ{a!}3Yv51n1zU4IuN=c1OXM1Ylejk*LcfO?5 z|0+RU#B)zTk;Oj8%u~Ve{2~U@f^rstql`E`YK6a@oi8%Bm`c<{0FeyhfTB`M@S7HJRKlR#`@n0$dvzQBH6eOXxsJxga8@J$oLhdK)VSJP0(t{&Cf0$jrVAdA>j!o=Oi_Xc$3HEl1z;s3+P$E%2tzy~>e7?yr`~PO1B;W?5BP)qw{g=VO$wC;BoN+VT0biC){~%$L+~f&&Z#=k!uc5M)9effe#` zc!N!(GDR_`Z28!{6 za9a@RO^iU9asL=2^7iAZ(8#sA;d#6Q1YitJlgZyvXh3F;eJ=97M!f(PWqH*^Q_RG~ zZCrNnyV@mDl?dAy+&7+*kW~fM?xU*TIDcNAbfn(|z1JI2I;O?d7Y>G%>A9TycHeO8 z;rsiP;J7iyu+!6s{Df9$;n3T>J7G0Y`UJgCpnT5-w=f=^V2{qD-7u>@OYQ6h5;+hL z`3y6KPdeUX-v7IG%bb^a58HDs*e!MOJ4a6^sQGF;sk3>72;GUsRl1MW-A`3AP9xfu<=e5@-6T}ccKlb1Qi-dYN zcZR-X(}_bF8n=9UZbuH>o*~|!pW3%@QLcmQrsS}{pb10#ZuIkkrYX@W zuJ3}7dnC<(O4G6F*x164EPG?Gc+ohZpsz z_O#|^JDWZV;GrF&Op2W{w zwr)kyDjz98beycT>=1#T8MOo41UNDWD;~p)o`;t8jV+qoql@AM9eAqXrOi#Vx}^s4 z8Dcu`;z8?6t6NmwRM@~K6S|=4GZv%o#9_qaBL%M*a48J^PEeY=CA3Gz1+a@o-cN#e z;t~V51;oNW-=ES*+f{wO^vLL;coe37PZjXS>_lP2tWT$E_d$X#y6&7Asdn59oan?8 zW9O$Q+DTHpBc}4RH*4(ZXuOJg&_83zyne)xz(5EvE^F5Bp~!>V zd=Fh$Z#RB>x3bgRsLj=uNzaItd zC&X150SKfh)!Y2{^>Eug0=&r<8QDReE|(1M#j`3!7@HgOHE~*rldSOXQ@D4g0!3}x z3hoL{`rMDA&yk6bd3dSh-pnGNW(Z`R(tZ29QxJmY9JOQb?BM*M*B+wvgXMdbVcJ0x zs(3`wffSK2$zocgezOTLp*o{5Pajo|h#~H@t#FW{78RM~<<%oy1;SaS@JxXz)H|(5 zf7R{;-l*^5O(O$Sb;gAH;T3g#q*JSy@h)hS=?`@Vl8?l4FUH}EN!XF__x&f@bpt0R z(^bg^bLu477`A0vnBcaltT@S4k zlkj2SO9BHQGpwnhRtvo-5-!4PC+b?7q*U|feZJ|CY+Fqpr=D^BP}!j{;D(aVoxeT19%U!H9>3E+e}i1oHB~CT=h(grrZ}6MhRNx zbM`AM(U}zEr}!K;qX842$9Ogj=VTX@k~d8P1Bqd;WvGpN2F#>=SDwAwCySQ zqOAb00(^xK#AJM%xlV}r0hKXt<}u=L>2+^#M&oC|b@GgG{Sxh+VV!CJ39QuypJ1W(8ik<|<6^=yC-t1q(%CXbP z4B7#P&^g!qB=M_rUHXHB880$B7@gzU_#*Qf0lMX_z=Bxk-p6va5vdRQJ0{H_K^Z;J z=A0|?R!ZmXOowD2iyBWu3EVqAzS@L#6}N3&M!gI2RiTa-LF3==oTK#ths1LCDnJb> zmr}>^wcw~w!44zquf>E1tO{3oNNeXHdKAj)l{0r~#1lXK6j4?bwBI;uh2DB!E%>x6 zw>fRJ#K$JZLz1yeAJKf%H)td*m17_kZ=7cALprmdp>|e&Q_~I{oU-57YAyp!%~V_N zUaB_8mbpsb#Fo7tgK435D`vM?%`WT>Y=afr5lfc8PTlpF({m( zIxxpxB#Qx}gU2SGvOb^xc>w9>gIchH0Bld6;i=|%QHzWD2gHw1w%FmIZ=1k5>nU`c zOF?#0^TBY3(*9>v7CLGHum@lK>erK6laY+qt;)hFKudCu=v7D~Yb>u;bu&OYNFxJr z{-`jg$dMb%@oD$pN^$zRMmu(>&xhN()~|8;F+_JpiQoITrWMcERlFpah7QfzcRG`aL>rzwmv3CGz8{Rx3qPs(jb zm#gSBVH7KZaXDmr2`(U8rCZytHBVM^*luB}!eyJe`PAwNQc5DJpAL-;#V4~;&)+Gm z$Y)%2kTWu+`wPtIOJ0LQzlmvTZOf&9vgmuMmdiv`*HDYL`rWbLxthvnvik=5-9VU% zPuVibM_xe)uduVgZoci&j>&IgsRwB*GdNiu5Vz+7%t3Pk5qWGWm00{&JEJ{$Mj{eT zQ#z+Q$C&Z_*`Od74ZelX!0m>a%&>BlU&g!}bCt?ejX&o)+jEeftg&)VTeigpbvLM@ zMxlwqVD(#6*Ii9AXkbaFK|WMXM?S2)(xBviG;~sP27^ajDaJ8Rnq4vbcq#wb4dP-! zB|ZxU`=++$*!mU$Z2AxM!mrIJQF|j_-#;NU0e2V8CX@8uTPjS7WPy+ZnHH&Pw3BAY zU!yQ3C67omz`D)f%J!+hDS))yYtPl4{Jb(A-sn&}9%p z(Yupq3s(k}=8?OXOgQ$HFL0DPhq9R}R>StnD$3Io+QwgBXNX^0J^z%3wvE`lwG#;% z|BbF>dQJK}WRBdke~&i47CT*RKnA!AKd7t`C>Agj8i;5PFr0lZ649eGpsar~Z)CjBG_5(!V5+AVV+g15YJNBK90B!L|uvKYlGCMlUu(hNT@d zOn3b88BOswtr^zJ!Dlt~p&!q*voDCLciNv(ti)!#BN|>pb$u^pCx0+_5|$gn?M9|Y z736K7AgI>pUGWTkW5eaJay4G9()WzV3pz5}!2If?G;p}l|J=v;ksK)&Cm;p*ZbgSv zl9u%0Rjjl+y2SNt+|rU9&4&AsS=nlZ2|7v}Ml^Ezuu1OGqJk z9@(6hS8s75eem5o8;tgyydvSfT(PjMbw2CUeH0{8Ayeo`0$*i9oB};3(6``adPPH4 z-(6!WQc{E%^O}7%$~rp@0^dYaC3mu!Xz}^*??h>gL2fKYV`E)}|BPQ2`#1fg z_B^5>7_;+ju#9Z;jBRByd}8JVc62@rki+Nw)k&c2^FqlJ9T2tE;TY>R(%B1{DR(01 z9#c86*4*@&Oa2&g+?>EEWjTnFu;`w5Y>sP#FaAh6<{{^M`&@Z9IERERy+4mO3RF0R zy@kXzB5hidCa-Yjw=f)@jBbzEFG+N1BJp_!+(qytB-^he)p+K4GR_{l=JteJ&>x-= zbN8z8I6P4n5zeH9X|g3esxRmplC!Qee}vmTBP%#Md#g5|61kGC^NO>fw8Z|xR@`h zk-x%z{Lz39PBPAm1)zxxN0pbpwMmoibp0g*U#*eyy-fn>St`d-X(-iSY)b+x5(I+BTIq8vuHvI$jqf3aC22%qx__OzL;7W$IHSz%DwXlm>>gdz!b@Aro&5Uk4OBa zus35a63rj{*QN_Na zDT5w&lm$&6A`83d6R`l7Q;6mO6+i*d@^D%MhVJcijsth~N4SlUu-;g`|6&319{y32 zEMjH8{}GWfpf~PxNMpjmuXPP;)>p;9$=-*R4+Sj7XgI3@tKUz+_j0da85;#th* z?;DAK^2qNap62^o5E~3HFK2wQzNmG_@HS`kAVBW^T zhY}NflyZJ>JtH3S9vDPyE?!C`n?6*8KTxS23bI~$Asm_MrKwCWTkX&&x9hs%z>Pxt zQ@TS)h9karY}VyzJX2_Jx(=N9n!9mt>h(K?T&GFV*o79bEW-KFtz+D049(t~BzmCS zp|enf%-u#Cw6w!c8JyGp9TZn!zdq?!gwiMk1cVV&BpRQ~woQxw7HRvJ#q#Tp6DILnOi*wHE_a-H%H8}jTe)MV~r=J4{o)!)V3g7Q7SFW%3${veh^i)h@G70qe!X`GnMeB*O3|}Dsn0iEcyMX6xAHC5CdQPVGa+%{587)LrutzzXBkX9|ji25k^O!52XO31b&DaIrS@H(}I5Y#G7H)u`BsEU;^>_&0<2P7K$%F zF39VI0pLSVLr&(LT~~G{gv0ULn?FtAaY^CLjT0seVM-5rv3W3-#{ZOOEjy19!yLHY z?z?b-r${e#12XSpfS_yy^8MLioCl;)I3o{E$8k}4e55|%yd2b zieR;kAb^sps|0yE0kecVwKEkUg|Y%~za+!_=+#_XRONf{kU_5;=>{xXT(AwC83^U( zj;v$nnSlb!pZ0!%v)?Tm)Nwm+gZxcoVT>|5jEc+cvKt{hTcEGEKjevm?Jmix=@TGn&$5j%B*zbxvr5y6rz^X9F!9OCFCCYxtHS?oAm`<74{I18N*I)Dt#nDMbV_ z6L&5w(87X?M6vrazyd6nQr7udxBUJ!TNk8VgTujZM@qMW;1LpYmk;2$4vSO&eqhm} z7qS~{P39He2`Kh0qtURcyygAdX?^kTJ3in)Dg1Dy2n0MI1ZeCVEq|cfMV*Vc70zGGB%mlpoekbKA@h0j+AbR5VD3SG{$7} zxGW>sQe8y0@p1D7g3LL$uTRa2CN^Mp%>_Q)4NEm-*pnA{>N{|G%65b6wUWn_`7RfqA~HTQj_RA#Rxtvvko7+QyZ0tpO>e`n3alW(80l5kY|PtL8VT?(5Eu zg=waMM4dZyxY|pHfL%(H>HVO4i^#ZpFa6>N5D*)Z@?V&Q&@uxKjf0{aLjuQ{3>Al3 z>6q$wDASimm>bg5*KI6BE^7(>e1b53;}+OMNf=p!sA~NMnO}2xavmB_xUpDne7VQT zJ;SWtv>{r~vngHo zzh8DMXBmI*4Wzm~lcs(M|4;k~4C4J8y(IAm(nM_G#oYDn#-lepj3qZmp`#g=RmWJ#a{x?ws#?`>=-q%&G zdcxCPGY7Qhd}bW5f{mk{x2jj%Me9%fw2QVM*jko0FDX{fCm2qPy%r}VsF4o zS%5{xErUfCn)y&CGZ7FP!-y;BLL?u)k$Fo^p1B0%mRo>Lm--=WAQvxc5FH@V2&u~P z021?>6%RjvtS9+jBpUFyBY;*q$WD!Cu8#_6UO=2*HpFs^+5Q1yk@*x`K!C}cf@xPY zwPy;S@dN^ssn`%Rjekk_L!HW?&f@=xr2d88e##Yrk|H&iSIX3IWk`R6U=6;bRjC@g ztixrF_{n%b1kM-41a$3=7<2r9_&2z*%Ru#iV5w+eE-)EcOM??bLm6<4ovALP_c$i5 zUjbjga#R=*4Kx0BM6IJ;{Q9D`WFFZ3l`(=tQlbk_&w~Yj~Xrte|RckvSIhn&s&$CagE=TG-k7 zRiDG96P6PaM+qfXm+8niWR=AxqtJ`70r9|Xom+f75N0vcin+N|8&qA1cv?u^kToiK zp90nFwuRe%KOEjbtZrHQ{ zI~JlHK?k_y@Rki1C1q`Z;8*NrJ6t;*-k2ldxvmo=@3gFq#4WmdLI=J8=IPU$>97_1 zsm~_eyZbDPsydm_bRR7pYHXtln`A7SoU&MG{}3Q5Ra8`O+H8@C1`vV$7zg!}#x%N# z^F9U)8kdB?qH8nixh>!Nzc9Fo{SNRM1cO?_2nl1!^}Ux9p^lJ#ScV30M^JW9?N#v@ zZi>&=>jNB+*KY8ws$TlTQrpLWa5zXl6FBgpu?r3$?=YGRza8vZH>i8h@`oCMI}~yN zgeq4%PdL<~aWhYMoD`lT15X#m%l`u*H`5fN=4OTsy(7?!NQQ|Iee8T09xVwoPl`P4CLQ&Di?!Tb#>=4zti_?4T(L#f5|^o`On|u6|@We~1UtRj|~$S0mTa|)63t(dBWKw_qRER(tYtXlr-+QU^uxlbuB zbo76M>C&Rj2fD0~n$pF}-i4O_F~&j5?Jz9kQ_)CcDHr%y*EQ?%grO6~9XDNPea!qL zsR1JenXj8PCNN&z(1NU!)Kgv)~Y5L+LW&u=D@P~CaTzE7@ zI^qe3q-3-|hwX{0d#xJj1FR?HyAtP_<@_P1P6rFhig`JmgV$<0M-U3Rs(B(r;D#c;2c;FpAbF@GRuRrL znHf#)Z+Zgo`^m4+H7NeTlCjCdpSGxvOTTeKpwRxFh!PZfJu03q2*%ZFH0^Q^cCkS{ zM=khrxga&;<3pXhtzX&Ef)n9`$?KsOrPN?s43Euh2MP+!a#VDYuFMmKIq+Iu|KTU& z8`IMbVq)OS4jkA^4a1gDd9oZNVrwqk?j3kFW%b1P$w$rdHctQ%k}`Wv{?7BpE|vWf z++59qoBou4B#)d}!I?XMaE*ruE0HwQzT@}KC%36>o0lmFqW(`950a>_v)6H*2jOMM zpxQD0%#kSYW@gQK&E}ox3YE_NA?4_EH468r4r|i^IDV9Rp7#rNZ+!Ms1z~KPuHKg< zssh;9@_cobs0_1m#)Q>AS))~hFBdFXCPm}nWLXHS)DL#QRxK@#Ne1MXZ zRksUj<0md`84Vb0aUDj!7JE7W!Q_=3ONB|E#swxumPz{lBAND4{i4#7(=qemQ!Ch^ zTouAiQdU#2A8&@=HQUt>#w$-?q|76oBzSxkU1ZvS84BZhzFSkgKi6Nkv5H)L3toJn zUcySuXqI0e)9&y6Zkn18V&$4=#3vQT&V-OKa}zBaTd>(bUl;7ZTS4Ig2rvw2 zuu=2pNmue+4x*MMm_B5bd80J8jhOWj-^hXb`!vXel=ehwz{c6I#5Q;1^)ICM(da~o z*Qgvlz>Dvw64^w8upwR~yNiSGq@uu73@SeZbG5IaQ;h=?<_nSS?q+Ow zHns7Ik($h&pwJ(WoaSvRfBZr9YF=etl#6(m09!~#R-v>`m{InLfv+ZR%m0nrvosuz zYrrtweF+ZRPNFTv2(!#6byY!8%&kGpdKaX1vNxD1Umy_^fxX0s%sGalcd5t zA0Nn{3@hD`e-+jK%Ytb^Tt?X~x-=8h>3RStXw0%cbnINXw#aV@w&L;+6PEoGtO|HT7^HY-3UaC=UW zBljb9@!8q1N-_qO)BP3||C{VXW`Ef){IsVHFoG&$K5l=BJR=e!hz3C!n7ak_5kH2D z{3LgeS#d$MR)u~o=)9ifuG47V(xyU!Kbb2dvRV8UP(83FA*3(5NOSYS0Lp`imhFTr zcU(ntT#|5y@<>)NG#R5kLPvl#>}buVbf*8UP&n{J1O?j}zJHC7a|tu& z<{ny+^7&~bB=);A&mvr|q+=^kqYKz?Pp0ra^|^ZrKX?Kz$OfrSE~2U?(cakD64!9I z9LRU#@C0$3vdgb}>QJ(G4oXgpqn0~pam6+Tm;Mh`UjYeN2 ziDq-cA`@IOH1DQRptLd3X;>WoI--JOx`N>lE+d;{-)!VdyiP1ECE58y?MXKbB)Sx{ z_l9ia;B19o4fk>Q=0G&!s6J7*^MVKXzW(ZsPPvn29IU~svNc-U)r!IVHp&1}U)x$aF#@<= z{R29j>C~46b?tW2(!E4s&|XfIGTEg9zvWJRL~FhJK4Ldiu4YF{ndqz#dSKMlId8-X zkGoob*{RJ{0vf$ckeUYJ-O^6^02|w~SNEK#5q5T6cc>=h{b8<6jbk6BX{aMAiuxo+ z=P;QWz~)O3u3}i~8d71@oU)28^(ZiG^pMNpudi<{m)U}h;woX+#6Kl(U`bm37T!d* zL&(@Cu^&u(9c!OpF4uPAIWCWOd5dmE5|x01i!0P1n<5?a{rB-IIpL`4&~ZC0nceUn zbCBa776HH)%7jN|nD*oLCyS_^+FX#N#ULvG;=9$5l+>Ff?iyIowJH~<@sRv$c7?5~ z#@(PiFFy?zfKy)n%rLHGtl)bHzx-645G7Nhq9>a*O(`B6H=)%3o#a>I>;1F7+JgIs zC_4Aw28Zwenw*VbD3gEH2{OEs`fDs)Q1Gt#l300wIlt8U-!8~LXyJarVcdK(C#R8X z$Uo*~0~o6}iUGCZ;RJ;DY4r95K)FRZViSq}IUZ#o?hP1?&G_?#XF!5>KC_MJ6?rbS zjg1z#G%U4%88F0wuY#8g3aeW;*VUBC!74|~5Zb`vS^D1Hd`ke)<;$$u{BEjyF|<d%-E1!k?XUL0itm5j&uW;y-YpQ$2lluwMuTz~yD%mu;)v{O%sZb? z{Qc0|7zMv7iq0_to4%19+SdC@_iP`DXyT-*O<95@pT3;wLL z-w_s6z~^6^a~|?-uPj`S)Vp@p_4DbHN1jf<>z^$%o9D)8=LfwB+_l&ixeK{oZgX#h z4jcL|kL;qwoHHwI;@YhYCFOfd8emPA$iF(t>E$w_8N)ld#R+)sU~Mh)xgB{wu|;|e zf6>#EN^NGB%trH@ln0az^*t5CJ{Y)?)Kio>NF^BUU~Th%8cLLO~3$4XH}x`CbBa!r;*%X z9&Og=gWt-1%hYo%g4499;CA=ihiGUMI7b}*r|2QM$5i4s>FnFUlZ9=|+3UG_k(g2j zjfR`V7E}7VjRis#(@}WP$D!@=`;F2MU{1wdj{VB%o7WWwAn$n6kXm>qL@lkYRbBQX zlub@^mQ{YLSk<7e{`cL3ttoKvM0C5*teDMRZVq`IKD_ywc2yc#(BEefu9$kH;N+k5 zpG;^68NTOnlpRYQ23zs4F&TF^Y+(od?uy2i@+ZR4`HPTvN{`2z$@r99oe}{9y67J< z^5#Z`LQ30fL+qq}S$9yyF{~>I^bDKhRmT3{^}4Y3dY8dbZ_K z+V8?dNXx1oZRe}TZCU;*PO{4tdeQpD89k1#ec&jwVGE%^^6G)hQsvw;pi<{m)tH;W z$~FJf&wtn}xvkPIm!EjN0{pc-bN34`1CZ8qyYK@GXEB=ZiKfm~E+$1^K%LT2+odV- z?B=Sao2@nXjUIQVm)9b@{1Gjg0k2MLYp^HG2LF&8`W+di&BX@mBCH3R_~k*24jP{1 z16>MEfph0{skhm!h~G_5DWH4OkHTTF?;CtDI}5a{R^#V+(~4ELwkEC1@Dw<-GO<3p zf%AU7a(dWNqW0Ae5e+tq?ew18B;@vo0o8T3QD(-Ysl@MaN#36Ish0c-0iXq+iqKUF z{(<|Vg~QQ7Ay0a3dj8h}&{G5CY=aN}ouGalvh&-xwTK8>`VWD!*tj7I^*&xGzsa~Y ziIgb-H#>3zquieJTrjiId^SJ&g2#Sk$!rt&W_#k2{zKfsS3%fUAr4QZ z=j_V&1M@cvu1~`$@zf^V()mxUy%8;ZW?|cct2eg`8WPt$AS!fR!dPGf_jJ`p;_O4I zSj4R4SrxF4TX03tEt-=Nn-ub5k8$PO!8bog@L7!fF~qryKWB`Hxd#fKe#(Aa8%<@$ zpKaBBctD-yj~oAc=TUX|9J)AB$OdOOVD_1Mnl5mx|kGqrMr>o7EPUBUMy)> zj({r^btHs(zHB-N75zOIIMsXpRXe;`Nuat8u4`*GQQnqcgvJ~BQNG>{?9LzjzT!N* zxYHK2;iN^4j%yQtj+^W_it7vYWT}`XGoUj~nN2Ge&IY3sBJbr$dK* zlGvlbZ?eC}L3_2U&aDscn~#5*J}90FnDG3)T#U^>=nAAFFWwc7#?}`Mpg$mhUeEq{ zPZikVmUwf^8nD^o$7hTS09HMI852GerFC*ysW2dYh0AzDYB%zQeI2rk|B%`91aE*^ zPw#>6KCj){w~VCLPFhH-Uep%AfsR{%mGL3H{)bS0$&@-vup8oYsl5m*_|z}uX9Rf` zBzw{w4_d>Iz*+g4zPD<%pf*KXC^i-R5qM{2F)Nh#raCqM{qB*@A-(yb<~~% z_s7gGu?es4qesNMqm>9K^etMTo*@AMAP|NcN$PwxIK%YA1TI4$SADS3W1~lP3qB%` zMjyW(2c>>*vF6)Rz_AatOvs~FxbwBSN&5i4{{X}`UEJ2RO=~4bnFAkPvxMfJ*>|-R z#=ADqolUfl0v1w3F6b5APAgisYXB4qfJFYhqSr($Swt7MAe8(~la&J0TRVJ`Df|wK zpc)CkA1Hbe)$){MOiD1EUoURoSgY3B?LoY)TCRmtnqs4uVanDoG};}NMFlRH#VYVF z3RjNH5+&kqfImp|Dd&0tAOP0L=#M`9vsMa5i&9d7Cm=;igSePd()bHmhBTN$v@lJK z1(Ey}#nCf&KuRqF9ilx5ZLl^d)%=jl%8wQfqqhY8t<|5RguRn{vDfLa{NqbWQQWgX zbhG8p3Zgk87P%yC*(eXgRyS2u8tKy#pY~SpL)^q&I(43n(9${6bw^MX5t=l=+-ZC} zxq0vPX54FZ6h`dac1(Q$4{E{hOW%2r5zr2sMN4~1-xY>3p`jSRsBRWpBj3@}J;$~j zj5ivMC;!C8O2R(3e3m{fg3hlDmQlFrzWGKHjmpJg>xO^;!`}%`vuVpe9XES?8qd=I zgW!#C(nlD$Xi6V)IXln6_}+qENBo!1emCBSH&>}8)3oCtq3EuG%+Rp$EMFTGE~PZ? zE~K2PKj)2s!<+mrywDpkWMJ!uZeX(c6tSmoEN{`KnR9&p@4lX^r`)ILP0G8ec74sW z#$A`MXySE)r{?IdDIfoIkcN;@aX;!zRF7Qg%myHe z>&qz^8L&I29Br$n@Btv0AH6GIEzW}l#@!DSq++fpnfegoq^LtWqurC*xW6?PeiN}D z<%U7})yJi!kSenFK-;16;M+Mpp~c1x2P(0+ z$jfG2!9CQ}j)Y=>Zl#2ZSh2R18JZ_PDtxbZIZ^s@|Hor{?eiy*^c~{7EI|>RSJ6N= z&>jNd6i~J5o?$>qXFf2CL}Up}cII?8v{7Z0@ZT~p5k4Jlhx)0zY6^7{^y@5_rI6v^ z2;0L$Eq=6i@7UBf-l5?T{LU2-SEp1oMnoZG+stOI3^1yUu@}~y+Ix;5*W|1Ri4VvG zxWb4-sp^YqX^Sy}ec`o1?G9vKo8=Cml;j0${yZky!~_^_R6Im$oAo9!>tW+f2TO&; zrXG?BYs>Z0FsH1C{q#T2Ce&<{%fZAZ>nIJBS^7ya9fHQv_}mI&I4QBga=+HPUx7S6 zpRk*rTsu=hJfmozL&o!BkjMN?C+CX_yz@M5*U&*O?W^TUX_&6on)+5EkQ4l>!elbR2GWixS@VlO{>j%? z+8;-c+Y6=q#d#PA_P8&TXcwx~burVlVTq-jCFb(0{)}h8KwIO|x@hONVhF0s%x4QczZ!I2&res@4y!2!ca33tuWQopyj%CFGa?8ofkZ3 zXclf7n?Whu{XGQXl=EcTrs&+eZeHd7Ew6;rn$=oTr}bTyOE#M<1&^H&sL0c;!e#6i z1Ts$P+)OpqQ1?80jX?v8>sJo)riaBRbS3%JjaY@Jbv^r$6{=(Lhqxui|SPFulp%%z}Gt~Lr_Tm#Acn)SgR;-0r4v1pkkp@ zD&2D^>5n>39ZS8UK^H4MQ5XIUsjgPvjduMT+MSMbaFM*DoQz1S0qcXWO`^B8rj-at z>s|rlF>WfaBU#9XRyJ|0ua4-cUhlIdY{CnVw^zE7ZdJ4H=u{O z9^~q4bjyS`UCWE7x|q0Xp!I63-CVHx77I7x^}&L<5tr@EBqWEvx{;=~6u<4yT|4BW zCBNRiT?uMVLf_p+iSORk+*LK8&DuH^T#F?6Iw^Zi^Y0O6623>K27x?g7NZ6Yfh%E5 z2`pm|k@VEAB+Up6T6+^Iy`{g+hsZI_er8y9{D?D&UuP1W>bl;z(b$IM>_4(%nt=i{ z#v9nn&r6f}pYyG^j5fjAo_jRViG35MIlj_e)6Sjo$Pm@QA}0~go9MO%Wh}KyFpa)w zsb9cE$QDOXHkImv@xEK9xP36zffe#-v#^`Ghb8-WC5UkPL-Akt!<)VsBs!ip#b;xR zv*)IgqhAV`f9^gPtiNg7uM`*@CA(*0N{DzbpHDdEg+ShKzxEF48^jiT31PW?4;&nr z=ftw}&wQ6T-*>jLOH3h*iZ5rJ9=9arzpAhefdB_WR+bPM_2}{z;6N2}Myy}-EvmZ7 zp8Y}MgYt26i?Z-ZXfX1V)(zfs~2w@=L;!Ect;Grid0@u!gMPYjJUTVzTmyFu#cD z5I~2nIAbXNQ!I6Nvepi`Y2+6ZQ}Qqaz5b#WV)>a)b(=6R2`CuoR$U;PA%=c7fQG0T zxR|u37O#fwAM_7EI_P5tRv5ZAeZMP;;zL^k=0_jeAdlkm!-8d->AwOrtfw^P=-^k1 z^|K6@AF;9oI)siebnt_uRegn^o!T3xCC!0`gy)!u?Hy z5;aEyYePU>g3~qvRewcQ-1Pi|Z}T{uxVWJA1?;+?vdbYQuWUyb&D66Tzs}$j`>Pza z74O*Q=L#aXta$>~*3|wFzRm)`0(7=NQ3FsX$Y%%hx32J@U8R$*tWJms{3Btb$oYs; zdt8$4U7z~wTE14-Ga?I7^Fof68m$O$SZXLpVSb}ItKCz$AVmEWGPs%p`e*=ZzOiIh z<469(1{$A$ys3l{Zt8GbXEk7F$)9`qia98NKIt=Z=we=w?dr%cV*sk{IA<7oHRKzm@# zBCwA&OP~$+nCVorhe?A$#bWAd{I9_u;`c*Fva%szkn%`Dz5E5$z}KDK-;Q;YeuBeC ztP=mb#$X_8OW;Xq{*17Z2`dsD9sTrZHa*^X)_CjqYXZ&}ow`|a{frzOkbnOxU7j{aMBRDst?V2dFakMu#1v(H!1g2P4bhHQ=f&IhyaN!zNfLBfB^ zg}xJrtG7O)tggDDi`#r4CtS=m7(1}NDbmwZ)N?^4K$Mq1^=zQRQ=L!B3!QkXh?DS- z($)T@nqL|h`e_ug-t1n%H$^e-U0d9r7O}8wN7uqitPpbLg&2XORjy>8ef&`y0BlB*x!VojFN<*jwci1QMFb)qw5 zR#)rkP$<3vQxKu}m{kM_k`Pdq?4tZ~7)qfI@zC#Gz7NU~cXmGQl6Q=|R?tvzYQ(oV zHNn`?^WC7&1Nb=4ZDeef{wCgsVUsbhzcbj+7Nb7B6pIy%;d&_+3&c|qEZFJj_n{#1b$%exHQv6Pr!K6MC<|RE%}o$x5ydkLyjT-p7W(^_3r!;9=fx;m38#0tB=B}3r*aU z?zi^Ka!fY^saS9@MO5NgUv3OS7(rnbS46_W1wU_x_-H&RomobV4H7o`VOulJVlgK z7CsnYQN^5{OHXVKo$iaV_5Vupf2Oa}aq1hBdTdF(E*36>oWjFAe?QD`U6PfSPQ!3R zj1L=~jUHFhAt50!$|#27Nmkdp;!lk+-W}C*Xz(nb1vut5x=I5Ni0NdkIa-YZOZYZg z!*5<5_-F_F{g6Zfh}kim`SpLEA`b$SjMPO?Y#%;wo!ZNzPNK*%s_-skU_IvJe;&qn zboslq-lcS7P6)Q09VqSoasJiLL-7<{N>o5EwYGoJ-LAasnu=-nj*jCVXo1hTh>rxh zejzu!9m|QsH~eMjc;uIfrJ=gS^RG#fG~LE|M}9)=;<1_(UtRW=QxZTN9{Q0G*CK-6 zXGGmWw0cml@8-8vx`>%8Lgp2fR6wL-w2i1Us}BlpUNxZ^^#_-1p#XU}8J!7nyf8W> z@VZ}Dw@g47(W_P!?LL27^E3f6P^jUJzKk+FBRWNMW=h=L{iXiT_^ot3G^suz>DGPe zzXOj1{eW8DsXx9?s1{zH*I%-Q1N|1I|6o&t#`ESrKdj7agqsn2LAoWO(Dbf`EHN$W2n?#n=3ly z@nXe)zD%9*wlv3VATqpdHQZaiq;z?^PicN>1pUFl?$jTDu6w~22@vsmvJrLLtHWjU zPyM9p2J>Ug$xAQ6H%Cn0Oq}LA@Bj{DVuJKlE?@}4nYE9PDLg(bMc0YV_8qzHYxK`f zkh)wf=>B;E+ZiRO0wk$($@2{_EJd=f`ogdlkVPzkXG){oO(^@{_xR!Ll0Mapo_+%4 zOmOFIeu!OAxKE->0Cp)l5PObTEiRz(|8X?j?TdC8jv9J+SVA^FI~}|F-C>V^^5dwS zX#Xs`3LTgq7^EZ_u3h36M4~M((6(wNQpqIfV_{1GDXh{+^0YG}*H^bxH(@zv!uC3& z!N@^X*Sm>mmjrKez-G9REw%+D$A_1eA&ItLNVQ%i*~{wvrW51R6X0sUOi7?`!CxfH zl;g&TPPtvVo#D1bhG0xm)Ep61zPen6ihpKTzbsL_?si#tt|5hzV4Wm9q$zuXC;3h; zlDMQ127ruEku>oFQd7iM4_V4C*?&g9D3^4EcnMN{{In+i84&qzB`lfNyaY>1_^4s` zS0h+bB475^(FPZ$QTr%-uV{h!p`U5n)IVG8ipYTsSkJ;d`eZ)-+BhJ%VI1lA^o9xf zv_~sc`YIo}4j1RFL!x8>2r~M6Cju&@koSQIbT1v`BxS?cI2XtYDLs`18j&~HTw6Q? z>Sl~z`1*C@a9XgJOFjd)5G{VNkd(GGHl?Y9u+A9qgTMpc%Pb&A_9Fd>MoN}vgnW#o zKbYqX;a~NzwWSh3>Ghu}foX`W`3eSCMUW z;|}C#`5}t2;cY8yh7PwlzWpOqi(H$`QGkk+8NAgv{ea`}#k+oF?@4J#6&m6gQ%S?$ z>R5C+lJB)GN7Z0UQR&tD0>k6=uCilZbsBo`8pQ<_r>PLeb;?{fEA4grVfgkthxKBh zVPhwQt?{fqClB+-YLN)r19rgvvpU`bI3z;fy+~FX!J;}cbG)cQ~6iAM?TZ1cO6{dEI z#;p@Y%oPcA@$iD#@Dm_Fs+;-Sbe^2733@N8pBSve8i}1$bqN>%&nYQMuwEUfdu9ps@q{xa|@UQx7 zWO;&2IP=*@h{rh(C*3<^-auo2V^?fF2%LTu=O?AJeffzd$0VHtwJ4Fx=n^WlI<${{ z>8C_BT;b`degvZbYSE&q*1{b#Wql1-47M$KZ2%?DcQpUf`^(!kz$;gyF!AWG+@l|Ch*55(Z95T$X4J8lipZ zFhElZSRL!$6phgz4FAe@ZdCTKkF@`{>DQTG0L1)T@=wi_Zkr`4Rz7*+P$E-0+;C>t zsS_T~g;9rPH*~%1|4+^;is998nDUKgz_0+TV;T$qidlh7&gexxj!9gSW+CY;X!U4O zFY;~Rg3U-X_+R;w0~`(j!@u=kxbS`Sos}`I#I^h%c1r+uX>btq3@lu%IQB^mVPshs zLbQn+N@l*EAiq?%eD4F{0BR0bsPdQqb$w4OnE$VW|0`vQk>hEVUef!0OzsSP*JP zLg-2}B31v@^ZlV2-Iw0%I`bb{E9Jew%w5syQhKRsdF6>4%|p@||6lzmi2ScegIRBN$%TW@D{wu8;t0MAWVZGR5sY*gA zNh&{ru7YWj4rvKmEk`ek42bK@o!Z(;@^^z2mP{9Ok!9oG90L;s;cNt!H_cN|a7n~; zZVT$t%YB~{dGq*oA@1SJ7M`8sWG(Ohp%p6?vhU1*s^@&_G4il z)&`2s?qG_>3;<~>MZ>q;U?iOt&S7}}G(*>eo>13oZP-8H~&l=@c zBc}^vJGtutUI4t@YXi|R`D(e1M-?oO%yo{~58jJxx<)V@7j0b#r3PG}umoOGx){>; z8Zw}rM@zv_{G4^G(Kx?XSzw?n^)}wxcs@Fax<4U|Q>KA%r?9@6Jdm7k-Rq}oxUKA^PsXr!(mIn^t^W5ek23E6MyxU5&`YWm&}KNuZ6)UPqwUafhCLSg@! z+~qb{EnKqE*qh4|7^(z;@e zBbgbvNsN}!(tk-z2~Eo=a-VfR9o9EXAQ=*G=^ZB%BFWxzme5haK72$q9UZy&ZH-5T z@(K%SJF{((~!->N^HN5UCI`H(`X#cd|X$ zEmSS0SRaTNV!C5M>R6?s9~p1%XkzMRCVIkq2p+Gt_V$0oU!C*cYqP4%J;EGi=3F`n zz68G?ePLN(;dQ@LtncqH&RH4ML2{;(&$_(z1obp*WHdz7oCv{_wD!VkM$4 zUDrBl6+gmqVr;Sl?u9n0NvGVL3Am9F-s9+(30NW)C~KPip`Gf@avNNELMP>k4fl@i zX`$U|_+L7x`vM%g7txPArG|q-OZHxq2!S5`uqs;0$cJ>{Z^YHdaqe6Ebv;#9_Rvsd zJv!(vA7+uXI)JM~u1lUr#a2JlHl3aHc2#`R6j`|sehVWGuS4giLNBr*($YbZP*k8T zOy03QZ|*D8DMzPK=u>|lsABo15*^6=vkhjyUb{lT3rva2p_l%vsuGN8t$!TghYlh?GV;)Cn5U!GFeU5@NvG03e^$ z{d$H7&NT+~VTl7Q3=tX1?3$yiu-pidJ?79=_T!FKPy;`L3zKZ zJPkR@bSHc<@2ctDkAyfN!XxQ9Gtc13taalB<2hjZ78GX*>fK&=t%<_UiE12Acq`k* z7_7TK*rR+#;U!&4=uFg!c5F2_ zDl}P+U90Ux6e_sGSbc~#*$7+Zktxa&)qCt?_6a<)nG$2l*l1MP;AQ!dYd=7aeUSb? zSb%nYxwV1HV%3P79=?p-AI@VHE)H_qteKR-oa;7{A3cL!&1Yyl37xcsY9cacY^b$9 zlA~)YHKatFTr62@|2-#@2OG+EsOXs9!%n#W2}&FCd!;?Hi&#^z?awNi7abB9DZucw zOVfUZ;6&+@Fgs77*}*oOPwFEAUqf8}%agKof^l9c_TGoO!f3j%j(zsP1nh^=RF-ph zKmqn7ym*%0zNSb{w-bs?D%8GUn_b_YSd%Va>hm zHS{)>uUIyy;nq=@>(2fAq#H)UJsMkq9rpV959K`mI$(xzT^(80_bsV zloO(UnHO0PBA{L(;RXn&>d+lcjs7(Tf)2~iDdOTVuZ3^H+{U%eu%NWn9uicBg1&B( zo~ZWky*5nO#OuF#EbZlLwI4SRks?WDfTy_G8W_K zTrjfLDnc%bu*v`znL!YS6{+?jne7xVf2m-Xi|`!OpOB9|KgzSYFHr|*B?Ytg{u|gR zt==d^?n?IaSN%|J`vcdpr4Y@QAs?riMM^KRrUu7YsEOxu(DHdM7LV_E{|Dy|1DBgM zF<7QS*sREV05C)Xy?ygzMT`h~>xa2|9wgR%#kP3n^oKXSy2V_(>5KAQkVOA$_1O7*U(7S9 zQcU5v4onekYcQPHpft?tFpfm3LDn z_B)%8N!)x#pYZ^(Yg5WOd>nhjyBOEI`LR07^QoMnv)z|s@VQ{DBH$6qi3U0`vAs8( zpVH##zO3?Y?Q$-WFToc4box&FAshPFo$@*mvVI-=Fwv<=QZ9O3dC_%-|97vl)_NK* zO{7i8b}g|JfB92YkVZbmn@9;dOp)W;zUMbO zQAV~KR2va8p+sjvfY0T@l+79h%=p(vx`DrlS#+k`7(4Vl-Lq$%v=ndD{c+AwLyRHT zc+SN#$v^}=ErJUDZn~S8zT4~VG#`kzKCZudGU7dsU!UwP*7w>t_!EkE*IUYdr-^H3 z8|9M@HbtiL`|(F;3T2r(nJikb`0W8_OGE(sOlKp3!<<5}vCyly{6;v;xKA7WHdkAv zp51{p`Yi+w-ziX!sRq;&A9mO6o_FK&Igv}N-nSGwRoir!K;>|$;BE3BeWdCu65J8) zfoiUN#T@4h)fPg0)>bpvx+s~VXA!z|%qNeRl;-2->}F&jQ-;r~5Ca3EnEmj};NA^u z(AZGp`=>x(>|f_tqncMvJK^WC>ZhEN+cg2wuf~q>8i@e0jRCy>r-g6*1dbz zwOTbFwC<}WyapXabnTYIn@_oJJLi?=lw=>g&i)L{ct%9Ys`(3+51MiL8VGClyUvpt zL~=oqxIQay>@T~c@#k~Ph7xr51AV;qJxtdKYJ7KmU;aA3)e~!@=Kt8jMZ6L7%!R_& zkq|<(KSw|jJ`mOr-y8d_`ghJ_5L)a^m7j$8?b3dCRCGSeo9=KvJAl3=OKS<%^n>z} z_bTN-z6odRfsO_f8QU?P0y{?9hG#(+%xOJW%vv>7k^~^b&Nxy~NaHYZje{LES%15J zOl26#nMY@@ojV14?hq^=Mmdr@!Gw-LRFR|E4?L(lxOoV#ID};4iSFUFf%Lp&;2zIS?{$1i z47lwCH4>74Z!;jE-mTKqsGMr5SxW(Gh2_IcG(S(|^RI_r``AG6HW1sa!?v!c#J+Fi z(G5#zrM}7M8cp@1??cFi5)tu4gJtYT0|HPW# zeQ}nKw<$pb*_9}-^J1EH@qM@{y|yvhTAf!zpI}crlTXvjuSSb(Ks?7URw225(!l;G zjp0|M(Stk+##S9eY_RKi=;e<~xCvE8^|;Pwy3i}xS8wdk95UCQ1}z>9 zCuF_|PsD^&SWaDcZj2r;W<)Gid)2~#oG3Q}gSJ)+{K7E7!zHo&*scw!XNxu0@7e}@ zmRT^H;?6}h9%G;e8ph`vKmKGT5=zb*%7CsCV|JOw`F2<@B9di@0y`ey_M?Gat~3uiAJabC^^+W*%L=Ge)m*W&=@|FKeIO=dUuy@< z+u5QrWQmQWsl9B>BCefNX&3qD@G0&uH#E8I*f$3(jhHdK~)>@w3b-V;%HNGrlRMm^f|KGBZ5 zE8^{}$2QGdGdw0m1Q1ck)R}s3HbHK)hDd7p&zUymWxD`hxZVnCMrE*>CN*SzBnx?d z;PKjYH_Bv_E2OVU*X)q0i&>+bOlAWe1IxMuEhJ^rb+tb6ex+3H4y-MJ&X%#uB)4Je z|E`z42V1h)Cm=|rzW_g<$6u@5<+l3%u4t`?G^ghx$jF#%C;SVxMdy8;Vb`ql5hb*u zIfnD&U2#e86|>xGqIM0ZH` zOT*Ml4E$%43{%lD)N(P&`Q|3Dv4z?io&h)be%(Su0)B)z8E+jO${$q0e)8aHSMSp$ zTV)qZ`IUK?(LUamb$+ZrW+}3f>D4s@-}b=QjYtKu&2?IqW^?LOdbh)`*S9#tLZF5? zuN#WgBx{fGRXpZRQhdv>bu#>a^tKp)1}^%(oFv66g@>{L%rQaa3_u^936L*EBEXbtG8&K6h0$_4~P%+Gbfu5sbpG&l#NT93D) zax=tB809)RON**7$`J8`Y#T-l^wbe`cU*3_1PyySh#oK4Xp`J7;%ttxF6}3BTCm)` zq>aTGjv@@*89TqYfOFs4E9DdvPwh35(Ddju^HdD7u!PyfH@njWOe1y`gNoDT{bM=G z1!H?uV<0%fK-&yRuf`Ne}o}5 zv{46TRynlOU}iDWWomNP2G4fO{0RuJ<6bK^Xi~?!sRxWY&22mKcQHNt-#T>>b@HB5 z@=mX9HkRB6B8t{lu_$v3;e>R3##R!+EvetP5~w6kj%*3JPlTP#&Uh65jHT+q(j$}H z8w$m2UW1%f1MN%=ip0jV{sef5uVXK!)f&{H)r+39xYapVXgj723>9lNxsr_VN(M??`;s<5!Yx}EOwD|!PvbwN zh=$ms;9?lbtgOOT=n0Z@Q?d)Q-FQXuq2p)N-$ z8#cYTAJ^L-g$2P%PhrG{*;{++Pb|_?O^(`xAdCq{QK+Y!nT;p zx-i)xP?B6EJo}Xq$s>omHm*wnnFrZgNM-64`DCdcH}m-CfLL#T0$NOezD|yz4tHb~ z@`W0`IN^-lS3Hza`k0V$=i51A^_)$DlXil5^xzd**+yfnbwT6gp8=0_pS%@!$g>{% z^;h@9XDQuIy=WYId=SL3yZxa?AkiP0<+BBZty#WbCG%_#K4o}!)e&<)91Ereu-fvR zBW9_Oy?VQ&)psImt_S8TVHbOFE;$JvW@Lok5oEj77XHy(6`tP1Llqf1TRw>$if&iR zQ*M010^udfA4fBfH1`+jnaC@U5O3o8-RXw7m*Z<*@#tyNoRBe>yU23B!$1<7eY>3Y z=QDYCbIe@5PPQnL?V7RJ$Q$<@4$q?RG-%DfiL|>ukPexZq=jWsn_;;&0elktQMv&(3d9a{3GAy%qFyLW&kT|Kq`;eq*q@{$d9t>C_ z5pr&+6#!e?#(#%&#v4kVQy-WsYb#s`%K~h*c)Vf_zQ5-0K7qP!hL8FY;=+SB7h+_V zz+yp~H(pyJ0<>y1q$P%yUE75w(iCkat>IK0FxAEw#@-4Bv5H`ok0X;@26crcNZv*E z;gkbK%USV`qx3%AY(H|>hIhH{gs#&QwS@SdhL+j}TE4Weof1qSxFF6!lPBH-x#Y8; zuIjTSw$mGjh4^=8ulGhkT5VKqXXDWd-qq0P;I~8Zn|Dg_7Cz3=7uw+Eh5YO6O%d6s znMEJ6cSMns#h~0axXV)&`nvKXqNjt82$3?5XoK&fsK>1gS?~>KwY zXphp#Y`ne+9XXOD*F3+9`l2VNx_SVYjvzylUu5H^1d-D@!PQ9hv{Xeg4F~~Rm1>IPESSsREYcO8y(T=tg2n)87BPu zI)5h$3S@s~@;BmoZ}|*KowRG-t+(~(ndEp|JZ+CvsOR^$v9D{1f7G~M&Tr@sFO}(n zo>Yq^tx73ENJKzy9xIQ9Sxir_B0QbxG=kjBtsa|QG1vt3h&Nw=~h0elQ`1V z2GhrD_y<0hi{Fx)C6M|7Md}Y!NzLjAC%ElJm(hP~tB^kye~L~n|MZ?gOri71Y=;_raSe zs`v(ST_BISc1~Wh!%H}+u0n~{b)3YYRz-yQY!@-L(rpcWA2ovIV57~RQGWUJY&pc! zbH}l_ve(2Z-5Vo9_?2zv3_aT&G?*s5vo5cSwC_=NoD1k4#R_$*`D~|^XfT{=WoE-DH%?=_dGZld0`Qb< z2>j;)Aq?;`6$1_vo#k0RBDB2WQ*8P`TQ=_F+=V-qDohg0aysxK=eLe z^7-knoc)$({C$CWNseL1qJz%$vTz!FO+WsqJ6K2qp(|mC&$MbpOQhLd@6obio(+T2 z8Dmiw2>r z!|5T@Kmj~%KuZ7x53G_dyl^)pDBXpr-LHxQeRePFF`|5Lkx+M6}nqp8oxCm1a9_jX1@9@oG7Q%4Vv`oz>L#`3L5V{fFBg4a52 zchqYWlRL0J(gKpB4wyTtXnNQ5!0K;P-T3c!NDpYWey88oEB(e0PvqC#5BSU=Kk;Ka zI2j~SY#*+)IT2(khT6Ya0682tzGj71nK{fgt~E>aJf^Pl^xUz!fj>pk6jT z`czpTL-p~-oi^hY0o@YV`zUlCvDWjeHJft{Kldsuv^!N)JuKF0C2x-Yz&SjIuHL0{ z?5W1`snt4H1#Fz)o+3VYUvHRyXC^(JIU}8IbA&0>puUKQHvVpqLfif@sBV`iy)f9)=_8aEY>_?t( z2DFRNvTvumk%XtSIixzDDQzB^;Y*Ez{@zdmG4M9K&ze`n(stV0moE-4EcHC968ZewsL)T+rkF~G zVA85?F02w4weYe?zgP98ntk3ft=W#c;|v1=4Cx-@JUn1$c?Vut*yu_6z6phlnFA&* zU_*AL{RQLJN0K7bYH^=l6wB~(Q;N4+YKE&9S!4KrH&4LL+}UHJr>!eK14-T zL9R&5?L?-a?jFH!xz=NoB@o)z!NMd6w{LHFH}yX39lR1~t#MNE4)G0W7oAmJhzK3@ElqAGtDbK&H1MRH)YF7h13W1bi(y>N@oZgSq(Om%iXBIed0YYb zIgzjiPK_N-9q^ictTRXY$+fQoi+OKgD{1Trbg#i)Xm>S$&7yXIe;t5{r=ra#B%S%= zz6@7k4`3jGE{C~#a$zElGQDR0QFiK%_oDpl^T%B}C@@LYto()d<#dk+RIu{d_4;>n zFuIG?^xvn&>HWb^OZVtjNB< ze1r#l>vE`S!ZuwKrh1}}DMe1aLI82npl&m*N^WX{*WtPgTkmO)VV$CqLOAN>g<|u3 zAd_KpSI0a6_|J3a_c67Hj=mGmd?8kFeBR9-_qcK}R%|9&;EwV0vZaX4%uM{z;_g1N z24m z00~?_a>y;~Bc3|F3`y!X8Saf@F#U@+JI3>y;b>dK^RaN|AEinmRQYmbitRanhnT=; zfdD%Hz64Ibke^I*CK`gFrt>n`ISg<{0G&HcEH|+4|7EfkZ|mqdNPqW82B^Fv&Ek>m zbzvcNF|4T`w}eOX%4aaG6a5#;%_sd~2KdGWq02|D=k4ntpgYL_d?)~Rq3s2b7MKkk zm1y&Ik2N3wG*0Zd)L5{+p{DI({{p}-V-RKk3&FJ{;NszHy=JEVGP=SH(4T{YbmM=3 z>`8!Y`Uk(2zqPnplTA*B#D#Fw`yL(%39RhFu!&==Qf$b|jY5}kQ|-Q`g{U)#vV9m# z3Xm9Yql=AQGo)YZ{ld~7n&j#_;PruRe9v#`w(u7QDWO6d#Ggjj!PC61q%zT|Fa(ryN=S&gsJzfpxheszm|vLy5Xj1R+ZlR}X!Nzkygou>y&x)3XLDK$TOXdv zW#76owNquo9e^)C{+%*N^a*YvImd+;tQE?Zfba>2O(bH$xy_XjjQ|D>LK<%WUG#B} z27pqkqO|D6s0@(>ZiG8}zR({VmfTH~1IU%86N{7yuWRCq;8o)Tg*QM8O463;BLD=} z+Cvbwot4cEpbaU=WR_;-?{+&-zuVgf=}G(JfMH4k*W@%M_ZYn&Wu5J1PlAaI7IU)>U=@GdOmbMUHvxxZhtcMW88C0hHRh=?eJTvVX) zacRO3YQRgR6-_Odv;Y`EgQx4lrv^qe5sKlRUlNY?y)dnVh>*QBs+gWB`om&XcON;x zXbjfFv;w07xeOiZEdk5Ss+~bvYX7QjR)ioB$_VgCRk50T&~s09wb8z>KauwB_@F zENUr|1?H3j=x&n`v*AD{26!$H8uv;)(cPrCY)tdk+iS7_1U>u~^@A@72CR9$YR_Zh zOYOFCPgVjf#ncI6-y3>sAbIS4E&J`ZB#q%>Q?WTRt*rLP6d%h|%%&fRYT3}0D?-3m zF?BHzE67hqumD^A*;uq1;EVN6tGt@t1rHl~CzFQ;eCwWsbO!ais+BfO%oDl4Zw!Y%L-$AjFaMr zl>f2-QE)JXml6fB|GyZ~{B}pQO5bAUc7)XCJs%+`D{!PFIB2(w5YN=ezd1Q4N$ z_Vm~=nAKsExlh(j9LwYUd<@&$siFUolVNlUK#5`g-#}#m#C^kyuX`_N?h9VicAumP z;f^481Mso3dRm*hu%+(^YLguGFZPo#SqkKA7!|IOM9CsuS!tbfP8qs?$kYExtA4O& zZB|aXf*2;!49l6VQ(l(M3$UpB`jOpw$X$&_p5;1NV0{_qjZwPS~EY6ks7s-Ws|k} z-;^|%n{^aW(PL5IJmu>R|B~wez}r!OIQ9SC8z-f3Ad?+$o9?8w?r-uCu{%!s837iX z`Z1)&iivNPqiHeXl!9aTL|3$unigCzfauJ*(49i9sbc2<( zpIIrWY{O_1*6sxcNmKdV{hbJ^rFNzV${TQ4Aos$py9K>2MCIckH|O_k!z+kfT>d^An9>MS)j_ zPQH7^CK$k=r9e{~axHCQK$_8x*8KReX3z7h$rDR5NcekbXt^W?Oz1NhrtUem&uh9S z{LZ2qv}7Li)jcp7U|OHLsevdDm_K0&M7F$-YbNku66-7w1v1SI8VHks)V6li;g6|o ztfNtXp=k)rP}*GfHT7bcAbpb2IclB$#(3tKxX?uo~yMzbOoqh-zB=lP!>Si&$a zhUu{dkO^rN_=416n<7PS?*SW(XAZnd2#5I^TAoW`d&_6rzMfHYvW^Mw;VcB8q!I&t zQ+vnFxql4^7=JVI&bZAr`>Sv5+YVE=dRR$ao}!TH(cdR2+OPoa#C&#@1PC=J>mp>q z5j=7eWAY-i>XmIOiF#ARwN(3H@Tv>QmdtBcagQ9@$hd{dJk?6P+y@Yg!p-s9fNnS4 zX@3BgL@vjgGxYKvbKMQMhAueMIuoiG^Vp-BQZK>P57Y9wSb zr$2*8I-eQCpcD}C_J9kHc%$2N=9}<%K>{p|pxCpA?2?o}gfry-Ae_gt#a^m`pqle6 zH^~M2C@flN7`56X10a}c5v_UB6N?To4%6*^@A0cNYoeSqBf^3SC&*nmaw zwS(x`_S*S%t54iY!5)p6@8@>FQqxEc>4;|j$1=UD*Ac(@2E>kGVvAzLYOo>*&hdg5 z0B=Z^&G2-W2z+C#nND9w-fx`3rr@K#NYyElWe zeHel1oABrVNjdupmWiCoI<6yDPre4IQ39@Jo(C{PUeN-EoEHJeSv~Y65df%%hbF+y zxBqKq4q2sKTH1yq}g?!%1T@5(-iiJ9qt68UVdK%ma!CTI} z6rpfW5X`VxHVt9^4CF1_Ceb8Ozs{S;%kmL!UI07ivlvFcq?)9tFL~Ex6ND0um=F!Z znLy=BYHKUJV8yW+XbXn!a(_sM<$rClgNmDlX zvYqW1&(e{X$OZA#ulezy?fVV&C*+$qS9tF>VmhUPBZq_Ps<_Puz@e3g2zGcqtnX__ zla$$q8)8Vq{0GSV+Kr8|VzAQ5!||`5ZStOWfw+#5i2^v__VBpT=MsMd?#&wl#k|!s zEl1vtH;ZRc9y|IEG2G;D`Snr=0r>~DbJ45c6KYe}8QX!YaQ28%d#xu@Vf!23bF=6& z|GFJ~Q!2`M_RkY;6mBmU#G|y92V5w9*L`j~&}wDC8$vTCcHnjF`Ge*mkbIgovZ_P8 z=n9+?uGEJ%0hHc>9!hBFRIt44yCdC7l6p(z#B-tcr8+P-@AXl110 z*}E4?K!DF%pnS>>$UgZMpt=|SFF>@6h#HWf)t6vTqtW5gZA!$ZDYpf zUCp20h@=oI+t}Y;RgzPASV#YT1|E!K{|{0+Q9{W%h;lV(9O6As>x4bv zQjJuRUWqv~MJ(mu`7~4O`~0#an%LXkMEUDp9?EICrB`d9G9*`{-UsSa$#jK)PuU3y zNPD{`4~)ncWSC@P-`K{NY^~$j9qhGYz6j*VgFd@;d6yN@&ia%ne5;gEbUmEEw&m~& zLn@!lKN*)L^~Nltp*9$x%RN4{ka(s3YMe&zt6nd$3To{8m+SWugkd$ppYI?dmigYM zMOk@6${&Gcv~qy%xR*pG++5K3mz^Hai_rx(%wkkn#%95rHp+P#wtc$MY`=OM^G<1S zDiZ~sC>uwR?{;=~H;y{r4^-x9)pSt4xJMQsIFzpT<;-keGgDcocXUW`>?3CIeTR&= zstgF7l+IWt|Dix(oHE*m%jAtw<@4mMEj%63zp{RR0PKG^!0>WF*>aJC(*{SB zLmI{I?-E%ayYXunK&-~q0H#`U<6hXlYx|8z#xovo#ytvy=?h2A%djxhR1PR!fEpe2 z`^q50xvB~i0Lzm#N@J!i0sJhAjyWW>9EjYEU58?HK8&n-7@||Zwd>*rT)35jSzX0# zIFcg7Bwdt|9r7;^EM~PIcYT4;0T>#nKmLI`Ay*l*qIDhx{FM4q?;cs2hV^`gULv;7 zuBj;}1L1Tie!Qo6f9rQ8cxoedoG08JX9@}=ZI1|(zFeBG7k?S3z$f8R)U8QQyfUo= zX+M11_nvf=IQ_0^Fq?Z(k+!{Hn(yTngV!89w-b8#2t5adoW)FFpi_o0hMMsQ&n_83 z-hDjK;i7O=W2>40W>`$~N!}V~^j-Jk1lLQlj;Q0#m5@Wg!5OmyJzuZ$GSIyC5@E9_gxt_N$m zOyQf4nO~7QjqVOqcpDeL>;Al~Z3FhjODY5M)oJObJM!G*#b!%tgEd4le>ycd3u@=P zW@d?~jthBJIee@#<(QLQo9+((%#+Wfw?5YuUOOUss05pJt3Kzh;R8@SUi3D&SBX_AF0BZ2{%ZN4fY4teKPqESURRed6-0FR39nz36euKTsdL*IL3Yw@r@asp;|&U1QKc$$rFfJ{;xDgI67eKWDT89$Bg~VY$ zl~hD=*gz{RcOa_Z^>J^rH}|84nOP9$3GsV4qyvG(7JU%`EYiPTaxcIS=^XK$|5IdH6Hs z>+|U=y+|5F9g1bBa*n-O$$fTan+5AqMJBxVC+|B+QWpSMC-VUYxIM(GgEmn`fT))u zq-faNrgpWtFq!7)jFP4yyYwcTj{GB9Y5jU;gZd@f8fuHbSP5H}cmZp z)S1mc%W2QHM&dI9(D>cU@C~6P=rbReySH8WC7hsSstQw7zefEwVjK7Qt7#6huGiQx z9-Zh%VFxK@ss^{JfxL>Y-f@*`Fw)_A)p*NVeohVaerPyTw6D6ww+})-G;n)X0OT%& zj8cT>F8~IwMNIN{eTl(9Ar{st>T_g1+xD`Z0#L( z5_^(bph2y~GQ3H=h=hEQ+GKgr7vl?n zi@@3Lyq?bR!$Aw0Kinfg&#@P|Qsrp7{C*YD)sD@|+HI8|dyMiWIs?)cuRkiR18v#1 zRR0&B#a!M^{feqtu{U-hz4v)VIs?eR1ODp`{R0cAd1y)x2vWdmwlFx#XDzne;7FvC zF3sR4X#v%G1)bt;?m(+|+`JtVrmPob-*Y0fNlrJtX~o%jhqLQyn26k<=sw8bUTuBM@@i?? zXn8|0tIE_sV=WOl3a%=IKr~Namy_)1T=1Kx;251QXi3^tHv*09yxC3S?#Ba^{5SU+mgbTLigwyEJVj(OUD5lF7Ze7NlASfq90x$&zMoL3^x`6| zqa8Xf8TcM{;gqZ7n{UsAe+v30@-J@d?Wb}$!Ck$RwhJv*j1}2RB%;FfC7nD-oZ^Ph zeJ7yiS`V#*?pm@iS$fq6u_g+qmNRe1_N}iBK3?fNBqms(Z@{YW&s0RrB5Ick!Rph? zlCn&-ruH7)Y1qEm0;=AImb5!Yx=I2yEPc>oq5_Ol{he7K4mvv4ZcqE`e9pZy)47O$ zcwF!qIdpD62X=9L*>bPQT|2&~l#Tp@gWl|Ad$i$7M*}#hl)+AZEP5$V6iW;QJ||2N zqSfmV%K^Lm8=iFXD4e=R$D?)PA4Bcg5Uwx@FAqfScnPV;4E{b7w~u|Qx|f5O(ri|x zZR)!(at%I$In!QV6Q`?DBYN(gXiLHWAz(?etS)c_$9ir7D^w|}K6-q1(1qjaNxQei z%-e6g4b`q{AfGtrwc_JNX~ntsSux?2WbvhI-NPVPr^AdV+Q92i1kVjvQGtuvK7{AY zjyY}ETVW32@agDlabXkhi^{sZE}XPL@Wf~cZ@lILi9a>0opt)`c>O*V#5hdXii6QX zrnwUye5+ZqztOOlt>uFweegO+yf4*FjnT9TQ;!sm)!i258Lt6gHjc50@At4vKznkg!T^ce;`mVcETH(=LPCbc+4M8re08)O!gwqItao&#ZPr>*h8I z(uI$!^TD(Itn1@(&+{&DU0ZZ)Ug30fJzWxw=fRdm6N}D-*O*e_K^$M;k}EZ%Okeqs zR)w}yQ^~E8juwFK2oxaj1uSrb4|Lw6lmaJ|(0sakU;&~UTFBk1f7Z}hx33ybD#7rQ zii!^MMoDcze9*bmefts-SXQZpQ?;nZ>U3;MkM7z7RRJCTG2&8_!Y9hZ=YS}!fQrSL zY!^hLDRU1JMS_NZC%!YVy26 zyLUzZM4HW1LFPOLQyw7T1rRb%Bt__08Ywo$D&j;C@7Blnd^roLtcsVK$X*Gj`K7(3 z)nDapc%}|v2XdwcZ<_ilxfuLTbs4wi3ntlbfcrFkwJ(RC3qN&c;dw_3LCwJGHu zyI83*sll;a5K2gfUl(Id0YllFCFE<^q1TnCpN-G;7;wr>HaQZl0w4TOF1hq);D>bI zFTVl7_YHH`${ck5E4i9d(=7yjvckZxkl`AZARFP&gK!Y2*CSnfGWbBY2|}^TiULZV z*{;bN@%Thc_24~yShh*p0^0ElmQ(0AKZ-$3NE$%$0EEtlZ@xc^d~%u_ERfSXVmmuj z>lF8%q%G-+=%hpeZgkn<5~@(3-(LHh@m9r3Re+Q!wLCJ~=`C>H$7&C~+r!#9w5WJa z*x;RJl`P05@`!AGiuWJZatxB92(yvohYtxP>lZka;O~dorMUG{FRYxse`(9vG^t`m zy&fLHRQIPAcWz()54;|57uU?RnBEqmZn(#vZuEpwI%ll|<3MJJ+c-II-j`ln>&-GF zq+Z|;+xh*0@Wtg-px3yS8LnRoCQh|;8GNa?Q11Kj7PbOByD;6zGes7TbWe@qBvvhS zlFe;dM=8myX@&*sT3O%oO}lHnQg^!2Ia1bfkSifQ9m*@A?+9syUYRo}l7h;DJuNtt z;B{AYBFuIXWDm0+Y1pIM1(7o~G|FU0M-gM>fO3g}gsniAN5-%xG+45GnlpgND>5=E zZha}?!;=O{uF$X=UpB-{Awc7Q6VEmG{p@>F%&M>Ib3g4cm!-l}w{dEFWs|%%rKTto z<8=*puqzlgl^wFMqEEQ`U|N2q(A5cjI47|8thmIzJpy`E9p0kz{Q8QdpEYsn8s|_r zN#H7?xCjF70#+FaFQ&O5R8M_``ZN^Lz;IHTdeBfrc!=hT*QK|WCZyg3Qa|96lE}}T z(#wJC+RIs2vyKIWlKohxiQj{=^yxqHOn+!4lUKr{3{gzM==-95ur%mG_E7=k0m$0x zT8K#~`yF156xF9k2BNE|_;K}i!-e6rd#Mv7;eK@0xmL056BMI(psp7Q1kzwJv|9+p z3_SQ;$P=l*Cf&)By6SC9*&d&%}Dy<_B$`svpf94+ScILlCSE8;n>gC zY6J8YWN!QXdmiWEU0Qvzm3{M)RIvheQ--+9oxF_nJrE!Lk9M=!?F+5TAr_eHPNNl? zOey<5&k70gA};O|uMwBGc}$9LR^%o(PJoa{sf-7L1Ft9<#k*Oj%asozNd42ZQYvuD zD;Vl6q-pHrDkuDK>Ca?|jHtAN&neyp*)*$OwpYLUR_L6N1-m~M+ZM0qL9<=yZJq7g zqdcehT5YwW(-(o5&YnW>#yG^v*x!XnB+v!InMWXPellU2)PXe}Ryq5rwpPQxntU>U zdv5Y;m5pd=M|M@%(KrZH$gdl9g59uVH19=R%m|+kyKX8y-}Uxk`1HP&&F6wlDQaP| zNZdiMtE7eg>&mA1_;W5*ND>Q%JmWiTJW;l1vfgiJe`>H99s5Nj$& zm2WpOTqvOwHrI&Aat#vk1NI9%h{UmU86Ql9Eh55Y+$A#BBBOT=qj=4Ql%mUSaob@ei%&chpR z9vJ*hPuO2D^<(VG9;a5;C)FC17zkFAadvKY4=kYIBKE>x;r_g5hXH52)L;moeoS3$zqMQv?`Ui`4lkQ)>oF4kN zQUwc>KAj@$yNm=kR~qKbfJ9}cLIboclp5BgU7t2^Pj%qo|Kgr`=Bv1E?)BL?I61gg zf|psj*~)*3&%dgSZh!3XD|q{w))AW4Tlye^INW7SRYCE&Wwk;Cw-6avJU>!+hv5gM32RjF zkXE10y>zQsE=2;?i5}ch-g;RKL@!U&htHU|rfSRCJ&K~&htl}p;Z+`}OezEeJ9Te` zOhA|Lck2)6=ifZ>de}g|AzUwrE-U#qVQk2$vc<#;N_7)HvT5b+=fI2@N&NerX0^iv zRNp%m5zR64^oNackg$gfDUF)yZVbrFM)g$gCXh-V3NfPA^J^+vII_QY+I@~bl`tho zq}~(Ut{*uX79@O>$8;4GB#Z_rb$#IA&c7_c+m{RsR!_e-D10c;{)A+c6hqa(xtQbo zmYI%|9><-+rPeboP1eUQyiNzt?xduA%QLs$qrA7DG9UNrAm&|Ludj;?6$U<)W8r1v zMJHs98oy;idZ+NZj#<59CmlIlYqjK-fblt&lQfa5V$7ub%FtPH$<@P|#Oh^THTAY- zrbNk#OJzPhsZ3kM-t?z(;c`JCxkRT|F}wna-bW``(u{ zD211}E`lfC8y1ZN^G`p@fDpLZkCm4MRZVT^^E!1Fl`Z50!H_|`m!w;{w^=`2PQBqU zq|}zrTF@+?u0b+1^^tM%J|1(EJ~GEpsixdLudCV;^FWh+W6Nh_J@>w=%T@{kNjLls zQ!om`7l5=TQ&ZnYi z^mT@s=dzFP{YdSwlpUVzadfZl0LyytC-pqMV~gf zI(z(_ZMu#}_WqiHBS$#yKsr>f=#d_H>)4Ci!C_oh6;%;t))^{ehd0}ziZHg%7*DjT z!qTM$A*s5Z^}^@Zl$52gBP!9-l$d?*K+L=7%<{DZLb36M7_utKj2BX$%7ZUI=}+93 zpk=If$8O>3Z+rFQ{2ZuGA7Y@)MHP#9u%SEe3r^ zzes&o2ji_tiF3mrn30=D(J2n5+6LzW!h!~#fW|qy3tgoE92O-q>1Z;a{qn)D9gr9; zh9DQR{dJ0AkA#ZyKmw$NQIbz+fX61}+jBwSjY7M51pgN2p1&Xaku;Rq0$y&m7^UX8 z6QQ*`C0r+`FF&!=J((hd&Q1Rr5czj~1 zJ)4W?0^7ByOrDE=w9X$uwMq$QrUMXT(DXq>V-F-!$sI0D`lqc`cO*dK9HnbOuN%7L zSX9;dCEM{qx{HB(C+*%LakvDeKeQ~euJ{3$L~i=a+J^J0Hf+ZNni!m~O?tJ}y07Ui4*cGK}r2#kZ5&e+_2~f*qX*+zvyWX1T zdp-TVE`IM2|M5NYmpc{)yYkMt(+_|Tsx*Rw1nU&)oWhST6u+ zfLGZ19?|J~(&=i9kN9c@;EbNQ$ac4o!Ra&u|#yAi@aCrk<-bx5A z!N!c}WosPserUH8Or71o{04u(>VD_dDGq*THisfFDWU}1Wasea+sDsPr!qks)_hnwmYHEB$~~1E9ah#axL98ctM8n^$FOrS*cNZ4#W&&zn5fF)0*JM890o{ZU*UU^p zfc@k`W9x{37wD4Zwy-^6A(Wxg8wecdRjJ?>dxs2%QH)H^#mB6IPE)1siW0WRbCACH zga*zc>3?>HiTA*Qwc~7=nAR0vZA%@D!^?rt-ht+qEHx(vJWq+Z zPk2yRZ}ilK+4-7oVLq-MVVX0y(#?N02dse*4ng0-p*lA9wJqyjpqPs{Y<|=R>pxNG z;@;^WJ^wJvV0)$M`zHB-6P@yl9mx*YB z(&(=V37yx>fWpE*81G2+^zdRR8{6Wn`x$VIruSce0JQo=q`DopJJ}@ihV+6zu{H?q zkS=Z})XGe44d65?j3cjO#`Yc4$C?iU~2Xl|A%gg4->CoX`? zbomAl?q}?4qt^!Ozme!5Baiq3=ikAMIK*s@1_Jk&PH=L(&khWx!)Cj>(e3RQ_m=+H z0>gR+gv&*u8Ej-sVQ&4oPj=!8LbGs0@^X3sf4=V3#M!$RX?qT4VRNg?R>s3QxlcUk zKT5La{R=mX$k~a-w&uw1AT%2Fo&+3kkR*dXo0sE+lwR~p%xnuG82v;<0GDp_9nfuv$*HQT7%~*s z%^*oor)a);DIwNe4dt0Mo2AL>CXHS?CjL!d`AUAtKd=r>k%X)9vN2!|`i?r@2>IBwG0sXs@o{!OC40!L9zsU5E?*c#an5V0yiwGoC^_~DC zR8{_4nQ)?+eAsX1c2rnw=n7K2z@3%!3flJ`JM`Z^Zom0oV$jN80%Dkk)79_;ty5+Z zkzNIXSR)_4Vb8hW&DWcIKJBhnG`Lx?)tF<8r?G;>fFo{Y4JJ7%IQ(1-GqCtm{?EoGS1fxc z2hs!DcOXwUtL+BR=AZuY9~rP@WSRe)Hcjd2?6$!&*!)usqzh_0>hXYa3~5TzTlUz+ zuHK~YsQ@>toRs&`AYGw68RD&%Su{q4w$dBg6ini zpbZk5O6?C{3uw^Kv0l%;a*Lxj43!iM4I02ejcF!|hyVS(0g1Ny%TXa3$4s61p&>9S=c2TiKwe`nVpy8@?mdQJ2=N_x zVF2&D(idDb&G7#|D}ygwif|qCy#2D&I=o7WQ2b#2%{LR>M@>; z#MNjxnV}FEjBZrki5CX*$O4|R5V#t0top+cqDcGBx=B{9^klbGH|U?o9HGkKu+>D^ z-Dcb`?C@8GefK_Gk8uJK3b~e=BGQ{j=I;XY@V#w1J>*W^rvH@v6A_u!(H5^qJfxi$ z;1e<3WT&9bvc2dxCNQFpJyg;Q0@@X{2^?)knQ58uMTP}vz`12VriAvfy}Ur0b@Ks} z{KgX^hwE(iWWaSQM^OT>9HOM`quyXcx#>U@9QREwp?x5Ueq7Fkq8+#3bc2N$AyL5~ zAvL%F=)ht)Dy*)%4)Nmb>FDo>OrylpKYEjLy!z;*x6Zax^?Jp8qD zj|l)`8OO3w^CtNARO0aJew^cUlBbrc5`{Cl>*ut>k4Wdh7-Hy+`1h26x+oPTqz7U{ zB|Zo{iMhd_8B8bTd_EwKBrEB#Y5?Z*V)nQRqtFm>vkac8gv+7CX7=fKPzVY}i~K|;LD{nfPVUPT5fk-#{CDglY&=lS?(cBDJ)3`RNpBg5 zxN`~^B3WX|ENM>QVmNhmco{*vkS~w49#Hm7y(?9)h!^W4d^~j}in|W^nN9VQ$)yMOZ2HZYTh;362mFd0nB=I@JAZ2$%uc1(;)7jogggc{RwuVGdsMq5hB2yfD>2UJm2OvU6o#Fep zz<;|p!g7%z7<~391G>x6&^{~SHP6r9c$q*exDQPwo%QuD6d4_~)lC3yB#tI|(QlPX zdu3+hoPrDR6AkZuB^eBSUY>i5Qd?lXscSLNWh)OAZCN`J>LFNk-%p z^8@Z;41ZtB<)J%-X_f~8^V7i6ht>ImKljj=^>bDtmE^)CYXc|wjngVh4C4twSFnwt zNs*@69w72uTBuRY-2yE^`7@`+3r^u-`Wj)D8(6tU^Pl^yoxa@C@40_FivaiRH>E&} ztHvJ3ve*e>{ZC?oI(EC^T-Y7UFH6g=^dXp;4q0;3v)k7-}$5EA`S0`Lj2cpqYQIv&aAJpmwWv*twcN{GrbMJ#%R-eMK4+Gvki8?`V?Qh5A)d?J{6Kai8R(i-xq3h8Ft?4R!M;kCC9%Lyd2BwhQUog&lXlfH9qGOe6q0 zo_a=;5HmISJlk;3*LZqKkr<6!^OB^uNjsB(?bOH1Ef|rB&U_AR%($qUf9cPo3shBq zn>gFF$q6wO@-n(m=bP0rt!T3T8ZRvY5}8HQw}ps3jGnE#VW*D?!grada-a=$w(eKY z=hgOwMkAT6z?O(k^K9Z{FG9I7@5M{k(Bjz|o2?`VYx)hpr}9^>z{xMEE{~rS4NEy4 zSXv5p5-9h);N*m_-rwBvn@}zby4{&va^B@2SO1KdhkyWTg?^OK)|!1sxFfi(LH?py zPi#?j#*aXWr|Ne5V;fUlHGTGV!t&Sb?w#p525BivZp zz#(PccmV*+U5a!>@qr_BUnrV zNhSYreG-8Y_DetXy6|!kH|VqJ4Lb7^;b_aj$5ZH^1x@HnzFQ%O@TLaLja1U=Brb*z z&@LmdV+HWvr@uYyVp7PrBWO)m zH8QZnvt<3_9&}ef!ZEDTe3UXwMsq<3*R=9L}wCOTLkuJ8Bbi3u(;F4*e}41E0PXy?0GzZA=m)sBK2e zKG}4^i$$NnvQl{Jw7^llYK&6%b}%aSc1dGrb$Fpe;v>=YK2g+_c8svVz2*)2Xgi*s zHshT7frC(58eBb~JxsABY+NcKwkos{)r?n3s;B}TonWXucIgV~3Rq1#f> znF~m!^8-)*I`+_esFLN7Gqz6fLTe@6JE-lESU%Qg>)NZQ5{Vi$hUE|eh zc?-?$pnwWN`Rj*b=aMN{J3haH44mtcNUG;(3F9c?Kwh9dpp zPzHk1_NGgQRChjsDnD2-B+XGZdoH6S2!o&Ku@Q_+@)#MyTFQrG?psU|)>lZJr=|_u ztPF`wrQdG2cht#Nph|OGiQLr_n#g$b({0o^p2akN?}ERE>p5obY<@>*jfZW8KgdWZ zcsJ50(XAtB)g6CKtE1_t18+PnX}(So0?KraHm1{8&VdGsrEl?(9klo~jeFE6Utgf@ zP3!DkeG#bSjP;S{W!o*@J!Lg4bM~<6(nO#$mxcdaa;q+eS;JR)w9eZFX7#R{Uwtn7 zY3{d!Lbu#96IZ7fMha+3_i5&;-QCgW*zw7c)E77iG^`cv>&cIdfyEa1?7Lg3Hpk~H z;<&}kGQ#3HZbCsbu%a~X>d(oWoH)G`HoILnkMZ<%qgTgdF(4om1{A$-8JS1Ys5Jc> z&`n+%?r)c>iF!h2UNs?RPgHHz$hX8J^qlKz#6PaTgfCT3v2UBNH~B>Rwb+7|Haltrk|OjaF<-USKvbQF+PWvg##d z>}H2RW;yBoO&j>KpX)Gbm1$YWzJYD{)7l6g&#gh8n3DI^O%isX@w-Y3o?A}kbok#ZIWPPQ&LNta6>YVNa&r=BDt<0Kb5J;hnFQU!xVY(-kB#qZ3Ri zucbZpfrH-gMb*d3e|dPWH`!o&@7f78A^MIOJ^-U^Sz){yS^E`=zy;l*i}ZQa>Cvw% zJs~c17M%sl%cbY$r|-`T&nZ>e{@J7O)kZ8rb_2dGp!h>|{+kVKMb5PXqQaff=@V^I zr7Gl&X&f=h{yeu*vYbo^R&)pJPCR5E2NvAtB^KM8#mBRCmaGjITU@WZB^4=CPR<@# zVx8+jz3Pc~_K+n?nygXdi*1`CCqHh5q6$p){IsH02f#z$CF>X$?-`-Bjy>s4rWffb zNU0FI{Ycy3wz=|fYN(8Jj#`iZ^oXd2<`qb><8k3itil0LH=oFtwe#`iLsk10D%kB&x*%z04WTv}) z7u>bhuw)|>*>6b>vm5nCbwX+$Guy>b zW}8cy+zP&|nz2aTKy{_5J5EJ3AA#BePqJSK)PCRKY2UY?-;-!t+Z-|6qZ97_PIO}( zdoS3A)Kr*-i0A#7{5x9%eKjMi(Cii>n>Xvcnz>T=3K(!R&M&{d&F)5L9=j)~y$@0Q z+%9Axo6nP}9D4dh){*tJqQPM6QMvr9v9kZZe|mVS35o|Qpq!pqe$ z?e6*gf|?c7xE+PHA_=WZ@OzT4!=AXGgqo=ic`k*T_%3<1#y<&|z9$kqJe42THI_OQ zjGpNk%-`62J8Zpa37!xxE8PU1Z8nK{rN^kD@ebDQw9`0AoDpXWoq01nhEkO0s-HQ7 z7)E9KS07Q5k+y#lbAtvu!%r}m=A#Y^VfGId-=vgS1$q2Gg`EXdRo&M25eW%FT3R}l zMjEA&ZjkO0;n0mBNJ@7~cXxMpNQWG{r5nC|z~_1HeeZqWk1-fSkDj&no@=hP=9>TC zo=c)8czkLVt2)y`d0>HP7-4D$ZksJr*XI!=9#oB)*T{zI(>WQP6B7H!AD-KxaZXp1{0mewo>;b$P)ew zr^&!+w~_zu;nketZdu8!H<)?d8DpshXrhGwm@c0kj$GPI?ae3Si-J#JOiX2B^A zjZ45wguXhLz6O&Hd(L$vq3`obm~WYW;i_xMe5^R!Dc|>mv0hT$jlSSvz0NiEVo_qE z+orHp&ZqpBUYhcoZ1Y#@{XLnr}}a{dnmU&XCssB^#%&H z(dxxJr08Ug1M7-1xtghGP#k`(3sAqGR7f6jM?-gXmP*_$Ae9P;eGWIf`HKZOGY}Cz zSsp!ji4ZY<7}39{OzA;b%@@A0Jo=e+I<0eS>wSX91*_rzi2(g=FPbdQmi~QC!LgbM_QNTZ#QF`CO?AhPYQsK@ zfA&249gPh!o&}$IlcB+)DN963Tw71UiAmFk{X^e-wtJSYn@&{*N;Vrw-y&EKw@)f% z_tv8sJhw4u1xrtUDHN{X_2))}-^Sc;!;b`IW-II73!lDuM*BhM=exa{twNPBf|(2G z#Y^oe{bQWs?ktZBMEmAt8;#mskKu|?7b8}AO3o;8YYI_QLv%h%U71orPo(LpYFW}x zaMJ9~`AKhAE@QM?q*t0xdZ_dSBXenH1GSg2z>ruvMf{BBYHdFWrS zlHFWipg!5WvQpod&X70D&^}qzE+vF2A!3+!enWJ-A)~YKmM>Vv{(>N;y(0|E{La8> z_F&;#caSA6nX2UC(0!aC3m!eom1wNI$#ezNF|I4f`0Unzcd5p3MwTF#&u3M2eLAz#SiZ}zSGTMF%vX_bUc%EAuGw+jBj9E%&j7D288c>@RP-2IJ z$cyVYHCP7k!;ZPrD#o)$Ht*Z(twOm~>y*zFz{GY$*U0Fx?q0M?#86Rl-$obx%)CwQ z;NT-mknufI%gwj3YX)LdxRDTJVti=;&K#2mpgFuPFDOHb60;n84NSh0?z3xnciW^y zae4`_T{3_+bjjB)TylIQ+)pQ|Lgek;jLerr2-P zBgAK?i`7KH6)p4LTiMxz9rGv8hNljv9q!mpks}E%qneV(#^HI%CH)I52A!@La&x`z z+2~KSI}UKgOXkO)Wy+~*zJQ{54c5|UTPYJWdvYFa2%IXi0kAGl$jpno)jgojik*=a zh|~fQ__|G^(Wt5&M=MX2EU{`@+iq4&r({WB$Mcs zyM5XsDGacKhAL|u-!D$y?@M`f7$pY#a1h44se3W^DGT)qH$`EnR-C}Xa5vnznNIgL z_2-y&VBXmR{R*IHQS%64&`O?7CA8F+IZ)meZ=9mPUbSv{8AtI6?pGDt)I2gztRy*u zx;u$`d9Hmn+HCFBHM1oJH1 zBk5{#`MziN=f-zY8jiPiu4I+|pNE@5e}B}y`Vr>G#T-mDEkjoE2CSvon?JjUFIE5B z>83eftK~9~If5lC<;-V7#ZoV4>F)d{T&@HM$DJvmYE{>RCYH6OKcykN{@K`VIwIN5%B+=*wA0jU zk{vDEA1m`5w?O)PmfFQ07W+87M946m8~3M4CukkX6%7yn!8$F-hL|U?H>Q2ZB|`&j zr_a4mj(KgLId%5Y-lx?6eVOZ)eXXAOmGkUm!WNmbZ3s>Y5Z3Abq||+0Qm*_fkc}VF zd}7|C_L^;=cAwCdjBTOOI3+d`Jqr~trhi$s>|T%M`$(N1cZAAfUqnmZ!cg2b@T#!K zJw}z=R!YgOO!(=^(F=?h;xniRGsRBG!<_M|)IySJYW64BnRbJ;hc4&w*elo=jJ@QV zKnO0S;ixq~I;=YHwp>j#Bq@wJmBhR^WW3mGRjEMMM9aCf=Af5ZqUz#&&R2RXH7@Hj zrS8CA@n%vhw1ly)xqn zQG5Qw;;Q;d+`xK3?&l7ZrTZ(0$oC8u;g)hx@d5})`yaMueZy`ffX|E0CxuYz0t8R zyO)1H{>(fGkd!C6jkP#%G49@6{_Kh8Z++RoQE_>>ij$rA72y*8=pwy(xFO4H5Ii|k zOk;mb2Rg5rh?l3J4}LS-xZGW%Bo;{?A&rW}y~~@tov3Et?7~x?Ja#MsP1%^B0ZBJC zq%VEkVUpxtv}nzvu-|AHx2J0FQ?hR|JkQi(kSrXSU<;iuJrp7FJol2CyQ%5%4vo#h z)MG3BYDlz3`G(#ykvl@DoTSK!^7FG-_4nrb$*4ys|S3=u%KO6B$u0_O+b&bsWv)W~Np6IV-ezm+|zwGo+ zX+=ihz5+HBw2gDBpVyNvzwq(YNzO0wxhxwWOY23=1}LRq*nhs(Je!pV7g;C+S)HBT z${c#`m^}d9j5Aoosy3uLl&r^Hr{XJ^OU7TXOqEQ}mba_5MjJrr0>;m!qNMq_!qpvH z_h$Dk7ebw|M;Y{ zu}QQqYNf6>igmV{ZfACgyrx5lQRidOV*Gp`o|EznuNFaSK~{(TUhH-0R_|TE!c)#G zhNm^4J?bw;Hz#?k(mq`$t;v-Ku_DTDq^EUx^WSB41)u|ySQsjAhCf$8y>K9L-9d$7 zB&O0jmt(!c(|pr*!t<(I+PrNdKe4#98gz;k*MR{w-V+}Vgw!590Q-76WwjH2JYcQ} zgz}lj>ca^~KthkIC{?M?{CKMt=Xj`Tbix!mRHJ-ncW03JBsVueg-`GNBd|_Kc{C03 z*OBHoO=&nJ4?63pmu!EkuM(GjN^f{u0wjWm`G$ZutF|Xul}3%w?oRaXoQ8C#loLE` zVb_!P430v3EFn8#`SdLJCMX8Gr%M;_?@Zn5-|l~qrbv7X?~ptmAJ50(bvid_F1)+w z&auHNa9L_;z_!=^Ru>TpsKK#uhYWT9l(_cB#^ji-PUGtLW*HJA!|iXADo8eW*xoI@ zQ1vtw$?6VOyV~_0q;Ux(kt@8y(5vL@d;H0|!gUL}E-&q4TrP|T>brR2O^#8E%aSIm z5ACm=Rc!`gW2+0i*=(FsTE_}j$ag3r$bnZVKiBc+6MYX;2mI?5-JPxEB=e^_es1@K zBOFo5-_yIuG4k@RS~Ir%>u#ItyG=gQ)Vai#;vz;(M28V*WO8aZOB}z>N>z?DdyOfqCs2H%WWb=!C zkFPsGZm^~J{rzAyWwQF3p|LmD``CD8D5NqY3pH5pb}ss^f9@$~Hd;7wxEpifUPd{F zyDzx1D=y*(ye9+%4jKJlKw$HcioMP2FHzHQe;tBolpeK>jk;k(9@Dd%mTHmA<1 z+l{Ct+q67DzKQ$^@W0?u4uJoyx)B=6P}(`cNZqk}$$R5I)2({Z39sM=C$%X9gva2i zt)>7t&GFa#O9A&-=7H~}Y=K<=Nyz+76 zO4vK(bYF){!T|UIw=#5l67r3tpqb_-`0j*o8TW38B$t0dqLB!@d2^Uj(h?bY#0@#B zUEbF%B?y5Y_){K8QUlOX(JmQ0{(J&_2!VbKj`*DL1o%J% z{`&Uen;I&2v+0sLQ8ZOUY9NJsBPGzZ1A< zoL#jYQJx3|h~EDdz9kJ;U8-8rH@TmKP`P-79f3QR=MXBF)Y|*lpYAD!YLs6$Q`y}T z+QNzlIvq35HEbMqy<-csa|o| zL61yt_HBVv8QY$IeL35Nf6LGuoNl>qyT${!Oh%Cqy%%r7IBs;)g_O&x1 z90ZK*>lvyXbbnRMc&!M{x4<_(K(JcJiQ@8W+r^CdUD(ccw9QtoKl=Ta=PNK;GuM~Q zK;iCGc>Eg_Nc~jT4ESxdkZ(ME`3^f8YYGtv1=Ff9p6kxtV{|p>D>_(`!Oe?07 zc@-3Ps0)KXH{$_t8hN^&-$Xjdv?xke#WZlum_4yjgr;18J!PR3(5D}$o>w9hY%;&eA=eN8ngu_?>clPrH$VMd zbsxZ0j4)LFVHnTupzw?(eXw(8Ag9gdaXwd|BIV);m{y%WkPZK^$wG+^Lf&5rj>ZK@ z=o*se0G?53l04@5dsDYVfDDDGk_%ns8{WXY0Uk9oN5JovX8=;Y;+zAx4gk3Y689eoT6kG-^6tZ z3O$SiFC?Y)p}H#~QdRX2P?rJn!L+Xs{k|n5))~Gp>b?*uo`6d?eCpPd?l#~7No!dF z8h7WUmZR$2u0ua+KUpIQ(xSm9L{?eomoq;@ro}M-gPqL<*x4a~opnp(+Ei?2dV;*` z_B5=NL^9Ea%WFO^6gZigCZ6bBe?_fHG8g{S14}`AjoyjYZ2&E*06aVzbWaE$xyW&f zp*~`JLoW19(2oFL$d!IoKm)MADl9-A0TS4DmgGBIE12}g+q>@#)}_PK(|m+`-AO^e zH<^Nnx&1b$LtUtVw>U^uwpyK8E2Q5aN2)#hPSDzun&j6_ibYU*g`olm3rs0&!Pq0C zjD>`!5%EtnMP2E=Rp}824|3rkQ@iT_WM|=T)JqoD_|id+Qm8+ijt))Y8J~{)gU>EN z0Unp5_wm&;UfGa$f*8=PaDaR18~nOO#QCH`1v11GPKAj+{*GaiC;B6o$XOHW*Oe-H zecIPQeaqT_Rm6#qbx!mJwy}gPMX)7vkceJ2N=Np zU%|t zPj~vri8=ttI}M5P13_EOrAGC(s*LoI2@>p#B?Z`^SKtaw=L1IRXUK0R{;4 zK6u@)fye%yUUZWR3niq%=gSJ88(yxA_J1|-&;u{v14+dJ8+L&J8=3*ec8Jd+3A55A z03Kpn{_!DVYpgxVGo#6ot%)#H0wS`UATA)O;Utxc2uh^}C>TsLg0~=J2pdd+Dg}?u z1Rp{*_&#rUK56F- zNr3#J67sP>Jy+{9bz*Yp@M@f9#j$98GUG-mJ zUfcKo0$Km*`!%y%^YRbJvIPRJZ-S8!=sWRxo&#UW-^?>g5X+Sntqq6(ioqhv{+I6i zbMp!h_WKZ)L5)ILLQWGl@aVq*BVnJ&`ne$2mw1GU0gw1k2L{#s00!@i-T?4BXo)iP zUO!;40H4XTY6_Ajf6#&S#eZ|WKZy9BhZhf&e-AA&hTi)PsUjb+w(!;$Y=>VQ8XF%z z{L^j!;K!gb)?$Fl{?l#YIGI5((W?~Uun!~ugXTvw809d0v~?ao8ONV%sed!se+c(~ z;lg8Uj^YKd;F(wUlZE{@1JjZ%(7697&3ys-#TI|AzrL9RGx{ z8AaSd*06mf`?uHn|1U0+JC=qirlVw<+wkb~@+zK2d6cRa2CFGTde`Y@G z%85#~OebnZk^s!k@D}+D#m|t5I+*;e*hdQ1ge+4ra#|k0ua2EqKmLz*1Rg9Hl_o1E z56nCtRg2gz39C?&4pa5dBjPQv#@Pd`FAmht-<_g?+2?$rygpSNi zJ_%iCh{xK^BX_KT2)JBd3JSKn`>t_3&iK@dks3gF!Z*-t^AWhuB0MP#Sm-B`DSWtl z7Oyv#no)gGi44X7jv0Uo<*z&ZV?j3r9bnMzoCnmh)$~CqEx_4B#*R;b{4)$yQi$o& z0YKBl)U<7Prd!wQ7b8l;QlM%9`xGh!lO?q?iT~C+o~?hii$cIN5t1$n1pD%8k3J0a zKtLyrHxz~nZT@lx7L;T%E82LLRNFOowf@EQkELZG5JW@cBn=b$_1p95OD=n48yP-W zsqf#Xn>{>!(x>E! zWm7OvedysWEFqyH|8BkM+o8D;FlZp24I`A%A6WGN*durLgU)4m4LEAuKP~^`?HR-- z6Q&r4i*uGFc+1X^3b8^NO+uueymQrLIi|5pc6m$7v5(6L(8(OB><<%Xenv;sl?D1U z7)F;f(B?wzS275m?w6C*fdSfGyhE9tn;V&&+;-qt45}mb6c~0z#Of3`qXI^rJTMig zWyz!jwR*O{)DA^bGE4Q%B2|7i8(hqzxEn%}IO+bu>2m zlm+#9XXiea-rs6HzR^f}G}!Z-5e|j8?PP15mzM$KVZnxct0Pa2!Lu_7_Gf2@lGb>Z zq`>_hUx&WN`Geb-&9yc*nwXl2=<9#o+0c84Rr~~mL0>D)Cd41+R>~qoM7XCW7Ur$X zU*R9OD1JQ-hpGg=XQ#S(fk<^Me>r{)iOQ*>2Q*|YV*)8voSm@)F8_)s@bY29h!%zk zhe9Z1qkyr!y-mRT=I{mQ$q|(z;KUR7Tev zG@M?yOevJQHx|6Z`L&ba?m6thVb@?5N|Qd?rBWp3)~NS>(tM)Omo}`4p>t=#LaUdki?_M%S+XXPVpVWwvzQi!a7B#ql#G(q#%-GZA%o;;LcLpon~(fY zJ#DgTN1kAP-)kmN8;odh_g@|Z{-&3oeTA;tv`a6RtOL%YugglUtwkdf1hOIW6-8JK zX&e6PBXOq@nT|T+fsQ7JpNP|})w+cpO~w~C&$v-S>#zt3Pj>#V`~U!Hu}kd&P$s0&t+` znaVOFh3^P9oYIeJk0s@l2G4pjn^IAhWow+XQ0?8YP&1sB=alVMwx7Ew@T-0)&gRlg zqHFdaw!A}3R&*Ead|jhIT3ge8z}Qf-4`*Ar7=G2Q;Am7?JoU;KDmYz$%j@L7z%~+d0`$f(F|loj+str zKv>|i3QW#leoxmd>0+9)^Lbco7oYEZei5z$RRZgvam4beJzv#MoYE=E{NvZt>#WoZ zO>L~K0^Ih^L4_H~^ag9G1X^7#B`%`+tx(`!L=uzyqhwwxz!kJ#8Lu2Sm-#cO@W%G3 z%}<~na%^$YDW1L2t;@CwE?`c`dQaYOL{2yA=1`ZIDXGNNZNksRP`{5YCH_7&NR- za#P=EtU@YXR1r=h(~d)&s&$kx*ygcW)a#3I(~eux9+^b%RsQ&Y4>nzFg$DX*!>i>7+g3Q_68Wj_zKMtz8r>R2Z|CmMfo-X*&NR z-)B@fzPmEpa2`F&?u2vjUX_#7nTw~R8@b2QemUb#gYY>Pa`ZvW&2@?9 z5XllJVsl6jO&C;{sOaeQi0asZ6Z3We3?i>Dog!wo(nq$VhmoZ|>Q7fCPU2YyMH&_g z^h!2B1xpX&Yn+3lcT>*!eDZOWIF9m;v3X46!Pb<@?guz(g4^6G?&GDwDpQE5Ht|)M zsF4j>Q(7lCcRvkWaTt1lLwnw|9esv!#L9P1D0n-U%d_BsbN_DIt=f=Fu&g|xk*_OG z)yc!C&bVxWzL!Q2VwhIDf!!<(+aisaQ`scVb7Q?VjQ!&5Nw<5h0?dt8`=#%`?x#o+ z0}_W{wwCid>UJ7nQ8PlSi9EL72h)&B9KK8S^(Rn};8H0ncl*+K;WEh)1AcONMaztG zoy&5$9|C((clg2xueKUWd&wZzCSj*azqIoCP)8qCK!UtJzwo|ffvW($!jb1hlVmje z<`u28^+fHSpwsz69M{qrva>TCoy=9qx%%9gbBKXR%f}U(L>}raO>2T?z%W}U@e)bZ zzr=SiPxtGkrW{+(hZkR8+_FP)VBj!ddK~okGJm?pCE62rFOEH@IV6}ot5n#GgVsD^7<=z1*f^zbU0Cp3MzS`2L9a?v*I4M)xsjyk&>Sxnwln%A~9(6slK56`Z z^4b+Jn7kjw^&P9b%-yPVH&}c6&BlmZvzvVR?`yw#fffB6OX|-g4$?R$K!6D>&WY=a zKYL;z>w^WbK7fG40zLLQcMy7G0O9ol_f_56Mwy0nW!&lx7lTj?)0N(`PcKT= zVaqrOt*QBFV4xH`DX>U*LACPvyN%a11wD%fb;;NZU+i)Z?6l2~LreCIFI@9JIh;I0 z1!W!McQc~1_tGFu;lWG)yeH)C?2L{xqn*eH1@ALt6CoR=uT&`d<=2&iRg-NLTeWyD z4Oe10@6;_KbOzNJa_itnX8diFGWy@}7+|Y7M2~0m+oL)&;-}ne9eI)+-`X5rDiuR> zV9+&(a^sGi!mhlhP{{6Gddj8prj*A!(O=qNcEM3|y^fwa_~cgZ6F!1}umyknhL62i zakkNXe_La8?PnI$L-5X+MOe(j0iFe2WUjgRl_$FmDnjvfGFl)bSh2=;>;QGaVeU;% zBHwyUih!l?8rn=F|LaaFg$)_)82b$l3wEy1#I)EZ6#2DU=k`VLY^FYYcy*rDLiWyb zKP=ISg!UIMn?i*1tV2=Hv_<~CyI4kiO&qE@y}5&G@}=fL=M-j^>f-Nu!86AjC%gs~ zBz1hr&X(d7bv7j~#621q6kTEYpTNd_wOu`m3Fmw}Y_ey@>a{7boh7+cB+)DDIL^aj z?u(eGYN`U-t;*ufmiLR{my#vdWZHM0h0zEn?? z6Z}Vz(_)@RX+ z{;I>4(5X6nZc4Xit<3STWRrtuh{fEZG^Y90 zMD$^N<=(oCUW?G~M(xNeD5HRb72)T2gnv41oxK3R5LL<$70-xLaM%Gg=Ms!F@&6nW ziF08`NGG?RY`pk#&JmP#u<#p;gwu$!%7v@+qZz@Lxe|SMbfU$C+x;^fd+E_CkIk!S@MUf55_z#xz5y0ENfKW5_#6vhL zBQrM+ZRII;w6H|Z$Yqq@J9V@pS|p3R8)OuJ{i!}S9=|KHYCu<}>3mM=6!`K$t+{36 zd?}*tEK%z~wqcUlKc|9piQnbz4Xv6C``+dCVDAdfIUhwyRq=OlaGcXHBY03F#$%{1 zh6Vdq#FlrNqttvv6lP(;C-vV{{fEr{$=Zgq_1)IbBX4Kc7mZu%*4kXs2OH}>d=D3s z>h4q|hnP3N_;Ab#- z#{AzhSeTaLDL{Qy=b^kc{ex)QZC5X=xap#Y#9VE)8lVWBh~sZU#WfmAr*33N>d#+y z4~4r=)BG^DGxCPjkVI|aeZh4rUL@Y^)`*$35-W`#9C$9KD zMgHOa(dUf}!)|0btL23a!(Q@+OZAQTO?uLI2>u#z4vIs!i>@!Yt#87LYlcyrfSpqZ zcpY{=?qN#~%$W1PjDB8RPwZU!(H$Y<7@td0ql>tx*8|HV728#AIm_i#KesbI;oih7 zfhB#llPo7NwsnFW(eAbnauoFjH+`~+-`VV_-bjtxrg1xmYMf_<%c=5ubaBxOJ8byW z>>`O3!bV=^+cD38D8t(?%SDGUI~7wdRWWdLhb*Y9W_M@a$z%!c80fveRM}&RP>4E` zP8ACdbsF<~uNsnZJsbGjgl3#ULq%jkjrff`nHm>~h9y>VJ$+}Fu10=tn48a>9&b_O zDlfeU7~9r<(W}2M`2!(0Mzhnoe7*sCE(@F6+bKN3sxzG9`ds*F=^r+Cwon2Dljnhn z-TlXRhEmX&SPGfU^4^X{%}Wz zXG0$cd(7e21rAY*d-)TvzJWpQXCJXdTGYUr)i*>ZCwx8!c;o3~-Bls{<~R(Xy!a9# zRp+EESL?eXr*kGyU#Mwz>4~#SE*cm3uw&E!jQ7z;q+d}!In9trl|{junaueJm|Dt; z${QVz`rClxma@5(V%t7A-Lqzi+r1HXpwiLQ&Z{}k9mDh(7&z(K-(RhaiJMH6*gt<_ zUN*K>X1I^9drPmox-?ZU+iI+yn*y7AN~6F;KWtN&u+!^&FxwDwIv>%9lJ&L2g`zxJ z4cEKQy`{p>-KdaRf1;WeFCGsK!G{yvr+}#Run~;;xw7|+ei%PVCC(V=hPR_8$jd57w^r^%gFW{rUff5`FrZDrGv7fcYxt&*b;zQ$^^|BZt%U~{0})j1 zRy3(Q8K<2Q#pKGYu*Z7(I0Ew;rx<5dfiJ9c7eM|BdEo+^u$ z4uf%1;(c5WSMg=FF+p6fuXOW!IF2;)x26oK7M$PSAnqtHe%~Kb|9#u3Zgu*t(P?Ek zD4GE+0ls^X5>9b>GrQEethGh$=Q%Df!{>!Zde$P$x{FC0yqtk@ z$#!pl3)s2{%C0Nfuud)_y z>{M&tSUoHS7I;IC9SBM0&X+VkZPO42{^m1r@Rq5?aeAlkCpat0GrZ1>nftDbBXUTA?wb>)TzZ`2e;Bg!^20FRn@w-wwV4xZ8J}g0kfAv%r1Rgo>>Xx=yrc6 z(Gx9HlJ#4+%_`X{l}k0qs;Kxn9drFYn|$rp4ZQd+h%QlVt}yx(8%S`;*>Skvxq6c_ z5#YYG+W+zQoHu?`7{5BR0CXdoiW!}A-K)6{x-c;q{q&Vs*1>i=AxLt`k30+B%OrsV zp&Jf9iSrdPi2fnP2WIui?{c*_#(*T$zmLOa+W#Jb${I|B1$J7Nkh6G5N#i7wmte*X6NzlwZ$VPn{Rp zHzCryGmY@6);LxhWWhd<8xP8M#a0bk;=EB&Hx2Rd}?Ul~QGA`38ig-oKi;O%YZ zE$QD+3wNKFUBMEQ{3mv$(KRuF>Ha3dc)qIe!}Qa`DV}+Rr_Y~navTbsoSu?s?ar$F zZD#yD3XmZ+pXXXX^xi50Wl^NfuU}|FD>~>8bc~ruNf8Q_`>(GY6CN+q_4R!`JpZ}n zd!&k(Y%AzMZlpXQ)8#`^Qhfl0?hIuk!-?hm>X!phZw~k=%C$6Uxfz%49!>AII@uh( zVF1D+AHBzYng=+qtb9Wn^3+9X`SE*lQSyV)V&o%p^~7P0(8n^|gs0!X-*r7?h*!O; z{396eE5<<=Jy0mboZVXvPaP=dSAKPM&aA9JC9)=>^z?LK?P@J;ec!!a*?9985Pi`Dc>O z@##?dK>h6bS23NBYsN+N=y@*yNywTBWauZ9<|i~SK9*zkvon2VpFq{8Ph267@!xf2 z*6-i*>Hk#farQSa{($r+Rx+ao8NM8QLXdZ}tK2Zgqc9O1IWJQ|%Q^PxuCkF!B}t;Z z;Ac)EW+tKnCk}(V-G08j_@ho}8sD`Vm6uU#`EOD?km9asUD`f;4b)2!tSf?F%;SM( zLf0|{GN1oi{l^91~N@(Olq=wF-bq?z}o> zB7~B@tdC+ajjw8t5u^V4kdcSTq<@-;`BO;?knP$soK_-%(qexr8#SqUDZd+#wc4AX zd_jS-8;n5rtjqd&CnNxrAq(=SgI#^;nF$d*Um62n0R+!Ce-)t`bo&Q)fJBahO8m=C z#5bv)T|LBK6ahwxYv}ffOLNKr*p)(j9>`P?B1lM4t2{s;Z>ox|W28sPAM2(8<1Uu< z98wtTj|2(p&QBH? zs98vpMB+tEf0wt~R|m`mY=^bvAJi258yO9yg&eweS)SC}`3v7ScdK7hE*G77e>88p zNwTGFifkllM-i9;HOw+1#Hf`Y>Yx#k7nv0U#ohik5b; zGxCwNeWJ(|xlyC>NJ zPUPRT?`w#!t@YZzN2j(;qIqa`&F}o<+#$vxe%&cn_6>h^Q~DI!eYpb@4Nx5Ip;TI_ zS6)-v^epASm22z#Q}|5=U8Pc`5F*C{#x#}G;1nGPYws=8fFy6Ul!5LUx@zOSPlJaJ zalwdA3ovu^FG8W!!lEh)Vo^Y`AC#|rT5@rx*Vgf`LT@7LdC|Z15u{nkcHXDOjW-pi zb5#iRP_b97!wge)ZsqlIG=Ops@oKPAj9_w~xS}Rp;A@M28svWL^nY&l6#FnUsZtslo1PiEF%V#Ci4+L z?xOh{!uiW9d*`YSU{{uhmluo$?D+@cTA;sNZfLpuFuj83OW&D&qavX2|oW8ZGAAoBO@_K?y~b@!!3 zY5!xzXB9aMq}kfySAN*eGH{(E|_c%Ocj0=+7cXa8ZTZC9jou~9&{&l z^2~F`-F>5Ssg97tB)(_oxp1oGS7ie3xKWfL$YOqU(!zTE>)b4JaBGwna?weiEeANI z37m!TQp4$0h7E~fs0(rtCdP3smVy`~a*==n>kHw+3)zK34i2@m&!Xwn`-x8HpA0hj zY4LTQyfar>>ZsyP61A?K7kpc3`0^`V<4cXLs_5mHxlgdGlV1GF&7UP(CBl%0tL^^v zfr%+aoyiBL{TD5(@Kd>o0MwtTMT-1(uq`~rA%TQzZFD+Oz>)X6JdQ;pV+#&-MNP|y zUpSQ9)GD?!1DjY(IHf1MetpPbeTse#12o<;1EF z&o50`L*EMPOKWADz}?7);^%KFx1o@rjxHzVU4{n(civiX%CYe8Yspzr-0uJIFu2I2 zTLc-_ybO)K37C`Cv0C#CE%J$czt2=7FFCNGEJ^Yn)@eH+TGlp&#;lVKPUU?N9!3g% zgVSvNJZjaw%))-r@$;zUipsrdN1CE$wzsFb&xGKi6kWed>R4q0=L$Cxp!Ce<^vURg z<`9sJc>ccWK7X~sSOKr)9j||Gx&v9~&07n0V@hr0hQgB+sj1A=EY@rfS6jF1-I&xx z4lNGz*YV>T639)oIV#_@u?qHkaewJ@uz&Qf{OPD4pCJ94CwMlo>dymI$V(46cVeOk zod=s+W*AaGm?nSUgdj2b`+~bq>-XvSe16|#SM42Pd3aTipysA}-R;{(BK*Om0R6cq zI!S>$aGk$CFuBf-z-Su2f{Pf zuq>!HgtWHm%_4Cjl|;wCtVv>!Y~Uim+L+TVFf ztOxfmpyp4sA>No{{ysBQ9pa7UAOFp5ApUJ$9SCLgL;-3y))Tk1rG#yCh4|XOQOLt% b^{7OfyPJH!VX8X{3iuHhmKG`!(DC|zCf9Vm diff --git a/docs/assets/vstd-overview.svg b/docs/assets/vstd-overview.svg index 2644a1a..3d35c84 100644 --- a/docs/assets/vstd-overview.svg +++ b/docs/assets/vstd-overview.svg @@ -68,10 +68,10 @@ REF. SUBSETREF. SUBSET - EXPERIMENTALCANDIDATE - IMPLEMENTEDCANDIDATE - CANDIDATECANDIDATE - DRAFTDRAFT + EXPERIMENTALREF. MECH. + IMPLEMENTEDREF. MECH. + REF. MECH.REF. MECH. + REF. MECH.REF. MECH. diff --git a/docs/index.html b/docs/index.html index bcc7867..2265d73 100644 --- a/docs/index.html +++ b/docs/index.html @@ -78,11 +78,11 @@

    &u6sC}zIlN2 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 b^{7OfyPJH!VX8X{3iuHhmKG`!(DC|zCf9Vm literal 225802 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