commit c3f5491e397ba1526e6021fad5c9a4f79b9be1a2
parent f45235936b8e551422cab34fd1792d02e63fdcb3
Author: triesap <tyson@radroots.org>
Date: Sun, 26 Jul 2026 09:55:14 +0000
blossom: persist sealed publication readiness evidence
- Add bounded canonical JSON persistence and strict sealed reload for readiness evidence.
- Generate the semantic readiness contract, evidence schema, and executable persistence vectors.
- Validate public API and sealed wire authority through structured Rust, Cargo, and operation checks.
- Exercise serde-only and raster lanes while providing Git to Nix provenance tests.
Diffstat:
23 files changed, 3833 insertions(+), 982 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -215,6 +215,11 @@ publish policy both pass for the same source revision.
Deterministic per-URL evidence remains transport-neutral and contains no
HTTP credentials, BUD-11 material, entitlement decision, or private service
topology.
+ The `serde` surface persists this sealed evidence as bounded canonical
+ compact JSON and reloads it only after strict schema/policy, field,
+ URL/hash/MIME/size/dimension/status, digest, and canonical-byte validation.
+ General-purpose deserialization cannot construct the sealed evidence state,
+ and the portable serde-only lane does not require the raster decoder.
- Bare-envelope replica ingestion is quarantined behind the explicit,
non-default `legacy-ingest` feature. Default replica APIs expose emit and sync
surfaces only; a future product ingest boundary must consume a store-produced
diff --git a/build/nix/checks.nix b/build/nix/checks.nix
@@ -19,6 +19,7 @@ let
inherit (common) cargoArtifacts;
pname = "radroots-cargo-test";
doCheck = false;
+ nativeBuildInputs = common.commonCraneArgs.nativeBuildInputs ++ [ pkgs.gitMinimal ];
buildPhaseCargoCommand = ''
cargo test ${common.coreContractCargoArgs}
'';
@@ -33,6 +34,7 @@ let
doCheck = false;
buildPhaseCargoCommand = ''
cargo check -p radroots_blossom --lib --no-default-features
+ cargo check -p radroots_blossom --lib --no-default-features --features serde
cargo check -p radroots_blossom --lib --no-default-features --features raster-decode
'';
installPhaseCommand = "mkdir -p $out";
@@ -45,6 +47,8 @@ let
pname = "radroots-blossom-raster-decode-test";
doCheck = false;
buildPhaseCargoCommand = ''
+ cargo test -p radroots_blossom --no-default-features --features serde \
+ --test publication_readiness_persistence
cargo test -p radroots_blossom --no-default-features --features raster-decode,serde
'';
installPhaseCommand = "mkdir -p $out";
diff --git a/contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json b/contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json
@@ -0,0 +1,319 @@
+{
+ "suite": "blossom_publication_readiness_persistence",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "canonical_evidence_reloads",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "input": {
+ "mutation": "none"
+ },
+ "expected": {
+ "policy_version": 1,
+ "schema_version": 1
+ }
+ },
+ {
+ "id": "canonical_evidence_serializes",
+ "kind": "blossom.publication_readiness_evidence.to_canonical_json.valid",
+ "input": {
+ "mutation": "none"
+ },
+ "expected": {
+ "policy_version": 1,
+ "schema_version": 1
+ }
+ },
+ {
+ "id": "leading_whitespace_is_noncanonical",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "leading_whitespace"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_json_non_canonical"
+ }
+ },
+ {
+ "id": "noncanonical_string_escaping_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "noncanonical_escaping"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_json_non_canonical"
+ }
+ },
+ {
+ "id": "field_reordering_is_noncanonical",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "reordered_fields"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_json_non_canonical"
+ }
+ },
+ {
+ "id": "unknown_field_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "unknown_field"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "private_bud11_field_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "bud11_authorization"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "missing_field_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "missing_uploaded"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "duplicate_field_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "duplicate_schema_version"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "wrong_field_type_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "size_wrong_type"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "schema_version_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "schema_version"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_schema_version_unsupported"
+ }
+ },
+ {
+ "id": "policy_version_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "policy_version"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_policy_version_unsupported"
+ }
+ },
+ {
+ "id": "url_must_be_canonical",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "url_invalid"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "url_exact_maximum_is_valid",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "input": {
+ "mutation": "url_exact_max"
+ },
+ "expected": {
+ "policy_version": 1,
+ "schema_version": 1
+ }
+ },
+ {
+ "id": "url_one_over_maximum_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "url_over_max"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "url_hash_must_match_sha256",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "url_hash_mismatch"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "sha256_must_be_lower_hex",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "sha256_invalid"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "size_must_be_nonzero",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "size_zero"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "size_must_be_bounded",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "size_over_max"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "mime_must_be_supported",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "mime_unsupported"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "format_must_match_mime",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "format_mismatch"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "dimensions_must_be_nonzero",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "width_zero"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "dimensions_must_respect_axis_limit",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "height_over_max"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "dimensions_must_respect_pixel_limit",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "pixels_over_max"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "bud02_status_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "bud02_status"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "bud01_head_status_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "bud01_head_status"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "bud01_get_status_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "bud01_get_status"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "uploaded_mutation_breaks_digest",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "uploaded"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_digest_mismatch"
+ }
+ },
+ {
+ "id": "digest_must_be_lower_hex",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "digest_invalid"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "digest_must_match_all_facts",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "digest_mismatch"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_digest_mismatch"
+ }
+ },
+ {
+ "id": "input_is_bounded_before_parse",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "oversized_input"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_too_large"
+ }
+ }
+ ]
+}
diff --git a/contracts/events/blossom-media.md b/contracts/events/blossom-media.md
@@ -330,6 +330,27 @@ future artifact readiness-binding digest. The artifact adapter remains responsib
exact URL-complete set, rejecting missing/duplicate/extra/reordered evidence, and binding that set
to its independently computed artifact digest.
+Evidence persistence schema v1 is the canonical compact JSON object defined by
+`crates/blossom/contracts/publication_readiness_evidence_v1.schema.json`. Its
+field order is fixed as schema version, policy version, URL, SHA-256, size,
+MIME, raster format, decoded dimensions, BUD-02 status, BUD-01 HEAD/GET
+statuses, upload timestamp, and evidence digest. Both input and output are
+bounded to `8,192` bytes. Reload uses private `deny_unknown_fields` wire models,
+then reconstructs every governed type, checks the URL/hash/MIME/format/size and
+dimension relationships, recomputes the frozen digest preimage, and requires
+byte-for-byte canonical reserialization. The sealed evidence and digest types
+do not implement general-purpose `Deserialize`.
+
+`contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json`
+executes canonical reload plus unknown, missing, duplicate, wrong-type,
+ordering, escaping, schema, policy, URL, hash, size, MIME, format, dimension,
+status, timestamp, digest, and exact-limit mutations. Its packaged mirror and
+behavior executor are authenticated by the generated semantic successor
+`radroots_blossom.publication_readiness_v1`. That successor preserves the
+immutable predecessor artifacts while binding protocol pins, schemas,
+operations, public API inventories, invariants, and executable vector bytes;
+mutable source formatting and release files are not protocol identity.
+
`contracts/conformance/vectors/blossom/publication_readiness.v1.json` executes the accepted status
set, exact evidence output, public limits, bounded body collection, complete-byte comparisons,
closed raster policy, frame and dimension bounds, and all agreement failures. The packaged mirror
diff --git a/contracts/operations.toml b/contracts/operations.toml
@@ -409,6 +409,47 @@ case_kinds = [
"blossom.verify_publication_readiness.invalid",
]
+[operations.blossom_publication_readiness_evidence_to_canonical_json]
+domain = "blossom"
+id = "blossom.publication_readiness_evidence.to_canonical_json"
+stability = "beta"
+inputs = ["RadrootsBlossomPublicationReadinessEvidence"]
+outputs = ["CanonicalJsonBytes"]
+error_class = "encode_error"
+deterministic = true
+signing = "none"
+transport = "none"
+
+[operations.blossom_publication_readiness_evidence_to_canonical_json.implementation]
+rust_modules = ["crates/blossom/src/publication_readiness.rs"]
+rust_types = ["radroots_blossom::RadrootsBlossomPublicationReadinessEvidence"]
+
+[operations.blossom_publication_readiness_evidence_to_canonical_json.conformance]
+vector = "contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json"
+case_kinds = ["blossom.publication_readiness_evidence.to_canonical_json.valid"]
+
+[operations.blossom_publication_readiness_evidence_from_canonical_json]
+domain = "blossom"
+id = "blossom.publication_readiness_evidence.from_canonical_json"
+stability = "beta"
+inputs = ["CanonicalJsonBytes"]
+outputs = ["RadrootsBlossomPublicationReadinessEvidence"]
+error_class = "validation_error"
+deterministic = true
+signing = "none"
+transport = "none"
+
+[operations.blossom_publication_readiness_evidence_from_canonical_json.implementation]
+rust_modules = ["crates/blossom/src/publication_readiness.rs"]
+rust_types = ["radroots_blossom::RadrootsBlossomPublicationReadinessEvidence"]
+
+[operations.blossom_publication_readiness_evidence_from_canonical_json.conformance]
+vector = "contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json"
+case_kinds = [
+ "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+]
+
[operations.blossom_build_upload_authorization_claim]
domain = "blossom"
id = "blossom.build_upload_authorization_claim"
@@ -982,9 +1023,7 @@ rust_types = [
[operations.phase1_publication_allowlist_allow_artifact.conformance]
vector = "contracts/conformance/vectors/publication/phase1_allowlist.v1.json"
-case_kinds = [
- "publication_allowlist.allow_artifact.valid",
-]
+case_kinds = ["publication_allowlist.allow_artifact.valid"]
[operations.phase1_publication_allowlist_allow_canonical_json]
domain = "publication"
diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml
@@ -509,4 +509,4 @@ semver_impacts = [
"add_enum_variant",
"add_conformance_vector",
]
-summary = "Add transport-neutral BUD-02 plus BUD-01 publication-readiness evidence with bounded complete-byte verification, exact sequential JPEG entropy accounting plus pinned strict zune-jpeg decoding, and internally authoritative full decoding for static JPEG, PNG, and WebP rasters."
+summary = "Add transport-neutral BUD-02 plus BUD-01 publication-readiness evidence with bounded complete-byte verification, canonical persisted reload, exact sequential JPEG entropy accounting plus pinned strict zune-jpeg decoding, and internally authoritative full decoding for static JPEG, PNG, and WebP rasters."
diff --git a/crates/blossom/Cargo.toml b/crates/blossom/Cargo.toml
@@ -14,7 +14,7 @@ readme = "README"
[features]
default = ["serde"]
-serde = ["dep:serde"]
+serde = ["dep:serde", "dep:serde_json"]
std = ["serde?/std", "sha2/std", "url_nostd/std"]
raster-decode = ["std", "dep:image", "dep:zune-core", "dep:zune-jpeg"]
@@ -22,6 +22,9 @@ raster-decode = ["std", "dep:image", "dep:zune-core", "dep:zune-jpeg"]
image = { workspace = true, optional = true }
mediatype = { workspace = true }
serde = { workspace = true, optional = true }
+serde_json = { workspace = true, default-features = false, features = [
+ "alloc",
+], optional = true }
sha2 = { workspace = true }
unicode-general-category = { workspace = true }
url_nostd = { workspace = true }
diff --git a/crates/blossom/README b/crates/blossom/README
@@ -38,6 +38,16 @@ remains available without `raster-decode`, but cannot construct readiness
evidence. Policy v1 has no serialized or caller-supplied decode-observation
input.
+With the `serde` feature, sealed readiness evidence can be stored as canonical
+compact JSON and reloaded through an explicit bounded validator. The v1 wire is
+limited to `8,192` bytes, fixes field order, rejects unknown, missing,
+duplicate, reordered, or noncanonical fields, and revalidates the canonical
+hash-path URL, closed raster profile, dimensions, observation statuses, and
+domain-separated evidence digest. The sealed evidence type itself does not
+implement `Deserialize`; ordinary JSON decoding cannot manufacture readiness
+authority. This persistence surface remains available in the portable
+`no_std + alloc` serde lane without enabling raster decoding.
+
Protocol behavior is pinned to Blossom commit
`b5bd2801d1763aa635fc8fea7a76597e0eb18990`:
diff --git a/crates/blossom/contracts/publication_readiness_evidence_v1.schema.json b/crates/blossom/contracts/publication_readiness_evidence_v1.schema.json
@@ -0,0 +1,98 @@
+{
+ "$id": "https://radroots.org/contracts/blossom/publication-readiness-evidence-v1.schema.json",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "properties": {
+ "bud01_get_status": {
+ "const": 200
+ },
+ "bud01_head_status": {
+ "const": 200
+ },
+ "bud02_status": {
+ "enum": [
+ 200,
+ 201
+ ]
+ },
+ "dimensions": {
+ "additionalProperties": false,
+ "properties": {
+ "height": {
+ "maximum": 16384,
+ "minimum": 1,
+ "type": "integer"
+ },
+ "width": {
+ "maximum": 16384,
+ "minimum": 1,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "width",
+ "height"
+ ],
+ "type": "object"
+ },
+ "evidence_digest": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ },
+ "media_type": {
+ "enum": [
+ "image/jpeg",
+ "image/png",
+ "image/webp"
+ ]
+ },
+ "policy_version": {
+ "const": 1
+ },
+ "raster_format": {
+ "enum": [
+ "jpeg",
+ "png",
+ "still_webp"
+ ]
+ },
+ "schema_version": {
+ "const": 1
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ },
+ "size": {
+ "maximum": 10485760,
+ "minimum": 1,
+ "type": "integer"
+ },
+ "uploaded": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "url": {
+ "maxLength": 4096,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "required": [
+ "schema_version",
+ "policy_version",
+ "url",
+ "sha256",
+ "size",
+ "media_type",
+ "raster_format",
+ "dimensions",
+ "bud02_status",
+ "bud01_head_status",
+ "bud01_get_status",
+ "uploaded",
+ "evidence_digest"
+ ],
+ "title": "Radroots Blossom Publication Readiness Evidence v1",
+ "type": "object"
+}
diff --git a/crates/blossom/contracts/publication_readiness_v1.descriptor.json b/crates/blossom/contracts/publication_readiness_v1.descriptor.json
@@ -0,0 +1,31 @@
+{
+ "behavior_vector_sha256": "6408e3b5bc4a376c7411e304833431af918a4072f196e8a8da55c3f0ef8610c9",
+ "contract_id": "radroots_blossom.publication_readiness_v1",
+ "evidence_schema": {
+ "byte_length": 1890,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/blossom/contracts/publication_readiness_evidence_v1.schema.json",
+ "sha256": "c8f5f3488dd91a660f8eaa018d9aba63d03c882dc3ff22b47ac192b7189b0ce2"
+ },
+ "manifest": {
+ "byte_length": 13038,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/blossom/contracts/publication_readiness_v1.manifest.json",
+ "sha256": "9359c5531548778b4a03e1a603a4048f17ba498b16dad50f7c62cca0ddb6240b"
+ },
+ "manifest_schema": {
+ "byte_length": 11884,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/blossom/contracts/publication_readiness_v1.manifest.schema.json",
+ "sha256": "e62ecf4b43bd03831f7e36e9cb4c98e2ed7e3a12d02cbca48a49e402b3feaa7d"
+ },
+ "manifest_sidecar": {
+ "byte_length": 65,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/blossom/contracts/publication_readiness_v1.manifest.sha256",
+ "sha256": "e8c2be84eef68e965ac0e119016d6840cb9503e58d5d9c40e13a6512e1ec74b7"
+ },
+ "persistence_vector_sha256": "e892ff6353afe8997a151a9aff4db2fd82c96d94db7bff31bc2269333adc2512",
+ "predecessor_manifest_sha256": "03253ce31dc31d465880a895d2685f5deb1274948e0a4eabe81a2f08f238c483",
+ "schema_version": 1
+}
diff --git a/crates/blossom/contracts/publication_readiness_v1.manifest.json b/crates/blossom/contracts/publication_readiness_v1.manifest.json
@@ -0,0 +1,314 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_blossom.publication_readiness_v1",
+ "authority_id": "blossom_publication_readiness_v1",
+ "manifest_schema": {
+ "path": "crates/blossom/contracts/publication_readiness_v1.manifest.schema.json",
+ "byte_length": 11884,
+ "sha256": "e62ecf4b43bd03831f7e36e9cb4c98e2ed7e3a12d02cbca48a49e402b3feaa7d",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "predecessor": {
+ "contract_id": "radroots_event_store.raw_source_rebuild_v1",
+ "immutable_artifacts": [
+ {
+ "path": "crates/event_store/contracts/raw_source_rebuild_v1.manifest.json",
+ "byte_length": 45449,
+ "sha256": "03253ce31dc31d465880a895d2685f5deb1274948e0a4eabe81a2f08f238c483",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "path": "crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json",
+ "byte_length": 17896,
+ "sha256": "f9d210967e54b66f39c8bb965d97b2001a0ebc0927e7c2c14edb8e474bfda695",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "path": "crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256",
+ "byte_length": 65,
+ "sha256": "2b8bc07cd479be2281781660efd26fd7a8f480e5f3f62053aeccd2b5e6b2070c",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "path": "crates/event_store/src/generated/raw_source_rebuild_manifest.rs",
+ "byte_length": 50735,
+ "sha256": "3763fbee3ee45621afca990002b9298c791bf0396ebf7bccde0ae1bc9aecb7f2",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "path": "contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json",
+ "byte_length": 26833,
+ "sha256": "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "path": "crates/event_store/tests/fixtures/raw_source_rebuild.v1.json",
+ "byte_length": 26833,
+ "sha256": "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15",
+ "hash_algorithm": "sha256_bytes_v1"
+ }
+ ]
+ },
+ "protocol_sources": [
+ {
+ "id": "blossom",
+ "repository": "https://github.com/hzrd149/blossom",
+ "revision": "b5bd2801d1763aa635fc8fea7a76597e0eb18990"
+ }
+ ],
+ "public_api": {
+ "constants": [
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES"
+ ],
+ "types": [
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomBud01GetCollector",
+ "RadrootsBlossomBud01GetObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud02UploadStatus",
+ "RadrootsBlossomPublicationReadinessEvidence",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest",
+ "RadrootsBlossomRasterDimensions",
+ "RadrootsBlossomRasterFormat"
+ ],
+ "functions": [
+ "verify_publication_readiness"
+ ],
+ "methods": [
+ "RadrootsBlossomBud01GetCollector::finish",
+ "RadrootsBlossomBud01GetCollector::new",
+ "RadrootsBlossomBud01GetCollector::push_chunk",
+ "RadrootsBlossomBud01GetObservation::bytes",
+ "RadrootsBlossomBud01GetObservation::declared_size",
+ "RadrootsBlossomBud01GetObservation::from_complete_body",
+ "RadrootsBlossomBud01GetObservation::url",
+ "RadrootsBlossomBud01HeadObservation::content_length",
+ "RadrootsBlossomBud01HeadObservation::media_type",
+ "RadrootsBlossomBud01HeadObservation::new",
+ "RadrootsBlossomBud01HeadObservation::url",
+ "RadrootsBlossomBud02UploadObservation::descriptor",
+ "RadrootsBlossomBud02UploadObservation::new",
+ "RadrootsBlossomBud02UploadObservation::status",
+ "RadrootsBlossomBud02UploadStatus::as_u16",
+ "RadrootsBlossomPublicationReadinessEvidence::bud02_status",
+ "RadrootsBlossomPublicationReadinessEvidence::dimensions",
+ "RadrootsBlossomPublicationReadinessEvidence::evidence_digest",
+ "RadrootsBlossomPublicationReadinessEvidence::from_canonical_json",
+ "RadrootsBlossomPublicationReadinessEvidence::media_type",
+ "RadrootsBlossomPublicationReadinessEvidence::policy_version",
+ "RadrootsBlossomPublicationReadinessEvidence::raster_format",
+ "RadrootsBlossomPublicationReadinessEvidence::schema_version",
+ "RadrootsBlossomPublicationReadinessEvidence::sha256",
+ "RadrootsBlossomPublicationReadinessEvidence::size",
+ "RadrootsBlossomPublicationReadinessEvidence::to_canonical_json",
+ "RadrootsBlossomPublicationReadinessEvidence::uploaded",
+ "RadrootsBlossomPublicationReadinessEvidence::url",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest::as_sha256",
+ "RadrootsBlossomRasterDimensions::height",
+ "RadrootsBlossomRasterDimensions::new",
+ "RadrootsBlossomRasterDimensions::pixels",
+ "RadrootsBlossomRasterDimensions::width",
+ "RadrootsBlossomRasterFormat::as_str",
+ "RadrootsBlossomRasterFormat::from_media_type"
+ ]
+ },
+ "readiness": {
+ "verify_operation_id": "blossom.verify_publication_readiness",
+ "input_types": [
+ "RadrootsBlossomByteVerifiedDescriptor",
+ "Bytes",
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud01GetObservation"
+ ],
+ "output_type": "RadrootsBlossomPublicationReadinessEvidence",
+ "evidence": {
+ "schema_version": 1,
+ "policy_version": 1,
+ "schema": {
+ "path": "crates/blossom/contracts/publication_readiness_evidence_v1.schema.json",
+ "byte_length": 1890,
+ "sha256": "c8f5f3488dd91a660f8eaa018d9aba63d03c882dc3ff22b47ac192b7189b0ce2",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "max_canonical_json_bytes": 8192,
+ "max_url_utf8_bytes": 4096,
+ "max_raster_bytes": 10485760,
+ "max_decoded_bytes": 160000000,
+ "max_dimension": 16384,
+ "max_pixels": 20000000,
+ "wire_field_order": [
+ "schema_version",
+ "policy_version",
+ "url",
+ "sha256",
+ "size",
+ "media_type",
+ "raster_format",
+ "dimensions",
+ "bud02_status",
+ "bud01_head_status",
+ "bud01_get_status",
+ "uploaded",
+ "evidence_digest"
+ ],
+ "digest_domain": "radroots.blossom.publication-readiness-evidence.v1\u0000",
+ "digest_framing": "domain_bytes_then_u16be_policy_then_u64be_length_prefixed_url_then_raw_sha256_then_u64be_size_then_u64be_length_prefixed_mime_then_u8_format_then_u32be_width_then_u32be_height_then_u16be_bud02_then_u16be_head_then_u16be_get_then_u64be_uploaded_v1",
+ "serialize_operation_id": "blossom.publication_readiness_evidence.to_canonical_json",
+ "reload_operation_id": "blossom.publication_readiness_evidence.from_canonical_json",
+ "invariants": [
+ "bounded_input_before_json_parse_v1",
+ "fixed_compact_json_field_order_v1",
+ "canonical_json_round_trip_required_v1",
+ "sealed_evidence_without_deserialize_v1",
+ "private_deny_unknown_fields_wire_models_v1",
+ "canonical_hash_path_url_max_4096_utf8_bytes_v1",
+ "closed_jpeg_png_still_webp_mime_and_format_v1",
+ "nonzero_size_max_10485760_bytes_v1",
+ "nonzero_dimensions_axis_max_16384_pixels_max_20000000_v1",
+ "bud02_status_200_or_201_and_bud01_status_200_v1",
+ "descriptor_head_get_complete_byte_agreement_v1",
+ "private_domain_separated_digest_derivation_v1",
+ "no_bud11_credentials_entitlement_or_topology_persistence_v1"
+ ],
+ "decode_error_codes": [
+ "publication_readiness_evidence_too_large",
+ "publication_readiness_evidence_invalid_json",
+ "publication_readiness_evidence_schema_version_unsupported",
+ "publication_readiness_evidence_policy_version_unsupported",
+ "publication_readiness_evidence_field_invalid",
+ "publication_readiness_evidence_json_non_canonical",
+ "publication_readiness_evidence_digest_mismatch"
+ ]
+ },
+ "behavior_vector": {
+ "canonical_path": "contracts/conformance/vectors/blossom/publication_readiness.v1.json",
+ "mirror_path": "crates/blossom/tests/fixtures/publication_readiness.v1.json",
+ "byte_length": 16423,
+ "sha256": "6408e3b5bc4a376c7411e304833431af918a4072f196e8a8da55c3f0ef8610c9",
+ "hash_algorithm": "sha256_bytes_v1",
+ "executor": {
+ "path": "crates/blossom/tests/publication_readiness.rs",
+ "byte_length": 33431,
+ "sha256": "17abd5491bcfe0c717f7f67202c8c2d85912a042167d027727c07d9f07306641",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "executor_test": "publication_readiness_conformance_vector_executes_every_case",
+ "case_ids": [
+ "valid_created",
+ "valid_ok_without_authored_dimensions",
+ "invalid_upload_status",
+ "invalid_head_status",
+ "invalid_get_status",
+ "declared_size_over_public_max",
+ "missing_get_body",
+ "short_get_body",
+ "trailing_get_body",
+ "authored_bytes_short",
+ "authored_bytes_wrong_hash",
+ "upload_url_mismatch",
+ "upload_hash_mismatch",
+ "upload_size_mismatch",
+ "upload_mime_mismatch",
+ "head_url_mismatch",
+ "head_size_mismatch",
+ "head_mime_mismatch",
+ "get_url_mismatch",
+ "get_declared_size_mismatch",
+ "get_complete_hash_mismatch",
+ "unsupported_raster_mime",
+ "malformed_raster",
+ "animated_png",
+ "declared_format_mismatch",
+ "corrupt_png_crc",
+ "corrupt_png_deflate",
+ "invalid_png_color_type",
+ "authored_dimension_mismatch",
+ "animated_webp",
+ "zero_width",
+ "dimension_over_max",
+ "pixel_limit",
+ "progressive_jpeg",
+ "jpeg_entropy_stripped",
+ "jpeg_entropy_partial",
+ "malformed_jpeg_dqt"
+ ]
+ },
+ "persistence_vector": {
+ "canonical_path": "contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json",
+ "mirror_path": "crates/blossom/tests/fixtures/publication_readiness_persistence.v1.json",
+ "byte_length": 9264,
+ "sha256": "e892ff6353afe8997a151a9aff4db2fd82c96d94db7bff31bc2269333adc2512",
+ "hash_algorithm": "sha256_bytes_v1",
+ "executor": {
+ "path": "crates/blossom/tests/publication_readiness_persistence.rs",
+ "byte_length": 11567,
+ "sha256": "aa14d20ee5747fcc979907b4b59a67687a3a02852850e33aede65e2ab0e0ca8d",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "executor_test": "publication_readiness_persistence_vector_executes_every_case",
+ "case_ids": [
+ "canonical_evidence_reloads",
+ "canonical_evidence_serializes",
+ "leading_whitespace_is_noncanonical",
+ "noncanonical_string_escaping_is_rejected",
+ "field_reordering_is_noncanonical",
+ "unknown_field_is_rejected",
+ "private_bud11_field_is_rejected",
+ "missing_field_is_rejected",
+ "duplicate_field_is_rejected",
+ "wrong_field_type_is_rejected",
+ "schema_version_is_strict",
+ "policy_version_is_strict",
+ "url_must_be_canonical",
+ "url_exact_maximum_is_valid",
+ "url_one_over_maximum_is_rejected",
+ "url_hash_must_match_sha256",
+ "sha256_must_be_lower_hex",
+ "size_must_be_nonzero",
+ "size_must_be_bounded",
+ "mime_must_be_supported",
+ "format_must_match_mime",
+ "dimensions_must_be_nonzero",
+ "dimensions_must_respect_axis_limit",
+ "dimensions_must_respect_pixel_limit",
+ "bud02_status_is_strict",
+ "bud01_head_status_is_strict",
+ "bud01_get_status_is_strict",
+ "uploaded_mutation_breaks_digest",
+ "digest_must_be_lower_hex",
+ "digest_must_match_all_facts",
+ "input_is_bounded_before_parse"
+ ]
+ }
+ },
+ "predecessor_source_supersessions": [
+ "Cargo.lock",
+ "Cargo.toml",
+ "build/nix/common.nix",
+ "CHANGELOG.md",
+ "contracts/releases/1.0.0-alpha.1.toml",
+ "tools/xtask/src/contract.rs",
+ "tools/xtask/src/contract/food_availability_projection.rs",
+ "tools/xtask/src/contract/nip09_reconciliation.rs",
+ "tools/xtask/src/contract/raw_source_rebuild.rs",
+ "tools/xtask/src/contract/source_maintenance.rs",
+ "tools/xtask/src/main.rs"
+ ],
+ "transitive_predecessor_source_supersessions": [
+ "Cargo.toml",
+ "crates/blossom/Cargo.toml",
+ "crates/blossom/src/error.rs",
+ "crates/blossom/src/lib.rs",
+ "crates/blossom/src/url.rs"
+ ]
+}
diff --git a/crates/blossom/contracts/publication_readiness_v1.manifest.schema.json b/crates/blossom/contracts/publication_readiness_v1.manifest.schema.json
@@ -0,0 +1,383 @@
+{
+ "$defs": {
+ "evidence": {
+ "additionalProperties": false,
+ "properties": {
+ "decode_error_codes": {
+ "const": [
+ "publication_readiness_evidence_too_large",
+ "publication_readiness_evidence_invalid_json",
+ "publication_readiness_evidence_schema_version_unsupported",
+ "publication_readiness_evidence_policy_version_unsupported",
+ "publication_readiness_evidence_field_invalid",
+ "publication_readiness_evidence_json_non_canonical",
+ "publication_readiness_evidence_digest_mismatch"
+ ]
+ },
+ "digest_domain": {
+ "const": "radroots.blossom.publication-readiness-evidence.v1\u0000"
+ },
+ "digest_framing": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "invariants": {
+ "const": [
+ "bounded_input_before_json_parse_v1",
+ "fixed_compact_json_field_order_v1",
+ "canonical_json_round_trip_required_v1",
+ "sealed_evidence_without_deserialize_v1",
+ "private_deny_unknown_fields_wire_models_v1",
+ "canonical_hash_path_url_max_4096_utf8_bytes_v1",
+ "closed_jpeg_png_still_webp_mime_and_format_v1",
+ "nonzero_size_max_10485760_bytes_v1",
+ "nonzero_dimensions_axis_max_16384_pixels_max_20000000_v1",
+ "bud02_status_200_or_201_and_bud01_status_200_v1",
+ "descriptor_head_get_complete_byte_agreement_v1",
+ "private_domain_separated_digest_derivation_v1",
+ "no_bud11_credentials_entitlement_or_topology_persistence_v1"
+ ]
+ },
+ "max_canonical_json_bytes": {
+ "const": 8192
+ },
+ "max_decoded_bytes": {
+ "const": 160000000
+ },
+ "max_dimension": {
+ "const": 16384
+ },
+ "max_pixels": {
+ "const": 20000000
+ },
+ "max_raster_bytes": {
+ "const": 10485760
+ },
+ "max_url_utf8_bytes": {
+ "const": 4096
+ },
+ "policy_version": {
+ "const": 1
+ },
+ "reload_operation_id": {
+ "const": "blossom.publication_readiness_evidence.from_canonical_json"
+ },
+ "schema": {
+ "$ref": "#/$defs/file"
+ },
+ "schema_version": {
+ "const": 1
+ },
+ "serialize_operation_id": {
+ "const": "blossom.publication_readiness_evidence.to_canonical_json"
+ },
+ "wire_field_order": {
+ "const": [
+ "schema_version",
+ "policy_version",
+ "url",
+ "sha256",
+ "size",
+ "media_type",
+ "raster_format",
+ "dimensions",
+ "bud02_status",
+ "bud01_head_status",
+ "bud01_get_status",
+ "uploaded",
+ "evidence_digest"
+ ]
+ }
+ },
+ "required": [
+ "schema_version",
+ "policy_version",
+ "schema",
+ "max_canonical_json_bytes",
+ "max_url_utf8_bytes",
+ "max_raster_bytes",
+ "max_decoded_bytes",
+ "max_dimension",
+ "max_pixels",
+ "wire_field_order",
+ "digest_domain",
+ "digest_framing",
+ "serialize_operation_id",
+ "reload_operation_id",
+ "invariants",
+ "decode_error_codes"
+ ],
+ "type": "object"
+ },
+ "file": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "path": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm"
+ ],
+ "type": "object"
+ },
+ "readiness": {
+ "additionalProperties": false,
+ "properties": {
+ "behavior_vector": {
+ "$ref": "#/$defs/vector"
+ },
+ "evidence": {
+ "$ref": "#/$defs/evidence"
+ },
+ "input_types": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "maxItems": 6,
+ "minItems": 6,
+ "type": "array"
+ },
+ "output_type": {
+ "const": "RadrootsBlossomPublicationReadinessEvidence"
+ },
+ "persistence_vector": {
+ "$ref": "#/$defs/vector"
+ },
+ "verify_operation_id": {
+ "const": "blossom.verify_publication_readiness"
+ }
+ },
+ "required": [
+ "verify_operation_id",
+ "input_types",
+ "output_type",
+ "evidence",
+ "behavior_vector",
+ "persistence_vector"
+ ],
+ "type": "object"
+ },
+ "vector": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "canonical_path": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "case_ids": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ },
+ "executor": {
+ "$ref": "#/$defs/file"
+ },
+ "executor_test": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "mirror_path": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "canonical_path",
+ "mirror_path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm",
+ "executor",
+ "executor_test",
+ "case_ids"
+ ],
+ "type": "object"
+ }
+ },
+ "$id": "https://radroots.org/contracts/blossom/publication-readiness-v1.schema.json",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "properties": {
+ "authority_id": {
+ "const": "blossom_publication_readiness_v1"
+ },
+ "contract_id": {
+ "const": "radroots_blossom.publication_readiness_v1"
+ },
+ "manifest_schema": {
+ "$ref": "#/$defs/file"
+ },
+ "predecessor": {
+ "additionalProperties": false,
+ "properties": {
+ "contract_id": {
+ "const": "radroots_event_store.raw_source_rebuild_v1"
+ },
+ "immutable_artifacts": {
+ "items": {
+ "$ref": "#/$defs/file"
+ },
+ "maxItems": 6,
+ "minItems": 6,
+ "type": "array"
+ }
+ },
+ "required": [
+ "contract_id",
+ "immutable_artifacts"
+ ],
+ "type": "object"
+ },
+ "predecessor_source_supersessions": {
+ "const": [
+ "Cargo.lock",
+ "Cargo.toml",
+ "build/nix/common.nix",
+ "CHANGELOG.md",
+ "contracts/releases/1.0.0-alpha.1.toml",
+ "tools/xtask/src/contract.rs",
+ "tools/xtask/src/contract/food_availability_projection.rs",
+ "tools/xtask/src/contract/nip09_reconciliation.rs",
+ "tools/xtask/src/contract/raw_source_rebuild.rs",
+ "tools/xtask/src/contract/source_maintenance.rs",
+ "tools/xtask/src/main.rs"
+ ]
+ },
+ "protocol_sources": {
+ "const": [
+ {
+ "id": "blossom",
+ "repository": "https://github.com/hzrd149/blossom",
+ "revision": "b5bd2801d1763aa635fc8fea7a76597e0eb18990"
+ }
+ ]
+ },
+ "public_api": {
+ "const": {
+ "constants": [
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES"
+ ],
+ "functions": [
+ "verify_publication_readiness"
+ ],
+ "methods": [
+ "RadrootsBlossomBud01GetCollector::finish",
+ "RadrootsBlossomBud01GetCollector::new",
+ "RadrootsBlossomBud01GetCollector::push_chunk",
+ "RadrootsBlossomBud01GetObservation::bytes",
+ "RadrootsBlossomBud01GetObservation::declared_size",
+ "RadrootsBlossomBud01GetObservation::from_complete_body",
+ "RadrootsBlossomBud01GetObservation::url",
+ "RadrootsBlossomBud01HeadObservation::content_length",
+ "RadrootsBlossomBud01HeadObservation::media_type",
+ "RadrootsBlossomBud01HeadObservation::new",
+ "RadrootsBlossomBud01HeadObservation::url",
+ "RadrootsBlossomBud02UploadObservation::descriptor",
+ "RadrootsBlossomBud02UploadObservation::new",
+ "RadrootsBlossomBud02UploadObservation::status",
+ "RadrootsBlossomBud02UploadStatus::as_u16",
+ "RadrootsBlossomPublicationReadinessEvidence::bud02_status",
+ "RadrootsBlossomPublicationReadinessEvidence::dimensions",
+ "RadrootsBlossomPublicationReadinessEvidence::evidence_digest",
+ "RadrootsBlossomPublicationReadinessEvidence::from_canonical_json",
+ "RadrootsBlossomPublicationReadinessEvidence::media_type",
+ "RadrootsBlossomPublicationReadinessEvidence::policy_version",
+ "RadrootsBlossomPublicationReadinessEvidence::raster_format",
+ "RadrootsBlossomPublicationReadinessEvidence::schema_version",
+ "RadrootsBlossomPublicationReadinessEvidence::sha256",
+ "RadrootsBlossomPublicationReadinessEvidence::size",
+ "RadrootsBlossomPublicationReadinessEvidence::to_canonical_json",
+ "RadrootsBlossomPublicationReadinessEvidence::uploaded",
+ "RadrootsBlossomPublicationReadinessEvidence::url",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest::as_sha256",
+ "RadrootsBlossomRasterDimensions::height",
+ "RadrootsBlossomRasterDimensions::new",
+ "RadrootsBlossomRasterDimensions::pixels",
+ "RadrootsBlossomRasterDimensions::width",
+ "RadrootsBlossomRasterFormat::as_str",
+ "RadrootsBlossomRasterFormat::from_media_type"
+ ],
+ "types": [
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomBud01GetCollector",
+ "RadrootsBlossomBud01GetObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud02UploadStatus",
+ "RadrootsBlossomPublicationReadinessEvidence",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest",
+ "RadrootsBlossomRasterDimensions",
+ "RadrootsBlossomRasterFormat"
+ ]
+ }
+ },
+ "readiness": {
+ "$ref": "#/$defs/readiness"
+ },
+ "schema_version": {
+ "const": 1
+ },
+ "transitive_predecessor_source_supersessions": {
+ "const": [
+ "Cargo.toml",
+ "crates/blossom/Cargo.toml",
+ "crates/blossom/src/error.rs",
+ "crates/blossom/src/lib.rs",
+ "crates/blossom/src/url.rs"
+ ]
+ }
+ },
+ "required": [
+ "schema_version",
+ "contract_id",
+ "authority_id",
+ "manifest_schema",
+ "predecessor",
+ "protocol_sources",
+ "public_api",
+ "readiness",
+ "predecessor_source_supersessions",
+ "transitive_predecessor_source_supersessions"
+ ],
+ "title": "Radroots Blossom Publication Readiness Semantic Contract",
+ "type": "object"
+}
diff --git a/crates/blossom/contracts/publication_readiness_v1.manifest.sha256 b/crates/blossom/contracts/publication_readiness_v1.manifest.sha256
@@ -0,0 +1 @@
+9359c5531548778b4a03e1a603a4048f17ba498b16dad50f7c62cca0ddb6240b
diff --git a/crates/blossom/src/error.rs b/crates/blossom/src/error.rs
@@ -43,6 +43,8 @@ pub enum RadrootsBlossomError {
InvalidBud02UploadStatus { actual: u16 },
InvalidBud01HeadStatus { actual: u16 },
InvalidBud01GetStatus { actual: u16 },
+ PublicationReadinessUrlTooLarge { max: usize, actual: usize },
+ PublicationRasterEmpty,
PublicationRasterByteLimitExceeded { declared: u64, maximum: u64 },
PublicationGetBodyAllocationFailed,
PublicationGetBodyLengthOverflow,
@@ -73,6 +75,14 @@ pub enum RadrootsBlossomError {
PublicationRasterDecodeFailed,
PublicationRasterContainerDimensionMismatch,
PublicationAuthoredRasterDimensionMismatch,
+ PublicationReadinessEvidenceTooLarge { max: usize, actual: usize },
+ PublicationReadinessEvidenceInvalidJson,
+ PublicationReadinessEvidenceUnsupportedSchemaVersion { expected: u32, actual: u32 },
+ PublicationReadinessEvidenceUnsupportedPolicyVersion { expected: u16, actual: u16 },
+ PublicationReadinessEvidenceInvalidField { field: &'static str },
+ PublicationReadinessEvidenceDigestMismatch,
+ PublicationReadinessEvidenceNonCanonicalJson,
+ PublicationReadinessEvidenceSerialization,
}
impl RadrootsBlossomError {
@@ -118,6 +128,8 @@ impl RadrootsBlossomError {
Self::InvalidBud02UploadStatus { .. } => "invalid_bud02_upload_status",
Self::InvalidBud01HeadStatus { .. } => "invalid_bud01_head_status",
Self::InvalidBud01GetStatus { .. } => "invalid_bud01_get_status",
+ Self::PublicationReadinessUrlTooLarge { .. } => "publication_readiness_url_too_large",
+ Self::PublicationRasterEmpty => "publication_raster_empty",
Self::PublicationRasterByteLimitExceeded { .. } => {
"publication_raster_byte_limit_exceeded"
}
@@ -172,6 +184,30 @@ impl RadrootsBlossomError {
Self::PublicationAuthoredRasterDimensionMismatch => {
"publication_authored_raster_dimension_mismatch"
}
+ Self::PublicationReadinessEvidenceTooLarge { .. } => {
+ "publication_readiness_evidence_too_large"
+ }
+ Self::PublicationReadinessEvidenceInvalidJson => {
+ "publication_readiness_evidence_invalid_json"
+ }
+ Self::PublicationReadinessEvidenceUnsupportedSchemaVersion { .. } => {
+ "publication_readiness_evidence_schema_version_unsupported"
+ }
+ Self::PublicationReadinessEvidenceUnsupportedPolicyVersion { .. } => {
+ "publication_readiness_evidence_policy_version_unsupported"
+ }
+ Self::PublicationReadinessEvidenceInvalidField { .. } => {
+ "publication_readiness_evidence_field_invalid"
+ }
+ Self::PublicationReadinessEvidenceDigestMismatch => {
+ "publication_readiness_evidence_digest_mismatch"
+ }
+ Self::PublicationReadinessEvidenceNonCanonicalJson => {
+ "publication_readiness_evidence_json_non_canonical"
+ }
+ Self::PublicationReadinessEvidenceSerialization => {
+ "publication_readiness_evidence_serialization_failed"
+ }
}
}
}
@@ -281,6 +317,13 @@ impl fmt::Display for RadrootsBlossomError {
Self::InvalidBud01GetStatus { actual } => {
write!(f, "BUD-01 GET status must be 200, got {actual}")
}
+ Self::PublicationReadinessUrlTooLarge { max, actual } => write!(
+ f,
+ "publication-readiness URL contains {actual} bytes, exceeding maximum {max}"
+ ),
+ Self::PublicationRasterEmpty => {
+ f.write_str("publication raster must contain at least one byte")
+ }
Self::PublicationRasterByteLimitExceeded { declared, maximum } => write!(
f,
"publication raster declares {declared} bytes, exceeding maximum {maximum}"
@@ -379,6 +422,40 @@ impl fmt::Display for RadrootsBlossomError {
Self::PublicationAuthoredRasterDimensionMismatch => {
f.write_str("decoded raster dimensions do not match the authored dimensions")
}
+ Self::PublicationReadinessEvidenceTooLarge { max, actual } => write!(
+ f,
+ "publication-readiness evidence is {actual} bytes, exceeding maximum {max}"
+ ),
+ Self::PublicationReadinessEvidenceInvalidJson => {
+ f.write_str("publication-readiness evidence is not valid strict JSON")
+ }
+ Self::PublicationReadinessEvidenceUnsupportedSchemaVersion { expected, actual } => {
+ write!(
+ f,
+ "publication-readiness evidence schema version mismatch: expected {expected}, got {actual}"
+ )
+ }
+ Self::PublicationReadinessEvidenceUnsupportedPolicyVersion { expected, actual } => {
+ write!(
+ f,
+ "publication-readiness evidence policy version mismatch: expected {expected}, got {actual}"
+ )
+ }
+ Self::PublicationReadinessEvidenceInvalidField { field } => {
+ write!(
+ f,
+ "publication-readiness evidence field `{field}` is invalid"
+ )
+ }
+ Self::PublicationReadinessEvidenceDigestMismatch => {
+ f.write_str("publication-readiness evidence digest does not match its facts")
+ }
+ Self::PublicationReadinessEvidenceNonCanonicalJson => {
+ f.write_str("publication-readiness evidence JSON is not canonical")
+ }
+ Self::PublicationReadinessEvidenceSerialization => {
+ f.write_str("publication-readiness evidence serialization failed")
+ }
}
}
}
@@ -415,6 +492,8 @@ mod tests {
RadrootsBlossomError::InvalidBud02UploadStatus { actual: 202 },
RadrootsBlossomError::InvalidBud01HeadStatus { actual: 204 },
RadrootsBlossomError::InvalidBud01GetStatus { actual: 206 },
+ RadrootsBlossomError::PublicationReadinessUrlTooLarge { max: 1, actual: 2 },
+ RadrootsBlossomError::PublicationRasterEmpty,
RadrootsBlossomError::PublicationRasterByteLimitExceeded {
declared: 2,
maximum: 1,
@@ -472,6 +551,20 @@ mod tests {
RadrootsBlossomError::PublicationRasterDecodeFailed,
RadrootsBlossomError::PublicationRasterContainerDimensionMismatch,
RadrootsBlossomError::PublicationAuthoredRasterDimensionMismatch,
+ RadrootsBlossomError::PublicationReadinessEvidenceTooLarge { max: 1, actual: 2 },
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidJson,
+ RadrootsBlossomError::PublicationReadinessEvidenceUnsupportedSchemaVersion {
+ expected: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::PublicationReadinessEvidenceUnsupportedPolicyVersion {
+ expected: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField { field: "url" },
+ RadrootsBlossomError::PublicationReadinessEvidenceDigestMismatch,
+ RadrootsBlossomError::PublicationReadinessEvidenceNonCanonicalJson,
+ RadrootsBlossomError::PublicationReadinessEvidenceSerialization,
RadrootsBlossomError::InvalidAuthorizationContent,
RadrootsBlossomError::InvalidAuthorizationAction,
RadrootsBlossomError::InvalidAuthorizationServerDomain,
diff --git a/crates/blossom/src/lib.rs b/crates/blossom/src/lib.rs
@@ -33,7 +33,10 @@ pub use publication_readiness::{
RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES,
RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION,
RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS,
- RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION, RadrootsBlossomAuthoredRasterDimensions,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES, RadrootsBlossomAuthoredRasterDimensions,
RadrootsBlossomBud01GetCollector, RadrootsBlossomBud01GetObservation,
RadrootsBlossomBud01HeadObservation, RadrootsBlossomBud02UploadObservation,
RadrootsBlossomBud02UploadStatus, RadrootsBlossomPublicationReadinessEvidence,
diff --git a/crates/blossom/src/publication_readiness.rs b/crates/blossom/src/publication_readiness.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "serde")]
+use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
#[cfg(feature = "raster-decode")]
@@ -5,7 +7,7 @@ use image::{
ImageDecoder, Limits,
codecs::{png::PngDecoder, webp::WebPDecoder},
};
-#[cfg(feature = "raster-decode")]
+#[cfg(any(feature = "raster-decode", feature = "serde"))]
use sha2::{Digest, Sha256};
#[cfg(feature = "raster-decode")]
use std::io::Cursor;
@@ -17,6 +19,8 @@ use zune_jpeg::JpegDecoder as StrictJpegDecoder;
#[cfg(feature = "raster-decode")]
mod sequential_jpeg;
+#[cfg(feature = "serde")]
+use crate::RadrootsBlossomBlobUrl;
#[cfg(feature = "raster-decode")]
use crate::RadrootsBlossomByteVerifiedDescriptor;
use crate::{
@@ -27,13 +31,16 @@ use crate::{
const _: () = assert!(usize::BITS <= u64::BITS);
pub const RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION: u16 = 1;
+pub const RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION: u32 = 1;
+pub const RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES: usize = 8 * 1024;
+pub const RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES: usize = 4 * 1024;
pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES: u64 = 10_485_760;
pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES: u64 =
RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS * 8;
pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION: u32 = 16_384;
pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_000;
-#[cfg(feature = "raster-decode")]
+#[cfg(any(feature = "raster-decode", feature = "serde"))]
const READINESS_EVIDENCE_DIGEST_DOMAIN: &[u8] =
b"radroots.blossom.publication-readiness-evidence.v1\0";
@@ -87,7 +94,7 @@ impl RadrootsBlossomRasterFormat {
}
}
- #[cfg(feature = "raster-decode")]
+ #[cfg(any(feature = "raster-decode", feature = "serde"))]
const fn digest_code(self) -> u8 {
match self {
Self::Jpeg => 1,
@@ -364,6 +371,14 @@ pub struct RadrootsBlossomPublicationReadinessEvidence {
}
impl RadrootsBlossomPublicationReadinessEvidence {
+ pub const fn schema_version(&self) -> u32 {
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION
+ }
+
+ pub const fn policy_version(&self) -> u16 {
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION
+ }
+
pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl {
&self.url
}
@@ -399,6 +414,28 @@ impl RadrootsBlossomPublicationReadinessEvidence {
pub const fn evidence_digest(&self) -> RadrootsBlossomPublicationReadinessEvidenceDigest {
self.evidence_digest
}
+
+ #[cfg(feature = "serde")]
+ pub fn to_canonical_json(&self) -> Result<Vec<u8>, RadrootsBlossomError> {
+ serialize_readiness_evidence(self)
+ }
+
+ #[cfg(feature = "serde")]
+ pub fn from_canonical_json(bytes: &[u8]) -> Result<Self, RadrootsBlossomError> {
+ if bytes.len() > RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES {
+ return Err(RadrootsBlossomError::PublicationReadinessEvidenceTooLarge {
+ max: RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES,
+ actual: bytes.len(),
+ });
+ }
+ let wire: PublicationReadinessEvidenceWire = serde_json::from_slice(bytes)
+ .map_err(|_| RadrootsBlossomError::PublicationReadinessEvidenceInvalidJson)?;
+ let evidence = readiness_evidence_from_wire(wire)?;
+ if serialize_readiness_evidence(&evidence)? != bytes {
+ return Err(RadrootsBlossomError::PublicationReadinessEvidenceNonCanonicalJson);
+ }
+ Ok(evidence)
+ }
}
#[cfg(feature = "raster-decode")]
@@ -415,6 +452,15 @@ pub fn verify_publication_readiness(
let expected_size = authored_descriptor.size();
let expected_media_type = authored_descriptor.media_type();
+ if expected_url.as_str().len() > RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES {
+ return Err(RadrootsBlossomError::PublicationReadinessUrlTooLarge {
+ max: RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES,
+ actual: expected_url.as_str().len(),
+ });
+ }
+ if expected_size == 0 {
+ return Err(RadrootsBlossomError::PublicationRasterEmpty);
+ }
if expected_size > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES {
return Err(RadrootsBlossomError::PublicationRasterByteLimitExceeded {
declared: expected_size,
@@ -487,12 +533,16 @@ pub fn verify_publication_readiness(
return Err(RadrootsBlossomError::PublicationAuthoredRasterDimensionMismatch);
}
- let evidence_digest = evidence_digest(
- authored_descriptor,
- expected_format,
- decoded_dimensions,
- upload,
- );
+ let evidence_digest = evidence_digest_from_facts(&ReadinessEvidenceDigestFacts {
+ url: expected_url,
+ sha256: expected_hash,
+ size: expected_size,
+ media_type: expected_media_type,
+ format: expected_format,
+ dimensions: decoded_dimensions,
+ bud02_status: upload.status(),
+ uploaded: upload_descriptor.uploaded(),
+ });
Ok(RadrootsBlossomPublicationReadinessEvidence {
url: expected_url.clone(),
sha256: expected_hash,
@@ -682,39 +732,254 @@ fn validate_retrieved_body(
Ok(())
}
-#[cfg(feature = "raster-decode")]
-fn evidence_digest(
- descriptor: &RadrootsBlossomByteVerifiedDescriptor,
+#[cfg(any(feature = "raster-decode", feature = "serde"))]
+struct ReadinessEvidenceDigestFacts<'a> {
+ url: &'a RadrootsBlossomApprovedBlobUrl,
+ sha256: RadrootsBlossomSha256,
+ size: u64,
+ media_type: &'a RadrootsBlossomMediaType,
format: RadrootsBlossomRasterFormat,
dimensions: RadrootsBlossomRasterDimensions,
- upload: &RadrootsBlossomBud02UploadObservation,
+ bud02_status: RadrootsBlossomBud02UploadStatus,
+ uploaded: u64,
+}
+
+#[cfg(any(feature = "raster-decode", feature = "serde"))]
+fn evidence_digest_from_facts(
+ facts: &ReadinessEvidenceDigestFacts<'_>,
) -> RadrootsBlossomPublicationReadinessEvidenceDigest {
let mut hasher = Sha256::new();
hasher.update(READINESS_EVIDENCE_DIGEST_DOMAIN);
hasher.update(RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION.to_be_bytes());
- update_length_prefixed(&mut hasher, descriptor.url().as_str().as_bytes());
- hasher.update(descriptor.sha256().as_bytes());
- hasher.update(descriptor.size().to_be_bytes());
- update_length_prefixed(&mut hasher, descriptor.media_type().as_str().as_bytes());
- hasher.update([format.digest_code()]);
- hasher.update(dimensions.width().to_be_bytes());
- hasher.update(dimensions.height().to_be_bytes());
- hasher.update(upload.status().as_u16().to_be_bytes());
+ update_length_prefixed(&mut hasher, facts.url.as_str().as_bytes());
+ hasher.update(facts.sha256.as_bytes());
+ hasher.update(facts.size.to_be_bytes());
+ update_length_prefixed(&mut hasher, facts.media_type.as_str().as_bytes());
+ hasher.update([facts.format.digest_code()]);
+ hasher.update(facts.dimensions.width().to_be_bytes());
+ hasher.update(facts.dimensions.height().to_be_bytes());
+ hasher.update(facts.bud02_status.as_u16().to_be_bytes());
hasher.update(200_u16.to_be_bytes());
hasher.update(200_u16.to_be_bytes());
- hasher.update(upload.descriptor().descriptor().uploaded().to_be_bytes());
+ hasher.update(facts.uploaded.to_be_bytes());
let digest = hasher.finalize();
let mut bytes = [0_u8; 32];
bytes.copy_from_slice(&digest);
RadrootsBlossomPublicationReadinessEvidenceDigest(RadrootsBlossomSha256::from_bytes(bytes))
}
-#[cfg(feature = "raster-decode")]
+#[cfg(any(feature = "raster-decode", feature = "serde"))]
fn update_length_prefixed(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update((bytes.len() as u64).to_be_bytes());
hasher.update(bytes);
}
+#[cfg(feature = "serde")]
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(deny_unknown_fields)]
+struct PublicationReadinessDimensionsWire {
+ width: u32,
+ height: u32,
+}
+
+#[cfg(feature = "serde")]
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(deny_unknown_fields)]
+struct PublicationReadinessEvidenceWire {
+ schema_version: u32,
+ policy_version: u16,
+ url: String,
+ sha256: String,
+ size: u64,
+ media_type: String,
+ raster_format: String,
+ dimensions: PublicationReadinessDimensionsWire,
+ bud02_status: u16,
+ bud01_head_status: u16,
+ bud01_get_status: u16,
+ uploaded: u64,
+ evidence_digest: String,
+}
+
+#[cfg(feature = "serde")]
+fn serialize_readiness_evidence(
+ evidence: &RadrootsBlossomPublicationReadinessEvidence,
+) -> Result<Vec<u8>, RadrootsBlossomError> {
+ let wire = PublicationReadinessEvidenceWire {
+ schema_version: RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION,
+ policy_version: RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION,
+ url: evidence.url.as_str().to_string(),
+ sha256: evidence.sha256.to_string(),
+ size: evidence.size,
+ media_type: evidence.media_type.as_str().to_string(),
+ raster_format: evidence.raster_format.as_str().to_string(),
+ dimensions: PublicationReadinessDimensionsWire {
+ width: evidence.dimensions.width(),
+ height: evidence.dimensions.height(),
+ },
+ bud02_status: evidence.bud02_status.as_u16(),
+ bud01_head_status: 200,
+ bud01_get_status: 200,
+ uploaded: evidence.uploaded,
+ evidence_digest: evidence.evidence_digest.to_string(),
+ };
+ let bytes = serde_json::to_vec(&wire)
+ .map_err(|_| RadrootsBlossomError::PublicationReadinessEvidenceSerialization)?;
+ if bytes.len() > RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES {
+ return Err(RadrootsBlossomError::PublicationReadinessEvidenceTooLarge {
+ max: RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES,
+ actual: bytes.len(),
+ });
+ }
+ Ok(bytes)
+}
+
+#[cfg(feature = "serde")]
+fn readiness_evidence_from_wire(
+ wire: PublicationReadinessEvidenceWire,
+) -> Result<RadrootsBlossomPublicationReadinessEvidence, RadrootsBlossomError> {
+ if wire.schema_version != RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceUnsupportedSchemaVersion {
+ expected: RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION,
+ actual: wire.schema_version,
+ },
+ );
+ }
+ if wire.policy_version != RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceUnsupportedPolicyVersion {
+ expected: RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION,
+ actual: wire.policy_version,
+ },
+ );
+ }
+ if wire.url.len() > RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField { field: "url" },
+ );
+ }
+ if wire.size == 0 || wire.size > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField { field: "size" },
+ );
+ }
+ let sha256 = RadrootsBlossomSha256::from_hex(&wire.sha256).map_err(|_| {
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField { field: "sha256" }
+ })?;
+ let media_type = RadrootsBlossomMediaType::parse(&wire.media_type).map_err(|_| {
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "media_type",
+ }
+ })?;
+ if media_type.as_str() != wire.media_type {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "media_type",
+ },
+ );
+ }
+ let raster_format = match wire.raster_format.as_str() {
+ "jpeg" => RadrootsBlossomRasterFormat::Jpeg,
+ "png" => RadrootsBlossomRasterFormat::Png,
+ "still_webp" => RadrootsBlossomRasterFormat::StillWebP,
+ _ => {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "raster_format",
+ },
+ );
+ }
+ };
+ if RadrootsBlossomRasterFormat::from_media_type(&media_type).map_err(|_| {
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "media_type",
+ }
+ })? != raster_format
+ {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "raster_format",
+ },
+ );
+ }
+ let dimensions =
+ RadrootsBlossomRasterDimensions::new(wire.dimensions.width, wire.dimensions.height)
+ .map_err(
+ |_| RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "dimensions",
+ },
+ )?;
+ let bud02_status =
+ RadrootsBlossomBud02UploadStatus::parse(wire.bud02_status).map_err(|_| {
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "bud02_status",
+ }
+ })?;
+ if wire.bud01_head_status != 200 {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "bud01_head_status",
+ },
+ );
+ }
+ if wire.bud01_get_status != 200 {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "bud01_get_status",
+ },
+ );
+ }
+ let blob_url = RadrootsBlossomBlobUrl::parse(&wire.url).map_err(|_| {
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField { field: "url" }
+ })?;
+ if blob_url.as_str() != wire.url {
+ return Err(
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField { field: "url" },
+ );
+ }
+ let approved = RadrootsBlossomBlobDescriptor::new(
+ blob_url,
+ sha256,
+ wire.size,
+ media_type.clone(),
+ wire.uploaded,
+ )
+ .and_then(RadrootsBlossomBlobDescriptor::approve_reference)
+ .map_err(|_| RadrootsBlossomError::PublicationReadinessEvidenceInvalidField { field: "url" })?;
+ let evidence_digest = RadrootsBlossomPublicationReadinessEvidenceDigest(
+ RadrootsBlossomSha256::from_hex(&wire.evidence_digest).map_err(|_| {
+ RadrootsBlossomError::PublicationReadinessEvidenceInvalidField {
+ field: "evidence_digest",
+ }
+ })?,
+ );
+ let expected_digest = evidence_digest_from_facts(&ReadinessEvidenceDigestFacts {
+ url: approved.url(),
+ sha256,
+ size: wire.size,
+ media_type: &media_type,
+ format: raster_format,
+ dimensions,
+ bud02_status,
+ uploaded: wire.uploaded,
+ });
+ if evidence_digest != expected_digest {
+ return Err(RadrootsBlossomError::PublicationReadinessEvidenceDigestMismatch);
+ }
+ Ok(RadrootsBlossomPublicationReadinessEvidence {
+ url: approved.url().clone(),
+ sha256,
+ size: wire.size,
+ media_type,
+ raster_format,
+ dimensions,
+ bud02_status,
+ uploaded: wire.uploaded,
+ evidence_digest,
+ })
+}
+
#[cfg(any(feature = "raster-decode", test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct RasterContainerInspection {
diff --git a/crates/blossom/tests/fixtures/publication_readiness_persistence.v1.json b/crates/blossom/tests/fixtures/publication_readiness_persistence.v1.json
@@ -0,0 +1,319 @@
+{
+ "suite": "blossom_publication_readiness_persistence",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "canonical_evidence_reloads",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "input": {
+ "mutation": "none"
+ },
+ "expected": {
+ "policy_version": 1,
+ "schema_version": 1
+ }
+ },
+ {
+ "id": "canonical_evidence_serializes",
+ "kind": "blossom.publication_readiness_evidence.to_canonical_json.valid",
+ "input": {
+ "mutation": "none"
+ },
+ "expected": {
+ "policy_version": 1,
+ "schema_version": 1
+ }
+ },
+ {
+ "id": "leading_whitespace_is_noncanonical",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "leading_whitespace"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_json_non_canonical"
+ }
+ },
+ {
+ "id": "noncanonical_string_escaping_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "noncanonical_escaping"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_json_non_canonical"
+ }
+ },
+ {
+ "id": "field_reordering_is_noncanonical",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "reordered_fields"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_json_non_canonical"
+ }
+ },
+ {
+ "id": "unknown_field_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "unknown_field"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "private_bud11_field_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "bud11_authorization"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "missing_field_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "missing_uploaded"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "duplicate_field_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "duplicate_schema_version"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "wrong_field_type_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "size_wrong_type"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_invalid_json"
+ }
+ },
+ {
+ "id": "schema_version_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "schema_version"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_schema_version_unsupported"
+ }
+ },
+ {
+ "id": "policy_version_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "policy_version"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_policy_version_unsupported"
+ }
+ },
+ {
+ "id": "url_must_be_canonical",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "url_invalid"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "url_exact_maximum_is_valid",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "input": {
+ "mutation": "url_exact_max"
+ },
+ "expected": {
+ "policy_version": 1,
+ "schema_version": 1
+ }
+ },
+ {
+ "id": "url_one_over_maximum_is_rejected",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "url_over_max"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "url_hash_must_match_sha256",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "url_hash_mismatch"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "sha256_must_be_lower_hex",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "sha256_invalid"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "size_must_be_nonzero",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "size_zero"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "size_must_be_bounded",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "size_over_max"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "mime_must_be_supported",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "mime_unsupported"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "format_must_match_mime",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "format_mismatch"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "dimensions_must_be_nonzero",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "width_zero"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "dimensions_must_respect_axis_limit",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "height_over_max"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "dimensions_must_respect_pixel_limit",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "pixels_over_max"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "bud02_status_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "bud02_status"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "bud01_head_status_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "bud01_head_status"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "bud01_get_status_is_strict",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "bud01_get_status"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "uploaded_mutation_breaks_digest",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "uploaded"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_digest_mismatch"
+ }
+ },
+ {
+ "id": "digest_must_be_lower_hex",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "digest_invalid"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_field_invalid"
+ }
+ },
+ {
+ "id": "digest_must_match_all_facts",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "digest_mismatch"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_digest_mismatch"
+ }
+ },
+ {
+ "id": "input_is_bounded_before_parse",
+ "kind": "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "input": {
+ "mutation": "oversized_input"
+ },
+ "expected": {
+ "error": "publication_readiness_evidence_too_large"
+ }
+ }
+ ]
+}
diff --git a/crates/blossom/tests/publication_readiness.rs b/crates/blossom/tests/publication_readiness.rs
@@ -2,10 +2,14 @@
use image::{ExtendedColorType, ImageEncoder, codecs::webp::WebPEncoder};
use radroots_blossom::{
- RadrootsBlossomApprovedBlobUrl, RadrootsBlossomAuthoredRasterDimensions,
- RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomBud01GetObservation,
- RadrootsBlossomBud01HeadObservation, RadrootsBlossomBud02UploadObservation,
- RadrootsBlossomError, RadrootsBlossomMediaType, RadrootsBlossomRasterDimensions,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES, RadrootsBlossomApprovedBlobUrl,
+ RadrootsBlossomAuthoredRasterDimensions, RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl,
+ RadrootsBlossomBud01GetObservation, RadrootsBlossomBud01HeadObservation,
+ RadrootsBlossomBud02UploadObservation, RadrootsBlossomError, RadrootsBlossomMediaType,
+ RadrootsBlossomPublicationReadinessEvidence, RadrootsBlossomRasterDimensions,
RadrootsBlossomRasterFormat, RadrootsBlossomSha256, verify_publication_readiness,
};
use serde::Deserialize;
@@ -80,6 +84,240 @@ fn publication_readiness_accepts_public_jpeg_and_still_webp() {
}
#[test]
+fn publication_readiness_evidence_round_trips_only_through_strict_canonical_json() {
+ let bytes = canonical_png();
+ let evidence = verify_public_raster(
+ &bytes,
+ "image/png",
+ "png",
+ RadrootsBlossomAuthoredRasterDimensions::Exact(
+ RadrootsBlossomRasterDimensions::new(1, 1).unwrap(),
+ ),
+ )
+ .unwrap();
+ let canonical = evidence.to_canonical_json().unwrap();
+ assert!(canonical.len() <= RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES);
+
+ let reloaded = RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(&canonical)
+ .expect("canonical evidence must reload");
+ assert_eq!(reloaded, evidence);
+ assert_eq!(
+ reloaded.schema_version(),
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION
+ );
+ assert_eq!(
+ reloaded.policy_version(),
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION
+ );
+ assert_eq!(reloaded.to_canonical_json().unwrap(), canonical);
+
+ let pretty = serde_json::to_vec_pretty(
+ &serde_json::from_slice::<Value>(&canonical).expect("canonical evidence JSON"),
+ )
+ .unwrap();
+ assert_eq!(
+ RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(&pretty)
+ .unwrap_err()
+ .code(),
+ "publication_readiness_evidence_json_non_canonical"
+ );
+
+ let mut unknown = canonical.clone();
+ unknown.pop();
+ unknown.extend_from_slice(br#","authorization":"Nostr token"}"#);
+ assert_eq!(
+ RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(&unknown)
+ .unwrap_err()
+ .code(),
+ "publication_readiness_evidence_invalid_json"
+ );
+
+ let oversized = vec![b' '; RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES + 1];
+ assert_eq!(
+ RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(&oversized)
+ .unwrap_err()
+ .code(),
+ "publication_readiness_evidence_too_large"
+ );
+}
+
+#[test]
+fn publication_readiness_evidence_revalidates_every_persisted_fact() {
+ let evidence = verify_public_raster(
+ &canonical_png(),
+ "image/png",
+ "png",
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ )
+ .unwrap();
+ let canonical = evidence.to_canonical_json().unwrap();
+
+ let cases = [
+ (
+ "schema_version",
+ serde_json::json!(2),
+ "publication_readiness_evidence_schema_version_unsupported",
+ ),
+ (
+ "policy_version",
+ serde_json::json!(2),
+ "publication_readiness_evidence_policy_version_unsupported",
+ ),
+ (
+ "size",
+ serde_json::json!(0),
+ "publication_readiness_evidence_field_invalid",
+ ),
+ (
+ "media_type",
+ serde_json::json!("image/jpeg"),
+ "publication_readiness_evidence_field_invalid",
+ ),
+ (
+ "raster_format",
+ serde_json::json!("jpeg"),
+ "publication_readiness_evidence_field_invalid",
+ ),
+ (
+ "bud02_status",
+ serde_json::json!(202),
+ "publication_readiness_evidence_field_invalid",
+ ),
+ (
+ "bud01_head_status",
+ serde_json::json!(204),
+ "publication_readiness_evidence_field_invalid",
+ ),
+ (
+ "bud01_get_status",
+ serde_json::json!(206),
+ "publication_readiness_evidence_field_invalid",
+ ),
+ (
+ "uploaded",
+ serde_json::json!(1_800_000_002_u64),
+ "publication_readiness_evidence_digest_mismatch",
+ ),
+ (
+ "evidence_digest",
+ serde_json::json!("00".repeat(32)),
+ "publication_readiness_evidence_digest_mismatch",
+ ),
+ ];
+ for (field, value, expected) in cases {
+ let mut wire: Value = serde_json::from_slice(&canonical).unwrap();
+ wire[field] = value;
+ let mutated = serde_json::to_vec(&wire).unwrap();
+ assert_eq!(
+ RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(&mutated)
+ .unwrap_err()
+ .code(),
+ expected,
+ "{field}"
+ );
+ }
+
+ let mut dimensions: Value = serde_json::from_slice(&canonical).unwrap();
+ dimensions["dimensions"]["width"] = serde_json::json!(0);
+ assert_eq!(
+ RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(
+ &serde_json::to_vec(&dimensions).unwrap(),
+ )
+ .unwrap_err()
+ .code(),
+ "publication_readiness_evidence_field_invalid"
+ );
+}
+
+#[test]
+fn publication_readiness_enforces_nonempty_bytes_and_bounded_urls() {
+ let empty_hash = RadrootsBlossomSha256::digest(&[]);
+ let empty_url = format!("https://cdn.example/{empty_hash}.png");
+ let empty_media_type = RadrootsBlossomMediaType::parse("image/png").unwrap();
+ let empty_descriptor = RadrootsBlossomBlobDescriptor::new(
+ RadrootsBlossomBlobUrl::parse(&empty_url).unwrap(),
+ empty_hash,
+ 0,
+ empty_media_type.clone(),
+ 1_800_000_000,
+ )
+ .unwrap()
+ .approve_reference()
+ .unwrap()
+ .verify_bytes(&[], &empty_media_type)
+ .unwrap();
+ let empty_upload = RadrootsBlossomBud02UploadObservation::new(
+ 201,
+ RadrootsBlossomBlobDescriptor::new(
+ RadrootsBlossomBlobUrl::parse(&empty_url).unwrap(),
+ empty_hash,
+ 0,
+ empty_media_type.clone(),
+ 1_800_000_001,
+ )
+ .unwrap(),
+ )
+ .unwrap();
+ let empty_approved_url = RadrootsBlossomBlobUrl::parse(&empty_url)
+ .unwrap()
+ .approve()
+ .unwrap();
+ let empty_head = RadrootsBlossomBud01HeadObservation::new(
+ 200,
+ empty_approved_url.clone(),
+ 0,
+ empty_media_type,
+ )
+ .unwrap();
+ let nonempty_get =
+ RadrootsBlossomBud01GetObservation::from_complete_body(200, empty_approved_url, 1, &[0])
+ .unwrap();
+ assert_eq!(
+ verify_publication_readiness(
+ &empty_descriptor,
+ &[],
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ &empty_upload,
+ &empty_head,
+ &nonempty_get,
+ )
+ .unwrap_err()
+ .code(),
+ "publication_raster_empty"
+ );
+
+ let bytes = canonical_png();
+ let hash = RadrootsBlossomSha256::digest(&bytes);
+ let prefix = format!("https://cdn.example/{hash}.");
+ let exact_extension =
+ "p".repeat(RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES - prefix.len());
+ let evidence = verify_public_raster(
+ &bytes,
+ "image/png",
+ &exact_extension,
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ )
+ .unwrap();
+ assert_eq!(
+ evidence.url().as_str().len(),
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES
+ );
+
+ let over_extension = format!("{exact_extension}p");
+ assert_eq!(
+ verify_public_raster(
+ &bytes,
+ "image/png",
+ &over_extension,
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ )
+ .unwrap_err()
+ .code(),
+ "publication_readiness_url_too_large"
+ );
+}
+
+#[test]
fn publication_readiness_rejects_forbidden_and_corrupt_jpeg_and_animated_rasters() {
let jpeg = encoded_jpeg();
let scan = jpeg
@@ -171,6 +409,13 @@ fn encoded_jpeg() -> Vec<u8> {
.unwrap()
}
+fn canonical_png() -> Vec<u8> {
+ hex::decode(
+ "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ )
+ .unwrap()
+}
+
fn encoded_still_webp() -> Vec<u8> {
let mut bytes = Vec::new();
WebPEncoder::new_lossless(&mut bytes)
diff --git a/crates/blossom/tests/publication_readiness_persistence.rs b/crates/blossom/tests/publication_readiness_persistence.rs
@@ -0,0 +1,306 @@
+#![cfg(feature = "serde")]
+
+use radroots_blossom::{
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES,
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES,
+ RadrootsBlossomPublicationReadinessEvidence,
+};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+
+const CANONICAL_VECTOR: &[u8] = include_bytes!(
+ "../../../contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json"
+);
+const PACKAGED_VECTOR: &[u8] = include_bytes!("fixtures/publication_readiness_persistence.v1.json");
+const CANONICAL_EVIDENCE: &str = concat!(
+ "{\"schema_version\":1,\"policy_version\":1,",
+ "\"url\":\"https://cdn.example/",
+ "4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd.png\",",
+ "\"sha256\":\"4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd\",",
+ "\"size\":70,\"media_type\":\"image/png\",\"raster_format\":\"png\",",
+ "\"dimensions\":{\"width\":1,\"height\":1},",
+ "\"bud02_status\":201,\"bud01_head_status\":200,\"bud01_get_status\":200,",
+ "\"uploaded\":1800000001,",
+ "\"evidence_digest\":\"637ac3e9ffbb00fbacb60dcf98b466c949f99fd9f608e163127bea970b91670c\"}"
+);
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct VectorSuite {
+ suite: String,
+ contract_version: String,
+ vectors: Vec<VectorCase>,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct VectorCase {
+ id: String,
+ kind: String,
+ input: VectorInput,
+ expected: VectorExpected,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct VectorInput {
+ mutation: String,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct VectorExpected {
+ #[serde(default)]
+ error: Option<String>,
+ #[serde(default)]
+ policy_version: Option<u16>,
+ #[serde(default)]
+ schema_version: Option<u32>,
+}
+
+#[derive(Serialize)]
+struct EvidenceDimensionsWire {
+ width: u32,
+ height: u32,
+}
+
+#[derive(Serialize)]
+struct EvidenceWire<'a> {
+ schema_version: u32,
+ policy_version: u16,
+ url: &'a str,
+ sha256: &'a str,
+ size: u64,
+ media_type: &'a str,
+ raster_format: &'a str,
+ dimensions: EvidenceDimensionsWire,
+ bud02_status: u16,
+ bud01_head_status: u16,
+ bud01_get_status: u16,
+ uploaded: u64,
+ evidence_digest: String,
+}
+
+#[test]
+fn publication_readiness_persistence_vector_executes_every_case() {
+ assert_eq!(CANONICAL_VECTOR, PACKAGED_VECTOR, "packaged vector drift");
+ let suite: VectorSuite = serde_json::from_slice(PACKAGED_VECTOR).expect("persistence vector");
+ assert_eq!(suite.suite, "blossom_publication_readiness_persistence");
+ assert_eq!(suite.contract_version, "1.0.0");
+ assert_eq!(suite.vectors.len(), 31);
+
+ for vector in suite.vectors {
+ let bytes = mutated_evidence(&vector.input.mutation);
+ match vector.kind.as_str() {
+ "blossom.publication_readiness_evidence.from_canonical_json.valid" => {
+ let evidence =
+ RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(&bytes)
+ .unwrap_or_else(|error| panic!("{}: {error}", vector.id));
+ assert_eq!(
+ Some(evidence.schema_version()),
+ vector.expected.schema_version
+ );
+ assert_eq!(
+ Some(evidence.policy_version()),
+ vector.expected.policy_version
+ );
+ assert_eq!(evidence.to_canonical_json().unwrap(), bytes);
+ assert!(vector.expected.error.is_none(), "{}", vector.id);
+ }
+ "blossom.publication_readiness_evidence.to_canonical_json.valid" => {
+ let evidence =
+ RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(&bytes)
+ .unwrap_or_else(|error| panic!("{}: {error}", vector.id));
+ assert_eq!(evidence.to_canonical_json().unwrap(), bytes);
+ assert_eq!(
+ Some(evidence.schema_version()),
+ vector.expected.schema_version
+ );
+ assert_eq!(
+ Some(evidence.policy_version()),
+ vector.expected.policy_version
+ );
+ assert!(vector.expected.error.is_none(), "{}", vector.id);
+ }
+ "blossom.publication_readiness_evidence.from_canonical_json.invalid" => {
+ let error =
+ RadrootsBlossomPublicationReadinessEvidence::from_canonical_json(&bytes)
+ .unwrap_err();
+ assert_eq!(
+ Some(error.code()),
+ vector.expected.error.as_deref(),
+ "{}",
+ vector.id
+ );
+ assert!(vector.expected.schema_version.is_none(), "{}", vector.id);
+ assert!(vector.expected.policy_version.is_none(), "{}", vector.id);
+ }
+ kind => panic!("{} has unsupported vector kind {kind}", vector.id),
+ }
+ }
+}
+
+fn mutated_evidence(mutation: &str) -> Vec<u8> {
+ let canonical = CANONICAL_EVIDENCE.as_bytes();
+ match mutation {
+ "none" => canonical.to_vec(),
+ "leading_whitespace" => [b" ".as_slice(), canonical].concat(),
+ "noncanonical_escaping" => replace_once(
+ canonical,
+ b"https://cdn.example/",
+ br"https:\/\/cdn.example/",
+ ),
+ "reordered_fields" => replace_once(
+ canonical,
+ b"{\"schema_version\":1,\"policy_version\":1",
+ b"{\"policy_version\":1,\"schema_version\":1",
+ ),
+ "unknown_field" => append_field(canonical, b",\"unknown\":true"),
+ "bud11_authorization" => append_field(canonical, b",\"authorization\":\"Nostr token\""),
+ "missing_uploaded" => replace_once(canonical, b",\"uploaded\":1800000001", b""),
+ "duplicate_schema_version" => replace_once(canonical, b"{", b"{\"schema_version\":1,"),
+ "size_wrong_type" => replace_once(canonical, b"\"size\":70", b"\"size\":\"70\""),
+ "schema_version" => {
+ replace_once(canonical, b"\"schema_version\":1", b"\"schema_version\":2")
+ }
+ "policy_version" => {
+ replace_once(canonical, b"\"policy_version\":1", b"\"policy_version\":2")
+ }
+ "url_invalid" => replace_once(canonical, b"https://cdn.example/", b"http://cdn.example/"),
+ "url_exact_max" => evidence_for_url(&url_with_length(
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES,
+ )),
+ "url_over_max" => evidence_for_url(&url_with_length(
+ RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES + 1,
+ )),
+ "url_hash_mismatch" => replace_once(
+ canonical,
+ b"https://cdn.example/449013",
+ b"https://cdn.example/049013",
+ ),
+ "sha256_invalid" => replace_once(canonical, b"\"sha256\":\"449013", b"\"sha256\":\"Z49013"),
+ "size_zero" => replace_once(canonical, b"\"size\":70", b"\"size\":0"),
+ "size_over_max" => replace_once(canonical, b"\"size\":70", b"\"size\":10485761"),
+ "mime_unsupported" => replace_once(
+ canonical,
+ b"\"media_type\":\"image/png\"",
+ b"\"media_type\":\"image/gif\"",
+ ),
+ "format_mismatch" => replace_once(
+ canonical,
+ b"\"raster_format\":\"png\"",
+ b"\"raster_format\":\"jpeg\"",
+ ),
+ "width_zero" => replace_once(canonical, b"\"width\":1", b"\"width\":0"),
+ "height_over_max" => replace_once(canonical, b"\"height\":1", b"\"height\":16385"),
+ "pixels_over_max" => replace_once(
+ canonical,
+ b"\"width\":1,\"height\":1",
+ b"\"width\":5000,\"height\":5000",
+ ),
+ "bud02_status" => replace_once(canonical, b"\"bud02_status\":201", b"\"bud02_status\":202"),
+ "bud01_head_status" => replace_once(
+ canonical,
+ b"\"bud01_head_status\":200",
+ b"\"bud01_head_status\":204",
+ ),
+ "bud01_get_status" => replace_once(
+ canonical,
+ b"\"bud01_get_status\":200",
+ b"\"bud01_get_status\":206",
+ ),
+ "uploaded" => replace_once(
+ canonical,
+ b"\"uploaded\":1800000001",
+ b"\"uploaded\":1800000002",
+ ),
+ "digest_invalid" => replace_once(
+ canonical,
+ b"\"evidence_digest\":\"637ac3",
+ b"\"evidence_digest\":\"Z37ac3",
+ ),
+ "digest_mismatch" => replace_once(
+ canonical,
+ b"\"evidence_digest\":\"637ac3",
+ b"\"evidence_digest\":\"037ac3",
+ ),
+ "oversized_input" => {
+ vec![b' '; RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES + 1]
+ }
+ other => panic!("unsupported persistence mutation {other}"),
+ }
+}
+
+fn url_with_length(length: usize) -> String {
+ const HASH: &str = "4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd";
+ let prefix = format!("https://cdn.example/{HASH}.");
+ assert!(length >= prefix.len());
+ let url = format!("{prefix}{}", "p".repeat(length - prefix.len()));
+ assert_eq!(url.len(), length);
+ url
+}
+
+fn evidence_for_url(url: &str) -> Vec<u8> {
+ const HASH: &str = "4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd";
+ let evidence = EvidenceWire {
+ schema_version: 1,
+ policy_version: 1,
+ url,
+ sha256: HASH,
+ size: 70,
+ media_type: "image/png",
+ raster_format: "png",
+ dimensions: EvidenceDimensionsWire {
+ width: 1,
+ height: 1,
+ },
+ bud02_status: 201,
+ bud01_head_status: 200,
+ bud01_get_status: 200,
+ uploaded: 1_800_000_001,
+ evidence_digest: readiness_evidence_digest(url, HASH),
+ };
+ serde_json::to_vec(&evidence).unwrap()
+}
+
+fn readiness_evidence_digest(url: &str, sha256: &str) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(b"radroots.blossom.publication-readiness-evidence.v1\0");
+ hasher.update(1_u16.to_be_bytes());
+ update_length_prefixed(&mut hasher, url.as_bytes());
+ hasher.update(hex::decode(sha256).unwrap());
+ hasher.update(70_u64.to_be_bytes());
+ update_length_prefixed(&mut hasher, b"image/png");
+ hasher.update([2]);
+ hasher.update(1_u32.to_be_bytes());
+ hasher.update(1_u32.to_be_bytes());
+ hasher.update(201_u16.to_be_bytes());
+ hasher.update(200_u16.to_be_bytes());
+ hasher.update(200_u16.to_be_bytes());
+ hasher.update(1_800_000_001_u64.to_be_bytes());
+ hex::encode(hasher.finalize())
+}
+
+fn update_length_prefixed(hasher: &mut Sha256, bytes: &[u8]) {
+ hasher.update((bytes.len() as u64).to_be_bytes());
+ hasher.update(bytes);
+}
+
+fn append_field(canonical: &[u8], field: &[u8]) -> Vec<u8> {
+ let mut output = canonical[..canonical.len() - 1].to_vec();
+ output.extend_from_slice(field);
+ output.push(b'}');
+ output
+}
+
+fn replace_once(input: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
+ let index = input
+ .windows(from.len())
+ .position(|candidate| candidate == from)
+ .unwrap_or_else(|| panic!("missing mutation source {}", String::from_utf8_lossy(from)));
+ let mut output = Vec::with_capacity(input.len() - from.len() + to.len());
+ output.extend_from_slice(&input[..index]);
+ output.extend_from_slice(to);
+ output.extend_from_slice(&input[index + from.len()..]);
+ output
+}
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -20,6 +20,9 @@ mod registry_v7;
mod release_provenance;
mod source_maintenance;
+pub(crate) use blossom_publication_readiness::{
+ validate_blossom_publication_readiness_manifest, write_blossom_publication_readiness_manifest,
+};
pub(crate) use food_availability_projection::{
validate_food_availability_projection_manifest, write_food_availability_projection_manifest,
};
@@ -123,7 +126,7 @@ const REPLICA_CONTRACT_RELATIVE: &str = "contracts/replica.toml";
const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract";
const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION";
const REPLICA_TRANSFER_VERSION: u32 = 2;
-const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 26] = [
+const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 27] = [
(
"contracts/conformance/vectors/blossom/bud11_claims.v1.json",
"crates/blossom/tests/fixtures/bud11_claims.v1.json",
@@ -137,6 +140,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 26] = [
"crates/blossom/tests/fixtures/publication_readiness.v1.json",
),
(
+ "contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json",
+ "crates/blossom/tests/fixtures/publication_readiness_persistence.v1.json",
+ ),
+ (
"contracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json",
"crates/nostr/tests/fixtures/bud11_nostr_adapter.v1.json",
),
diff --git a/tools/xtask/src/contract/blossom_publication_readiness.rs b/tools/xtask/src/contract/blossom_publication_readiness.rs
@@ -1,1012 +1,1438 @@
-use super::artifact_bundle::{read_regular_file, with_artifact_bundle_transaction};
-use super::raw_source_rebuild::validate_raw_source_rebuild_predecessor_production_sources_under_lock;
-use serde_json::Value;
+use super::{
+ artifact_bundle::{GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction},
+ raw_source_rebuild::validate_raw_source_rebuild_predecessor_production_sources_under_lock,
+};
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
use sha2::{Digest, Sha256};
-use std::collections::BTreeSet;
-use std::fs;
-use std::path::Path;
+use std::{collections::BTreeSet, path::Path};
+use syn::{ImplItem, Item, Visibility};
-const VECTOR_CANONICAL_RELATIVE: &str =
+const SCHEMA_VERSION: u32 = 1;
+const CONTRACT_ID: &str = "radroots_blossom.publication_readiness_v1";
+const AUTHORITY_ID: &str = "blossom_publication_readiness_v1";
+const HASH_ALGORITHM: &str = "sha256_bytes_v1";
+const WRITE_COMMAND: &str = "cargo xtask contract blossom-publication-readiness-manifest --write";
+
+const MANIFEST_RELATIVE: &str = "crates/blossom/contracts/publication_readiness_v1.manifest.json";
+const MANIFEST_SCHEMA_RELATIVE: &str =
+ "crates/blossom/contracts/publication_readiness_v1.manifest.schema.json";
+const MANIFEST_SHA256_RELATIVE: &str =
+ "crates/blossom/contracts/publication_readiness_v1.manifest.sha256";
+const GENERATED_DESCRIPTOR_RELATIVE: &str =
+ "crates/blossom/contracts/publication_readiness_v1.descriptor.json";
+const EVIDENCE_SCHEMA_RELATIVE: &str =
+ "crates/blossom/contracts/publication_readiness_evidence_v1.schema.json";
+const BEHAVIOR_VECTOR_RELATIVE: &str =
"contracts/conformance/vectors/blossom/publication_readiness.v1.json";
-const VECTOR_MIRROR_RELATIVE: &str = "crates/blossom/tests/fixtures/publication_readiness.v1.json";
-const WORKSPACE_MANIFEST_RELATIVE: &str = "Cargo.toml";
-const WORKSPACE_LOCK_RELATIVE: &str = "Cargo.lock";
+const BEHAVIOR_VECTOR_MIRROR_RELATIVE: &str =
+ "crates/blossom/tests/fixtures/publication_readiness.v1.json";
+const BEHAVIOR_VECTOR_EXECUTOR_RELATIVE: &str = "crates/blossom/tests/publication_readiness.rs";
+const BEHAVIOR_VECTOR_EXECUTOR_TEST: &str =
+ "publication_readiness_conformance_vector_executes_every_case";
+const PERSISTENCE_VECTOR_RELATIVE: &str =
+ "contracts/conformance/vectors/blossom/publication_readiness_persistence.v1.json";
+const PERSISTENCE_VECTOR_MIRROR_RELATIVE: &str =
+ "crates/blossom/tests/fixtures/publication_readiness_persistence.v1.json";
+const PERSISTENCE_VECTOR_EXECUTOR_RELATIVE: &str =
+ "crates/blossom/tests/publication_readiness_persistence.rs";
+const PERSISTENCE_VECTOR_EXECUTOR_TEST: &str =
+ "publication_readiness_persistence_vector_executes_every_case";
const READINESS_SOURCE_RELATIVE: &str = "crates/blossom/src/publication_readiness.rs";
-const SEQUENTIAL_JPEG_SOURCE_RELATIVE: &str =
- "crates/blossom/src/publication_readiness/sequential_jpeg.rs";
const BLOSSOM_LIB_RELATIVE: &str = "crates/blossom/src/lib.rs";
-const BLOSSOM_URL_RELATIVE: &str = "crates/blossom/src/url.rs";
const BLOSSOM_MANIFEST_RELATIVE: &str = "crates/blossom/Cargo.toml";
-const COVERAGE_PROFILES_RELATIVE: &str = "contracts/coverage-profiles.toml";
-const NIX_COMMON_RELATIVE: &str = "build/nix/common.nix";
-const NIX_CHECKS_RELATIVE: &str = "build/nix/checks.nix";
const OPERATIONS_RELATIVE: &str = "contracts/operations.toml";
const RELEASE_RELATIVE: &str = "contracts/releases/1.0.0-alpha.1.toml";
const CHANGELOG_RELATIVE: &str = "CHANGELOG.md";
-const RAW_PREDECESSOR_GOVERNANCE_RELATIVE: &str = "tools/xtask/src/contract/raw_source_rebuild.rs";
const RELEASE_CHANGE_ID: &str = "blossom-publication-readiness-evidence";
const CHANGELOG_MARKER: &str = "<!-- release-change: blossom-publication-readiness-evidence -->";
+const VERIFY_OPERATION_ID: &str = "blossom.verify_publication_readiness";
+const SERIALIZE_OPERATION_ID: &str = "blossom.publication_readiness_evidence.to_canonical_json";
+const RELOAD_OPERATION_ID: &str = "blossom.publication_readiness_evidence.from_canonical_json";
+const EVIDENCE_DIGEST_DOMAIN: &str = "radroots.blossom.publication-readiness-evidence.v1\0";
+
+const EVIDENCE_SCHEMA_VERSION: u32 = 1;
+const READINESS_POLICY_VERSION: u16 = 1;
+const EVIDENCE_MAX_BYTES: usize = 8 * 1024;
+const URL_MAX_BYTES: usize = 4 * 1024;
+const RASTER_MAX_BYTES: u64 = 10_485_760;
+const RASTER_MAX_DECODED_BYTES: u64 = 160_000_000;
+const RASTER_MAX_DIMENSION: u32 = 16_384;
+const RASTER_MAX_PIXELS: u64 = 20_000_000;
+
+const PUBLIC_CONSTANTS: &[&str] = &[
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_MAX_BYTES",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_EVIDENCE_SCHEMA_VERSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION",
+ "RADROOTS_BLOSSOM_PUBLICATION_READINESS_URL_MAX_BYTES",
+];
+const PUBLIC_TYPES: &[&str] = &[
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomBud01GetCollector",
+ "RadrootsBlossomBud01GetObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud02UploadStatus",
+ "RadrootsBlossomPublicationReadinessEvidence",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest",
+ "RadrootsBlossomRasterDimensions",
+ "RadrootsBlossomRasterFormat",
+];
+const PUBLIC_FUNCTIONS: &[&str] = &["verify_publication_readiness"];
+const PUBLIC_METHODS: &[&str] = &[
+ "RadrootsBlossomBud01GetCollector::finish",
+ "RadrootsBlossomBud01GetCollector::new",
+ "RadrootsBlossomBud01GetCollector::push_chunk",
+ "RadrootsBlossomBud01GetObservation::bytes",
+ "RadrootsBlossomBud01GetObservation::declared_size",
+ "RadrootsBlossomBud01GetObservation::from_complete_body",
+ "RadrootsBlossomBud01GetObservation::url",
+ "RadrootsBlossomBud01HeadObservation::content_length",
+ "RadrootsBlossomBud01HeadObservation::media_type",
+ "RadrootsBlossomBud01HeadObservation::new",
+ "RadrootsBlossomBud01HeadObservation::url",
+ "RadrootsBlossomBud02UploadObservation::descriptor",
+ "RadrootsBlossomBud02UploadObservation::new",
+ "RadrootsBlossomBud02UploadObservation::status",
+ "RadrootsBlossomBud02UploadStatus::as_u16",
+ "RadrootsBlossomPublicationReadinessEvidence::bud02_status",
+ "RadrootsBlossomPublicationReadinessEvidence::dimensions",
+ "RadrootsBlossomPublicationReadinessEvidence::evidence_digest",
+ "RadrootsBlossomPublicationReadinessEvidence::from_canonical_json",
+ "RadrootsBlossomPublicationReadinessEvidence::media_type",
+ "RadrootsBlossomPublicationReadinessEvidence::policy_version",
+ "RadrootsBlossomPublicationReadinessEvidence::raster_format",
+ "RadrootsBlossomPublicationReadinessEvidence::schema_version",
+ "RadrootsBlossomPublicationReadinessEvidence::sha256",
+ "RadrootsBlossomPublicationReadinessEvidence::size",
+ "RadrootsBlossomPublicationReadinessEvidence::to_canonical_json",
+ "RadrootsBlossomPublicationReadinessEvidence::uploaded",
+ "RadrootsBlossomPublicationReadinessEvidence::url",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest::as_sha256",
+ "RadrootsBlossomRasterDimensions::height",
+ "RadrootsBlossomRasterDimensions::new",
+ "RadrootsBlossomRasterDimensions::pixels",
+ "RadrootsBlossomRasterDimensions::width",
+ "RadrootsBlossomRasterFormat::as_str",
+ "RadrootsBlossomRasterFormat::from_media_type",
+];
+const SEALED_TYPES: &[&str] = &[
+ "RadrootsBlossomPublicationReadinessEvidence",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest",
+];
+const PRIVATE_WIRE_TYPES: &[&str] = &[
+ "PublicationReadinessDimensionsWire",
+ "PublicationReadinessEvidenceWire",
+];
+const WIRE_FIELD_ORDER: &[&str] = &[
+ "schema_version",
+ "policy_version",
+ "url",
+ "sha256",
+ "size",
+ "media_type",
+ "raster_format",
+ "dimensions",
+ "bud02_status",
+ "bud01_head_status",
+ "bud01_get_status",
+ "uploaded",
+ "evidence_digest",
+];
+const SEMANTIC_INVARIANTS: &[&str] = &[
+ "bounded_input_before_json_parse_v1",
+ "fixed_compact_json_field_order_v1",
+ "canonical_json_round_trip_required_v1",
+ "sealed_evidence_without_deserialize_v1",
+ "private_deny_unknown_fields_wire_models_v1",
+ "canonical_hash_path_url_max_4096_utf8_bytes_v1",
+ "closed_jpeg_png_still_webp_mime_and_format_v1",
+ "nonzero_size_max_10485760_bytes_v1",
+ "nonzero_dimensions_axis_max_16384_pixels_max_20000000_v1",
+ "bud02_status_200_or_201_and_bud01_status_200_v1",
+ "descriptor_head_get_complete_byte_agreement_v1",
+ "private_domain_separated_digest_derivation_v1",
+ "no_bud11_credentials_entitlement_or_topology_persistence_v1",
+];
+const DECODE_ERROR_CODES: &[&str] = &[
+ "publication_readiness_evidence_too_large",
+ "publication_readiness_evidence_invalid_json",
+ "publication_readiness_evidence_schema_version_unsupported",
+ "publication_readiness_evidence_policy_version_unsupported",
+ "publication_readiness_evidence_field_invalid",
+ "publication_readiness_evidence_json_non_canonical",
+ "publication_readiness_evidence_digest_mismatch",
+];
+const PROTOCOL_SOURCE_PINS: &[(&str, &str, &str)] = &[(
+ "blossom",
+ "https://github.com/hzrd149/blossom",
+ "b5bd2801d1763aa635fc8fea7a76597e0eb18990",
+)];
+
const RAW_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[
- WORKSPACE_LOCK_RELATIVE,
- WORKSPACE_MANIFEST_RELATIVE,
- NIX_COMMON_RELATIVE,
+ "Cargo.lock",
+ "Cargo.toml",
+ "build/nix/common.nix",
CHANGELOG_RELATIVE,
RELEASE_RELATIVE,
"tools/xtask/src/contract.rs",
"tools/xtask/src/contract/food_availability_projection.rs",
"tools/xtask/src/contract/nip09_reconciliation.rs",
- RAW_PREDECESSOR_GOVERNANCE_RELATIVE,
+ "tools/xtask/src/contract/raw_source_rebuild.rs",
"tools/xtask/src/contract/source_maintenance.rs",
"tools/xtask/src/main.rs",
];
const TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[
- WORKSPACE_MANIFEST_RELATIVE,
- BLOSSOM_MANIFEST_RELATIVE,
- "crates/blossom/src/error.rs",
- BLOSSOM_LIB_RELATIVE,
- BLOSSOM_URL_RELATIVE,
-];
-
-const SOURCE_INVENTORY: &[&str] = &[
- WORKSPACE_LOCK_RELATIVE,
- WORKSPACE_MANIFEST_RELATIVE,
- NIX_CHECKS_RELATIVE,
- NIX_COMMON_RELATIVE,
- CHANGELOG_RELATIVE,
+ "Cargo.toml",
BLOSSOM_MANIFEST_RELATIVE,
- "crates/blossom/README",
"crates/blossom/src/error.rs",
BLOSSOM_LIB_RELATIVE,
- READINESS_SOURCE_RELATIVE,
- SEQUENTIAL_JPEG_SOURCE_RELATIVE,
- BLOSSOM_URL_RELATIVE,
- "crates/blossom/tests/publication_readiness.rs",
- VECTOR_MIRROR_RELATIVE,
- "contracts/events/blossom-media.md",
- COVERAGE_PROFILES_RELATIVE,
- VECTOR_CANONICAL_RELATIVE,
- OPERATIONS_RELATIVE,
- RELEASE_RELATIVE,
- "tools/xtask/src/contract.rs",
- "tools/xtask/src/main.rs",
- "tools/xtask/src/contract/blossom_publication_readiness.rs",
- "tools/xtask/src/contract/food_availability_projection.rs",
- "tools/xtask/src/contract/nip09_reconciliation.rs",
- RAW_PREDECESSOR_GOVERNANCE_RELATIVE,
- "tools/xtask/src/contract/source_maintenance.rs",
+ "crates/blossom/src/url.rs",
];
-const IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS: &[(&str, usize, &str)] = &[
- (
+const IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS: &[ImmutableArtifactSpec] = &[
+ ImmutableArtifactSpec::new(
"crates/event_store/contracts/raw_source_rebuild_v1.manifest.json",
45_449,
"03253ce31dc31d465880a895d2685f5deb1274948e0a4eabe81a2f08f238c483",
),
- (
+ ImmutableArtifactSpec::new(
"crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json",
17_896,
"f9d210967e54b66f39c8bb965d97b2001a0ebc0927e7c2c14edb8e474bfda695",
),
- (
+ ImmutableArtifactSpec::new(
"crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256",
65,
"2b8bc07cd479be2281781660efd26fd7a8f480e5f3f62053aeccd2b5e6b2070c",
),
- (
+ ImmutableArtifactSpec::new(
"crates/event_store/src/generated/raw_source_rebuild_manifest.rs",
50_735,
"3763fbee3ee45621afca990002b9298c791bf0396ebf7bccde0ae1bc9aecb7f2",
),
- (
+ ImmutableArtifactSpec::new(
"contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json",
26_833,
"c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15",
),
- (
+ ImmutableArtifactSpec::new(
"crates/event_store/tests/fixtures/raw_source_rebuild.v1.json",
26_833,
"c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15",
),
];
-const CURRENT_BYTE_BOUND_BLOSSOM_SOURCES: &[(&str, usize, &str)] = &[
- (
- BLOSSOM_LIB_RELATIVE,
- 2_172,
- "97cae38f693795445cc17671649f3d88c0c492fc8d71d2c98a4ca02502e5d43a",
- ),
- (
- "crates/blossom/src/error.rs",
- 30_918,
- "bd77810306b3556434d93057ca5ee62db474e9b0af94176ba4aa7a2ef25be7d8",
- ),
- (
- BLOSSOM_URL_RELATIVE,
- 15_794,
- "e9673f074ba6328a121aa3008fc11cd4d9d22cae3b09f26602ea6fea3f964c80",
- ),
+const BEHAVIOR_CASE_IDS: &[&str] = &[
+ "valid_created",
+ "valid_ok_without_authored_dimensions",
+ "invalid_upload_status",
+ "invalid_head_status",
+ "invalid_get_status",
+ "declared_size_over_public_max",
+ "missing_get_body",
+ "short_get_body",
+ "trailing_get_body",
+ "authored_bytes_short",
+ "authored_bytes_wrong_hash",
+ "upload_url_mismatch",
+ "upload_hash_mismatch",
+ "upload_size_mismatch",
+ "upload_mime_mismatch",
+ "head_url_mismatch",
+ "head_size_mismatch",
+ "head_mime_mismatch",
+ "get_url_mismatch",
+ "get_declared_size_mismatch",
+ "get_complete_hash_mismatch",
+ "unsupported_raster_mime",
+ "malformed_raster",
+ "animated_png",
+ "declared_format_mismatch",
+ "corrupt_png_crc",
+ "corrupt_png_deflate",
+ "invalid_png_color_type",
+ "authored_dimension_mismatch",
+ "animated_webp",
+ "zero_width",
+ "dimension_over_max",
+ "pixel_limit",
+ "progressive_jpeg",
+ "jpeg_entropy_stripped",
+ "jpeg_entropy_partial",
+ "malformed_jpeg_dqt",
];
-
-const REQUIRED_PUBLIC_TYPES: &[&str] = &[
- "RadrootsBlossomBud02UploadStatus",
- "RadrootsBlossomBud02UploadObservation",
- "RadrootsBlossomBud01HeadObservation",
- "RadrootsBlossomBud01GetCollector",
- "RadrootsBlossomBud01GetObservation",
- "RadrootsBlossomRasterFormat",
- "RadrootsBlossomRasterDimensions",
- "RadrootsBlossomAuthoredRasterDimensions",
- "RadrootsBlossomPublicationReadinessEvidenceDigest",
- "RadrootsBlossomPublicationReadinessEvidence",
+const PERSISTENCE_CASE_IDS: &[&str] = &[
+ "canonical_evidence_reloads",
+ "canonical_evidence_serializes",
+ "leading_whitespace_is_noncanonical",
+ "noncanonical_string_escaping_is_rejected",
+ "field_reordering_is_noncanonical",
+ "unknown_field_is_rejected",
+ "private_bud11_field_is_rejected",
+ "missing_field_is_rejected",
+ "duplicate_field_is_rejected",
+ "wrong_field_type_is_rejected",
+ "schema_version_is_strict",
+ "policy_version_is_strict",
+ "url_must_be_canonical",
+ "url_exact_maximum_is_valid",
+ "url_one_over_maximum_is_rejected",
+ "url_hash_must_match_sha256",
+ "sha256_must_be_lower_hex",
+ "size_must_be_nonzero",
+ "size_must_be_bounded",
+ "mime_must_be_supported",
+ "format_must_match_mime",
+ "dimensions_must_be_nonzero",
+ "dimensions_must_respect_axis_limit",
+ "dimensions_must_respect_pixel_limit",
+ "bud02_status_is_strict",
+ "bud01_head_status_is_strict",
+ "bud01_get_status_is_strict",
+ "uploaded_mutation_breaks_digest",
+ "digest_must_be_lower_hex",
+ "digest_must_match_all_facts",
+ "input_is_bounded_before_parse",
];
-const VECTOR_EXPECTATIONS: &[(&str, &str, &str, Option<&str>)] = &[
- (
- "valid_created",
- "blossom.verify_publication_readiness.valid",
- "none",
- None,
- ),
- (
- "valid_ok_without_authored_dimensions",
- "blossom.verify_publication_readiness.valid",
- "upload_status_200",
- None,
- ),
- (
- "invalid_upload_status",
- "blossom.verify_publication_readiness.invalid",
- "upload_status_202",
- Some("invalid_bud02_upload_status"),
- ),
- (
- "invalid_head_status",
- "blossom.verify_publication_readiness.invalid",
- "head_status_204",
- Some("invalid_bud01_head_status"),
- ),
- (
- "invalid_get_status",
- "blossom.verify_publication_readiness.invalid",
- "get_status_206",
- Some("invalid_bud01_get_status"),
- ),
- (
- "declared_size_over_public_max",
- "blossom.verify_publication_readiness.invalid",
- "get_size_over_max",
- Some("publication_raster_byte_limit_exceeded"),
- ),
- (
- "missing_get_body",
- "blossom.verify_publication_readiness.invalid",
- "get_body_missing",
- Some("publication_get_body_missing"),
- ),
- (
- "short_get_body",
- "blossom.verify_publication_readiness.invalid",
- "get_body_short",
- Some("publication_get_body_short"),
- ),
- (
- "trailing_get_body",
- "blossom.verify_publication_readiness.invalid",
- "get_body_trailing",
- Some("publication_get_body_trailing"),
- ),
- (
- "authored_bytes_short",
- "blossom.verify_publication_readiness.invalid",
- "authored_bytes_short",
- Some("publication_authored_bytes_size_mismatch"),
- ),
- (
- "authored_bytes_wrong_hash",
- "blossom.verify_publication_readiness.invalid",
- "authored_bytes_wrong_hash",
- Some("publication_authored_bytes_hash_mismatch"),
- ),
- (
- "upload_url_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "upload_url_mismatch",
- Some("publication_upload_url_mismatch"),
- ),
- (
- "upload_hash_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "upload_hash_mismatch",
- Some("publication_upload_hash_mismatch"),
- ),
- (
- "upload_size_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "upload_size_mismatch",
- Some("publication_upload_size_mismatch"),
- ),
- (
- "upload_mime_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "upload_mime_mismatch",
- Some("publication_upload_media_type_mismatch"),
- ),
- (
- "head_url_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "head_url_mismatch",
- Some("publication_head_url_mismatch"),
- ),
- (
- "head_size_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "head_size_mismatch",
- Some("publication_head_size_mismatch"),
- ),
- (
- "head_mime_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "head_mime_mismatch",
- Some("publication_head_media_type_mismatch"),
- ),
- (
- "get_url_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "get_url_mismatch",
- Some("publication_get_url_mismatch"),
- ),
- (
- "get_declared_size_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "get_declared_size_mismatch",
- Some("publication_get_declared_size_mismatch"),
- ),
- (
- "get_complete_hash_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "get_bytes_wrong_hash",
- Some("publication_retrieved_bytes_hash_mismatch"),
- ),
- (
- "unsupported_raster_mime",
- "blossom.verify_publication_readiness.invalid",
- "unsupported_mime",
- Some("unsupported_publication_raster_media_type"),
- ),
- (
- "malformed_raster",
- "blossom.verify_publication_readiness.invalid",
- "malformed_container",
- Some("invalid_publication_raster"),
- ),
- (
- "animated_png",
- "blossom.verify_publication_readiness.invalid",
- "animated_png",
- Some("publication_raster_animation_forbidden"),
- ),
- (
- "declared_format_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "declared_mime_jpeg",
- Some("invalid_publication_raster"),
- ),
- (
- "corrupt_png_crc",
- "blossom.verify_publication_readiness.invalid",
- "corrupt_png_crc",
- Some("publication_raster_decode_failed"),
- ),
- (
- "corrupt_png_deflate",
- "blossom.verify_publication_readiness.invalid",
- "corrupt_png_deflate",
- Some("publication_raster_decode_failed"),
- ),
- (
- "invalid_png_color_type",
- "blossom.verify_publication_readiness.invalid",
- "invalid_png_color_type",
- Some("publication_raster_decode_failed"),
- ),
- (
- "authored_dimension_mismatch",
- "blossom.verify_publication_readiness.invalid",
- "authored_dimension_mismatch",
- Some("publication_authored_raster_dimension_mismatch"),
- ),
- (
- "animated_webp",
- "blossom.verify_publication_readiness.invalid",
- "animated_webp",
- Some("publication_raster_animation_forbidden"),
- ),
- (
- "zero_width",
- "blossom.verify_publication_readiness.invalid",
- "zero_width",
- Some("publication_raster_dimensions_out_of_range"),
- ),
- (
- "dimension_over_max",
- "blossom.verify_publication_readiness.invalid",
- "dimension_over_max",
- Some("publication_raster_dimensions_out_of_range"),
- ),
- (
- "pixel_limit",
- "blossom.verify_publication_readiness.invalid",
- "pixel_limit",
- Some("publication_raster_pixel_limit_exceeded"),
- ),
- (
- "progressive_jpeg",
- "blossom.verify_publication_readiness.invalid",
- "progressive_jpeg",
- Some("publication_jpeg_process_forbidden"),
- ),
- (
- "jpeg_entropy_stripped",
- "blossom.verify_publication_readiness.invalid",
- "jpeg_entropy_stripped",
- Some("publication_raster_decode_failed"),
- ),
- (
- "jpeg_entropy_partial",
- "blossom.verify_publication_readiness.invalid",
- "jpeg_entropy_partial",
- Some("publication_raster_decode_failed"),
- ),
- (
- "malformed_jpeg_dqt",
- "blossom.verify_publication_readiness.invalid",
- "malformed_jpeg_dqt",
- Some("invalid_publication_raster"),
- ),
-];
+#[derive(Clone, Copy)]
+struct ImmutableArtifactSpec {
+ relative: &'static str,
+ byte_length: usize,
+ sha256: &'static str,
+}
-pub(super) fn validate_blossom_publication_readiness(workspace_root: &Path) -> Result<(), String> {
+impl ImmutableArtifactSpec {
+ const fn new(relative: &'static str, byte_length: usize, sha256: &'static str) -> Self {
+ Self {
+ relative,
+ byte_length,
+ sha256,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct FileDescriptor {
+ path: String,
+ byte_length: u64,
+ sha256: String,
+ hash_algorithm: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct PredecessorDescriptor {
+ contract_id: String,
+ immutable_artifacts: Vec<FileDescriptor>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ProtocolSourcePin {
+ id: String,
+ repository: String,
+ revision: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct PublicApiDescriptor {
+ constants: Vec<String>,
+ types: Vec<String>,
+ functions: Vec<String>,
+ methods: Vec<String>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct EvidenceDescriptor {
+ schema_version: u32,
+ policy_version: u16,
+ schema: FileDescriptor,
+ max_canonical_json_bytes: u64,
+ max_url_utf8_bytes: u64,
+ max_raster_bytes: u64,
+ max_decoded_bytes: u64,
+ max_dimension: u32,
+ max_pixels: u64,
+ wire_field_order: Vec<String>,
+ digest_domain: String,
+ digest_framing: String,
+ serialize_operation_id: String,
+ reload_operation_id: String,
+ invariants: Vec<String>,
+ decode_error_codes: Vec<String>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct VectorDescriptor {
+ canonical_path: String,
+ mirror_path: String,
+ byte_length: u64,
+ sha256: String,
+ hash_algorithm: String,
+ executor: FileDescriptor,
+ executor_test: String,
+ case_ids: Vec<String>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ReadinessDescriptor {
+ verify_operation_id: String,
+ input_types: Vec<String>,
+ output_type: String,
+ evidence: EvidenceDescriptor,
+ behavior_vector: VectorDescriptor,
+ persistence_vector: VectorDescriptor,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct PublicationReadinessManifest {
+ schema_version: u32,
+ contract_id: String,
+ authority_id: String,
+ manifest_schema: FileDescriptor,
+ predecessor: PredecessorDescriptor,
+ protocol_sources: Vec<ProtocolSourcePin>,
+ public_api: PublicApiDescriptor,
+ readiness: ReadinessDescriptor,
+ predecessor_source_supersessions: Vec<String>,
+ transitive_predecessor_source_supersessions: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct VectorSuite {
+ suite: String,
+ contract_version: String,
+ vectors: Vec<VectorCase>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct VectorCase {
+ id: String,
+ kind: String,
+ input: Value,
+ expected: Value,
+}
+
+struct ValidatedVector {
+ bytes: Vec<u8>,
+ case_ids: Vec<String>,
+}
+
+pub(crate) fn write_blossom_publication_readiness_manifest(
+ workspace_root: &Path,
+) -> Result<(), String> {
+ with_artifact_bundle_transaction(workspace_root, |transaction| {
+ transaction.write(expected_artifacts(workspace_root)?)?;
+ validate_manifest_under_lock(workspace_root)
+ })
+}
+
+pub(crate) fn validate_blossom_publication_readiness_manifest(
+ workspace_root: &Path,
+) -> Result<(), String> {
with_artifact_bundle_transaction(workspace_root, |_| {
- validate_blossom_publication_readiness_under_lock(workspace_root)
+ validate_manifest_under_lock(workspace_root)
})
}
-fn validate_blossom_publication_readiness_under_lock(workspace_root: &Path) -> Result<(), String> {
+pub(super) fn validate_blossom_publication_readiness(workspace_root: &Path) -> Result<(), String> {
+ validate_blossom_publication_readiness_manifest(workspace_root)
+}
+
+fn validate_manifest_under_lock(workspace_root: &Path) -> Result<(), String> {
+ validate_immutable_predecessor(workspace_root)?;
+ validate_raw_source_rebuild_predecessor_production_sources_under_lock(
+ workspace_root,
+ RAW_PREDECESSOR_SUPERSEDED_PATHS,
+ TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS,
+ )?;
+ for artifact in expected_artifacts(workspace_root)? {
+ let actual = read_regular_file(workspace_root, artifact.relative)?;
+ if actual != artifact.contents {
+ return Err(format!(
+ "generated Blossom publication-readiness contract {} is stale; run {WRITE_COMMAND}",
+ artifact.relative
+ ));
+ }
+ }
+
+ let bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
+ let manifest: PublicationReadinessManifest = serde_json::from_slice(&bytes)
+ .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
+ validate_canonical_json(MANIFEST_RELATIVE, &bytes, &manifest)?;
+ validate_manifest_shape(&manifest)?;
+
+ let schema_bytes = read_regular_file(workspace_root, MANIFEST_SCHEMA_RELATIVE)?;
+ let schema: Value = serde_json::from_slice(&schema_bytes)
+ .map_err(|error| format!("parse {MANIFEST_SCHEMA_RELATIVE}: {error}"))?;
+ validate_canonical_json(MANIFEST_SCHEMA_RELATIVE, &schema_bytes, &schema)?;
+ validate_json_schema(
+ &schema,
+ &serde_json::to_value(&manifest).map_err(|error| {
+ format!("serialize {MANIFEST_RELATIVE} for schema validation: {error}")
+ })?,
+ )?;
+
+ let evidence_schema_bytes = read_regular_file(workspace_root, EVIDENCE_SCHEMA_RELATIVE)?;
+ let evidence_schema: Value = serde_json::from_slice(&evidence_schema_bytes)
+ .map_err(|error| format!("parse {EVIDENCE_SCHEMA_RELATIVE}: {error}"))?;
+ validate_canonical_json(
+ EVIDENCE_SCHEMA_RELATIVE,
+ &evidence_schema_bytes,
+ &evidence_schema,
+ )?;
+
+ let sidecar = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?;
+ if sidecar != format!("{}\n", sha256_hex(&bytes)).as_bytes() {
+ return Err(format!(
+ "{MANIFEST_SHA256_RELATIVE} must authenticate the exact manifest bytes"
+ ));
+ }
+ Ok(())
+}
+
+fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> {
validate_immutable_predecessor(workspace_root)?;
validate_raw_source_rebuild_predecessor_production_sources_under_lock(
workspace_root,
RAW_PREDECESSOR_SUPERSEDED_PATHS,
TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS,
)?;
- validate_source_inventory(workspace_root)?;
- validate_source_boundary(workspace_root)?;
- validate_vector(workspace_root)?;
- validate_operation(workspace_root)?;
- validate_release(workspace_root)
+ validate_source_contract(workspace_root)?;
+
+ let evidence_schema_bytes = canonical_json_bytes(&evidence_schema())?;
+ let manifest_schema_bytes = canonical_json_bytes(&manifest_schema())?;
+ let manifest = describe_manifest(
+ workspace_root,
+ &manifest_schema_bytes,
+ &evidence_schema_bytes,
+ )?;
+ let manifest_bytes = canonical_json_bytes(&manifest)?;
+ let sidecar_bytes = format!("{}\n", sha256_hex(&manifest_bytes)).into_bytes();
+ let behavior_vector_bytes = read_regular_file(workspace_root, BEHAVIOR_VECTOR_RELATIVE)?;
+ let persistence_vector_bytes = read_regular_file(workspace_root, PERSISTENCE_VECTOR_RELATIVE)?;
+ let descriptor_bytes = canonical_json_bytes(&json!({
+ "schema_version": SCHEMA_VERSION,
+ "contract_id": CONTRACT_ID,
+ "manifest": descriptor_for_bytes(MANIFEST_RELATIVE, &manifest_bytes),
+ "manifest_schema": descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, &manifest_schema_bytes),
+ "manifest_sidecar": descriptor_for_bytes(MANIFEST_SHA256_RELATIVE, &sidecar_bytes),
+ "evidence_schema": descriptor_for_bytes(EVIDENCE_SCHEMA_RELATIVE, &evidence_schema_bytes),
+ "behavior_vector_sha256": sha256_hex(&behavior_vector_bytes),
+ "persistence_vector_sha256": sha256_hex(&persistence_vector_bytes),
+ "predecessor_manifest_sha256": IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS[0].sha256,
+ }))?;
+
+ Ok(vec![
+ GeneratedArtifact {
+ relative: MANIFEST_RELATIVE,
+ contents: manifest_bytes,
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SCHEMA_RELATIVE,
+ contents: manifest_schema_bytes,
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SHA256_RELATIVE,
+ contents: sidecar_bytes,
+ },
+ GeneratedArtifact {
+ relative: GENERATED_DESCRIPTOR_RELATIVE,
+ contents: descriptor_bytes,
+ },
+ GeneratedArtifact {
+ relative: EVIDENCE_SCHEMA_RELATIVE,
+ contents: evidence_schema_bytes,
+ },
+ GeneratedArtifact {
+ relative: BEHAVIOR_VECTOR_MIRROR_RELATIVE,
+ contents: behavior_vector_bytes,
+ },
+ GeneratedArtifact {
+ relative: PERSISTENCE_VECTOR_MIRROR_RELATIVE,
+ contents: persistence_vector_bytes,
+ },
+ ])
+}
+
+fn describe_manifest(
+ workspace_root: &Path,
+ manifest_schema_bytes: &[u8],
+ evidence_schema_bytes: &[u8],
+) -> Result<PublicationReadinessManifest, String> {
+ let behavior = validate_vector(
+ workspace_root,
+ BEHAVIOR_VECTOR_RELATIVE,
+ "blossom_publication_readiness",
+ BEHAVIOR_CASE_IDS,
+ &[
+ "blossom.verify_publication_readiness.valid",
+ "blossom.verify_publication_readiness.invalid",
+ ],
+ )?;
+ let persistence = validate_vector(
+ workspace_root,
+ PERSISTENCE_VECTOR_RELATIVE,
+ "blossom_publication_readiness_persistence",
+ PERSISTENCE_CASE_IDS,
+ &[
+ "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "blossom.publication_readiness_evidence.to_canonical_json.valid",
+ ],
+ )?;
+
+ Ok(PublicationReadinessManifest {
+ schema_version: SCHEMA_VERSION,
+ contract_id: CONTRACT_ID.to_owned(),
+ authority_id: AUTHORITY_ID.to_owned(),
+ manifest_schema: descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, manifest_schema_bytes),
+ predecessor: PredecessorDescriptor {
+ contract_id: "radroots_event_store.raw_source_rebuild_v1".to_owned(),
+ immutable_artifacts: IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS
+ .iter()
+ .map(|artifact| FileDescriptor {
+ path: artifact.relative.to_owned(),
+ byte_length: artifact.byte_length as u64,
+ sha256: artifact.sha256.to_owned(),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ })
+ .collect(),
+ },
+ protocol_sources: expected_protocol_sources(),
+ public_api: expected_public_api(),
+ readiness: ReadinessDescriptor {
+ verify_operation_id: VERIFY_OPERATION_ID.to_owned(),
+ input_types: owned(&[
+ "RadrootsBlossomByteVerifiedDescriptor",
+ "Bytes",
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud01GetObservation",
+ ]),
+ output_type: "RadrootsBlossomPublicationReadinessEvidence".to_owned(),
+ evidence: EvidenceDescriptor {
+ schema_version: EVIDENCE_SCHEMA_VERSION,
+ policy_version: READINESS_POLICY_VERSION,
+ schema: descriptor_for_bytes(EVIDENCE_SCHEMA_RELATIVE, evidence_schema_bytes),
+ max_canonical_json_bytes: EVIDENCE_MAX_BYTES as u64,
+ max_url_utf8_bytes: URL_MAX_BYTES as u64,
+ max_raster_bytes: RASTER_MAX_BYTES,
+ max_decoded_bytes: RASTER_MAX_DECODED_BYTES,
+ max_dimension: RASTER_MAX_DIMENSION,
+ max_pixels: RASTER_MAX_PIXELS,
+ wire_field_order: owned(WIRE_FIELD_ORDER),
+ digest_domain: EVIDENCE_DIGEST_DOMAIN.to_owned(),
+ digest_framing: "domain_bytes_then_u16be_policy_then_u64be_length_prefixed_url_then_raw_sha256_then_u64be_size_then_u64be_length_prefixed_mime_then_u8_format_then_u32be_width_then_u32be_height_then_u16be_bud02_then_u16be_head_then_u16be_get_then_u64be_uploaded_v1".to_owned(),
+ serialize_operation_id: SERIALIZE_OPERATION_ID.to_owned(),
+ reload_operation_id: RELOAD_OPERATION_ID.to_owned(),
+ invariants: owned(SEMANTIC_INVARIANTS),
+ decode_error_codes: owned(DECODE_ERROR_CODES),
+ },
+ behavior_vector: vector_descriptor(
+ behavior,
+ BEHAVIOR_VECTOR_RELATIVE,
+ BEHAVIOR_VECTOR_MIRROR_RELATIVE,
+ workspace_root,
+ BEHAVIOR_VECTOR_EXECUTOR_RELATIVE,
+ BEHAVIOR_VECTOR_EXECUTOR_TEST,
+ )?,
+ persistence_vector: vector_descriptor(
+ persistence,
+ PERSISTENCE_VECTOR_RELATIVE,
+ PERSISTENCE_VECTOR_MIRROR_RELATIVE,
+ workspace_root,
+ PERSISTENCE_VECTOR_EXECUTOR_RELATIVE,
+ PERSISTENCE_VECTOR_EXECUTOR_TEST,
+ )?,
+ },
+ predecessor_source_supersessions: owned(RAW_PREDECESSOR_SUPERSEDED_PATHS),
+ transitive_predecessor_source_supersessions: owned(
+ TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS,
+ ),
+ })
+}
+
+fn vector_descriptor(
+ vector: ValidatedVector,
+ canonical_path: &str,
+ mirror_path: &str,
+ workspace_root: &Path,
+ executor_path: &str,
+ executor_test: &str,
+) -> Result<VectorDescriptor, String> {
+ Ok(VectorDescriptor {
+ canonical_path: canonical_path.to_owned(),
+ mirror_path: mirror_path.to_owned(),
+ byte_length: vector.bytes.len() as u64,
+ sha256: sha256_hex(&vector.bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ executor: descriptor_for_file(workspace_root, executor_path)?,
+ executor_test: executor_test.to_owned(),
+ case_ids: vector.case_ids,
+ })
+}
+
+fn validate_source_contract(workspace_root: &Path) -> Result<(), String> {
+ validate_public_api_authority(workspace_root)?;
+ validate_manifest_features(workspace_root)?;
+ validate_operations_authority(workspace_root)?;
+ validate_release_authority(workspace_root)?;
+ validate_vector(
+ workspace_root,
+ BEHAVIOR_VECTOR_RELATIVE,
+ "blossom_publication_readiness",
+ BEHAVIOR_CASE_IDS,
+ &[
+ "blossom.verify_publication_readiness.valid",
+ "blossom.verify_publication_readiness.invalid",
+ ],
+ )?;
+ validate_vector(
+ workspace_root,
+ PERSISTENCE_VECTOR_RELATIVE,
+ "blossom_publication_readiness_persistence",
+ PERSISTENCE_CASE_IDS,
+ &[
+ "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "blossom.publication_readiness_evidence.to_canonical_json.valid",
+ ],
+ )?;
+ Ok(())
}
fn validate_immutable_predecessor(workspace_root: &Path) -> Result<(), String> {
- for (relative, expected_length, expected_sha256) in IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS {
- let bytes = read_regular_file(workspace_root, relative)?;
- if bytes.len() != *expected_length || sha256_hex(&bytes) != *expected_sha256 {
+ for artifact in IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS {
+ let bytes = read_regular_file(workspace_root, artifact.relative)?;
+ if bytes.len() != artifact.byte_length || sha256_hex(&bytes) != artifact.sha256 {
return Err(format!(
- "immutable raw-source rebuild predecessor artifact `{relative}` drifted"
+ "immutable raw-source rebuild predecessor artifact `{}` drifted",
+ artifact.relative
));
}
}
Ok(())
}
-fn validate_source_inventory(workspace_root: &Path) -> Result<(), String> {
- let unique = SOURCE_INVENTORY.iter().copied().collect::<BTreeSet<_>>();
- if unique.len() != SOURCE_INVENTORY.len() {
- return Err("publication-readiness source inventory contains duplicates".to_owned());
- }
- for relative in SOURCE_INVENTORY {
- let metadata = fs::symlink_metadata(workspace_root.join(relative)).map_err(|error| {
- format!("inspect publication-readiness source `{relative}`: {error}")
- })?;
- if !metadata.file_type().is_file() {
- return Err(format!(
- "publication-readiness source `{relative}` must be a regular file"
- ));
+fn validate_public_api_authority(workspace_root: &Path) -> Result<(), String> {
+ let source = read_utf8(workspace_root, READINESS_SOURCE_RELATIVE)?;
+ validate_public_api_source(&source)?;
+ let lib = read_utf8(workspace_root, BLOSSOM_LIB_RELATIVE)?;
+ validate_lib_reexports(&lib)
+}
+
+fn validate_public_api_source(source: &str) -> Result<(), String> {
+ let file = syn::parse_file(source)
+ .map_err(|error| format!("parse {READINESS_SOURCE_RELATIVE}: {error}"))?;
+ let mut constants = BTreeSet::new();
+ let mut types = BTreeSet::new();
+ let mut functions = BTreeSet::new();
+ let mut methods = BTreeSet::new();
+ let mut public_type_fields_private = BTreeSet::new();
+ let mut private_wire_types = BTreeSet::new();
+ let mut forbidden_deserialize = BTreeSet::new();
+
+ for item in &file.items {
+ match item {
+ Item::Const(item) if is_public(&item.vis) => {
+ constants.insert(item.ident.to_string());
+ }
+ Item::Enum(item) if is_public(&item.vis) => {
+ let name = item.ident.to_string();
+ types.insert(name.clone());
+ if item
+ .variants
+ .iter()
+ .flat_map(|variant| variant.fields.iter())
+ .all(|field| !is_public(&field.vis))
+ {
+ public_type_fields_private.insert(name);
+ }
+ }
+ Item::Struct(item) => {
+ let name = item.ident.to_string();
+ if is_public(&item.vis) {
+ types.insert(name.clone());
+ if item.fields.iter().all(|field| !is_public(&field.vis)) {
+ public_type_fields_private.insert(name.clone());
+ }
+ if SEALED_TYPES.contains(&name.as_str()) && derives(&item.attrs, "Deserialize")
+ {
+ forbidden_deserialize.insert(name);
+ }
+ } else if PRIVATE_WIRE_TYPES.contains(&name.as_str())
+ && derives(&item.attrs, "Serialize")
+ && derives(&item.attrs, "Deserialize")
+ && has_serde_deny_unknown_fields(&item.attrs)
+ {
+ private_wire_types.insert(name);
+ }
+ }
+ Item::Fn(item) if is_public(&item.vis) => {
+ functions.insert(item.sig.ident.to_string());
+ }
+ Item::Impl(item) => {
+ let syn::Type::Path(self_type) = item.self_ty.as_ref() else {
+ continue;
+ };
+ let Some(type_name) = self_type.path.segments.last() else {
+ continue;
+ };
+ let type_name = type_name.ident.to_string();
+ if item.trait_.as_ref().is_some_and(|(_, path, _)| {
+ path.segments
+ .last()
+ .is_some_and(|segment| segment.ident == "Deserialize")
+ }) && SEALED_TYPES.contains(&type_name.as_str())
+ {
+ forbidden_deserialize.insert(type_name.clone());
+ }
+ if item.trait_.is_none() {
+ for method in &item.items {
+ if let ImplItem::Fn(method) = method
+ && is_public(&method.vis)
+ {
+ methods.insert(format!("{type_name}::{}", method.sig.ident));
+ }
+ }
+ }
+ }
+ _ => {}
}
}
- for superseded in RAW_PREDECESSOR_SUPERSEDED_PATHS
- .iter()
- .chain(TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS)
- {
- if !unique.contains(*superseded) {
+
+ for (label, actual, expected) in [
+ ("constants", constants, expected_set(PUBLIC_CONSTANTS)),
+ ("types", types.clone(), expected_set(PUBLIC_TYPES)),
+ ("functions", functions, expected_set(PUBLIC_FUNCTIONS)),
+ ("methods", methods, expected_set(PUBLIC_METHODS)),
+ ] {
+ if actual != expected {
return Err(format!(
- "superseded predecessor source `{superseded}` is not current-byte governed"
+ "{READINESS_SOURCE_RELATIVE} public {label} drifted: expected {expected:?}, found {actual:?}"
));
}
}
- for (relative, expected_length, expected_sha256) in CURRENT_BYTE_BOUND_BLOSSOM_SOURCES {
- validate_current_byte_bound_blossom_source(
- relative,
- &read_regular_file(workspace_root, relative)?,
- *expected_length,
- expected_sha256,
- )?;
+ if public_type_fields_private != types {
+ return Err(format!(
+ "{READINESS_SOURCE_RELATIVE} public readiness types must expose no public fields"
+ ));
+ }
+ if private_wire_types != expected_set(PRIVATE_WIRE_TYPES) {
+ return Err(format!(
+ "{READINESS_SOURCE_RELATIVE} private wire models must derive Serialize/Deserialize with deny_unknown_fields"
+ ));
+ }
+ if !forbidden_deserialize.is_empty() {
+ return Err(format!(
+ "sealed readiness types must not implement Deserialize: {forbidden_deserialize:?}"
+ ));
}
Ok(())
}
-fn validate_current_byte_bound_blossom_source(
- relative: &str,
- bytes: &[u8],
- expected_length: usize,
- expected_sha256: &str,
-) -> Result<(), String> {
- if bytes.len() != expected_length || sha256_hex(bytes) != expected_sha256 {
+fn validate_lib_reexports(source: &str) -> Result<(), String> {
+ let file = syn::parse_file(source)
+ .map_err(|error| format!("parse {BLOSSOM_LIB_RELATIVE}: {error}"))?;
+ let mut names = BTreeSet::new();
+ for item in &file.items {
+ if let Item::Use(item) = item
+ && is_public(&item.vis)
+ {
+ collect_use_names(&item.tree, &mut names);
+ }
+ }
+ let required = PUBLIC_CONSTANTS
+ .iter()
+ .chain(PUBLIC_TYPES)
+ .copied()
+ .collect::<BTreeSet<_>>();
+ let missing = required
+ .into_iter()
+ .filter(|name| !names.contains(*name))
+ .collect::<Vec<_>>();
+ if !missing.is_empty() || !names.contains("verify_publication_readiness") {
return Err(format!(
- "publication-readiness current-byte source `{relative}` drifted"
+ "{BLOSSOM_LIB_RELATIVE} readiness reexports are incomplete: missing {missing:?}"
));
}
Ok(())
}
-fn validate_source_boundary(workspace_root: &Path) -> Result<(), String> {
- let source = String::from_utf8(read_regular_file(
- workspace_root,
- READINESS_SOURCE_RELATIVE,
- )?)
- .map_err(|error| format!("{READINESS_SOURCE_RELATIVE} must be UTF-8: {error}"))?;
- let sequential_jpeg_source = String::from_utf8(read_regular_file(
- workspace_root,
- SEQUENTIAL_JPEG_SOURCE_RELATIVE,
- )?)
- .map_err(|error| format!("{SEQUENTIAL_JPEG_SOURCE_RELATIVE} must be UTF-8: {error}"))?;
- validate_readiness_source_text(&source, &sequential_jpeg_source)?;
- let lib = String::from_utf8(read_regular_file(workspace_root, BLOSSOM_LIB_RELATIVE)?)
- .map_err(|error| format!("{BLOSSOM_LIB_RELATIVE} must be UTF-8: {error}"))?;
- if lib.matches("pub mod publication_readiness;").count() != 1
- || !lib.contains(
- "#[cfg(feature = \"raster-decode\")]\npub use publication_readiness::verify_publication_readiness;",
- )
- || !lib.contains("RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES")
- || !lib.contains("RadrootsBlossomPublicationReadinessEvidence")
- {
- return Err(
- "Blossom crate root must route the readiness module and public API exactly".to_owned(),
- );
+fn collect_use_names(tree: &syn::UseTree, names: &mut BTreeSet<String>) {
+ match tree {
+ syn::UseTree::Name(name) => {
+ names.insert(name.ident.to_string());
+ }
+ syn::UseTree::Rename(rename) => {
+ names.insert(rename.rename.to_string());
+ }
+ syn::UseTree::Path(path) => collect_use_names(&path.tree, names),
+ syn::UseTree::Group(group) => {
+ for item in &group.items {
+ collect_use_names(item, names);
+ }
+ }
+ syn::UseTree::Glob(_) => {}
}
+}
+fn validate_manifest_features(workspace_root: &Path) -> Result<(), String> {
let manifest = parse_toml(workspace_root, BLOSSOM_MANIFEST_RELATIVE)?;
let dependencies = manifest
.get("dependencies")
.and_then(toml::Value::as_table)
.ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare dependencies"))?;
- let actual = dependencies
+ let actual_dependencies = dependencies
.keys()
.map(String::as_str)
.collect::<BTreeSet<_>>();
- let expected = [
+ let expected_dependencies = BTreeSet::from([
"image",
"mediatype",
"serde",
+ "serde_json",
"sha2",
"unicode-general-category",
"url_nostd",
"zune-core",
"zune-jpeg",
- ]
- .into_iter()
- .collect::<BTreeSet<_>>();
- if actual != expected {
- return Err(format!(
- "{BLOSSOM_MANIFEST_RELATIVE} dependency boundary drifted: expected {expected:?}, found {actual:?}"
- ));
- }
- let image_dependency = dependencies
- .get("image")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare optional image"))?;
- if image_dependency
- .get("workspace")
- .and_then(toml::Value::as_bool)
- != Some(true)
- || image_dependency
- .get("optional")
- .and_then(toml::Value::as_bool)
- != Some(true)
- {
- return Err(format!(
- "{BLOSSOM_MANIFEST_RELATIVE} image dependency must be optional and workspace-governed"
- ));
- }
- let zune_core_dependency = dependencies
- .get("zune-core")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare optional zune-core"))?;
- if zune_core_dependency
- .get("workspace")
- .and_then(toml::Value::as_bool)
- != Some(true)
- || zune_core_dependency
- .get("optional")
- .and_then(toml::Value::as_bool)
- != Some(true)
- {
- return Err(format!(
- "{BLOSSOM_MANIFEST_RELATIVE} zune-core dependency must be optional and workspace-governed"
- ));
- }
- let zune_jpeg_dependency = dependencies
- .get("zune-jpeg")
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare optional zune-jpeg"))?;
- if zune_jpeg_dependency
- .get("workspace")
- .and_then(toml::Value::as_bool)
- != Some(true)
- || zune_jpeg_dependency
- .get("optional")
- .and_then(toml::Value::as_bool)
- != Some(true)
- {
- return Err(format!(
- "{BLOSSOM_MANIFEST_RELATIVE} zune-jpeg dependency must be optional and workspace-governed"
- ));
- }
- let workspace_manifest = parse_toml(workspace_root, WORKSPACE_MANIFEST_RELATIVE)?;
- let workspace_image = workspace_manifest
- .get("workspace")
- .and_then(|value| value.get("dependencies"))
- .and_then(|value| value.get("image"))
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{WORKSPACE_MANIFEST_RELATIVE} must govern image"))?;
- let workspace_image_features = workspace_image
- .get("features")
- .and_then(toml::Value::as_array)
- .into_iter()
- .flatten()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- if workspace_image.get("version").and_then(toml::Value::as_str) != Some("=0.25.10")
- || workspace_image
- .get("default-features")
- .and_then(toml::Value::as_bool)
- != Some(false)
- || workspace_image_features != BTreeSet::from(["png", "webp"])
- {
- return Err(format!(
- "{WORKSPACE_MANIFEST_RELATIVE} image decoder dependency must be exactly pinned to the PNG/WebP set"
- ));
- }
- let workspace_zune_core = workspace_manifest
- .get("workspace")
- .and_then(|value| value.get("dependencies"))
- .and_then(|value| value.get("zune-core"))
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{WORKSPACE_MANIFEST_RELATIVE} must govern zune-core"))?;
- let workspace_zune_core_features = workspace_zune_core
- .get("features")
- .and_then(toml::Value::as_array)
- .into_iter()
- .flatten()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- if workspace_zune_core
- .get("version")
- .and_then(toml::Value::as_str)
- != Some("=0.5.1")
- || workspace_zune_core
- .get("default-features")
- .and_then(toml::Value::as_bool)
- != Some(false)
- || workspace_zune_core_features != BTreeSet::from(["std"])
- {
+ ]);
+ if actual_dependencies != expected_dependencies {
return Err(format!(
- "{WORKSPACE_MANIFEST_RELATIVE} JPEG decoder core must be exactly pinned to zune-core 0.5.1 with std only"
+ "{BLOSSOM_MANIFEST_RELATIVE} dependency boundary drifted: expected {expected_dependencies:?}, found {actual_dependencies:?}"
));
}
- let workspace_zune_jpeg = workspace_manifest
- .get("workspace")
- .and_then(|value| value.get("dependencies"))
- .and_then(|value| value.get("zune-jpeg"))
- .and_then(toml::Value::as_table)
- .ok_or_else(|| format!("{WORKSPACE_MANIFEST_RELATIVE} must govern zune-jpeg"))?;
- let workspace_zune_jpeg_features = workspace_zune_jpeg
- .get("features")
- .and_then(toml::Value::as_array)
- .into_iter()
- .flatten()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- if workspace_zune_jpeg
- .get("version")
- .and_then(toml::Value::as_str)
- != Some("=0.5.15")
- || workspace_zune_jpeg
- .get("default-features")
- .and_then(toml::Value::as_bool)
- != Some(false)
- || workspace_zune_jpeg_features != BTreeSet::from(["std"])
- {
- return Err(format!(
- "{WORKSPACE_MANIFEST_RELATIVE} strict JPEG authority must be exactly pinned to zune-jpeg 0.5.15 with std only"
- ));
+ for dependency in ["serde", "serde_json", "image", "zune-core", "zune-jpeg"] {
+ let table = dependencies
+ .get(dependency)
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| {
+ format!("{BLOSSOM_MANIFEST_RELATIVE} must declare {dependency} as a table")
+ })?;
+ if table.get("workspace").and_then(toml::Value::as_bool) != Some(true)
+ || table.get("optional").and_then(toml::Value::as_bool) != Some(true)
+ {
+ return Err(format!(
+ "{BLOSSOM_MANIFEST_RELATIVE} dependency {dependency} must be optional and workspace-governed"
+ ));
+ }
}
let features = manifest
.get("features")
.and_then(toml::Value::as_table)
.ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare features"))?;
- let raster_decode = features
- .get("raster-decode")
- .and_then(toml::Value::as_array)
- .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare raster-decode feature"))?
- .iter()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- if raster_decode != BTreeSet::from(["dep:image", "dep:zune-core", "dep:zune-jpeg", "std"]) {
- return Err(format!(
- "{BLOSSOM_MANIFEST_RELATIVE} raster-decode feature must select only std, image, zune-core, and zune-jpeg"
- ));
- }
- let coverage = parse_toml(workspace_root, COVERAGE_PROFILES_RELATIVE)?;
- let blossom_coverage = coverage
- .get("profiles")
- .and_then(|value| value.get("crates"))
- .and_then(|value| value.get("radroots_blossom"))
- .ok_or_else(|| {
- format!("{COVERAGE_PROFILES_RELATIVE} must declare radroots_blossom coverage")
- })?;
- let coverage_features = blossom_coverage
- .get("features")
- .and_then(toml::Value::as_array)
- .into_iter()
- .flatten()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- if blossom_coverage
- .get("no_default_features")
- .and_then(toml::Value::as_bool)
- != Some(true)
- || coverage_features != BTreeSet::from(["raster-decode", "serde"])
- {
- return Err(format!(
- "{COVERAGE_PROFILES_RELATIVE} must measure the explicit Blossom raster-decode surface"
- ));
- }
- let nix_common = String::from_utf8(read_regular_file(workspace_root, NIX_COMMON_RELATIVE)?)
- .map_err(|error| format!("{NIX_COMMON_RELATIVE} must be UTF-8: {error}"))?;
- if !nix_common.contains("radroots_blossom/raster-decode") {
- return Err(format!(
- "{NIX_COMMON_RELATIVE} core contract lane must enable raster-decode"
- ));
- }
- let nix_checks = String::from_utf8(read_regular_file(workspace_root, NIX_CHECKS_RELATIVE)?)
- .map_err(|error| format!("{NIX_CHECKS_RELATIVE} must be UTF-8: {error}"))?;
- let nix_check_commands = nix_checks.lines().map(str::trim).collect::<BTreeSet<_>>();
- for required in [
- "cargo check -p radroots_blossom --lib --no-default-features",
- "cargo check -p radroots_blossom --lib --no-default-features --features raster-decode",
- "cargo test -p radroots_blossom --no-default-features --features raster-decode,serde",
+ for (feature, expected) in [
+ ("default", BTreeSet::from(["serde"])),
+ ("serde", BTreeSet::from(["dep:serde", "dep:serde_json"])),
+ (
+ "std",
+ BTreeSet::from(["serde?/std", "sha2/std", "url_nostd/std"]),
+ ),
+ (
+ "raster-decode",
+ BTreeSet::from(["dep:image", "dep:zune-core", "dep:zune-jpeg", "std"]),
+ ),
] {
- if !nix_check_commands.contains(required) {
+ let expected = expected
+ .into_iter()
+ .map(str::to_owned)
+ .collect::<BTreeSet<_>>();
+ let actual = toml_string_set(
+ &format!("{BLOSSOM_MANIFEST_RELATIVE} feature {feature}"),
+ features.get(feature),
+ )?;
+ if actual != expected {
return Err(format!(
- "{NIX_CHECKS_RELATIVE} lacks governed Blossom verification `{required}`"
+ "{BLOSSOM_MANIFEST_RELATIVE} feature {feature} drifted: expected {expected:?}, found {actual:?}"
));
}
}
- let raw_predecessor = String::from_utf8(read_regular_file(
- workspace_root,
- RAW_PREDECESSOR_GOVERNANCE_RELATIVE,
- )?)
- .map_err(|error| format!("{RAW_PREDECESSOR_GOVERNANCE_RELATIVE} must be UTF-8: {error}"))?;
- validate_raw_predecessor_successor_routing(&raw_predecessor)?;
Ok(())
}
-fn validate_raw_predecessor_successor_routing(source: &str) -> Result<(), String> {
- let compact = source.split_whitespace().collect::<String>();
- for required in [
- "pub(crate)fnwrite_raw_source_rebuild_manifest(workspace_root:&Path)->Result<(),String>{validate_raw_source_rebuild_manifest(workspace_root)}",
- "super::blossom_publication_readiness::validate_blossom_publication_readiness(workspace_root)",
- "constBLOSSOM_READINESS_SUCCESSOR_TRANSITIVE_PATHS:&[&str]=&[\"Cargo.toml\",\"crates/blossom/Cargo.toml\",\"crates/blossom/src/error.rs\",\"crates/blossom/src/lib.rs\",\"crates/blossom/src/url.rs\",];",
- ] {
- if !compact.contains(required) {
- return Err(format!(
- "{RAW_PREDECESSOR_GOVERNANCE_RELATIVE} lacks validation-only successor route `{required}`"
- ));
- }
- }
- Ok(())
+fn validate_operations_authority(workspace_root: &Path) -> Result<(), String> {
+ let operations = parse_toml(workspace_root, OPERATIONS_RELATIVE)?;
+ let table = operations
+ .get("operations")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{OPERATIONS_RELATIVE} must declare operations"))?;
+ validate_operation(
+ table,
+ "blossom_verify_publication_readiness",
+ VERIFY_OPERATION_ID,
+ BEHAVIOR_VECTOR_RELATIVE,
+ &[
+ "blossom.verify_publication_readiness.valid",
+ "blossom.verify_publication_readiness.invalid",
+ ],
+ )?;
+ validate_operation(
+ table,
+ "blossom_publication_readiness_evidence_to_canonical_json",
+ SERIALIZE_OPERATION_ID,
+ PERSISTENCE_VECTOR_RELATIVE,
+ &["blossom.publication_readiness_evidence.to_canonical_json.valid"],
+ )?;
+ validate_operation(
+ table,
+ "blossom_publication_readiness_evidence_from_canonical_json",
+ RELOAD_OPERATION_ID,
+ PERSISTENCE_VECTOR_RELATIVE,
+ &[
+ "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ ],
+ )
}
-fn validate_readiness_source_text(
- source: &str,
- sequential_jpeg_source: &str,
+fn validate_operation(
+ operations: &toml::map::Map<String, toml::Value>,
+ key: &str,
+ id: &str,
+ vector: &str,
+ case_kinds: &[&str],
) -> Result<(), String> {
- for required in [
- "RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION: u16 = 1",
- "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES: u64 = 10_485_760",
- "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES: u64 =",
- "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION: u32 = 16_384",
- "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_000",
- "#[cfg(feature = \"raster-decode\")]\npub fn verify_publication_readiness(",
- "use zune_core::{bytestream::ZCursor, colorspace::ColorSpace, options::DecoderOptions};",
- "use zune_jpeg::JpegDecoder as StrictJpegDecoder;",
- "mod sequential_jpeg;",
- "sequential_jpeg::validate(bytes, container)?;",
- "StrictJpegDecoder::new_with_options(ZCursor::new(bytes), strict_jpeg_decoder_options())",
- ".set_strict_mode(true)",
- ".set_use_unsafe(false)",
- ".jpeg_set_out_colorspace(ColorSpace::RGB)",
- ".decode_headers()",
- ".output_buffer_size()",
- ".decode_into(&mut decoded)",
- "if !matches!(marker, 0xc0 | 0xc1) || data[0] != 8",
- "PublicationJpegProcessForbidden",
- "PngDecoder::with_limits(Cursor::new(bytes), raster_decode_limits())",
- "WebPDecoder::new(Cursor::new(bytes))",
- ".read_image(&mut decoded)",
- "b\"radroots.blossom.publication-readiness-evidence.v1\\0\"",
- ] {
- if !source.contains(required) {
- return Err(format!(
- "{READINESS_SOURCE_RELATIVE} is missing governed fragment `{required}`"
- ));
- }
- }
- for required in [
- "struct SequentialJpegHuffmanTable",
- "struct SequentialJpegEntropyReader",
- "sampling_product_sum > 10",
- "value_count > 256",
- "seen_values[value_index]",
- "unused_codes == 0",
- "payload[payload.len() - 3..] != [0, 63, 0]",
- "reader.finish_restart(expected_restart)?",
- "seen_components[component.frame_index] = true",
- "checked_mcu_grid_count",
- ] {
- if !sequential_jpeg_source.contains(required) {
- return Err(format!(
- "{SEQUENTIAL_JPEG_SOURCE_RELATIVE} is missing governed fragment `{required}`"
- ));
- }
- }
- let combined = format!("{source}\n{sequential_jpeg_source}");
- let lowercase = combined.to_ascii_lowercase();
- for forbidden in [
- "reqwest",
- "hyper::",
- "tokio::",
- "axum::",
- "std::net",
- "std::fs",
- "authorization: nostr",
- "bearer ",
- "cookie",
- ] {
- if lowercase.contains(forbidden) {
- return Err(format!(
- "{READINESS_SOURCE_RELATIVE} crosses the transport-neutral boundary with `{forbidden}`"
- ));
- }
- }
- if combined.contains("serde::Deserialize") || combined.contains("derive(Deserialize") {
- return Err(
- "publication readiness typestates must not gain forgeable Deserialize implementations"
- .to_owned(),
- );
- }
- if combined.contains("pub struct RadrootsBlossomRasterDecodeObservation")
- || combined.contains("decode: &RadrootsBlossom")
- || combined.contains("ImageReader")
- || combined.contains("load_from_memory")
- || combined.contains("JpegDecoder::new(Cursor::new(bytes))")
- || combined.contains("push_backend(")
- || combined.contains("gamut_core")
- || combined.contains("gamut_jpeg")
- || combined.contains("jpeg_decoder::")
- || combined.contains("use jpeg_decoder")
- || combined.contains("extern crate jpeg_decoder")
- || combined.contains("zenjpeg")
- {
- return Err(
- "publication readiness must force declared-format decode authority internally from exact bytes"
- .to_owned(),
- );
- }
- Ok(())
-}
-
-fn validate_vector(workspace_root: &Path) -> Result<(), String> {
- let canonical = read_regular_file(workspace_root, VECTOR_CANONICAL_RELATIVE)?;
- let mirror = read_regular_file(workspace_root, VECTOR_MIRROR_RELATIVE)?;
- if canonical != mirror {
- return Err(format!(
- "{VECTOR_MIRROR_RELATIVE} must byte-match {VECTOR_CANONICAL_RELATIVE}"
- ));
- }
- let vector: Value = serde_json::from_slice(&canonical)
- .map_err(|error| format!("parse {VECTOR_CANONICAL_RELATIVE}: {error}"))?;
- validate_vector_value(&vector)
-}
-
-fn validate_vector_value(vector: &Value) -> Result<(), String> {
- if vector.get("suite").and_then(Value::as_str) != Some("blossom_publication_readiness")
- || vector.get("contract_version").and_then(Value::as_str) != Some("1.0.0")
- {
- return Err("publication-readiness vector identity drifted".to_owned());
- }
- let cases = vector
- .get("vectors")
- .and_then(Value::as_array)
- .ok_or_else(|| "publication-readiness vectors must be an array".to_owned())?;
- if cases.len() != VECTOR_EXPECTATIONS.len() {
- return Err(format!(
- "publication-readiness vector count drifted: expected {}, found {}",
- VECTOR_EXPECTATIONS.len(),
- cases.len()
- ));
- }
- for (case, (id, kind, mutation, expected_error)) in cases.iter().zip(VECTOR_EXPECTATIONS) {
- let actual_id = case.get("id").and_then(Value::as_str);
- let actual_kind = case.get("kind").and_then(Value::as_str);
- let actual_mutation = case
- .get("input")
- .and_then(|input| input.get("mutation"))
- .and_then(Value::as_str);
- let actual_error = case
- .get("expected")
- .and_then(|expected| expected.get("error"))
- .and_then(Value::as_str);
- if actual_id != Some(*id)
- || actual_kind != Some(*kind)
- || actual_mutation != Some(*mutation)
- || actual_error != *expected_error
- {
- return Err(format!(
- "publication-readiness vector `{id}` identity or expected error drifted"
- ));
- }
- let bytes_hex = case
- .get("input")
- .and_then(|input| input.get("bytes_hex"))
- .and_then(Value::as_str)
- .ok_or_else(|| format!("publication-readiness vector `{id}` lacks bytes_hex"))?;
- if bytes_hex.len() != 140
- || !bytes_hex
- .bytes()
- .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
- {
- return Err(format!(
- "publication-readiness vector `{id}` must bind the exact 70-byte lowercase-hex raster"
- ));
- }
- }
- Ok(())
-}
-
-fn validate_operation(workspace_root: &Path) -> Result<(), String> {
- let manifest = parse_toml(workspace_root, OPERATIONS_RELATIVE)?;
- let operation = manifest
- .get("operations")
- .and_then(|value| value.get("blossom_verify_publication_readiness"))
- .ok_or_else(|| "operations contract lacks Blossom publication readiness".to_owned())?;
- if operation.get("domain").and_then(toml::Value::as_str) != Some("blossom")
- || operation.get("id").and_then(toml::Value::as_str)
- != Some("blossom.verify_publication_readiness")
- || operation.get("transport").and_then(toml::Value::as_str) != Some("none")
- || operation.get("signing").and_then(toml::Value::as_str) != Some("none")
+ let operation = operations
+ .get(key)
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{OPERATIONS_RELATIVE} must declare {key}"))?;
+ let conformance = operation
+ .get("conformance")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{OPERATIONS_RELATIVE} operation {key} needs conformance"))?;
+ if operation.get("id").and_then(toml::Value::as_str) != Some(id)
+ || operation.get("domain").and_then(toml::Value::as_str) != Some("blossom")
|| operation
.get("deterministic")
.and_then(toml::Value::as_bool)
!= Some(true)
- {
- return Err("Blossom publication-readiness operation authority drifted".to_owned());
- }
- let shared = manifest
- .get("shared_types")
- .and_then(|value| value.get("public"))
- .and_then(toml::Value::as_array)
- .ok_or_else(|| "operations contract shared public types are missing".to_owned())?
- .iter()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- if let Some(missing) = REQUIRED_PUBLIC_TYPES
- .iter()
- .find(|required| !shared.contains(**required))
+ || operation.get("transport").and_then(toml::Value::as_str) != Some("none")
+ || operation.get("signing").and_then(toml::Value::as_str) != Some("none")
+ || conformance.get("vector").and_then(toml::Value::as_str) != Some(vector)
+ || toml_string_set("operation case kinds", conformance.get("case_kinds"))?
+ != case_kinds.iter().map(|kind| (*kind).to_owned()).collect()
{
return Err(format!(
- "operations contract lacks readiness public type `{missing}`"
+ "{OPERATIONS_RELATIVE} operation {key} semantic authority drifted"
));
}
- if shared.contains("RadrootsBlossomRasterDecodeObservation") {
- return Err(
- "operations contract must not expose caller-constructible raster decode authority"
- .to_owned(),
- );
- }
- let inputs = operation
- .get("inputs")
- .and_then(toml::Value::as_array)
- .into_iter()
- .flatten()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- let expected_inputs = BTreeSet::from([
- "Bytes",
- "RadrootsBlossomAuthoredRasterDimensions",
- "RadrootsBlossomBud01GetObservation",
- "RadrootsBlossomBud01HeadObservation",
- "RadrootsBlossomBud02UploadObservation",
- "RadrootsBlossomByteVerifiedDescriptor",
- ]);
- if inputs != expected_inputs {
- return Err(
- "Blossom publication-readiness inputs must contain transport evidence and exact bytes only"
- .to_owned(),
- );
- }
- let rust_types = operation
- .get("implementation")
- .and_then(|value| value.get("rust_types"))
- .and_then(toml::Value::as_array)
- .into_iter()
- .flatten()
- .filter_map(toml::Value::as_str)
- .collect::<BTreeSet<_>>();
- if rust_types.contains("radroots_blossom::RadrootsBlossomRasterDecodeObservation") {
- return Err(
- "Blossom publication-readiness implementation must derive decode facts internally"
- .to_owned(),
- );
- }
Ok(())
}
-fn validate_release(workspace_root: &Path) -> Result<(), String> {
+fn validate_release_authority(workspace_root: &Path) -> Result<(), String> {
let release = parse_toml(workspace_root, RELEASE_RELATIVE)?;
let changes = release
.get("changes")
.and_then(toml::Value::as_array)
.ok_or_else(|| format!("{RELEASE_RELATIVE} must declare changes"))?;
- let matches = changes
+ let matching = changes
.iter()
.filter(|change| change.get("id").and_then(toml::Value::as_str) == Some(RELEASE_CHANGE_ID))
.collect::<Vec<_>>();
- if matches.len() != 1
- || matches[0]
+ if matching.len() != 1
+ || matching[0]
.get("classification")
.and_then(toml::Value::as_str)
!= Some("feature")
{
return Err(format!(
- "{RELEASE_RELATIVE} must contain one feature change `{RELEASE_CHANGE_ID}`"
+ "{RELEASE_RELATIVE} must contain exactly one feature change {RELEASE_CHANGE_ID}"
));
}
- let changelog = String::from_utf8(read_regular_file(workspace_root, CHANGELOG_RELATIVE)?)
- .map_err(|error| format!("{CHANGELOG_RELATIVE} must be UTF-8: {error}"))?;
+ let changelog = read_utf8(workspace_root, CHANGELOG_RELATIVE)?;
if changelog.matches(CHANGELOG_MARKER).count() != 1 {
return Err(format!(
- "{CHANGELOG_RELATIVE} must contain exactly one readiness release marker"
+ "{CHANGELOG_RELATIVE} must contain exactly one {CHANGELOG_MARKER}"
));
}
Ok(())
}
-fn parse_toml(workspace_root: &Path, relative: &str) -> Result<toml::Value, String> {
+fn validate_vector(
+ workspace_root: &Path,
+ relative: &str,
+ suite_name: &str,
+ expected_ids: &[&str],
+ allowed_kinds: &[&str],
+) -> Result<ValidatedVector, String> {
let bytes = read_regular_file(workspace_root, relative)?;
- let source = std::str::from_utf8(&bytes)
- .map_err(|error| format!("{relative} must be UTF-8 TOML: {error}"))?;
- toml::from_str(source).map_err(|error| format!("parse {relative}: {error}"))
+ let suite: VectorSuite =
+ serde_json::from_slice(&bytes).map_err(|error| format!("parse {relative}: {error}"))?;
+ let ids = suite
+ .vectors
+ .iter()
+ .map(|case| case.id.as_str())
+ .collect::<Vec<_>>();
+ if suite.suite != suite_name
+ || suite.contract_version != "1.0.0"
+ || ids != expected_ids
+ || suite.vectors.iter().any(|case| {
+ !allowed_kinds.contains(&case.kind.as_str())
+ || !case.input.is_object()
+ || !case.expected.is_object()
+ })
+ {
+ return Err(format!("{relative} case inventory or shape drifted"));
+ }
+ let unique = ids.iter().copied().collect::<BTreeSet<_>>();
+ if unique.len() != ids.len() {
+ return Err(format!("{relative} contains duplicate case ids"));
+ }
+ Ok(ValidatedVector {
+ bytes,
+ case_ids: expected_ids.iter().map(|id| (*id).to_owned()).collect(),
+ })
}
-fn sha256_hex(bytes: &[u8]) -> String {
- let digest = Sha256::digest(bytes);
- let mut output = String::with_capacity(64);
- for byte in digest {
- use std::fmt::Write;
- write!(&mut output, "{byte:02x}").expect("String writes cannot fail");
+fn validate_manifest_shape(manifest: &PublicationReadinessManifest) -> Result<(), String> {
+ if manifest.schema_version != SCHEMA_VERSION
+ || manifest.contract_id != CONTRACT_ID
+ || manifest.authority_id != AUTHORITY_ID
+ || manifest.predecessor.contract_id != "radroots_event_store.raw_source_rebuild_v1"
+ || manifest.protocol_sources != expected_protocol_sources()
+ || manifest.public_api != expected_public_api()
+ || manifest.readiness.verify_operation_id != VERIFY_OPERATION_ID
+ || manifest.readiness.input_types
+ != owned(&[
+ "RadrootsBlossomByteVerifiedDescriptor",
+ "Bytes",
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud01GetObservation",
+ ])
+ || manifest.readiness.output_type != "RadrootsBlossomPublicationReadinessEvidence"
+ || manifest.readiness.evidence.schema_version != EVIDENCE_SCHEMA_VERSION
+ || manifest.readiness.evidence.policy_version != READINESS_POLICY_VERSION
+ || manifest.readiness.evidence.max_canonical_json_bytes != EVIDENCE_MAX_BYTES as u64
+ || manifest.readiness.evidence.max_url_utf8_bytes != URL_MAX_BYTES as u64
+ || manifest.readiness.evidence.max_raster_bytes != RASTER_MAX_BYTES
+ || manifest.readiness.evidence.max_decoded_bytes != RASTER_MAX_DECODED_BYTES
+ || manifest.readiness.evidence.max_dimension != RASTER_MAX_DIMENSION
+ || manifest.readiness.evidence.max_pixels != RASTER_MAX_PIXELS
+ || manifest.readiness.evidence.wire_field_order != owned(WIRE_FIELD_ORDER)
+ || manifest.readiness.evidence.digest_domain != EVIDENCE_DIGEST_DOMAIN
+ || manifest.readiness.evidence.serialize_operation_id != SERIALIZE_OPERATION_ID
+ || manifest.readiness.evidence.reload_operation_id != RELOAD_OPERATION_ID
+ || manifest.readiness.evidence.invariants != owned(SEMANTIC_INVARIANTS)
+ || manifest.readiness.evidence.decode_error_codes != owned(DECODE_ERROR_CODES)
+ || manifest.predecessor_source_supersessions != owned(RAW_PREDECESSOR_SUPERSEDED_PATHS)
+ || manifest.transitive_predecessor_source_supersessions
+ != owned(TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS)
+ || manifest.readiness.behavior_vector.case_ids != owned(BEHAVIOR_CASE_IDS)
+ || manifest.readiness.persistence_vector.case_ids != owned(PERSISTENCE_CASE_IDS)
+ {
+ return Err(format!("{MANIFEST_RELATIVE} semantic shape drifted"));
+ }
+ let predecessor = manifest
+ .predecessor
+ .immutable_artifacts
+ .iter()
+ .map(|artifact| {
+ (
+ artifact.path.as_str(),
+ artifact.byte_length as usize,
+ artifact.sha256.as_str(),
+ )
+ })
+ .collect::<Vec<_>>();
+ let expected = IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS
+ .iter()
+ .map(|artifact| (artifact.relative, artifact.byte_length, artifact.sha256))
+ .collect::<Vec<_>>();
+ if predecessor != expected {
+ return Err(format!("{MANIFEST_RELATIVE} predecessor identity drifted"));
+ }
+ Ok(())
+}
+
+fn manifest_schema() -> Value {
+ json!({
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://radroots.org/contracts/blossom/publication-readiness-v1.schema.json",
+ "title": "Radroots Blossom Publication Readiness Semantic Contract",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema_version", "contract_id", "authority_id", "manifest_schema",
+ "predecessor", "protocol_sources", "public_api", "readiness",
+ "predecessor_source_supersessions", "transitive_predecessor_source_supersessions"
+ ],
+ "properties": {
+ "schema_version": {"const": SCHEMA_VERSION},
+ "contract_id": {"const": CONTRACT_ID},
+ "authority_id": {"const": AUTHORITY_ID},
+ "manifest_schema": {"$ref": "#/$defs/file"},
+ "predecessor": {
+ "type": "object", "additionalProperties": false,
+ "required": ["contract_id", "immutable_artifacts"],
+ "properties": {
+ "contract_id": {"const": "radroots_event_store.raw_source_rebuild_v1"},
+ "immutable_artifacts": {"type": "array", "minItems": 6, "maxItems": 6, "items": {"$ref": "#/$defs/file"}}
+ }
+ },
+ "protocol_sources": {"const": expected_protocol_sources()},
+ "public_api": {"const": expected_public_api()},
+ "readiness": {"$ref": "#/$defs/readiness"},
+ "predecessor_source_supersessions": {"const": owned(RAW_PREDECESSOR_SUPERSEDED_PATHS)},
+ "transitive_predecessor_source_supersessions": {"const": owned(TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS)}
+ },
+ "$defs": {
+ "file": {
+ "type": "object", "additionalProperties": false,
+ "required": ["path", "byte_length", "sha256", "hash_algorithm"],
+ "properties": {
+ "path": {"type": "string", "minLength": 1},
+ "byte_length": {"type": "integer", "minimum": 1},
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "hash_algorithm": {"const": HASH_ALGORITHM}
+ }
+ },
+ "vector": {
+ "type": "object", "additionalProperties": false,
+ "required": ["canonical_path", "mirror_path", "byte_length", "sha256", "hash_algorithm", "executor", "executor_test", "case_ids"],
+ "properties": {
+ "canonical_path": {"type": "string", "minLength": 1},
+ "mirror_path": {"type": "string", "minLength": 1},
+ "byte_length": {"type": "integer", "minimum": 1},
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "hash_algorithm": {"const": HASH_ALGORITHM},
+ "executor": {"$ref": "#/$defs/file"},
+ "executor_test": {"type": "string", "minLength": 1},
+ "case_ids": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}
+ }
+ },
+ "evidence": {
+ "type": "object", "additionalProperties": false,
+ "required": ["schema_version", "policy_version", "schema", "max_canonical_json_bytes", "max_url_utf8_bytes", "max_raster_bytes", "max_decoded_bytes", "max_dimension", "max_pixels", "wire_field_order", "digest_domain", "digest_framing", "serialize_operation_id", "reload_operation_id", "invariants", "decode_error_codes"],
+ "properties": {
+ "schema_version": {"const": EVIDENCE_SCHEMA_VERSION},
+ "policy_version": {"const": READINESS_POLICY_VERSION},
+ "schema": {"$ref": "#/$defs/file"},
+ "max_canonical_json_bytes": {"const": EVIDENCE_MAX_BYTES},
+ "max_url_utf8_bytes": {"const": URL_MAX_BYTES},
+ "max_raster_bytes": {"const": RASTER_MAX_BYTES},
+ "max_decoded_bytes": {"const": RASTER_MAX_DECODED_BYTES},
+ "max_dimension": {"const": RASTER_MAX_DIMENSION},
+ "max_pixels": {"const": RASTER_MAX_PIXELS},
+ "wire_field_order": {"const": owned(WIRE_FIELD_ORDER)},
+ "digest_domain": {"const": EVIDENCE_DIGEST_DOMAIN},
+ "digest_framing": {"type": "string", "minLength": 1},
+ "serialize_operation_id": {"const": SERIALIZE_OPERATION_ID},
+ "reload_operation_id": {"const": RELOAD_OPERATION_ID},
+ "invariants": {"const": owned(SEMANTIC_INVARIANTS)},
+ "decode_error_codes": {"const": owned(DECODE_ERROR_CODES)}
+ }
+ },
+ "readiness": {
+ "type": "object", "additionalProperties": false,
+ "required": ["verify_operation_id", "input_types", "output_type", "evidence", "behavior_vector", "persistence_vector"],
+ "properties": {
+ "verify_operation_id": {"const": VERIFY_OPERATION_ID},
+ "input_types": {"type": "array", "minItems": 6, "maxItems": 6, "items": {"type": "string", "minLength": 1}},
+ "output_type": {"const": "RadrootsBlossomPublicationReadinessEvidence"},
+ "evidence": {"$ref": "#/$defs/evidence"},
+ "behavior_vector": {"$ref": "#/$defs/vector"},
+ "persistence_vector": {"$ref": "#/$defs/vector"}
+ }
+ }
+ }
+ })
+}
+
+fn evidence_schema() -> Value {
+ json!({
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://radroots.org/contracts/blossom/publication-readiness-evidence-v1.schema.json",
+ "title": "Radroots Blossom Publication Readiness Evidence v1",
+ "type": "object",
+ "additionalProperties": false,
+ "required": WIRE_FIELD_ORDER,
+ "properties": {
+ "schema_version": {"const": EVIDENCE_SCHEMA_VERSION},
+ "policy_version": {"const": READINESS_POLICY_VERSION},
+ "url": {"type": "string", "minLength": 1, "maxLength": URL_MAX_BYTES},
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "size": {"type": "integer", "minimum": 1, "maximum": RASTER_MAX_BYTES},
+ "media_type": {"enum": ["image/jpeg", "image/png", "image/webp"]},
+ "raster_format": {"enum": ["jpeg", "png", "still_webp"]},
+ "dimensions": {
+ "type": "object", "additionalProperties": false,
+ "required": ["width", "height"],
+ "properties": {
+ "width": {"type": "integer", "minimum": 1, "maximum": RASTER_MAX_DIMENSION},
+ "height": {"type": "integer", "minimum": 1, "maximum": RASTER_MAX_DIMENSION}
+ }
+ },
+ "bud02_status": {"enum": [200, 201]},
+ "bud01_head_status": {"const": 200},
+ "bud01_get_status": {"const": 200},
+ "uploaded": {"type": "integer", "minimum": 0},
+ "evidence_digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"}
+ }
+ })
+}
+
+fn expected_protocol_sources() -> Vec<ProtocolSourcePin> {
+ PROTOCOL_SOURCE_PINS
+ .iter()
+ .map(|(id, repository, revision)| ProtocolSourcePin {
+ id: (*id).to_owned(),
+ repository: (*repository).to_owned(),
+ revision: (*revision).to_owned(),
+ })
+ .collect()
+}
+
+fn expected_public_api() -> PublicApiDescriptor {
+ PublicApiDescriptor {
+ constants: owned(PUBLIC_CONSTANTS),
+ types: owned(PUBLIC_TYPES),
+ functions: owned(PUBLIC_FUNCTIONS),
+ methods: owned(PUBLIC_METHODS),
+ }
+}
+
+fn descriptor_for_file(workspace_root: &Path, path: &str) -> Result<FileDescriptor, String> {
+ Ok(descriptor_for_bytes(
+ path,
+ &read_regular_file(workspace_root, path)?,
+ ))
+}
+
+fn descriptor_for_bytes(path: &str, bytes: &[u8]) -> FileDescriptor {
+ FileDescriptor {
+ path: path.to_owned(),
+ byte_length: bytes.len() as u64,
+ sha256: sha256_hex(bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ }
+}
+
+fn read_utf8(workspace_root: &Path, relative: &str) -> Result<String, String> {
+ String::from_utf8(read_regular_file(workspace_root, relative)?)
+ .map_err(|error| format!("{relative} must be UTF-8: {error}"))
+}
+
+fn parse_toml(workspace_root: &Path, relative: &str) -> Result<toml::Value, String> {
+ let source = read_utf8(workspace_root, relative)?;
+ toml::from_str(&source).map_err(|error| format!("parse {relative}: {error}"))
+}
+
+fn toml_string_set(label: &str, value: Option<&toml::Value>) -> Result<BTreeSet<String>, String> {
+ value
+ .and_then(toml::Value::as_array)
+ .ok_or_else(|| format!("{label} must be an array"))?
+ .iter()
+ .map(|value| {
+ value
+ .as_str()
+ .map(str::to_owned)
+ .ok_or_else(|| format!("{label} values must be strings"))
+ })
+ .collect()
+}
+
+fn derives(attributes: &[syn::Attribute], derive: &str) -> bool {
+ attributes.iter().any(|attribute| {
+ attribute.path().is_ident("derive")
+ && attribute
+ .parse_args_with(
+ syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
+ )
+ .is_ok_and(|paths| {
+ paths.iter().any(|path| {
+ path.segments
+ .last()
+ .is_some_and(|segment| segment.ident == derive)
+ })
+ })
+ })
+}
+
+fn has_serde_deny_unknown_fields(attributes: &[syn::Attribute]) -> bool {
+ attributes.iter().any(|attribute| {
+ if !attribute.path().is_ident("serde") {
+ return false;
+ }
+ let mut found = false;
+ let _ = attribute.parse_nested_meta(|meta| {
+ if meta.path.is_ident("deny_unknown_fields") {
+ found = true;
+ }
+ Ok(())
+ });
+ found
+ })
+}
+
+fn is_public(visibility: &Visibility) -> bool {
+ matches!(visibility, Visibility::Public(_))
+}
+
+fn expected_set(values: &[&str]) -> BTreeSet<String> {
+ values.iter().map(|value| (*value).to_owned()).collect()
+}
+
+fn owned(values: &[&str]) -> Vec<String> {
+ values.iter().map(|value| (*value).to_owned()).collect()
+}
+
+fn canonical_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
+ let mut bytes =
+ serde_json::to_vec_pretty(value).map_err(|error| format!("serialize JSON: {error}"))?;
+ bytes.push(b'\n');
+ Ok(bytes)
+}
+
+fn validate_canonical_json<T: Serialize>(
+ relative: &str,
+ bytes: &[u8],
+ value: &T,
+) -> Result<(), String> {
+ if canonical_json_bytes(value)? != bytes {
+ return Err(format!("{relative} is not canonical pretty JSON"));
+ }
+ Ok(())
+}
+
+fn validate_json_schema(schema: &Value, instance: &Value) -> Result<(), String> {
+ let validator = jsonschema::validator_for(schema)
+ .map_err(|error| format!("compile {MANIFEST_SCHEMA_RELATIVE}: {error}"))?;
+ let errors = validator
+ .iter_errors(instance)
+ .map(|error| error.to_string())
+ .collect::<Vec<_>>();
+ if errors.is_empty() {
+ Ok(())
+ } else {
+ Err(format!(
+ "{MANIFEST_RELATIVE} violates its schema: {}",
+ errors.join("; ")
+ ))
}
- output
+}
+
+fn sha256_hex(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
}
#[cfg(test)]
@@ -1022,128 +1448,76 @@ mod tests {
}
#[test]
- fn publication_readiness_vector_inventory_is_exact_and_mutation_sensitive() {
- let bytes = read_regular_file(&workspace_root(), VECTOR_CANONICAL_RELATIVE).unwrap();
- let mut vector: Value = serde_json::from_slice(&bytes).unwrap();
- validate_vector_value(&vector).unwrap();
- vector["vectors"][0]["input"]["mutation"] = Value::String("renamed".to_owned());
- assert!(
- validate_vector_value(&vector)
- .unwrap_err()
- .contains("valid_created")
- );
- }
+ fn readiness_public_api_is_ast_governed() {
+ let source = read_utf8(&workspace_root(), READINESS_SOURCE_RELATIVE).unwrap();
+ validate_public_api_source(&source).unwrap();
+ validate_public_api_source(&format!("// unrelated comment\n{source}")).unwrap();
- #[test]
- fn publication_readiness_source_boundary_rejects_transport_and_contract_drift() {
- let source = String::from_utf8(
- read_regular_file(&workspace_root(), READINESS_SOURCE_RELATIVE).unwrap(),
- )
- .unwrap();
- let sequential_jpeg_source = String::from_utf8(
- read_regular_file(&workspace_root(), SEQUENTIAL_JPEG_SOURCE_RELATIVE).unwrap(),
- )
- .unwrap();
- validate_readiness_source_text(&source, &sequential_jpeg_source).unwrap();
- let injected = format!("{source}\nfn injected() {{ let _ = reqwest::get; }}\n");
- assert!(
- validate_readiness_source_text(&injected, &sequential_jpeg_source)
- .unwrap_err()
- .contains("transport-neutral")
- );
- let removed = source.replace(
- "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_000",
- "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_001",
+ let exposed = source.replacen(
+ " url: RadrootsBlossomApprovedBlobUrl,",
+ " pub url: RadrootsBlossomApprovedBlobUrl,",
+ 1,
);
assert!(
- validate_readiness_source_text(&removed, &sequential_jpeg_source)
+ validate_public_api_source(&exposed)
.unwrap_err()
- .contains("missing governed fragment")
+ .contains("public readiness types")
);
- let weakened_jpeg = sequential_jpeg_source
- .replace("sampling_product_sum > 10", "sampling_product_sum > 16");
- assert!(
- validate_readiness_source_text(&source, &weakened_jpeg)
- .unwrap_err()
- .contains("missing governed fragment")
+
+ let deserializable = source.replacen(
+ "#[derive(Clone, Debug, PartialEq, Eq)]\npub struct RadrootsBlossomPublicationReadinessEvidence",
+ "#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]\npub struct RadrootsBlossomPublicationReadinessEvidence",
+ 1,
);
- let legacy_decoder = format!("{source}\nuse jpeg_decoder::Decoder;\n");
assert!(
- validate_readiness_source_text(&legacy_decoder, &sequential_jpeg_source)
+ validate_public_api_source(&deserializable)
.unwrap_err()
- .contains("decode authority")
+ .contains("must not implement Deserialize")
);
-
- for (relative, expected_length, expected_sha256) in CURRENT_BYTE_BOUND_BLOSSOM_SOURCES {
- let mut bytes = read_regular_file(&workspace_root(), relative).unwrap();
- validate_current_byte_bound_blossom_source(
- relative,
- &bytes,
- *expected_length,
- expected_sha256,
- )
- .unwrap();
- bytes.push(b' ');
- assert!(
- validate_current_byte_bound_blossom_source(
- relative,
- &bytes,
- *expected_length,
- expected_sha256,
- )
- .unwrap_err()
- .contains("current-byte source")
- );
- }
}
#[test]
- fn raw_predecessor_supersession_rejects_unknown_paths() {
- let error = validate_raw_source_rebuild_predecessor_production_sources_under_lock(
- &workspace_root(),
- &["not/a/predecessor.rs"],
- &[],
+ fn readiness_vectors_have_exact_semantic_inventories() {
+ let root = workspace_root();
+ validate_vector(
+ &root,
+ BEHAVIOR_VECTOR_RELATIVE,
+ "blossom_publication_readiness",
+ BEHAVIOR_CASE_IDS,
+ &[
+ "blossom.verify_publication_readiness.valid",
+ "blossom.verify_publication_readiness.invalid",
+ ],
)
- .unwrap_err();
- assert!(error.contains("not predecessor-bound"), "{error}");
+ .unwrap();
+ validate_vector(
+ &root,
+ PERSISTENCE_VECTOR_RELATIVE,
+ "blossom_publication_readiness_persistence",
+ PERSISTENCE_CASE_IDS,
+ &[
+ "blossom.publication_readiness_evidence.from_canonical_json.valid",
+ "blossom.publication_readiness_evidence.from_canonical_json.invalid",
+ "blossom.publication_readiness_evidence.to_canonical_json.valid",
+ ],
+ )
+ .unwrap();
}
#[test]
- fn retired_raw_predecessor_write_route_is_validation_only() {
+ fn semantic_manifest_excludes_mutable_source_and_release_identity() {
let root = workspace_root();
- let before = IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS
- .iter()
- .map(|(relative, _, _)| {
- (
- *relative,
- read_regular_file(&root, relative).expect("immutable raw predecessor artifact"),
- )
- })
- .collect::<Vec<_>>();
- super::super::raw_source_rebuild::write_raw_source_rebuild_manifest(&root)
- .expect("retired raw predecessor write route validates its active successor");
- for (relative, expected) in before {
- assert_eq!(
- read_regular_file(&root, relative).expect("raw predecessor after validation"),
- expected,
- "retired raw predecessor writer mutated {relative}"
- );
- }
+ let schema = canonical_json_bytes(&manifest_schema()).unwrap();
+ let evidence_schema = canonical_json_bytes(&evidence_schema()).unwrap();
+ let manifest = describe_manifest(&root, &schema, &evidence_schema).unwrap();
+ let value = serde_json::to_value(manifest).unwrap();
+ assert!(value.get("source_files").is_none());
+ assert!(value.get("release").is_none());
+ assert!(value.get("workspace_lock").is_none());
+ }
- let source = String::from_utf8(
- read_regular_file(&root, RAW_PREDECESSOR_GOVERNANCE_RELATIVE).unwrap(),
- )
- .unwrap();
- validate_raw_predecessor_successor_routing(&source).unwrap();
- let bypass = source.replacen(
- "validate_raw_source_rebuild_manifest(workspace_root)",
- "Ok(())",
- 1,
- );
- assert!(
- validate_raw_predecessor_successor_routing(&bypass)
- .unwrap_err()
- .contains("validation-only successor route")
- );
+ #[test]
+ fn generated_readiness_contract_is_current() {
+ validate_blossom_publication_readiness_manifest(&workspace_root()).unwrap();
}
}
diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs
@@ -225,7 +225,7 @@ const GOVERNED_DEPENDENCY_TABLE_SHA256: [(&str, &str); 7] = [
),
(
BLOSSOM_CARGO_MANIFEST_RELATIVE,
- "fecd2c6bd1d4920715b54e7b542d4ca85ff5b869b9697b4b7fe589955145e60e",
+ "03bd2a20057d4be04f2d0ec613ec65536b42eb6be4f8cd396fdf8b05e5b856fa",
),
(
EVENT_STORE_CARGO_MANIFEST_RELATIVE,
diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs
@@ -24,6 +24,7 @@ fn usage() {
eprintln!(" cargo xtask contract raw-source-rebuild-manifest [--write]");
eprintln!(" cargo xtask contract phase1-publication-artifact-manifest [--write]");
eprintln!(" cargo xtask contract phase1-publication-allowlist-manifest [--write]");
+ eprintln!(" cargo xtask contract blossom-publication-readiness-manifest [--write]");
eprintln!(" cargo xtask contract release-provenance-schema [--write]");
eprintln!(" cargo xtask contract knowledge-manifest [--write]");
eprintln!(" cargo xtask dto-roots --check|--write");
@@ -194,6 +195,16 @@ fn run_contract(args: &[String]) -> Result<(), String> {
.to_string(),
),
},
+ Some("blossom-publication-readiness-manifest") => match &args[1..] {
+ [] => contract::validate_blossom_publication_readiness_manifest(&workspace_root()),
+ [flag] if flag == "--write" => {
+ contract::write_blossom_publication_readiness_manifest(&workspace_root())
+ }
+ _ => Err(
+ "blossom-publication-readiness-manifest accepts no arguments or exactly --write"
+ .to_string(),
+ ),
+ },
Some("release-provenance-schema") => match &args[1..] {
[] => contract::validate_release_provenance_schema(&workspace_root()),
[flag] if flag == "--write" => {