diff --git a/README.md b/README.md index 97dfd67..139efc9 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,13 @@ correction to the pipeline framing this file used to carry. > result are bound by construction); `build_verdict`/`verdict.Build` is the > low-level assembler and checks structure only -- see `docs/VERDICT-SCHEMA.md`. > +> **A receipt verifier exists in both languages:** `engine/src/receipt.rs` and +> `go/receipt` check a materialization receipt (`docs/RECEIPT-SCHEMA.md`) for +> structural and content consistency against the materialized object, the +> authored input and an explicit path binding -- see `docs/RECEIPT-VERIFIER.md` +> for exactly what a success does and does not establish. No receipt producer +> exists in this crate; materialization is the environment's job. +> > Nothing here is a stable API. --- diff --git a/conformance/differential/README.md b/conformance/differential/README.md index edac046..814554c 100644 --- a/conformance/differential/README.md +++ b/conformance/differential/README.md @@ -43,6 +43,7 @@ differential/ digest/ -- F9: digestprojection.* materialized/ -- MaterializedObject::try_new / NewMaterializedObject verdict/ -- the assembled five-field verdict artifact (build_verdict / verdict.Build) + receipt/ -- the receipt verifier (verify_receipt / receipt.Verify) ``` One group per primitive, landed incrementally — matching how F6 @@ -221,3 +222,26 @@ vector's `object`/`fields` must equal the same-named `plan/` vector's pin. A second test per language applies eight deliberate divergences to a correct artifact and requires the comparison to catch each. +### `receipt/` + +```text +input.json: receipt the receipt document ({"materialization": {...}}) + authored_input the processed authored input (any value) + object a materialized object, in the `digest/` group's + {"fields": [...]} fixture shape + path_binding optional: receipt path -> object key +expected.json: {accepted: true} + {accepted: false, error: {code, rule, path}} +``` + +Both languages must accept or reject identically, and a rejection must name +the same code, rule and location. Every digest in these vectors was computed +by an independent script over the documented canonical form (sorted keys, no +whitespace) and cross-checked against the already-pinned +`materialized_object_digest` vector before use; neither engine produced them. +A second test per language alters one field of each accepted receipt (an input +digest, an output digest, the version, a field's type) and requires the +verifier to reject it. + +Documents are decoded so an integer stays an integer and `1.0` stays a float +(Go: `Decoder.UseNumber`), because the receipt's version is a bare integer. diff --git a/conformance/differential/receipt/applied_defaults_duplicate_path/expected.json b/conformance/differential/receipt/applied_defaults_duplicate_path/expected.json new file mode 100644 index 0000000..b8784e9 --- /dev/null +++ b/conformance/differential/receipt/applied_defaults_duplicate_path/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "one entry per defaulted field: a repeated path is not strictly ascending", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-ORDER", + "path": "$.materialization.applied_defaults[1].path" + } +} diff --git a/conformance/differential/receipt/applied_defaults_duplicate_path/input.json b/conformance/differential/receipt/applied_defaults_duplicate_path/input.json new file mode 100644 index 0000000..42ffcc5 --- /dev/null +++ b/conformance/differential/receipt/applied_defaults_duplicate_path/input.json @@ -0,0 +1,58 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [ + { + "path": "$.b", + "rule": "schema-default", + "value_digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + { + "path": "$.b", + "rule": "schema-default", + "value_digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + } + ], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/applied_defaults_unsorted/expected.json b/conformance/differential/receipt/applied_defaults_unsorted/expected.json new file mode 100644 index 0000000..c05c884 --- /dev/null +++ b/conformance/differential/receipt/applied_defaults_unsorted/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "applied_defaults must be ascending by .path", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-ORDER", + "path": "$.materialization.applied_defaults[1].path" + } +} diff --git a/conformance/differential/receipt/applied_defaults_unsorted/input.json b/conformance/differential/receipt/applied_defaults_unsorted/input.json new file mode 100644 index 0000000..6e695a9 --- /dev/null +++ b/conformance/differential/receipt/applied_defaults_unsorted/input.json @@ -0,0 +1,58 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [ + { + "path": "$.b", + "rule": "schema-default", + "value_digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + { + "path": "$.a", + "rule": "schema-default", + "value_digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666" + } + ], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/binding_covers_only_one_path/expected.json b/conformance/differential/receipt/binding_covers_only_one_path/expected.json new file mode 100644 index 0000000..ca66bda --- /dev/null +++ b/conformance/differential/receipt/binding_covers_only_one_path/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "the derived entry's path is missing from the binding: no partial success", + "error": { + "code": "E_RECEIPT_UNBOUND", + "rule": "R-RECEIPT-PATH-BINDING", + "path": "$.materialization.derived_values[0].path" + } +} diff --git a/conformance/differential/receipt/binding_covers_only_one_path/input.json b/conformance/differential/receipt/binding_covers_only_one_path/input.json new file mode 100644 index 0000000..44d5974 --- /dev/null +++ b/conformance/differential/receipt/binding_covers_only_one_path/input.json @@ -0,0 +1,71 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas" + } +} diff --git a/conformance/differential/receipt/binding_to_unknown_key/expected.json b/conformance/differential/receipt/binding_to_unknown_key/expected.json new file mode 100644 index 0000000..b9167f5 --- /dev/null +++ b/conformance/differential/receipt/binding_to_unknown_key/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "the path is bound to a key the object does not have", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-MEMBERSHIP", + "path": "$.materialization.applied_defaults[0].path" + } +} diff --git a/conformance/differential/receipt/binding_to_unknown_key/input.json b/conformance/differential/receipt/binding_to_unknown_key/input.json new file mode 100644 index 0000000..da20ffd --- /dev/null +++ b/conformance/differential/receipt/binding_to_unknown_key/input.json @@ -0,0 +1,72 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "nope", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/bound_defaults_and_derived_accepted/expected.json b/conformance/differential/receipt/bound_defaults_and_derived_accepted/expected.json new file mode 100644 index 0000000..66f29d7 --- /dev/null +++ b/conformance/differential/receipt/bound_defaults_and_derived_accepted/expected.json @@ -0,0 +1,4 @@ +{ + "accepted": true, + "why": "one defaulted and one derived field, each bound through an explicit path binding; membership and both value digests are checked" +} diff --git a/conformance/differential/receipt/bound_defaults_and_derived_accepted/input.json b/conformance/differential/receipt/bound_defaults_and_derived_accepted/input.json new file mode 100644 index 0000000..93b2a37 --- /dev/null +++ b/conformance/differential/receipt/bound_defaults_and_derived_accepted/input.json @@ -0,0 +1,72 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.oper_state" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/default_bound_to_authored_field/expected.json b/conformance/differential/receipt/default_bound_to_authored_field/expected.json new file mode 100644 index 0000000..6f95b7e --- /dev/null +++ b/conformance/differential/receipt/default_bound_to_authored_field/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "an applied_defaults entry bound to a field whose provenance is authored", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-MEMBERSHIP", + "path": "$.materialization.applied_defaults[0].path" + } +} diff --git a/conformance/differential/receipt/default_bound_to_authored_field/input.json b/conformance/differential/receipt/default_bound_to_authored_field/input.json new file mode 100644 index 0000000..77c8025 --- /dev/null +++ b/conformance/differential/receipt/default_bound_to_authored_field/input.json @@ -0,0 +1,72 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "admin_state", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/default_rule_not_the_literal/expected.json b/conformance/differential/receipt/default_rule_not_the_literal/expected.json new file mode 100644 index 0000000..dcb4c8c --- /dev/null +++ b/conformance/differential/receipt/default_rule_not_the_literal/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "DefaultEvidence.rule is the literal schema-default", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-EVIDENCE-FORM", + "path": "$.materialization.applied_defaults[0].rule" + } +} diff --git a/conformance/differential/receipt/default_rule_not_the_literal/input.json b/conformance/differential/receipt/default_rule_not_the_literal/input.json new file mode 100644 index 0000000..81d0419 --- /dev/null +++ b/conformance/differential/receipt/default_rule_not_the_literal/input.json @@ -0,0 +1,53 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default-v2", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/default_value_digest_changed/expected.json b/conformance/differential/receipt/default_value_digest_changed/expected.json new file mode 100644 index 0000000..97ac565 --- /dev/null +++ b/conformance/differential/receipt/default_value_digest_changed/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "value_digest is not the digest of the bound field's value", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-VALUE", + "path": "$.materialization.applied_defaults[0].value_digest" + } +} diff --git a/conformance/differential/receipt/default_value_digest_changed/input.json b/conformance/differential/receipt/default_value_digest_changed/input.json new file mode 100644 index 0000000..9bab616 --- /dev/null +++ b/conformance/differential/receipt/default_value_digest_changed/input.json @@ -0,0 +1,72 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/derived_bound_to_default_field/expected.json b/conformance/differential/receipt/derived_bound_to_default_field/expected.json new file mode 100644 index 0000000..bd5b658 --- /dev/null +++ b/conformance/differential/receipt/derived_bound_to_default_field/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "a derived_values entry bound to a schema_default field", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-MEMBERSHIP", + "path": "$.materialization.derived_values[0].path" + } +} diff --git a/conformance/differential/receipt/derived_bound_to_default_field/input.json b/conformance/differential/receipt/derived_bound_to_default_field/input.json new file mode 100644 index 0000000..7713586 --- /dev/null +++ b/conformance/differential/receipt/derived_bound_to_default_field/input.json @@ -0,0 +1,72 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas", + "$.state.effective_state": "replicas" + } +} diff --git a/conformance/differential/receipt/derived_inputs_need_not_exist_in_object/expected.json b/conformance/differential/receipt/derived_inputs_need_not_exist_in_object/expected.json new file mode 100644 index 0000000..230c53a --- /dev/null +++ b/conformance/differential/receipt/derived_inputs_need_not_exist_in_object/expected.json @@ -0,0 +1,4 @@ +{ + "accepted": true, + "why": "inputs is checked for form and order only: the finished object cannot show what a derivation read, so an input path absent from the object is not an error" +} diff --git a/conformance/differential/receipt/derived_inputs_need_not_exist_in_object/input.json b/conformance/differential/receipt/derived_inputs_need_not_exist_in_object/input.json new file mode 100644 index 0000000..5f93879 --- /dev/null +++ b/conformance/differential/receipt/derived_inputs_need_not_exist_in_object/input.json @@ -0,0 +1,72 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/derived_rule_not_versioned/expected.json b/conformance/differential/receipt/derived_rule_not_versioned/expected.json new file mode 100644 index 0000000..a7ed398 --- /dev/null +++ b/conformance/differential/receipt/derived_rule_not_versioned/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "DerivedEvidence.rule is name-vN", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-EVIDENCE-FORM", + "path": "$.materialization.derived_values[0].rule" + } +} diff --git a/conformance/differential/receipt/derived_rule_not_versioned/input.json b/conformance/differential/receipt/derived_rule_not_versioned/input.json new file mode 100644 index 0000000..eea4abf --- /dev/null +++ b/conformance/differential/receipt/derived_rule_not_versioned/input.json @@ -0,0 +1,54 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [ + { + "path": "$.a", + "rule": "effective-state", + "inputs": [], + "value_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777" + } + ] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/derived_value_digest_changed/expected.json b/conformance/differential/receipt/derived_value_digest_changed/expected.json new file mode 100644 index 0000000..a2406f9 --- /dev/null +++ b/conformance/differential/receipt/derived_value_digest_changed/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "value_digest is not the digest of the bound field's value", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-VALUE", + "path": "$.materialization.derived_values[0].value_digest" + } +} diff --git a/conformance/differential/receipt/derived_value_digest_changed/input.json b/conformance/differential/receipt/derived_value_digest_changed/input.json new file mode 100644 index 0000000..105bd33 --- /dev/null +++ b/conformance/differential/receipt/derived_value_digest_changed/input.json @@ -0,0 +1,72 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:7515e88eb641afbd3a1e2b5be034ba82cc8f4c48d098433a815b5eeaea588f20" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/derived_values_unsorted/expected.json b/conformance/differential/receipt/derived_values_unsorted/expected.json new file mode 100644 index 0000000..21b385f --- /dev/null +++ b/conformance/differential/receipt/derived_values_unsorted/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "derived_values must be ascending by .path", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-ORDER", + "path": "$.materialization.derived_values[1].path" + } +} diff --git a/conformance/differential/receipt/derived_values_unsorted/input.json b/conformance/differential/receipt/derived_values_unsorted/input.json new file mode 100644 index 0000000..be9af3d --- /dev/null +++ b/conformance/differential/receipt/derived_values_unsorted/input.json @@ -0,0 +1,60 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [ + { + "path": "$.b", + "rule": "x-v1", + "inputs": [], + "value_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777" + }, + { + "path": "$.a", + "rule": "x-v1", + "inputs": [], + "value_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777" + } + ] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/digest_bare_hex/expected.json b/conformance/differential/receipt/digest_bare_hex/expected.json new file mode 100644 index 0000000..92fc4fa --- /dev/null +++ b/conformance/differential/receipt/digest_bare_hex/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "a bare-hex digest lacks the sha256: prefix", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-DIGEST-FORMAT", + "path": "$.materialization.schema_digest" + } +} diff --git a/conformance/differential/receipt/digest_bare_hex/input.json b/conformance/differential/receipt/digest_bare_hex/input.json new file mode 100644 index 0000000..8723202 --- /dev/null +++ b/conformance/differential/receipt/digest_bare_hex/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/digest_base64/expected.json b/conformance/differential/receipt/digest_base64/expected.json new file mode 100644 index 0000000..ce6eb7f --- /dev/null +++ b/conformance/differential/receipt/digest_base64/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "a base64 digest is not accepted as is; only a verified re-spelling of the same SHA-256 bytes may be converted by the producer", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-DIGEST-FORMAT", + "path": "$.materialization.primitive_build_hash" + } +} diff --git a/conformance/differential/receipt/digest_base64/input.json b/conformance/differential/receipt/digest_base64/input.json new file mode 100644 index 0000000..18afc1d --- /dev/null +++ b/conformance/differential/receipt/digest_base64/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "i/W6jEVC1JCafwHmWbxO7LrtFdGVxgAS4SA1789JGx0=", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/digest_uppercase_hex/expected.json b/conformance/differential/receipt/digest_uppercase_hex/expected.json new file mode 100644 index 0000000..de07463 --- /dev/null +++ b/conformance/differential/receipt/digest_uppercase_hex/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "uppercase hex is not the normative spelling", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-DIGEST-FORMAT", + "path": "$.materialization.grammar_sha256" + } +} diff --git a/conformance/differential/receipt/digest_uppercase_hex/input.json b/conformance/differential/receipt/digest_uppercase_hex/input.json new file mode 100644 index 0000000..bfc36cd --- /dev/null +++ b/conformance/differential/receipt/digest_uppercase_hex/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/empty_evidence_accepted/expected.json b/conformance/differential/receipt/empty_evidence_accepted/expected.json new file mode 100644 index 0000000..a5cf295 --- /dev/null +++ b/conformance/differential/receipt/empty_evidence_accepted/expected.json @@ -0,0 +1,4 @@ +{ + "accepted": true, + "why": "both evidence lists are empty and the object holds no defaulted or derived field: verifiable with no path binding" +} diff --git a/conformance/differential/receipt/empty_evidence_accepted/input.json b/conformance/differential/receipt/empty_evidence_accepted/input.json new file mode 100644 index 0000000..34cab2b --- /dev/null +++ b/conformance/differential/receipt/empty_evidence_accepted/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/evidence_path_not_rooted/expected.json b/conformance/differential/receipt/evidence_path_not_rooted/expected.json new file mode 100644 index 0000000..adeccd4 --- /dev/null +++ b/conformance/differential/receipt/evidence_path_not_rooted/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "a path is $.-rooted with non-empty segments", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-EVIDENCE-FORM", + "path": "$.materialization.applied_defaults[0].path" + } +} diff --git a/conformance/differential/receipt/evidence_path_not_rooted/input.json b/conformance/differential/receipt/evidence_path_not_rooted/input.json new file mode 100644 index 0000000..47c832a --- /dev/null +++ b/conformance/differential/receipt/evidence_path_not_rooted/input.json @@ -0,0 +1,53 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [ + { + "path": "config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/evidence_without_binding_is_refused/expected.json b/conformance/differential/receipt/evidence_without_binding_is_refused/expected.json new file mode 100644 index 0000000..64c9868 --- /dev/null +++ b/conformance/differential/receipt/evidence_without_binding_is_refused/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "non-empty evidence with no path binding fails closed; flat object keys are never turned into $.config.* paths by guessing", + "error": { + "code": "E_RECEIPT_UNBOUND", + "rule": "R-RECEIPT-PATH-BINDING", + "path": "$.materialization.applied_defaults[0].path" + } +} diff --git a/conformance/differential/receipt/evidence_without_binding_is_refused/input.json b/conformance/differential/receipt/evidence_without_binding_is_refused/input.json new file mode 100644 index 0000000..af00730 --- /dev/null +++ b/conformance/differential/receipt/evidence_without_binding_is_refused/input.json @@ -0,0 +1,68 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + } +} diff --git a/conformance/differential/receipt/input_digest_changed/expected.json b/conformance/differential/receipt/input_digest_changed/expected.json new file mode 100644 index 0000000..70c4f5a --- /dev/null +++ b/conformance/differential/receipt/input_digest_changed/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "input_digest is the digest of a different authored input", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-BINDS-INPUT", + "path": "$.materialization.input_digest" + } +} diff --git a/conformance/differential/receipt/input_digest_changed/input.json b/conformance/differential/receipt/input_digest_changed/input.json new file mode 100644 index 0000000..38d7d4e --- /dev/null +++ b/conformance/differential/receipt/input_digest_changed/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:31685b60fbe09626137acfd9dcf3d1c299d415d57415510efba2e20c09ec3691", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/input_path_empty_segment/expected.json b/conformance/differential/receipt/input_path_empty_segment/expected.json new file mode 100644 index 0000000..d9330bd --- /dev/null +++ b/conformance/differential/receipt/input_path_empty_segment/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "an input path with an empty segment", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-EVIDENCE-FORM", + "path": "$.materialization.derived_values[0].inputs[0]" + } +} diff --git a/conformance/differential/receipt/input_path_empty_segment/input.json b/conformance/differential/receipt/input_path_empty_segment/input.json new file mode 100644 index 0000000..c4a91ad --- /dev/null +++ b/conformance/differential/receipt/input_path_empty_segment/input.json @@ -0,0 +1,56 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [ + { + "path": "$.a", + "rule": "x-v1", + "inputs": [ + "$.a..b" + ], + "value_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777" + } + ] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/inputs_duplicate/expected.json b/conformance/differential/receipt/inputs_duplicate/expected.json new file mode 100644 index 0000000..09f5455 --- /dev/null +++ b/conformance/differential/receipt/inputs_duplicate/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "inputs is deduplicated", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-ORDER", + "path": "$.materialization.derived_values[0].inputs[1]" + } +} diff --git a/conformance/differential/receipt/inputs_duplicate/input.json b/conformance/differential/receipt/inputs_duplicate/input.json new file mode 100644 index 0000000..b782272 --- /dev/null +++ b/conformance/differential/receipt/inputs_duplicate/input.json @@ -0,0 +1,57 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [ + { + "path": "$.a", + "rule": "x-v1", + "inputs": [ + "$.y", + "$.y" + ], + "value_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777" + } + ] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/inputs_unsorted/expected.json b/conformance/differential/receipt/inputs_unsorted/expected.json new file mode 100644 index 0000000..4d60cea --- /dev/null +++ b/conformance/differential/receipt/inputs_unsorted/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "inputs is a sorted path set", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-ORDER", + "path": "$.materialization.derived_values[0].inputs[1]" + } +} diff --git a/conformance/differential/receipt/inputs_unsorted/input.json b/conformance/differential/receipt/inputs_unsorted/input.json new file mode 100644 index 0000000..67bb3e7 --- /dev/null +++ b/conformance/differential/receipt/inputs_unsorted/input.json @@ -0,0 +1,57 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [ + { + "path": "$.a", + "rule": "x-v1", + "inputs": [ + "$.z", + "$.y" + ], + "value_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777" + } + ] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/listed_evidence_for_object_without_it/expected.json b/conformance/differential/receipt/listed_evidence_for_object_without_it/expected.json new file mode 100644 index 0000000..bec2815 --- /dev/null +++ b/conformance/differential/receipt/listed_evidence_for_object_without_it/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "an applied_defaults entry for an object with no defaulted field", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-MEMBERSHIP", + "path": "$.materialization.applied_defaults[0].path" + } +} diff --git a/conformance/differential/receipt/listed_evidence_for_object_without_it/input.json b/conformance/differential/receipt/listed_evidence_for_object_without_it/input.json new file mode 100644 index 0000000..fc05f72 --- /dev/null +++ b/conformance/differential/receipt/listed_evidence_for_object_without_it/input.json @@ -0,0 +1,56 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + }, + "path_binding": { + "$.config.replicas": "shape" + } +} diff --git a/conformance/differential/receipt/missing_evidence_list/expected.json b/conformance/differential/receipt/missing_evidence_list/expected.json new file mode 100644 index 0000000..ee7beea --- /dev/null +++ b/conformance/differential/receipt/missing_evidence_list/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "an omitted applied_defaults is ambiguous; [] is required", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-SHAPE", + "path": "$.materialization.applied_defaults" + } +} diff --git a/conformance/differential/receipt/missing_evidence_list/input.json b/conformance/differential/receipt/missing_evidence_list/input.json new file mode 100644 index 0000000..4a87b81 --- /dev/null +++ b/conformance/differential/receipt/missing_evidence_list/input.json @@ -0,0 +1,46 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/missing_field/expected.json b/conformance/differential/receipt/missing_field/expected.json new file mode 100644 index 0000000..2e57229 --- /dev/null +++ b/conformance/differential/receipt/missing_field/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "engine_version is absent", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-SHAPE", + "path": "$.materialization.engine_version" + } +} diff --git a/conformance/differential/receipt/missing_field/input.json b/conformance/differential/receipt/missing_field/input.json new file mode 100644 index 0000000..bf160ea --- /dev/null +++ b/conformance/differential/receipt/missing_field/input.json @@ -0,0 +1,46 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/missing_wrapper/expected.json b/conformance/differential/receipt/missing_wrapper/expected.json new file mode 100644 index 0000000..a5b47dd --- /dev/null +++ b/conformance/differential/receipt/missing_wrapper/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "the sole wrapper key `materialization` is absent", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-SHAPE", + "path": "$" + } +} diff --git a/conformance/differential/receipt/missing_wrapper/input.json b/conformance/differential/receipt/missing_wrapper/input.json new file mode 100644 index 0000000..7d4b1d8 --- /dev/null +++ b/conformance/differential/receipt/missing_wrapper/input.json @@ -0,0 +1,45 @@ +{ + "receipt": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/object_default_not_listed/expected.json b/conformance/differential/receipt/object_default_not_listed/expected.json new file mode 100644 index 0000000..27fe08b --- /dev/null +++ b/conformance/differential/receipt/object_default_not_listed/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "the object's schema_default field has no applied_defaults entry", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-MEMBERSHIP", + "path": "object/replicas" + } +} diff --git a/conformance/differential/receipt/object_default_not_listed/input.json b/conformance/differential/receipt/object_default_not_listed/input.json new file mode 100644 index 0000000..6a93296 --- /dev/null +++ b/conformance/differential/receipt/object_default_not_listed/input.json @@ -0,0 +1,66 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/object_derived_not_listed/expected.json b/conformance/differential/receipt/object_derived_not_listed/expected.json new file mode 100644 index 0000000..4dbe94c --- /dev/null +++ b/conformance/differential/receipt/object_derived_not_listed/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "the object's derived field has no derived_values entry", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-MEMBERSHIP", + "path": "object/effective_state" + } +} diff --git a/conformance/differential/receipt/object_derived_not_listed/input.json b/conformance/differential/receipt/object_derived_not_listed/input.json new file mode 100644 index 0000000..9f154c2 --- /dev/null +++ b/conformance/differential/receipt/object_derived_not_listed/input.json @@ -0,0 +1,62 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/output_digest_changed/expected.json b/conformance/differential/receipt/output_digest_changed/expected.json new file mode 100644 index 0000000..a0fa239 --- /dev/null +++ b/conformance/differential/receipt/output_digest_changed/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "output_digest belongs to another materialized object", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-BINDS-OUTPUT", + "path": "$.materialization.output_digest" + } +} diff --git a/conformance/differential/receipt/output_digest_changed/input.json b/conformance/differential/receipt/output_digest_changed/input.json new file mode 100644 index 0000000..45164ba --- /dev/null +++ b/conformance/differential/receipt/output_digest_changed/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/two_paths_one_field/expected.json b/conformance/differential/receipt/two_paths_one_field/expected.json new file mode 100644 index 0000000..a437b5e --- /dev/null +++ b/conformance/differential/receipt/two_paths_one_field/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "two receipt paths bound to the same object field", + "error": { + "code": "E_RECEIPT_MISMATCH", + "rule": "R-RECEIPT-EVIDENCE-MEMBERSHIP", + "path": "$.materialization.applied_defaults[1].path" + } +} diff --git a/conformance/differential/receipt/two_paths_one_field/input.json b/conformance/differential/receipt/two_paths_one_field/input.json new file mode 100644 index 0000000..7fd661c --- /dev/null +++ b/conformance/differential/receipt/two_paths_one_field/input.json @@ -0,0 +1,78 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:fa436f473bd3ca77dfd99e29b023038ddc8bfcc04cac2b16057c7b8aef65c4a7", + "output_digest": "sha256:1fe567ac0333d3a4d60162cce7e7c3f8a272ed8b63e02d9774c61dce94dea0c0", + "applied_defaults": [ + { + "path": "$.config.replicas", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + }, + { + "path": "$.config.replicas2", + "rule": "schema-default", + "value_digest": "sha256:4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" + } + ], + "derived_values": [ + { + "path": "$.state.effective_state", + "rule": "effective-state-v1", + "inputs": [ + "$.state.admin_state", + "$.state.never_materialized" + ], + "value_digest": "sha256:294bdbf9fed030b63703f2925091c2da3e4bf211b2b6b1b6ad3a3cfd9478353a" + } + ] + } + }, + "authored_input": { + "admin_state": "up" + }, + "object": { + "fields": [ + { + "key": "admin_state", + "capability": "implemented", + "provenance": "authored", + "value": "up" + }, + { + "key": "effective_state", + "capability": "implemented", + "provenance": "derived", + "value": "RUNNING" + }, + { + "key": "oper_state", + "capability": "implemented", + "coverage": "observed", + "value": "up" + }, + { + "key": "replicas", + "capability": "implemented", + "provenance": "schema_default", + "value": 3 + } + ] + }, + "path_binding": { + "$.config.replicas": "replicas", + "$.config.replicas2": "replicas", + "$.state.effective_state": "effective_state" + } +} diff --git a/conformance/differential/receipt/unknown_top_level_member/expected.json b/conformance/differential/receipt/unknown_top_level_member/expected.json new file mode 100644 index 0000000..a3172b1 --- /dev/null +++ b/conformance/differential/receipt/unknown_top_level_member/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "the layout is closed; the schema has no signature field", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-SHAPE", + "path": "$.materialization.signature" + } +} diff --git a/conformance/differential/receipt/unknown_top_level_member/input.json b/conformance/differential/receipt/unknown_top_level_member/input.json new file mode 100644 index 0000000..62a7070 --- /dev/null +++ b/conformance/differential/receipt/unknown_top_level_member/input.json @@ -0,0 +1,48 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [], + "signature": "x" + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/unsupported_version/expected.json b/conformance/differential/receipt/unsupported_version/expected.json new file mode 100644 index 0000000..e8ffe63 --- /dev/null +++ b/conformance/differential/receipt/unsupported_version/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "only receipt_schema_version 1 is supported", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-VERSION", + "path": "$.materialization.receipt_schema_version" + } +} diff --git a/conformance/differential/receipt/unsupported_version/input.json b/conformance/differential/receipt/unsupported_version/input.json new file mode 100644 index 0000000..5ff8581 --- /dev/null +++ b/conformance/differential/receipt/unsupported_version/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 2, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/version_not_an_integer/expected.json b/conformance/differential/receipt/version_not_an_integer/expected.json new file mode 100644 index 0000000..9301f35 --- /dev/null +++ b/conformance/differential/receipt/version_not_an_integer/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "1.0 is a float, not the integer 1", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-SHAPE", + "path": "$.materialization.receipt_schema_version" + } +} diff --git a/conformance/differential/receipt/version_not_an_integer/input.json b/conformance/differential/receipt/version_not_an_integer/input.json new file mode 100644 index 0000000..dd9a5a9 --- /dev/null +++ b/conformance/differential/receipt/version_not_an_integer/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": 1.0, + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/conformance/differential/receipt/version_wrong_type/expected.json b/conformance/differential/receipt/version_wrong_type/expected.json new file mode 100644 index 0000000..2f99bab --- /dev/null +++ b/conformance/differential/receipt/version_wrong_type/expected.json @@ -0,0 +1,9 @@ +{ + "accepted": false, + "why": "the version is a string", + "error": { + "code": "E_INVALID_RECEIPT", + "rule": "R-RECEIPT-SHAPE", + "path": "$.materialization.receipt_schema_version" + } +} diff --git a/conformance/differential/receipt/version_wrong_type/input.json b/conformance/differential/receipt/version_wrong_type/input.json new file mode 100644 index 0000000..05c68a9 --- /dev/null +++ b/conformance/differential/receipt/version_wrong_type/input.json @@ -0,0 +1,47 @@ +{ + "receipt": { + "materialization": { + "receipt_schema_version": "1", + "grammar_sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "grammar_schema_sha256": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "primitive_release": "primitives/@v0.2.0", + "primitive_source_commit": "960ee0f048d969a85ad20819bc0f2366ad91fa40", + "primitive_build_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "schema_name": "cic:storage:StorageResource", + "schema_version": "v1.2.0", + "schema_digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "engine_identity": "environment-reference-fixture", + "engine_version": "0.0.0", + "input_digest": "sha256:7e6dbda8c11818a96d216350f13efb0edb5399b801e3ec054f1f2fab8d63324c", + "output_digest": "sha256:28a5a9b53b4239b38731bb401b23cacc18e38d3656275a8b62a6fecce95cb110", + "applied_defaults": [], + "derived_values": [] + } + }, + "authored_input": { + "shape": "E4.Flex", + "memory_gb": 16 + }, + "object": { + "fields": [ + { + "key": "memory_gb", + "capability": "implemented", + "provenance": "authored", + "value": 16 + }, + { + "key": "power_state", + "capability": "implemented", + "coverage": "observed", + "value": "running" + }, + { + "key": "shape", + "capability": "implemented", + "provenance": "authored", + "value": "E4.Flex" + } + ] + } +} diff --git a/docs/RECEIPT-VERIFIER.md b/docs/RECEIPT-VERIFIER.md new file mode 100644 index 0000000..ec6599a --- /dev/null +++ b/docs/RECEIPT-VERIFIER.md @@ -0,0 +1,90 @@ +# The receipt verifier + +`engine/src/receipt.rs` (Rust) and `go/receipt` (Go) check a materialization +receipt whose layout `RECEIPT-SCHEMA.md` decided. They are verifiers. **No +receipt producer exists in this crate**, and none is added by this change: +materialization, and the evidence a default or a derivation leaves while it +runs, belong to the environment (`BOUNDARY.md`). The receipt contract, the +digest format, D-018 and the scope boundary are not changed here. + +## What a success establishes + +Structural and content consistency, and nothing more. + +- All 15 required top-level fields are present, correctly typed, in a closed + layout (no other member), under the sole wrapper key `materialization`. +- `receipt_schema_version` is the integer `1`. A float (`1.0`) is not an + integer. +- Every digest is `sha256:` followed by 64 lowercase hex characters. A base64 + or bare-hex spelling is refused: re-spelling the same SHA-256 bytes is the + producer's job, and only an equal-bytes re-spelling is a valid conversion; a + different hash payload or a different canonicalization does not become + equivalent by changing the spelling. +- `applied_defaults` and `derived_values` are strictly ascending by `.path` + (raw UTF-8 byte order; a repeated path is a rejection, not a tolerated + duplicate), every `rule`, `path` and `value_digest` has its decided form + (`applied_defaults[].rule` is the literal `schema-default`; a derived rule is + `name-vN`; paths are `$.`-rooted with non-empty segments), and each + `derived_values[].inputs` is a strictly ascending, deduplicated path set. +- The evidence lists and every `inputs` list are required arrays: `[]` when + empty, never null. In Go a `nil` slice is the null spelling, so the typed API + refuses it (`R-RECEIPT-SHAPE`) and `Parse` turns a JSON `[]` into a non-nil + empty slice. Rust's `Vec` cannot be null, so the invariant already holds. +- `output_digest` equals `materialized_object_digest` of the object it was + handed. +- `input_digest` equals the A7 digest of the canonical form of the authored + input it was handed. The input is a processed value, not raw source bytes, + and no YAML reader is assumed or used. +- The object's defaulted fields (`schema_default` provenance) and derived + fields (`derived` provenance, intent- or state-side) are exactly the ones the + receipt lists, each entry's `value_digest` equals the digest of that field's + value, and no object field is listed twice. + +## What it does not establish + +- That a default was applied or a derivation ran. A receipt that passes is + consistent with the object; it is not evidence about the execution that + produced the object. `rule` and `inputs` are the producer's statements. +- That `inputs` is what the derivation actually read. The finished object cannot + show that, so no input path is required to exist in the object, and none is + looked up. Form and order are all that is checked. +- Anything about the identifiers. `grammar_*`, `primitive_*`, `schema_*` and + `engine_*` are claims: their form is checked, the release signature and the + origin of the pinned schema are not authenticated, and no signature is + verified (the schema has none). `engine_identity` names the implementation + that materialized the data, not this verifier. + +## Evidence needs an explicit path binding + +A `MaterializedObject` is keyed by flat field names; a receipt names fields by +path. The mapping between them is not specified anywhere in the contract, so +this verifier does not guess it. The caller passes a `PathBinding` (receipt +path to object key). An evidence entry whose path is not in the binding is +refused with `E_RECEIPT_UNBOUND`; nothing is accepted partially. + +With both evidence lists empty and no defaulted or derived field in the +object, no binding is needed, and that case verifies completely. + +## Rejections + +| code | meaning | rules | +|---|---|---| +| `E_INVALID_RECEIPT` | not a well-formed receipt | `R-RECEIPT-SHAPE`, `-VERSION`, `-DIGEST-FORMAT`, `-EVIDENCE-FORM`, `-ORDER` | +| `E_RECEIPT_MISMATCH` | well formed, but disagrees with the artifacts | `R-RECEIPT-BINDS-INPUT`, `-BINDS-OUTPUT`, `-EVIDENCE-MEMBERSHIP`, `-EVIDENCE-VALUE` | +| `E_RECEIPT_UNBOUND` | an evidence path has no explicit binding | `R-RECEIPT-PATH-BINDING` | + +The reported location is a path inside the receipt +(`$.materialization.applied_defaults[0].value_digest`) or, for an object field +the receipt does not list, `object/`. Checks run in a fixed order (shape +and structure in declared field order, `input_digest`, `output_digest`, +evidence entries in list order, the object's own fields in key order) so both +languages name the same location. + +## Corpus + +`conformance/differential/receipt/` holds the shared vectors; see that +directory's README for the format. The rejection vectors cover a changed input +digest, a changed output digest, a missing field, an unsupported version, a +version that is not an integer, unsorted and duplicated evidence, unsorted and +duplicated `inputs`, missing path binding, and evidence that disagrees with +the bound object field. diff --git a/engine/src/error.rs b/engine/src/error.rs index f378a67..b8c165a 100644 --- a/engine/src/error.rs +++ b/engine/src/error.rs @@ -140,6 +140,26 @@ pub mod code { /// `object` does not follow from `fields` (`R-VERDICT-CONSISTENT`, /// `docs/VERDICT-SCHEMA.md`). Raised by `verdict::build_verdict`. pub const INCONSISTENT_VERDICT: &str = "E_INCONSISTENT_VERDICT"; + + /// A receipt is not well formed: a required field is missing or has the + /// wrong type, a member is outside the layout, `receipt_schema_version` is + /// unsupported, a digest is not `sha256:` + 64 lowercase hex, or an + /// evidence list is malformed or out of order (`R-RECEIPT-SHAPE`, + /// `-VERSION`, `-DIGEST-FORMAT`, `-EVIDENCE-FORM`, `-ORDER`; + /// `docs/RECEIPT-SCHEMA.md`). Raised by `receipt::verify_receipt`. + pub const INVALID_RECEIPT: &str = "E_INVALID_RECEIPT"; + + /// A well-formed receipt disagrees with the artifacts it claims to + /// describe: `input_digest` or `output_digest` does not match, or an + /// evidence entry does not match the bound object field + /// (`R-RECEIPT-BINDS-INPUT`, `-BINDS-OUTPUT`, `-EVIDENCE-MEMBERSHIP`, + /// `-EVIDENCE-VALUE`). Raised by `receipt::verify_receipt`. + pub const RECEIPT_MISMATCH: &str = "E_RECEIPT_MISMATCH"; + + /// An evidence entry's path has no explicit binding to a materialized + /// object key (`R-RECEIPT-PATH-BINDING`). Refused, never partially + /// accepted. Raised by `receipt::verify_receipt`. + pub const RECEIPT_UNBOUND: &str = "E_RECEIPT_UNBOUND"; } /// The single error type this crate raises. diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 3ef4770..ab1eda2 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -226,6 +226,7 @@ pub mod error; pub mod materialized; pub mod plan; pub mod reader; +pub mod receipt; pub mod verdict; pub use canonical::{digest, to_canonical_json}; @@ -245,6 +246,9 @@ pub use plan::{ evaluate, CollectionPlan, ConformancePlan, ConsumedField, FieldPlan, ObjectConformance, ObjectVerdict, }; +pub use receipt::{ + verify_receipt, DefaultEvidence, DerivedEvidence, PathBinding, Receipt, RECEIPT_SCHEMA_VERSION, +}; pub use verdict::{ build_verdict, evaluate_verdict, verdict_canonical_bytes, verdict_value, ConformanceVerdict, VERDICT_SCHEMA_VERSION, diff --git a/engine/src/receipt.rs b/engine/src/receipt.rs new file mode 100644 index 0000000..f0d34da --- /dev/null +++ b/engine/src/receipt.rs @@ -0,0 +1,768 @@ +//! A verifier for the materialization receipt (`docs/RECEIPT-SCHEMA.md`). +//! +//! This module checks a receipt that somebody else produced. It never +//! produces one: materialization, and the evidence a default or a derivation +//! leaves while it runs, belong to the environment (`docs/BOUNDARY.md`). +//! +//! # What a successful result establishes +//! +//! **Structural and content consistency, and nothing more.** A receipt that +//! passes [`verify_receipt`]: +//! +//! * has all 15 required top-level fields, with the right types, a supported +//! `receipt_schema_version` (`1`) and every digest spelled `sha256:` + 64 +//! lowercase hex characters; +//! * lists `applied_defaults` and `derived_values` in strictly ascending +//! `.path` order, and each `inputs` list as a strictly ascending +//! (deduplicated) path set; +//! * carries an `input_digest` equal to the A7 digest of the canonical form of +//! the authored input it was handed, and an `output_digest` equal to +//! [`materialized_object_digest`] of the object it was handed; +//! * lists exactly the defaulted and the derived fields of that object, each +//! with the digest of the field's value, **through the explicit +//! [`PathBinding`] the caller supplied**. +//! +//! It does **not** establish that a default was applied or a derivation ran, +//! that `rule` or `inputs` describe what actually happened, who produced the +//! receipt, or that any identifier is genuine: +//! +//! * `grammar_*`, `primitive_*`, `schema_*` and `engine_*` are claims. Their +//! form is checked; the release signature and the origin of the pinned +//! schema are not (external authentication is not this module's job). +//! * `engine_identity` names the implementation that **materialized** the +//! data, not this verifier. +//! * No signature is checked. The receipt schema has no signature field. +//! +//! # `input_digest` +//! +//! The caller passes the processed authored input as a [`Value`]. The digest +//! is taken over that value's canonical JSON (section A), not over raw source +//! bytes, and no YAML reader is assumed. +//! +//! # Evidence needs an explicit path binding +//! +//! A `MaterializedObject` is keyed by flat field names. A receipt names fields +//! by path (`$.config.replicas`). This module does not guess a mapping from +//! one to the other. When the receipt lists any evidence entry, the caller +//! must pass a [`PathBinding`] from receipt path to object key; an entry whose +//! path is not in the binding is refused with `E_RECEIPT_UNBOUND` +//! (fail-closed), never accepted partially. With both evidence lists empty and +//! no defaulted or derived field in the object, no binding is needed. +//! +//! `derived_values[].inputs` is checked for form and order only. The finished +//! object cannot show which paths a derivation actually read, so no input path +//! is required to exist in the object. +//! +//! # Check order +//! +//! Shape and structure first (declared field order), then `input_digest`, then +//! `output_digest`, then evidence entries in list order, then the object's own +//! defaulted/derived fields in key order. The Go peer (`go/receipt`) runs the +//! same order, so both name the same offending location. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::canonical::{digest, to_canonical_json}; +use crate::digest_projection::materialized_object_digest; +use crate::error::{code, Error, Result, Stage}; +use crate::materialized::{FieldEvidence, IntentEvidence, MaterializedField, MaterializedObject}; +use crate::reader::Value; + +/// The only `receipt_schema_version` this verifier supports. +pub const RECEIPT_SCHEMA_VERSION: i64 = 1; + +/// `DefaultEvidence.rule`: a literal v1 constant (`RECEIPT-SCHEMA.md`, C5). +pub const DEFAULT_RULE: &str = "schema-default"; + +/// The sole wrapper key of a receipt document. +const WRAPPER: &str = "materialization"; + +/// The 15 required top-level fields, in declared order. +const FIELDS: [&str; 15] = [ + "receipt_schema_version", + "grammar_sha256", + "grammar_schema_sha256", + "primitive_release", + "primitive_source_commit", + "primitive_build_hash", + "schema_name", + "schema_version", + "schema_digest", + "engine_identity", + "engine_version", + "input_digest", + "output_digest", + "applied_defaults", + "derived_values", +]; + +/// Receipt path -> `MaterializedObject` key, supplied by the environment. +pub type PathBinding = BTreeMap; + +/// `applied_defaults[]` entry (`DefaultEvidence`). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DefaultEvidence { + pub path: String, + pub rule: String, + pub value_digest: String, +} + +/// `derived_values[]` entry (`DerivedEvidence`). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DerivedEvidence { + pub path: String, + pub rule: String, + pub inputs: Vec, + pub value_digest: String, +} + +/// The materialization receipt: 15 required fields, none optional. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Receipt { + pub receipt_schema_version: i64, + pub grammar_sha256: String, + pub grammar_schema_sha256: String, + pub primitive_release: String, + pub primitive_source_commit: String, + pub primitive_build_hash: String, + pub schema_name: String, + pub schema_version: String, + pub schema_digest: String, + pub engine_identity: String, + pub engine_version: String, + pub input_digest: String, + pub output_digest: String, + pub applied_defaults: Vec, + pub derived_values: Vec, +} + +fn invalid(rule: &'static str, path: &str, detail: String) -> Error { + Error::new(code::INVALID_RECEIPT, rule, Stage::Validate, path, detail) +} + +fn mismatch(rule: &'static str, path: &str, detail: String) -> Error { + Error::new(code::RECEIPT_MISMATCH, rule, Stage::Validate, path, detail) +} + +fn field_path(name: &str) -> String { + format!("$.{WRAPPER}.{name}") +} + +fn shape(path: &str, want: &str) -> Error { + invalid("R-RECEIPT-SHAPE", path, format!("expected {want}")) +} + +fn take_str(m: &crate::reader::Map, key: &str, path: &str) -> Result { + match m.get(key) { + Some(Value::Str(s)) => Ok(s.clone()), + Some(_) => Err(shape(path, "a string")), + None => Err(shape(path, "this required field (it is absent)")), + } +} + +fn take_map<'a>(v: &'a Value, path: &str) -> Result<&'a crate::reader::Map> { + match v { + Value::Map(m) => Ok(m), + _ => Err(shape(path, "a mapping")), + } +} + +fn check_keys(m: &crate::reader::Map, allowed: &[&str], path: &str) -> Result<()> { + let mut extra: Vec<&str> = m + .keys() + .into_iter() + .filter(|k| !allowed.contains(k)) + .collect(); + extra.sort_unstable(); + match extra.first() { + Some(k) => Err(shape( + &format!("{path}.{k}"), + "no member outside the receipt layout", + )), + None => Ok(()), + } +} + +impl Receipt { + /// Reads a receipt document: a mapping whose only key is + /// `materialization`, holding the 15 fields. Shape errors (missing or + /// unknown member, wrong type) are raised here, in declared field order; + /// form and order errors are raised by [`Receipt::check_structure`], which + /// this calls last. + /// + /// # Errors + /// `E_INVALID_RECEIPT`. + pub fn from_value(doc: &Value) -> Result { + let top = take_map(doc, "$")?; + if top.get(WRAPPER).is_none() { + return Err(shape("$", "the sole wrapper key `materialization`")); + } + check_keys(top, &[WRAPPER], "$")?; + let base = format!("$.{WRAPPER}"); + let body = take_map(top.get(WRAPPER).expect("checked above"), &base)?; + + let mut version = 0_i64; + let mut strings: BTreeMap<&str, String> = BTreeMap::new(); + let mut defaults = Vec::new(); + let mut derived = Vec::new(); + for name in FIELDS { + let path = field_path(name); + match name { + "receipt_schema_version" => match body.get(name) { + Some(Value::Int(n)) => version = *n, + Some(_) => return Err(shape(&path, "an integer")), + None => return Err(shape(&path, "this required field (it is absent)")), + }, + "applied_defaults" => match body.get(name) { + Some(Value::Seq(items)) => { + for (i, item) in items.iter().enumerate() { + defaults.push(parse_default(item, &format!("{path}[{i}]"))?); + } + } + Some(_) => return Err(shape(&path, "a sequence")), + None => return Err(shape(&path, "this required field (it is absent)")), + }, + "derived_values" => match body.get(name) { + Some(Value::Seq(items)) => { + for (i, item) in items.iter().enumerate() { + derived.push(parse_derived(item, &format!("{path}[{i}]"))?); + } + } + Some(_) => return Err(shape(&path, "a sequence")), + None => return Err(shape(&path, "this required field (it is absent)")), + }, + _ => { + strings.insert(name, take_str(body, name, &path)?); + } + } + } + check_keys(body, &FIELDS, &base)?; + + let mut s = |k: &str| { + strings + .remove(k) + .expect("every string field was read above") + }; + let receipt = Receipt { + receipt_schema_version: version, + grammar_sha256: s("grammar_sha256"), + grammar_schema_sha256: s("grammar_schema_sha256"), + primitive_release: s("primitive_release"), + primitive_source_commit: s("primitive_source_commit"), + primitive_build_hash: s("primitive_build_hash"), + schema_name: s("schema_name"), + schema_version: s("schema_version"), + schema_digest: s("schema_digest"), + engine_identity: s("engine_identity"), + engine_version: s("engine_version"), + input_digest: s("input_digest"), + output_digest: s("output_digest"), + applied_defaults: defaults, + derived_values: derived, + }; + receipt.check_structure()?; + Ok(receipt) + } + + /// Form, version and ordering checks that need no other artifact. + /// + /// # Errors + /// `E_INVALID_RECEIPT`. + pub fn check_structure(&self) -> Result<()> { + if self.receipt_schema_version != RECEIPT_SCHEMA_VERSION { + return Err(invalid( + "R-RECEIPT-VERSION", + &field_path("receipt_schema_version"), + format!( + "unsupported receipt_schema_version {} (supported: {RECEIPT_SCHEMA_VERSION})", + self.receipt_schema_version + ), + )); + } + check_digest(&self.grammar_sha256, "grammar_sha256")?; + check_digest(&self.grammar_schema_sha256, "grammar_schema_sha256")?; + check_nonempty(&self.primitive_release, "primitive_release")?; + check_nonempty(&self.primitive_source_commit, "primitive_source_commit")?; + check_digest(&self.primitive_build_hash, "primitive_build_hash")?; + check_nonempty(&self.schema_name, "schema_name")?; + check_nonempty(&self.schema_version, "schema_version")?; + check_digest(&self.schema_digest, "schema_digest")?; + check_nonempty(&self.engine_identity, "engine_identity")?; + check_nonempty(&self.engine_version, "engine_version")?; + check_digest(&self.input_digest, "input_digest")?; + check_digest(&self.output_digest, "output_digest")?; + + for (i, e) in self.applied_defaults.iter().enumerate() { + let at = format!("{}[{i}]", field_path("applied_defaults")); + check_path_form(&e.path, &format!("{at}.path"))?; + if e.rule != DEFAULT_RULE { + return Err(invalid( + "R-RECEIPT-EVIDENCE-FORM", + &format!("{at}.rule"), + format!( + "rule must be the literal {DEFAULT_RULE:?}, got {:?}", + e.rule + ), + )); + } + check_digest_at(&e.value_digest, &format!("{at}.value_digest"))?; + if i > 0 { + check_ascending( + &self.applied_defaults[i - 1].path, + &e.path, + &format!("{at}.path"), + )?; + } + } + for (i, e) in self.derived_values.iter().enumerate() { + let at = format!("{}[{i}]", field_path("derived_values")); + check_path_form(&e.path, &format!("{at}.path"))?; + check_derived_rule(&e.rule, &format!("{at}.rule"))?; + for (j, input) in e.inputs.iter().enumerate() { + let iat = format!("{at}.inputs[{j}]"); + check_path_form(input, &iat)?; + if j > 0 { + check_ascending(&e.inputs[j - 1], input, &iat)?; + } + } + check_digest_at(&e.value_digest, &format!("{at}.value_digest"))?; + if i > 0 { + check_ascending( + &self.derived_values[i - 1].path, + &e.path, + &format!("{at}.path"), + )?; + } + } + Ok(()) + } +} + +fn parse_default(v: &Value, at: &str) -> Result { + let m = take_map(v, at)?; + let out = DefaultEvidence { + path: take_str(m, "path", &format!("{at}.path"))?, + rule: take_str(m, "rule", &format!("{at}.rule"))?, + value_digest: take_str(m, "value_digest", &format!("{at}.value_digest"))?, + }; + check_keys(m, &["path", "rule", "value_digest"], at)?; + Ok(out) +} + +fn parse_derived(v: &Value, at: &str) -> Result { + let m = take_map(v, at)?; + let path = take_str(m, "path", &format!("{at}.path"))?; + let rule = take_str(m, "rule", &format!("{at}.rule"))?; + let inputs_at = format!("{at}.inputs"); + let inputs = match m.get("inputs") { + Some(Value::Seq(items)) => { + let mut out = Vec::with_capacity(items.len()); + for (j, item) in items.iter().enumerate() { + match item { + Value::Str(s) => out.push(s.clone()), + _ => return Err(shape(&format!("{inputs_at}[{j}]"), "a string")), + } + } + out + } + Some(_) => return Err(shape(&inputs_at, "a sequence")), + None => return Err(shape(&inputs_at, "this required field (it is absent)")), + }; + let value_digest = take_str(m, "value_digest", &format!("{at}.value_digest"))?; + check_keys(m, &["path", "rule", "inputs", "value_digest"], at)?; + Ok(DerivedEvidence { + path, + rule, + inputs, + value_digest, + }) +} + +fn is_digest(s: &str) -> bool { + s.strip_prefix("sha256:").is_some_and(|hex| { + hex.len() == 64 && hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) + }) +} + +fn check_digest_at(s: &str, path: &str) -> Result<()> { + if is_digest(s) { + Ok(()) + } else { + Err(invalid( + "R-RECEIPT-DIGEST-FORMAT", + path, + "a digest is `sha256:` followed by 64 lowercase hex characters".to_string(), + )) + } +} + +fn check_digest(s: &str, name: &str) -> Result<()> { + check_digest_at(s, &field_path(name)) +} + +fn check_nonempty(s: &str, name: &str) -> Result<()> { + if s.is_empty() { + Err(shape(&field_path(name), "a non-empty string")) + } else { + Ok(()) + } +} + +/// `$.` followed by one or more non-empty dot-separated segments. +fn check_path_form(p: &str, at: &str) -> Result<()> { + let ok = p + .strip_prefix("$.") + .is_some_and(|rest| rest.split('.').all(|seg| !seg.is_empty())); + if ok { + Ok(()) + } else { + Err(invalid( + "R-RECEIPT-EVIDENCE-FORM", + at, + format!("{p:?} is not a `$.`-rooted path with non-empty segments"), + )) + } +} + +/// `name` + `-v` + one or more ASCII digits, with a non-empty name. +fn check_derived_rule(rule: &str, at: &str) -> Result<()> { + let ok = rule.rsplit_once("-v").is_some_and(|(name, n)| { + !name.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()) + }); + if ok { + Ok(()) + } else { + Err(invalid( + "R-RECEIPT-EVIDENCE-FORM", + at, + format!("{rule:?} is not a versioned rule identifier (`name-vN`)"), + )) + } +} + +/// Strictly ascending in raw UTF-8 byte order: equal means a duplicate, +/// smaller means out of order. +fn check_ascending(prev: &str, cur: &str, at: &str) -> Result<()> { + if prev < cur { + return Ok(()); + } + let what = if prev == cur { + "duplicate" + } else { + "out of order" + }; + Err(invalid( + "R-RECEIPT-ORDER", + at, + format!("{cur:?} is {what} after {prev:?} (strictly ascending byte order required)"), + )) +} + +#[derive(PartialEq, Eq)] +enum Kind { + Default, + Derived, + Other, +} + +fn kind(f: &MaterializedField) -> Kind { + match &f.evidence { + FieldEvidence::Intent(IntentEvidence::SchemaDefault(_)) => Kind::Default, + FieldEvidence::Intent(IntentEvidence::Derived(_)) + | FieldEvidence::DerivedObservation(_) => Kind::Derived, + _ => Kind::Other, + } +} + +fn unbound(at: &str, path: &str) -> Error { + Error::new( + code::RECEIPT_UNBOUND, + "R-RECEIPT-PATH-BINDING", + Stage::Validate, + at, + format!("receipt path {path:?} has no explicit binding to a materialized object key"), + ) +} + +/// Resolves one evidence entry to its bound field and compares the value +/// digest. `want` is the kind the entry's list requires. +fn check_entry<'a>( + object: &MaterializedObject, + binding: &'a PathBinding, + path: &str, + value_digest: &str, + want: &Kind, + at: &str, +) -> Result<&'a str> { + let Some(key) = binding.get(path) else { + return Err(unbound(&format!("{at}.path"), path)); + }; + let Some(field) = object.get(key) else { + return Err(mismatch( + "R-RECEIPT-EVIDENCE-MEMBERSHIP", + &format!("{at}.path"), + format!("{path:?} is bound to {key:?}, which is not a key of the object"), + )); + }; + if &kind(field) != want { + return Err(mismatch( + "R-RECEIPT-EVIDENCE-MEMBERSHIP", + &format!("{at}.path"), + format!("object field {key:?} does not have the provenance this list requires"), + )); + } + let Some(value) = field.value() else { + return Err(mismatch( + "R-RECEIPT-EVIDENCE-VALUE", + &format!("{at}.value_digest"), + format!("object field {key:?} carries no value to digest"), + )); + }; + let got = digest(&to_canonical_json(value)?); + if got != value_digest { + return Err(mismatch( + "R-RECEIPT-EVIDENCE-VALUE", + &format!("{at}.value_digest"), + format!("value digest of {key:?} is {got}, the receipt says {value_digest}"), + )); + } + Ok(key) +} + +/// Verifies `receipt` against the object it describes and the authored input +/// it was made from. See the module documentation for exactly what a success +/// does and does not establish. +/// +/// `binding` maps receipt paths to `object` keys; it may be empty only when +/// the receipt lists no evidence. +/// +/// # Errors +/// * `E_INVALID_RECEIPT` -- shape, version, digest form, evidence form or order; +/// * `E_RECEIPT_MISMATCH` -- a digest or an evidence entry disagrees with the +/// object or the input; +/// * `E_RECEIPT_UNBOUND` -- an evidence entry has no explicit path binding. +pub fn verify_receipt( + receipt: &Receipt, + object: &MaterializedObject, + authored_input: &Value, + binding: &PathBinding, +) -> Result<()> { + receipt.check_structure()?; + + let input = digest(&to_canonical_json(authored_input)?); + if input != receipt.input_digest { + return Err(mismatch( + "R-RECEIPT-BINDS-INPUT", + &field_path("input_digest"), + format!("canonical digest of the authored input is {input}"), + )); + } + let output = materialized_object_digest(object)?; + if output != receipt.output_digest { + return Err(mismatch( + "R-RECEIPT-BINDS-OUTPUT", + &field_path("output_digest"), + format!("digest of the materialized object is {output}"), + )); + } + + let mut defaulted: BTreeSet<&str> = BTreeSet::new(); + for (i, e) in receipt.applied_defaults.iter().enumerate() { + let at = format!("{}[{i}]", field_path("applied_defaults")); + let key = check_entry( + object, + binding, + &e.path, + &e.value_digest, + &Kind::Default, + &at, + )?; + if !defaulted.insert(key) { + return Err(mismatch( + "R-RECEIPT-EVIDENCE-MEMBERSHIP", + &format!("{at}.path"), + format!("object field {key:?} is listed twice"), + )); + } + } + let mut derived: BTreeSet<&str> = BTreeSet::new(); + for (i, e) in receipt.derived_values.iter().enumerate() { + let at = format!("{}[{i}]", field_path("derived_values")); + let key = check_entry( + object, + binding, + &e.path, + &e.value_digest, + &Kind::Derived, + &at, + )?; + if !derived.insert(key) { + return Err(mismatch( + "R-RECEIPT-EVIDENCE-MEMBERSHIP", + &format!("{at}.path"), + format!("object field {key:?} is listed twice"), + )); + } + } + + for (key, field) in object.iter() { + let (listed, list) = match kind(field) { + Kind::Default => (defaulted.contains(key.as_str()), "applied_defaults"), + Kind::Derived => (derived.contains(key.as_str()), "derived_values"), + Kind::Other => continue, + }; + if !listed { + return Err(mismatch( + "R-RECEIPT-EVIDENCE-MEMBERSHIP", + &format!("object/{key}"), + format!("the object's field {key:?} has no entry in {list}"), + )); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::materialized::Capability; + use crate::reader::Map; + + const D: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + + fn field(evidence: FieldEvidence) -> MaterializedField { + MaterializedField { + capability: Capability::Implemented, + evidence, + } + } + + fn object(fields: Vec<(&str, MaterializedField)>) -> MaterializedObject { + let keys = fields.iter().map(|(k, _)| (*k).to_string()).collect(); + let map = fields + .into_iter() + .map(|(k, f)| (k.to_string(), f)) + .collect(); + MaterializedObject::try_new(map, &keys).expect("fixture keys match themselves") + } + + fn input() -> Value { + let mut m = Map::default(); + m.push("shape", Value::Str("E4.Flex".into())); + Value::Map(m) + } + + fn empty_receipt(obj: &MaterializedObject) -> Receipt { + Receipt { + receipt_schema_version: 1, + grammar_sha256: D.into(), + grammar_schema_sha256: D.into(), + primitive_release: "primitives/@v0.2.0".into(), + primitive_source_commit: "960ee0f".into(), + primitive_build_hash: D.into(), + schema_name: "cic:storage:StorageResource".into(), + schema_version: "v1.2.0".into(), + schema_digest: D.into(), + engine_identity: "environment-under-test".into(), + engine_version: "0.0.0".into(), + input_digest: digest(&to_canonical_json(&input()).unwrap()), + output_digest: materialized_object_digest(obj).unwrap(), + applied_defaults: vec![], + derived_values: vec![], + } + } + + fn authored_only() -> MaterializedObject { + object(vec![( + "shape", + field(FieldEvidence::Intent(IntentEvidence::Authored(Some( + Value::Str("E4.Flex".into()), + )))), + )]) + } + + #[test] + fn empty_evidence_over_an_object_without_defaults_verifies_without_a_binding() { + let obj = authored_only(); + let r = empty_receipt(&obj); + verify_receipt(&r, &obj, &input(), &PathBinding::new()).expect("must verify"); + } + + #[test] + fn a_defaulted_field_the_receipt_does_not_list_is_refused() { + let obj = object(vec![( + "replicas", + field(FieldEvidence::Intent(IntentEvidence::SchemaDefault( + Value::Int(3), + ))), + )]); + let r = empty_receipt(&obj); + let e = verify_receipt(&r, &obj, &input(), &PathBinding::new()).unwrap_err(); + assert_eq!(e.code, code::RECEIPT_MISMATCH); + assert_eq!(e.path, "object/replicas"); + } + + #[test] + fn evidence_without_a_binding_fails_closed() { + let obj = object(vec![( + "replicas", + field(FieldEvidence::Intent(IntentEvidence::SchemaDefault( + Value::Int(3), + ))), + )]); + let mut r = empty_receipt(&obj); + r.applied_defaults.push(DefaultEvidence { + path: "$.config.replicas".into(), + rule: DEFAULT_RULE.into(), + value_digest: digest(&to_canonical_json(&Value::Int(3)).unwrap()), + }); + let e = verify_receipt(&r, &obj, &input(), &PathBinding::new()).unwrap_err(); + assert_eq!(e.code, code::RECEIPT_UNBOUND); + + let mut b = PathBinding::new(); + b.insert("$.config.replicas".into(), "replicas".into()); + verify_receipt(&r, &obj, &input(), &b).expect("bound, consistent"); + } + + #[test] + fn derived_inputs_are_not_required_to_exist_in_the_object() { + let obj = object(vec![( + "effective_state", + field(FieldEvidence::Intent(IntentEvidence::Derived(Value::Str( + "RUNNING".into(), + )))), + )]); + let mut r = empty_receipt(&obj); + r.derived_values.push(DerivedEvidence { + path: "$.state.effective_state".into(), + rule: "effective-state-v1".into(), + inputs: vec!["$.state.admin_state".into(), "$.state.oper_state".into()], + value_digest: digest(&to_canonical_json(&Value::Str("RUNNING".into())).unwrap()), + }); + let mut b = PathBinding::new(); + b.insert("$.state.effective_state".into(), "effective_state".into()); + verify_receipt(&r, &obj, &input(), &b).expect("inputs are checked for form and order only"); + } + + #[test] + fn digest_form_is_exactly_sha256_and_64_lowercase_hex() { + assert!(is_digest(D)); + assert!(!is_digest(&D.to_uppercase())); + assert!(!is_digest(&D[7..])); + assert!(!is_digest("sha256:00")); + assert!(!is_digest(&format!("{D}0"))); + assert!(!is_digest(&format!("sha512:{}", &D[7..]))); + } + + #[test] + fn path_and_rule_forms() { + assert!(check_path_form("$.a.b", "x").is_ok()); + for bad in ["$", "$.", "a.b", "$.a..b", "$.a."] { + assert!(check_path_form(bad, "x").is_err(), "{bad}"); + } + assert!(check_derived_rule("effective-state-v1", "x").is_ok()); + for bad in ["effective-state", "-v1", "x-v", "x-v1a", "x-vv1"] { + assert!(check_derived_rule(bad, "x").is_err(), "{bad}"); + } + } +} diff --git a/engine/tests/differential.rs b/engine/tests/differential.rs index de8befa..b5d7519 100644 --- a/engine/tests/differential.rs +++ b/engine/tests/differential.rs @@ -11,10 +11,10 @@ use std::path::{Path, PathBuf}; use cic_primitive_engine::{ build_verdict, classify_field_value, conformance_plan_digest, evaluate, evaluate_verdict, materialized_object_digest, observation_digest, reader, to_canonical_json, - verdict_canonical_bytes, Capability, Collection, CollectionPlan, CollectionTopology, - CompareType, ConformancePlan, ConformanceVerdict, ConsumedField, Coverage, FieldEvidence, - FieldPlan, FieldVerdict, IntentEvidence, MaterializedField, MaterializedObject, - ObjectConformance, ObjectVerdict, Observation, Stage, + verdict_canonical_bytes, verify_receipt, Capability, Collection, CollectionPlan, + CollectionTopology, CompareType, ConformancePlan, ConformanceVerdict, ConsumedField, Coverage, + FieldEvidence, FieldPlan, FieldVerdict, IntentEvidence, MaterializedField, MaterializedObject, + ObjectConformance, ObjectVerdict, Observation, PathBinding, Receipt, Stage, }; use reader::Value; use std::collections::{BTreeMap, BTreeSet}; @@ -889,3 +889,143 @@ fn verdict_vectors_through_the_entry_point() { } assert!(checked > 0, "no artifact vector was checked"); } + +// --------------------------------------------------------------------------- +// conformance/differential/receipt/ -- the receipt verifier +// (`docs/RECEIPT-VERIFIER.md`). Each vector holds a receipt document, the +// authored input, a materialized object and, optionally, an explicit path +// binding. Both languages must accept or reject identically and, on +// rejection, name the same code, rule and location. +// --------------------------------------------------------------------------- + +/// `Ok(())` for an accepted vector, else the rejection's `(code, rule, path)`. +fn run_receipt_vector(input: &Value) -> std::result::Result<(), (String, String, String)> { + let object = build_materialized_object_from_fixture(map_get(input, "object").unwrap()); + let authored = map_get(input, "authored_input").unwrap(); + let mut binding = PathBinding::new(); + if let Some(Value::Map(m)) = map_get(input, "path_binding") { + for (k, v) in m.iter() { + let Value::Str(key) = v else { + panic!("path_binding values are strings") + }; + binding.insert(k.to_string(), key.clone()); + } + } + let triple = |e: cic_primitive_engine::Error| (e.code.to_string(), e.rule.to_string(), e.path); + let receipt = Receipt::from_value(map_get(input, "receipt").unwrap()).map_err(triple)?; + verify_receipt(&receipt, &object, authored, &binding).map_err(triple) +} + +#[test] +fn receipt_vectors() { + let vectors = load_vectors("receipt"); + assert!( + !vectors.is_empty(), + "differential/receipt has no vectors -- an empty corpus trivially passes" + ); + let mut failures = Vec::new(); + let (mut accepted, mut rejected) = (0, 0); + for v in &vectors { + let got = run_receipt_vector(&v.input); + if bool_field(&v.expected, "accepted") { + accepted += 1; + if let Err(e) = got { + failures.push(format!("{}: want accepted, got {e:?}", v.name)); + } + } else { + rejected += 1; + let want = map_get(&v.expected, "error").expect("a rejection vector names its error"); + let want = ( + str_field(want, "code").to_string(), + str_field(want, "rule").to_string(), + str_field(want, "path").to_string(), + ); + match got { + Ok(()) => { + failures.push(format!("{}: want rejection {want:?}, got accepted", v.name)) + } + Err(e) if e != want => { + failures.push(format!("{}: want {want:?}, got {e:?}", v.name)); + } + Err(_) => {} + } + } + } + assert!( + accepted >= 3 && rejected >= 20, + "corpus too thin: {accepted} accepted, {rejected} rejected" + ); + assert!( + failures.is_empty(), + "{} of {} receipt vectors failed:\n {}", + failures.len(), + vectors.len(), + failures.join("\n ") + ); +} + +/// The harness must be able to fail: every accepted vector, with one field +/// of its receipt altered, must turn into the rejection that alteration +/// deserves. +#[test] +fn receipt_vectors_detect_deliberate_divergence() { + fn flip_hex(d: &str) -> String { + let mut chars: Vec = d.chars().collect(); + let last = chars.len() - 1; + chars[last] = if chars[last] == '0' { '1' } else { '0' }; + chars.into_iter().collect() + } + type Mutation = ( + &'static str, + &'static str, + fn(&mut cic_primitive_engine::reader::Map), + ); + let mutations: [Mutation; 4] = [ + ("input digest altered", "E_RECEIPT_MISMATCH", |m| { + let Some(Value::Str(d)) = m.get("input_digest") else { + panic!() + }; + let d = flip_hex(d); + m.insert("input_digest", Value::Str(d)); + }), + ("output digest altered", "E_RECEIPT_MISMATCH", |m| { + let Some(Value::Str(d)) = m.get("output_digest") else { + panic!() + }; + let d = flip_hex(d); + m.insert("output_digest", Value::Str(d)); + }), + ("version altered", "E_INVALID_RECEIPT", |m| { + m.insert("receipt_schema_version", Value::Int(2)); + }), + ("field retyped", "E_INVALID_RECEIPT", |m| { + m.insert("engine_version", Value::Int(1)); + }), + ]; + + let mut checked = 0; + for v in load_vectors("receipt") { + if !bool_field(&v.expected, "accepted") { + continue; + } + assert!(run_receipt_vector(&v.input).is_ok(), "{}: baseline", v.name); + for (what, want_code, mutate) in &mutations { + let mut input = v.input.clone(); + let Value::Map(top) = &mut input else { + panic!() + }; + let Some(Value::Map(doc)) = top.get_mut("receipt") else { + panic!() + }; + let Some(Value::Map(body)) = doc.get_mut("materialization") else { + panic!() + }; + mutate(body); + match run_receipt_vector(&input) { + Err((code, _, _)) if code == *want_code => checked += 1, + other => panic!("{}: {what}: expected {want_code}, got {other:?}", v.name), + } + } + } + assert!(checked >= 12, "too few mutations were exercised: {checked}"); +} diff --git a/go/receipt/differential_test.go b/go/receipt/differential_test.go new file mode 100644 index 0000000..921ac0d --- /dev/null +++ b/go/receipt/differential_test.go @@ -0,0 +1,222 @@ +package receipt + +// Runs the receipt group of the language-independent differential corpus in +// conformance/differential/ -- the Go peer of engine/tests/differential.rs's +// receipt_vectors and receipt_vectors_detect_deliberate_divergence, reading +// the identical JSON files. See that directory's README for why the corpus +// is JSON-only. +// +// Documents are decoded with UseNumber so an integer stays an integer and +// 1.0 stays a non-integer, as the Rust reader reads them. + +import ( + "bytes" + "encoding/json" + "errors" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/CentralInfraCore/cic-primitive-engine/go/conformance" + "github.com/CentralInfraCore/cic-primitive-engine/go/materialized" +) + +func corpusRoot(group string) string { + // go test runs in this package's directory (go/receipt/). + return filepath.Join("..", "..", "conformance", "differential", group) +} + +func decodeFile(t *testing.T, path string) map[string]interface{} { + t.Helper() + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("%s: %v", path, err) + } + dec := json.NewDecoder(bytes.NewReader(b)) + dec.UseNumber() + var v map[string]interface{} + if err := dec.Decode(&v); err != nil { + t.Fatalf("%s: %v", path, err) + } + return v +} + +// objectFromFixture builds a MaterializedObject from a digest-group style +// {"fields": [{"key", "capability", "coverage"?, "provenance"?, "value"?}]} +// fixture -- the identical shape engine/tests/differential.rs's +// build_materialized_object_from_fixture reads. +func objectFromFixture(t *testing.T, name string, fixture interface{}) materialized.MaterializedObject { + t.Helper() + items := fixture.(map[string]interface{})["fields"].([]interface{}) + out := make(map[string]materialized.MaterializedField, len(items)) + keys := make(map[string]struct{}, len(items)) + for _, raw := range items { + f := raw.(map[string]interface{}) + key, _ := f["key"].(string) + capability, _ := f["capability"].(string) + coverageRaw, hasCoverage := f["coverage"] + provenanceRaw, hasProvenance := f["provenance"] + valueRaw, hasValue := f["value"] + + var evidence materialized.FieldEvidence + switch { + case hasCoverage && hasProvenance: + cov, _ := coverageRaw.(string) + evidence = materialized.NewDerivedObservationFieldEvidence(conformance.Coverage(cov), valueRaw) + case hasCoverage: + cov, _ := coverageRaw.(string) + evidence = materialized.NewObservationFieldEvidence(conformance.Coverage(cov), valueRaw) + case hasProvenance: + prov, _ := provenanceRaw.(string) + switch prov { + case "authored": + evidence = materialized.NewIntentFieldEvidence(materialized.NewAuthoredIntentEvidence(valueRaw, hasValue)) + case "schema_default": + evidence = materialized.NewIntentFieldEvidence(materialized.NewSchemaDefaultIntentEvidence(valueRaw)) + case "derived": + evidence = materialized.NewIntentFieldEvidence(materialized.NewDerivedIntentEvidence(valueRaw)) + default: + t.Fatalf("%s: field %q: unknown provenance %q", name, key, prov) + } + default: + t.Fatalf("%s: field %q: neither coverage nor provenance", name, key) + } + out[key] = materialized.MaterializedField{Capability: materialized.Capability(capability), Evidence: evidence} + keys[key] = struct{}{} + } + obj, err := materialized.NewMaterializedObject(out, keys) + if err != nil { + t.Fatalf("%s: fixture object: %v", name, err) + } + return obj +} + +type outcome struct{ code, rule, path string } + +// runVector returns nil for an accepted vector, else the rejection. +func runVector(t *testing.T, name string, input map[string]interface{}) *outcome { + t.Helper() + obj := objectFromFixture(t, name, input["object"]) + binding := PathBinding{} + if raw, ok := input["path_binding"].(map[string]interface{}); ok { + for k, v := range raw { + binding[k] = v.(string) + } + } + r, err := Parse(input["receipt"]) + if err == nil { + err = Verify(r, obj, input["authored_input"], binding) + } + if err == nil { + return nil + } + var e *Error + if !errors.As(err, &e) { + t.Fatalf("%s: rejection is not a *receipt.Error: %v", name, err) + } + return &outcome{e.Code, e.Rule, e.Path} +} + +func vectorNames(t *testing.T) []string { + t.Helper() + entries, err := os.ReadDir(corpusRoot("receipt")) + if err != nil { + t.Fatal(err) + } + var names []string + for _, e := range entries { + if e.IsDir() { + names = append(names, e.Name()) + } + } + sort.Strings(names) + return names +} + +func TestDifferentialReceiptVectors(t *testing.T) { + names := vectorNames(t) + if len(names) == 0 { + t.Fatal("differential/receipt has no vectors -- an empty corpus trivially passes") + } + accepted, rejected := 0, 0 + for _, name := range names { + dir := filepath.Join(corpusRoot("receipt"), name) + input := decodeFile(t, filepath.Join(dir, "input.json")) + expected := decodeFile(t, filepath.Join(dir, "expected.json")) + got := runVector(t, name, input) + if expected["accepted"].(bool) { + accepted++ + if got != nil { + t.Errorf("%s: want accepted, got %+v", name, *got) + } + continue + } + rejected++ + w := expected["error"].(map[string]interface{}) + want := outcome{w["code"].(string), w["rule"].(string), w["path"].(string)} + switch { + case got == nil: + t.Errorf("%s: want rejection %+v, got accepted", name, want) + case *got != want: + t.Errorf("%s: want %+v, got %+v", name, want, *got) + } + } + if accepted < 3 || rejected < 20 { + t.Fatalf("corpus too thin: %d accepted, %d rejected", accepted, rejected) + } +} + +// The harness must be able to fail: every accepted vector, with one field of +// its receipt altered, must turn into the rejection that alteration deserves. +func TestDifferentialReceiptVectorsDetectDeliberateDivergence(t *testing.T) { + flipHex := func(d string) string { + b := []byte(d) + if b[len(b)-1] == '0' { + b[len(b)-1] = '1' + } else { + b[len(b)-1] = '0' + } + return string(b) + } + mutations := []struct { + what, code string + mutate func(m map[string]interface{}) + }{ + {"input digest altered", CodeReceiptMismatch, func(m map[string]interface{}) { + m["input_digest"] = flipHex(m["input_digest"].(string)) + }}, + {"output digest altered", CodeReceiptMismatch, func(m map[string]interface{}) { + m["output_digest"] = flipHex(m["output_digest"].(string)) + }}, + {"version altered", CodeInvalidReceipt, func(m map[string]interface{}) { + m["receipt_schema_version"] = json.Number("2") + }}, + {"field retyped", CodeInvalidReceipt, func(m map[string]interface{}) { + m["engine_version"] = json.Number("1") + }}, + } + checked := 0 + for _, name := range vectorNames(t) { + dir := filepath.Join(corpusRoot("receipt"), name) + if !decodeFile(t, filepath.Join(dir, "expected.json"))["accepted"].(bool) { + continue + } + if got := runVector(t, name, decodeFile(t, filepath.Join(dir, "input.json"))); got != nil { + t.Fatalf("%s: baseline rejected: %+v", name, *got) + } + for _, m := range mutations { + input := decodeFile(t, filepath.Join(dir, "input.json")) + body := input["receipt"].(map[string]interface{})["materialization"].(map[string]interface{}) + m.mutate(body) + got := runVector(t, name, input) + if got == nil || got.code != m.code { + t.Fatalf("%s: %s: expected %s, got %v", name, m.what, m.code, got) + } + checked++ + } + } + if checked < 12 { + t.Fatalf("too few mutations were exercised: %d", checked) + } +} diff --git a/go/receipt/receipt.go b/go/receipt/receipt.go new file mode 100644 index 0000000..14ec52e --- /dev/null +++ b/go/receipt/receipt.go @@ -0,0 +1,715 @@ +// Package receipt verifies a materialization receipt (docs/RECEIPT-SCHEMA.md) +// -- the Go peer of this repo's own engine/src/receipt.rs. +// +// This package checks a receipt that somebody else produced. It never +// produces one: materialization, and the evidence a default or a derivation +// leaves while it runs, belong to the environment (docs/BOUNDARY.md). +// +// # What a successful result establishes +// +// Structural and content consistency, and nothing more. A receipt that passes +// Verify: +// +// - has all 15 required top-level fields, with the right types, a supported +// receipt_schema_version (1) and every digest spelled "sha256:" + 64 +// lowercase hex characters; +// - lists applied_defaults and derived_values in strictly ascending .path +// order, and each inputs list as a strictly ascending (deduplicated) path +// set; +// - carries an input_digest equal to the A7 digest of the canonical form of +// the authored input it was handed, and an output_digest equal to +// digestprojection.MaterializedObjectDigest of the object it was handed; +// - lists exactly the defaulted and the derived fields of that object, each +// with the digest of the field's value, through the explicit PathBinding +// the caller supplied. +// +// It does NOT establish that a default was applied or a derivation ran, that +// rule or inputs describe what actually happened, who produced the receipt, +// or that any identifier is genuine: +// +// - grammar_*, primitive_*, schema_* and engine_* are claims. Their form is +// checked; the release signature and the origin of the pinned schema are +// not (external authentication is not this package's job). +// - engine_identity names the implementation that materialized the data, +// not this verifier. +// - No signature is checked. The receipt schema has no signature field. +// +// # input_digest +// +// The caller passes the processed authored input as a canonical value +// (canonical.IsValue). The digest is taken over that value's canonical JSON +// (section A), not over raw source bytes, and no YAML reader is assumed. +// +// # Evidence needs an explicit path binding +// +// A MaterializedObject is keyed by flat field names. A receipt names fields by +// path ("$.config.replicas"). This package does not guess a mapping from one +// to the other. When the receipt lists any evidence entry, the caller must +// pass a PathBinding from receipt path to object key; an entry whose path is +// not in the binding is refused with CodeReceiptUnbound (fail-closed), never +// accepted partially. With both evidence lists empty and no defaulted or +// derived field in the object, no binding is needed. +// +// derived_values[].inputs is checked for form and order only. The finished +// object cannot show which paths a derivation actually read, so no input path +// is required to exist in the object. +// +// # Parsing a document +// +// Parse takes a decoded document. Integers must arrive as json.Number (decode +// with Decoder.UseNumber), int or int64; a float64 -- even 1.0 -- is not an +// integer, matching the Rust reader, which reads 1.0 as a float. +// +// # Check order +// +// Shape and structure first (declared field order), then input_digest, then +// output_digest, then evidence entries in list order, then the object's own +// defaulted/derived fields in key order. The Rust peer runs the same order, so +// both name the same offending location. +package receipt + +import ( + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/CentralInfraCore/cic-primitive-engine/go/canonical" + "github.com/CentralInfraCore/cic-primitive-engine/go/digestprojection" + "github.com/CentralInfraCore/cic-primitive-engine/go/materialized" +) + +// SchemaVersion is the only receipt_schema_version this verifier supports. +const SchemaVersion int64 = 1 + +// DefaultRule is DefaultEvidence.rule: a literal v1 constant (C5). +const DefaultRule = "schema-default" + +// Rejection codes -- engine/src/error.rs calls these INVALID_RECEIPT, +// RECEIPT_MISMATCH and RECEIPT_UNBOUND. +const ( + CodeInvalidReceipt = "E_INVALID_RECEIPT" + CodeReceiptMismatch = "E_RECEIPT_MISMATCH" + CodeReceiptUnbound = "E_RECEIPT_UNBOUND" +) + +const wrapper = "materialization" + +// fields are the 15 required top-level fields, in declared order. +var fields = []string{ + "receipt_schema_version", + "grammar_sha256", + "grammar_schema_sha256", + "primitive_release", + "primitive_source_commit", + "primitive_build_hash", + "schema_name", + "schema_version", + "schema_digest", + "engine_identity", + "engine_version", + "input_digest", + "output_digest", + "applied_defaults", + "derived_values", +} + +// PathBinding maps a receipt path to a MaterializedObject key, supplied by +// the environment. +type PathBinding map[string]string + +// DefaultEvidence is an applied_defaults[] entry. +type DefaultEvidence struct { + Path string + Rule string + ValueDigest string +} + +// DerivedEvidence is a derived_values[] entry. +type DerivedEvidence struct { + Path string + Rule string + Inputs []string + ValueDigest string +} + +// Receipt is the materialization receipt: 15 required fields, none optional. +type Receipt struct { + ReceiptSchemaVersion int64 + GrammarSHA256 string + GrammarSchemaSHA256 string + PrimitiveRelease string + PrimitiveSourceCommit string + PrimitiveBuildHash string + SchemaName string + SchemaVersion string + SchemaDigest string + EngineIdentity string + EngineVersion string + InputDigest string + OutputDigest string + AppliedDefaults []DefaultEvidence + DerivedValues []DerivedEvidence +} + +// Error is a rejection with the same code/rule/path fields the Rust peer's +// Error carries. +type Error struct { + Code string + Rule string + Path string + Detail string +} + +func (e *Error) Error() string { + return fmt.Sprintf("%s (%s) at %s: %s", e.Code, e.Rule, e.Path, e.Detail) +} + +func invalid(rule, path, detail string) *Error { + return &Error{Code: CodeInvalidReceipt, Rule: rule, Path: path, Detail: detail} +} + +func mismatch(rule, path, detail string) *Error { + return &Error{Code: CodeReceiptMismatch, Rule: rule, Path: path, Detail: detail} +} + +func fieldPath(name string) string { return "$." + wrapper + "." + name } + +func shape(path, want string) *Error { + return invalid("R-RECEIPT-SHAPE", path, "expected "+want) +} + +const absent = "this required field (it is absent)" + +const emptyNotNull = "a sequence, `[]` when empty, never null" + +func takeStr(m map[string]interface{}, key, path string) (string, *Error) { + v, ok := m[key] + if !ok { + return "", shape(path, absent) + } + s, ok := v.(string) + if !ok { + return "", shape(path, "a string") + } + return s, nil +} + +func takeMap(v interface{}, path string) (map[string]interface{}, *Error) { + m, ok := v.(map[string]interface{}) + if !ok { + return nil, shape(path, "a mapping") + } + return m, nil +} + +func checkKeys(m map[string]interface{}, allowed []string, path string) *Error { + var extra []string + for k := range m { + known := false + for _, a := range allowed { + if a == k { + known = true + break + } + } + if !known { + extra = append(extra, k) + } + } + if len(extra) == 0 { + return nil + } + sort.Strings(extra) + return shape(path+"."+extra[0], "no member outside the receipt layout") +} + +// integer reads an integer from a decoded value; a float64 is not one. +func integer(v interface{}) (int64, bool) { + switch n := v.(type) { + case json.Number: + i, err := strconv.ParseInt(n.String(), 10, 64) + return i, err == nil + case int: + return int64(n), true + case int64: + return n, true + default: + return 0, false + } +} + +// Parse reads a receipt document: a mapping whose only key is +// "materialization", holding the 15 fields. Shape errors (missing or unknown +// member, wrong type) are raised here, in declared field order; form and +// order errors are raised by CheckStructure, which Parse calls last. +func Parse(doc interface{}) (Receipt, error) { + var r Receipt + top, err := takeMap(doc, "$") + if err != nil { + return r, err + } + bodyRaw, ok := top[wrapper] + if !ok { + return r, shape("$", "the sole wrapper key `materialization`") + } + if err := checkKeys(top, []string{wrapper}, "$"); err != nil { + return r, err + } + base := "$." + wrapper + body, err := takeMap(bodyRaw, base) + if err != nil { + return r, err + } + + // Required arrays are never null: [] must come out as a non-nil empty slice. + r.AppliedDefaults = []DefaultEvidence{} + r.DerivedValues = []DerivedEvidence{} + strs := map[string]string{} + for _, name := range fields { + path := fieldPath(name) + switch name { + case "receipt_schema_version": + raw, ok := body[name] + if !ok { + return r, shape(path, absent) + } + n, ok := integer(raw) + if !ok { + return r, shape(path, "an integer") + } + r.ReceiptSchemaVersion = n + case "applied_defaults": + raw, ok := body[name] + if !ok { + return r, shape(path, absent) + } + items, ok := raw.([]interface{}) + if !ok { + return r, shape(path, "a sequence") + } + for i, item := range items { + e, err := parseDefault(item, fmt.Sprintf("%s[%d]", path, i)) + if err != nil { + return r, err + } + r.AppliedDefaults = append(r.AppliedDefaults, e) + } + case "derived_values": + raw, ok := body[name] + if !ok { + return r, shape(path, absent) + } + items, ok := raw.([]interface{}) + if !ok { + return r, shape(path, "a sequence") + } + for i, item := range items { + e, err := parseDerived(item, fmt.Sprintf("%s[%d]", path, i)) + if err != nil { + return r, err + } + r.DerivedValues = append(r.DerivedValues, e) + } + default: + s, err := takeStr(body, name, path) + if err != nil { + return r, err + } + strs[name] = s + } + } + if err := checkKeys(body, fields, base); err != nil { + return r, err + } + + r.GrammarSHA256 = strs["grammar_sha256"] + r.GrammarSchemaSHA256 = strs["grammar_schema_sha256"] + r.PrimitiveRelease = strs["primitive_release"] + r.PrimitiveSourceCommit = strs["primitive_source_commit"] + r.PrimitiveBuildHash = strs["primitive_build_hash"] + r.SchemaName = strs["schema_name"] + r.SchemaVersion = strs["schema_version"] + r.SchemaDigest = strs["schema_digest"] + r.EngineIdentity = strs["engine_identity"] + r.EngineVersion = strs["engine_version"] + r.InputDigest = strs["input_digest"] + r.OutputDigest = strs["output_digest"] + if err := r.CheckStructure(); err != nil { + return r, err + } + return r, nil +} + +func parseDefault(v interface{}, at string) (DefaultEvidence, *Error) { + var e DefaultEvidence + m, err := takeMap(v, at) + if err != nil { + return e, err + } + if e.Path, err = takeStr(m, "path", at+".path"); err != nil { + return e, err + } + if e.Rule, err = takeStr(m, "rule", at+".rule"); err != nil { + return e, err + } + if e.ValueDigest, err = takeStr(m, "value_digest", at+".value_digest"); err != nil { + return e, err + } + if err := checkKeys(m, []string{"path", "rule", "value_digest"}, at); err != nil { + return e, err + } + return e, nil +} + +func parseDerived(v interface{}, at string) (DerivedEvidence, *Error) { + var e DerivedEvidence + m, err := takeMap(v, at) + if err != nil { + return e, err + } + if e.Path, err = takeStr(m, "path", at+".path"); err != nil { + return e, err + } + if e.Rule, err = takeStr(m, "rule", at+".rule"); err != nil { + return e, err + } + inputsAt := at + ".inputs" + raw, ok := m["inputs"] + if !ok { + return e, shape(inputsAt, absent) + } + items, ok := raw.([]interface{}) + if !ok { + return e, shape(inputsAt, "a sequence") + } + e.Inputs = make([]string, 0, len(items)) + for j, item := range items { + s, ok := item.(string) + if !ok { + return e, shape(fmt.Sprintf("%s[%d]", inputsAt, j), "a string") + } + e.Inputs = append(e.Inputs, s) + } + if e.ValueDigest, err = takeStr(m, "value_digest", at+".value_digest"); err != nil { + return e, err + } + if err := checkKeys(m, []string{"path", "rule", "inputs", "value_digest"}, at); err != nil { + return e, err + } + return e, nil +} + +// CheckStructure runs the form, version and ordering checks that need no +// other artifact. +func (r Receipt) CheckStructure() error { + if r.ReceiptSchemaVersion != SchemaVersion { + return invalid("R-RECEIPT-VERSION", fieldPath("receipt_schema_version"), + fmt.Sprintf("unsupported receipt_schema_version %d (supported: %d)", r.ReceiptSchemaVersion, SchemaVersion)) + } + steps := []func() *Error{ + func() *Error { return checkDigest(r.GrammarSHA256, "grammar_sha256") }, + func() *Error { return checkDigest(r.GrammarSchemaSHA256, "grammar_schema_sha256") }, + func() *Error { return checkNonEmpty(r.PrimitiveRelease, "primitive_release") }, + func() *Error { return checkNonEmpty(r.PrimitiveSourceCommit, "primitive_source_commit") }, + func() *Error { return checkDigest(r.PrimitiveBuildHash, "primitive_build_hash") }, + func() *Error { return checkNonEmpty(r.SchemaName, "schema_name") }, + func() *Error { return checkNonEmpty(r.SchemaVersion, "schema_version") }, + func() *Error { return checkDigest(r.SchemaDigest, "schema_digest") }, + func() *Error { return checkNonEmpty(r.EngineIdentity, "engine_identity") }, + func() *Error { return checkNonEmpty(r.EngineVersion, "engine_version") }, + func() *Error { return checkDigest(r.InputDigest, "input_digest") }, + func() *Error { return checkDigest(r.OutputDigest, "output_digest") }, + } + for _, step := range steps { + if err := step(); err != nil { + return err + } + } + + // The evidence lists are required arrays: `[]` when empty, never null. A + // nil slice is Go's spelling of null/absent, so the typed API refuses it. + if r.AppliedDefaults == nil { + return shape(fieldPath("applied_defaults"), emptyNotNull) + } + for i, e := range r.AppliedDefaults { + at := fmt.Sprintf("%s[%d]", fieldPath("applied_defaults"), i) + if err := checkPathForm(e.Path, at+".path"); err != nil { + return err + } + if e.Rule != DefaultRule { + return invalid("R-RECEIPT-EVIDENCE-FORM", at+".rule", + fmt.Sprintf("rule must be the literal %q, got %q", DefaultRule, e.Rule)) + } + if err := checkDigestAt(e.ValueDigest, at+".value_digest"); err != nil { + return err + } + if i > 0 { + if err := checkAscending(r.AppliedDefaults[i-1].Path, e.Path, at+".path"); err != nil { + return err + } + } + } + if r.DerivedValues == nil { + return shape(fieldPath("derived_values"), emptyNotNull) + } + for i, e := range r.DerivedValues { + at := fmt.Sprintf("%s[%d]", fieldPath("derived_values"), i) + if err := checkPathForm(e.Path, at+".path"); err != nil { + return err + } + if err := checkDerivedRule(e.Rule, at+".rule"); err != nil { + return err + } + if e.Inputs == nil { + return shape(at+".inputs", emptyNotNull) + } + for j, input := range e.Inputs { + iat := fmt.Sprintf("%s.inputs[%d]", at, j) + if err := checkPathForm(input, iat); err != nil { + return err + } + if j > 0 { + if err := checkAscending(e.Inputs[j-1], input, iat); err != nil { + return err + } + } + } + if err := checkDigestAt(e.ValueDigest, at+".value_digest"); err != nil { + return err + } + if i > 0 { + if err := checkAscending(r.DerivedValues[i-1].Path, e.Path, at+".path"); err != nil { + return err + } + } + } + return nil +} + +func isDigest(s string) bool { + hex, ok := strings.CutPrefix(s, "sha256:") + if !ok || len(hex) != 64 { + return false + } + for i := 0; i < len(hex); i++ { + c := hex[i] + if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') { + return false + } + } + return true +} + +func checkDigestAt(s, path string) *Error { + if isDigest(s) { + return nil + } + return invalid("R-RECEIPT-DIGEST-FORMAT", path, + "a digest is `sha256:` followed by 64 lowercase hex characters") +} + +func checkDigest(s, name string) *Error { return checkDigestAt(s, fieldPath(name)) } + +func checkNonEmpty(s, name string) *Error { + if s == "" { + return shape(fieldPath(name), "a non-empty string") + } + return nil +} + +// checkPathForm accepts "$." followed by one or more non-empty dot-separated +// segments. +func checkPathForm(p, at string) *Error { + rest, ok := strings.CutPrefix(p, "$.") + if ok { + for _, seg := range strings.Split(rest, ".") { + if seg == "" { + ok = false + break + } + } + } + if ok { + return nil + } + return invalid("R-RECEIPT-EVIDENCE-FORM", at, + fmt.Sprintf("%q is not a `$.`-rooted path with non-empty segments", p)) +} + +// checkDerivedRule accepts name + "-v" + one or more ASCII digits, with a +// non-empty name. +func checkDerivedRule(rule, at string) *Error { + idx := strings.LastIndex(rule, "-v") + ok := idx > 0 + if ok { + n := rule[idx+2:] + ok = n != "" + for i := 0; ok && i < len(n); i++ { + ok = n[i] >= '0' && n[i] <= '9' + } + } + if ok { + return nil + } + return invalid("R-RECEIPT-EVIDENCE-FORM", at, + fmt.Sprintf("%q is not a versioned rule identifier (`name-vN`)", rule)) +} + +// checkAscending requires strictly ascending raw UTF-8 byte order: equal +// means a duplicate, smaller means out of order. +func checkAscending(prev, cur, at string) *Error { + if prev < cur { + return nil + } + what := "out of order" + if prev == cur { + what = "duplicate" + } + return invalid("R-RECEIPT-ORDER", at, + fmt.Sprintf("%q is %s after %q (strictly ascending byte order required)", cur, what, prev)) +} + +type kind int + +const ( + kindOther kind = iota + kindDefault + kindDerived +) + +func fieldKind(f materialized.MaterializedField) kind { + if f.Evidence.IsDerived() { + return kindDerived + } + if p, ok := f.Evidence.Provenance(); ok { + switch p { + case materialized.ProvenanceSchemaDefault: + return kindDefault + case materialized.ProvenanceDerived: + return kindDerived + } + } + return kindOther +} + +func unbound(at, path string) *Error { + return &Error{ + Code: CodeReceiptUnbound, + Rule: "R-RECEIPT-PATH-BINDING", + Path: at, + Detail: fmt.Sprintf("receipt path %q has no explicit binding to a materialized object key", path), + } +} + +// checkEntry resolves one evidence entry to its bound field and compares the +// value digest. want is the kind the entry's list requires. +func checkEntry(obj materialized.MaterializedObject, binding PathBinding, path, valueDigest string, want kind, at string) (string, error) { + key, ok := binding[path] + if !ok { + return "", unbound(at+".path", path) + } + field, ok := obj.Get(key) + if !ok { + return "", mismatch("R-RECEIPT-EVIDENCE-MEMBERSHIP", at+".path", + fmt.Sprintf("%q is bound to %q, which is not a key of the object", path, key)) + } + if fieldKind(field) != want { + return "", mismatch("R-RECEIPT-EVIDENCE-MEMBERSHIP", at+".path", + fmt.Sprintf("object field %q does not have the provenance this list requires", key)) + } + value, has := field.Value() + if !has { + return "", mismatch("R-RECEIPT-EVIDENCE-VALUE", at+".value_digest", + fmt.Sprintf("object field %q carries no value to digest", key)) + } + b, err := canonical.ToCanonicalJSON(value) + if err != nil { + return "", err + } + if got := canonical.Digest(b); got != valueDigest { + return "", mismatch("R-RECEIPT-EVIDENCE-VALUE", at+".value_digest", + fmt.Sprintf("value digest of %q is %s, the receipt says %s", key, got, valueDigest)) + } + return key, nil +} + +// Verify checks r against the object it describes and the authored input it +// was made from. See the package documentation for exactly what a success +// does and does not establish. +// +// binding maps receipt paths to object keys; it may be empty only when the +// receipt lists no evidence. The returned error is an *Error, except for a +// canonicalization failure of authoredInput or of the object, which is +// returned as that error. +func Verify(r Receipt, obj materialized.MaterializedObject, authoredInput interface{}, binding PathBinding) error { + if err := r.CheckStructure(); err != nil { + return err + } + + inputBytes, err := canonical.ToCanonicalJSON(authoredInput) + if err != nil { + return err + } + if got := canonical.Digest(inputBytes); got != r.InputDigest { + return mismatch("R-RECEIPT-BINDS-INPUT", fieldPath("input_digest"), + "canonical digest of the authored input is "+got) + } + got, err := digestprojection.MaterializedObjectDigest(obj) + if err != nil { + return err + } + if got != r.OutputDigest { + return mismatch("R-RECEIPT-BINDS-OUTPUT", fieldPath("output_digest"), + "digest of the materialized object is "+got) + } + + defaulted := map[string]struct{}{} + for i, e := range r.AppliedDefaults { + at := fmt.Sprintf("%s[%d]", fieldPath("applied_defaults"), i) + key, cerr := checkEntry(obj, binding, e.Path, e.ValueDigest, kindDefault, at) + if cerr != nil { + return cerr + } + if _, dup := defaulted[key]; dup { + return mismatch("R-RECEIPT-EVIDENCE-MEMBERSHIP", at+".path", + fmt.Sprintf("object field %q is listed twice", key)) + } + defaulted[key] = struct{}{} + } + derived := map[string]struct{}{} + for i, e := range r.DerivedValues { + at := fmt.Sprintf("%s[%d]", fieldPath("derived_values"), i) + key, cerr := checkEntry(obj, binding, e.Path, e.ValueDigest, kindDerived, at) + if cerr != nil { + return cerr + } + if _, dup := derived[key]; dup { + return mismatch("R-RECEIPT-EVIDENCE-MEMBERSHIP", at+".path", + fmt.Sprintf("object field %q is listed twice", key)) + } + derived[key] = struct{}{} + } + + for _, key := range obj.Keys() { + field, _ := obj.Get(key) + var listed bool + var list string + switch fieldKind(field) { + case kindDefault: + _, listed = defaulted[key] + list = "applied_defaults" + case kindDerived: + _, listed = derived[key] + list = "derived_values" + default: + continue + } + if !listed { + return mismatch("R-RECEIPT-EVIDENCE-MEMBERSHIP", "object/"+key, + fmt.Sprintf("the object's field %q has no entry in %s", key, list)) + } + } + return nil +} diff --git a/go/receipt/receipt_test.go b/go/receipt/receipt_test.go new file mode 100644 index 0000000..9dfe79a --- /dev/null +++ b/go/receipt/receipt_test.go @@ -0,0 +1,236 @@ +package receipt + +import ( + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/CentralInfraCore/cic-primitive-engine/go/canonical" + "github.com/CentralInfraCore/cic-primitive-engine/go/conformance" + "github.com/CentralInfraCore/cic-primitive-engine/go/digestprojection" + "github.com/CentralInfraCore/cic-primitive-engine/go/materialized" +) + +var zero = "sha256:" + strings.Repeat("0", 64) + +func mustObject(t *testing.T, fields map[string]materialized.FieldEvidence) materialized.MaterializedObject { + t.Helper() + m := map[string]materialized.MaterializedField{} + keys := map[string]struct{}{} + for k, ev := range fields { + m[k] = materialized.MaterializedField{Capability: materialized.CapabilityImplemented, Evidence: ev} + keys[k] = struct{}{} + } + obj, err := materialized.NewMaterializedObject(m, keys) + if err != nil { + t.Fatal(err) + } + return obj +} + +func digestOf(t *testing.T, v interface{}) string { + t.Helper() + b, err := canonical.ToCanonicalJSON(v) + if err != nil { + t.Fatal(err) + } + return canonical.Digest(b) +} + +func baseReceipt(t *testing.T, input interface{}, obj materialized.MaterializedObject) Receipt { + t.Helper() + out, err := digestprojection.MaterializedObjectDigest(obj) + if err != nil { + t.Fatal(err) + } + return Receipt{ + ReceiptSchemaVersion: 1, GrammarSHA256: zero, GrammarSchemaSHA256: zero, + PrimitiveRelease: "primitives/@v0.2.0", PrimitiveSourceCommit: "960ee0f", PrimitiveBuildHash: zero, + SchemaName: "cic:storage:StorageResource", SchemaVersion: "v1.2.0", SchemaDigest: zero, + EngineIdentity: "environment-under-test", EngineVersion: "0.0.0", + InputDigest: digestOf(t, input), OutputDigest: out, + AppliedDefaults: []DefaultEvidence{}, DerivedValues: []DerivedEvidence{}, + } +} + +func code(err error) string { + var e *Error + if errors.As(err, &e) { + return e.Code + } + return "" +} + +func authoredShape() materialized.FieldEvidence { + return materialized.NewIntentFieldEvidence(materialized.NewAuthoredIntentEvidence("E4.Flex", true)) +} + +func TestEmptyEvidenceOverAnObjectWithoutDefaultsVerifiesWithoutABinding(t *testing.T) { + input := map[string]interface{}{"shape": "E4.Flex"} + obj := mustObject(t, map[string]materialized.FieldEvidence{"shape": authoredShape()}) + if err := Verify(baseReceipt(t, input, obj), obj, input, nil); err != nil { + t.Fatal(err) + } +} + +func TestADefaultedFieldTheReceiptDoesNotListIsRefused(t *testing.T) { + input := map[string]interface{}{"shape": "E4.Flex"} + obj := mustObject(t, map[string]materialized.FieldEvidence{ + "replicas": materialized.NewIntentFieldEvidence(materialized.NewSchemaDefaultIntentEvidence(int64(3))), + }) + err := Verify(baseReceipt(t, input, obj), obj, input, nil) + var e *Error + if !errors.As(err, &e) || e.Code != CodeReceiptMismatch || e.Path != "object/replicas" { + t.Fatalf("got %v", err) + } +} + +func TestEvidenceWithoutABindingFailsClosed(t *testing.T) { + input := map[string]interface{}{"shape": "E4.Flex"} + obj := mustObject(t, map[string]materialized.FieldEvidence{ + "replicas": materialized.NewIntentFieldEvidence(materialized.NewSchemaDefaultIntentEvidence(int64(3))), + }) + r := baseReceipt(t, input, obj) + r.AppliedDefaults = []DefaultEvidence{{Path: "$.config.replicas", Rule: DefaultRule, ValueDigest: digestOf(t, int64(3))}} + if got := code(Verify(r, obj, input, nil)); got != CodeReceiptUnbound { + t.Fatalf("no binding: got %q", got) + } + if err := Verify(r, obj, input, PathBinding{"$.config.replicas": "replicas"}); err != nil { + t.Fatal(err) + } +} + +func TestDerivedInputsAreNotRequiredToExistInTheObject(t *testing.T) { + input := map[string]interface{}{"admin_state": "up"} + obj := mustObject(t, map[string]materialized.FieldEvidence{ + "effective_state": materialized.NewIntentFieldEvidence(materialized.NewDerivedIntentEvidence("RUNNING")), + }) + r := baseReceipt(t, input, obj) + r.DerivedValues = []DerivedEvidence{{ + Path: "$.state.effective_state", Rule: "effective-state-v1", + Inputs: []string{"$.state.admin_state", "$.state.oper_state"}, + ValueDigest: digestOf(t, "RUNNING"), + }} + if err := Verify(r, obj, input, PathBinding{"$.state.effective_state": "effective_state"}); err != nil { + t.Fatal(err) + } +} + +func TestADerivedObservationCountsAsDerived(t *testing.T) { + input := map[string]interface{}{} + obj := mustObject(t, map[string]materialized.FieldEvidence{ + "c": materialized.NewDerivedObservationFieldEvidence(conformance.CoverageObserved, "RUNNING"), + }) + r := baseReceipt(t, input, obj) + if got := code(Verify(r, obj, input, nil)); got != CodeReceiptMismatch { + t.Fatalf("unlisted derived observation: got %q", got) + } +} + +func TestDigestFormIsExactlySha256And64LowercaseHex(t *testing.T) { + for _, ok := range []string{zero} { + if !isDigest(ok) { + t.Errorf("%q should be a digest", ok) + } + } + for _, bad := range []string{ + strings.ToUpper(zero), zero[7:], "sha256:00", zero + "0", "sha512:" + zero[7:], "", + } { + if isDigest(bad) { + t.Errorf("%q should not be a digest", bad) + } + } +} + +func TestPathAndRuleForms(t *testing.T) { + if checkPathForm("$.a.b", "x") != nil { + t.Error("$.a.b should be a path") + } + for _, bad := range []string{"$", "$.", "a.b", "$.a..b", "$.a."} { + if checkPathForm(bad, "x") == nil { + t.Errorf("%q should not be a path", bad) + } + } + if checkDerivedRule("effective-state-v1", "x") != nil { + t.Error("effective-state-v1 should be a rule") + } + for _, bad := range []string{"effective-state", "-v1", "x-v", "x-v1a", "x-vv1"} { + if checkDerivedRule(bad, "x") == nil { + t.Errorf("%q should not be a rule", bad) + } + } +} + +// The evidence lists are required arrays, never null: a Go nil slice is the +// absent/null spelling, [] is the empty one (RECEIPT-SCHEMA.md). +func TestNilEvidenceListsAreRefusedOnADirectlyBuiltReceipt(t *testing.T) { + input := map[string]interface{}{"shape": "E4.Flex"} + obj := mustObject(t, map[string]materialized.FieldEvidence{"shape": authoredShape()}) + + for _, c := range []struct { + name, path string + mutate func(r *Receipt) + }{ + {"nil applied_defaults", "$.materialization.applied_defaults", func(r *Receipt) { r.AppliedDefaults = nil }}, + {"nil derived_values", "$.materialization.derived_values", func(r *Receipt) { r.DerivedValues = nil }}, + {"nil inputs", "$.materialization.derived_values[0].inputs", func(r *Receipt) { + r.DerivedValues = []DerivedEvidence{{Path: "$.a", Rule: "x-v1", Inputs: nil, ValueDigest: zero}} + }}, + } { + r := baseReceipt(t, input, obj) + c.mutate(&r) + var e *Error + for _, err := range []error{r.CheckStructure(), Verify(r, obj, input, nil)} { + if !errors.As(err, &e) || e.Code != CodeInvalidReceipt || e.Rule != "R-RECEIPT-SHAPE" || e.Path != c.path { + t.Errorf("%s: got %v", c.name, err) + } + } + } +} + +func TestEmptyJSONListsParseToNonNilSlicesAndVerify(t *testing.T) { + input := map[string]interface{}{"shape": "E4.Flex"} + obj := mustObject(t, map[string]materialized.FieldEvidence{"shape": authoredShape()}) + built := baseReceipt(t, input, obj) + + doc := map[string]interface{}{"materialization": map[string]interface{}{ + "receipt_schema_version": json.Number("1"), + "grammar_sha256": built.GrammarSHA256, + "grammar_schema_sha256": built.GrammarSchemaSHA256, + "primitive_release": built.PrimitiveRelease, + "primitive_source_commit": built.PrimitiveSourceCommit, + "primitive_build_hash": built.PrimitiveBuildHash, + "schema_name": built.SchemaName, + "schema_version": built.SchemaVersion, + "schema_digest": built.SchemaDigest, + "engine_identity": built.EngineIdentity, + "engine_version": built.EngineVersion, + "input_digest": built.InputDigest, + "output_digest": built.OutputDigest, + "applied_defaults": []interface{}{}, + "derived_values": []interface{}{}, + }} + r, err := Parse(doc) + if err != nil { + t.Fatal(err) + } + if r.AppliedDefaults == nil || r.DerivedValues == nil { + t.Fatalf("[] must parse to non-nil slices: %#v %#v", r.AppliedDefaults, r.DerivedValues) + } + if err := Verify(r, obj, input, nil); err != nil { + t.Fatal(err) + } + + // An empty inputs list inside a derived entry is non-nil too. + doc["materialization"].(map[string]interface{})["derived_values"] = []interface{}{ + map[string]interface{}{"path": "$.a", "rule": "x-v1", "inputs": []interface{}{}, "value_digest": zero}, + } + r, err = Parse(doc) + if err != nil { + t.Fatal(err) + } + if r.DerivedValues[0].Inputs == nil { + t.Fatal("[] inputs must parse to a non-nil slice") + } +}