commit ee2c1cf580d462f8633a17f4a5a89c60cba10d12
parent 2c967a9d5de11b1985e16a6a001752e11fd1c3f2
Author: triesap <tyson@radroots.org>
Date: Tue, 21 Jul 2026 03:19:46 +0000
event-store: add current visibility and FoodAvailability projection
- add schema v3 current visibility, transition feed, and FoodAvailability projection
- bind migration, vectors, and source-capacity guards to the successor contract
- split relay outcomes and bound raw fetch resource consumption
- quarantine legacy replica ingest behind a non-default feature
Diffstat:
52 files changed, 21988 insertions(+), 663 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -89,10 +89,16 @@ publish policy both pass for the same source revision.
observations, or heads. Read-only consumers can inspect the same fail-closed
status summary from an initialized pool without duplicating schema-sensitive
SQL or running migrations.
-- Nostr fetch-ingest receipts now distinguish admitted, unsupported, invalid,
- malformed, inserted, duplicate, and ephemeral not-persisted events, carry
- stable admission codes when classification occurs, and name valid-stream
- eligibility directly. Local event-store failures now abort fetch ingest as
+- Nostr fetch-ingest receipts now report exhaustive verification, contract
+ admission, valid-stream, and current-visibility outcomes independently.
+ Verification failures no longer share an `invalid` bucket with contract
+ failures, unsupported admissions retain their stable code without masking
+ visibility, and persisted events obtain visibility from the event store's
+ central authority. Fetches enforce a 64,000 raw-event ceiling, a 64 MiB
+ aggregate raw-JSON prefix budget, and the 256 KiB per-event wire limit before
+ Radroots parses adapter raw JSON; after upstream SDK frame decoding, the
+ official SDK stream applies the same retained-prefix bounds before retaining
+ serialized adapter output. Local event-store failures abort fetch ingest as
operational errors instead of being reported as malformed relay input.
- Generic outbox APIs now reject every NIP-16 ephemeral event before durable
queue persistence. Live-only events, including NIP-42 relay-auth and NIP-98
@@ -102,6 +108,16 @@ publish policy both pass for the same source revision.
- Retired trade order-workflow and product-projection source files that were no
longer compiled or exported have been removed. Current FoodAvailability
projection ownership remains with the event store.
+- Event-store schema v3 adds one central current-visibility authority, a
+ generation-bound addressable transition feed, and an atomic focused
+ FoodAvailability projection with bounded FTS search. The successor contract
+ authenticates schema `0003`, registry-v7 admission, exact kind scope `30402`,
+ executable transition/projection vectors, and the frozen NIP-09 predecessor.
+ Stored Blossom image digests use the public typed SHA-256 value.
+- 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
+ verified, valid-stream-eligible, currently visible admission.
- Blossom blob URLs now validate complete raw Unicode text before URL parsing
and exact raw ASCII DNS label grammar before returning a typed value. Unicode
control/format text, implicit IDNA conversion, empty labels, underscores,
@@ -213,13 +229,15 @@ publish policy both pass for the same source revision.
Generic signing and client publication reject focused or mixed kind-`30402`
profiles before signer access; signed-event relay remains transport-only.
Typed signing and publication do not attest BUD-02 upload completion.
-- Legacy replica ingestion now verifies kind-`30402` signatures, selects the
- raw addressable head before profile decoding, and sends only the Operational
- Listing partition to its trade-product projection. Selected focused/generic
- exclusions and invalid/ambiguous rejections remove an older projection while
- advancing the head, preventing stale projection fallback. The public
- head-only helper rejects kind `30402`; callers must use profile-aware
- ingestion so the head and projection remain atomic.
+- Behind the explicit non-default `legacy-ingest` feature, legacy replica
+ ingestion verifies kind-`30402` signatures, selects the raw addressable head
+ before profile decoding, and sends only the Operational Listing partition to
+ its trade-product projection. Selected focused/generic exclusions and
+ invalid/ambiguous rejections remove an older projection while advancing the
+ head, preventing stale projection fallback. The feature-gated public
+ head-only helper rejects kind `30402`; callers must use profile-aware legacy
+ ingestion so the head and projection remain atomic. These helpers are not a
+ Phase 1 product ingest boundary.
- Event-contract identification now selects Operational Listing only for its
raw marker partition. Focused FoodAvailability is admission-only, while
marker-free generic and mixed-marker NIP-99 events cannot be mislabeled as
diff --git a/Cargo.lock b/Cargo.lock
@@ -4596,6 +4596,7 @@ dependencies = [
"getrandom 0.2.17",
"hex",
"nostr",
+ "radroots_blossom",
"radroots_event",
"radroots_event_codec",
"radroots_transport",
diff --git a/build/nix/checks.nix b/build/nix/checks.nix
@@ -25,11 +25,46 @@ let
installPhaseCommand = "mkdir -p $out";
}
);
+ mkReplicaSyncLane =
+ {
+ pname,
+ command,
+ }:
+ common.craneLib.mkCargoDerivation (
+ common.commonCraneArgs
+ // {
+ inherit (common) cargoArtifacts;
+ inherit pname;
+ doCheck = false;
+ buildPhaseCargoCommand = command;
+ installPhaseCommand = "mkdir -p $out";
+ }
+ );
+ replicaSyncDefaultCheck = mkReplicaSyncLane {
+ pname = "radroots-replica-sync-default-check";
+ command = "cargo check -p radroots_replica_sync --all-targets";
+ };
+ replicaSyncDefaultTest = mkReplicaSyncLane {
+ pname = "radroots-replica-sync-default-test";
+ command = "cargo test -p radroots_replica_sync";
+ };
+ replicaSyncLegacyCheck = mkReplicaSyncLane {
+ pname = "radroots-replica-sync-legacy-ingest-check";
+ command = "cargo check -p radroots_replica_sync --all-targets --features legacy-ingest";
+ };
+ replicaSyncLegacyTest = mkReplicaSyncLane {
+ pname = "radroots-replica-sync-legacy-ingest-test";
+ command = "cargo test -p radroots_replica_sync --features legacy-ingest";
+ };
in
{
cargo-fmt = cargoFmt;
cargo-check = cargoCheck;
cargo-test = cargoTest;
+ replica-sync-default-check = replicaSyncDefaultCheck;
+ replica-sync-default-test = replicaSyncDefaultTest;
+ replica-sync-legacy-ingest-check = replicaSyncLegacyCheck;
+ replica-sync-legacy-ingest-test = replicaSyncLegacyTest;
guards = common.mkRepoCheck {
name = "repo-guards";
diff --git a/contracts/conformance/vectors/event_store/food_availability_projection.v1.json b/contracts/conformance/vectors/event_store/food_availability_projection.v1.json
@@ -0,0 +1,2889 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_event_store.food_availability_projection_v1",
+ "feed_version": 1,
+ "projection_version": 1,
+ "scope_kinds": [
+ 30402
+ ],
+ "cases": [
+ {
+ "id": "visible_food_availability_projects_and_searches",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "b9d0da50b69689fdd71adc0d698b3e2d04e53c94ec31e23496b4d2fdb96bc1719c5d626881f407682f287f2a5d2bc57159f70a8cd3d1aaae96bd1394c5b1ea0a"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "content": "Carrots available this week.",
+ "title": "Nantes Carrots",
+ "summary": "Fresh bunches",
+ "published_at": 1700000000,
+ "location": "Central Saanich, BC",
+ "price_amount": "3",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": null,
+ "quantity_unit": null,
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "Fresh",
+ "event_ids": [
+ "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7"
+ ]
+ },
+ {
+ "query": "Nantes Saanich",
+ "event_ids": [
+ "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 1,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 1
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "817789914f92ad1401bb128f2c176f725143cbbc10f0d1b97fa4b31d5d1ab05f",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": []
+ }
+ },
+ {
+ "id": "invalid_same_timestamp_winner_retracts_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "b9d0da50b69689fdd71adc0d698b3e2d04e53c94ec31e23496b4d2fdb96bc1719c5d626881f407682f287f2a5d2bc57159f70a8cd3d1aaae96bd1394c5b1ea0a"
+ }
+ },
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060001,
+ "expected_ingest": {
+ "admission_status": "invalid",
+ "admission_code": "food_status_invalid",
+ "contract_id": null,
+ "event_class": "addressable",
+ "valid_stream_eligible": false,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "paused"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "5728cd88796dc4beed1d293db77a9e5a9297bb9e3d7d56b9ee108f3c6d3ad2fd5468b359d1fba09a4eeca7539d678d2e373c8bd151969e19feaceb9aebaca917"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": null,
+ "searches": [
+ {
+ "query": "Fresh",
+ "event_ids": []
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 2,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 2
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "817789914f92ad1401bb128f2c176f725143cbbc10f0d1b97fa4b31d5d1ab05f",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "event_seq": 2
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "invalid",
+ "admission_code": "food_status_invalid",
+ "contract_id": null,
+ "visibility": "not_admitted",
+ "suppression": null,
+ "cause_event": {
+ "event": {
+ "event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "event_seq": 2
+ },
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "invalid",
+ "admission_code": "food_status_invalid",
+ "contract_id": null
+ },
+ "canonical_visible_event": null,
+ "retracted_event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "invalid",
+ "decision": "not_admitted",
+ "is_raw_head": true,
+ "raw_head_event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "suppression": null
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ },
+ {
+ "id": "blossom_digest_and_image_diagnostics_are_preserved",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "image",
+ "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp"
+ ],
+ [
+ "image",
+ "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp",
+ "0x600",
+ "extra"
+ ],
+ [
+ "image",
+ "not-a-url",
+ "800x600"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "1d5eb699944bc1eba93c85071ad1d94878f1c767015391e200b45f1a2066558675a382ede737fb15307a944598687843618c067accf6608f53daad377683c2c9"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "content": "Carrots available this week.",
+ "title": "Nantes Carrots",
+ "summary": "Fresh bunches",
+ "published_at": 1700000000,
+ "location": "Central Saanich, BC",
+ "price_amount": "3",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": null,
+ "quantity_unit": null,
+ "status": "active",
+ "diagnostics": [
+ "food_image_shape_invalid",
+ "food_image_dimensions_missing",
+ "food_image_shape_invalid",
+ "food_image_dimensions_invalid",
+ "food_image_duplicate_url",
+ "food_image_duplicate_digest",
+ "food_image_url_invalid"
+ ],
+ "images": [
+ {
+ "url": "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp",
+ "width": null,
+ "height": null,
+ "blossom_sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
+ "diagnostics": [
+ "food_image_shape_invalid",
+ "food_image_dimensions_missing"
+ ],
+ "qualifies": false
+ },
+ {
+ "url": "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp",
+ "width": null,
+ "height": null,
+ "blossom_sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
+ "diagnostics": [
+ "food_image_shape_invalid",
+ "food_image_dimensions_invalid",
+ "food_image_duplicate_url",
+ "food_image_duplicate_digest"
+ ],
+ "qualifies": false
+ },
+ {
+ "url": null,
+ "width": 800,
+ "height": 600,
+ "blossom_sha256": null,
+ "diagnostics": [
+ "food_image_url_invalid"
+ ],
+ "qualifies": false
+ }
+ ]
+ },
+ "searches": [
+ {
+ "query": "Carrots",
+ "event_ids": [
+ "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 1,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 1
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "event_seq": 1
+ },
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "090b5663c36c2d7ee1361866ec6314d64e9c3ba386d430e1265aa4bb4c563884",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": []
+ }
+ },
+ {
+ "id": "authorized_address_deletion_retracts_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "causal",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1",
+ "event_class": "regular",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "not_head_selected"
+ },
+ "event": {
+ "id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 5,
+ "tags": [
+ [
+ "a",
+ "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:nantes-carrots"
+ ]
+ ],
+ "content": "Harvest listing withdrawn.",
+ "sig": "d360cbde755ad9c04bcc8a82c5d144bbb98bb1366304f0162544622ab648267b0e8e4e8d35baffe98c0cac7dc67e0c1a44a7ae172db49f71641291294759da28"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": null,
+ "searches": [
+ {
+ "query": "Fresh",
+ "event_ids": []
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 2,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 2
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "suppressed",
+ "suppression": {
+ "outcome": "suppressed",
+ "reason": "deletion_address_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "event_seq": 2
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 5,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1"
+ },
+ "canonical_visible_event": null,
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "not_head_selected"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "suppressed",
+ "is_raw_head": true,
+ "raw_head_event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "suppression": {
+ "outcome": "suppressed",
+ "reason": "deletion_address_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ }
+ },
+ {
+ "event": {
+ "event_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": null,
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_immune",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "suppressed"
+ }
+ ]
+ }
+ },
+ {
+ "id": "wrong_author_address_deletion_preserves_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "causal",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1",
+ "event_class": "regular",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "not_head_selected"
+ },
+ "event": {
+ "id": "51fa76379d859f70a30f4e44b97373c35bf86cc1efdce2bf23ed0e8605387535",
+ "pubkey": "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af",
+ "created_at": 1700000070,
+ "kind": 5,
+ "tags": [
+ [
+ "a",
+ "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:nantes-carrots"
+ ]
+ ],
+ "content": "Unauthorized withdrawal attempt.",
+ "sig": "58e88c1dcd442c6336a090085d8945cb935aff84f859e8132fa9fd4d596ae746c366b56e4a73f23490e550b6d0b0ba60489e1aa3130e7018b4397d4dab3039d2"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "content": "Carrots available this week.",
+ "title": "Nantes Carrots",
+ "summary": "Fresh bunches",
+ "published_at": 1700000000,
+ "location": "Central Saanich, BC",
+ "price_amount": "3",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": null,
+ "quantity_unit": null,
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "Fresh",
+ "event_ids": [
+ "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 2,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 2
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_author_mismatch",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "51fa76379d859f70a30f4e44b97373c35bf86cc1efdce2bf23ed0e8605387535",
+ "event_seq": 2
+ },
+ "pubkey": "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af",
+ "created_at": 1700000070,
+ "kind": 5,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "not_head_selected"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_author_mismatch",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "51fa76379d859f70a30f4e44b97373c35bf86cc1efdce2bf23ed0e8605387535",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": null,
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_immune",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": []
+ }
+ },
+ {
+ "id": "post_cutoff_replacement_restores_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "causal",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1",
+ "event_class": "regular",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "not_head_selected"
+ },
+ "event": {
+ "id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 5,
+ "tags": [
+ [
+ "a",
+ "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:nantes-carrots"
+ ]
+ ],
+ "content": "Harvest listing withdrawn.",
+ "sig": "d360cbde755ad9c04bcc8a82c5d144bbb98bb1366304f0162544622ab648267b0e8e4e8d35baffe98c0cac7dc67e0c1a44a7ae172db49f71641291294759da28"
+ }
+ },
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000080000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "New harvest bunches"
+ ],
+ [
+ "published_at",
+ "1700000080"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3.5",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "radroots:quantity",
+ "24",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "A new carrot harvest is available.",
+ "sig": "fcfbab5dee277d70fb26740a8b058a884c82ceedc83819ff6920cc42634adfbbbe149c2cc207f2c176436c0cb738c007d3fc378d0d530d09d45d818ecad5be4c"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "content": "A new carrot harvest is available.",
+ "title": "Nantes Carrots",
+ "summary": "New harvest bunches",
+ "published_at": 1700000080,
+ "location": "Central Saanich, BC",
+ "price_amount": "3.5",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": "24",
+ "quantity_unit": "lb",
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "New harvest",
+ "event_ids": [
+ "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 3,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 3
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "suppressed",
+ "suppression": {
+ "outcome": "suppressed",
+ "reason": "deletion_address_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "event_seq": 2
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 5,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1"
+ },
+ "canonical_visible_event": null,
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "not_head_selected"
+ },
+ {
+ "transition_seq": 3,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_head_created_at": 1700000080,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_address_cutoff_precedes_target",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_json_sha256": "465617b3c73ea58efd9351aa162540614ac50c92115d599a582fec4b98b5e195",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "suppressed",
+ "reason": "deletion_address_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ }
+ },
+ {
+ "event": {
+ "event_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": null,
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_immune",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_address_cutoff_precedes_target",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ },
+ {
+ "id": "operational_listing_head_retracts_food_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "scoped_non_food",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "p",
+ "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"
+ ],
+ [
+ "a",
+ "30340:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000070"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "inventory",
+ "24"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "delivery",
+ "pickup"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "fd70b10cd97d71e49a622482edcc6d4a1ab065fa2d614ca848187fd4c00a8ea3c46c414a96d26b75f0244f03a0cfd0b94495d3ef46cedeec05d1cd84e8864bbb"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": null,
+ "searches": [
+ {
+ "query": "Nantes",
+ "event_ids": []
+ },
+ {
+ "query": "Carrots available",
+ "event_ids": []
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 2,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 2
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_head_created_at": 1700000070,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_json_sha256": "19a34cd6af7a207c60b1c3e0c9b4f58e9cb7af2a8c26f323e7570298992833cb",
+ "admission_status": "admitted",
+ "contract_id": "radroots.operational_listing.published.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ },
+ {
+ "id": "food_head_after_operational_listing_restores_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "scoped_non_food",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "p",
+ "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"
+ ],
+ [
+ "a",
+ "30340:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000070"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "inventory",
+ "24"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "delivery",
+ "pickup"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "fd70b10cd97d71e49a622482edcc6d4a1ab065fa2d614ca848187fd4c00a8ea3c46c414a96d26b75f0244f03a0cfd0b94495d3ef46cedeec05d1cd84e8864bbb"
+ }
+ },
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000080000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "New harvest bunches"
+ ],
+ [
+ "published_at",
+ "1700000080"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3.5",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "radroots:quantity",
+ "24",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "A new carrot harvest is available.",
+ "sig": "fcfbab5dee277d70fb26740a8b058a884c82ceedc83819ff6920cc42634adfbbbe149c2cc207f2c176436c0cb738c007d3fc378d0d530d09d45d818ecad5be4c"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "content": "A new carrot harvest is available.",
+ "title": "Nantes Carrots",
+ "summary": "New harvest bunches",
+ "published_at": 1700000080,
+ "location": "Central Saanich, BC",
+ "price_amount": "3.5",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": "24",
+ "quantity_unit": "lb",
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "New harvest",
+ "event_ids": [
+ "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 3,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 3
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_head_created_at": 1700000070,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_json_sha256": "19a34cd6af7a207c60b1c3e0c9b4f58e9cb7af2a8c26f323e7570298992833cb",
+ "admission_status": "admitted",
+ "contract_id": "radroots.operational_listing.published.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 3,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_head_created_at": 1700000080,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_json_sha256": "465617b3c73ea58efd9351aa162540614ac50c92115d599a582fec4b98b5e195",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "not_current"
+ },
+ {
+ "transition_seq": 2,
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ },
+ {
+ "id": "food_feed_cursor_advances_across_unrelated_addressable_traffic",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "unrelated_addressable",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.farm.profile.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "384e9060b4bbc919ca2ea75a47241078a5ca911c18450d62081232e10b66fea4",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30340,
+ "tags": [
+ [
+ "d",
+ "victoria-farm-traffic"
+ ]
+ ],
+ "content": "{}",
+ "sig": "7987bef7ebd6c23b6d8a1435600ded7804acaf4caa28a054e977da27c10b1493101d822eb0a113872b7bb07fc5632dbec0e5205f805b0e6d240da30b9c08d862"
+ }
+ },
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000080000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "New harvest bunches"
+ ],
+ [
+ "published_at",
+ "1700000080"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3.5",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "radroots:quantity",
+ "24",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "A new carrot harvest is available.",
+ "sig": "fcfbab5dee277d70fb26740a8b058a884c82ceedc83819ff6920cc42634adfbbbe149c2cc207f2c176436c0cb738c007d3fc378d0d530d09d45d818ecad5be4c"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "content": "A new carrot harvest is available.",
+ "title": "Nantes Carrots",
+ "summary": "New harvest bunches",
+ "published_at": 1700000080,
+ "location": "Central Saanich, BC",
+ "price_amount": "3.5",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": "24",
+ "quantity_unit": "lb",
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "New harvest",
+ "event_ids": [
+ "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 3,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 3
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 3,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_head_created_at": 1700000080,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_json_sha256": "465617b3c73ea58efd9351aa162540614ac50c92115d599a582fec4b98b5e195",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "384e9060b4bbc919ca2ea75a47241078a5ca911c18450d62081232e10b66fea4",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "384e9060b4bbc919ca2ea75a47241078a5ca911c18450d62081232e10b66fea4",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/contracts/coverage-profiles.toml b/contracts/coverage-profiles.toml
@@ -22,3 +22,8 @@ test_threads = 1
no_default_features = false
features = ["blossom"]
test_threads = 1
+
+[profiles.crates."radroots_replica_sync"]
+no_default_features = false
+features = ["legacy-ingest"]
+test_threads = 1
diff --git a/contracts/event_boundary_matrix.md b/contracts/event_boundary_matrix.md
@@ -105,13 +105,16 @@ The `radroots_nostr` `events` feature seals strict FoodAvailability wire parts
behind a builder whose timestamp cannot be mutated after validation. It supports
local signing and typed client publication; generic authoring rejects focused
and mixed kind-`30402` profiles. Signed-event relay remains transport-only.
-Legacy replica ingestion verifies NIP-01 first, selects the raw addressable head
-before profile decoding, and routes only Operational Listing into its
-trade-product projection. Selected focused or generic exclusions and invalid or
-ambiguous rejections remove an older operational projection and still advance
-the raw head. The head-only replica helper rejects kind `30402`; these events
-require profile-aware ingestion so projection cleanup and head movement remain
-atomic.
+The non-default `legacy-ingest` replica feature verifies kind-`30402` NIP-01
+events first, selects the raw addressable head before profile decoding, and
+routes only Operational Listing into its trade-product projection. Selected
+focused or generic exclusions and invalid or ambiguous rejections remove an
+older operational projection and still advance the raw head. The head-only
+replica helper rejects kind `30402`; these events require profile-aware legacy
+ingestion so projection cleanup and head movement remain atomic. This
+bare-envelope module is absent from default builds and is not a Phase 1 product
+ingestion boundary. A future product replacement must accept only a
+store-produced verified, valid-stream-eligible, currently visible admission.
A validated authored image proves local Blossom descriptor-to-byte agreement
only. Successful BUD-02 upload completion and any raster, retrievability, or
@@ -301,7 +304,7 @@ not weaken or add effect fields to the request corpus.
| document | 30361 | RadrootsDocument | events.document.publish, events.document.list, events.document.get | requires `d` and pubkey tags; optional address tag |
| resource_area | 30370 | RadrootsResourceArea | events.resource_area.publish, events.resource_area.list, events.resource_area.get | addressable; GCS location and `g` tag required |
| resource_cap | 30371 | RadrootsResourceHarvestCap | events.resource_cap.publish, events.resource_cap.list, events.resource_cap.get | addressable; required address, pubkey, key, start, and end tags |
-| food_availability | 30402 | RadrootsFoodAvailabilityDetails / RadrootsInboundFoodAvailabilityProjection / RadrootsAdmittedFoodAvailabilityEvent / RadrootsNostrFoodAvailabilityEventBuilder | food_availability.build_authored_draft, food_availability.project_verified_event, food_availability.verify_and_admit_event, food_availability.validate_revision | focused `radroots.food.availability.v1` profile; strict deterministic authoring, verified projection/admission, stable-coordinate revision validation, sealed Nostr signing/publication, generic-authoring reservation, and raw-head-first legacy replica partitioning; BUD-02 upload evidence remains a runtime prerequisite |
+| food_availability | 30402 | RadrootsFoodAvailabilityDetails / RadrootsInboundFoodAvailabilityProjection / RadrootsAdmittedFoodAvailabilityEvent / RadrootsNostrFoodAvailabilityEventBuilder | food_availability.build_authored_draft, food_availability.project_verified_event, food_availability.verify_and_admit_event, food_availability.validate_revision | focused `radroots.food.availability.v1` profile; strict deterministic authoring, verified projection/admission, stable-coordinate revision validation, sealed Nostr signing/publication, generic-authoring reservation, and raw-head-first partitioning only behind the non-default `legacy-ingest` replica feature; BUD-02 upload evidence remains a runtime prerequisite |
| operational_listing | 30402 | RadrootsOperationalListing | events.operational_listing.publish, events.operational_listing.list, events.operational_listing.get | NIP-99 classified-listing kind with the richer Radroots operational profile; canonical Markdown content and tags; farm author required |
| dvm_request | 5000-5999 | RadrootsJobRequest | events.dvm_request.publish, events.dvm_request.list, events.dvm_request.get | generic DVM request surface |
| dvm_result | 6000-6999 | RadrootsJobResult | events.dvm_result.publish, events.dvm_result.list, events.dvm_result.get | generic DVM result surface |
diff --git a/contracts/events/social-events.md b/contracts/events/social-events.md
@@ -490,6 +490,122 @@ The raw SQL pool is a fully trusted escape hatch rather than a security
boundary; arbitrary DML or reproduction of the internal maintenance protocol
is outside the supported mutation contract.
+### Durable Visibility, Transition Feed, and Food Projection
+
+Event-store schema version `3` installs one central current-visibility authority,
+`radroots_event_store_current_visibility_v1`, for every persisted event. Ephemeral events never
+enter durable storage. Regular events are their own raw heads; replaceable and addressable events
+are compared with the current raw head for their coordinate. The authority composes that raw-head
+selection with registry-v7 admission and canonical NIP-09 evidence. All-event reads that claim
+current visibility, including `current_event_visibility_v1`, `event_visibility`, `visible_event`,
+and `visible_event_head`, must delegate to this view and fail closed on incoherent state. A current
+addressable product projection may instead join the transactionally maintained
+`radroots_event_store_addressable_head_state` materialization, but must bind the active generation,
+complete coordinate and head identity, admission, contract, visibility, and NIP-09 outcome. The
+FoodAvailability queries use that bounded specialization.
+
+The decision precedence is exact: a non-admitted event is `not_admitted`; otherwise an admitted
+event that is not the raw head is `not_current`; otherwise an admitted raw head with a suppressed
+NIP-09 outcome is `suppressed`; every remaining admitted raw head is `visible`. NIP-09 evidence is
+computed independently before the current-head decision, so an admitted `not_current` event can
+retain visible or suppressed historical evidence without becoming product-visible. Non-admitted
+events carry no NIP-09 evidence. Admitted events also retain the visible reasons for no authorized
+reference, an author mismatch, or an address cutoff that precedes the target. A persisted kind-`5`
+deletion request is immune, carries visible `deletion_request_immune` evidence with no request
+identifiers or cutoff, and cannot be suppressed by another kind-`5` request.
+
+Addressable transition feed version `1` accepts one through 64 input kinds, all in the
+`30000..=39999` range, and fingerprints their sorted, deduplicated canonical set. A cursor binds the
+active 32-byte source generation, feed version, exact scope fingerprint, and last scanned global
+transition sequence. With no cursor, scanning starts at the active generation floor. A cursor from
+another generation or scope is a mismatch, one below the floor is expired, one above the sealed
+high-water is ahead, and an absent sequence inside the sealed interval is corruption.
+
+Transition sequence is global rather than scope-local. A page therefore scans unrelated kinds and
+advances its cursor over them, checks sequence continuity, and reads at most 1024 transition rows.
+It returns at most the requested number of matching transitions, with a maximum request limit of
+64, and at most 4 MiB of visible raw-event JSON. If adding the next matching transition would cross
+either returned-item or aggregate-payload limit, the page stops before that transition and the next
+call retries it; a single matching row that alone exceeds the payload budget is a typed error. The
+page reports a fixed
+`source_high_water`, sets `has_more` from the last scanned sequence, and emits that sequence in its
+next cursor. It never advances past an unreturned matching transition.
+
+`RadrootsStoreProducedCanonicalEventV1` means the store selected a signature-verified, admitted,
+visible event at the instant represented by its containing transition. Historical transition pages
+can therefore carry a canonical payload whose event is `not_current` at the page's final
+high-water. The payload is replay input, not a detached proof of present visibility: consumers must
+apply every transition in sequence, including later retractions, and use the central current-
+visibility API when they need present-state authority. It does not mean that `raw_json` has a
+canonical JSON serialization. The wrapper exposes only the typed signed event id, author pubkey,
+created-at timestamp, kind, and verified opaque `raw_json`; it deliberately does not expose the
+backing `RadrootsStoredRawEvent` or duplicate its containing transition's generation/sequence
+witness. The remaining signed fields are available from `raw_json` through the verified event
+codec. Store `seq`, `inserted_at_ms`, `updated_at_ms`, source generation, and transition sequence
+are local database metadata and must not be persisted as cross-store event identity or replay
+authority. Consumers may transport `raw_json`, but must not infer cross-store equality from its JSON
+spelling instead of the signed event id.
+
+FoodAvailability projection version `1` has the sole scope kind `30402` and contract
+`radroots.food.availability.v1`. The post-core v2 capability applies pending global transitions in
+the same write transaction after protocol reconciliation. Its cursor advances over unrelated
+global transitions as well as matching ones. A visible admitted focused replacement is reverified
+and registry-v7 reprojected before insertion. A deletion, suppression, selected invalid event, or
+selected event outside the focused contract retracts the existing row for the author plus `d`
+coordinate. It never restores an older event; a later replacement created after an address-deletion
+cutoff can become visible and project normally. Projection rows are unique by generation, author,
+and `d`; image rows cascade with their parent, and projection insertion or deletion updates FTS5
+through database triggers in the same transaction.
+
+Point lookup uses author plus typed FoodAvailability identifier. Recent and search queries accept
+the `Any`, `Active`, or `Sold` status filter and a limit from 1 through 1000. Both return only the
+current visible projection and order by `published_at DESC, event_id ASC`. Search covers title,
+summary, content, and location. Its input is at most 256 UTF-8 bytes and 16 nonempty terms split on
+Unicode whitespace or control characters; terms are escaped as quoted FTS5 literals joined with
+`AND`, so caller input cannot introduce column selectors, operators, prefix matching, or grouping.
+Every returned projection row is bounded by the query limit and is individually checked against a
+fresh signature verification and registry-v7 reprojection of its immutable raw event, including its
+ordered image projection. Food point, recent, and search reads bind each row to the active
+generation's persisted addressable-head authority, including its exact head identity, admission,
+contract, and visible NIP-09 outcome. That state is maintained by the same transactional
+current-visibility predicate; bounded Food reads do not recompute deletion-reference history for
+every result row.
+
+For `RadrootsStoredFoodAvailabilityImageV1`, `qualifies()` means only that tolerant inbound image
+projection produced no structural diagnostics. An ordinary valid HTTP(S) URL with valid dimensions
+can therefore qualify while `blossom_sha256()` is `None`. A digest extracted from a Blossom-shaped
+URL is structural metadata, not evidence that bytes hash to it or that upload, retrieval, decoding,
+safety, or availability succeeded. Strict product authoring still requires an approved
+byte-verified descriptor, and publication still requires runtime BUD-02 upload-completion evidence.
+Blossom-specific client behavior must require the typed digest and the applicable runtime evidence,
+not `qualifies()` alone.
+
+The FoodAvailability cursor seals active source generation, feed version, projection version, exact
+scope fingerprint, hook-manifest SHA-256, last transition sequence, and a nonnegative projected-row
+count. The addressable feed seal binds the same generation's floor, high-water, and contiguous
+count. Each supported projection page compare-and-swaps the prior sequence and row count; it can
+advance by at most 1024 global transitions and change the row count by at most 64 and no more than
+the number of scoped transitions in that interval.
+
+Ordinary schema inspection and FoodAvailability point, recent, and search reads use one bounded
+fast authority check. It verifies the active generation, feed floor/high-water/count seal,
+feed/projection/scope/manifest identity, cursor equality with the source high-water, and the
+nonnegative sealed row count without scanning all projection or FTS rows. Explicit migration,
+supported rebuild, and conformance integrity paths must call
+`audit_food_availability_projection_v1`: the exhaustive audit reverifies and reprojects every
+projected signed event, compares all columns and ordered images, binds each row to the active
+generation's latest applicable transition for its exact Food coordinate and visible event, requires
+the exact admitted and visible head-coordinate witness set and sealed projection and FTS counts, compares
+each FTS shadow row, and runs SQLite's FTS5 integrity check. The governed Food read view joins
+immutable raw JSON and aggregates ordered image rows so a bounded point/recent/search result does
+not issue per-row SQL lookups before those cryptographic checks.
+
+The fast seal detects drift across supported typed transactions; it is not a claim to detect
+arbitrary direct SQL, disk modification, or FTS index corruption on every hot-path read. `pool()` is
+a trusted escape hatch outside the supported mutation guarantees. A caller that uses it assumes
+authority for any resulting state; only a governed migration, rebuild, or conformance path that
+invokes the exhaustive validator can re-establish integrity evidence for that state.
+
`RadrootsReaction` uses strict NIP-25 semantics. Empty content, `+`, `-`, emoji, and custom reaction
content are valid when the target tags are valid. Missing targets remain invalid.
@@ -580,16 +696,18 @@ mixed-marker kind-`30402` events before signer access. Marker-free generic NIP-9
operational-only builders remain available for explicit compatibility, while relaying an already
signed event remains transport-only and establishes no Radroots authoring claim.
-Legacy replica ingestion verifies kind-`30402` identifiers and signatures before acquiring its
-write transaction, then selects the raw addressable head before profile decoding. Only the
+Behind the explicit non-default `legacy-ingest` feature, legacy replica ingestion verifies
+kind-`30402` identifiers and signatures before acquiring its write transaction, then selects the
+raw addressable head before profile decoding. Only the
Operational Listing partition can reach the legacy trade-product projection. A signature-valid,
coordinate-valid, selected focused event or marker-free generic NIP-99 event advances the raw head
as excluded; selected invalid focused, mixed-marker, and malformed operational profiles advance it
as rejected. Every selected excluded or rejected replacement removes an older operational
projection, so stale events cannot resurrect it. A signature failure changes neither the raw head
nor the projection; a missing or invalid `d` tag fails before an addressable head can be selected.
-The public head-only helper rejects kind `30402`, which must use profile-aware ingestion so head and
-projection changes remain atomic.
+The feature-gated public head-only helper rejects kind `30402`, which must use profile-aware legacy
+ingestion so head and projection changes remain atomic. Neither helper is a Phase 1 product ingest
+boundary.
The typed Nostr boundary does not prove BUD-02 upload completion. Every media-bearing caller must
obtain successful Blossom upload evidence before signing or publishing; byte-verified descriptors
diff --git a/contracts/manifest.toml b/contracts/manifest.toml
@@ -80,4 +80,6 @@ require_conformance_vectors = true
[policy.replica]
forbid_legacy_alias_identifiers = true
require_transport_agnostic_sync_contract = true
+# Emit is required in default builds; ingest is required only under the
+# explicit non-default legacy-ingest feature governed by contracts/replica.toml.
require_deterministic_emit_ingest = true
diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml
@@ -387,3 +387,35 @@ semver_impacts = [
"change_exported_algorithm_behavior",
]
summary = "Seal and revalidate transport identity, remove policy-free relay deserialization, require typed nonempty fetch targets, reject forged adapter provenance, bind fetch observations to request time, bound caller-redacted diagnostics, constrain relay policies to trusted public configuration or exact loopback hosts, and require canonical reset and reseeding of unreleased-alpha state."
+
+[[changes]]
+id = "event-store-current-visibility-and-food-projection"
+classification = "breaking"
+semver_impacts = [
+ "add_exported_type",
+ "add_exported_function",
+ "add_exported_constant",
+ "add_enum_variant",
+ "add_conformance_vector",
+ "change_exported_constant_value",
+ "change_exported_field_type",
+ "change_exported_algorithm_behavior",
+]
+summary = "Advance the event store to schema version 3 with central current visibility, a generation-bound addressable transition feed, an atomic registry-v7 FoodAvailability projection and bounded search authority, typed Blossom digests, and an executable successor contract that preserves the frozen NIP-09 predecessor."
+
+[[changes]]
+id = "transport-event-outcomes-and-replica-quarantine"
+classification = "breaking"
+semver_impacts = [
+ "add_exported_type",
+ "add_exported_function",
+ "add_exported_constant",
+ "add_exported_field",
+ "add_enum_variant",
+ "remove_exported_field",
+ "remove_exported_module",
+ "remove_exported_function",
+ "change_exported_field_type",
+ "change_exported_algorithm_behavior",
+]
+summary = "Represent relay-event verification, contract admission, valid-stream eligibility, and current visibility as independent exhaustive outcomes; enforce hard raw-event, aggregate raw-JSON, and pre-parse per-event fetch bounds; and remove bare-envelope legacy replica ingestion from the default public feature surface."
diff --git a/contracts/replica.toml b/contracts/replica.toml
@@ -12,6 +12,8 @@ sync = "radroots_replica_sync"
[policy]
transport_agnostic_sync_core = true
+# Emit is part of the default sync surface; ingest determinism applies only to
+# the optional legacy feature named by legacy_ingest_feature below.
deterministic_emit_and_ingest = true
forbid_legacy_alias_identifiers = true
profile_event_emission = "excluded"
@@ -21,6 +23,10 @@ classified_listing_head_selection = "raw_before_profile"
classified_listing_operational_projection = "operational_partition_only"
classified_listing_excluded_or_rejected_head = "remove_projection_and_advance"
classified_listing_head_only_ingest = "reject_require_profile_aware"
+legacy_bare_envelope_ingest = "explicit_non_default_feature_only"
+legacy_ingest_feature = "legacy-ingest"
+phase_1_ingest_replacement = "none"
+future_product_ingest_input = "store_produced_verified_valid_visible_admission"
[transfer]
version = 2
diff --git a/crates/event_store/Cargo.toml b/crates/event_store/Cargo.toml
@@ -17,6 +17,9 @@ sqlite = ["dep:getrandom", "dep:sqlx", "sqlx/sqlite-bundled"]
runtime-tokio = ["sqlx/runtime-tokio"]
[dependencies]
+radroots_blossom = { workspace = true, default-features = false, features = [
+ "std",
+] }
radroots_event = { workspace = true, default-features = false, features = [
"std",
"serde",
@@ -38,7 +41,7 @@ thiserror = { workspace = true }
[dev-dependencies]
nostr = { workspace = true }
tempfile = { workspace = true }
-tokio = { workspace = true, features = ["macros", "rt"] }
+tokio = { workspace = true, features = ["macros", "rt", "sync"] }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
diff --git a/crates/event_store/README b/crates/event_store/README
@@ -36,26 +36,37 @@ The public read APIs name their authority explicitly:
protocol identifier; a missing `d` tag or missing first value becomes the
empty identifier. Product-contract admission may impose stricter `d` rules
independently.
-- `event_visibility`, `visible_event`, and `visible_event_head` expose current
- product visibility. A visible head is the exact raw head only when that event
- is admitted; an unsupported or invalid winning raw head yields no older
- fallback. Non-head durable revisions report `NotCurrent`.
+- `event_visibility`, `event_visibilities`, `visible_event`, and
+ `visible_event_head` expose current product visibility. The bounded batch API
+ evaluates distinct canonical event IDs once in a single read transaction and
+ preserves requested order and duplicate cardinality. A visible head is the
+ exact raw head only when that event is admitted; an unsupported or invalid
+ winning raw head yields no older fallback. Non-head durable revisions report
+ `NotCurrent`.
Verified ephemeral events are classified but never written to the raw sequence,
tags, observations, or heads. Their ingest receipt carries
`RadrootsEventPersistence::NotPersisted`; repeated delivery remains live-only
and is never reported as a durable duplicate.
-Schema version 2 persists NIP-09 reconciliation as derived authority without
-rewriting or deleting raw envelopes. It stores generation-partitioned event
-coordinate facts, admitted deletion-request facts, normalized event and
-address targets, canonical addressable-head state, and an ordered transition
-history. Kind-`5` requests remain durable protocol events and are immune from
-suppression. These derived facts do not claim that a relay removed an event.
-The existing `event_visibility`, `visible_event`, and `visible_event_head`
-queries remain NIP-01 admission/head views in this schema checkpoint; callers
-must not treat them as NIP-09-aware until the generation-bound visibility and
-transition read contract is published.
+Schema version 3 composes NIP-09 reconciliation with a generation-bound current
+visibility view, generic addressable transition feed, and focused kind-`30402`
+FoodAvailability projection. Version 2 stores generation-partitioned event
+coordinate facts, admitted deletion-request facts, normalized event and address
+targets, canonical addressable-head state, and ordered transition history.
+Version 3 applies the same admission, raw-head, and suppression predicate to
+regular, replaceable, addressable, feed, and focused projection reads. Kind-`5`
+requests remain durable protocol events and are immune from suppression. These
+derived facts do not claim that a relay removed an event.
+
+`current_event_visibility_v1` returns the complete generation-bound decision
+and NIP-09 evidence for a stored event. Food point, recent, and literal FTS5
+search APIs expose only the active, visible FoodAvailability heads and reverify
+each returned raw event and projection. Their persisted cursor seals generation,
+feed/projection versions, scope, manifest digest, high-water, and row count.
+`audit_food_availability_projection_v1` is the explicit exhaustive projection,
+image, source-transition, exact visible-head coordinate, FTS, and signed-event
+integrity check; ordinary reads use bounded seal validation.
The `0002_nip09` migration also installs guards that reject accidental
out-of-sequence SQL mutation of raw envelopes, tags, heads, reconciliation
@@ -93,12 +104,13 @@ rollback failure.
The reconciliation core and its shared raw-head storage are isolated in
versioned modules whose complete production AST identities are bound by the
-manifest. The authenticated ingest wrapper then creates a private post-core
-storage capability whose only production methods write the declared trade and
-transport-observation operation/table pairs. Extension orchestration receives
-that capability rather than a SQL transaction and has no SQLx authority. The
-capability policy rejects transaction escape, dynamic or compound SQL, schema
-changes, attached databases, and event-store protocol authority.
+manifest. The authenticated ingest wrapper then creates private post-core
+storage capabilities. Version 1 can write only its declared trade and
+transport-observation operation/table pairs; version 2 can apply only pending
+FoodAvailability transitions. Extension orchestration receives those bounded
+capabilities rather than a SQL transaction. The capability policy rejects
+transaction escape, dynamic or compound SQL, schema changes, attached
+databases, and ungoverned protocol authority.
The capability borrow ends before an identity-bound validator compares the
exact source generation/profile authority, maintained raw counters and indexed
@@ -110,9 +122,10 @@ than quadratic per-ingest scans.
## Schema authority
Every constructor converges the database through a versioned migration
-authority whose current version is `2`. The frozen `0001_event_store` and
-`0002_nip09` SQL inputs are pinned by byte length and SHA-256 in the embedded
-registry. Fresh databases install them transactionally; an existing
+authority whose current version is `3`. The frozen `0001_event_store`,
+`0002_nip09`, and additive `0003_food_availability_projection` SQL inputs are
+pinned by byte length and SHA-256 in the embedded registry. Fresh databases
+install them transactionally; an existing
unledgered database is adopted only when its exact 46-object SQLite catalog
matches the frozen version-1 baseline fingerprint. Partial schemas, altered
tables, attached indexes or triggers, counterfeit ledgers, history gaps,
@@ -121,8 +134,9 @@ checksum drift, and schemas newer than this crate fail closed.
Checksummed, tamper-evident managed history lives in the strict, without-rowid
`radroots_event_store_schema_migrations` ledger and fails closed on drift.
Current-schema opens return after read-only inspection and validate the active
-reconciliation source authority. Every open validates all hooks in applied
-migration order, so a later hookless migration cannot mask an earlier hook.
+reconciliation source authority plus the bounded Food projection seal. Every
+open validates all hooks in applied migration order, so a later hook cannot
+mask an earlier hook.
Mutating migration work rechecks under `BEGIN IMMEDIATE`, validates exact
catalog deltas, SQLite and FTS5 integrity, event-store-owned foreign keys, and
all applied migration-hook results before commit, and never changes
@@ -143,21 +157,23 @@ block event-store migration or source rebuild.
Migration `0002_nip09` re-verifies the frozen raw JSON and immutable columns,
rebuilds derived registry-v7 admission and raw-head facts, then creates one
-random 32-byte source generation. Initial reconciliation and every later full
-rebuild use the same marker-first state machine: the marker binds the exact
+random 32-byte source generation. Its marker-first state machine binds the exact
prior and raw authority, an impossible deferred foreign-key barrier prevents a
partial rebuild from committing, and guarded closure requires the rebuilt
-authority before commit. Each successful rebuild appends a fresh random
-generation and preserves prior generations, facts, states, and transition
-history as immutable records. All NIP-09 facts, canonical addressable state,
-and transitions are partitioned by generation. The reconciliation version,
-addressable-feed version, registry version, hook-manifest digest, raw counts,
-raw high-water sequence, and transition floor are stored with each generation
-and validated on open.
-
-The one-time reconciliation preflights and then rechecks under the writer lock
-at most 25,000 raw events, 250,000 raw tags, 64 MiB of retained event-row text,
-and 32 MiB of retained tag-row text. It loads both authorities in checked
+authority before commit. Generations, NIP-09 facts, canonical addressable state,
+and transitions are immutable and generation-partitioned. The reconciliation
+version, addressable-feed version, registry version, hook-manifest digest, raw
+counts, raw high-water sequence, and transition floor are stored with each
+generation and validated on open. A supported current-schema full rebuild must
+also reset and replay the version-3 Food authority before that marker closes;
+this checkpoint does not yet expose such a maintenance operation.
+
+Every pending rebuild-bound migration, including `0002_nip09` and
+`0003_food_availability_projection`, preflights and then rechecks under the
+writer lock at most 25,000 raw events, 250,000 raw tags, 64 MiB of retained
+event-row text, and 32 MiB of retained tag-row text. This same immutable-source
+bound caps the rows and payload from which the Food projection, image rows, and
+FTS authority are derived. Reconciliation loads both raw authorities in checked
512-row pages. Candidate heads evaluate only the exact event- and
address-target request indices, merging shared matches once in canonical
request order without cloning request payloads. Exceeding a dimension returns
diff --git a/crates/event_store/contracts/food_availability_projection_v1.manifest.json b/crates/event_store/contracts/food_availability_projection_v1.manifest.json
@@ -0,0 +1,418 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_event_store.food_availability_projection_v1",
+ "hook_id": "food_availability_projection_v1",
+ "manifest_schema": {
+ "path": "crates/event_store/contracts/food_availability_projection_v1.manifest.schema.json",
+ "byte_length": 7964,
+ "sha256": "39171f6ef872a8d1483bc3d55049df5e0d110d9131c5adb4450b7c418f546910",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "predecessor": {
+ "hook_id": "nip09_reconciliation_v1",
+ "manifest": {
+ "path": "crates/event_store/contracts/nip09_reconciliation_v1.manifest.json",
+ "byte_length": 537538,
+ "sha256": "74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77",
+ "hash_algorithm": "sha256_bytes_v1"
+ }
+ },
+ "migration": {
+ "version": 3,
+ "name": "food_availability_projection",
+ "up": {
+ "path": "crates/event_store/migrations/0003_food_availability_projection.up.sql",
+ "byte_length": 23683,
+ "sha256": "4e7edfb981b25f76055efc7802ec30b4034eeae9b9c0809ea4ea7c574678748a",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "down": {
+ "path": "crates/event_store/migrations/0003_food_availability_projection.down.sql",
+ "byte_length": 1755,
+ "sha256": "29d663320109d9dd0df6a00b6a53d8d988438d01f7a66960a9d4ba3482ffffb8",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "schema_sha256": "dd12467e04addcbddb5ea0f386c12a8ac05ef5ebaaf949f24dd2c62745f5aaac",
+ "catalog": {
+ "objects": [
+ "radroots_event_store_addressable_feed_generation_insert",
+ "radroots_event_store_addressable_feed_integrity_v1",
+ "radroots_event_store_addressable_feed_transition_insert",
+ "radroots_event_store_addressable_transition_coordinate_idx",
+ "radroots_event_store_current_visibility_head_lookup_idx",
+ "radroots_event_store_current_visibility_v1",
+ "radroots_event_store_food_availability_author_idx",
+ "radroots_event_store_food_availability_cursor",
+ "radroots_event_store_food_availability_cursor_delete_guard",
+ "radroots_event_store_food_availability_cursor_insert_guard",
+ "radroots_event_store_food_availability_cursor_update_guard",
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_image_delete_guard",
+ "radroots_event_store_food_availability_image_insert_guard",
+ "radroots_event_store_food_availability_image_update_guard",
+ "radroots_event_store_food_availability_projection",
+ "radroots_event_store_food_availability_projection_delete_guard",
+ "radroots_event_store_food_availability_projection_insert_guard",
+ "radroots_event_store_food_availability_projection_update_guard",
+ "radroots_event_store_food_availability_read_v1",
+ "radroots_event_store_food_availability_recent_idx",
+ "radroots_event_store_food_availability_search_delete",
+ "radroots_event_store_food_availability_search_fts",
+ "radroots_event_store_food_availability_search_fts_config",
+ "radroots_event_store_food_availability_search_fts_content",
+ "radroots_event_store_food_availability_search_fts_data",
+ "radroots_event_store_food_availability_search_fts_docsize",
+ "radroots_event_store_food_availability_search_fts_idx",
+ "radroots_event_store_food_availability_search_insert",
+ "radroots_event_store_food_availability_status_idx",
+ "radroots_event_store_nip09_address_target_visibility_lookup_idx"
+ ],
+ "tables": [
+ "radroots_event_store_addressable_feed_integrity_v1",
+ "radroots_event_store_food_availability_cursor",
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_projection",
+ "radroots_event_store_food_availability_search_fts",
+ "radroots_event_store_food_availability_search_fts_config",
+ "radroots_event_store_food_availability_search_fts_content",
+ "radroots_event_store_food_availability_search_fts_data",
+ "radroots_event_store_food_availability_search_fts_docsize",
+ "radroots_event_store_food_availability_search_fts_idx"
+ ],
+ "fts5_tables": [
+ "radroots_event_store_food_availability_search_fts"
+ ]
+ }
+ },
+ "profile": {
+ "event_contract_registry_version": 7,
+ "addressable_feed_version": 1,
+ "projection_version": 1,
+ "scope_kinds": [
+ 30402
+ ],
+ "scope_fingerprint_sha256": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "food_contract_id": "radroots.food.availability.v1",
+ "admission_authority": "event_contract_registry_v7",
+ "current_visibility_authority": "radroots_event_store_current_visibility_v1",
+ "post_core_capability": "apply_v2"
+ },
+ "registry_inventory": {
+ "path": "contracts/event_store/event_contract_registry_v7.inventory.json",
+ "byte_length": 158021,
+ "sha256": "91595544310f865bdef064ee760c227c870417a95b87b3e27278f8da74fdddea",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "food_profile_vector": {
+ "path": "contracts/conformance/vectors/food_availability/profile.v1.json",
+ "byte_length": 90099,
+ "sha256": "dede1eb1f682ecd548e8cd3ccb251d298762e504e2588a73528e5f7a5b9eff21",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "entry_points": [
+ {
+ "role": "registry_v7_admission",
+ "rust_path": "radroots_event_codec::admit_verified_event_registry_v7"
+ },
+ {
+ "role": "migration_registry",
+ "rust_path": "radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[2]"
+ },
+ {
+ "role": "migration_apply_hook",
+ "rust_path": "radroots_event_store::schema::apply_migration_hook"
+ },
+ {
+ "role": "migration_validation_hook",
+ "rust_path": "radroots_event_store::schema::validate_migration_hook_state"
+ },
+ {
+ "role": "post_core_extension",
+ "rust_path": "radroots_event_store::store::PostCoreExtensionCapabilities::apply_v2"
+ },
+ {
+ "role": "addressable_transition_feed",
+ "rust_path": "radroots_event_store::RadrootsEventStore::addressable_transition_page_v1"
+ },
+ {
+ "role": "current_visibility",
+ "rust_path": "radroots_event_store::RadrootsEventStore::current_event_visibility_v1"
+ },
+ {
+ "role": "event_visibility_batch",
+ "rust_path": "radroots_event_store::RadrootsEventStore::event_visibilities"
+ },
+ {
+ "role": "projection_lookup",
+ "rust_path": "radroots_event_store::RadrootsEventStore::food_availability_v1"
+ },
+ {
+ "role": "projection_recent",
+ "rust_path": "radroots_event_store::RadrootsEventStore::recent_food_availability_v1"
+ },
+ {
+ "role": "projection_search",
+ "rust_path": "radroots_event_store::RadrootsEventStore::search_food_availability_v1"
+ },
+ {
+ "role": "projection_audit",
+ "rust_path": "radroots_event_store::RadrootsEventStore::audit_food_availability_projection_v1"
+ },
+ {
+ "role": "result_vector_executor",
+ "rust_path": "food_availability_projection_v1_result_vector"
+ }
+ ],
+ "source_files": [
+ {
+ "role": "workspace_dependency_authority",
+ "path": "Cargo.toml",
+ "byte_length": 10836,
+ "sha256": "285532dbb0894204843a832880f136ceac5ee312a3203ff951fb0551fac63ec4",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "event_store_dependency_authority",
+ "path": "crates/event_store/Cargo.toml",
+ "byte_length": 1466,
+ "sha256": "9e82d57b64bb7fcf145ee8b919682dd6d788b5503be743d11326f5b882fb2d7e",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "blossom_public_surface",
+ "path": "crates/blossom/src/lib.rs",
+ "byte_length": 1335,
+ "sha256": "61ebee86f02887c45507bab052686da082f74ae26f23d18ff4019e02c70097bd",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "blossom_sha256_value_object",
+ "path": "crates/blossom/src/hash.rs",
+ "byte_length": 11251,
+ "sha256": "753eff6ef9a810045c88839d39a46b37d62a4ad0a5ea57aa30e2a8219e3cdbda",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "food_event_contract",
+ "path": "crates/event/src/food_availability.rs",
+ "byte_length": 45680,
+ "sha256": "1c8c3fba6514f9b246545b3305e8ac2742258857d1b813fa34c5fbefa6135e12",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "food_admission",
+ "path": "crates/event_codec/src/food_availability/admission.rs",
+ "byte_length": 5799,
+ "sha256": "727ebf8f086c14376aba745a0a02b269115fed01e8ce95f90fdd211b9640f842",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "registry_v7_admission",
+ "path": "crates/event_codec/src/admission/registry_v7.rs",
+ "byte_length": 5350,
+ "sha256": "755e63dd7ad1115c219e5a3ea540bef67d2770fc9f2a5aa6ba97c925c99bdced",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "registry_v7_food_projection",
+ "path": "crates/event_codec/src/food_availability/inbound/registry_v7.rs",
+ "byte_length": 26865,
+ "sha256": "908015346eaec97bee6f44fe2a524410b701529ac19a5dab4505706d40d527da",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "event_store_error_surface",
+ "path": "crates/event_store/src/error.rs",
+ "byte_length": 17557,
+ "sha256": "ac206fafdc67e4c25f4a2f1bcd38eb34ae224b38aca55894b1a9cc67242d8b1a",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "generated_descriptor_registration",
+ "path": "crates/event_store/src/generated.rs",
+ "byte_length": 100,
+ "sha256": "ddd71b2245f307cac0c8ef835311d0dd9eb2fdaf9d2a76a6c9dc98e56f4faaec",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "public_surface",
+ "path": "crates/event_store/src/lib.rs",
+ "byte_length": 3419,
+ "sha256": "d5bc071309fd4ec9e26a3cfe9c0e7c00b5de5b6c248902b888891b39e6780d23",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "migration_registry",
+ "path": "crates/event_store/src/migrations.rs",
+ "byte_length": 55541,
+ "sha256": "ef62808fe75b5c0704567eecccfe23d0b11ae25d332bc5e38451ea2a446c535a",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "model_registration",
+ "path": "crates/event_store/src/model.rs",
+ "byte_length": 33617,
+ "sha256": "79296b8f263aa06d17005795e4515f769f064ea6fd971eeb1296e1151debaf20",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "schema_hooks",
+ "path": "crates/event_store/src/schema.rs",
+ "byte_length": 97574,
+ "sha256": "6d259548559f32e259d3502787ccd4efbd22eebd1406dd7dd68c8f3145411543",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "store_ingest_and_wal_authority",
+ "path": "crates/event_store/src/store.rs",
+ "byte_length": 357693,
+ "sha256": "8bca94e5c7b26cee60a06ea327fcba385e6cbc51550f1774ef4d0488ffa2727b",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "addressable_transition_feed_model",
+ "path": "crates/event_store/src/model/addressable_transition_feed_v1.rs",
+ "byte_length": 22314,
+ "sha256": "b1c6b0a68f34459f7e14bd63857596154c0aa3fd02dc6c1661d543bb681324a7",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "current_visibility_model",
+ "path": "crates/event_store/src/model/current_visibility_v1.rs",
+ "byte_length": 5691,
+ "sha256": "25ec92f45006e2f66f2e1c8b954a021334bb1595b849c4d8529f289d3f7aeb25",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "food_projection_model",
+ "path": "crates/event_store/src/model/food_availability_projection_v1.rs",
+ "byte_length": 17493,
+ "sha256": "1e5ff9c05a81fda223ed1a27ff18a1b08bcdeaec9047a13fdd577390b3e0fdb9",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "addressable_transition_feed_store",
+ "path": "crates/event_store/src/store/addressable_transition_feed_v1.rs",
+ "byte_length": 40253,
+ "sha256": "fe23424aa1e6b39f9aba2dfa4470652b26b4990f91204a2bdfe379c03da9b610",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "current_visibility_store",
+ "path": "crates/event_store/src/store/current_visibility_v1.rs",
+ "byte_length": 15860,
+ "sha256": "8615086e674c30700305debcef11de5b3dbfe5aec735c0f58b4ac11caa518596",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "food_projection_store",
+ "path": "crates/event_store/src/store/food_availability_projection_v1.rs",
+ "byte_length": 49029,
+ "sha256": "0cec060c50c50d9333e0583e338fe311a1c64a1d586c222e391d8cef685cf871",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "predecessor_protocol_storage",
+ "path": "crates/event_store/src/store/protocol_storage_v1.rs",
+ "byte_length": 10975,
+ "sha256": "155c74d27eee5db1d6f0f844f9d319604eefbbf640f4b2371ac5d0e370816e50",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "predecessor_active_state_fast_validation",
+ "path": "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "byte_length": 183733,
+ "sha256": "3bddb664491bd9abe878f5b2441d1230fb188368dae341f3425a2b035211366a",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "post_core_capabilities",
+ "path": "crates/event_store/src/store/post_core_extension_capabilities.rs",
+ "byte_length": 1255,
+ "sha256": "cb434372156cb7ff31dac392d7095c2e5f44b128fae4e705516d6d000e2e2502",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "post_core_dispatcher",
+ "path": "crates/event_store/src/store/post_core_extension_dispatcher.rs",
+ "byte_length": 576,
+ "sha256": "df62ee92e9f165502d5e533997a47f533129fd3cffab2d9b2012e2ed22405f48",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "post_core_v2_extension",
+ "path": "crates/event_store/src/store/post_core_extensions_v2.rs",
+ "byte_length": 294,
+ "sha256": "8dcbc503ed9ea6fb06ed9a2a83b0804d928f9590b5706de25a057ad72c0d38d2",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "post_core_v2_storage",
+ "path": "crates/event_store/src/store/post_core_storage_v2.rs",
+ "byte_length": 632,
+ "sha256": "4b672770f3c34bf887e4cc949c068cb0c87396cb4af8efb6e13d39aa4e0d973a",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "predecessor_post_core_v1_extension",
+ "path": "crates/event_store/src/store/post_core_extensions_v1.rs",
+ "byte_length": 6935,
+ "sha256": "fb165704c64d982cf3be0a880c44985be6b375758451e94b2aaaf30881769f18",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "predecessor_post_core_v1_storage",
+ "path": "crates/event_store/src/store/post_core_storage_v1.rs",
+ "byte_length": 16871,
+ "sha256": "a6dca0884762cec3c32e460d17662ced9d0335f30e79b3d5fdb3461259d3ec19",
+ "hash_algorithm": "sha256_bytes_v1"
+ }
+ ],
+ "public_api": [
+ "RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1",
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1",
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1",
+ "RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1",
+ "RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1",
+ "RadrootsAddressableTransitionCauseV1",
+ "RadrootsAddressableTransitionCoordinateV1",
+ "RadrootsAddressableTransitionCursorV1",
+ "RadrootsAddressableTransitionEventReferenceV1",
+ "RadrootsAddressableTransitionOriginV1",
+ "RadrootsAddressableTransitionPageV1",
+ "RadrootsAddressableTransitionRawHeadDecisionV1",
+ "RadrootsAddressableTransitionScopeFingerprintV1",
+ "RadrootsAddressableTransitionScopeV1",
+ "RadrootsAddressableTransitionV1",
+ "RadrootsAddressableTransitionVisibilityV1",
+ "RadrootsCurrentEventVisibilityV1",
+ "RadrootsCurrentVisibilityDecisionV1",
+ "RadrootsFoodAvailabilitySearchQueryV1",
+ "RadrootsFoodAvailabilityStatusFilterV1",
+ "RadrootsNip09SuppressionEvidenceV1",
+ "RadrootsNip09SuppressionOutcome",
+ "RadrootsNip09SuppressionReason",
+ "RadrootsStoreProducedCanonicalEventV1",
+ "RadrootsStoredFoodAvailabilityImageV1",
+ "RadrootsStoredFoodAvailabilityV1"
+ ],
+ "result_vector": {
+ "canonical_path": "contracts/conformance/vectors/event_store/food_availability_projection.v1.json",
+ "mirror_path": "crates/event_store/tests/fixtures/food_availability_projection.v1.json",
+ "byte_length": 103659,
+ "sha256": "fca2b71b47736ed04ed1e908823b65b3fc3cf0366cb162128369fe328295bb63",
+ "hash_algorithm": "sha256_bytes_v1",
+ "executor_id": "radroots_event_store.food_availability_projection_v1.result_vector_executor.v1",
+ "executor_path": "crates/event_store/tests/food_availability_projection_v1_result_vector.rs",
+ "executor_test": "food_availability_projection_v1_result_vector",
+ "executor_byte_length": 34075,
+ "executor_sha256": "9e8e11abae7bbc7dda30eab6f0a79074ffc3761aa6b955cff58c4c62fa581aa3",
+ "executor_hash_algorithm": "sha256_bytes_v1"
+ }
+}
diff --git a/crates/event_store/contracts/food_availability_projection_v1.manifest.schema.json b/crates/event_store/contracts/food_availability_projection_v1.manifest.schema.json
@@ -0,0 +1,327 @@
+{
+ "$defs": {
+ "file": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "path": {
+ "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$",
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm"
+ ],
+ "type": "object"
+ },
+ "source_file": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "path": {
+ "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$",
+ "type": "string"
+ },
+ "role": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "role",
+ "path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm"
+ ],
+ "type": "object"
+ }
+ },
+ "$id": "https://radroots.org/core/event-store/food-availability-projection-manifest-v1.schema.json",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "properties": {
+ "contract_id": {
+ "const": "radroots_event_store.food_availability_projection_v1"
+ },
+ "entry_points": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "role": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "rust_path": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "required": [
+ "role",
+ "rust_path"
+ ],
+ "type": "object"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "food_profile_vector": {
+ "$ref": "#/$defs/file"
+ },
+ "hook_id": {
+ "const": "food_availability_projection_v1"
+ },
+ "manifest_schema": {
+ "$ref": "#/$defs/file"
+ },
+ "migration": {
+ "additionalProperties": false,
+ "properties": {
+ "catalog": {
+ "additionalProperties": false,
+ "properties": {
+ "fts5_tables": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ "objects": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ "tables": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ }
+ },
+ "required": [
+ "objects",
+ "tables",
+ "fts5_tables"
+ ],
+ "type": "object"
+ },
+ "down": {
+ "$ref": "#/$defs/file"
+ },
+ "name": {
+ "const": "food_availability_projection"
+ },
+ "schema_sha256": {
+ "const": "dd12467e04addcbddb5ea0f386c12a8ac05ef5ebaaf949f24dd2c62745f5aaac"
+ },
+ "up": {
+ "$ref": "#/$defs/file"
+ },
+ "version": {
+ "const": 3
+ }
+ },
+ "required": [
+ "version",
+ "name",
+ "up",
+ "down",
+ "schema_sha256",
+ "catalog"
+ ],
+ "type": "object"
+ },
+ "predecessor": {
+ "additionalProperties": false,
+ "properties": {
+ "hook_id": {
+ "const": "nip09_reconciliation_v1"
+ },
+ "manifest": {
+ "$ref": "#/$defs/file"
+ }
+ },
+ "required": [
+ "hook_id",
+ "manifest"
+ ],
+ "type": "object"
+ },
+ "profile": {
+ "additionalProperties": false,
+ "properties": {
+ "addressable_feed_version": {
+ "const": 1
+ },
+ "admission_authority": {
+ "const": "event_contract_registry_v7"
+ },
+ "current_visibility_authority": {
+ "const": "radroots_event_store_current_visibility_v1"
+ },
+ "event_contract_registry_version": {
+ "const": 7
+ },
+ "food_contract_id": {
+ "const": "radroots.food.availability.v1"
+ },
+ "post_core_capability": {
+ "const": "apply_v2"
+ },
+ "projection_version": {
+ "const": 1
+ },
+ "scope_fingerprint_sha256": {
+ "const": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0"
+ },
+ "scope_kinds": {
+ "maxItems": 1,
+ "minItems": 1,
+ "prefixItems": [
+ {
+ "const": 30402
+ }
+ ],
+ "type": "array"
+ }
+ },
+ "required": [
+ "event_contract_registry_version",
+ "addressable_feed_version",
+ "projection_version",
+ "scope_kinds",
+ "scope_fingerprint_sha256",
+ "food_contract_id",
+ "admission_authority",
+ "current_visibility_authority",
+ "post_core_capability"
+ ],
+ "type": "object"
+ },
+ "public_api": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array",
+ "uniqueItems": true
+ },
+ "registry_inventory": {
+ "$ref": "#/$defs/file"
+ },
+ "result_vector": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "canonical_path": {
+ "const": "contracts/conformance/vectors/event_store/food_availability_projection.v1.json"
+ },
+ "executor_byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "executor_hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "executor_id": {
+ "const": "radroots_event_store.food_availability_projection_v1.result_vector_executor.v1"
+ },
+ "executor_path": {
+ "const": "crates/event_store/tests/food_availability_projection_v1_result_vector.rs"
+ },
+ "executor_sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ },
+ "executor_test": {
+ "const": "food_availability_projection_v1_result_vector"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "mirror_path": {
+ "const": "crates/event_store/tests/fixtures/food_availability_projection.v1.json"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "canonical_path",
+ "mirror_path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm",
+ "executor_id",
+ "executor_path",
+ "executor_test",
+ "executor_byte_length",
+ "executor_sha256",
+ "executor_hash_algorithm"
+ ],
+ "type": "object"
+ },
+ "schema_version": {
+ "const": 1
+ },
+ "source_files": {
+ "items": {
+ "$ref": "#/$defs/source_file"
+ },
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "schema_version",
+ "contract_id",
+ "hook_id",
+ "manifest_schema",
+ "predecessor",
+ "migration",
+ "profile",
+ "registry_inventory",
+ "food_profile_vector",
+ "entry_points",
+ "source_files",
+ "public_api",
+ "result_vector"
+ ],
+ "title": "Radroots event-store FoodAvailability projection manifest v1",
+ "type": "object"
+}
diff --git a/crates/event_store/contracts/food_availability_projection_v1.manifest.sha256 b/crates/event_store/contracts/food_availability_projection_v1.manifest.sha256
@@ -0,0 +1 @@
+33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23
diff --git a/crates/event_store/migrations/0003_food_availability_projection.down.sql b/crates/event_store/migrations/0003_food_availability_projection.down.sql
@@ -0,0 +1,26 @@
+DROP TRIGGER radroots_event_store_food_availability_cursor_delete_guard;
+DROP TRIGGER radroots_event_store_food_availability_cursor_update_guard;
+DROP TRIGGER radroots_event_store_food_availability_cursor_insert_guard;
+DROP TRIGGER radroots_event_store_food_availability_search_delete;
+DROP TRIGGER radroots_event_store_food_availability_search_insert;
+DROP TRIGGER radroots_event_store_food_availability_image_delete_guard;
+DROP TRIGGER radroots_event_store_food_availability_image_update_guard;
+DROP TRIGGER radroots_event_store_food_availability_image_insert_guard;
+DROP TRIGGER radroots_event_store_food_availability_projection_delete_guard;
+DROP TRIGGER radroots_event_store_food_availability_projection_update_guard;
+DROP TRIGGER radroots_event_store_food_availability_projection_insert_guard;
+DROP TABLE radroots_event_store_food_availability_cursor;
+DROP TABLE radroots_event_store_food_availability_search_fts;
+DROP VIEW radroots_event_store_food_availability_read_v1;
+DROP TABLE radroots_event_store_food_availability_image;
+DROP INDEX radroots_event_store_food_availability_author_idx;
+DROP INDEX radroots_event_store_food_availability_recent_idx;
+DROP INDEX radroots_event_store_food_availability_status_idx;
+DROP TABLE radroots_event_store_food_availability_projection;
+DROP INDEX radroots_event_store_addressable_transition_coordinate_idx;
+DROP TRIGGER radroots_event_store_addressable_feed_transition_insert;
+DROP TRIGGER radroots_event_store_addressable_feed_generation_insert;
+DROP TABLE radroots_event_store_addressable_feed_integrity_v1;
+DROP INDEX radroots_event_store_nip09_address_target_visibility_lookup_idx;
+DROP INDEX radroots_event_store_current_visibility_head_lookup_idx;
+DROP VIEW radroots_event_store_current_visibility_v1;
diff --git a/crates/event_store/migrations/0003_food_availability_projection.up.sql b/crates/event_store/migrations/0003_food_availability_projection.up.sql
@@ -0,0 +1,638 @@
+CREATE VIEW radroots_event_store_current_visibility_v1 AS
+WITH active AS (
+ SELECT active_generation
+ FROM radroots_event_store_source_state
+ WHERE singleton = 1
+), event_facts AS (
+ SELECT
+ event.seq AS event_seq,
+ event.event_id,
+ event.pubkey,
+ event.created_at,
+ event.kind,
+ event.contract_status AS admission_status,
+ event.contract_id,
+ event.event_class,
+ active.active_generation AS source_generation,
+ coordinate.raw_d_tag,
+ coordinate.nip09_matchable,
+ coordinate.nip09_d_tag,
+ CASE
+ WHEN event.event_class = 'regular' THEN 1
+ WHEN head.event_id = event.event_id THEN 1
+ ELSE 0
+ END AS is_raw_head,
+ head.event_id AS raw_head_event_id
+ FROM event_envelopes AS event
+ CROSS JOIN active
+ LEFT JOIN radroots_event_store_event_coordinate AS coordinate
+ ON coordinate.source_generation = active.active_generation
+ AND coordinate.event_id = event.event_id
+ LEFT JOIN event_envelope_head AS head
+ ON head.coordinate_type = event.event_class
+ AND head.kind = event.kind
+ AND head.pubkey = event.pubkey
+ AND (
+ (event.event_class = 'replaceable' AND head.d_tag IS NULL)
+ OR (event.event_class = 'addressable' AND head.d_tag = coordinate.raw_d_tag)
+ )
+), evidence AS (
+ SELECT
+ fact.*,
+ CASE WHEN fact.kind != 5 THEN (
+ SELECT request.request_event_id
+ FROM radroots_event_store_nip09_event_target AS target
+ INDEXED BY radroots_event_store_nip09_event_target_lookup_idx
+ CROSS JOIN radroots_event_store_nip09_request AS request
+ ON request.source_generation = target.source_generation
+ AND request.request_event_id = target.request_event_id
+ WHERE target.source_generation = fact.source_generation
+ AND target.target_event_id = fact.event_id
+ AND request.request_pubkey = fact.pubkey
+ ORDER BY target.request_event_id
+ LIMIT 1
+ ) END AS event_reference_request_id,
+ CASE WHEN fact.kind != 5 THEN (
+ SELECT request.request_event_id
+ FROM radroots_event_store_nip09_address_target AS target
+ INDEXED BY radroots_event_store_nip09_address_target_visibility_lookup_idx
+ CROSS JOIN radroots_event_store_nip09_request AS request
+ ON request.source_generation = target.source_generation
+ AND request.request_event_id = target.request_event_id
+ WHERE target.source_generation = fact.source_generation
+ AND target.target_kind = fact.kind
+ AND target.target_pubkey = fact.pubkey
+ AND fact.nip09_matchable = 1
+ AND target.target_d_tag = fact.nip09_d_tag
+ AND request.request_pubkey = fact.pubkey
+ ORDER BY target.inclusive_cutoff DESC, target.request_event_id
+ LIMIT 1
+ ) END AS address_reference_request_id,
+ CASE WHEN fact.kind != 5 THEN (
+ SELECT target.inclusive_cutoff
+ FROM radroots_event_store_nip09_address_target AS target
+ INDEXED BY radroots_event_store_nip09_address_target_visibility_lookup_idx
+ CROSS JOIN radroots_event_store_nip09_request AS request
+ ON request.source_generation = target.source_generation
+ AND request.request_event_id = target.request_event_id
+ WHERE target.source_generation = fact.source_generation
+ AND target.target_kind = fact.kind
+ AND target.target_pubkey = fact.pubkey
+ AND fact.nip09_matchable = 1
+ AND target.target_d_tag = fact.nip09_d_tag
+ AND request.request_pubkey = fact.pubkey
+ ORDER BY target.inclusive_cutoff DESC, target.request_event_id
+ LIMIT 1
+ ) END AS address_reference_cutoff,
+ CASE WHEN fact.kind = 5 THEN 0 ELSE EXISTS (
+ SELECT 1
+ FROM radroots_event_store_nip09_event_target AS target
+ INDEXED BY radroots_event_store_nip09_event_target_lookup_idx
+ CROSS JOIN radroots_event_store_nip09_request AS request
+ ON request.source_generation = target.source_generation
+ AND request.request_event_id = target.request_event_id
+ WHERE target.source_generation = fact.source_generation
+ AND target.target_event_id = fact.event_id
+ AND request.request_pubkey != fact.pubkey
+ ) OR EXISTS (
+ SELECT 1
+ FROM radroots_event_store_nip09_address_target AS target
+ INDEXED BY radroots_event_store_nip09_address_target_visibility_lookup_idx
+ CROSS JOIN radroots_event_store_nip09_request AS request
+ ON request.source_generation = target.source_generation
+ AND request.request_event_id = target.request_event_id
+ WHERE target.source_generation = fact.source_generation
+ AND target.target_kind = fact.kind
+ AND target.target_pubkey = fact.pubkey
+ AND fact.nip09_matchable = 1
+ AND target.target_d_tag = fact.nip09_d_tag
+ AND request.request_pubkey != fact.pubkey
+ ) END AS has_unauthorized_reference
+ FROM event_facts AS fact
+), suppression AS (
+ SELECT
+ evidence.*,
+ CASE
+ WHEN admission_status != 'admitted' THEN NULL
+ WHEN kind = 5 THEN 'visible'
+ WHEN event_reference_request_id IS NOT NULL THEN 'suppressed'
+ WHEN address_reference_cutoff >= created_at THEN 'suppressed'
+ ELSE 'visible'
+ END AS suppression_outcome,
+ CASE
+ WHEN admission_status != 'admitted' THEN NULL
+ WHEN kind = 5 THEN 'deletion_request_immune'
+ WHEN event_reference_request_id IS NOT NULL
+ AND address_reference_cutoff >= created_at
+ THEN 'deletion_event_id_and_address_reference'
+ WHEN event_reference_request_id IS NOT NULL
+ THEN 'deletion_event_id_reference'
+ WHEN address_reference_cutoff >= created_at
+ THEN 'deletion_address_reference'
+ WHEN address_reference_request_id IS NOT NULL
+ THEN 'deletion_address_cutoff_precedes_target'
+ WHEN has_unauthorized_reference = 1
+ THEN 'deletion_request_author_mismatch'
+ ELSE 'deletion_no_authorized_reference'
+ END AS suppression_reason
+ FROM evidence
+)
+SELECT
+ event_seq,
+ event_id,
+ pubkey,
+ created_at,
+ kind,
+ admission_status,
+ contract_id,
+ event_class,
+ source_generation,
+ raw_d_tag,
+ is_raw_head,
+ raw_head_event_id,
+ suppression_outcome,
+ suppression_reason,
+ CASE WHEN admission_status = 'admitted'
+ THEN event_reference_request_id ELSE NULL END AS event_reference_request_id,
+ CASE WHEN admission_status = 'admitted'
+ THEN address_reference_request_id ELSE NULL END AS address_reference_request_id,
+ CASE WHEN admission_status = 'admitted'
+ THEN address_reference_cutoff ELSE NULL END AS address_reference_cutoff,
+ CASE
+ WHEN admission_status != 'admitted' THEN 'not_admitted'
+ WHEN is_raw_head = 0 THEN 'not_current'
+ WHEN suppression_outcome = 'suppressed' THEN 'suppressed'
+ ELSE 'visible'
+ END AS current_visibility
+FROM suppression;
+
+CREATE INDEX radroots_event_store_current_visibility_head_lookup_idx
+ON event_envelope_head(coordinate_type, kind, pubkey, d_tag);
+
+CREATE INDEX radroots_event_store_nip09_address_target_visibility_lookup_idx
+ON radroots_event_store_nip09_address_target(
+ source_generation,
+ target_kind,
+ target_pubkey,
+ target_d_tag,
+ inclusive_cutoff DESC,
+ request_event_id ASC
+);
+
+CREATE TABLE radroots_event_store_addressable_feed_integrity_v1 (
+ source_generation BLOB PRIMARY KEY NOT NULL
+ REFERENCES radroots_event_store_source_generation(source_generation)
+ ON DELETE RESTRICT,
+ transition_floor_seq INTEGER NOT NULL CHECK (transition_floor_seq >= 0),
+ last_transition_seq INTEGER NOT NULL CHECK (
+ last_transition_seq >= transition_floor_seq
+ ),
+ transition_count INTEGER NOT NULL CHECK (
+ transition_count >= 0
+ AND transition_count = last_transition_seq - transition_floor_seq
+ )
+) STRICT, WITHOUT ROWID;
+
+INSERT INTO radroots_event_store_addressable_feed_integrity_v1(
+ source_generation,
+ transition_floor_seq,
+ last_transition_seq,
+ transition_count
+)
+SELECT
+ generation.source_generation,
+ generation.transition_floor_seq,
+ COALESCE(MAX(transition.transition_seq), generation.transition_floor_seq),
+ COUNT(transition.transition_seq)
+FROM radroots_event_store_source_generation AS generation
+LEFT JOIN radroots_event_store_addressable_head_transition AS transition
+ ON transition.source_generation = generation.source_generation
+GROUP BY generation.source_generation, generation.transition_floor_seq;
+
+CREATE TRIGGER radroots_event_store_addressable_feed_generation_insert
+AFTER INSERT ON radroots_event_store_source_generation
+BEGIN
+ INSERT INTO radroots_event_store_addressable_feed_integrity_v1(
+ source_generation,
+ transition_floor_seq,
+ last_transition_seq,
+ transition_count
+ ) VALUES (
+ NEW.source_generation,
+ NEW.transition_floor_seq,
+ NEW.transition_floor_seq,
+ 0
+ );
+END;
+
+CREATE TRIGGER radroots_event_store_addressable_feed_transition_insert
+AFTER INSERT ON radroots_event_store_addressable_head_transition
+BEGIN
+ UPDATE radroots_event_store_addressable_feed_integrity_v1
+ SET
+ last_transition_seq = NEW.transition_seq,
+ transition_count = transition_count + 1
+ WHERE source_generation = NEW.source_generation
+ AND NEW.transition_seq = last_transition_seq + 1;
+ SELECT CASE
+ WHEN changes() != 1
+ THEN RAISE(ABORT, 'event-store addressable feed integrity advancement failed')
+ END;
+END;
+
+CREATE INDEX radroots_event_store_addressable_transition_coordinate_idx
+ON radroots_event_store_addressable_head_transition(
+ source_generation,
+ kind,
+ pubkey,
+ d_tag,
+ transition_seq
+);
+
+CREATE TABLE radroots_event_store_food_availability_projection (
+ source_generation BLOB NOT NULL
+ REFERENCES radroots_event_store_source_generation(source_generation)
+ ON DELETE RESTRICT,
+ kind INTEGER NOT NULL CHECK (kind = 30402),
+ pubkey TEXT NOT NULL CHECK (
+ length(pubkey) = 64
+ AND pubkey = lower(pubkey)
+ AND pubkey NOT GLOB '*[^0-9a-f]*'
+ ),
+ d_tag TEXT NOT NULL CHECK (length(d_tag) > 0),
+ event_id TEXT NOT NULL CHECK (
+ length(event_id) = 64
+ AND event_id = lower(event_id)
+ AND event_id NOT GLOB '*[^0-9a-f]*'
+ ),
+ event_seq INTEGER NOT NULL CHECK (event_seq > 0),
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
+ contract_id TEXT NOT NULL CHECK (contract_id = 'radroots.food.availability.v1'),
+ content TEXT NOT NULL,
+ title TEXT NOT NULL CHECK (length(title) > 0),
+ summary TEXT NOT NULL CHECK (length(summary) > 0),
+ published_at INTEGER NOT NULL CHECK (published_at > 0 AND published_at <= created_at),
+ location TEXT NOT NULL CHECK (length(location) > 0),
+ price_amount TEXT NOT NULL CHECK (length(price_amount) > 0),
+ price_currency TEXT NOT NULL CHECK (
+ length(price_currency) = 3
+ AND price_currency NOT GLOB '*[^A-Z]*'
+ ),
+ price_unit TEXT NOT NULL CHECK (
+ price_unit IN ('g', 'kg', 'lb', 'oz', 'each', 'dozen', 'bunch', 'punnet', 'bag', 'basket')
+ ),
+ quantity_amount TEXT,
+ quantity_unit TEXT,
+ status TEXT NOT NULL CHECK (status IN ('active', 'sold')),
+ diagnostic_codes_json TEXT NOT NULL CHECK (
+ json_valid(diagnostic_codes_json)
+ AND json_type(diagnostic_codes_json) = 'array'
+ ),
+ source_transition_seq INTEGER NOT NULL CHECK (source_transition_seq > 0),
+ CHECK (
+ (quantity_amount IS NULL AND quantity_unit IS NULL)
+ OR (
+ quantity_amount IS NOT NULL
+ AND length(quantity_amount) > 0
+ AND quantity_unit = price_unit
+ )
+ ),
+ PRIMARY KEY (source_generation, pubkey, d_tag),
+ UNIQUE (source_generation, event_id),
+ UNIQUE (source_generation, event_seq),
+ FOREIGN KEY (event_seq, event_id)
+ REFERENCES event_envelopes(seq, event_id) ON DELETE RESTRICT,
+ FOREIGN KEY (source_transition_seq)
+ REFERENCES radroots_event_store_addressable_head_transition(transition_seq)
+ ON DELETE RESTRICT
+) STRICT, WITHOUT ROWID;
+
+CREATE INDEX radroots_event_store_food_availability_status_idx
+ON radroots_event_store_food_availability_projection(
+ source_generation,
+ status,
+ published_at DESC,
+ event_id
+);
+
+CREATE INDEX radroots_event_store_food_availability_recent_idx
+ON radroots_event_store_food_availability_projection(
+ source_generation,
+ published_at DESC,
+ event_id
+);
+
+CREATE INDEX radroots_event_store_food_availability_author_idx
+ON radroots_event_store_food_availability_projection(
+ source_generation,
+ pubkey,
+ published_at DESC,
+ event_id
+);
+
+CREATE TABLE radroots_event_store_food_availability_image (
+ source_generation BLOB NOT NULL,
+ pubkey TEXT NOT NULL,
+ d_tag TEXT NOT NULL,
+ image_index INTEGER NOT NULL CHECK (image_index >= 0 AND image_index < 64),
+ raw_tag_json TEXT NOT NULL CHECK (
+ json_valid(raw_tag_json)
+ AND json_type(raw_tag_json) = 'array'
+ ),
+ url TEXT,
+ width INTEGER CHECK (width IS NULL OR width > 0),
+ height INTEGER CHECK (height IS NULL OR height > 0),
+ blossom_sha256 TEXT CHECK (
+ blossom_sha256 IS NULL
+ OR (
+ length(blossom_sha256) = 64
+ AND blossom_sha256 = lower(blossom_sha256)
+ AND blossom_sha256 NOT GLOB '*[^0-9a-f]*'
+ )
+ ),
+ qualifies INTEGER NOT NULL CHECK (qualifies IN (0, 1)),
+ diagnostic_codes_json TEXT NOT NULL CHECK (
+ json_valid(diagnostic_codes_json)
+ AND json_type(diagnostic_codes_json) = 'array'
+ ),
+ PRIMARY KEY (source_generation, pubkey, d_tag, image_index),
+ FOREIGN KEY (source_generation, pubkey, d_tag)
+ REFERENCES radroots_event_store_food_availability_projection(
+ source_generation,
+ pubkey,
+ d_tag
+ ) ON DELETE CASCADE
+) STRICT, WITHOUT ROWID;
+
+CREATE VIEW radroots_event_store_food_availability_read_v1 AS
+SELECT
+ projection.*,
+ event.raw_json AS immutable_raw_json,
+ (
+ SELECT json_group_array(json_object(
+ 'image_index', ordered_image.image_index,
+ 'raw_tag_json', ordered_image.raw_tag_json,
+ 'url', ordered_image.url,
+ 'width', ordered_image.width,
+ 'height', ordered_image.height,
+ 'blossom_sha256', ordered_image.blossom_sha256,
+ 'qualifies', ordered_image.qualifies,
+ 'diagnostic_codes_json', ordered_image.diagnostic_codes_json
+ ))
+ FROM (
+ SELECT
+ image.image_index,
+ image.raw_tag_json,
+ image.url,
+ image.width,
+ image.height,
+ image.blossom_sha256,
+ image.qualifies,
+ image.diagnostic_codes_json
+ FROM radroots_event_store_food_availability_image AS image
+ WHERE image.source_generation = projection.source_generation
+ AND image.pubkey = projection.pubkey
+ AND image.d_tag = projection.d_tag
+ ORDER BY image.image_index
+ ) AS ordered_image
+ ) AS stored_images_json
+FROM radroots_event_store_food_availability_projection AS projection
+JOIN event_envelopes AS event
+ ON event.seq = projection.event_seq
+ AND event.event_id = projection.event_id;
+
+CREATE VIRTUAL TABLE radroots_event_store_food_availability_search_fts USING fts5(
+ event_id UNINDEXED,
+ pubkey UNINDEXED,
+ d_tag UNINDEXED,
+ title,
+ summary,
+ content,
+ location,
+ tokenize = 'unicode61'
+);
+
+CREATE TABLE radroots_event_store_food_availability_cursor (
+ singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
+ source_generation BLOB NOT NULL
+ REFERENCES radroots_event_store_source_generation(source_generation)
+ ON DELETE RESTRICT,
+ feed_version INTEGER NOT NULL CHECK (feed_version = 1),
+ projection_version INTEGER NOT NULL CHECK (projection_version = 1),
+ scope_fingerprint BLOB NOT NULL CHECK (
+ length(scope_fingerprint) = 32
+ AND hex(scope_fingerprint) = '8B63C5DDC48A2CC7DB69295238B96D5F814DBA50427C80B4D0079F061E6D3DE0'
+ ),
+ hook_manifest_sha256 TEXT NOT NULL CHECK (
+ length(hook_manifest_sha256) = 64
+ AND hook_manifest_sha256 NOT GLOB '*[^0-9a-f]*'
+ ),
+ last_transition_seq INTEGER NOT NULL CHECK (last_transition_seq >= 0),
+ projected_row_count INTEGER NOT NULL CHECK (projected_row_count >= 0)
+) STRICT;
+
+CREATE TRIGGER radroots_event_store_food_availability_projection_insert_guard
+BEFORE INSERT ON radroots_event_store_food_availability_projection
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_source_state AS source
+ ON source.active_generation = cursor.source_generation
+ JOIN radroots_event_store_addressable_head_transition AS transition
+ ON transition.source_generation = source.active_generation
+ AND transition.transition_seq = NEW.source_transition_seq
+ AND transition.transition_seq > cursor.last_transition_seq
+ AND transition.kind = NEW.kind
+ AND transition.pubkey = NEW.pubkey
+ AND transition.d_tag = NEW.d_tag
+ AND transition.visible_event_id = NEW.event_id
+ AND transition.visible_event_seq = NEW.event_seq
+ AND transition.contract_id = NEW.contract_id
+ AND transition.visibility = 'visible'
+ WHERE source.singleton = 1
+ AND source.active_generation = NEW.source_generation
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability projection insert is not backed by a visible transition');
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_projection_update_guard
+BEFORE UPDATE ON radroots_event_store_food_availability_projection
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability projection rows are replaced by delete and insert');
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_projection_delete_guard
+BEFORE DELETE ON radroots_event_store_food_availability_projection
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_source_state AS source ON source.singleton = 1
+ WHERE cursor.singleton = 1
+ AND cursor.source_generation != source.active_generation
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_addressable_head_transition AS transition
+ ON transition.source_generation = cursor.source_generation
+ AND transition.transition_seq > cursor.last_transition_seq
+ AND transition.kind = OLD.kind
+ AND transition.pubkey = OLD.pubkey
+ AND transition.d_tag = OLD.d_tag
+ AND transition.retracted_event_id = OLD.event_id
+ WHERE cursor.singleton = 1
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability projection delete is not backed by a pending retraction');
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_image_insert_guard
+BEFORE INSERT ON radroots_event_store_food_availability_image
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_projection AS projection
+ JOIN radroots_event_store_food_availability_cursor AS cursor
+ ON cursor.source_generation = projection.source_generation
+ WHERE projection.source_generation = NEW.source_generation
+ AND projection.pubkey = NEW.pubkey
+ AND projection.d_tag = NEW.d_tag
+ AND projection.source_transition_seq > cursor.last_transition_seq
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability image insert is not backed by a pending projection');
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_image_update_guard
+BEFORE UPDATE ON radroots_event_store_food_availability_image
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability image rows are immutable');
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_image_delete_guard
+BEFORE DELETE ON radroots_event_store_food_availability_image
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_source_state AS source ON source.singleton = 1
+ WHERE cursor.singleton = 1
+ AND cursor.source_generation != source.active_generation
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_addressable_head_transition AS transition
+ ON transition.source_generation = cursor.source_generation
+ AND transition.transition_seq > cursor.last_transition_seq
+ AND transition.kind = 30402
+ AND transition.pubkey = OLD.pubkey
+ AND transition.d_tag = OLD.d_tag
+ AND transition.retracted_event_id IS NOT NULL
+ WHERE cursor.singleton = 1
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability image delete is not backed by a pending retraction');
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_search_insert
+AFTER INSERT ON radroots_event_store_food_availability_projection
+BEGIN
+ INSERT INTO radroots_event_store_food_availability_search_fts(
+ rowid,
+ event_id,
+ pubkey,
+ d_tag,
+ title,
+ summary,
+ content,
+ location
+ ) VALUES (
+ NEW.event_seq,
+ NEW.event_id,
+ NEW.pubkey,
+ NEW.d_tag,
+ NEW.title,
+ NEW.summary,
+ NEW.content,
+ NEW.location
+ );
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_search_delete
+AFTER DELETE ON radroots_event_store_food_availability_projection
+BEGIN
+ DELETE FROM radroots_event_store_food_availability_search_fts
+ WHERE rowid = OLD.event_seq;
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_cursor_insert_guard
+BEFORE INSERT ON radroots_event_store_food_availability_cursor
+WHEN EXISTS (
+ SELECT 1 FROM radroots_event_store_food_availability_cursor
+)
+OR EXISTS (
+ SELECT 1 FROM radroots_event_store_food_availability_projection
+)
+OR NEW.feed_version != 1
+OR NEW.projection_version != 1
+OR NEW.projected_row_count != 0
+OR hex(NEW.scope_fingerprint) != '8B63C5DDC48A2CC7DB69295238B96D5F814DBA50427C80B4D0079F061E6D3DE0'
+OR NEW.hook_manifest_sha256 != lower(NEW.hook_manifest_sha256)
+OR NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_state AS source
+ JOIN radroots_event_store_source_generation AS generation
+ ON generation.source_generation = source.active_generation
+ JOIN radroots_event_store_addressable_feed_integrity_v1 AS integrity
+ ON integrity.source_generation = source.active_generation
+ WHERE source.singleton = 1
+ AND source.active_generation = NEW.source_generation
+ AND generation.addressable_feed_version = NEW.feed_version
+ AND NEW.last_transition_seq = generation.transition_floor_seq
+ AND integrity.transition_floor_seq = generation.transition_floor_seq
+ AND integrity.last_transition_seq = source.last_transition_seq
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability cursor identity is invalid');
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_cursor_update_guard
+BEFORE UPDATE ON radroots_event_store_food_availability_cursor
+WHEN NEW.singleton != OLD.singleton
+OR NEW.source_generation != OLD.source_generation
+OR NEW.feed_version != OLD.feed_version
+OR NEW.projection_version != OLD.projection_version
+OR NEW.scope_fingerprint != OLD.scope_fingerprint
+OR NEW.hook_manifest_sha256 != OLD.hook_manifest_sha256
+OR NEW.last_transition_seq <= OLD.last_transition_seq
+OR NEW.last_transition_seq - OLD.last_transition_seq > 1024
+OR abs(NEW.projected_row_count - OLD.projected_row_count) > 64
+OR abs(NEW.projected_row_count - OLD.projected_row_count) > (
+ SELECT COUNT(*)
+ FROM radroots_event_store_addressable_head_transition AS transition
+ WHERE transition.source_generation = NEW.source_generation
+ AND transition.transition_seq > OLD.last_transition_seq
+ AND transition.transition_seq <= NEW.last_transition_seq
+ AND transition.kind = 30402
+)
+OR NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_state AS source
+ WHERE source.singleton = 1
+ AND source.active_generation = NEW.source_generation
+ AND NEW.last_transition_seq <= source.last_transition_seq
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability cursor update is invalid');
+END;
+
+CREATE TRIGGER radroots_event_store_food_availability_cursor_delete_guard
+BEFORE DELETE ON radroots_event_store_food_availability_cursor
+WHEN OLD.source_generation = (
+ SELECT active_generation
+ FROM radroots_event_store_source_state
+ WHERE singleton = 1
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability cursor is immutable for the active generation');
+END;
diff --git a/crates/event_store/src/error.rs b/crates/event_store/src/error.rs
@@ -60,8 +60,50 @@ pub enum RadrootsEventStoreError {
EmptyContractList,
#[error("event-store contract list length {actual} exceeds {max}")]
ContractListTooLarge { max: usize, actual: usize },
+ #[error("addressable transition scope cannot be empty")]
+ AddressableTransitionScopeEmpty,
+ #[error("addressable transition scope length {actual} exceeds {max}")]
+ AddressableTransitionScopeTooLarge { max: usize, actual: usize },
+ #[error("addressable transition scope kind {kind} is outside 30000..=39999")]
+ AddressableTransitionScopeKindInvalid { kind: u32 },
+ #[error("addressable transition cursor sequence cannot be negative: {value}")]
+ AddressableTransitionCursorNegative { value: i64 },
+ #[error("addressable transition cursor JSON is {actual} bytes; maximum is {max}")]
+ AddressableTransitionCursorTooLarge { max: usize, actual: usize },
+ #[error("addressable transition cursor field `{field}` is not canonical lowercase 32-byte hex")]
+ AddressableTransitionCursorEncoding { field: &'static str },
+ #[error(
+ "addressable transition cursor feed version mismatch: expected {expected}, found {actual}"
+ )]
+ AddressableTransitionFeedVersionMismatch { expected: u32, actual: u32 },
+ #[error("addressable transition cursor scope fingerprint does not match the requested scope")]
+ AddressableTransitionScopeMismatch,
+ #[error("addressable transition cursor source generation is no longer active")]
+ AddressableTransitionSourceGenerationMismatch,
+ #[error("addressable transition cursor {cursor} precedes the active generation floor {floor}")]
+ AddressableTransitionCursorExpired { cursor: i64, floor: i64 },
+ #[error("addressable transition cursor {cursor} is ahead of source high-water {high_water}")]
+ AddressableTransitionCursorAhead { cursor: i64, high_water: i64 },
+ #[error("addressable transition feed sequence interval has a gap: {reason}")]
+ AddressableTransitionSequenceGap { reason: String },
+ #[error("addressable transition feed contains corrupt authority: {reason}")]
+ AddressableTransitionCorruption { reason: String },
+ #[error("addressable transition canonical payload is {actual} bytes; page maximum is {max}")]
+ AddressableTransitionPagePayloadTooLarge { max: usize, actual: usize },
+ #[error("event-store current-visibility authority is inconsistent: {reason}")]
+ CurrentVisibilityDrift { reason: String },
+ #[error("FoodAvailability projection authority is inconsistent: {reason}")]
+ FoodAvailabilityProjectionDrift { reason: String },
+ #[error("FoodAvailability search query must not be empty")]
+ FoodAvailabilitySearchEmpty,
+ #[error("FoodAvailability search query is {actual} bytes; maximum is {max}")]
+ FoodAvailabilitySearchTooLarge { max: usize, actual: usize },
+ #[error("FoodAvailability search query has {actual} terms; maximum is {max}")]
+ FoodAvailabilitySearchTooManyTerms { max: usize, actual: usize },
#[error("event-store query limit {actual} is outside {min}..={max}")]
QueryLimitOutOfRange { min: u32, max: u32, actual: u32 },
+ #[error("event visibility batch contains more than {max} event ids")]
+ EventVisibilityBatchTooLarge { max: usize },
#[error(
"an in-memory event-store pool must have exactly one connection, configured maximum was {actual}"
)]
@@ -73,6 +115,10 @@ pub enum RadrootsEventStoreError {
#[error("event-store SQLite connection has no main database")]
SqliteMainDatabaseUnavailable,
#[error(
+ "event-store SQLite file connection did not enter WAL journal mode; reported `{actual}`"
+ )]
+ SqliteFileJournalModeNotWal { actual: String },
+ #[error(
"temporary schema object `{name}` ({object_type}, table `{table_name}`) collides with event-store authority"
)]
TemporarySchemaCollision {
diff --git a/crates/event_store/src/generated.rs b/crates/event_store/src/generated.rs
@@ -1 +1,2 @@
+pub(crate) mod food_availability_projection_manifest;
pub(crate) mod nip09_reconciliation_manifest;
diff --git a/crates/event_store/src/generated/food_availability_projection_manifest.rs b/crates/event_store/src/generated/food_availability_projection_manifest.rs
@@ -0,0 +1,35 @@
+// @generated by `cargo xtask contract food-availability-projection-manifest --write`; do not edit.
+#![allow(dead_code)]
+
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.food_availability_projection_v1\",\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.schema.json\",\n \"byte_length\": 7964,\n \"sha256\": \"39171f6ef872a8d1483bc3d55049df5e0d110d9131c5adb4450b7c418f546910\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"nip09_reconciliation_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/nip09_reconciliation_v1.manifest.json\",\n \"byte_length\": 537538,\n \"sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 3,\n \"name\": \"food_availability_projection\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0003_food_availability_projection.up.sql\",\n \"byte_length\": 23683,\n \"sha256\": \"4e7edfb981b25f76055efc7802ec30b4034eeae9b9c0809ea4ea7c574678748a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0003_food_availability_projection.down.sql\",\n \"byte_length\": 1755,\n \"sha256\": \"29d663320109d9dd0df6a00b6a53d8d988438d01f7a66960a9d4ba3482ffffb8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"dd12467e04addcbddb5ea0f386c12a8ac05ef5ebaaf949f24dd2c62745f5aaac\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_addressable_feed_generation_insert\",\n \"radroots_event_store_addressable_feed_integrity_v1\",\n \"radroots_event_store_addressable_feed_transition_insert\",\n \"radroots_event_store_addressable_transition_coordinate_idx\",\n \"radroots_event_store_current_visibility_head_lookup_idx\",\n \"radroots_event_store_current_visibility_v1\",\n \"radroots_event_store_food_availability_author_idx\",\n \"radroots_event_store_food_availability_cursor\",\n \"radroots_event_store_food_availability_cursor_delete_guard\",\n \"radroots_event_store_food_availability_cursor_insert_guard\",\n \"radroots_event_store_food_availability_cursor_update_guard\",\n \"radroots_event_store_food_availability_image\",\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_image_insert_guard\",\n \"radroots_event_store_food_availability_image_update_guard\",\n \"radroots_event_store_food_availability_projection\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_food_availability_projection_insert_guard\",\n \"radroots_event_store_food_availability_projection_update_guard\",\n \"radroots_event_store_food_availability_read_v1\",\n \"radroots_event_store_food_availability_recent_idx\",\n \"radroots_event_store_food_availability_search_delete\",\n \"radroots_event_store_food_availability_search_fts\",\n \"radroots_event_store_food_availability_search_fts_config\",\n \"radroots_event_store_food_availability_search_fts_content\",\n \"radroots_event_store_food_availability_search_fts_data\",\n \"radroots_event_store_food_availability_search_fts_docsize\",\n \"radroots_event_store_food_availability_search_fts_idx\",\n \"radroots_event_store_food_availability_search_insert\",\n \"radroots_event_store_food_availability_status_idx\",\n \"radroots_event_store_nip09_address_target_visibility_lookup_idx\"\n ],\n \"tables\": [\n \"radroots_event_store_addressable_feed_integrity_v1\",\n \"radroots_event_store_food_availability_cursor\",\n \"radroots_event_store_food_availability_image\",\n \"radroots_event_store_food_availability_projection\",\n \"radroots_event_store_food_availability_search_fts\",\n \"radroots_event_store_food_availability_search_fts_config\",\n \"radroots_event_store_food_availability_search_fts_content\",\n \"radroots_event_store_food_availability_search_fts_data\",\n \"radroots_event_store_food_availability_search_fts_docsize\",\n \"radroots_event_store_food_availability_search_fts_idx\"\n ],\n \"fts5_tables\": [\n \"radroots_event_store_food_availability_search_fts\"\n ]\n }\n },\n \"profile\": {\n \"event_contract_registry_version\": 7,\n \"addressable_feed_version\": 1,\n \"projection_version\": 1,\n \"scope_kinds\": [\n 30402\n ],\n \"scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"food_contract_id\": \"radroots.food.availability.v1\",\n \"admission_authority\": \"event_contract_registry_v7\",\n \"current_visibility_authority\": \"radroots_event_store_current_visibility_v1\",\n \"post_core_capability\": \"apply_v2\"\n },\n \"registry_inventory\": {\n \"path\": \"contracts/event_store/event_contract_registry_v7.inventory.json\",\n \"byte_length\": 158021,\n \"sha256\": \"91595544310f865bdef064ee760c227c870417a95b87b3e27278f8da74fdddea\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"food_profile_vector\": {\n \"path\": \"contracts/conformance/vectors/food_availability/profile.v1.json\",\n \"byte_length\": 90099,\n \"sha256\": \"dede1eb1f682ecd548e8cd3ccb251d298762e504e2588a73528e5f7a5b9eff21\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"entry_points\": [\n {\n \"role\": \"registry_v7_admission\",\n \"rust_path\": \"radroots_event_codec::admit_verified_event_registry_v7\"\n },\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[2]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"post_core_extension\",\n \"rust_path\": \"radroots_event_store::store::PostCoreExtensionCapabilities::apply_v2\"\n },\n {\n \"role\": \"addressable_transition_feed\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::addressable_transition_page_v1\"\n },\n {\n \"role\": \"current_visibility\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::current_event_visibility_v1\"\n },\n {\n \"role\": \"event_visibility_batch\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::event_visibilities\"\n },\n {\n \"role\": \"projection_lookup\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::food_availability_v1\"\n },\n {\n \"role\": \"projection_recent\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::recent_food_availability_v1\"\n },\n {\n \"role\": \"projection_search\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::search_food_availability_v1\"\n },\n {\n \"role\": \"projection_audit\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::audit_food_availability_projection_v1\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"food_availability_projection_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 10836,\n \"sha256\": \"285532dbb0894204843a832880f136ceac5ee312a3203ff951fb0551fac63ec4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_dependency_authority\",\n \"path\": \"crates/event_store/Cargo.toml\",\n \"byte_length\": 1466,\n \"sha256\": \"9e82d57b64bb7fcf145ee8b919682dd6d788b5503be743d11326f5b882fb2d7e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_public_surface\",\n \"path\": \"crates/blossom/src/lib.rs\",\n \"byte_length\": 1335,\n \"sha256\": \"61ebee86f02887c45507bab052686da082f74ae26f23d18ff4019e02c70097bd\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_sha256_value_object\",\n \"path\": \"crates/blossom/src/hash.rs\",\n \"byte_length\": 11251,\n \"sha256\": \"753eff6ef9a810045c88839d39a46b37d62a4ad0a5ea57aa30e2a8219e3cdbda\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_event_contract\",\n \"path\": \"crates/event/src/food_availability.rs\",\n \"byte_length\": 45680,\n \"sha256\": \"1c8c3fba6514f9b246545b3305e8ac2742258857d1b813fa34c5fbefa6135e12\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_admission\",\n \"path\": \"crates/event_codec/src/food_availability/admission.rs\",\n \"byte_length\": 5799,\n \"sha256\": \"727ebf8f086c14376aba745a0a02b269115fed01e8ce95f90fdd211b9640f842\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"registry_v7_admission\",\n \"path\": \"crates/event_codec/src/admission/registry_v7.rs\",\n \"byte_length\": 5350,\n \"sha256\": \"755e63dd7ad1115c219e5a3ea540bef67d2770fc9f2a5aa6ba97c925c99bdced\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"registry_v7_food_projection\",\n \"path\": \"crates/event_codec/src/food_availability/inbound/registry_v7.rs\",\n \"byte_length\": 26865,\n \"sha256\": \"908015346eaec97bee6f44fe2a524410b701529ac19a5dab4505706d40d527da\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_surface\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 17557,\n \"sha256\": \"ac206fafdc67e4c25f4a2f1bcd38eb34ae224b38aca55894b1a9cc67242d8b1a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 100,\n \"sha256\": \"ddd71b2245f307cac0c8ef835311d0dd9eb2fdaf9d2a76a6c9dc98e56f4faaec\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3419,\n \"sha256\": \"d5bc071309fd4ec9e26a3cfe9c0e7c00b5de5b6c248902b888891b39e6780d23\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 55541,\n \"sha256\": \"ef62808fe75b5c0704567eecccfe23d0b11ae25d332bc5e38451ea2a446c535a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"model_registration\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 33617,\n \"sha256\": \"79296b8f263aa06d17005795e4515f769f064ea6fd971eeb1296e1151debaf20\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_hooks\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 97574,\n \"sha256\": \"6d259548559f32e259d3502787ccd4efbd22eebd1406dd7dd68c8f3145411543\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"store_ingest_and_wal_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 357693,\n \"sha256\": \"8bca94e5c7b26cee60a06ea327fcba385e6cbc51550f1774ef4d0488ffa2727b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_model\",\n \"path\": \"crates/event_store/src/model/addressable_transition_feed_v1.rs\",\n \"byte_length\": 22314,\n \"sha256\": \"b1c6b0a68f34459f7e14bd63857596154c0aa3fd02dc6c1661d543bb681324a7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_model\",\n \"path\": \"crates/event_store/src/model/current_visibility_v1.rs\",\n \"byte_length\": 5691,\n \"sha256\": \"25ec92f45006e2f66f2e1c8b954a021334bb1595b849c4d8529f289d3f7aeb25\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_model\",\n \"path\": \"crates/event_store/src/model/food_availability_projection_v1.rs\",\n \"byte_length\": 17493,\n \"sha256\": \"1e5ff9c05a81fda223ed1a27ff18a1b08bcdeaec9047a13fdd577390b3e0fdb9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_store\",\n \"path\": \"crates/event_store/src/store/addressable_transition_feed_v1.rs\",\n \"byte_length\": 40253,\n \"sha256\": \"fe23424aa1e6b39f9aba2dfa4470652b26b4990f91204a2bdfe379c03da9b610\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_store\",\n \"path\": \"crates/event_store/src/store/current_visibility_v1.rs\",\n \"byte_length\": 15860,\n \"sha256\": \"8615086e674c30700305debcef11de5b3dbfe5aec735c0f58b4ac11caa518596\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_store\",\n \"path\": \"crates/event_store/src/store/food_availability_projection_v1.rs\",\n \"byte_length\": 49029,\n \"sha256\": \"0cec060c50c50d9333e0583e338fe311a1c64a1d586c222e391d8cef685cf871\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_protocol_storage\",\n \"path\": \"crates/event_store/src/store/protocol_storage_v1.rs\",\n \"byte_length\": 10975,\n \"sha256\": \"155c74d27eee5db1d6f0f844f9d319604eefbbf640f4b2371ac5d0e370816e50\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_active_state_fast_validation\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 183733,\n \"sha256\": \"3bddb664491bd9abe878f5b2441d1230fb188368dae341f3425a2b035211366a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"post_core_capabilities\",\n \"path\": \"crates/event_store/src/store/post_core_extension_capabilities.rs\",\n \"byte_length\": 1255,\n \"sha256\": \"cb434372156cb7ff31dac392d7095c2e5f44b128fae4e705516d6d000e2e2502\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"post_core_dispatcher\",\n \"path\": \"crates/event_store/src/store/post_core_extension_dispatcher.rs\",\n \"byte_length\": 576,\n \"sha256\": \"df62ee92e9f165502d5e533997a47f533129fd3cffab2d9b2012e2ed22405f48\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"post_core_v2_extension\",\n \"path\": \"crates/event_store/src/store/post_core_extensions_v2.rs\",\n \"byte_length\": 294,\n \"sha256\": \"8dcbc503ed9ea6fb06ed9a2a83b0804d928f9590b5706de25a057ad72c0d38d2\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"post_core_v2_storage\",\n \"path\": \"crates/event_store/src/store/post_core_storage_v2.rs\",\n \"byte_length\": 632,\n \"sha256\": \"4b672770f3c34bf887e4cc949c068cb0c87396cb4af8efb6e13d39aa4e0d973a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_extension\",\n \"path\": \"crates/event_store/src/store/post_core_extensions_v1.rs\",\n \"byte_length\": 6935,\n \"sha256\": \"fb165704c64d982cf3be0a880c44985be6b375758451e94b2aaaf30881769f18\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_storage\",\n \"path\": \"crates/event_store/src/store/post_core_storage_v1.rs\",\n \"byte_length\": 16871,\n \"sha256\": \"a6dca0884762cec3c32e460d17662ced9d0335f30e79b3d5fdb3461259d3ec19\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/food_availability_projection.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/food_availability_projection.v1.json\",\n \"byte_length\": 103659,\n \"sha256\": \"fca2b71b47736ed04ed1e908823b65b3fc3cf0366cb162128369fe328295bb63\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.food_availability_projection_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/food_availability_projection_v1_result_vector.rs\",\n \"executor_test\": \"food_availability_projection_v1_result_vector\",\n \"executor_byte_length\": 34075,\n \"executor_sha256\": \"9e8e11abae7bbc7dda30eab6f0a79074ffc3761aa6b955cff58c4c62fa581aa3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MANIFEST_BYTE_LENGTH: usize = 17455;
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256: &str =
+ "33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MANIFEST_SCHEMA_VERSION: u32 = 1;
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_CONTRACT_ID: &str =
+ "radroots_event_store.food_availability_projection_v1";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_HOOK_ID: &str = "food_availability_projection_v1";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION: u32 = 3;
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_NAME: &str = "food_availability_projection";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_VERSION: u32 = 1;
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_FEED_VERSION: u32 = 1;
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION: u32 = 7;
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_BYTE_LENGTH: usize = 23683;
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_SHA256: &str =
+ "4e7edfb981b25f76055efc7802ec30b4034eeae9b9c0809ea4ea7c574678748a";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_BYTE_LENGTH: usize = 1755;
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_SHA256: &str =
+ "29d663320109d9dd0df6a00b6a53d8d988438d01f7a66960a9d4ba3482ffffb8";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_SCHEMA_SHA256: &str =
+ "dd12467e04addcbddb5ea0f386c12a8ac05ef5ebaaf949f24dd2c62745f5aaac";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_SCOPE_KINDS: &[u32] = &[30_402];
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_SCOPE_FINGERPRINT_SHA256: &str =
+ "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_PREDECESSOR_MANIFEST_SHA256: &str =
+ "74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_SHA256: &str =
+ "fca2b71b47736ed04ed1e908823b65b3fc3cf0366cb162128369fe328295bb63";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_ID: &str =
+ "radroots_event_store.food_availability_projection_v1.result_vector_executor.v1";
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_SHA256: &str =
+ "9e8e11abae7bbc7dda30eab6f0a79074ffc3761aa6b955cff58c4c62fa581aa3";
diff --git a/crates/event_store/src/lib.rs b/crates/event_store/src/lib.rs
@@ -24,12 +24,33 @@ pub use migrations::{
};
#[cfg(feature = "sqlite")]
pub use model::{
- RADROOTS_TRANSPORT_OBSERVATION_MESSAGE_MAX_BYTES, RadrootsEventAdmissionStatus,
- RadrootsEventIngest, RadrootsEventIngestReceipt, RadrootsEventPersistence,
- RadrootsEventStoreSourceGeneration, RadrootsEventStoreStatusSummary, RadrootsEventVisibility,
- RadrootsProjectionCursor, RadrootsProjectionRebuildPrior, RadrootsProjectionRebuildTicket,
- RadrootsRawHeadDecision, RadrootsStoredEventTag, RadrootsStoredRawEvent,
- RadrootsStoredRawEventHead, RadrootsStoredSellerReservation,
+ RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1,
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1,
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1,
+ RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1,
+ RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1,
+ RADROOTS_TRANSPORT_OBSERVATION_MESSAGE_MAX_BYTES, RadrootsAddressableTransitionCauseV1,
+ RadrootsAddressableTransitionCoordinateV1, RadrootsAddressableTransitionCursorV1,
+ RadrootsAddressableTransitionEventReferenceV1, RadrootsAddressableTransitionOriginV1,
+ RadrootsAddressableTransitionPageV1, RadrootsAddressableTransitionRawHeadDecisionV1,
+ RadrootsAddressableTransitionScopeFingerprintV1, RadrootsAddressableTransitionScopeV1,
+ RadrootsAddressableTransitionV1, RadrootsAddressableTransitionVisibilityV1,
+ RadrootsCurrentEventVisibilityV1, RadrootsCurrentVisibilityDecisionV1,
+ RadrootsEventAdmissionStatus, RadrootsEventIngest, RadrootsEventIngestReceipt,
+ RadrootsEventPersistence, RadrootsEventStoreSourceGeneration, RadrootsEventStoreStatusSummary,
+ RadrootsEventVisibility, RadrootsFoodAvailabilitySearchQueryV1,
+ RadrootsFoodAvailabilityStatusFilterV1, RadrootsNip09SuppressionEvidenceV1,
+ RadrootsNip09SuppressionOutcome, RadrootsNip09SuppressionReason, RadrootsProjectionCursor,
+ RadrootsProjectionRebuildPrior, RadrootsProjectionRebuildTicket, RadrootsRawHeadDecision,
+ RadrootsStoreProducedCanonicalEventV1, RadrootsStoredEventTag,
+ RadrootsStoredFoodAvailabilityImageV1, RadrootsStoredFoodAvailabilityV1,
+ RadrootsStoredRawEvent, RadrootsStoredRawEventHead, RadrootsStoredSellerReservation,
RadrootsStoredSellerReservationLine, RadrootsStoredTradeMissingParent,
RadrootsStoredTradeMutation, RadrootsStoredTradeMutationParent,
RadrootsStoredTradeTransportEnvelope, RadrootsStoredValidEvent, RadrootsStoredVisibleEvent,
diff --git a/crates/event_store/src/migrations.rs b/crates/event_store/src/migrations.rs
@@ -1,4 +1,5 @@
use crate::RadrootsEventStoreError;
+use crate::generated::food_availability_projection_manifest as food_manifest;
use crate::generated::nip09_reconciliation_manifest as nip09_manifest;
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
@@ -7,7 +8,7 @@ pub(crate) const EVENT_STORE_LEDGER_NAME: &str = "radroots_event_store_schema_mi
pub(crate) const EVENT_STORE_RESERVED_PREFIX: &str = "radroots_event_store_";
pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN: u32 = 1;
-pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 2;
+pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 3;
pub(crate) const EVENT_STORE_LEDGER_DDL: &str = "CREATE TABLE radroots_event_store_schema_migrations (
version INTEGER PRIMARY KEY NOT NULL CHECK (version > 0),
@@ -103,6 +104,7 @@ pub(crate) const EVENT_STORE_BASELINE_FTS5_TABLE_NAMES: &[&str] = &["listing_sea
pub(crate) enum EventStoreMigrationHook {
None,
Nip09ReconciliationV1,
+ FoodAvailabilityProjectionV1,
}
impl EventStoreMigrationHook {
@@ -110,6 +112,9 @@ impl EventStoreMigrationHook {
match self {
Self::None => "none",
Self::Nip09ReconciliationV1 => nip09_manifest::NIP09_RECONCILIATION_HOOK_ID,
+ Self::FoodAvailabilityProjectionV1 => {
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_HOOK_ID
+ }
}
}
@@ -119,10 +124,63 @@ impl EventStoreMigrationHook {
Self::Nip09ReconciliationV1 => {
Some(nip09_manifest::NIP09_RECONCILIATION_MANIFEST_SHA256)
}
+ Self::FoodAvailabilityProjectionV1 => {
+ Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256)
+ }
}
}
}
+pub(crate) const EVENT_STORE_FOOD_AVAILABILITY_OBJECT_NAMES: &[&str] = &[
+ "radroots_event_store_addressable_feed_generation_insert",
+ "radroots_event_store_addressable_feed_integrity_v1",
+ "radroots_event_store_addressable_feed_transition_insert",
+ "radroots_event_store_addressable_transition_coordinate_idx",
+ "radroots_event_store_current_visibility_head_lookup_idx",
+ "radroots_event_store_current_visibility_v1",
+ "radroots_event_store_food_availability_author_idx",
+ "radroots_event_store_food_availability_cursor",
+ "radroots_event_store_food_availability_cursor_delete_guard",
+ "radroots_event_store_food_availability_cursor_insert_guard",
+ "radroots_event_store_food_availability_cursor_update_guard",
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_image_delete_guard",
+ "radroots_event_store_food_availability_image_insert_guard",
+ "radroots_event_store_food_availability_image_update_guard",
+ "radroots_event_store_food_availability_projection",
+ "radroots_event_store_food_availability_projection_delete_guard",
+ "radroots_event_store_food_availability_projection_insert_guard",
+ "radroots_event_store_food_availability_projection_update_guard",
+ "radroots_event_store_food_availability_read_v1",
+ "radroots_event_store_food_availability_recent_idx",
+ "radroots_event_store_food_availability_search_delete",
+ "radroots_event_store_food_availability_search_fts",
+ "radroots_event_store_food_availability_search_fts_config",
+ "radroots_event_store_food_availability_search_fts_content",
+ "radroots_event_store_food_availability_search_fts_data",
+ "radroots_event_store_food_availability_search_fts_docsize",
+ "radroots_event_store_food_availability_search_fts_idx",
+ "radroots_event_store_food_availability_search_insert",
+ "radroots_event_store_food_availability_status_idx",
+ "radroots_event_store_nip09_address_target_visibility_lookup_idx",
+];
+
+pub(crate) const EVENT_STORE_FOOD_AVAILABILITY_TABLE_NAMES: &[&str] = &[
+ "radroots_event_store_addressable_feed_integrity_v1",
+ "radroots_event_store_food_availability_cursor",
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_projection",
+ "radroots_event_store_food_availability_search_fts",
+ "radroots_event_store_food_availability_search_fts_config",
+ "radroots_event_store_food_availability_search_fts_content",
+ "radroots_event_store_food_availability_search_fts_data",
+ "radroots_event_store_food_availability_search_fts_docsize",
+ "radroots_event_store_food_availability_search_fts_idx",
+];
+
+pub(crate) const EVENT_STORE_FOOD_AVAILABILITY_FTS5_TABLE_NAMES: &[&str] =
+ &["radroots_event_store_food_availability_search_fts"];
+
pub(crate) const EVENT_STORE_NIP09_OBJECT_NAMES: &[&str] = &[
"radroots_event_store_addressable_head_state",
"radroots_event_store_addressable_head_transition",
@@ -278,6 +336,25 @@ pub(crate) const EVENT_STORE_MIGRATIONS: &[EventStoreMigration] = &[
nip09_manifest::NIP09_RECONCILIATION_EVENT_CONTRACT_REGISTRY_VERSION,
),
},
+ EventStoreMigration {
+ version: 3,
+ name: "food_availability_projection",
+ up_sql: include_str!("../migrations/0003_food_availability_projection.up.sql"),
+ down_sql: include_str!("../migrations/0003_food_availability_projection.down.sql"),
+ up_len: food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_BYTE_LENGTH,
+ down_len: food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_BYTE_LENGTH,
+ up_sha256: food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_SHA256,
+ down_sha256: food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_SHA256,
+ schema_sha256: food_manifest::FOOD_AVAILABILITY_PROJECTION_SCHEMA_SHA256,
+ owned_object_names: EVENT_STORE_FOOD_AVAILABILITY_OBJECT_NAMES,
+ owned_table_names: EVENT_STORE_FOOD_AVAILABILITY_TABLE_NAMES,
+ fts5_table_names: EVENT_STORE_FOOD_AVAILABILITY_FTS5_TABLE_NAMES,
+ hook: EventStoreMigrationHook::FoodAvailabilityProjectionV1,
+ hook_manifest_sha256: Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256),
+ event_contract_registry_version: Some(
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION,
+ ),
+ },
];
pub(crate) fn migration_for_version(
@@ -344,6 +421,12 @@ pub(crate) fn validate_migration_registry(
{
validate_generated_nip09_manifest_descriptor()?;
}
+ if registry
+ .iter()
+ .any(|migration| migration.hook == EventStoreMigrationHook::FoodAvailabilityProjectionV1)
+ {
+ validate_generated_food_availability_projection_manifest_descriptor()?;
+ }
if minimum == 0 || current < minimum || registry.is_empty() {
return Err(RadrootsEventStoreError::MigrationRegistryDefect {
reason: format!(
@@ -468,6 +551,11 @@ pub(crate) fn validate_migration_registry(
EventStoreMigrationHook::Nip09ReconciliationV1,
Some(nip09_manifest::NIP09_RECONCILIATION_MANIFEST_SHA256),
Some(nip09_manifest::NIP09_RECONCILIATION_EVENT_CONTRACT_REGISTRY_VERSION),
+ )
+ | (
+ EventStoreMigrationHook::FoodAvailabilityProjectionV1,
+ Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256),
+ Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION),
) => {}
(hook, manifest, registry_version) => {
return Err(RadrootsEventStoreError::MigrationRegistryDefect {
@@ -638,6 +726,190 @@ fn validate_generated_nip09_manifest_descriptor() -> Result<(), RadrootsEventSto
Ok(())
}
+fn validate_generated_food_availability_projection_manifest_descriptor()
+-> Result<(), RadrootsEventStoreError> {
+ let bytes = food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_JSON.as_bytes();
+ if bytes.len() != food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_BYTE_LENGTH {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated FoodAvailability projection manifest byte length is inconsistent"
+ .to_owned(),
+ });
+ }
+ validate_sha256_literal(
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION,
+ "hook manifest",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256,
+ )?;
+ if sha256_hex(bytes) != food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256 {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated FoodAvailability projection manifest digest is inconsistent"
+ .to_owned(),
+ });
+ }
+ let manifest: serde_json::Value = serde_json::from_slice(bytes).map_err(|error| {
+ RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "generated FoodAvailability projection manifest JSON is invalid: {error}"
+ ),
+ }
+ })?;
+ let up_byte_length =
+ u64::try_from(food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_BYTE_LENGTH)
+ .map_err(|_| RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated FoodAvailability migration up byte length is out of range"
+ .to_owned(),
+ })?;
+ let down_byte_length =
+ u64::try_from(food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_BYTE_LENGTH)
+ .map_err(|_| RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated FoodAvailability migration down byte length is out of range"
+ .to_owned(),
+ })?;
+ let expected_numbers = [
+ (
+ "/schema_version",
+ u64::from(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SCHEMA_VERSION),
+ ),
+ (
+ "/migration/version",
+ u64::from(food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION),
+ ),
+ ("/migration/up/byte_length", up_byte_length),
+ ("/migration/down/byte_length", down_byte_length),
+ (
+ "/profile/projection_version",
+ u64::from(food_manifest::FOOD_AVAILABILITY_PROJECTION_VERSION),
+ ),
+ (
+ "/profile/addressable_feed_version",
+ u64::from(food_manifest::FOOD_AVAILABILITY_PROJECTION_FEED_VERSION),
+ ),
+ (
+ "/profile/event_contract_registry_version",
+ u64::from(food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION),
+ ),
+ ];
+ let expected_strings = [
+ (
+ "/contract_id",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_CONTRACT_ID,
+ ),
+ (
+ "/hook_id",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_HOOK_ID,
+ ),
+ (
+ "/predecessor/manifest/sha256",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_PREDECESSOR_MANIFEST_SHA256,
+ ),
+ (
+ "/migration/name",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_NAME,
+ ),
+ (
+ "/migration/up/sha256",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_SHA256,
+ ),
+ (
+ "/migration/down/sha256",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_SHA256,
+ ),
+ (
+ "/migration/schema_sha256",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_SCHEMA_SHA256,
+ ),
+ (
+ "/profile/scope_fingerprint_sha256",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_SCOPE_FINGERPRINT_SHA256,
+ ),
+ (
+ "/result_vector/sha256",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_SHA256,
+ ),
+ (
+ "/result_vector/executor_id",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_ID,
+ ),
+ (
+ "/result_vector/executor_sha256",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_SHA256,
+ ),
+ ];
+ let numbers_match = expected_numbers.iter().all(|(pointer, expected)| {
+ manifest.pointer(pointer).and_then(|value| value.as_u64()) == Some(*expected)
+ });
+ let strings_match = expected_strings.iter().all(|(pointer, expected)| {
+ manifest.pointer(pointer).and_then(|value| value.as_str()) == Some(*expected)
+ });
+ let scope_matches = manifest
+ .pointer("/profile/scope_kinds")
+ .and_then(|value| value.as_array())
+ .is_some_and(|scope| scope.len() == 1 && scope[0].as_u64() == Some(30_402));
+ if food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SCHEMA_VERSION != 1
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_CONTRACT_ID
+ != "radroots_event_store.food_availability_projection_v1"
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_HOOK_ID != "food_availability_projection_v1"
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION != 3
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_NAME
+ != "food_availability_projection"
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_VERSION != 1
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_FEED_VERSION != 1
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION != 7
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_SCOPE_KINDS != [30_402]
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_SCOPE_FINGERPRINT_SHA256
+ != "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0"
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_PREDECESSOR_MANIFEST_SHA256
+ != nip09_manifest::NIP09_RECONCILIATION_MANIFEST_SHA256
+ || food_manifest::FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_ID
+ != "radroots_event_store.food_availability_projection_v1.result_vector_executor.v1"
+ || !numbers_match
+ || !strings_match
+ || !scope_matches
+ {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated FoodAvailability projection manifest metadata is inconsistent"
+ .to_owned(),
+ });
+ }
+ for (field, digest) in [
+ (
+ "migration up",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_SHA256,
+ ),
+ (
+ "migration down",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_SHA256,
+ ),
+ (
+ "schema",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_SCHEMA_SHA256,
+ ),
+ (
+ "scope fingerprint",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_SCOPE_FINGERPRINT_SHA256,
+ ),
+ (
+ "predecessor manifest",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_PREDECESSOR_MANIFEST_SHA256,
+ ),
+ (
+ "result vector",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_SHA256,
+ ),
+ (
+ "result-vector executor",
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_SHA256,
+ ),
+ ] {
+ validate_sha256_literal(
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION,
+ field,
+ digest,
+ )?;
+ }
+ Ok(())
+}
+
fn validate_owned_schema_name(
version: u32,
object_kind: &'static str,
@@ -874,7 +1146,7 @@ mod migration_framework {
#[test]
fn embedded_registry_is_contiguous_and_byte_pinned() {
validate_embedded_migration_registry().expect("valid registry");
- assert_eq!(EVENT_STORE_MIGRATIONS.len(), 2);
+ assert_eq!(EVENT_STORE_MIGRATIONS.len(), 3);
assert_eq!(EVENT_STORE_MIGRATIONS[0].version, 1);
assert_eq!(EVENT_STORE_MIGRATIONS[0].name, "event_store");
assert_eq!(EVENT_STORE_MIGRATIONS[0].up_len, FROZEN_V1_UP_LEN);
@@ -899,6 +1171,19 @@ mod migration_framework {
EVENT_STORE_MIGRATIONS[1].event_contract_registry_version,
Some(nip09_manifest::NIP09_RECONCILIATION_EVENT_CONTRACT_REGISTRY_VERSION)
);
+ assert_eq!(EVENT_STORE_MIGRATIONS[2].version, 3);
+ assert_eq!(
+ EVENT_STORE_MIGRATIONS[2].name,
+ "food_availability_projection"
+ );
+ assert_eq!(
+ EVENT_STORE_MIGRATIONS[2].hook,
+ EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ );
+ assert_eq!(
+ EVENT_STORE_MIGRATIONS[2].event_contract_registry_version,
+ Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION)
+ );
}
#[test]
diff --git a/crates/event_store/src/model.rs b/crates/event_store/src/model.rs
@@ -1,6 +1,38 @@
+mod addressable_transition_feed_v1;
+mod current_visibility_v1;
+mod food_availability_projection_v1;
mod ingest_reconciliation_v1;
pub(crate) mod reconciliation_v1;
+pub use addressable_transition_feed_v1::{
+ RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1, RadrootsAddressableTransitionCauseV1,
+ RadrootsAddressableTransitionCoordinateV1, RadrootsAddressableTransitionCursorV1,
+ RadrootsAddressableTransitionEventReferenceV1, RadrootsAddressableTransitionOriginV1,
+ RadrootsAddressableTransitionPageV1, RadrootsAddressableTransitionRawHeadDecisionV1,
+ RadrootsAddressableTransitionScopeFingerprintV1, RadrootsAddressableTransitionScopeV1,
+ RadrootsAddressableTransitionV1, RadrootsAddressableTransitionVisibilityV1,
+ RadrootsStoreProducedCanonicalEventV1,
+};
+pub use current_visibility_v1::{
+ RadrootsCurrentEventVisibilityV1, RadrootsCurrentVisibilityDecisionV1,
+ RadrootsNip09SuppressionEvidenceV1, RadrootsNip09SuppressionOutcome,
+ RadrootsNip09SuppressionReason,
+};
+pub use food_availability_projection_v1::{
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1,
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1,
+ RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1,
+ RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1, RadrootsFoodAvailabilitySearchQueryV1,
+ RadrootsFoodAvailabilityStatusFilterV1, RadrootsStoredFoodAvailabilityImageV1,
+ RadrootsStoredFoodAvailabilityV1,
+};
+
use crate::RadrootsEventStoreError;
use radroots_event::RadrootsEventKind;
use radroots_event::ids::{
@@ -349,7 +381,15 @@ impl RadrootsStoredVisibleEventHead {
pub enum RadrootsEventVisibility {
Visible,
NotAdmitted,
- NotCurrent { raw_head_event_id: String },
+ NotCurrent {
+ raw_head_event_id: String,
+ },
+ Suppressed {
+ reason: RadrootsNip09SuppressionReason,
+ event_reference_request_id: Option<RadrootsEventId>,
+ address_reference_request_id: Option<RadrootsEventId>,
+ address_reference_cutoff: Option<u64>,
+ },
}
#[derive(Clone, Debug, PartialEq, Eq)]
diff --git a/crates/event_store/src/model/addressable_transition_feed_v1.rs b/crates/event_store/src/model/addressable_transition_feed_v1.rs
@@ -0,0 +1,647 @@
+use super::{
+ RadrootsEventAdmissionStatus, RadrootsEventStoreSourceGeneration,
+ RadrootsNip09SuppressionEvidenceV1,
+};
+use crate::RadrootsEventStoreError;
+use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use std::collections::BTreeSet;
+
+pub const RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1: u32 = 1;
+pub const RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1: usize = 64;
+pub const RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1: u32 = 64;
+pub const RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1: u32 = 1_024;
+pub const RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1: usize = 4 * 1024 * 1024;
+pub const RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1: usize =
+ radroots_event::wire::v1::DEFAULT_TAG_ELEMENT_MAX_BYTES;
+pub const RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1: usize = 512;
+const SCOPE_FINGERPRINT_DOMAIN_V1: &[u8] = b"radroots.addressable-transition-scope.v1\0";
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct RadrootsAddressableTransitionScopeFingerprintV1([u8; 32]);
+
+impl RadrootsAddressableTransitionScopeFingerprintV1 {
+ pub const fn from_bytes(bytes: [u8; 32]) -> Self {
+ Self(bytes)
+ }
+
+ pub const fn as_bytes(&self) -> &[u8; 32] {
+ &self.0
+ }
+
+ pub fn to_hex(self) -> String {
+ hex::encode(self.0)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsAddressableTransitionScopeV1 {
+ kinds: Vec<u32>,
+ fingerprint: RadrootsAddressableTransitionScopeFingerprintV1,
+}
+
+impl RadrootsAddressableTransitionScopeV1 {
+ pub fn new(kinds: impl IntoIterator<Item = u32>) -> Result<Self, RadrootsEventStoreError> {
+ let mut canonical_kinds = BTreeSet::new();
+ let mut input_count = 0usize;
+ for kind in kinds {
+ input_count = input_count.saturating_add(1);
+ if input_count > RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1 {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionScopeTooLarge {
+ max: RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1,
+ actual: input_count,
+ },
+ );
+ }
+ canonical_kinds.insert(kind);
+ }
+ let kinds = canonical_kinds;
+ if kinds.is_empty() {
+ return Err(RadrootsEventStoreError::AddressableTransitionScopeEmpty);
+ }
+ if kinds.len() > RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1 {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionScopeTooLarge {
+ max: RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1,
+ actual: kinds.len(),
+ },
+ );
+ }
+ if let Some(kind) = kinds
+ .iter()
+ .copied()
+ .find(|kind| !(30_000..=39_999).contains(kind))
+ {
+ return Err(RadrootsEventStoreError::AddressableTransitionScopeKindInvalid { kind });
+ }
+ let kinds = kinds.into_iter().collect::<Vec<_>>();
+ let mut hasher = Sha256::new();
+ hasher.update(SCOPE_FINGERPRINT_DOMAIN_V1);
+ hasher.update(
+ u32::try_from(kinds.len())
+ .expect("scope maximum fits u32")
+ .to_be_bytes(),
+ );
+ for kind in &kinds {
+ hasher.update(kind.to_be_bytes());
+ }
+ let fingerprint =
+ RadrootsAddressableTransitionScopeFingerprintV1::from_bytes(hasher.finalize().into());
+ Ok(Self { kinds, fingerprint })
+ }
+
+ pub fn food_availability() -> Self {
+ Self::new([30_402]).expect("the FoodAvailability kind is addressable")
+ }
+
+ pub fn kinds(&self) -> &[u32] {
+ &self.kinds
+ }
+
+ pub const fn fingerprint(&self) -> RadrootsAddressableTransitionScopeFingerprintV1 {
+ self.fingerprint
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsAddressableTransitionCursorV1 {
+ source_generation: RadrootsEventStoreSourceGeneration,
+ scope_fingerprint: RadrootsAddressableTransitionScopeFingerprintV1,
+ last_transition_seq: i64,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct AddressableTransitionCursorWireV1 {
+ source_generation: String,
+ feed_version: u32,
+ scope_fingerprint: String,
+ last_transition_seq: i64,
+}
+
+impl RadrootsAddressableTransitionCursorV1 {
+ pub fn new(
+ source_generation: RadrootsEventStoreSourceGeneration,
+ scope_fingerprint: RadrootsAddressableTransitionScopeFingerprintV1,
+ last_transition_seq: i64,
+ ) -> Result<Self, RadrootsEventStoreError> {
+ if last_transition_seq < 0 {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionCursorNegative {
+ value: last_transition_seq,
+ },
+ );
+ }
+ Ok(Self {
+ source_generation,
+ scope_fingerprint,
+ last_transition_seq,
+ })
+ }
+
+ pub const fn source_generation(&self) -> RadrootsEventStoreSourceGeneration {
+ self.source_generation
+ }
+
+ pub const fn feed_version(&self) -> u32 {
+ RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1
+ }
+
+ pub const fn scope_fingerprint(&self) -> RadrootsAddressableTransitionScopeFingerprintV1 {
+ self.scope_fingerprint
+ }
+
+ pub const fn last_transition_seq(&self) -> i64 {
+ self.last_transition_seq
+ }
+
+ pub fn to_json(&self) -> Result<String, RadrootsEventStoreError> {
+ Ok(serde_json::to_string(&AddressableTransitionCursorWireV1 {
+ source_generation: hex::encode(self.source_generation.as_bytes()),
+ feed_version: RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ scope_fingerprint: self.scope_fingerprint.to_hex(),
+ last_transition_seq: self.last_transition_seq,
+ })?)
+ }
+
+ pub fn from_json(value: &str) -> Result<Self, RadrootsEventStoreError> {
+ if value.len() > RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1 {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionCursorTooLarge {
+ max: RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1,
+ actual: value.len(),
+ },
+ );
+ }
+ let cursor: AddressableTransitionCursorWireV1 = serde_json::from_str(value)?;
+ if cursor.feed_version != RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1 {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionFeedVersionMismatch {
+ expected: RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ actual: cursor.feed_version,
+ },
+ );
+ }
+ Self::new(
+ RadrootsEventStoreSourceGeneration::from_bytes(decode_cursor_hex(
+ "source_generation",
+ cursor.source_generation.as_str(),
+ )?),
+ RadrootsAddressableTransitionScopeFingerprintV1::from_bytes(decode_cursor_hex(
+ "scope_fingerprint",
+ cursor.scope_fingerprint.as_str(),
+ )?),
+ cursor.last_transition_seq,
+ )
+ }
+}
+
+fn decode_cursor_hex(
+ field: &'static str,
+ value: &str,
+) -> Result<[u8; 32], RadrootsEventStoreError> {
+ if value.len() != 64
+ || value
+ .bytes()
+ .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
+ {
+ return Err(RadrootsEventStoreError::AddressableTransitionCursorEncoding { field });
+ }
+ let decoded = hex::decode(value)
+ .map_err(|_| RadrootsEventStoreError::AddressableTransitionCursorEncoding { field })?;
+ decoded
+ .try_into()
+ .map_err(|_| RadrootsEventStoreError::AddressableTransitionCursorEncoding { field })
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsAddressableTransitionOriginV1 {
+ Baseline,
+ Incremental,
+}
+
+impl RadrootsAddressableTransitionOriginV1 {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Baseline => "baseline",
+ Self::Incremental => "incremental",
+ }
+ }
+
+ pub(crate) fn parse(value: &str) -> Result<Self, RadrootsEventStoreError> {
+ match value {
+ "baseline" => Ok(Self::Baseline),
+ "incremental" => Ok(Self::Incremental),
+ _ => Err(RadrootsEventStoreError::InvalidStoredEnum {
+ field: "addressable_transition.origin",
+ value: value.to_owned(),
+ }),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsAddressableTransitionRawHeadDecisionV1 {
+ BaselineRebuild,
+ Applied,
+ NotHeadSelected,
+ SkippedOlder,
+ SkippedSameTimestampHigherEventId,
+ MalformedCoordinate,
+}
+
+impl RadrootsAddressableTransitionRawHeadDecisionV1 {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::BaselineRebuild => "baseline_rebuild",
+ Self::Applied => "applied",
+ Self::NotHeadSelected => "not_head_selected",
+ Self::SkippedOlder => "skipped_older",
+ Self::SkippedSameTimestampHigherEventId => "skipped_same_timestamp_higher_event_id",
+ Self::MalformedCoordinate => "malformed_coordinate",
+ }
+ }
+
+ pub(crate) fn parse(value: &str) -> Result<Self, RadrootsEventStoreError> {
+ match value {
+ "baseline_rebuild" => Ok(Self::BaselineRebuild),
+ "applied" => Ok(Self::Applied),
+ "not_head_selected" => Ok(Self::NotHeadSelected),
+ "skipped_older" => Ok(Self::SkippedOlder),
+ "skipped_same_timestamp_higher_event_id" => Ok(Self::SkippedSameTimestampHigherEventId),
+ "malformed_coordinate" => Ok(Self::MalformedCoordinate),
+ _ => Err(RadrootsEventStoreError::InvalidStoredEnum {
+ field: "addressable_transition.raw_head_decision",
+ value: value.to_owned(),
+ }),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsAddressableTransitionEventReferenceV1 {
+ pub(crate) event_id: RadrootsEventId,
+ pub(crate) event_seq: i64,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsAddressableTransitionCauseV1 {
+ pub(crate) event: RadrootsAddressableTransitionEventReferenceV1,
+ pub(crate) pubkey: RadrootsPublicKey,
+ pub(crate) created_at: u64,
+ pub(crate) kind: u32,
+ pub(crate) admission_status: RadrootsEventAdmissionStatus,
+ pub(crate) admission_code: Option<String>,
+ pub(crate) contract_id: Option<String>,
+}
+
+impl RadrootsAddressableTransitionCauseV1 {
+ pub const fn event(&self) -> &RadrootsAddressableTransitionEventReferenceV1 {
+ &self.event
+ }
+
+ pub const fn pubkey(&self) -> &RadrootsPublicKey {
+ &self.pubkey
+ }
+
+ pub const fn created_at(&self) -> u64 {
+ self.created_at
+ }
+
+ pub const fn kind(&self) -> u32 {
+ self.kind
+ }
+
+ pub const fn admission_status(&self) -> RadrootsEventAdmissionStatus {
+ self.admission_status
+ }
+
+ pub fn admission_code(&self) -> Option<&str> {
+ self.admission_code.as_deref()
+ }
+
+ pub fn contract_id(&self) -> Option<&str> {
+ self.contract_id.as_deref()
+ }
+}
+
+/// The exact addressable head identity retained by the event store.
+///
+/// This is intentionally not a [`radroots_event::ids::RadrootsNip01Coordinate`]:
+/// an individually valid maximum-size `d` tag can make the combined NIP-01
+/// coordinate too large for a wire tag element while still remaining valid raw
+/// head identity.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsAddressableTransitionCoordinateV1 {
+ pub(crate) kind: u32,
+ pub(crate) pubkey: RadrootsPublicKey,
+ pub(crate) d_tag: String,
+}
+
+impl RadrootsAddressableTransitionCoordinateV1 {
+ pub const fn kind(&self) -> u32 {
+ self.kind
+ }
+
+ pub const fn pubkey(&self) -> &RadrootsPublicKey {
+ &self.pubkey
+ }
+
+ pub fn d_tag(&self) -> &str {
+ self.d_tag.as_str()
+ }
+}
+
+impl RadrootsAddressableTransitionEventReferenceV1 {
+ pub const fn event_id(&self) -> &RadrootsEventId {
+ &self.event_id
+ }
+
+ pub const fn event_seq(&self) -> i64 {
+ self.event_seq
+ }
+}
+
+/// A store-selected transition-time visible event with portable signed identity.
+///
+/// The opaque JSON has already passed id and signature verification. Local
+/// database sequence and observation timestamps are deliberately excluded.
+/// This snapshot is meaningful only in its containing ordered transition; it
+/// is not proof that the event remains current when a historical page is read.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsStoreProducedCanonicalEventV1 {
+ pub(crate) event_id: RadrootsEventId,
+ pub(crate) pubkey: RadrootsPublicKey,
+ pub(crate) created_at: u64,
+ pub(crate) kind: u32,
+ pub(crate) raw_json: String,
+}
+
+impl RadrootsStoreProducedCanonicalEventV1 {
+ pub const fn event_id(&self) -> &RadrootsEventId {
+ &self.event_id
+ }
+
+ pub const fn pubkey(&self) -> &RadrootsPublicKey {
+ &self.pubkey
+ }
+
+ pub const fn created_at(&self) -> u64 {
+ self.created_at
+ }
+
+ pub const fn kind(&self) -> u32 {
+ self.kind
+ }
+
+ pub fn raw_json(&self) -> &str {
+ self.raw_json.as_str()
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsAddressableTransitionVisibilityV1 {
+ Visible,
+ NotAdmitted,
+ Suppressed,
+}
+
+impl RadrootsAddressableTransitionVisibilityV1 {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Visible => "visible",
+ Self::NotAdmitted => "not_admitted",
+ Self::Suppressed => "suppressed",
+ }
+ }
+
+ pub(crate) fn parse(value: &str) -> Result<Self, RadrootsEventStoreError> {
+ match value {
+ "visible" => Ok(Self::Visible),
+ "not_admitted" => Ok(Self::NotAdmitted),
+ "suppressed" => Ok(Self::Suppressed),
+ _ => Err(RadrootsEventStoreError::InvalidStoredEnum {
+ field: "addressable_transition.visibility",
+ value: value.to_owned(),
+ }),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsAddressableTransitionV1 {
+ pub(crate) transition_seq: i64,
+ pub(crate) source_generation: RadrootsEventStoreSourceGeneration,
+ pub(crate) origin: RadrootsAddressableTransitionOriginV1,
+ pub(crate) coordinate: RadrootsAddressableTransitionCoordinateV1,
+ pub(crate) raw_head: RadrootsAddressableTransitionEventReferenceV1,
+ pub(crate) raw_head_created_at: u64,
+ pub(crate) visible_event: Option<RadrootsStoreProducedCanonicalEventV1>,
+ pub(crate) retracted_event: Option<RadrootsAddressableTransitionEventReferenceV1>,
+ pub(crate) admission_status: RadrootsEventAdmissionStatus,
+ pub(crate) admission_code: Option<String>,
+ pub(crate) contract_id: Option<String>,
+ pub(crate) visibility: RadrootsAddressableTransitionVisibilityV1,
+ pub(crate) suppression: Option<RadrootsNip09SuppressionEvidenceV1>,
+ pub(crate) cause_event: Option<RadrootsAddressableTransitionCauseV1>,
+ pub(crate) raw_head_decision: RadrootsAddressableTransitionRawHeadDecisionV1,
+}
+
+impl RadrootsAddressableTransitionV1 {
+ pub const fn transition_seq(&self) -> i64 {
+ self.transition_seq
+ }
+
+ pub const fn source_generation(&self) -> RadrootsEventStoreSourceGeneration {
+ self.source_generation
+ }
+
+ pub const fn origin(&self) -> RadrootsAddressableTransitionOriginV1 {
+ self.origin
+ }
+
+ pub const fn coordinate(&self) -> &RadrootsAddressableTransitionCoordinateV1 {
+ &self.coordinate
+ }
+
+ pub const fn raw_head(&self) -> &RadrootsAddressableTransitionEventReferenceV1 {
+ &self.raw_head
+ }
+
+ pub const fn raw_head_created_at(&self) -> u64 {
+ self.raw_head_created_at
+ }
+
+ pub const fn visible_event(&self) -> Option<&RadrootsStoreProducedCanonicalEventV1> {
+ self.visible_event.as_ref()
+ }
+
+ pub const fn retracted_event(&self) -> Option<&RadrootsAddressableTransitionEventReferenceV1> {
+ self.retracted_event.as_ref()
+ }
+
+ pub const fn admission_status(&self) -> RadrootsEventAdmissionStatus {
+ self.admission_status
+ }
+
+ pub fn admission_code(&self) -> Option<&str> {
+ self.admission_code.as_deref()
+ }
+
+ pub fn contract_id(&self) -> Option<&str> {
+ self.contract_id.as_deref()
+ }
+
+ pub const fn visibility(&self) -> RadrootsAddressableTransitionVisibilityV1 {
+ self.visibility
+ }
+
+ pub const fn suppression(&self) -> Option<&RadrootsNip09SuppressionEvidenceV1> {
+ self.suppression.as_ref()
+ }
+
+ pub const fn cause_event(&self) -> Option<&RadrootsAddressableTransitionCauseV1> {
+ self.cause_event.as_ref()
+ }
+
+ pub const fn raw_head_decision(&self) -> RadrootsAddressableTransitionRawHeadDecisionV1 {
+ self.raw_head_decision
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsAddressableTransitionPageV1 {
+ pub(crate) source_high_water: i64,
+ pub(crate) transitions: Vec<RadrootsAddressableTransitionV1>,
+ pub(crate) next_cursor: RadrootsAddressableTransitionCursorV1,
+ pub(crate) has_more: bool,
+}
+
+impl RadrootsAddressableTransitionPageV1 {
+ pub const fn source_high_water(&self) -> i64 {
+ self.source_high_water
+ }
+
+ pub fn transitions(&self) -> &[RadrootsAddressableTransitionV1] {
+ &self.transitions
+ }
+
+ pub const fn next_cursor(&self) -> &RadrootsAddressableTransitionCursorV1 {
+ &self.next_cursor
+ }
+
+ pub const fn has_more(&self) -> bool {
+ self.has_more
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn scope_is_canonical_bounded_and_fingerprinted() {
+ let scope = RadrootsAddressableTransitionScopeV1::new([39_999, 30_402, 30_402])
+ .expect("canonical scope");
+ assert_eq!(scope.kinds(), &[30_402, 39_999]);
+ let maximum_scope = RadrootsAddressableTransitionScopeV1::new(
+ (0..RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1)
+ .map(|index| 30_000 + u32::try_from(index).expect("bounded index")),
+ )
+ .expect("maximum-size scope");
+ assert_eq!(
+ maximum_scope.kinds().len(),
+ RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1,
+ );
+ assert_eq!(
+ RadrootsAddressableTransitionScopeV1::food_availability()
+ .fingerprint()
+ .to_hex(),
+ "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0"
+ );
+ assert!(matches!(
+ RadrootsAddressableTransitionScopeV1::new([]),
+ Err(RadrootsEventStoreError::AddressableTransitionScopeEmpty)
+ ));
+ assert!(matches!(
+ RadrootsAddressableTransitionScopeV1::new([29_999]),
+ Err(RadrootsEventStoreError::AddressableTransitionScopeKindInvalid { kind: 29_999 })
+ ));
+ assert!(matches!(
+ RadrootsAddressableTransitionScopeV1::new(
+ (0..=RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1)
+ .map(|index| 30_000 + u32::try_from(index).expect("bounded index"))
+ ),
+ Err(RadrootsEventStoreError::AddressableTransitionScopeTooLarge {
+ max: RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1,
+ actual
+ }) if actual == RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1 + 1
+ ));
+ }
+
+ #[test]
+ fn cursor_wire_round_trip_and_typed_failures_are_stable() {
+ let scope = RadrootsAddressableTransitionScopeV1::food_availability();
+ let cursor = RadrootsAddressableTransitionCursorV1::new(
+ RadrootsEventStoreSourceGeneration::from_bytes([0x42; 32]),
+ scope.fingerprint(),
+ 17,
+ )
+ .expect("cursor");
+ let json = cursor.to_json().expect("cursor JSON");
+ assert_eq!(
+ RadrootsAddressableTransitionCursorV1::from_json(json.as_str()).expect("round trip"),
+ cursor
+ );
+
+ let mut value: serde_json::Value = serde_json::from_str(&json).expect("wire object");
+ value["feed_version"] = serde_json::json!(2);
+ assert!(matches!(
+ RadrootsAddressableTransitionCursorV1::from_json(
+ serde_json::to_string(&value)
+ .expect("version JSON")
+ .as_str()
+ ),
+ Err(
+ RadrootsEventStoreError::AddressableTransitionFeedVersionMismatch {
+ expected: RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ actual: 2,
+ }
+ )
+ ));
+
+ value["feed_version"] = serde_json::json!(RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1);
+ value["source_generation"] = serde_json::json!("AA".repeat(32));
+ assert!(matches!(
+ RadrootsAddressableTransitionCursorV1::from_json(
+ serde_json::to_string(&value)
+ .expect("encoding JSON")
+ .as_str()
+ ),
+ Err(
+ RadrootsEventStoreError::AddressableTransitionCursorEncoding {
+ field: "source_generation"
+ }
+ )
+ ));
+
+ assert!(matches!(
+ RadrootsAddressableTransitionCursorV1::new(
+ cursor.source_generation(),
+ cursor.scope_fingerprint(),
+ -1,
+ ),
+ Err(RadrootsEventStoreError::AddressableTransitionCursorNegative { value: -1 })
+ ));
+ let oversized = " ".repeat(RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1 + 1);
+ assert!(matches!(
+ RadrootsAddressableTransitionCursorV1::from_json(&oversized),
+ Err(RadrootsEventStoreError::AddressableTransitionCursorTooLarge {
+ max: RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1,
+ actual
+ }) if actual == RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1 + 1
+ ));
+ }
+}
diff --git a/crates/event_store/src/model/current_visibility_v1.rs b/crates/event_store/src/model/current_visibility_v1.rs
@@ -0,0 +1,154 @@
+use super::{
+ RadrootsEventAdmissionStatus, RadrootsEventStoreSourceGeneration, RadrootsStoredRawEvent,
+};
+use radroots_event::ids::RadrootsEventId;
+pub use radroots_event_codec::deletion::reconciliation_v1::evaluator::{
+ RadrootsNip09SuppressionOutcome, RadrootsNip09SuppressionReason,
+};
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsNip09SuppressionEvidenceV1 {
+ pub(crate) outcome: RadrootsNip09SuppressionOutcome,
+ pub(crate) reason: RadrootsNip09SuppressionReason,
+ pub(crate) event_reference_request_id: Option<RadrootsEventId>,
+ pub(crate) address_reference_request_id: Option<RadrootsEventId>,
+ pub(crate) address_reference_cutoff: Option<u64>,
+}
+
+impl RadrootsNip09SuppressionEvidenceV1 {
+ pub const fn outcome(&self) -> RadrootsNip09SuppressionOutcome {
+ self.outcome
+ }
+
+ pub const fn reason(&self) -> RadrootsNip09SuppressionReason {
+ self.reason
+ }
+
+ pub const fn event_reference_request_id(&self) -> Option<&RadrootsEventId> {
+ self.event_reference_request_id.as_ref()
+ }
+
+ pub const fn address_reference_request_id(&self) -> Option<&RadrootsEventId> {
+ self.address_reference_request_id.as_ref()
+ }
+
+ pub const fn address_reference_cutoff(&self) -> Option<u64> {
+ self.address_reference_cutoff
+ }
+
+ pub(crate) fn is_coherent_for_event(&self, kind: u32, created_at: u64) -> bool {
+ let event_reference = self.event_reference_request_id.is_some();
+ let address_reference = self.address_reference_request_id.is_some();
+ let cutoff = self.address_reference_cutoff;
+ if address_reference != cutoff.is_some() {
+ return false;
+ }
+ if kind == 5 {
+ return self.outcome == RadrootsNip09SuppressionOutcome::Visible
+ && self.reason == RadrootsNip09SuppressionReason::DeletionRequestImmune
+ && !event_reference
+ && !address_reference;
+ }
+ match self.reason {
+ RadrootsNip09SuppressionReason::DeletionRequestImmune => false,
+ RadrootsNip09SuppressionReason::NoAuthorizedReference
+ | RadrootsNip09SuppressionReason::RequestAuthorMismatch => {
+ self.outcome == RadrootsNip09SuppressionOutcome::Visible
+ && !event_reference
+ && !address_reference
+ }
+ RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget => {
+ self.outcome == RadrootsNip09SuppressionOutcome::Visible
+ && !event_reference
+ && cutoff.is_some_and(|value| value < created_at)
+ }
+ RadrootsNip09SuppressionReason::EventIdReference => {
+ self.outcome == RadrootsNip09SuppressionOutcome::Suppressed
+ && event_reference
+ && cutoff.is_none_or(|value| value < created_at)
+ }
+ RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff => {
+ self.outcome == RadrootsNip09SuppressionOutcome::Suppressed
+ && !event_reference
+ && cutoff.is_some_and(|value| value >= created_at)
+ }
+ RadrootsNip09SuppressionReason::EventIdAndAddressReference => {
+ self.outcome == RadrootsNip09SuppressionOutcome::Suppressed
+ && event_reference
+ && cutoff.is_some_and(|value| value >= created_at)
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RadrootsCurrentVisibilityDecisionV1 {
+ Visible,
+ NotAdmitted,
+ NotCurrent,
+ Suppressed,
+}
+
+impl RadrootsCurrentVisibilityDecisionV1 {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Visible => "visible",
+ Self::NotAdmitted => "not_admitted",
+ Self::NotCurrent => "not_current",
+ Self::Suppressed => "suppressed",
+ }
+ }
+
+ pub(crate) fn parse(value: &str) -> Result<Self, crate::RadrootsEventStoreError> {
+ match value {
+ "visible" => Ok(Self::Visible),
+ "not_admitted" => Ok(Self::NotAdmitted),
+ "not_current" => Ok(Self::NotCurrent),
+ "suppressed" => Ok(Self::Suppressed),
+ _ => Err(crate::RadrootsEventStoreError::InvalidStoredEnum {
+ field: "current_visibility.current_visibility",
+ value: value.to_owned(),
+ }),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsCurrentEventVisibilityV1 {
+ pub(crate) source_generation: RadrootsEventStoreSourceGeneration,
+ pub(crate) event: RadrootsStoredRawEvent,
+ pub(crate) is_raw_head: bool,
+ pub(crate) raw_head_event_id: Option<RadrootsEventId>,
+ pub(crate) suppression: Option<RadrootsNip09SuppressionEvidenceV1>,
+ pub(crate) decision: RadrootsCurrentVisibilityDecisionV1,
+}
+
+impl RadrootsCurrentEventVisibilityV1 {
+ pub const fn source_generation(&self) -> RadrootsEventStoreSourceGeneration {
+ self.source_generation
+ }
+
+ pub fn event(&self) -> &RadrootsStoredRawEvent {
+ &self.event
+ }
+
+ pub const fn admission_status(&self) -> RadrootsEventAdmissionStatus {
+ self.event.admission_status
+ }
+
+ pub const fn is_raw_head(&self) -> bool {
+ self.is_raw_head
+ }
+
+ pub const fn raw_head_event_id(&self) -> Option<&RadrootsEventId> {
+ self.raw_head_event_id.as_ref()
+ }
+
+ pub const fn suppression(&self) -> Option<&RadrootsNip09SuppressionEvidenceV1> {
+ self.suppression.as_ref()
+ }
+
+ pub const fn decision(&self) -> RadrootsCurrentVisibilityDecisionV1 {
+ self.decision
+ }
+}
diff --git a/crates/event_store/src/model/food_availability_projection_v1.rs b/crates/event_store/src/model/food_availability_projection_v1.rs
@@ -0,0 +1,531 @@
+use super::{
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1, RadrootsEventStoreSourceGeneration,
+};
+use crate::RadrootsEventStoreError;
+use radroots_blossom::RadrootsBlossomSha256;
+use radroots_event::{
+ food_availability::{
+ RADROOTS_FOOD_IMAGE_MAX_COUNT, RadrootsFoodAvailabilityStatus, RadrootsFoodContent,
+ RadrootsFoodIdentifier, RadrootsFoodImageDimensions, RadrootsFoodPrice,
+ RadrootsFoodPublishedAt, RadrootsFoodQuantity, RadrootsFoodText, food_media_blossom_digest,
+ },
+ ids::{RadrootsEventId, RadrootsPublicKey},
+};
+use radroots_event_codec::food_availability::inbound::{
+ RadrootsFoodAvailabilityImageDiagnostic, RadrootsInboundFoodAvailabilityImage,
+ RadrootsInboundFoodAvailabilityProjection,
+};
+
+pub const RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1: u32 = 1;
+pub const RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1: u32 =
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1;
+pub const RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1: usize = 256;
+pub const RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1: usize = 16;
+
+/// A bounded literal-term query for the FoodAvailability FTS5 projection.
+///
+/// Terms are combined with `AND`. Every term is emitted as an escaped FTS5
+/// string literal, so caller text cannot introduce columns, operators, prefix
+/// matching, or grouping into the generated expression.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsFoodAvailabilitySearchQueryV1 {
+ canonical_query: String,
+ terms: Vec<String>,
+ fts5_match_expression: String,
+}
+
+impl RadrootsFoodAvailabilitySearchQueryV1 {
+ pub fn parse(value: impl AsRef<str>) -> Result<Self, RadrootsEventStoreError> {
+ let value = value.as_ref();
+ if value.len() > RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1 {
+ return Err(RadrootsEventStoreError::FoodAvailabilitySearchTooLarge {
+ max: RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1,
+ actual: value.len(),
+ });
+ }
+
+ let terms = value
+ .split(|character: char| character.is_whitespace() || character.is_control())
+ .filter(|term| !term.is_empty())
+ .map(str::to_owned)
+ .collect::<Vec<_>>();
+ if terms.is_empty() {
+ return Err(RadrootsEventStoreError::FoodAvailabilitySearchEmpty);
+ }
+ if terms.len() > RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1 {
+ return Err(
+ RadrootsEventStoreError::FoodAvailabilitySearchTooManyTerms {
+ max: RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1,
+ actual: terms.len(),
+ },
+ );
+ }
+
+ let canonical_query = terms.join(" ");
+ let mut fts5_match_expression = String::with_capacity(
+ canonical_query
+ .len()
+ .saturating_add(terms.len().saturating_mul(8)),
+ );
+ for (index, term) in terms.iter().enumerate() {
+ if index > 0 {
+ fts5_match_expression.push_str(" AND ");
+ }
+ push_fts5_string_literal(&mut fts5_match_expression, term);
+ }
+
+ Ok(Self {
+ canonical_query,
+ terms,
+ fts5_match_expression,
+ })
+ }
+
+ pub fn as_str(&self) -> &str {
+ self.canonical_query.as_str()
+ }
+
+ pub fn terms(&self) -> &[String] {
+ &self.terms
+ }
+
+ pub(crate) fn fts5_match_expression(&self) -> &str {
+ self.fts5_match_expression.as_str()
+ }
+}
+
+impl core::fmt::Display for RadrootsFoodAvailabilitySearchQueryV1 {
+ fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ formatter.write_str(self.as_str())
+ }
+}
+
+impl TryFrom<&str> for RadrootsFoodAvailabilitySearchQueryV1 {
+ type Error = RadrootsEventStoreError;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ Self::parse(value)
+ }
+}
+
+fn push_fts5_string_literal(output: &mut String, term: &str) {
+ output.push('"');
+ for character in term.chars() {
+ if character == '"' {
+ output.push('"');
+ }
+ output.push(character);
+ }
+ output.push('"');
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum RadrootsFoodAvailabilityStatusFilterV1 {
+ #[default]
+ Any,
+ Active,
+ Sold,
+}
+
+impl RadrootsFoodAvailabilityStatusFilterV1 {
+ pub const fn status(self) -> Option<RadrootsFoodAvailabilityStatus> {
+ match self {
+ Self::Any => None,
+ Self::Active => Some(RadrootsFoodAvailabilityStatus::Active),
+ Self::Sold => Some(RadrootsFoodAvailabilityStatus::Sold),
+ }
+ }
+
+ pub(crate) const fn storage_value(self) -> Option<&'static str> {
+ match self.status() {
+ None => None,
+ Some(status) => Some(status.as_str()),
+ }
+ }
+}
+
+impl From<RadrootsFoodAvailabilityStatus> for RadrootsFoodAvailabilityStatusFilterV1 {
+ fn from(value: RadrootsFoodAvailabilityStatus) -> Self {
+ match value {
+ RadrootsFoodAvailabilityStatus::Active => Self::Active,
+ RadrootsFoodAvailabilityStatus::Sold => Self::Sold,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsStoredFoodAvailabilityImageV1 {
+ image_index: u32,
+ raw_tag: Vec<String>,
+ url: Option<String>,
+ dimensions: Option<RadrootsFoodImageDimensions>,
+ blossom_sha256: Option<RadrootsBlossomSha256>,
+ diagnostics: Vec<RadrootsFoodAvailabilityImageDiagnostic>,
+}
+
+impl RadrootsStoredFoodAvailabilityImageV1 {
+ fn from_projection(
+ image_index: usize,
+ image: &RadrootsInboundFoodAvailabilityImage,
+ ) -> Result<Self, RadrootsEventStoreError> {
+ let image_index = u32::try_from(image_index).map_err(|_| {
+ food_projection_drift("projected image index exceeds the u32 storage range")
+ })?;
+ let url = image.url().map(str::to_owned);
+ let blossom_sha256 = url.as_deref().and_then(food_media_blossom_digest);
+ Ok(Self {
+ image_index,
+ raw_tag: image.raw_tag().to_vec(),
+ url,
+ dimensions: image.dimensions(),
+ blossom_sha256,
+ diagnostics: image.diagnostics().to_vec(),
+ })
+ }
+
+ pub const fn image_index(&self) -> u32 {
+ self.image_index
+ }
+
+ pub fn raw_tag(&self) -> &[String] {
+ &self.raw_tag
+ }
+
+ pub fn url(&self) -> Option<&str> {
+ self.url.as_deref()
+ }
+
+ pub const fn dimensions(&self) -> Option<RadrootsFoodImageDimensions> {
+ self.dimensions
+ }
+
+ pub const fn blossom_sha256(&self) -> Option<RadrootsBlossomSha256> {
+ self.blossom_sha256
+ }
+
+ pub fn diagnostics(&self) -> &[RadrootsFoodAvailabilityImageDiagnostic] {
+ &self.diagnostics
+ }
+
+ /// Reports whether tolerant inbound projection produced no diagnostics.
+ ///
+ /// This does not establish Blossom hosting or byte verification. Callers
+ /// must require `blossom_sha256()` plus runtime upload/retrieval evidence
+ /// for Blossom-specific product behavior.
+ pub fn qualifies(&self) -> bool {
+ self.diagnostics.is_empty()
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsStoredFoodAvailabilityV1 {
+ source_generation: RadrootsEventStoreSourceGeneration,
+ pubkey: RadrootsPublicKey,
+ identifier: RadrootsFoodIdentifier,
+ event_id: RadrootsEventId,
+ event_seq: i64,
+ created_at: u64,
+ content: RadrootsFoodContent,
+ title: RadrootsFoodText,
+ summary: RadrootsFoodText,
+ published_at: RadrootsFoodPublishedAt,
+ location: RadrootsFoodText,
+ price: RadrootsFoodPrice,
+ quantity: Option<RadrootsFoodQuantity>,
+ status: RadrootsFoodAvailabilityStatus,
+ source_transition_seq: i64,
+ diagnostics: Vec<RadrootsFoodAvailabilityImageDiagnostic>,
+ images: Vec<RadrootsStoredFoodAvailabilityImageV1>,
+}
+
+impl RadrootsStoredFoodAvailabilityV1 {
+ #[allow(clippy::too_many_arguments)]
+ pub(crate) fn from_projection(
+ source_generation: RadrootsEventStoreSourceGeneration,
+ pubkey: RadrootsPublicKey,
+ event_id: RadrootsEventId,
+ event_seq: i64,
+ created_at: u64,
+ source_transition_seq: i64,
+ projection: &RadrootsInboundFoodAvailabilityProjection,
+ ) -> Result<Self, RadrootsEventStoreError> {
+ if event_seq <= 0 {
+ return Err(food_projection_drift(format!(
+ "event sequence must be positive, found {event_seq}"
+ )));
+ }
+ if source_transition_seq <= 0 {
+ return Err(food_projection_drift(format!(
+ "source transition sequence must be positive, found {source_transition_seq}"
+ )));
+ }
+ projection
+ .published_at()
+ .validate_created_at(created_at)
+ .map_err(|error| food_projection_drift(error.to_string()))?;
+ if projection
+ .quantity()
+ .is_some_and(|quantity| quantity.unit() != projection.price().unit())
+ {
+ return Err(food_projection_drift(
+ "quantity unit does not match the price unit",
+ ));
+ }
+ if projection.images().len() > RADROOTS_FOOD_IMAGE_MAX_COUNT {
+ return Err(food_projection_drift(format!(
+ "bounded projection has {} images; maximum is {RADROOTS_FOOD_IMAGE_MAX_COUNT}",
+ projection.images().len()
+ )));
+ }
+
+ let images = projection
+ .images()
+ .iter()
+ .enumerate()
+ .map(|(index, image)| {
+ RadrootsStoredFoodAvailabilityImageV1::from_projection(index, image)
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+ validate_projection_diagnostics(projection.diagnostics(), &images)?;
+
+ Ok(Self {
+ source_generation,
+ pubkey,
+ identifier: projection.identifier().clone(),
+ event_id,
+ event_seq,
+ created_at,
+ content: projection.content().clone(),
+ title: projection.title().clone(),
+ summary: projection.summary().clone(),
+ published_at: projection.published_at(),
+ location: projection.location().clone(),
+ price: projection.price().clone(),
+ quantity: projection.quantity().cloned(),
+ status: projection.status(),
+ source_transition_seq,
+ diagnostics: projection.diagnostics().to_vec(),
+ images,
+ })
+ }
+
+ pub const fn source_generation(&self) -> RadrootsEventStoreSourceGeneration {
+ self.source_generation
+ }
+
+ pub const fn pubkey(&self) -> &RadrootsPublicKey {
+ &self.pubkey
+ }
+
+ pub const fn identifier(&self) -> &RadrootsFoodIdentifier {
+ &self.identifier
+ }
+
+ pub const fn d_tag(&self) -> &RadrootsFoodIdentifier {
+ self.identifier()
+ }
+
+ pub const fn event_id(&self) -> &RadrootsEventId {
+ &self.event_id
+ }
+
+ pub const fn event_seq(&self) -> i64 {
+ self.event_seq
+ }
+
+ pub const fn created_at(&self) -> u64 {
+ self.created_at
+ }
+
+ pub const fn content(&self) -> &RadrootsFoodContent {
+ &self.content
+ }
+
+ pub const fn title(&self) -> &RadrootsFoodText {
+ &self.title
+ }
+
+ pub const fn summary(&self) -> &RadrootsFoodText {
+ &self.summary
+ }
+
+ pub const fn published_at(&self) -> RadrootsFoodPublishedAt {
+ self.published_at
+ }
+
+ pub const fn location(&self) -> &RadrootsFoodText {
+ &self.location
+ }
+
+ pub const fn price(&self) -> &RadrootsFoodPrice {
+ &self.price
+ }
+
+ pub const fn quantity(&self) -> Option<&RadrootsFoodQuantity> {
+ self.quantity.as_ref()
+ }
+
+ pub const fn status(&self) -> RadrootsFoodAvailabilityStatus {
+ self.status
+ }
+
+ pub const fn source_transition_seq(&self) -> i64 {
+ self.source_transition_seq
+ }
+
+ pub fn diagnostics(&self) -> &[RadrootsFoodAvailabilityImageDiagnostic] {
+ &self.diagnostics
+ }
+
+ pub fn images(&self) -> &[RadrootsStoredFoodAvailabilityImageV1] {
+ &self.images
+ }
+}
+
+fn validate_projection_diagnostics(
+ diagnostics: &[RadrootsFoodAvailabilityImageDiagnostic],
+ images: &[RadrootsStoredFoodAvailabilityImageV1],
+) -> Result<(), RadrootsEventStoreError> {
+ if images.iter().any(|image| {
+ image
+ .diagnostics()
+ .contains(&RadrootsFoodAvailabilityImageDiagnostic::CountExceeded)
+ }) {
+ return Err(food_projection_drift(
+ "image-level diagnostics contain the projection-wide count diagnostic",
+ ));
+ }
+
+ let count_exceeded =
+ diagnostics.first() == Some(&RadrootsFoodAvailabilityImageDiagnostic::CountExceeded);
+ if count_exceeded && images.len() != RADROOTS_FOOD_IMAGE_MAX_COUNT {
+ return Err(food_projection_drift(format!(
+ "count-exceeded projection retained {} images instead of {RADROOTS_FOOD_IMAGE_MAX_COUNT}",
+ images.len()
+ )));
+ }
+ let mut expected = Vec::new();
+ if count_exceeded {
+ expected.push(RadrootsFoodAvailabilityImageDiagnostic::CountExceeded);
+ }
+ for image in images {
+ expected.extend_from_slice(image.diagnostics());
+ }
+ if diagnostics != expected {
+ return Err(food_projection_drift(
+ "projection diagnostics do not match the ordered bounded image diagnostics",
+ ));
+ }
+ Ok(())
+}
+
+fn food_projection_drift(reason: impl Into<String>) -> RadrootsEventStoreError {
+ RadrootsEventStoreError::FoodAvailabilityProjectionDrift {
+ reason: reason.into(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn search_query_is_bounded_canonical_and_literal() {
+ let query =
+ RadrootsFoodAvailabilitySearchQueryV1::parse(" fresh\ncarrots OR title:beets* a\"b ")
+ .expect("bounded query");
+
+ assert_eq!(query.as_str(), "fresh carrots OR title:beets* a\"b");
+ assert_eq!(
+ query.terms(),
+ &["fresh", "carrots", "OR", "title:beets*", "a\"b"]
+ );
+ assert_eq!(
+ query.fts5_match_expression(),
+ "\"fresh\" AND \"carrots\" AND \"OR\" AND \"title:beets*\" AND \"a\"\"b\""
+ );
+ }
+
+ #[test]
+ fn search_query_rejects_empty_oversized_and_excessive_terms() {
+ assert!(matches!(
+ RadrootsFoodAvailabilitySearchQueryV1::parse(" \n\t\0 "),
+ Err(RadrootsEventStoreError::FoodAvailabilitySearchEmpty)
+ ));
+
+ let oversized = "a".repeat(RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1 + 1);
+ assert!(matches!(
+ RadrootsFoodAvailabilitySearchQueryV1::parse(&oversized),
+ Err(RadrootsEventStoreError::FoodAvailabilitySearchTooLarge {
+ max: RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1,
+ actual,
+ }) if actual == oversized.len()
+ ));
+
+ let excessive = (0..=RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1)
+ .map(|index| format!("term{index}"))
+ .collect::<Vec<_>>()
+ .join(" ");
+ assert!(matches!(
+ RadrootsFoodAvailabilitySearchQueryV1::parse(&excessive),
+ Err(RadrootsEventStoreError::FoodAvailabilitySearchTooManyTerms {
+ max: RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1,
+ actual,
+ }) if actual == RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1 + 1
+ ));
+ }
+
+ #[test]
+ fn status_filter_exposes_only_governed_storage_values() {
+ assert_eq!(
+ RadrootsFoodAvailabilityStatusFilterV1::default().storage_value(),
+ None
+ );
+ assert_eq!(
+ RadrootsFoodAvailabilityStatusFilterV1::Active.storage_value(),
+ Some("active")
+ );
+ assert_eq!(
+ RadrootsFoodAvailabilityStatusFilterV1::from(RadrootsFoodAvailabilityStatus::Sold)
+ .storage_value(),
+ Some("sold")
+ );
+ }
+
+ #[test]
+ fn stored_image_derives_qualification_from_typed_diagnostics() {
+ let qualified = RadrootsStoredFoodAvailabilityImageV1 {
+ image_index: 0,
+ raw_tag: vec![
+ "image".to_owned(),
+ "https://example.test/image.webp".to_owned(),
+ ],
+ url: Some("https://example.test/image.webp".to_owned()),
+ dimensions: None,
+ blossom_sha256: None,
+ diagnostics: Vec::new(),
+ };
+ let diagnosed = RadrootsStoredFoodAvailabilityImageV1 {
+ diagnostics: vec![RadrootsFoodAvailabilityImageDiagnostic::DimensionsMissing],
+ ..qualified.clone()
+ };
+
+ assert!(qualified.qualifies());
+ assert!(!diagnosed.qualifies());
+ assert_eq!(
+ diagnosed.diagnostics(),
+ &[RadrootsFoodAvailabilityImageDiagnostic::DimensionsMissing]
+ );
+ }
+
+ #[test]
+ fn projection_wide_count_diagnostic_requires_the_bounded_image_count() {
+ assert!(matches!(
+ validate_projection_diagnostics(
+ &[RadrootsFoodAvailabilityImageDiagnostic::CountExceeded],
+ &[]
+ ),
+ Err(RadrootsEventStoreError::FoodAvailabilityProjectionDrift { .. })
+ ));
+ }
+}
diff --git a/crates/event_store/src/nip09/reconciliation_v1.rs b/crates/event_store/src/nip09/reconciliation_v1.rs
@@ -842,8 +842,13 @@ async fn validate_hook_state_with_events(
pub(crate) async fn validate_active_hook_state_fast(
connection: &mut SqliteConnection,
) -> Result<(), RadrootsEventStoreError> {
- let state = validate_structural_hook_state(connection).await?;
- validate_latest_transitions_match_state(connection, state.generation).await
+ // Supported writes are guarded transactionally. Reopen validates only
+ // constant-cost authority bounds; full history/state and cursor inventory
+ // comparisons remain part of migration and rebuild audits.
+ validate_rebuild_marker_absent(connection).await?;
+ validate_structural_source_state_fast(connection)
+ .await
+ .map(|_| ())
}
async fn validate_structural_hook_state(
@@ -857,6 +862,12 @@ async fn validate_structural_source_state(
connection: &mut SqliteConnection,
) -> Result<SourceState, RadrootsEventStoreError> {
validate_projection_cursor_authority(connection).await?;
+ validate_structural_source_state_fast(connection).await
+}
+
+async fn validate_structural_source_state_fast(
+ connection: &mut SqliteConnection,
+) -> Result<SourceState, RadrootsEventStoreError> {
let rows = sqlx::query(
"SELECT state.active_generation, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, state.last_transition_seq, generation.generation_ordinal, (SELECT MAX(candidate.generation_ordinal) FROM radroots_event_store_source_generation AS candidate) AS max_generation_ordinal, generation.reconciliation_version, generation.addressable_feed_version, generation.event_contract_registry_version, generation.hook_id, generation.hook_manifest_sha256, generation.transition_floor_seq, generation.baseline_raw_event_count, generation.baseline_raw_tag_count, generation.baseline_raw_high_water_seq FROM radroots_event_store_source_state AS state JOIN radroots_event_store_source_generation AS generation ON generation.source_generation = state.active_generation WHERE state.singleton = 1",
)
diff --git a/crates/event_store/src/schema.rs b/crates/event_store/src/schema.rs
@@ -15,6 +15,10 @@ use crate::nip09::reconciliation_v1::{
OsSourceGenerationProvider, ReconciliationCapacityLimits, SourceGenerationProvider,
apply_reconciliation_hook, validate_active_hook_state_fast, validate_reconciliation_capacity,
};
+use crate::store::food_availability_projection_v1::{
+ apply_food_availability_projection_hook_v1,
+ validate_food_availability_projection_hook_state_fast_v1,
+};
#[cfg(test)]
const EMPTY_SCHEMA_SHA256: &str =
@@ -152,7 +156,7 @@ async fn migrate_event_store_schema_with_registry_and_generation_provider(
{
return Ok(());
}
- if has_pending_reconciliation_hook(&status, registry) {
+ if has_pending_source_capacity_hook(&status, registry) {
let mut connection = pool.acquire().await?;
validate_event_store_temp_schema_with_registry(&mut connection, registry).await?;
validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;
@@ -170,7 +174,7 @@ async fn migrate_event_store_schema_with_registry_and_generation_provider(
finish_schema_transaction(transaction, result).await
}
-fn has_pending_reconciliation_hook(
+fn has_pending_source_capacity_hook(
status: &RadrootsEventStoreSchemaStatus,
registry: &[EventStoreMigration],
) -> bool {
@@ -181,7 +185,11 @@ fn has_pending_reconciliation_hook(
};
registry.iter().any(|migration| {
migration.version > current_version
- && migration.hook == EventStoreMigrationHook::Nip09ReconciliationV1
+ && matches!(
+ migration.hook,
+ EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ )
})
}
@@ -290,7 +298,11 @@ async fn migrate_schema_on_connection(
.iter()
.filter(|migration| migration.version > current_version)
{
- if migration.hook == EventStoreMigrationHook::Nip09ReconciliationV1 {
+ if matches!(
+ migration.hook,
+ EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ ) {
validate_reconciliation_capacity(connection, reconciliation_limits).await?;
}
apply_migration_up(connection, registry, migration).await?;
@@ -614,6 +626,9 @@ async fn apply_migration_hook(
EventStoreMigrationHook::Nip09ReconciliationV1 => {
apply_reconciliation_hook(connection, generation_provider, reconciliation_limits).await
}
+ EventStoreMigrationHook::FoodAvailabilityProjectionV1 => {
+ apply_food_availability_projection_hook_v1(connection).await
+ }
}
}
@@ -626,6 +641,9 @@ async fn validate_migration_hook_state(
EventStoreMigrationHook::Nip09ReconciliationV1 => {
validate_active_hook_state_fast(connection).await
}
+ EventStoreMigrationHook::FoodAvailabilityProjectionV1 => {
+ validate_food_availability_projection_hook_state_fast_v1(connection).await
+ }
}
}
@@ -1068,7 +1086,7 @@ DROP TABLE radroots_event_store_v2_parent;";
}
#[tokio::test]
- async fn nip09_capacity_is_rechecked_inside_migration_transaction() {
+ async fn source_capacity_is_rechecked_for_every_rebuild_bound_migration() {
let pool = memory_pool().await;
install_unledgered_baseline(&pool).await;
sqlx::query(
@@ -1097,14 +1115,20 @@ DROP TABLE radroots_event_store_v2_parent;";
limits,
)
.await;
- assert!(matches!(
- finish_schema_transaction(transaction, result).await,
- Err(RadrootsEventStoreError::ReconciliationCapacityExceeded {
- resource: crate::RadrootsEventStoreReconciliationResource::RawEvents,
- actual: 1,
- limit: 0,
- })
- ));
+ let error = finish_schema_transaction(transaction, result)
+ .await
+ .expect_err("reconciliation capacity excess must fail");
+ assert!(
+ matches!(
+ error,
+ RadrootsEventStoreError::ReconciliationCapacityExceeded {
+ resource: crate::RadrootsEventStoreReconciliationResource::RawEvents,
+ actual: 1,
+ limit: 0,
+ }
+ ),
+ "unexpected reconciliation capacity failure: {error:?}"
+ );
assert_eq!(
inspect_event_store_schema_status(&pool)
.await
@@ -1118,6 +1142,78 @@ DROP TABLE radroots_event_store_v2_parent;";
.await
.expect("v2 object count");
assert_eq!(v2_object_count, 0);
+
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(
+ &pool,
+ &EVENT_STORE_MIGRATIONS[..2],
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ 2,
+ )
+ .await
+ .expect("install v2 schema");
+ sqlx::query(
+ "INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, 1, 1, '[]', '', ?, '{}', 'verified', 'unsupported', NULL, 'regular', 0, 1, 1)",
+ )
+ .bind("d".repeat(64))
+ .bind("e".repeat(64))
+ .bind("f".repeat(128))
+ .execute(&pool)
+ .await
+ .expect("post-v2 raw event");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET raw_event_count = 1, raw_high_water_seq = (SELECT MAX(seq) FROM event_envelopes) WHERE singleton = 1",
+ )
+ .execute(&pool)
+ .await
+ .expect("advance post-v2 source authority");
+
+ let mut transaction = pool
+ .begin_with("BEGIN IMMEDIATE")
+ .await
+ .expect("v3 migration transaction");
+ let result = migrate_schema_on_connection(
+ &mut transaction,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ &OsSourceGenerationProvider,
+ limits,
+ )
+ .await;
+ let error = finish_schema_transaction(transaction, result)
+ .await
+ .expect_err("v3 capacity excess must fail");
+ assert!(
+ matches!(
+ error,
+ RadrootsEventStoreError::ReconciliationCapacityExceeded {
+ resource: crate::RadrootsEventStoreReconciliationResource::RawEvents,
+ actual: 1,
+ limit: 0,
+ }
+ ),
+ "unexpected v3 capacity failure: {error:?}"
+ );
+ assert_eq!(
+ inspect_event_store_schema_status(&pool)
+ .await
+ .expect("v2 status after rejected v3 migration"),
+ RadrootsEventStoreSchemaStatus::Managed { version: 2 }
+ );
+ let v3_object_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM sqlite_schema WHERE name = 'radroots_event_store_food_availability_projection'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v3 object count");
+ assert_eq!(v3_object_count, 0);
+ let v3_ledger_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_schema_migrations WHERE version = 3",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v3 ledger count");
+ assert_eq!(v3_ledger_count, 0);
}
#[tokio::test]
@@ -1472,12 +1568,13 @@ DROP TABLE event_envelopes;";
#[tokio::test]
async fn nip09_catalog_matches_the_declared_schema_fingerprint() {
+ let predecessor_registry = &EVENT_STORE_MIGRATIONS[..2];
let pool = memory_pool().await;
- sqlx::raw_sql(EVENT_STORE_MIGRATIONS[0].up_sql)
+ sqlx::raw_sql(predecessor_registry[0].up_sql)
.execute(&pool)
.await
.expect("v1 schema");
- sqlx::raw_sql(EVENT_STORE_MIGRATIONS[1].up_sql)
+ sqlx::raw_sql(predecessor_registry[1].up_sql)
.execute(&pool)
.await
.expect("v2 schema");
@@ -1485,6 +1582,33 @@ DROP TABLE event_envelopes;";
let mut connection = pool.acquire().await.expect("connection");
let catalog = governed_catalog(
&read_catalog(&mut connection).await.expect("catalog"),
+ predecessor_registry,
+ );
+ let declared_object_count = predecessor_registry
+ .iter()
+ .map(|migration| migration.owned_object_names.len())
+ .sum::<usize>();
+
+ assert_eq!(catalog.len(), declared_object_count);
+ assert_eq!(
+ catalog_fingerprint(&catalog),
+ predecessor_registry[1].schema_sha256
+ );
+ }
+
+ #[tokio::test]
+ async fn current_catalog_matches_the_declared_schema_fingerprint() {
+ let pool = memory_pool().await;
+ for migration in EVENT_STORE_MIGRATIONS {
+ sqlx::raw_sql(migration.up_sql)
+ .execute(&pool)
+ .await
+ .expect("migration schema");
+ }
+
+ let mut connection = pool.acquire().await.expect("connection");
+ let catalog = governed_catalog(
+ &read_catalog(&mut connection).await.expect("catalog"),
EVENT_STORE_MIGRATIONS,
);
let declared_object_count = EVENT_STORE_MIGRATIONS
@@ -1495,7 +1619,10 @@ DROP TABLE event_envelopes;";
assert_eq!(catalog.len(), declared_object_count);
assert_eq!(
catalog_fingerprint(&catalog),
- EVENT_STORE_MIGRATIONS[1].schema_sha256
+ EVENT_STORE_MIGRATIONS
+ .last()
+ .expect("current migration")
+ .schema_sha256
);
}
@@ -1927,7 +2054,7 @@ DROP TABLE event_envelopes;";
));
let unknown = AppliedMigration {
- version: 3,
+ version: 4,
name: "future".to_owned(),
up_sha256: "0".repeat(64),
down_sha256: "1".repeat(64),
@@ -1938,12 +2065,13 @@ DROP TABLE event_envelopes;";
&[
row(&EVENT_STORE_MIGRATIONS[0]),
row(&EVENT_STORE_MIGRATIONS[1]),
+ row(&EVENT_STORE_MIGRATIONS[2]),
unknown
],
EVENT_STORE_MIGRATIONS,
- 3
+ 4
),
- Err(RadrootsEventStoreError::UnknownMigration { version: 3 })
+ Err(RadrootsEventStoreError::UnknownMigration { version: 4 })
));
}
@@ -1988,12 +2116,13 @@ DROP TABLE event_envelopes;";
target: 0
})
));
+ let ahead = RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT + 1;
assert!(matches!(
- rollback_event_store_schema_offline(&managed, 3).await,
+ rollback_event_store_schema_offline(&managed, ahead).await,
Err(RadrootsEventStoreError::RollbackAhead {
current: RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
- target: 3
- })
+ target
+ }) if target == ahead
));
rollback_event_store_schema_offline(&managed, 1)
.await
diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs
@@ -1,10 +1,16 @@
+mod addressable_transition_feed_v1;
+mod current_visibility_v1;
+pub(crate) mod food_availability_projection_v1;
mod post_core_extension_capabilities;
mod post_core_extension_dispatcher;
mod post_core_extensions_v1;
+mod post_core_extensions_v2;
mod post_core_storage_v1;
+mod post_core_storage_v2;
mod protocol_reconciliation_v1;
mod protocol_storage_v1;
+use self::current_visibility_v1::current_visibility_in_transaction;
use self::post_core_extension_capabilities::PostCoreExtensionCapabilities;
use self::post_core_extension_dispatcher::dispatch_post_core_extensions;
#[cfg(test)]
@@ -27,13 +33,10 @@ use self::protocol_reconciliation_v1::apply_raw_event_head;
use self::protocol_reconciliation_v1::{
ingest_event_protocol_reconciliation_v1, validate_protocol_post_extensions,
};
-use self::protocol_storage_v1::{
- RawHeadSnapshot, raw_head_coordinate_for_stored_event, raw_head_snapshot_in_transaction,
- stored_raw_event_from_row,
-};
+use self::protocol_storage_v1::{raw_head_snapshot_in_transaction, stored_raw_event_from_row};
use crate::RadrootsEventStoreError;
use crate::model::{
- RadrootsEventAdmissionStatus, RadrootsEventIngest, RadrootsEventIngestReceipt,
+ RadrootsCurrentVisibilityDecisionV1, RadrootsEventIngest, RadrootsEventIngestReceipt,
RadrootsEventStoreSourceGeneration, RadrootsEventStoreStatusSummary, RadrootsEventVisibility,
RadrootsProjectionCursor, RadrootsProjectionRebuildPrior, RadrootsProjectionRebuildTicket,
RadrootsStoredEventTag, RadrootsStoredRawEvent, RadrootsStoredRawEventHead,
@@ -41,11 +44,12 @@ use crate::model::{
RadrootsStoredTradeMissingParent, RadrootsStoredTradeMutation,
RadrootsStoredTradeMutationParent, RadrootsStoredTradeTransportEnvelope,
RadrootsStoredValidEvent, RadrootsStoredVisibleEvent, RadrootsStoredVisibleEventHead,
- RadrootsTradeProjectionCheckpoint, RadrootsTransportObservationType, StoredEventClass,
+ RadrootsTradeProjectionCheckpoint, RadrootsTransportObservationType,
};
#[cfg(test)]
use crate::model::{
- RadrootsEventPersistence, RadrootsRawHeadDecision, RadrootsTransportObservation,
+ RadrootsEventAdmissionStatus, RadrootsEventPersistence, RadrootsRawHeadDecision,
+ RadrootsTransportObservation,
};
#[cfg(test)]
use crate::nip09::reconciliation_v1::ReconciliationProfile;
@@ -61,9 +65,9 @@ use radroots_event::event_head::v1::RadrootsEventHeadCoordinate;
use radroots_event::event_head::v1::{
RadrootsEventHeadCandidateResult, event_head_candidate_for_nip01_event_v1,
};
-#[cfg(test)]
-use radroots_event::ids::RadrootsEventId;
-use radroots_event::ids::{RadrootsDTag, RadrootsTradeId, RadrootsTradeMutationId};
+use radroots_event::ids::{
+ RadrootsDTag, RadrootsEventId, RadrootsTradeId, RadrootsTradeMutationId,
+};
use radroots_event::trade::RadrootsTradeMutationKindV1;
use radroots_transport::{
RadrootsTransportKind, RadrootsTransportTarget, RadrootsTransportTargetFingerprint,
@@ -73,6 +77,8 @@ use radroots_transport::{
use sha2::{Digest, Sha256};
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
use sqlx::{Connection, Row, SqliteConnection, SqlitePool};
+use std::collections::BTreeMap;
+use std::future::Future;
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
@@ -166,7 +172,7 @@ impl RadrootsEventStore {
}
pub async fn pragma_journal_mode(&self) -> Result<String, RadrootsEventStoreError> {
- query_string(&self.pool, "PRAGMA journal_mode").await
+ query_string(&self.pool, "PRAGMA main.journal_mode").await
}
pub async fn status_summary(
@@ -328,13 +334,86 @@ impl RadrootsEventStore {
event_id: &str,
) -> Result<Option<RadrootsEventVisibility>, RadrootsEventStoreError> {
let mut tx = self.pool.begin().await?;
- let Some(snapshot) = visible_event_snapshot(&mut tx, event_id).await? else {
- tx.commit().await?;
- return Ok(None);
- };
- let visibility = visibility_from_snapshot(&snapshot);
+ let visibility = event_visibility_in_transaction(&mut tx, event_id).await?;
+ tx.commit().await?;
+ Ok(visibility)
+ }
+
+ /// Evaluates all requested event ids against one coherent database snapshot.
+ ///
+ /// Results preserve input order and cardinality, including duplicate or
+ /// missing ids, while each distinct id is evaluated only once. The request
+ /// is bounded by [`RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX`], and every id must
+ /// be canonical lowercase 32-byte hex. This is the batch authority for
+ /// callers that must not mix current-visibility decisions from different
+ /// SQLite snapshots.
+ pub async fn event_visibilities<I, S>(
+ &self,
+ event_ids: I,
+ ) -> Result<Vec<Option<RadrootsEventVisibility>>, RadrootsEventStoreError>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+ {
+ self.event_visibilities_with_probe(event_ids, |_| async {
+ Ok::<(), RadrootsEventStoreError>(())
+ })
+ .await
+ }
+
+ async fn event_visibilities_with_probe<I, S, F, Fut>(
+ &self,
+ event_ids: I,
+ mut after_evaluation: F,
+ ) -> Result<Vec<Option<RadrootsEventVisibility>>, RadrootsEventStoreError>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<str>,
+ F: FnMut(usize) -> Fut,
+ Fut: Future<Output = Result<(), RadrootsEventStoreError>>,
+ {
+ let max = RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX as usize;
+ let event_ids = event_ids
+ .into_iter()
+ .take(max.saturating_add(1))
+ .collect::<Vec<_>>();
+ if event_ids.len() > max {
+ return Err(RadrootsEventStoreError::EventVisibilityBatchTooLarge { max });
+ }
+ let event_ids = event_ids
+ .into_iter()
+ .map(|event_id| RadrootsEventId::parse(event_id.as_ref()))
+ .collect::<Result<Vec<_>, _>>()?;
+ let mut unique_event_ids = Vec::new();
+ let mut seen_event_ids = BTreeMap::new();
+ for event_id in &event_ids {
+ if seen_event_ids.insert(event_id.clone(), ()).is_none() {
+ unique_event_ids.push(event_id.clone());
+ }
+ }
+ if unique_event_ids.is_empty() {
+ return Ok(Vec::new());
+ }
+
+ let mut tx = self.pool.begin().await?;
+ let mut evaluated = BTreeMap::new();
+ for (index, event_id) in unique_event_ids.into_iter().enumerate() {
+ let visibility = event_visibility_in_transaction(&mut tx, event_id.as_str()).await?;
+ evaluated.insert(event_id, visibility);
+ after_evaluation(index + 1).await?;
+ }
tx.commit().await?;
- Ok(Some(visibility?))
+
+ event_ids
+ .into_iter()
+ .map(|event_id| {
+ evaluated.get(&event_id).cloned().ok_or_else(|| {
+ RadrootsEventStoreError::CurrentVisibilityDrift {
+ reason: format!("event visibility batch lost evaluated id `{event_id}`"),
+ }
+ })
+ })
+ .collect()
}
pub async fn visible_event(
@@ -342,15 +421,15 @@ impl RadrootsEventStore {
event_id: &str,
) -> Result<Option<RadrootsStoredVisibleEvent>, RadrootsEventStoreError> {
let mut tx = self.pool.begin().await?;
- let Some(snapshot) = visible_event_snapshot(&mut tx, event_id).await? else {
+ let Some(current) = current_visibility_in_transaction(&mut tx, event_id).await? else {
tx.commit().await?;
return Ok(None);
};
- if visibility_from_snapshot(&snapshot)? != RadrootsEventVisibility::Visible {
+ if current.decision() != RadrootsCurrentVisibilityDecisionV1::Visible {
tx.commit().await?;
return Ok(None);
}
- let valid_event = RadrootsStoredValidEvent::try_from_raw(snapshot.raw_event)?;
+ let valid_event = RadrootsStoredValidEvent::try_from_raw(current.event)?;
tx.commit().await?;
Ok(Some(RadrootsStoredVisibleEvent::new(valid_event)))
}
@@ -364,15 +443,19 @@ impl RadrootsEventStore {
tx.commit().await?;
return Ok(None);
};
- let RawHeadSnapshot {
- raw_head,
- raw_event,
- } = snapshot;
- if raw_event.admission_status != RadrootsEventAdmissionStatus::Admitted {
+ let raw_head = snapshot.raw_head;
+ let Some(current) =
+ current_visibility_in_transaction(&mut tx, raw_head.event_id.as_str()).await?
+ else {
+ return Err(RadrootsEventStoreError::StoredHeadInconsistent {
+ event_id: raw_head.event_id,
+ });
+ };
+ if current.decision() != RadrootsCurrentVisibilityDecisionV1::Visible {
tx.commit().await?;
return Ok(None);
}
- let valid_event = RadrootsStoredValidEvent::try_from_raw(raw_event)?;
+ let valid_event = RadrootsStoredValidEvent::try_from_raw(current.event().clone())?;
let event = RadrootsStoredVisibleEvent::new(valid_event);
tx.commit().await?;
Ok(Some(RadrootsStoredVisibleEventHead::new(raw_head, event)))
@@ -969,6 +1052,46 @@ impl RadrootsEventStore {
}
}
+async fn event_visibility_in_transaction(
+ tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
+ event_id: &str,
+) -> Result<Option<RadrootsEventVisibility>, RadrootsEventStoreError> {
+ let Some(current) = current_visibility_in_transaction(tx, event_id).await? else {
+ return Ok(None);
+ };
+ let visibility = match current.decision() {
+ RadrootsCurrentVisibilityDecisionV1::Visible => RadrootsEventVisibility::Visible,
+ RadrootsCurrentVisibilityDecisionV1::NotAdmitted => RadrootsEventVisibility::NotAdmitted,
+ RadrootsCurrentVisibilityDecisionV1::NotCurrent => RadrootsEventVisibility::NotCurrent {
+ raw_head_event_id: current
+ .raw_head_event_id()
+ .ok_or_else(
+ || RadrootsEventStoreError::StoredHeadCoordinateUnavailable {
+ event_id: event_id.to_owned(),
+ },
+ )?
+ .as_str()
+ .to_owned(),
+ },
+ RadrootsCurrentVisibilityDecisionV1::Suppressed => {
+ let evidence = current.suppression().ok_or_else(|| {
+ RadrootsEventStoreError::CurrentVisibilityDrift {
+ reason: format!(
+ "suppressed current visibility is missing evidence for `{event_id}`"
+ ),
+ }
+ })?;
+ RadrootsEventVisibility::Suppressed {
+ reason: evidence.reason,
+ event_reference_request_id: evidence.event_reference_request_id.clone(),
+ address_reference_request_id: evidence.address_reference_request_id.clone(),
+ address_reference_cutoff: evidence.address_reference_cutoff,
+ }
+ }
+ };
+ Ok(Some(visibility))
+}
+
/// Inspects an existing event-store pool without configuring or migrating it.
///
/// The inspection uses one read transaction and applies the same fail-closed
@@ -1020,11 +1143,6 @@ pub struct RadrootsTransportObservationRow {
pub caller_redacted_message: Option<crate::model::RadrootsTransportObservationMessage>,
}
-struct VisibleEventSnapshot {
- raw_event: RadrootsStoredRawEvent,
- raw_head_event_id: Option<String>,
-}
-
async fn configure_pool(
pool: &SqlitePool,
file_backed: bool,
@@ -1082,11 +1200,14 @@ async fn configure_file_journal_mode(
) -> Result<(), RadrootsEventStoreError> {
let mut busy_retries = 0;
loop {
- match sqlx::query("PRAGMA journal_mode = WAL")
- .execute(&mut *connection)
+ match sqlx::query_scalar::<_, String>("PRAGMA main.journal_mode = WAL")
+ .fetch_one(&mut *connection)
.await
{
- Ok(_) => return Ok(()),
+ Ok(actual) if actual == "wal" => return Ok(()),
+ Ok(actual) => {
+ return Err(RadrootsEventStoreError::SqliteFileJournalModeNotWal { actual });
+ }
Err(error)
if sqlite_error_is_busy(&error)
&& busy_retries < FILE_JOURNAL_MODE_BUSY_RETRY_LIMIT =>
@@ -1338,71 +1459,6 @@ async fn validate_projection_cursor_high_water(
Ok(())
}
-async fn visible_event_snapshot(
- tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
- event_id: &str,
-) -> Result<Option<VisibleEventSnapshot>, RadrootsEventStoreError> {
- let row = sqlx::query(
- "SELECT seq, event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms FROM event_envelopes WHERE event_id = ?",
- )
- .bind(event_id)
- .fetch_optional(&mut **tx)
- .await?;
- let Some(row) = row else {
- return Ok(None);
- };
- let raw_event = stored_raw_event_from_row(row)?;
- let raw_head_event_id = match raw_event.event_class {
- StoredEventClass::Regular | StoredEventClass::Ephemeral => None,
- StoredEventClass::Replaceable | StoredEventClass::Addressable => {
- let coordinate = raw_head_coordinate_for_stored_event(&raw_event)?;
- raw_head_snapshot_in_transaction(tx, &coordinate)
- .await?
- .map(|snapshot| snapshot.raw_head.event_id)
- }
- };
- Ok(Some(VisibleEventSnapshot {
- raw_event,
- raw_head_event_id,
- }))
-}
-
-fn visibility_from_snapshot(
- snapshot: &VisibleEventSnapshot,
-) -> Result<RadrootsEventVisibility, RadrootsEventStoreError> {
- let event = &snapshot.raw_event;
- match event.event_class {
- StoredEventClass::Ephemeral => Err(
- RadrootsEventStoreError::StoredRawEventClassificationInconsistent {
- event_id: event.event_id.clone(),
- },
- ),
- StoredEventClass::Regular
- if event.admission_status != RadrootsEventAdmissionStatus::Admitted =>
- {
- Ok(RadrootsEventVisibility::NotAdmitted)
- }
- StoredEventClass::Regular => Ok(RadrootsEventVisibility::Visible),
- StoredEventClass::Replaceable | StoredEventClass::Addressable => {
- if event.admission_status != RadrootsEventAdmissionStatus::Admitted {
- return Ok(RadrootsEventVisibility::NotAdmitted);
- }
- let raw_head_event_id = snapshot.raw_head_event_id.as_ref().ok_or_else(|| {
- RadrootsEventStoreError::StoredHeadCoordinateUnavailable {
- event_id: event.event_id.clone(),
- }
- })?;
- if raw_head_event_id == &event.event_id {
- Ok(RadrootsEventVisibility::Visible)
- } else {
- Ok(RadrootsEventVisibility::NotCurrent {
- raw_head_event_id: raw_head_event_id.clone(),
- })
- }
- }
- }
-}
-
#[cfg_attr(coverage_nightly, coverage(off))]
fn trade_mutation_from_row(
row: sqlx::sqlite::SqliteRow,
@@ -1696,12 +1752,15 @@ mod tests {
TagKind as RadrootsNostrTagKind, Timestamp as RadrootsNostrTimestamp,
};
use radroots_event::draft::RadrootsSignedEvent;
+ use radroots_event::food_availability::{
+ RadrootsFoodAvailabilityStatus, RadrootsFoodIdentifier,
+ };
use radroots_event::ids::{
RadrootsClassifiedListingAddress, RadrootsInventoryBinId, RadrootsPublicKey,
};
use radroots_event::kinds::{
- KIND_CLASSIFIED_LISTING, KIND_DELETION_REQUEST, KIND_GEOCHAT, KIND_LIST_SET_RELAY,
- KIND_POST, KIND_PROFILE, KIND_RELAY_AUTH,
+ KIND_CALENDAR_DATE_EVENT, KIND_CLASSIFIED_LISTING, KIND_DELETION_REQUEST, KIND_FARM,
+ KIND_GEOCHAT, KIND_LIST_SET_RELAY, KIND_POST, KIND_PROFILE, KIND_RELAY_AUTH,
};
use radroots_event::trade::{
RADROOTS_TRADE_DECISION_CONTRACT_ID, RADROOTS_TRADE_PROPOSAL_CONTRACT_ID,
@@ -1714,7 +1773,7 @@ mod tests {
canonical_trade_mutation_content,
};
use radroots_event::wire::{RadrootsNip01EventWire, compute_canonical_nip01_event_id};
- use std::collections::BTreeMap;
+ use radroots_event_codec::food_availability::inbound::RadrootsFoodAvailabilityImageDiagnostic;
const FIXTURE_ALICE_SECRET_KEY_HEX: &str =
"10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
@@ -2037,6 +2096,52 @@ mod tests {
]
}
+ fn admitted_operational_listing_tags(d_tag: &str, published_at: u64) -> Vec<Vec<String>> {
+ vec![
+ vec!["d".to_owned(), d_tag.to_owned()],
+ vec!["p".to_owned(), FIXTURE_ALICE_PUBLIC_KEY_HEX.to_owned()],
+ vec![
+ "a".to_owned(),
+ format!("{KIND_FARM}:{FIXTURE_ALICE_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAA"),
+ ],
+ vec!["key".to_owned(), "carrot-nantes".to_owned()],
+ vec!["title".to_owned(), "Nantes Carrots".to_owned()],
+ vec!["category".to_owned(), "produce".to_owned()],
+ vec![
+ "summary".to_owned(),
+ "Fresh bunches harvested in Saanich".to_owned(),
+ ],
+ vec!["published_at".to_owned(), published_at.to_string()],
+ vec!["radroots:primary_bin".to_owned(), "bunch".to_owned()],
+ vec![
+ "radroots:bin".to_owned(),
+ "bunch".to_owned(),
+ "1".to_owned(),
+ "each".to_owned(),
+ ],
+ vec![
+ "radroots:price".to_owned(),
+ "bunch".to_owned(),
+ "4".to_owned(),
+ "CAD".to_owned(),
+ "1".to_owned(),
+ "each".to_owned(),
+ ],
+ vec!["price".to_owned(), "4".to_owned(), "CAD".to_owned()],
+ vec!["inventory".to_owned(), "24".to_owned()],
+ vec!["status".to_owned(), "active".to_owned()],
+ vec!["delivery".to_owned(), "pickup".to_owned()],
+ vec![
+ "location".to_owned(),
+ "Saanich Peninsula".to_owned(),
+ "Victoria".to_owned(),
+ "BC".to_owned(),
+ "CA".to_owned(),
+ ],
+ vec!["g".to_owned(), "c28hr".to_owned()],
+ ]
+ }
+
fn head_coordinate_for_event(event: &RadrootsSignedEvent) -> RadrootsEventHeadCoordinate {
let RadrootsEventHeadCandidateResult::Candidate(candidate) =
event_head_candidate_for_nip01_event_v1(event.envelope())
@@ -2070,6 +2175,62 @@ mod tests {
signed_event_with_keys(keys, KIND_DELETION_REQUEST, created_at, tags, "")
}
+ fn food_availability_event(
+ created_at: u32,
+ d_tag: &str,
+ title: &str,
+ summary: &str,
+ status: &str,
+ mut images: Vec<Vec<String>>,
+ ) -> RadrootsSignedEvent {
+ let mut tags = vec![
+ vec!["d".to_owned(), d_tag.to_owned()],
+ vec!["title".to_owned(), title.to_owned()],
+ vec!["summary".to_owned(), summary.to_owned()],
+ vec!["published_at".to_owned(), "100".to_owned()],
+ vec!["location".to_owned(), "Central Saanich, BC".to_owned()],
+ vec!["price".to_owned(), "3".to_owned(), "CAD".to_owned()],
+ vec!["radroots:price_unit".to_owned(), "lb".to_owned()],
+ vec![
+ "radroots:quantity".to_owned(),
+ "10".to_owned(),
+ "lb".to_owned(),
+ ],
+ vec!["status".to_owned(), status.to_owned()],
+ ];
+ tags.append(&mut images);
+ signed_event(
+ KIND_CLASSIFIED_LISTING,
+ created_at,
+ tags,
+ format!("{summary} Available in Victoria this week.").as_str(),
+ )
+ }
+
+ fn calendar_date_event(
+ created_at: u32,
+ d_tag: &str,
+ content: impl Into<String>,
+ ) -> RadrootsSignedEvent {
+ let content = content.into();
+ signed_event(
+ KIND_CALENDAR_DATE_EVENT,
+ created_at,
+ vec![
+ vec!["d".to_owned(), d_tag.to_owned()],
+ vec!["title".to_owned(), "Victoria Market Day".to_owned()],
+ vec!["start".to_owned(), "2026-07-20".to_owned()],
+ vec!["end".to_owned(), "2026-07-21".to_owned()],
+ vec!["location".to_owned(), "Victoria, BC".to_owned()],
+ ],
+ content.as_str(),
+ )
+ }
+
+ fn food_availability_coordinate(d_tag: &str) -> String {
+ format!("{KIND_CLASSIFIED_LISTING}:{FIXTURE_ALICE_PUBLIC_KEY_HEX}:{d_tag}")
+ }
+
fn addressable_coordinate(d_tag: &str) -> String {
format!("{KIND_LIST_SET_RELAY}:{FIXTURE_ALICE_PUBLIC_KEY_HEX}:{d_tag}")
}
@@ -2086,6 +2247,18 @@ mod tests {
);
}
+ async fn rollback_store_to_v2(store: &RadrootsEventStore) {
+ rollback_event_store_schema_offline(store.pool(), 2)
+ .await
+ .expect("rollback to v2");
+ assert_eq!(
+ inspect_event_store_schema_status(store.pool())
+ .await
+ .expect("v2 status"),
+ RadrootsEventStoreSchemaStatus::Managed { version: 2 }
+ );
+ }
+
async fn migrate_store_with_generation(
store: &RadrootsEventStore,
generation: [u8; 32],
@@ -2621,6 +2794,108 @@ mod tests {
}
#[tokio::test]
+ async fn food_projection_migration_backfills_v2_and_survives_rollback_reupgrade() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let food = food_availability_event(
+ 200,
+ "migration-carrots",
+ "Nantes Carrots",
+ "Fresh bunches",
+ "active",
+ vec![vec![
+ "image".to_owned(),
+ "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp"
+ .to_owned(),
+ "800x600".to_owned(),
+ ]],
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(food.clone(), 2_000))
+ .await
+ .expect("FoodAvailability ingest");
+ let generation = store.source_generation().await.expect("source generation");
+ let raw_digest = raw_authority_digest(&store).await;
+ let projected = store
+ .food_availability_v1(
+ &RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author"),
+ &RadrootsFoodIdentifier::parse("migration-carrots").expect("identifier"),
+ )
+ .await
+ .expect("projection lookup")
+ .expect("projection");
+ assert_eq!(projected.event_id().as_str(), food.id_str());
+ assert_eq!(projected.images().len(), 1);
+ assert!(projected.images()[0].qualifies());
+ assert_eq!(
+ projected.images()[0].blossom_sha256(),
+ Some(
+ radroots_blossom::RadrootsBlossomSha256::from_hex(
+ "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
+ )
+ .expect("Blossom digest"),
+ )
+ );
+ let search = crate::RadrootsFoodAvailabilitySearchQueryV1::parse("Nantes Carrots")
+ .expect("search query");
+
+ for cycle in 0..2 {
+ rollback_store_to_v2(&store).await;
+ let successor_objects: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM sqlite_schema WHERE name LIKE 'radroots_event_store_food_availability_%' OR name IN ('radroots_event_store_addressable_feed_generation_insert', 'radroots_event_store_addressable_feed_integrity_v1', 'radroots_event_store_addressable_feed_transition_insert', 'radroots_event_store_addressable_transition_coordinate_idx', 'radroots_event_store_current_visibility_head_lookup_idx', 'radroots_event_store_current_visibility_v1', 'radroots_event_store_nip09_address_target_visibility_lookup_idx')",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("successor object count");
+ assert_eq!(successor_objects, 0);
+ assert_eq!(raw_authority_digest(&store).await, raw_digest);
+ let transitions: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_addressable_head_transition WHERE kind = 30402",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("v2 FoodAvailability transitions");
+ assert_eq!(transitions, 1);
+
+ migrate_store_with_generation(
+ &store,
+ [0x70 + u8::try_from(cycle).expect("bounded cycle"); 32],
+ )
+ .await
+ .expect("v2 to v3 re-upgrade");
+ assert_eq!(
+ store.source_generation().await.expect("generation"),
+ generation
+ );
+ assert_eq!(raw_authority_digest(&store).await, raw_digest);
+ let rebuilt = store
+ .food_availability_v1(
+ &RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author"),
+ &RadrootsFoodIdentifier::parse("migration-carrots").expect("identifier"),
+ )
+ .await
+ .expect("rebuilt projection lookup")
+ .expect("rebuilt projection");
+ assert_eq!(rebuilt, projected);
+ let matches = store
+ .search_food_availability_v1(
+ &search,
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Active,
+ 10,
+ )
+ .await
+ .expect("rebuilt FTS search");
+ assert_eq!(matches, vec![projected.clone()]);
+ let cursor_and_high_water: (i64, i64) = sqlx::query_as(
+ "SELECT cursor.last_transition_seq, source.last_transition_seq FROM radroots_event_store_food_availability_cursor AS cursor JOIN radroots_event_store_source_state AS source ON source.singleton = 1 WHERE cursor.singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("projection cursor high-water");
+ assert_eq!(cursor_and_high_water.0, cursor_and_high_water.1);
+ }
+ }
+
+ #[tokio::test]
async fn nip09_migration_entropy_and_legacy_source_failures_are_atomic() {
let entropy_store = RadrootsEventStore::open_memory().await.expect("open");
let event = signed_event(KIND_POST, 20, Vec::new(), "entropy");
@@ -3649,6 +3924,37 @@ mod tests {
}
#[tokio::test]
+ async fn file_journal_mode_configuration_rejects_successful_non_wal_result() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let path = tempdir.path().join("immutable-delete.sqlite");
+ let mut writer = SqliteConnection::connect_with(
+ &SqliteConnectOptions::new()
+ .filename(&path)
+ .create_if_missing(true),
+ )
+ .await
+ .expect("writer connection");
+ let initial_mode: String = sqlx::query_scalar("PRAGMA main.journal_mode = DELETE")
+ .fetch_one(&mut writer)
+ .await
+ .expect("delete journal mode");
+ assert_eq!(initial_mode, "delete");
+ writer.close().await.expect("close writer");
+
+ let mut connection = SqliteConnection::connect_with(
+ &SqliteConnectOptions::new().filename(&path).immutable(true),
+ )
+ .await
+ .expect("immutable connection");
+
+ assert!(matches!(
+ configure_file_journal_mode(&mut connection).await,
+ Err(RadrootsEventStoreError::SqliteFileJournalModeNotWal { actual })
+ if actual == "delete"
+ ));
+ }
+
+ #[tokio::test]
async fn open_pool_configures_every_file_connection_and_rejects_multi_connection_memory() {
let memory_options = SqliteConnectOptions::from_str("sqlite::memory:")
.expect("memory options")
@@ -3735,6 +4041,11 @@ mod tests {
.await
.expect("foreign keys");
assert_eq!(foreign_keys, 1);
+ let journal_mode: String = sqlx::query_scalar("PRAGMA main.journal_mode")
+ .fetch_one(&mut **connection)
+ .await
+ .expect("journal mode");
+ assert_eq!(journal_mode, "wal");
let orphan = sqlx::query(
"INSERT INTO event_envelope_tags(event_id, tag_index, tag_name, tag_value, tag_json, contract_semantic, contract_value_type, relay_indexed) VALUES ('missing', 0, 'd', 'value', '[\"d\",\"value\"]', NULL, NULL, 0)",
)
@@ -4010,10 +4321,12 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
let first = RadrootsEventStore::open_file(&path).await.expect("first");
assert_eq!(first.pragma_foreign_keys().await.expect("foreign_keys"), 1);
+ assert_eq!(first.pragma_journal_mode().await.expect("journal"), "wal");
drop(first);
let second = RadrootsEventStore::open_file(&path).await.expect("second");
assert_eq!(second.pragma_foreign_keys().await.expect("foreign_keys"), 1);
+ assert_eq!(second.pragma_journal_mode().await.expect("journal"), "wal");
}
#[tokio::test]
@@ -5375,6 +5688,1926 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
}
#[tokio::test]
+ async fn food_availability_projection_replaces_and_queries_real_ingest_events() {
+ const BLOSSOM_CARROTS: &str = "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp";
+ const BLOSSOM_DETAIL: &str = "https://media.example/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.jpg";
+
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let carrots = food_availability_event(
+ 200,
+ "nantes-carrots",
+ "Nantes Carrots",
+ "Fresh bunches",
+ "active",
+ vec![
+ vec![
+ "image".to_owned(),
+ BLOSSOM_CARROTS.to_owned(),
+ "800x600".to_owned(),
+ ],
+ vec!["image".to_owned(), BLOSSOM_DETAIL.to_owned()],
+ ],
+ );
+ let kale = food_availability_event(
+ 201,
+ "lacinato-kale",
+ "Lacinato Kale",
+ "Tender greens",
+ "active",
+ Vec::new(),
+ );
+
+ for (observed_at_ms, event) in [(10_000, &carrots), (10_001, &kale)] {
+ let receipt = store
+ .ingest_event(RadrootsEventIngest::new(event.clone(), observed_at_ms))
+ .await
+ .expect("food ingest");
+ assert_eq!(
+ receipt.admission_status,
+ RadrootsEventAdmissionStatus::Admitted
+ );
+ assert_eq!(
+ receipt.contract_id.as_deref(),
+ Some("radroots.food.availability.v1")
+ );
+ }
+ let initial_projection_count: i64 = sqlx::query_scalar(
+ "SELECT projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("sealed projection count");
+ assert_eq!(initial_projection_count, 2);
+
+ let author = RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author");
+ let carrot_id = RadrootsFoodIdentifier::parse("nantes-carrots").expect("identifier");
+ let projected = store
+ .food_availability_v1(&author, &carrot_id)
+ .await
+ .expect("food lookup")
+ .expect("projected carrots");
+ assert_eq!(projected.event_id().as_str(), carrots.id_str());
+ assert_eq!(projected.title().as_str(), "Nantes Carrots");
+ assert_eq!(projected.summary().as_str(), "Fresh bunches");
+ assert_eq!(projected.price().amount(), "3");
+ assert_eq!(projected.price().currency().as_str(), "CAD");
+ assert_eq!(projected.price().unit().as_str(), "lb");
+ assert_eq!(projected.quantity().expect("quantity").amount(), "10");
+ assert_eq!(projected.status(), RadrootsFoodAvailabilityStatus::Active);
+ assert_eq!(
+ projected.diagnostics(),
+ &[
+ RadrootsFoodAvailabilityImageDiagnostic::ShapeInvalid,
+ RadrootsFoodAvailabilityImageDiagnostic::DimensionsMissing,
+ ]
+ );
+ assert_eq!(projected.images().len(), 2);
+ assert!(projected.images()[0].qualifies());
+ assert_eq!(
+ projected.images()[0].blossom_sha256(),
+ Some(
+ radroots_blossom::RadrootsBlossomSha256::from_hex(
+ "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
+ )
+ .expect("Blossom digest"),
+ )
+ );
+ assert!(!projected.images()[1].qualifies());
+ assert_eq!(
+ projected.images()[1].blossom_sha256(),
+ Some(
+ radroots_blossom::RadrootsBlossomSha256::from_hex(
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ )
+ .expect("Blossom digest"),
+ )
+ );
+
+ let active = store
+ .recent_food_availability_v1(crate::RadrootsFoodAvailabilityStatusFilterV1::Active, 10)
+ .await
+ .expect("active food");
+ assert_eq!(active.len(), 2);
+ let carrots_query =
+ crate::RadrootsFoodAvailabilitySearchQueryV1::parse("Nantes Central Saanich")
+ .expect("search query");
+ let matches = store
+ .search_food_availability_v1(
+ &carrots_query,
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Any,
+ 10,
+ )
+ .await
+ .expect("search");
+ assert_eq!(matches.len(), 1);
+ assert_eq!(matches[0].event_id().as_str(), carrots.id_str());
+ let summary_query = crate::RadrootsFoodAvailabilitySearchQueryV1::parse("Fresh")
+ .expect("summary search query");
+ let summary_matches = store
+ .search_food_availability_v1(
+ &summary_query,
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Any,
+ 10,
+ )
+ .await
+ .expect("summary search");
+ assert_eq!(summary_matches.len(), 1);
+ assert_eq!(summary_matches[0].event_id().as_str(), carrots.id_str());
+
+ let sold = food_availability_event(
+ 220,
+ "nantes-carrots",
+ "Nantes Carrots Sold",
+ "Farm stand sold out",
+ "sold",
+ Vec::new(),
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(sold.clone(), 10_002))
+ .await
+ .expect("sold replacement");
+
+ let replacement = store
+ .food_availability_v1(&author, &carrot_id)
+ .await
+ .expect("replacement lookup")
+ .expect("sold projection");
+ assert_eq!(replacement.event_id().as_str(), sold.id_str());
+ assert_eq!(replacement.status(), RadrootsFoodAvailabilityStatus::Sold);
+ assert_eq!(replacement.published_at().as_u64(), 100);
+ assert!(replacement.images().is_empty());
+ let replacement_projection_count: i64 = sqlx::query_scalar(
+ "SELECT projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("sealed replacement projection count");
+ assert_eq!(replacement_projection_count, 2);
+ assert_eq!(
+ store
+ .current_event_visibility_v1(carrots.id_str())
+ .await
+ .expect("old visibility")
+ .expect("stored old revision")
+ .decision(),
+ crate::RadrootsCurrentVisibilityDecisionV1::NotCurrent
+ );
+ assert_eq!(
+ store
+ .current_event_visibility_v1(sold.id_str())
+ .await
+ .expect("sold visibility")
+ .expect("stored sold revision")
+ .decision(),
+ crate::RadrootsCurrentVisibilityDecisionV1::Visible
+ );
+
+ let active = store
+ .recent_food_availability_v1(crate::RadrootsFoodAvailabilityStatusFilterV1::Active, 10)
+ .await
+ .expect("active after replacement");
+ assert_eq!(active.len(), 1);
+ assert_eq!(active[0].event_id().as_str(), kale.id_str());
+ let sold_rows = store
+ .recent_food_availability_v1(crate::RadrootsFoodAvailabilityStatusFilterV1::Sold, 10)
+ .await
+ .expect("sold food");
+ assert_eq!(sold_rows.len(), 1);
+ assert_eq!(sold_rows[0].event_id().as_str(), sold.id_str());
+
+ let stale_query = crate::RadrootsFoodAvailabilitySearchQueryV1::parse("Fresh bunches")
+ .expect("stale query");
+ assert!(
+ store
+ .search_food_availability_v1(
+ &stale_query,
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Any,
+ 10,
+ )
+ .await
+ .expect("stale search")
+ .is_empty()
+ );
+ let replacement_query = crate::RadrootsFoodAvailabilitySearchQueryV1::parse("Farm sold")
+ .expect("replacement query");
+ let replacement_matches = store
+ .search_food_availability_v1(
+ &replacement_query,
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Sold,
+ 10,
+ )
+ .await
+ .expect("replacement search");
+ assert_eq!(replacement_matches.len(), 1);
+ assert_eq!(replacement_matches[0].event_id().as_str(), sold.id_str());
+ }
+
+ #[tokio::test]
+ async fn food_availability_projection_guards_images_and_exhaustive_audit_detects_fts_drift() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let with_image = food_availability_event(
+ 200,
+ "guarded-carrots",
+ "Guarded Carrots",
+ "Fresh harvest",
+ "active",
+ vec![vec![
+ "image".to_owned(),
+ "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp"
+ .to_owned(),
+ "800x600".to_owned(),
+ ]],
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(with_image, 19_000))
+ .await
+ .expect("FoodAvailability ingest");
+
+ let image_delete = sqlx::query(
+ "DELETE FROM radroots_event_store_food_availability_image WHERE d_tag = 'guarded-carrots'",
+ )
+ .execute(store.pool())
+ .await
+ .expect_err("direct image delete must be guarded");
+ assert!(
+ image_delete
+ .to_string()
+ .contains("image delete is not backed by a pending retraction")
+ );
+ let cursor_update = sqlx::query(
+ "UPDATE radroots_event_store_food_availability_cursor SET last_transition_seq = last_transition_seq WHERE singleton = 1",
+ )
+ .execute(store.pool())
+ .await
+ .expect_err("projection cursor must reject direct writes");
+ assert!(
+ cursor_update
+ .to_string()
+ .contains("FoodAvailability cursor update is invalid")
+ );
+
+ let replacement = food_availability_event(
+ 210,
+ "guarded-carrots",
+ "Guarded Carrots",
+ "Sold at market",
+ "sold",
+ Vec::new(),
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(replacement.clone(), 19_001))
+ .await
+ .expect("authorized replacement cascade");
+ let image_count: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM radroots_event_store_food_availability_image")
+ .fetch_one(store.pool())
+ .await
+ .expect("image count");
+ assert_eq!(image_count, 0);
+
+ let event_seq: i64 = sqlx::query_scalar(
+ "SELECT event_seq FROM radroots_event_store_food_availability_projection WHERE event_id = ?",
+ )
+ .bind(replacement.id_str())
+ .fetch_one(store.pool())
+ .await
+ .expect("replacement sequence");
+ sqlx::query(
+ "DELETE FROM radroots_event_store_food_availability_search_fts WHERE rowid = ?",
+ )
+ .bind(event_seq)
+ .execute(store.pool())
+ .await
+ .expect("test-only FTS corruption");
+ assert!(matches!(
+ store.audit_food_availability_projection_v1().await,
+ Err(RadrootsEventStoreError::FoodAvailabilityProjectionDrift { .. })
+ ));
+ }
+
+ #[tokio::test]
+ async fn food_availability_exhaustive_audit_rejects_wrong_source_transition_authority() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ for event in [
+ food_availability_event(
+ 200,
+ "transition-carrots",
+ "Transition Carrots",
+ "First harvest",
+ "active",
+ Vec::new(),
+ ),
+ food_availability_event(
+ 201,
+ "transition-kale",
+ "Transition Kale",
+ "Second harvest",
+ "active",
+ Vec::new(),
+ ),
+ ] {
+ store
+ .ingest_event(RadrootsEventIngest::new(event, 19_100))
+ .await
+ .expect("FoodAvailability ingest");
+ }
+
+ let wrong_transition_seq: i64 = sqlx::query_scalar(
+ "SELECT source_transition_seq FROM radroots_event_store_food_availability_projection WHERE d_tag = 'transition-kale'",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("wrong-coordinate transition");
+ sqlx::query("DROP TRIGGER radroots_event_store_food_availability_projection_update_guard")
+ .execute(store.pool())
+ .await
+ .expect("trusted projection guard removal");
+ sqlx::query(
+ "UPDATE radroots_event_store_food_availability_projection SET source_transition_seq = ? WHERE d_tag = 'transition-carrots'",
+ )
+ .bind(wrong_transition_seq)
+ .execute(store.pool())
+ .await
+ .expect("trusted source-transition corruption");
+
+ let error = store
+ .audit_food_availability_projection_v1()
+ .await
+ .expect_err("wrong source transition must fail exhaustive audit");
+ assert!(
+ matches!(
+ error,
+ RadrootsEventStoreError::FoodAvailabilityProjectionDrift { ref reason }
+ if reason.contains("source transition")
+ ),
+ "{error}"
+ );
+ }
+
+ #[tokio::test]
+ async fn food_availability_exhaustive_audit_compares_exact_head_coordinates() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ store
+ .ingest_event(RadrootsEventIngest::new(
+ food_availability_event(
+ 200,
+ "coordinate-carrots",
+ "Coordinate Carrots",
+ "Coordinate harvest",
+ "active",
+ Vec::new(),
+ ),
+ 19_200,
+ ))
+ .await
+ .expect("FoodAvailability ingest");
+
+ let mut connection = store.pool().acquire().await.expect("trusted connection");
+ for statement in [
+ "DROP TRIGGER radroots_event_store_addressable_state_identity_update_guard",
+ "DROP TRIGGER radroots_event_store_addressable_state_old_update_guard",
+ ] {
+ sqlx::query(statement)
+ .execute(&mut *connection)
+ .await
+ .expect("trusted head-state guard removal");
+ }
+ sqlx::query("PRAGMA foreign_keys = OFF")
+ .execute(&mut *connection)
+ .await
+ .expect("disable trusted foreign-key enforcement");
+ sqlx::query(
+ "UPDATE radroots_event_store_addressable_head_state SET d_tag = 'retargeted-carrots' WHERE d_tag = 'coordinate-carrots'",
+ )
+ .execute(&mut *connection)
+ .await
+ .expect("trusted head-coordinate corruption");
+ sqlx::query("PRAGMA foreign_keys = ON")
+ .execute(&mut *connection)
+ .await
+ .expect("restore foreign-key enforcement");
+ drop(connection);
+
+ let error = store
+ .audit_food_availability_projection_v1()
+ .await
+ .expect_err("retargeted head coordinate must fail exhaustive audit");
+ assert!(
+ matches!(
+ error,
+ RadrootsEventStoreError::FoodAvailabilityProjectionDrift { ref reason }
+ if reason.contains("coordinate witnesses")
+ ),
+ "{error}"
+ );
+ }
+
+ #[tokio::test]
+ async fn food_availability_exhaustive_audit_reserves_wal_writer_before_snapshot_reads() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let path = tempdir.path().join("food-audit-wal.sqlite");
+ let audit_store = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("audit store");
+ let writer_store = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("writer store");
+ audit_store
+ .ingest_event(RadrootsEventIngest::new(
+ food_availability_event(
+ 200,
+ "wal-carrots",
+ "WAL Carrots",
+ "Serialized audit harvest",
+ "active",
+ Vec::new(),
+ ),
+ 19_300,
+ ))
+ .await
+ .expect("FoodAvailability ingest");
+ assert_eq!(
+ audit_store
+ .pragma_journal_mode()
+ .await
+ .expect("journal mode"),
+ "wal"
+ );
+
+ let checkpoint_reached = std::sync::Arc::new(tokio::sync::Notify::new());
+ let checkpoint_release = std::sync::Arc::new(tokio::sync::Notify::new());
+ let audit_task = tokio::spawn(
+ super::food_availability_projection_v1::FOOD_AVAILABILITY_AUDIT_FTS_CHECKPOINT.scope(
+ (
+ std::sync::Arc::clone(&checkpoint_reached),
+ std::sync::Arc::clone(&checkpoint_release),
+ ),
+ async move { audit_store.audit_food_availability_projection_v1().await },
+ ),
+ );
+ checkpoint_reached.notified().await;
+
+ let writer_started = std::sync::Arc::new(tokio::sync::Notify::new());
+ let writer_started_task = std::sync::Arc::clone(&writer_started);
+ let writer_task = tokio::spawn(async move {
+ writer_started_task.notify_one();
+ let transaction = writer_store
+ .begin_write_transaction()
+ .await
+ .expect("competing writer transaction");
+ transaction.commit().await.expect("competing writer commit");
+ });
+ writer_started.notified().await;
+ tokio::task::yield_now().await;
+ assert!(
+ !writer_task.is_finished(),
+ "competing writer acquired while the audit was paused before FTS integrity-check"
+ );
+
+ checkpoint_release.notify_one();
+ audit_task
+ .await
+ .expect("audit task")
+ .expect("serialized exhaustive audit");
+ writer_task.await.expect("competing writer task");
+ }
+
+ #[tokio::test]
+ async fn food_availability_queries_enforce_limits_order_ties_and_execute_literal_fts_input() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let events = [
+ food_availability_event(
+ 200,
+ "query-carrots",
+ "Query Carrots",
+ "Shared harvest",
+ "active",
+ Vec::new(),
+ ),
+ food_availability_event(
+ 201,
+ "query-kale",
+ "Query Kale",
+ "Shared harvest",
+ "active",
+ Vec::new(),
+ ),
+ food_availability_event(
+ 202,
+ "query-beets",
+ "Query Beets",
+ "Shared harvest",
+ "active",
+ Vec::new(),
+ ),
+ ];
+ for (index, event) in events.iter().enumerate() {
+ store
+ .ingest_event(RadrootsEventIngest::new(
+ event.clone(),
+ 19_100 + i64::try_from(index).expect("index"),
+ ))
+ .await
+ .expect("query fixture ingest");
+ }
+
+ let mut expected_ids = events
+ .iter()
+ .map(|event| event.id_str().to_owned())
+ .collect::<Vec<_>>();
+ expected_ids.sort();
+ let recent = store
+ .recent_food_availability_v1(
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Any,
+ RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX,
+ )
+ .await
+ .expect("maximum-limit recent query");
+ assert_eq!(
+ recent
+ .iter()
+ .map(|projection| projection.event_id().as_str())
+ .collect::<Vec<_>>(),
+ expected_ids.iter().map(String::as_str).collect::<Vec<_>>(),
+ );
+ assert_eq!(
+ store
+ .recent_food_availability_v1(crate::RadrootsFoodAvailabilityStatusFilterV1::Any, 1,)
+ .await
+ .expect("minimum-limit recent query")[0]
+ .event_id()
+ .as_str(),
+ expected_ids[0],
+ );
+ for invalid_limit in [0, RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX + 1] {
+ assert!(matches!(
+ store
+ .recent_food_availability_v1(
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Any,
+ invalid_limit,
+ )
+ .await,
+ Err(RadrootsEventStoreError::QueryLimitOutOfRange { .. })
+ ));
+ }
+
+ let shared = crate::RadrootsFoodAvailabilitySearchQueryV1::parse("Shared")
+ .expect("shared search query");
+ let search = store
+ .search_food_availability_v1(
+ &shared,
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Any,
+ RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX,
+ )
+ .await
+ .expect("maximum-limit search query");
+ assert_eq!(
+ search
+ .iter()
+ .map(|projection| projection.event_id().as_str())
+ .collect::<Vec<_>>(),
+ expected_ids.iter().map(String::as_str).collect::<Vec<_>>(),
+ );
+ for hostile in ["\"", "()", "title:", "*", "---", "OR title:beets*"] {
+ let query = crate::RadrootsFoodAvailabilitySearchQueryV1::parse(hostile)
+ .expect("literal hostile query");
+ store
+ .search_food_availability_v1(
+ &query,
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Any,
+ 1,
+ )
+ .await
+ .expect("hostile input remains a valid literal FTS query");
+ }
+ assert!(matches!(
+ store
+ .search_food_availability_v1(
+ &shared,
+ crate::RadrootsFoodAvailabilityStatusFilterV1::Any,
+ 0,
+ )
+ .await,
+ Err(RadrootsEventStoreError::QueryLimitOutOfRange { .. })
+ ));
+ }
+
+ #[tokio::test]
+ async fn current_visibility_and_food_reads_use_bounded_authority_indexes() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let visibility_plan = explain_query_plan(
+ &store,
+ "EXPLAIN QUERY PLAN SELECT suppression_outcome, suppression_reason, event_reference_request_id, address_reference_request_id, address_reference_cutoff, current_visibility FROM radroots_event_store_current_visibility_v1 WHERE event_id = ?",
+ event_id('a').as_str(),
+ )
+ .await;
+ assert!(
+ visibility_plan.contains("radroots_event_store_nip09_event_target_lookup_idx"),
+ "{visibility_plan}"
+ );
+ assert!(
+ visibility_plan
+ .contains("radroots_event_store_nip09_address_target_visibility_lookup_idx"),
+ "{visibility_plan}"
+ );
+ assert!(
+ !visibility_plan.contains("USE TEMP B-TREE"),
+ "{visibility_plan}"
+ );
+
+ let food_sql = format!(
+ "EXPLAIN QUERY PLAN {}",
+ super::food_availability_projection_v1::FOOD_AVAILABILITY_RECENT_QUERY_V1
+ );
+ let food_plan = explain_query_plan(&store, food_sql.as_str(), "1000").await;
+ assert!(
+ food_plan.contains("radroots_event_store_food_availability_recent_idx"),
+ "{food_plan}"
+ );
+ assert!(
+ food_plan.contains("SEARCH head USING PRIMARY KEY"),
+ "{food_plan}"
+ );
+ assert!(
+ !food_plan.contains("radroots_event_store_nip09_event_target")
+ && !food_plan.contains("radroots_event_store_nip09_address_target"),
+ "{food_plan}"
+ );
+ assert!(!food_plan.contains("USE TEMP B-TREE"), "{food_plan}");
+ }
+
+ #[tokio::test]
+ async fn food_availability_retraction_never_resurrects_an_older_revision() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let active = food_availability_event(
+ 200,
+ "nantes-carrots",
+ "Nantes Carrots",
+ "Fresh bunches",
+ "active",
+ Vec::new(),
+ );
+ let sold = food_availability_event(
+ 220,
+ "nantes-carrots",
+ "Nantes Carrots Sold",
+ "Sold at market",
+ "sold",
+ Vec::new(),
+ );
+ let deletion = deletion_event(
+ &fixture_keys(),
+ 230,
+ vec![vec![
+ "a".to_owned(),
+ food_availability_coordinate("nantes-carrots"),
+ ]],
+ );
+ let older = food_availability_event(
+ 210,
+ "nantes-carrots",
+ "Nantes Carrots Older",
+ "Older active revision",
+ "active",
+ Vec::new(),
+ );
+ let recovered = food_availability_event(
+ 240,
+ "nantes-carrots",
+ "Nantes Carrots Restocked",
+ "Fresh restock",
+ "active",
+ Vec::new(),
+ );
+ let author = RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author");
+ let identifier = RadrootsFoodIdentifier::parse("nantes-carrots").expect("identifier");
+
+ for (observed_at_ms, event) in [(20_000, &active), (20_001, &sold)] {
+ store
+ .ingest_event(RadrootsEventIngest::new(event.clone(), observed_at_ms))
+ .await
+ .expect("food revision");
+ }
+ store
+ .ingest_event(RadrootsEventIngest::new(deletion.clone(), 20_002))
+ .await
+ .expect("address deletion");
+ assert!(
+ store
+ .food_availability_v1(&author, &identifier)
+ .await
+ .expect("retracted lookup")
+ .is_none()
+ );
+ let retracted_projection_count: i64 = sqlx::query_scalar(
+ "SELECT projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("sealed retracted projection count");
+ assert_eq!(retracted_projection_count, 0);
+ let suppressed = store
+ .current_event_visibility_v1(sold.id_str())
+ .await
+ .expect("suppressed visibility")
+ .expect("stored sold revision");
+ assert_eq!(
+ suppressed.decision(),
+ crate::RadrootsCurrentVisibilityDecisionV1::Suppressed
+ );
+ let evidence = suppressed.suppression().expect("suppression evidence");
+ assert_eq!(
+ evidence.reason(),
+ crate::RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff
+ );
+ assert_eq!(
+ evidence
+ .address_reference_request_id()
+ .expect("address deletion id")
+ .as_str(),
+ deletion.id_str()
+ );
+ assert_eq!(evidence.address_reference_cutoff(), Some(230));
+
+ let older_receipt = store
+ .ingest_event(RadrootsEventIngest::new(older.clone(), 20_003))
+ .await
+ .expect("older late arrival");
+ assert_eq!(
+ older_receipt.raw_head_decision,
+ RadrootsRawHeadDecision::SkippedOlder
+ );
+ assert!(
+ store
+ .food_availability_v1(&author, &identifier)
+ .await
+ .expect("no resurrection lookup")
+ .is_none()
+ );
+ assert_eq!(
+ store
+ .current_event_visibility_v1(older.id_str())
+ .await
+ .expect("older visibility")
+ .expect("stored older revision")
+ .decision(),
+ crate::RadrootsCurrentVisibilityDecisionV1::NotCurrent
+ );
+
+ store
+ .ingest_event(RadrootsEventIngest::new(recovered.clone(), 20_004))
+ .await
+ .expect("post-cutoff replacement");
+ let projection = store
+ .food_availability_v1(&author, &identifier)
+ .await
+ .expect("recovered lookup")
+ .expect("recovered projection");
+ assert_eq!(projection.event_id().as_str(), recovered.id_str());
+ assert_eq!(projection.status(), RadrootsFoodAvailabilityStatus::Active);
+ let recovered_projection_count: i64 = sqlx::query_scalar(
+ "SELECT projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("sealed recovered projection count");
+ assert_eq!(recovered_projection_count, 1);
+
+ let scope = crate::RadrootsAddressableTransitionScopeV1::food_availability();
+ let page = store
+ .addressable_transition_page_v1(&scope, None, 64)
+ .await
+ .expect("transition page");
+ assert_eq!(page.transitions().len(), 4);
+ assert_eq!(page.source_high_water(), 4);
+ assert_eq!(page.next_cursor().last_transition_seq(), 4);
+ assert!(!page.has_more());
+ let deletion_cause = page.transitions()[2]
+ .cause_event()
+ .expect("deletion cause metadata");
+ assert_eq!(
+ deletion_cause.event().event_id().as_str(),
+ deletion.id_str()
+ );
+ assert_eq!(
+ deletion_cause.pubkey().as_str(),
+ FIXTURE_ALICE_PUBLIC_KEY_HEX
+ );
+ assert_eq!(deletion_cause.created_at(), 230);
+ assert_eq!(deletion_cause.kind(), KIND_DELETION_REQUEST);
+ assert_eq!(
+ deletion_cause.admission_status(),
+ RadrootsEventAdmissionStatus::Admitted
+ );
+ assert!(deletion_cause.admission_code().is_none());
+ assert!(deletion_cause.contract_id().is_some());
+ assert_eq!(
+ page.transitions()[2]
+ .retracted_event()
+ .expect("sold retraction")
+ .event_id()
+ .as_str(),
+ sold.id_str()
+ );
+ assert!(page.transitions()[2].visible_event().is_none());
+ assert_eq!(
+ page.transitions()[2]
+ .suppression()
+ .expect("feed suppression")
+ .reason(),
+ crate::RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff
+ );
+ assert_eq!(
+ page.transitions()[3]
+ .visible_event()
+ .expect("recovered canonical event")
+ .event_id()
+ .as_str(),
+ recovered.id_str()
+ );
+ assert!(page.transitions()[3].retracted_event().is_none());
+ assert_eq!(projection.source_transition_seq(), 4);
+ let cursor_state: (i64, i64) = sqlx::query_as(
+ "SELECT last_transition_seq, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("projection cursor");
+ assert_eq!(cursor_state, (page.source_high_water(), 1));
+ }
+
+ #[tokio::test]
+ async fn admitted_operational_listing_retracts_food_until_a_later_food_revision() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let identifier =
+ RadrootsFoodIdentifier::parse("AAAAAAAAAAAAAAAAAAAAAg").expect("identifier");
+ let author = RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author");
+ let food = food_availability_event(
+ 200,
+ identifier.as_str(),
+ "Partition Carrots",
+ "Focused contract",
+ "active",
+ Vec::new(),
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(food, 29_000))
+ .await
+ .expect("focused FoodAvailability ingest");
+
+ let operational = signed_event(
+ KIND_CLASSIFIED_LISTING,
+ 210,
+ admitted_operational_listing_tags(identifier.as_str(), 210),
+ "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ );
+ let receipt = store
+ .ingest_event(RadrootsEventIngest::new(operational.clone(), 29_001))
+ .await
+ .expect("operational listing ingest");
+ assert_eq!(
+ receipt.admission_status,
+ RadrootsEventAdmissionStatus::Admitted
+ );
+ assert_eq!(
+ receipt.contract_id.as_deref(),
+ Some("radroots.operational_listing.published.v1"),
+ );
+ assert!(
+ store
+ .food_availability_v1(&author, &identifier)
+ .await
+ .expect("projection after operational head")
+ .is_none()
+ );
+ let retracted_count: i64 = sqlx::query_scalar(
+ "SELECT projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("sealed operational-head count");
+ assert_eq!(retracted_count, 0);
+
+ let restored = food_availability_event(
+ 220,
+ identifier.as_str(),
+ "Partition Carrots Restored",
+ "Focused contract restored",
+ "active",
+ Vec::new(),
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(restored.clone(), 29_002))
+ .await
+ .expect("restored FoodAvailability ingest");
+ assert_eq!(
+ store
+ .food_availability_v1(&author, &identifier)
+ .await
+ .expect("restored lookup")
+ .expect("restored projection")
+ .event_id()
+ .as_str(),
+ restored.id_str(),
+ );
+ let page = store
+ .addressable_transition_page_v1(
+ &crate::RadrootsAddressableTransitionScopeV1::food_availability(),
+ None,
+ 64,
+ )
+ .await
+ .expect("partition transition page");
+ assert_eq!(page.transitions().len(), 3);
+ assert_eq!(
+ page.transitions()[1].contract_id(),
+ receipt.contract_id.as_deref()
+ );
+ assert_eq!(
+ page.transitions()[1]
+ .visible_event()
+ .expect("operational transition payload")
+ .event_id()
+ .as_str(),
+ operational.id_str(),
+ );
+ assert_eq!(
+ page.transitions()[1]
+ .retracted_event()
+ .expect("focused projection retraction")
+ .event_id()
+ .as_str(),
+ page.transitions()[0]
+ .visible_event()
+ .expect("initial focused payload")
+ .event_id()
+ .as_str(),
+ );
+ }
+
+ #[tokio::test]
+ async fn food_availability_projection_and_feed_rollback_atomically() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let scope = crate::RadrootsAddressableTransitionScopeV1::food_availability();
+ let initial_page = store
+ .addressable_transition_page_v1(&scope, None, 64)
+ .await
+ .expect("initial feed");
+ assert!(initial_page.transitions().is_empty());
+ assert_eq!(initial_page.next_cursor().last_transition_seq(), 0);
+ let initial_cursor = initial_page.next_cursor().clone();
+ let event = food_availability_event(
+ 200,
+ "rollback-carrots",
+ "Rollback Carrots",
+ "Transaction fixture",
+ "active",
+ Vec::new(),
+ );
+ let author = RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author");
+ let identifier = RadrootsFoodIdentifier::parse("rollback-carrots").expect("identifier");
+
+ let mut transaction = store.begin_write_transaction().await.expect("transaction");
+ store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::new(event.clone(), 30_000),
+ )
+ .await
+ .expect("transactional ingest");
+ let in_transaction: (i64, i64, i64) = sqlx::query_as(
+ "SELECT source.last_transition_seq, cursor.last_transition_seq, (SELECT COUNT(*) FROM radroots_event_store_food_availability_projection) FROM radroots_event_store_source_state AS source JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.singleton = 1 WHERE source.singleton = 1",
+ )
+ .fetch_one(&mut *transaction)
+ .await
+ .expect("transactional projection state");
+ assert_eq!(in_transaction, (1, 1, 1));
+ transaction.rollback().await.expect("rollback");
+
+ assert!(
+ store
+ .raw_event(event.id_str())
+ .await
+ .expect("raw event after rollback")
+ .is_none()
+ );
+ assert!(
+ store
+ .food_availability_v1(&author, &identifier)
+ .await
+ .expect("projection after rollback")
+ .is_none()
+ );
+ let after_rollback = store
+ .addressable_transition_page_v1(&scope, Some(&initial_cursor), 64)
+ .await
+ .expect("feed after rollback");
+ assert!(after_rollback.transitions().is_empty());
+ assert_eq!(after_rollback.source_high_water(), 0);
+ assert_eq!(after_rollback.next_cursor().last_transition_seq(), 0);
+ let cursor_after_rollback: i64 = sqlx::query_scalar(
+ "SELECT last_transition_seq FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("cursor after rollback");
+ assert_eq!(cursor_after_rollback, 0);
+
+ store
+ .ingest_event(RadrootsEventIngest::new(event.clone(), 30_001))
+ .await
+ .expect("committed ingest");
+ let committed = store
+ .food_availability_v1(&author, &identifier)
+ .await
+ .expect("committed projection")
+ .expect("projected event");
+ assert_eq!(committed.event_id().as_str(), event.id_str());
+ let committed_page = store
+ .addressable_transition_page_v1(&scope, Some(&initial_cursor), 64)
+ .await
+ .expect("committed feed");
+ assert_eq!(committed_page.transitions().len(), 1);
+ assert_eq!(committed_page.source_high_water(), 1);
+ assert_eq!(committed.source_transition_seq(), 1);
+ }
+
+ #[tokio::test]
+ async fn food_availability_wrong_author_deletion_preserves_projection() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let food = food_availability_event(
+ 200,
+ "protected-carrots",
+ "Protected Carrots",
+ "Still available",
+ "active",
+ Vec::new(),
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(food.clone(), 35_000))
+ .await
+ .expect("food ingest");
+ let wrong_author_deletion = deletion_event(
+ &alternate_keys(),
+ 210,
+ vec![vec!["e".to_owned(), food.id_str().to_owned()]],
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(
+ wrong_author_deletion.clone(),
+ 35_001,
+ ))
+ .await
+ .expect("wrong-author deletion remains a valid event");
+
+ let author = RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author");
+ let identifier = RadrootsFoodIdentifier::parse("protected-carrots").expect("identifier");
+ let projection = store
+ .food_availability_v1(&author, &identifier)
+ .await
+ .expect("projection lookup")
+ .expect("projection remains visible");
+ assert_eq!(projection.event_id().as_str(), food.id_str());
+ assert_eq!(projection.source_transition_seq(), 1);
+ let visibility = store
+ .current_event_visibility_v1(food.id_str())
+ .await
+ .expect("current visibility")
+ .expect("stored event");
+ assert_eq!(
+ visibility.decision(),
+ crate::RadrootsCurrentVisibilityDecisionV1::Visible
+ );
+ assert_eq!(
+ visibility
+ .suppression()
+ .expect("visibility evidence")
+ .reason(),
+ crate::RadrootsNip09SuppressionReason::RequestAuthorMismatch
+ );
+
+ let scope = crate::RadrootsAddressableTransitionScopeV1::food_availability();
+ let page = store
+ .addressable_transition_page_v1(&scope, None, 64)
+ .await
+ .expect("transition page");
+ assert_eq!(page.transitions().len(), 2);
+ let unchanged = &page.transitions()[1];
+ assert_eq!(
+ unchanged
+ .visible_event()
+ .expect("same visible event")
+ .event_id()
+ .as_str(),
+ food.id_str()
+ );
+ assert!(unchanged.retracted_event().is_none());
+ assert_eq!(
+ unchanged
+ .cause_event()
+ .expect("deletion cause")
+ .event()
+ .event_id()
+ .as_str(),
+ wrong_author_deletion.id_str()
+ );
+ }
+
+ #[tokio::test]
+ async fn event_visibility_batches_validate_before_read_and_preserve_duplicate_order() {
+ use std::sync::Arc;
+ use std::sync::atomic::{AtomicUsize, Ordering};
+
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let event = signed_event(KIND_POST, 100, Vec::new(), "Victoria harvest update");
+ store
+ .ingest_event(RadrootsEventIngest::new(event.clone(), 36_000))
+ .await
+ .expect("event ingest");
+ let missing_event_id = "f".repeat(64);
+ let evaluation_count = Arc::new(AtomicUsize::new(0));
+ let probe_count = Arc::clone(&evaluation_count);
+ let visibilities = store
+ .event_visibilities_with_probe(
+ [
+ event.id_str().to_owned(),
+ missing_event_id.clone(),
+ event.id_str().to_owned(),
+ ],
+ move |_| {
+ let probe_count = Arc::clone(&probe_count);
+ async move {
+ probe_count.fetch_add(1, Ordering::SeqCst);
+ Ok(())
+ }
+ },
+ )
+ .await
+ .expect("visibility batch");
+ assert_eq!(
+ visibilities,
+ vec![
+ Some(RadrootsEventVisibility::Visible),
+ None,
+ Some(RadrootsEventVisibility::Visible),
+ ]
+ );
+ assert_eq!(evaluation_count.load(Ordering::SeqCst), 2);
+
+ let max = RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX as usize;
+ let exact_max = store
+ .event_visibilities(vec![event.id_str().to_owned(); max])
+ .await
+ .expect("maximum visibility batch");
+ assert_eq!(exact_max.len(), max);
+ assert!(
+ exact_max
+ .iter()
+ .all(|visibility| *visibility == Some(RadrootsEventVisibility::Visible))
+ );
+
+ let closed = RadrootsEventStore::open_memory()
+ .await
+ .expect("closed fixture");
+ closed.pool().close().await;
+ assert!(
+ closed
+ .event_visibilities(Vec::<String>::new())
+ .await
+ .expect("empty batch does not open a transaction")
+ .is_empty()
+ );
+ assert!(matches!(
+ closed
+ .event_visibilities([event.id_str().to_owned(), "not-an-event-id".to_owned(),])
+ .await,
+ Err(RadrootsEventStoreError::IdParse(_))
+ ));
+ assert!(matches!(
+ closed
+ .event_visibilities(vec![event.id_str().to_owned(); max + 1])
+ .await,
+ Err(RadrootsEventStoreError::EventVisibilityBatchTooLarge {
+ max: actual_max,
+ }) if actual_max == max
+ ));
+ }
+
+ #[tokio::test]
+ async fn event_visibility_batch_holds_one_snapshot_across_concurrent_head_commits() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let path = tempdir.path().join("visibility-batch-snapshot.sqlite");
+ let pool = SqlitePoolOptions::new()
+ .max_connections(2)
+ .connect_with(
+ SqliteConnectOptions::new()
+ .filename(path)
+ .create_if_missing(true),
+ )
+ .await
+ .expect("file pool");
+ let store = RadrootsEventStore::open_pool(pool, true)
+ .await
+ .expect("file store");
+
+ let old_alice = signed_event_with_keys(
+ &fixture_keys(),
+ KIND_PROFILE,
+ 100,
+ Vec::new(),
+ r#"{"name":"Alice"}"#,
+ );
+ let new_alice = signed_event_with_keys(
+ &fixture_keys(),
+ KIND_PROFILE,
+ 200,
+ Vec::new(),
+ r#"{"name":"Alice Farm"}"#,
+ );
+ let old_bob = signed_event_with_keys(
+ &alternate_keys(),
+ KIND_PROFILE,
+ 100,
+ Vec::new(),
+ r#"{"name":"Bob"}"#,
+ );
+ let new_bob = signed_event_with_keys(
+ &alternate_keys(),
+ KIND_PROFILE,
+ 200,
+ Vec::new(),
+ r#"{"name":"Bob Farm"}"#,
+ );
+ for (observed_at_ms, event) in [(36_100, &old_alice), (36_101, &old_bob)] {
+ store
+ .ingest_event(RadrootsEventIngest::new(event.clone(), observed_at_ms))
+ .await
+ .expect("old profile ingest");
+ }
+
+ let concurrent_store = store.clone();
+ let committed_new_alice = new_alice.clone();
+ let committed_new_bob = new_bob.clone();
+ let snapshot = store
+ .event_visibilities_with_probe(
+ [old_alice.id_str(), old_bob.id_str()],
+ move |evaluated| {
+ let concurrent_store = concurrent_store.clone();
+ let new_alice = committed_new_alice.clone();
+ let new_bob = committed_new_bob.clone();
+ async move {
+ if evaluated == 1 {
+ concurrent_store
+ .ingest_event(RadrootsEventIngest::new(new_alice, 36_200))
+ .await?;
+ concurrent_store
+ .ingest_event(RadrootsEventIngest::new(new_bob, 36_201))
+ .await?;
+ }
+ Ok(())
+ }
+ },
+ )
+ .await
+ .expect("coherent visibility snapshot");
+ assert_eq!(
+ snapshot,
+ vec![
+ Some(RadrootsEventVisibility::Visible),
+ Some(RadrootsEventVisibility::Visible),
+ ]
+ );
+ for (old, new) in [(&old_alice, &new_alice), (&old_bob, &new_bob)] {
+ assert_eq!(
+ store
+ .event_visibility(old.id_str())
+ .await
+ .expect("post-commit visibility"),
+ Some(RadrootsEventVisibility::NotCurrent {
+ raw_head_event_id: new.id_str().to_owned(),
+ })
+ );
+ }
+ }
+
+ #[tokio::test]
+ async fn current_visibility_agrees_for_regular_replaceable_and_addressable_reads() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let regular = signed_event(KIND_POST, 100, Vec::new(), "Victoria harvest update");
+ let older_profile = signed_event(KIND_PROFILE, 110, Vec::new(), "{\"name\":\"Alice\"}");
+ let newer_profile =
+ signed_event(KIND_PROFILE, 120, Vec::new(), "{\"name\":\"Alice Farm\"}");
+ let older_food = food_availability_event(
+ 130,
+ "visibility-carrots",
+ "Visibility Carrots",
+ "First harvest",
+ "active",
+ Vec::new(),
+ );
+ let newer_food = food_availability_event(
+ 140,
+ "visibility-carrots",
+ "Visibility Carrots",
+ "Second harvest",
+ "sold",
+ Vec::new(),
+ );
+ for (index, event) in [
+ ®ular,
+ &older_profile,
+ &newer_profile,
+ &older_food,
+ &newer_food,
+ ]
+ .into_iter()
+ .enumerate()
+ {
+ store
+ .ingest_event(RadrootsEventIngest::new(
+ event.clone(),
+ 37_000 + i64::try_from(index).expect("index"),
+ ))
+ .await
+ .expect("visibility fixture ingest");
+ }
+
+ let regular_current = store
+ .current_event_visibility_v1(regular.id_str())
+ .await
+ .expect("regular current visibility")
+ .expect("regular event");
+ assert_eq!(
+ regular_current.decision(),
+ crate::RadrootsCurrentVisibilityDecisionV1::Visible
+ );
+ assert!(regular_current.is_raw_head());
+ assert!(regular_current.raw_head_event_id().is_none());
+ assert_eq!(
+ store
+ .event_visibility(regular.id_str())
+ .await
+ .expect("regular compatibility visibility"),
+ Some(RadrootsEventVisibility::Visible)
+ );
+ assert!(
+ store
+ .visible_event(regular.id_str())
+ .await
+ .expect("regular visible event")
+ .is_some()
+ );
+
+ for (event, expected_head, expected_decision) in [
+ (
+ &older_profile,
+ newer_profile.id_str(),
+ crate::RadrootsCurrentVisibilityDecisionV1::NotCurrent,
+ ),
+ (
+ &newer_profile,
+ newer_profile.id_str(),
+ crate::RadrootsCurrentVisibilityDecisionV1::Visible,
+ ),
+ (
+ &older_food,
+ newer_food.id_str(),
+ crate::RadrootsCurrentVisibilityDecisionV1::NotCurrent,
+ ),
+ (
+ &newer_food,
+ newer_food.id_str(),
+ crate::RadrootsCurrentVisibilityDecisionV1::Visible,
+ ),
+ ] {
+ let current = store
+ .current_event_visibility_v1(event.id_str())
+ .await
+ .expect("coordinate current visibility")
+ .expect("coordinate event");
+ assert_eq!(current.decision(), expected_decision);
+ assert_eq!(
+ current
+ .raw_head_event_id()
+ .expect("coordinate raw head")
+ .as_str(),
+ expected_head
+ );
+ assert_eq!(
+ store
+ .visible_event(event.id_str())
+ .await
+ .expect("coordinate visible event")
+ .is_some(),
+ expected_decision == crate::RadrootsCurrentVisibilityDecisionV1::Visible
+ );
+ }
+ assert!(
+ store
+ .visible_event_head(&profile_coordinate())
+ .await
+ .expect("profile visible head")
+ .is_some_and(|head| head.raw_head().event_id == newer_profile.id_str())
+ );
+ assert!(
+ store
+ .visible_event_head(&head_coordinate_for_event(&newer_food))
+ .await
+ .expect("food visible head")
+ .is_some_and(|head| head.raw_head().event_id == newer_food.id_str())
+ );
+
+ let regular_deletion = deletion_event(
+ &fixture_keys(),
+ 150,
+ vec![vec!["e".to_owned(), regular.id_str().to_owned()]],
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(regular_deletion.clone(), 37_005))
+ .await
+ .expect("regular deletion");
+ assert_eq!(
+ store
+ .current_event_visibility_v1(regular.id_str())
+ .await
+ .expect("suppressed regular visibility")
+ .expect("stored regular event")
+ .decision(),
+ crate::RadrootsCurrentVisibilityDecisionV1::Suppressed
+ );
+ assert!(matches!(
+ store
+ .event_visibility(regular.id_str())
+ .await
+ .expect("regular compatibility suppression"),
+ Some(RadrootsEventVisibility::Suppressed {
+ reason: crate::RadrootsNip09SuppressionReason::EventIdReference,
+ event_reference_request_id: Some(request_id),
+ address_reference_request_id: None,
+ address_reference_cutoff: None,
+ }) if request_id.as_str() == regular_deletion.id_str()
+ ));
+ assert!(
+ store
+ .visible_event(regular.id_str())
+ .await
+ .expect("suppressed regular event")
+ .is_none()
+ );
+
+ let deletion_of_deletion = deletion_event(
+ &fixture_keys(),
+ 160,
+ vec![vec!["e".to_owned(), regular_deletion.id_str().to_owned()]],
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(deletion_of_deletion, 37_006))
+ .await
+ .expect("deletion-of-deletion ingest");
+ let immune = store
+ .current_event_visibility_v1(regular_deletion.id_str())
+ .await
+ .expect("deletion request visibility")
+ .expect("stored deletion request");
+ assert_eq!(
+ immune.decision(),
+ crate::RadrootsCurrentVisibilityDecisionV1::Visible
+ );
+ let immune_evidence = immune.suppression().expect("kind-5 immunity evidence");
+ assert_eq!(
+ immune_evidence.reason(),
+ crate::RadrootsNip09SuppressionReason::DeletionRequestImmune
+ );
+ assert!(immune_evidence.event_reference_request_id().is_none());
+ assert!(immune_evidence.address_reference_request_id().is_none());
+ assert!(immune_evidence.address_reference_cutoff().is_none());
+ assert_eq!(
+ store
+ .event_visibility(regular_deletion.id_str())
+ .await
+ .expect("compatibility deletion-request visibility"),
+ Some(RadrootsEventVisibility::Visible)
+ );
+ }
+
+ #[tokio::test]
+ async fn addressable_transition_feed_advances_across_unrelated_kinds() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let first = food_availability_event(
+ 200,
+ "first-food",
+ "First Food",
+ "First harvest",
+ "active",
+ Vec::new(),
+ );
+ let unrelated_first = signed_event(
+ 30_340,
+ 201,
+ vec![vec!["d".to_owned(), "unrelated".to_owned()]],
+ "unrelated addressable state",
+ );
+ let unrelated_second = signed_event(
+ 30_340,
+ 202,
+ vec![vec!["d".to_owned(), "unrelated".to_owned()]],
+ "new unrelated addressable state",
+ );
+ let second = food_availability_event(
+ 203,
+ "second-food",
+ "Second Food",
+ "Second harvest",
+ "active",
+ Vec::new(),
+ );
+ for (index, event) in [&first, &unrelated_first, &unrelated_second, &second]
+ .into_iter()
+ .enumerate()
+ {
+ store
+ .ingest_event(RadrootsEventIngest::new(
+ event.clone(),
+ 40_000 + i64::try_from(index).expect("index"),
+ ))
+ .await
+ .expect("addressable ingest");
+ }
+
+ let scope = crate::RadrootsAddressableTransitionScopeV1::food_availability();
+ let first_page = store
+ .addressable_transition_page_v1(&scope, None, 1)
+ .await
+ .expect("first scoped page");
+ assert_eq!(first_page.transitions().len(), 1);
+ assert_eq!(
+ first_page.transitions()[0]
+ .visible_event()
+ .expect("first visible event")
+ .event_id()
+ .as_str(),
+ first.id_str()
+ );
+ assert_eq!(first_page.source_high_water(), 4);
+ assert_eq!(first_page.next_cursor().last_transition_seq(), 3);
+ assert!(first_page.has_more());
+
+ let second_page = store
+ .addressable_transition_page_v1(&scope, Some(first_page.next_cursor()), 1)
+ .await
+ .expect("second scoped page");
+ assert_eq!(second_page.transitions().len(), 1);
+ assert_eq!(
+ second_page.transitions()[0]
+ .visible_event()
+ .expect("second visible event")
+ .event_id()
+ .as_str(),
+ second.id_str()
+ );
+ assert_eq!(second_page.next_cursor().last_transition_seq(), 4);
+ assert!(!second_page.has_more());
+
+ let unrelated_scope =
+ crate::RadrootsAddressableTransitionScopeV1::new([30_340]).expect("scope");
+ assert!(matches!(
+ store
+ .addressable_transition_page_v1(
+ &unrelated_scope,
+ Some(first_page.next_cursor()),
+ 1,
+ )
+ .await,
+ Err(RadrootsEventStoreError::AddressableTransitionScopeMismatch)
+ ));
+ let generation_mismatch = crate::RadrootsAddressableTransitionCursorV1::new(
+ crate::RadrootsEventStoreSourceGeneration::from_bytes([0xff; 32]),
+ scope.fingerprint(),
+ 3,
+ )
+ .expect("generation-mismatched cursor");
+ assert!(matches!(
+ store
+ .addressable_transition_page_v1(&scope, Some(&generation_mismatch), 1)
+ .await,
+ Err(RadrootsEventStoreError::AddressableTransitionSourceGenerationMismatch)
+ ));
+ let ahead = crate::RadrootsAddressableTransitionCursorV1::new(
+ first_page.next_cursor().source_generation(),
+ scope.fingerprint(),
+ 5,
+ )
+ .expect("ahead cursor");
+ assert!(matches!(
+ store
+ .addressable_transition_page_v1(&scope, Some(&ahead), 1)
+ .await,
+ Err(RadrootsEventStoreError::AddressableTransitionCursorAhead {
+ cursor: 5,
+ high_water: 4,
+ })
+ ));
+ }
+
+ #[tokio::test]
+ async fn addressable_transition_feed_pages_at_the_exact_transition_limit() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let mut expected_ids = Vec::new();
+ let mut transaction = store.begin_write_transaction().await.expect("transaction");
+ for index in 0..=crate::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1 {
+ let event = signed_event(
+ 30_340,
+ 500 + index,
+ vec![vec!["d".to_owned(), format!("page-limit-{index:02}")]],
+ "page-limit fixture",
+ );
+ expected_ids.push(event.id_str().to_owned());
+ store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::new(event, 45_100 + i64::from(index)),
+ )
+ .await
+ .expect("scoped transition ingest");
+ }
+ transaction.commit().await.expect("commit fixtures");
+
+ let scope = crate::RadrootsAddressableTransitionScopeV1::new([30_340]).expect("scope");
+ let first = store
+ .addressable_transition_page_v1(
+ &scope,
+ None,
+ crate::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1,
+ )
+ .await
+ .expect("full first page");
+ assert_eq!(
+ first.transitions().len(),
+ usize::try_from(crate::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1)
+ .expect("page limit"),
+ );
+ assert!(first.has_more());
+ assert_eq!(
+ first
+ .transitions()
+ .iter()
+ .map(|transition| transition.raw_head().event_id().as_str())
+ .collect::<Vec<_>>(),
+ expected_ids[..usize::try_from(
+ crate::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1
+ )
+ .expect("page limit")],
+ );
+
+ let second = store
+ .addressable_transition_page_v1(
+ &scope,
+ Some(first.next_cursor()),
+ crate::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1,
+ )
+ .await
+ .expect("continued page");
+ assert_eq!(second.transitions().len(), 1);
+ assert_eq!(
+ second.transitions()[0].raw_head().event_id().as_str(),
+ expected_ids.last().expect("last expected event"),
+ );
+ assert!(!second.has_more());
+ }
+
+ #[tokio::test]
+ async fn addressable_transition_feed_preserves_the_maximum_opaque_d_tag() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let d_tag = "d".repeat(crate::RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1);
+ let event = signed_event(
+ 30_340,
+ 204,
+ vec![vec!["d".to_owned(), d_tag.clone()]],
+ "maximum d-tag boundary",
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(event.clone(), 45_000))
+ .await
+ .expect("maximum d-tag ingest");
+
+ let scope = crate::RadrootsAddressableTransitionScopeV1::new([30_340]).expect("scope");
+ let page = store
+ .addressable_transition_page_v1(&scope, None, 1)
+ .await
+ .expect("maximum d-tag feed");
+ assert_eq!(page.transitions().len(), 1);
+ let transition = &page.transitions()[0];
+ assert_eq!(transition.coordinate().kind(), 30_340);
+ assert_eq!(transition.coordinate().d_tag(), d_tag);
+ assert_eq!(transition.raw_head().event_id().as_str(), event.id_str());
+ assert_eq!(transition.raw_head_created_at(), 204);
+ assert!(!page.has_more());
+ }
+
+ #[tokio::test]
+ async fn addressable_transition_feed_scan_boundary_does_not_skip_scoped_events() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let mut transaction = store.begin_write_transaction().await.expect("transaction");
+ for index in 0..crate::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1 {
+ let unrelated = signed_event(
+ 30_340,
+ 1_000 + index,
+ vec![vec!["d".to_owned(), "scan-boundary".to_owned()]],
+ "unrelated addressable transition",
+ );
+ store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::new(unrelated, 46_000 + i64::from(index)),
+ )
+ .await
+ .expect("unrelated transition ingest");
+ }
+ let food = food_availability_event(
+ 3_000,
+ "scan-boundary-food",
+ "Scan Boundary Carrots",
+ "Scoped transition after unrelated traffic",
+ "active",
+ Vec::new(),
+ );
+ store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::new(food.clone(), 47_025),
+ )
+ .await
+ .expect("scoped transition ingest");
+ transaction.commit().await.expect("commit fixtures");
+
+ let scope = crate::RadrootsAddressableTransitionScopeV1::food_availability();
+ let first = store
+ .addressable_transition_page_v1(&scope, None, 64)
+ .await
+ .expect("first scan page");
+ assert!(first.transitions().is_empty());
+ assert_eq!(
+ first.next_cursor().last_transition_seq(),
+ i64::from(crate::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1)
+ );
+ assert_eq!(
+ first.source_high_water(),
+ i64::from(crate::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1) + 1
+ );
+ assert!(first.has_more());
+
+ let second = store
+ .addressable_transition_page_v1(&scope, Some(first.next_cursor()), 64)
+ .await
+ .expect("second scan page");
+ assert_eq!(second.transitions().len(), 1);
+ assert_eq!(
+ second.transitions()[0]
+ .visible_event()
+ .expect("FoodAvailability canonical event")
+ .event_id()
+ .as_str(),
+ food.id_str()
+ );
+ assert_eq!(second.next_cursor().last_transition_seq(), 1_025);
+ assert!(!second.has_more());
+ }
+
+ #[tokio::test]
+ async fn addressable_transition_feed_payload_cap_continues_without_loss() {
+ const EVENT_COUNT: u32 = 33;
+
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let content = "x".repeat(radroots_event::wire::v1::DEFAULT_CONTENT_MAX_BYTES);
+ let mut expected_ids = Vec::new();
+ let mut transaction = store.begin_write_transaction().await.expect("transaction");
+ for index in 0..EVENT_COUNT {
+ let event = calendar_date_event(
+ 4_000 + index,
+ format!("payload-cap-{index:02}").as_str(),
+ content.clone(),
+ );
+ expected_ids.push(event.id_str().to_owned());
+ store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::new(event, 48_000 + i64::from(index)),
+ )
+ .await
+ .expect("calendar transition ingest");
+ }
+ transaction.commit().await.expect("commit fixtures");
+
+ let scope = crate::RadrootsAddressableTransitionScopeV1::new([KIND_CALENDAR_DATE_EVENT])
+ .expect("calendar scope");
+ let mut cursor = None;
+ let mut actual_ids = Vec::new();
+ let mut page_count = 0_u32;
+ loop {
+ let page = store
+ .addressable_transition_page_v1(&scope, cursor.as_ref(), 64)
+ .await
+ .expect("payload-bounded page");
+ page_count += 1;
+ assert!(!page.transitions().is_empty());
+ for transition in page.transitions() {
+ actual_ids.push(
+ transition
+ .visible_event()
+ .expect("admitted calendar canonical event")
+ .event_id()
+ .as_str()
+ .to_owned(),
+ );
+ }
+ cursor = Some(page.next_cursor().clone());
+ if !page.has_more() {
+ break;
+ }
+ }
+ assert!(page_count > 1, "payload cap must force continuation");
+ assert_eq!(actual_ids, expected_ids);
+ assert_eq!(
+ cursor.expect("terminal cursor").last_transition_seq(),
+ i64::from(EVENT_COUNT)
+ );
+ }
+
+ #[tokio::test]
+ async fn addressable_transition_feed_distinguishes_gaps_from_corruption() {
+ let gap_store = RadrootsEventStore::open_memory().await.expect("gap store");
+ let first = food_availability_event(
+ 200,
+ "gap-first",
+ "Gap First",
+ "First row",
+ "active",
+ Vec::new(),
+ );
+ let unrelated = signed_event(
+ 30_340,
+ 201,
+ vec![vec!["d".to_owned(), "gap-middle".to_owned()]],
+ "middle row",
+ );
+ let second = food_availability_event(
+ 202,
+ "gap-second",
+ "Gap Second",
+ "Last row",
+ "active",
+ Vec::new(),
+ );
+ for event in [&first, &unrelated, &second] {
+ gap_store
+ .ingest_event(RadrootsEventIngest::new(event.clone(), 50_000))
+ .await
+ .expect("gap fixture ingest");
+ }
+ sqlx::query("DROP TRIGGER radroots_event_store_addressable_transition_delete_guard")
+ .execute(gap_store.pool())
+ .await
+ .expect("drop test-only delete guard");
+ sqlx::query(
+ "DELETE FROM radroots_event_store_addressable_head_transition WHERE transition_seq = 2",
+ )
+ .execute(gap_store.pool())
+ .await
+ .expect("remove middle transition");
+ let scope = crate::RadrootsAddressableTransitionScopeV1::food_availability();
+ assert!(matches!(
+ gap_store
+ .addressable_transition_page_v1(&scope, None, 64)
+ .await,
+ Err(RadrootsEventStoreError::AddressableTransitionSequenceGap { .. })
+ ));
+
+ let corrupt_store = RadrootsEventStore::open_memory()
+ .await
+ .expect("corrupt store");
+ corrupt_store
+ .ingest_event(RadrootsEventIngest::new(first, 50_001))
+ .await
+ .expect("corruption fixture ingest");
+ sqlx::query("DROP TRIGGER radroots_event_store_addressable_transition_update_guard")
+ .execute(corrupt_store.pool())
+ .await
+ .expect("drop test-only update guard");
+ sqlx::query(
+ "UPDATE radroots_event_store_addressable_head_transition SET raw_head_created_at = raw_head_created_at + 1 WHERE transition_seq = 1",
+ )
+ .execute(corrupt_store.pool())
+ .await
+ .expect("corrupt transition metadata");
+ assert!(matches!(
+ corrupt_store
+ .addressable_transition_page_v1(&scope, None, 64)
+ .await,
+ Err(RadrootsEventStoreError::AddressableTransitionCorruption { .. })
+ ));
+ }
+
+ #[tokio::test]
async fn raw_addressable_heads_use_the_first_opaque_d_value_or_empty() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let missing = signed_event(39_990, 30, Vec::new(), "missing");
diff --git a/crates/event_store/src/store/addressable_transition_feed_v1.rs b/crates/event_store/src/store/addressable_transition_feed_v1.rs
@@ -0,0 +1,958 @@
+use super::current_visibility_v1::{parse_suppression_outcome, parse_suppression_reason};
+use super::protocol_storage_v1::stored_raw_event_from_row;
+use super::{RadrootsEventStore, u32_from_i64, u64_from_i64};
+use crate::RadrootsEventStoreError;
+use crate::model::{
+ RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1,
+ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1, RadrootsAddressableTransitionCauseV1,
+ RadrootsAddressableTransitionCoordinateV1, RadrootsAddressableTransitionCursorV1,
+ RadrootsAddressableTransitionEventReferenceV1, RadrootsAddressableTransitionOriginV1,
+ RadrootsAddressableTransitionPageV1, RadrootsAddressableTransitionRawHeadDecisionV1,
+ RadrootsAddressableTransitionScopeV1, RadrootsAddressableTransitionV1,
+ RadrootsAddressableTransitionVisibilityV1, RadrootsEventAdmissionStatus, RadrootsEventIngest,
+ RadrootsEventStoreSourceGeneration, RadrootsNip09SuppressionEvidenceV1,
+ RadrootsNip09SuppressionOutcome, RadrootsStoreProducedCanonicalEventV1, RadrootsStoredRawEvent,
+ StoredEventClass,
+};
+use crate::nip09::reconciliation_v1::{
+ EventAdmission, ReconciliationProfile, generation_from_blob,
+};
+use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey};
+use sqlx::{QueryBuilder, Row, Sqlite, SqliteConnection};
+
+impl RadrootsEventStore {
+ pub async fn addressable_transition_page_v1(
+ &self,
+ scope: &RadrootsAddressableTransitionScopeV1,
+ cursor: Option<&RadrootsAddressableTransitionCursorV1>,
+ limit: u32,
+ ) -> Result<RadrootsAddressableTransitionPageV1, RadrootsEventStoreError> {
+ validate_limit(limit)?;
+ let mut tx = self.pool.begin().await?;
+ let page =
+ addressable_transition_page_in_transaction_v1(&mut tx, scope, cursor, limit).await?;
+ tx.commit().await?;
+ Ok(page)
+ }
+}
+
+pub(super) async fn addressable_transition_page_in_transaction_v1(
+ connection: &mut SqliteConnection,
+ scope: &RadrootsAddressableTransitionScopeV1,
+ cursor: Option<&RadrootsAddressableTransitionCursorV1>,
+ limit: u32,
+) -> Result<RadrootsAddressableTransitionPageV1, RadrootsEventStoreError> {
+ validate_limit(limit)?;
+ let source = read_and_validate_source_authority(connection).await?;
+ let start = validate_or_create_cursor(connection, scope, cursor, &source).await?;
+ let fetch_limit = i64::from(RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1) + 1;
+ let mut query = QueryBuilder::<Sqlite>::new(
+ "SELECT transition_seq, source_generation, origin, kind, pubkey, d_tag, raw_head_event_id, raw_head_event_seq, raw_head_created_at, visible_event_id, visible_event_seq, retracted_event_id, retracted_event_seq, admission_status, admission_code, contract_id, visibility, nip09_outcome, nip09_reason, event_reference_request_id, address_reference_request_id, address_reference_cutoff, cause_event_seq, cause_event_id, raw_head_decision FROM radroots_event_store_addressable_head_transition WHERE source_generation = ",
+ );
+ query.push_bind(source.generation.as_bytes().as_slice());
+ query.push(" AND transition_seq > ");
+ query.push_bind(start);
+ query.push(" AND transition_seq <= ");
+ query.push_bind(source.high_water);
+ query.push(" ORDER BY transition_seq LIMIT ");
+ query.push_bind(fetch_limit);
+
+ let mut rows = query.build().fetch_all(&mut *connection).await?;
+ let scan_limited = rows.len()
+ > usize::try_from(RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1)
+ .expect("u32 fits usize");
+ if scan_limited {
+ rows.pop();
+ }
+ let mut transitions = Vec::with_capacity(rows.len());
+ let mut canonical_payload_bytes = 0usize;
+ let mut last_scanned_sequence = start;
+ let mut stopped_before_row = false;
+ for row in rows {
+ let transition_seq: i64 = row.try_get("transition_seq").map_err(|error| {
+ corruption(format!("transition sequence cannot be decoded: {error}"))
+ })?;
+ let expected_sequence = last_scanned_sequence
+ .checked_add(1)
+ .ok_or_else(|| corruption("transition sequence overflow"))?;
+ if transition_seq != expected_sequence {
+ return Err(RadrootsEventStoreError::AddressableTransitionSequenceGap {
+ reason: format!(
+ "expected transition sequence {expected_sequence}, found {transition_seq}"
+ ),
+ });
+ }
+ let kind = u32_from_i64(
+ "transition.kind",
+ row.try_get("kind").map_err(|error| {
+ corruption(format!("transition kind cannot be decoded: {error}"))
+ })?,
+ )
+ .map_err(|error| corruption(error.to_string()))?;
+ if !scope.kinds().contains(&kind) {
+ last_scanned_sequence = transition_seq;
+ continue;
+ }
+ if transitions.len() == usize::try_from(limit).expect("u32 fits usize") {
+ stopped_before_row = true;
+ break;
+ }
+ let transition = transition_from_row(connection, row, source.generation).await?;
+ let transition_payload_bytes = transition
+ .visible_event()
+ .map_or(0, |event| event.raw_json().len());
+ let next_payload_bytes = canonical_payload_bytes
+ .checked_add(transition_payload_bytes)
+ .ok_or(
+ RadrootsEventStoreError::AddressableTransitionPagePayloadTooLarge {
+ max: RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1,
+ actual: usize::MAX,
+ },
+ )?;
+ if next_payload_bytes > RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1 {
+ if transitions.is_empty() {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionPagePayloadTooLarge {
+ max: RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1,
+ actual: next_payload_bytes,
+ },
+ );
+ }
+ stopped_before_row = true;
+ break;
+ }
+ canonical_payload_bytes = next_payload_bytes;
+ last_scanned_sequence = transition_seq;
+ transitions.push(transition);
+ }
+ if !scan_limited && !stopped_before_row && last_scanned_sequence < source.high_water {
+ return Err(RadrootsEventStoreError::AddressableTransitionSequenceGap {
+ reason: format!(
+ "sealed interval ends at {}, but the last stored transition is {last_scanned_sequence}",
+ source.high_water
+ ),
+ });
+ }
+ let has_more = last_scanned_sequence < source.high_water;
+ Ok(RadrootsAddressableTransitionPageV1 {
+ source_high_water: source.high_water,
+ transitions,
+ next_cursor: RadrootsAddressableTransitionCursorV1::new(
+ source.generation,
+ scope.fingerprint(),
+ last_scanned_sequence,
+ )?,
+ has_more,
+ })
+}
+
+fn validate_limit(limit: u32) -> Result<(), RadrootsEventStoreError> {
+ if !(1..=RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1).contains(&limit) {
+ return Err(RadrootsEventStoreError::QueryLimitOutOfRange {
+ min: 1,
+ max: RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1,
+ actual: limit,
+ });
+ }
+ Ok(())
+}
+
+struct FeedSourceAuthority {
+ generation: RadrootsEventStoreSourceGeneration,
+ feed_version: u32,
+ floor: i64,
+ high_water: i64,
+}
+
+async fn read_and_validate_source_authority(
+ connection: &mut SqliteConnection,
+) -> Result<FeedSourceAuthority, RadrootsEventStoreError> {
+ let row = sqlx::query(
+ "SELECT source.active_generation, source.last_transition_seq, generation.addressable_feed_version, generation.transition_floor_seq, integrity.transition_floor_seq AS sealed_floor_seq, integrity.last_transition_seq AS sealed_last_transition_seq, integrity.transition_count AS sealed_transition_count FROM radroots_event_store_source_state AS source JOIN radroots_event_store_source_generation AS generation ON generation.source_generation = source.active_generation JOIN radroots_event_store_addressable_feed_integrity_v1 AS integrity ON integrity.source_generation = source.active_generation WHERE source.singleton = 1",
+ )
+ .fetch_optional(&mut *connection)
+ .await?
+ .ok_or_else(|| corruption("active source authority is missing"))?;
+ let authority = FeedSourceAuthority {
+ generation: generation_from_blob(row.try_get("active_generation")?)
+ .map_err(|error| corruption(format!("active generation is invalid: {error}")))?,
+ feed_version: u32_from_i64(
+ "addressable_feed_version",
+ row.try_get("addressable_feed_version")?,
+ )
+ .map_err(|error| corruption(error.to_string()))?,
+ floor: row.try_get("transition_floor_seq")?,
+ high_water: row.try_get("last_transition_seq")?,
+ };
+ if authority.feed_version != RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1 {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionFeedVersionMismatch {
+ expected: RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ actual: authority.feed_version,
+ },
+ );
+ }
+ if authority.floor < 0 || authority.high_water < authority.floor {
+ return Err(corruption(format!(
+ "active transition interval has floor={} and high-water={}",
+ authority.floor, authority.high_water
+ )));
+ }
+ let expected_count = authority.high_water - authority.floor;
+ let sealed_floor: i64 = row.try_get("sealed_floor_seq")?;
+ let sealed_high_water: i64 = row.try_get("sealed_last_transition_seq")?;
+ let sealed_count: i64 = row.try_get("sealed_transition_count")?;
+ if sealed_floor != authority.floor
+ || sealed_high_water != authority.high_water
+ || sealed_count != expected_count
+ {
+ return Err(RadrootsEventStoreError::AddressableTransitionSequenceGap {
+ reason: format!(
+ "active interval floor={} high-water={} disagrees with seal floor={sealed_floor}, high-water={sealed_high_water}, count={sealed_count}",
+ authority.floor, authority.high_water,
+ ),
+ });
+ }
+ if authority.high_water > authority.floor {
+ let first_sequence = authority
+ .floor
+ .checked_add(1)
+ .ok_or_else(|| corruption("active transition interval floor overflow"))?;
+ let boundary_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_addressable_head_transition WHERE source_generation = ? AND transition_seq IN (?, ?)",
+ )
+ .bind(authority.generation.as_bytes().as_slice())
+ .bind(first_sequence)
+ .bind(authority.high_water)
+ .fetch_one(&mut *connection)
+ .await?;
+ let expected_boundary_count = if first_sequence == authority.high_water {
+ 1
+ } else {
+ 2
+ };
+ if boundary_count != expected_boundary_count {
+ return Err(RadrootsEventStoreError::AddressableTransitionSequenceGap {
+ reason: format!(
+ "sealed interval {}..={} is missing a boundary transition",
+ first_sequence, authority.high_water
+ ),
+ });
+ }
+ }
+ Ok(authority)
+}
+
+async fn validate_or_create_cursor(
+ connection: &mut SqliteConnection,
+ scope: &RadrootsAddressableTransitionScopeV1,
+ cursor: Option<&RadrootsAddressableTransitionCursorV1>,
+ source: &FeedSourceAuthority,
+) -> Result<i64, RadrootsEventStoreError> {
+ let Some(cursor) = cursor else {
+ return Ok(source.floor);
+ };
+ if cursor.feed_version() != RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1 {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionFeedVersionMismatch {
+ expected: RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ actual: cursor.feed_version(),
+ },
+ );
+ }
+ if cursor.scope_fingerprint() != scope.fingerprint() {
+ return Err(RadrootsEventStoreError::AddressableTransitionScopeMismatch);
+ }
+ if cursor.source_generation() != source.generation {
+ return Err(RadrootsEventStoreError::AddressableTransitionSourceGenerationMismatch);
+ }
+ if cursor.last_transition_seq() < source.floor {
+ return Err(
+ RadrootsEventStoreError::AddressableTransitionCursorExpired {
+ cursor: cursor.last_transition_seq(),
+ floor: source.floor,
+ },
+ );
+ }
+ if cursor.last_transition_seq() > source.high_water {
+ return Err(RadrootsEventStoreError::AddressableTransitionCursorAhead {
+ cursor: cursor.last_transition_seq(),
+ high_water: source.high_water,
+ });
+ }
+ if cursor.last_transition_seq() != source.floor
+ && cursor.last_transition_seq() != source.high_water
+ {
+ let exists: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_addressable_head_transition WHERE source_generation = ? AND transition_seq = ?",
+ )
+ .bind(source.generation.as_bytes().as_slice())
+ .bind(cursor.last_transition_seq())
+ .fetch_one(&mut *connection)
+ .await?;
+ if exists != 1 {
+ return Err(RadrootsEventStoreError::AddressableTransitionSequenceGap {
+ reason: format!(
+ "cursor sequence {} is absent from the sealed active interval",
+ cursor.last_transition_seq()
+ ),
+ });
+ }
+ }
+ Ok(cursor.last_transition_seq())
+}
+
+async fn transition_from_row(
+ connection: &mut SqliteConnection,
+ row: sqlx::sqlite::SqliteRow,
+ expected_generation: RadrootsEventStoreSourceGeneration,
+) -> Result<RadrootsAddressableTransitionV1, RadrootsEventStoreError> {
+ let transition_seq: i64 = row.try_get("transition_seq")?;
+ if transition_seq <= 0 {
+ return Err(corruption(format!(
+ "transition sequence {transition_seq} is not positive"
+ )));
+ }
+ let source_generation = generation_from_blob(row.try_get("source_generation")?)
+ .map_err(|error| corruption(format!("transition generation is invalid: {error}")))?;
+ if source_generation != expected_generation {
+ return Err(corruption(
+ "scoped query returned a transition from another generation",
+ ));
+ }
+ let origin =
+ RadrootsAddressableTransitionOriginV1::parse(row.try_get::<String, _>("origin")?.as_str())
+ .map_err(|error| corruption(error.to_string()))?;
+ let kind = u32_from_i64("transition.kind", row.try_get("kind")?)
+ .map_err(|error| corruption(error.to_string()))?;
+ let pubkey: String = row.try_get("pubkey")?;
+ let d_tag: String = row.try_get("d_tag")?;
+ if !(30_000..=39_999).contains(&kind)
+ || d_tag.len() > RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1
+ {
+ return Err(corruption("transition coordinate is outside wire bounds"));
+ }
+ let coordinate = RadrootsAddressableTransitionCoordinateV1 {
+ kind,
+ pubkey: RadrootsPublicKey::parse(pubkey.as_str())
+ .map_err(|error| corruption(format!("transition pubkey is invalid: {error}")))?,
+ d_tag,
+ };
+ let raw_head = required_reference(
+ "raw_head",
+ row.try_get("raw_head_event_id")?,
+ row.try_get("raw_head_event_seq")?,
+ )?;
+ let raw_head_created_at = u64_from_i64(
+ "transition.raw_head_created_at",
+ row.try_get("raw_head_created_at")?,
+ )
+ .map_err(|error| corruption(error.to_string()))?;
+ let visible_reference = optional_reference(
+ "visible_event",
+ row.try_get("visible_event_id")?,
+ row.try_get("visible_event_seq")?,
+ )?;
+ let retracted_event = optional_reference(
+ "retracted_event",
+ row.try_get("retracted_event_id")?,
+ row.try_get("retracted_event_seq")?,
+ )?;
+ let admission_status =
+ RadrootsEventAdmissionStatus::parse(row.try_get::<String, _>("admission_status")?.as_str())
+ .map_err(|error| corruption(error.to_string()))?;
+ let admission_code: Option<String> = row.try_get("admission_code")?;
+ let contract_id: Option<String> = row.try_get("contract_id")?;
+ let visibility = RadrootsAddressableTransitionVisibilityV1::parse(
+ row.try_get::<String, _>("visibility")?.as_str(),
+ )
+ .map_err(|error| corruption(error.to_string()))?;
+ let suppression = suppression_evidence_from_transition_row(&row)?;
+ let cause_reference = optional_reference(
+ "cause_event",
+ row.try_get("cause_event_id")?,
+ row.try_get("cause_event_seq")?,
+ )?;
+ let raw_head_decision = RadrootsAddressableTransitionRawHeadDecisionV1::parse(
+ row.try_get::<String, _>("raw_head_decision")?.as_str(),
+ )
+ .map_err(|error| corruption(error.to_string()))?;
+
+ validate_transition_shape(
+ origin,
+ &raw_head,
+ visible_reference.as_ref(),
+ retracted_event.as_ref(),
+ admission_status,
+ admission_code.as_deref(),
+ contract_id.as_deref(),
+ visibility,
+ suppression.as_ref(),
+ cause_reference.as_ref(),
+ raw_head_decision,
+ raw_head_created_at,
+ )?;
+
+ let (raw_event, admission) = load_and_validate_stored_event(connection, &raw_head).await?;
+ validate_addressable_reference(
+ connection,
+ source_generation,
+ &coordinate,
+ &raw_head,
+ &raw_event,
+ )
+ .await?;
+ if raw_event.created_at != raw_head_created_at
+ || admission.status != admission_status
+ || admission.code.as_deref() != admission_code.as_deref()
+ || admission.contract.map(|contract| contract.id) != contract_id.as_deref()
+ {
+ return Err(corruption(format!(
+ "transition {transition_seq} disagrees with its raw-head event"
+ )));
+ }
+
+ let visible_event = if let Some(reference) = visible_reference.as_ref() {
+ if reference != &raw_head {
+ return Err(corruption(format!(
+ "transition {transition_seq} visible event is not the raw head"
+ )));
+ }
+ Some(RadrootsStoreProducedCanonicalEventV1 {
+ event_id: raw_head.event_id().clone(),
+ pubkey: coordinate.pubkey().clone(),
+ created_at: raw_event.created_at,
+ kind: coordinate.kind(),
+ raw_json: raw_event.raw_json.clone(),
+ })
+ } else {
+ None
+ };
+
+ if let Some(reference) = retracted_event.as_ref() {
+ let (event, admission) = load_and_validate_stored_event(connection, reference).await?;
+ validate_addressable_reference(
+ connection,
+ source_generation,
+ &coordinate,
+ reference,
+ &event,
+ )
+ .await?;
+ if admission.status != RadrootsEventAdmissionStatus::Admitted {
+ return Err(corruption(format!(
+ "transition {transition_seq} retracts an event that is not admitted"
+ )));
+ }
+ }
+ let cause = if let Some(reference) = cause_reference.as_ref() {
+ if reference == &raw_head {
+ Some((raw_event.clone(), admission.clone()))
+ } else {
+ Some(load_and_validate_stored_event(connection, reference).await?)
+ }
+ } else {
+ None
+ };
+ validate_incremental_cause(
+ connection,
+ source_generation,
+ origin,
+ &coordinate,
+ &raw_head,
+ cause_reference.as_ref(),
+ cause.as_ref(),
+ suppression.as_ref(),
+ raw_head_decision,
+ )
+ .await?;
+ let current_state = TransitionStateSnapshot {
+ raw_head: raw_head.clone(),
+ raw_head_created_at,
+ admission_status,
+ admission_code: admission_code.clone(),
+ contract_id: contract_id.clone(),
+ visibility,
+ suppression: suppression.clone(),
+ };
+ validate_retraction_lineage(
+ connection,
+ source_generation,
+ transition_seq,
+ origin,
+ &coordinate,
+ ¤t_state,
+ retracted_event.as_ref(),
+ )
+ .await?;
+ let cause_event = cause
+ .map(|(event, admission)| {
+ let event_reference = cause_reference
+ .clone()
+ .ok_or_else(|| corruption("loaded transition cause has no reference"))?;
+ let pubkey = RadrootsPublicKey::parse(event.pubkey.as_str()).map_err(|error| {
+ corruption(format!("transition cause pubkey is invalid: {error}"))
+ })?;
+ Ok::<RadrootsAddressableTransitionCauseV1, RadrootsEventStoreError>(
+ RadrootsAddressableTransitionCauseV1 {
+ event: event_reference,
+ pubkey,
+ created_at: event.created_at,
+ kind: event.kind,
+ admission_status: admission.status,
+ admission_code: admission.code,
+ contract_id: admission.contract.map(|contract| contract.id.to_owned()),
+ },
+ )
+ })
+ .transpose()?;
+
+ Ok(RadrootsAddressableTransitionV1 {
+ transition_seq,
+ source_generation,
+ origin,
+ coordinate,
+ raw_head,
+ raw_head_created_at,
+ visible_event,
+ retracted_event,
+ admission_status,
+ admission_code,
+ contract_id,
+ visibility,
+ suppression,
+ cause_event,
+ raw_head_decision,
+ })
+}
+
+#[derive(PartialEq, Eq)]
+struct TransitionStateSnapshot {
+ raw_head: RadrootsAddressableTransitionEventReferenceV1,
+ raw_head_created_at: u64,
+ admission_status: RadrootsEventAdmissionStatus,
+ admission_code: Option<String>,
+ contract_id: Option<String>,
+ visibility: RadrootsAddressableTransitionVisibilityV1,
+ suppression: Option<RadrootsNip09SuppressionEvidenceV1>,
+}
+
+#[allow(clippy::too_many_arguments)]
+async fn validate_incremental_cause(
+ connection: &mut SqliteConnection,
+ generation: RadrootsEventStoreSourceGeneration,
+ origin: RadrootsAddressableTransitionOriginV1,
+ coordinate: &RadrootsAddressableTransitionCoordinateV1,
+ raw_head: &RadrootsAddressableTransitionEventReferenceV1,
+ cause_reference: Option<&RadrootsAddressableTransitionEventReferenceV1>,
+ cause: Option<&(RadrootsStoredRawEvent, EventAdmission)>,
+ suppression: Option<&RadrootsNip09SuppressionEvidenceV1>,
+ decision: RadrootsAddressableTransitionRawHeadDecisionV1,
+) -> Result<(), RadrootsEventStoreError> {
+ if origin == RadrootsAddressableTransitionOriginV1::Baseline {
+ return Ok(());
+ }
+ let cause_reference = cause_reference
+ .ok_or_else(|| corruption("incremental transition has no cause reference"))?;
+ let (cause_event, cause_admission) =
+ cause.ok_or_else(|| corruption("incremental transition cause could not be loaded"))?;
+ match decision {
+ RadrootsAddressableTransitionRawHeadDecisionV1::Applied => {
+ if cause_reference != raw_head {
+ return Err(corruption(
+ "applied incremental transition cause is not its new raw head",
+ ));
+ }
+ }
+ RadrootsAddressableTransitionRawHeadDecisionV1::NotHeadSelected => {
+ if cause_event.kind != 5
+ || cause_admission.status != RadrootsEventAdmissionStatus::Admitted
+ {
+ return Err(corruption(
+ "non-head incremental transition was not caused by an admitted deletion request",
+ ));
+ }
+ let author_matches = cause_event.pubkey == coordinate.pubkey().as_str();
+ let records_author_mismatch = suppression.is_some_and(|evidence| {
+ evidence.reason()
+ == crate::model::RadrootsNip09SuppressionReason::RequestAuthorMismatch
+ });
+ if author_matches == records_author_mismatch {
+ return Err(corruption(
+ "deletion cause author does not agree with suppression evidence",
+ ));
+ }
+ let targeted: i64 = sqlx::query_scalar(
+ "SELECT EXISTS(SELECT 1 FROM radroots_event_store_nip09_event_target WHERE source_generation = ? AND request_event_id = ? AND target_event_id = ?) OR EXISTS(SELECT 1 FROM radroots_event_store_nip09_address_target WHERE source_generation = ? AND request_event_id = ? AND target_kind = ? AND target_pubkey = ? AND target_d_tag = ?)",
+ )
+ .bind(generation.as_bytes().as_slice())
+ .bind(cause_reference.event_id().as_str())
+ .bind(raw_head.event_id().as_str())
+ .bind(generation.as_bytes().as_slice())
+ .bind(cause_reference.event_id().as_str())
+ .bind(i64::from(coordinate.kind()))
+ .bind(coordinate.pubkey().as_str())
+ .bind(coordinate.d_tag())
+ .fetch_one(&mut *connection)
+ .await?;
+ if targeted != 1 {
+ return Err(corruption(
+ "deletion cause does not target the transitioned coordinate",
+ ));
+ }
+ }
+ RadrootsAddressableTransitionRawHeadDecisionV1::BaselineRebuild
+ | RadrootsAddressableTransitionRawHeadDecisionV1::SkippedOlder
+ | RadrootsAddressableTransitionRawHeadDecisionV1::SkippedSameTimestampHigherEventId
+ | RadrootsAddressableTransitionRawHeadDecisionV1::MalformedCoordinate => {
+ return Err(corruption(format!(
+ "raw-head decision `{}` cannot emit an incremental transition",
+ decision.as_str()
+ )));
+ }
+ }
+ Ok(())
+}
+
+#[allow(clippy::too_many_arguments)]
+async fn validate_retraction_lineage(
+ connection: &mut SqliteConnection,
+ generation: RadrootsEventStoreSourceGeneration,
+ transition_seq: i64,
+ origin: RadrootsAddressableTransitionOriginV1,
+ coordinate: &RadrootsAddressableTransitionCoordinateV1,
+ current: &TransitionStateSnapshot,
+ retracted: Option<&RadrootsAddressableTransitionEventReferenceV1>,
+) -> Result<(), RadrootsEventStoreError> {
+ let prior = sqlx::query(
+ "SELECT raw_head_event_id, raw_head_event_seq, raw_head_created_at, admission_status, admission_code, contract_id, visibility, nip09_outcome, nip09_reason, event_reference_request_id, address_reference_request_id, address_reference_cutoff FROM radroots_event_store_addressable_head_transition WHERE source_generation = ? AND kind = ? AND pubkey = ? AND d_tag = ? AND transition_seq < ? ORDER BY transition_seq DESC LIMIT 1",
+ )
+ .bind(generation.as_bytes().as_slice())
+ .bind(i64::from(coordinate.kind()))
+ .bind(coordinate.pubkey().as_str())
+ .bind(coordinate.d_tag())
+ .bind(transition_seq)
+ .fetch_optional(&mut *connection)
+ .await?;
+ let expected = if let Some(prior) = prior {
+ if origin == RadrootsAddressableTransitionOriginV1::Baseline {
+ return Err(corruption(
+ "baseline transition follows existing coordinate state",
+ ));
+ }
+ let prior_state = TransitionStateSnapshot {
+ raw_head: required_reference(
+ "prior_raw_head",
+ prior.try_get("raw_head_event_id")?,
+ prior.try_get("raw_head_event_seq")?,
+ )?,
+ raw_head_created_at: u64_from_i64(
+ "prior_transition.raw_head_created_at",
+ prior.try_get("raw_head_created_at")?,
+ )
+ .map_err(|error| corruption(error.to_string()))?,
+ admission_status: RadrootsEventAdmissionStatus::parse(
+ prior.try_get::<String, _>("admission_status")?.as_str(),
+ )
+ .map_err(|error| corruption(error.to_string()))?,
+ admission_code: prior.try_get("admission_code")?,
+ contract_id: prior.try_get("contract_id")?,
+ visibility: RadrootsAddressableTransitionVisibilityV1::parse(
+ prior.try_get::<String, _>("visibility")?.as_str(),
+ )
+ .map_err(|error| corruption(error.to_string()))?,
+ suppression: suppression_evidence_from_transition_row(&prior)?,
+ };
+ if prior_state == *current {
+ return Err(corruption(
+ "incremental transition repeats the complete prior state",
+ ));
+ }
+ (prior_state.visibility == RadrootsAddressableTransitionVisibilityV1::Visible
+ && (current.visibility != RadrootsAddressableTransitionVisibilityV1::Visible
+ || prior_state.raw_head != current.raw_head))
+ .then_some(prior_state.raw_head)
+ } else {
+ if origin == RadrootsAddressableTransitionOriginV1::Baseline && retracted.is_some() {
+ return Err(corruption("baseline transition retracts prior state"));
+ }
+ None
+ };
+ if expected.as_ref() != retracted {
+ return Err(corruption(
+ "transition retraction does not match the immediately preceding visible state",
+ ));
+ }
+ Ok(())
+}
+
+#[allow(clippy::too_many_arguments)]
+fn validate_transition_shape(
+ origin: RadrootsAddressableTransitionOriginV1,
+ raw_head: &RadrootsAddressableTransitionEventReferenceV1,
+ visible_event: Option<&RadrootsAddressableTransitionEventReferenceV1>,
+ retracted_event: Option<&RadrootsAddressableTransitionEventReferenceV1>,
+ admission_status: RadrootsEventAdmissionStatus,
+ admission_code: Option<&str>,
+ contract_id: Option<&str>,
+ visibility: RadrootsAddressableTransitionVisibilityV1,
+ suppression: Option<&RadrootsNip09SuppressionEvidenceV1>,
+ cause_event: Option<&RadrootsAddressableTransitionEventReferenceV1>,
+ raw_head_decision: RadrootsAddressableTransitionRawHeadDecisionV1,
+ raw_head_created_at: u64,
+) -> Result<(), RadrootsEventStoreError> {
+ let origin_valid = match origin {
+ RadrootsAddressableTransitionOriginV1::Baseline => {
+ cause_event.is_none()
+ && retracted_event.is_none()
+ && raw_head_decision
+ == RadrootsAddressableTransitionRawHeadDecisionV1::BaselineRebuild
+ }
+ RadrootsAddressableTransitionOriginV1::Incremental => {
+ cause_event.is_some()
+ && raw_head_decision
+ != RadrootsAddressableTransitionRawHeadDecisionV1::BaselineRebuild
+ }
+ };
+ let admission_valid = match admission_status {
+ RadrootsEventAdmissionStatus::Admitted => admission_code.is_none() && contract_id.is_some(),
+ RadrootsEventAdmissionStatus::Unsupported | RadrootsEventAdmissionStatus::Invalid => {
+ admission_code.is_some() && contract_id.is_none()
+ }
+ };
+ let visibility_valid = match visibility {
+ RadrootsAddressableTransitionVisibilityV1::Visible => {
+ admission_status == RadrootsEventAdmissionStatus::Admitted
+ && visible_event == Some(raw_head)
+ && suppression.is_some_and(|evidence| {
+ evidence.outcome() == RadrootsNip09SuppressionOutcome::Visible
+ })
+ }
+ RadrootsAddressableTransitionVisibilityV1::NotAdmitted => {
+ admission_status != RadrootsEventAdmissionStatus::Admitted
+ && visible_event.is_none()
+ && suppression.is_none()
+ }
+ RadrootsAddressableTransitionVisibilityV1::Suppressed => {
+ admission_status == RadrootsEventAdmissionStatus::Admitted
+ && visible_event.is_none()
+ && suppression.is_some_and(|evidence| {
+ evidence.outcome() == RadrootsNip09SuppressionOutcome::Suppressed
+ })
+ }
+ };
+ if !origin_valid || !admission_valid || !visibility_valid {
+ return Err(corruption(
+ "transition fields have an incoherent decision shape",
+ ));
+ }
+ if let (Some(visible), Some(retracted)) = (visible_event, retracted_event)
+ && visible == retracted
+ {
+ return Err(corruption("transition retracts the event it makes visible"));
+ }
+ if let Some(evidence) = suppression {
+ validate_suppression_shape(evidence, raw_head_created_at)?;
+ }
+ Ok(())
+}
+
+fn validate_suppression_shape(
+ evidence: &RadrootsNip09SuppressionEvidenceV1,
+ raw_head_created_at: u64,
+) -> Result<(), RadrootsEventStoreError> {
+ if !evidence.is_coherent_for_event(30_000, raw_head_created_at) {
+ return Err(corruption(
+ "suppression evidence is internally inconsistent",
+ ));
+ }
+ Ok(())
+}
+
+fn suppression_evidence_from_transition_row(
+ row: &sqlx::sqlite::SqliteRow,
+) -> Result<Option<RadrootsNip09SuppressionEvidenceV1>, RadrootsEventStoreError> {
+ let outcome: Option<String> = row.try_get("nip09_outcome")?;
+ let reason: Option<String> = row.try_get("nip09_reason")?;
+ let event_reference_request_id = optional_event_id(
+ "event_reference_request_id",
+ row.try_get("event_reference_request_id")?,
+ )?;
+ let address_reference_request_id = optional_event_id(
+ "address_reference_request_id",
+ row.try_get("address_reference_request_id")?,
+ )?;
+ let address_reference_cutoff = row
+ .try_get::<Option<i64>, _>("address_reference_cutoff")?
+ .map(|value| u64_from_i64("transition.address_reference_cutoff", value))
+ .transpose()
+ .map_err(|error| corruption(error.to_string()))?;
+ match (outcome, reason) {
+ (Some(outcome), Some(reason)) => Ok(Some(RadrootsNip09SuppressionEvidenceV1 {
+ outcome: parse_suppression_outcome(outcome.as_str())
+ .map_err(|error| corruption(error.to_string()))?,
+ reason: parse_suppression_reason(reason.as_str())
+ .map_err(|error| corruption(error.to_string()))?,
+ event_reference_request_id,
+ address_reference_request_id,
+ address_reference_cutoff,
+ })),
+ (None, None)
+ if event_reference_request_id.is_none()
+ && address_reference_request_id.is_none()
+ && address_reference_cutoff.is_none() =>
+ {
+ Ok(None)
+ }
+ _ => Err(corruption("transition has incomplete suppression evidence")),
+ }
+}
+
+fn required_reference(
+ field: &'static str,
+ event_id: String,
+ event_seq: i64,
+) -> Result<RadrootsAddressableTransitionEventReferenceV1, RadrootsEventStoreError> {
+ if event_seq <= 0 {
+ return Err(corruption(format!("{field} sequence is not positive")));
+ }
+ Ok(RadrootsAddressableTransitionEventReferenceV1 {
+ event_id: parse_event_id(field, event_id)?,
+ event_seq,
+ })
+}
+
+fn optional_reference(
+ field: &'static str,
+ event_id: Option<String>,
+ event_seq: Option<i64>,
+) -> Result<Option<RadrootsAddressableTransitionEventReferenceV1>, RadrootsEventStoreError> {
+ match (event_id, event_seq) {
+ (Some(event_id), Some(event_seq)) => {
+ required_reference(field, event_id, event_seq).map(Some)
+ }
+ (None, None) => Ok(None),
+ _ => Err(corruption(format!("{field} identity is partial"))),
+ }
+}
+
+fn optional_event_id(
+ field: &'static str,
+ value: Option<String>,
+) -> Result<Option<RadrootsEventId>, RadrootsEventStoreError> {
+ value.map(|value| parse_event_id(field, value)).transpose()
+}
+
+fn parse_event_id(
+ field: &'static str,
+ value: String,
+) -> Result<RadrootsEventId, RadrootsEventStoreError> {
+ RadrootsEventId::parse(value.as_str())
+ .map_err(|error| corruption(format!("{field} event id is invalid: {error}")))
+}
+
+async fn load_and_validate_stored_event(
+ connection: &mut SqliteConnection,
+ reference: &RadrootsAddressableTransitionEventReferenceV1,
+) -> Result<(RadrootsStoredRawEvent, EventAdmission), RadrootsEventStoreError> {
+ let row = sqlx::query(
+ "SELECT seq, event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms FROM event_envelopes WHERE seq = ? AND event_id = ?",
+ )
+ .bind(reference.event_seq())
+ .bind(reference.event_id().as_str())
+ .fetch_optional(&mut *connection)
+ .await?
+ .ok_or_else(|| {
+ corruption(format!(
+ "referenced event `{}` at sequence {} is missing",
+ reference.event_id(),
+ reference.event_seq()
+ ))
+ })?;
+ let stored = stored_raw_event_from_row(row)
+ .map_err(|error| corruption(format!("stored raw event row is invalid: {error}")))?;
+ let reconstructed = RadrootsEventIngest::from_raw_json(stored.raw_json.clone(), 0)
+ .map_err(|error| corruption(format!("stored raw event cannot be reverified: {error}")))?;
+ let event = reconstructed.event();
+ let tags_json = serde_json::to_string(&event.tags_as_vec()).map_err(|error| {
+ corruption(format!(
+ "stored raw event tags cannot be canonicalized: {error}"
+ ))
+ })?;
+ if stored.event_id != event.id_str()
+ || stored.pubkey != event.author_str()
+ || stored.created_at != event.created_at_u64()
+ || stored.kind != event.kind_u32()
+ || stored.tags_json != tags_json
+ || stored.content != event.content()
+ || stored.sig != event.sig_str()
+ {
+ return Err(corruption(format!(
+ "stored event `{}` disagrees with its signed raw JSON",
+ reference.event_id()
+ )));
+ }
+ let admission = EventAdmission::for_profile(
+ ReconciliationProfile::Nip09V1RegistryV7,
+ reconstructed.verified_event(),
+ )
+ .map_err(|error| corruption(format!("stored raw event cannot be admitted: {error}")))?;
+ if admission.status != stored.admission_status
+ || admission.contract.map(|contract| contract.id) != stored.contract_id.as_deref()
+ || admission.valid_stream_eligible(event.kind_class()) != stored.valid_stream_eligible
+ {
+ return Err(corruption(format!(
+ "stored event `{}` disagrees with registry-v7 admission",
+ reference.event_id()
+ )));
+ }
+ Ok((stored, admission))
+}
+
+async fn validate_addressable_reference(
+ connection: &mut SqliteConnection,
+ generation: RadrootsEventStoreSourceGeneration,
+ coordinate: &RadrootsAddressableTransitionCoordinateV1,
+ reference: &RadrootsAddressableTransitionEventReferenceV1,
+ event: &RadrootsStoredRawEvent,
+) -> Result<(), RadrootsEventStoreError> {
+ if event.event_class != StoredEventClass::Addressable
+ || event.kind != coordinate.kind()
+ || event.pubkey != coordinate.pubkey().as_str()
+ {
+ return Err(corruption(format!(
+ "event `{}` does not match transition coordinate `{}:{}:{}`",
+ reference.event_id(),
+ coordinate.kind(),
+ coordinate.pubkey(),
+ coordinate.d_tag()
+ )));
+ }
+ let exists: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_event_coordinate WHERE source_generation = ? AND event_seq = ? AND event_id = ? AND coordinate_type = 'addressable' AND kind = ? AND pubkey = ? AND raw_d_tag = ?",
+ )
+ .bind(generation.as_bytes().as_slice())
+ .bind(reference.event_seq())
+ .bind(reference.event_id().as_str())
+ .bind(i64::from(coordinate.kind()))
+ .bind(coordinate.pubkey().as_str())
+ .bind(coordinate.d_tag())
+ .fetch_one(&mut *connection)
+ .await?;
+ if exists != 1 {
+ return Err(corruption(format!(
+ "event `{}` has no matching addressable coordinate authority",
+ reference.event_id()
+ )));
+ }
+ Ok(())
+}
+
+fn corruption(reason: impl Into<String>) -> RadrootsEventStoreError {
+ RadrootsEventStoreError::AddressableTransitionCorruption {
+ reason: reason.into(),
+ }
+}
diff --git a/crates/event_store/src/store/current_visibility_v1.rs b/crates/event_store/src/store/current_visibility_v1.rs
@@ -0,0 +1,345 @@
+use super::protocol_storage_v1::stored_raw_event_from_row;
+use super::{RadrootsEventStore, bool_from_i64};
+use crate::RadrootsEventStoreError;
+use crate::model::{
+ RadrootsCurrentEventVisibilityV1, RadrootsCurrentVisibilityDecisionV1,
+ RadrootsNip09SuppressionEvidenceV1, RadrootsNip09SuppressionOutcome,
+ RadrootsNip09SuppressionReason, StoredEventClass,
+};
+use crate::nip09::reconciliation_v1::generation_from_blob;
+use radroots_event::ids::RadrootsEventId;
+use sqlx::{Row, Sqlite, Transaction};
+
+impl RadrootsEventStore {
+ pub async fn current_event_visibility_v1(
+ &self,
+ event_id: &str,
+ ) -> Result<Option<RadrootsCurrentEventVisibilityV1>, RadrootsEventStoreError> {
+ let mut tx = self.pool.begin().await?;
+ let result = current_visibility_in_transaction(&mut tx, event_id).await?;
+ tx.commit().await?;
+ Ok(result)
+ }
+}
+
+pub(super) async fn current_visibility_in_transaction(
+ tx: &mut Transaction<'_, Sqlite>,
+ event_id: &str,
+) -> Result<Option<RadrootsCurrentEventVisibilityV1>, RadrootsEventStoreError> {
+ let event_exists: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM event_envelopes WHERE event_id = ?")
+ .bind(event_id)
+ .fetch_one(&mut **tx)
+ .await?;
+ if event_exists == 0 {
+ return Ok(None);
+ }
+ let row = sqlx::query(
+ "SELECT event.seq, event.event_id, event.pubkey, event.created_at, event.kind, event.tags_json, event.content, event.sig, event.raw_json, event.verification_status, event.contract_status, event.contract_id, event.event_class, event.projection_eligible, event.inserted_at_ms, event.updated_at_ms, visibility.source_generation, visibility.raw_d_tag, visibility.is_raw_head, visibility.raw_head_event_id, visibility.suppression_outcome, visibility.suppression_reason, visibility.event_reference_request_id, visibility.address_reference_request_id, visibility.address_reference_cutoff, visibility.current_visibility FROM radroots_event_store_current_visibility_v1 AS visibility JOIN event_envelopes AS event ON event.event_id = visibility.event_id WHERE visibility.event_id = ?",
+ )
+ .bind(event_id)
+ .fetch_optional(&mut **tx)
+ .await?;
+ let row = row.ok_or_else(|| RadrootsEventStoreError::CurrentVisibilityDrift {
+ reason: format!("stored event `{event_id}` has no current-visibility authority"),
+ })?;
+
+ let source_generation = generation_from_blob(row.try_get("source_generation")?)
+ .map_err(|error| visibility_authority_error("source generation", error))?;
+ let raw_d_tag: Option<String> = row.try_get("raw_d_tag")?;
+ let is_raw_head = bool_from_i64(
+ "current_visibility.is_raw_head",
+ row.try_get("is_raw_head")?,
+ )
+ .map_err(|error| visibility_authority_error("raw-head marker", error))?;
+ let raw_head_event_id = row
+ .try_get::<Option<String>, _>("raw_head_event_id")?
+ .map(RadrootsEventId::parse)
+ .transpose()
+ .map_err(|error| visibility_authority_error("raw-head event id", error))?;
+ let decision = RadrootsCurrentVisibilityDecisionV1::parse(
+ row.try_get::<String, _>("current_visibility")?.as_str(),
+ )
+ .map_err(|error| visibility_authority_error("visibility decision", error))?;
+ let suppression = suppression_evidence_from_row(&row)
+ .map_err(|error| visibility_authority_error("suppression evidence", error))?;
+ let event = stored_raw_event_from_row(row)
+ .map_err(|error| visibility_authority_error("stored raw event", error))?;
+ let visibility = RadrootsCurrentEventVisibilityV1 {
+ source_generation,
+ event,
+ is_raw_head,
+ raw_head_event_id,
+ suppression,
+ decision,
+ };
+ validate_visibility_shape(&visibility)?;
+ validate_addressable_head_projection(tx, &visibility, raw_d_tag.as_deref()).await?;
+ Ok(Some(visibility))
+}
+
+pub(super) fn suppression_evidence_from_row(
+ row: &sqlx::sqlite::SqliteRow,
+) -> Result<Option<RadrootsNip09SuppressionEvidenceV1>, RadrootsEventStoreError> {
+ let outcome: Option<String> = row.try_get("suppression_outcome")?;
+ let reason: Option<String> = row.try_get("suppression_reason")?;
+ let event_reference_request_id = row
+ .try_get::<Option<String>, _>("event_reference_request_id")?
+ .map(RadrootsEventId::parse)
+ .transpose()
+ .map_err(|error| visibility_authority_error("event deletion request id", error))?;
+ let address_reference_request_id = row
+ .try_get::<Option<String>, _>("address_reference_request_id")?
+ .map(RadrootsEventId::parse)
+ .transpose()
+ .map_err(|error| visibility_authority_error("address deletion request id", error))?;
+ let address_reference_cutoff = row
+ .try_get::<Option<i64>, _>("address_reference_cutoff")?
+ .map(|value| {
+ u64::try_from(value).map_err(|_| RadrootsEventStoreError::IntegerRange {
+ field: "current_visibility.address_reference_cutoff",
+ value,
+ })
+ })
+ .transpose()
+ .map_err(|error| visibility_authority_error("address deletion cutoff", error))?;
+ match (outcome, reason) {
+ (Some(outcome), Some(reason)) => Ok(Some(RadrootsNip09SuppressionEvidenceV1 {
+ outcome: parse_suppression_outcome(outcome.as_str())
+ .map_err(|error| visibility_authority_error("suppression outcome", error))?,
+ reason: parse_suppression_reason(reason.as_str())
+ .map_err(|error| visibility_authority_error("suppression reason", error))?,
+ event_reference_request_id,
+ address_reference_request_id,
+ address_reference_cutoff,
+ })),
+ (None, None)
+ if event_reference_request_id.is_none()
+ && address_reference_request_id.is_none()
+ && address_reference_cutoff.is_none() =>
+ {
+ Ok(None)
+ }
+ _ => current_visibility_drift("suppression evidence is incomplete"),
+ }
+}
+
+pub(super) fn parse_suppression_outcome(
+ value: &str,
+) -> Result<RadrootsNip09SuppressionOutcome, RadrootsEventStoreError> {
+ match value {
+ "visible" => Ok(RadrootsNip09SuppressionOutcome::Visible),
+ "suppressed" => Ok(RadrootsNip09SuppressionOutcome::Suppressed),
+ _ => Err(RadrootsEventStoreError::InvalidStoredEnum {
+ field: "nip09.suppression_outcome",
+ value: value.to_owned(),
+ }),
+ }
+}
+
+pub(super) fn parse_suppression_reason(
+ value: &str,
+) -> Result<RadrootsNip09SuppressionReason, RadrootsEventStoreError> {
+ match value {
+ "deletion_request_immune" => Ok(RadrootsNip09SuppressionReason::DeletionRequestImmune),
+ "deletion_no_authorized_reference" => {
+ Ok(RadrootsNip09SuppressionReason::NoAuthorizedReference)
+ }
+ "deletion_request_author_mismatch" => {
+ Ok(RadrootsNip09SuppressionReason::RequestAuthorMismatch)
+ }
+ "deletion_address_cutoff_precedes_target" => {
+ Ok(RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget)
+ }
+ "deletion_event_id_reference" => Ok(RadrootsNip09SuppressionReason::EventIdReference),
+ "deletion_address_reference" => {
+ Ok(RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff)
+ }
+ "deletion_event_id_and_address_reference" => {
+ Ok(RadrootsNip09SuppressionReason::EventIdAndAddressReference)
+ }
+ _ => Err(RadrootsEventStoreError::InvalidStoredEnum {
+ field: "nip09.suppression_reason",
+ value: value.to_owned(),
+ }),
+ }
+}
+
+fn validate_visibility_shape(
+ visibility: &RadrootsCurrentEventVisibilityV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let event_id = visibility.event.event_id.as_str();
+ if visibility.event.event_class == StoredEventClass::Ephemeral {
+ return current_visibility_drift(format!(
+ "persisted ephemeral event `{event_id}` entered current visibility"
+ ));
+ }
+ let regular = visibility.event.event_class == StoredEventClass::Regular;
+ if regular && (visibility.raw_head_event_id.is_some() || !visibility.is_raw_head) {
+ return current_visibility_drift(format!(
+ "event `{event_id}` has inconsistent raw-head identity"
+ ));
+ }
+ if !regular
+ && visibility.is_raw_head
+ && visibility
+ .raw_head_event_id
+ .as_ref()
+ .is_none_or(|raw_head_event_id| raw_head_event_id.as_str() != event_id)
+ {
+ return current_visibility_drift(format!(
+ "event `{event_id}` is marked as the raw head without matching head identity"
+ ));
+ }
+ if !regular
+ && visibility.raw_head_event_id.is_none()
+ && visibility.decision != RadrootsCurrentVisibilityDecisionV1::NotAdmitted
+ {
+ return current_visibility_drift(format!(
+ "event `{event_id}` has no coordinate head but is not classified as not admitted"
+ ));
+ }
+ let evidence = visibility.suppression.as_ref();
+ if evidence.is_some_and(|value| {
+ !value.is_coherent_for_event(visibility.event.kind, visibility.event.created_at)
+ }) {
+ return current_visibility_drift(format!(
+ "event `{event_id}` has incoherent suppression evidence"
+ ));
+ }
+ let valid = match visibility.decision {
+ RadrootsCurrentVisibilityDecisionV1::Visible => {
+ visibility.event.admission_status
+ == crate::model::RadrootsEventAdmissionStatus::Admitted
+ && visibility.is_raw_head
+ && evidence
+ .is_some_and(|value| value.outcome == RadrootsNip09SuppressionOutcome::Visible)
+ }
+ RadrootsCurrentVisibilityDecisionV1::NotAdmitted => {
+ visibility.event.admission_status
+ != crate::model::RadrootsEventAdmissionStatus::Admitted
+ && evidence.is_none()
+ }
+ RadrootsCurrentVisibilityDecisionV1::NotCurrent => {
+ visibility.event.admission_status
+ == crate::model::RadrootsEventAdmissionStatus::Admitted
+ && !visibility.is_raw_head
+ && visibility.raw_head_event_id.is_some()
+ && evidence.is_some()
+ }
+ RadrootsCurrentVisibilityDecisionV1::Suppressed => {
+ visibility.event.admission_status
+ == crate::model::RadrootsEventAdmissionStatus::Admitted
+ && visibility.is_raw_head
+ && evidence.is_some_and(|value| {
+ value.outcome == RadrootsNip09SuppressionOutcome::Suppressed
+ })
+ }
+ };
+ if !valid {
+ return current_visibility_drift(format!(
+ "event `{event_id}` has an incoherent visibility decision"
+ ));
+ }
+ Ok(())
+}
+
+async fn validate_addressable_head_projection(
+ tx: &mut Transaction<'_, Sqlite>,
+ visibility: &RadrootsCurrentEventVisibilityV1,
+ raw_d_tag: Option<&str>,
+) -> Result<(), RadrootsEventStoreError> {
+ if visibility.event.event_class != StoredEventClass::Addressable || !visibility.is_raw_head {
+ return Ok(());
+ }
+ let raw_d_tag = raw_d_tag.ok_or_else(|| RadrootsEventStoreError::CurrentVisibilityDrift {
+ reason: format!(
+ "addressable event `{}` has no raw d tag",
+ visibility.event.event_id
+ ),
+ })?;
+ let row = sqlx::query(
+ "SELECT state.raw_head_event_id, state.raw_head_event_seq, state.raw_head_created_at, state.admission_status, state.admission_code, coordinate.admission_code AS coordinate_admission_code, state.contract_id, state.visibility, state.nip09_outcome, state.nip09_reason, state.event_reference_request_id, state.address_reference_request_id, state.address_reference_cutoff FROM radroots_event_store_addressable_head_state AS state JOIN radroots_event_store_event_coordinate AS coordinate ON coordinate.source_generation = state.source_generation AND coordinate.event_seq = state.raw_head_event_seq AND coordinate.event_id = state.raw_head_event_id AND coordinate.coordinate_type = 'addressable' AND coordinate.kind = state.kind AND coordinate.pubkey = state.pubkey AND coordinate.raw_d_tag = state.d_tag WHERE state.source_generation = ? AND state.kind = ? AND state.pubkey = ? AND state.d_tag = ?",
+ )
+ .bind(visibility.source_generation.as_bytes().as_slice())
+ .bind(i64::from(visibility.event.kind))
+ .bind(visibility.event.pubkey.as_str())
+ .bind(raw_d_tag)
+ .fetch_optional(&mut **tx)
+ .await?;
+ let Some(row) = row else {
+ return current_visibility_drift(format!(
+ "addressable head state is missing for `{}`",
+ visibility.event.event_id
+ ));
+ };
+ let evidence = visibility.suppression.as_ref();
+ let stored_cutoff = row
+ .try_get::<Option<i64>, _>("address_reference_cutoff")?
+ .map(|value| {
+ u64::try_from(value).map_err(|_| RadrootsEventStoreError::IntegerRange {
+ field: "addressable_head_state.address_reference_cutoff",
+ value,
+ })
+ })
+ .transpose()
+ .map_err(|error| visibility_authority_error("stored address deletion cutoff", error))?;
+ if row.try_get::<String, _>("raw_head_event_id")? != visibility.event.event_id
+ || row.try_get::<i64, _>("raw_head_event_seq")? != visibility.event.seq
+ || row.try_get::<i64, _>("raw_head_created_at")?
+ != i64::try_from(visibility.event.created_at).map_err(|_| {
+ RadrootsEventStoreError::CurrentVisibilityDrift {
+ reason: format!(
+ "addressable event `{}` timestamp is outside SQLite range",
+ visibility.event.event_id
+ ),
+ }
+ })?
+ || row.try_get::<String, _>("admission_status")?
+ != visibility.event.admission_status.as_str()
+ || row.try_get::<Option<String>, _>("admission_code")?
+ != row.try_get::<Option<String>, _>("coordinate_admission_code")?
+ || row.try_get::<Option<String>, _>("contract_id")? != visibility.event.contract_id
+ || row.try_get::<String, _>("visibility")? != visibility.decision.as_str()
+ || row
+ .try_get::<Option<String>, _>("nip09_outcome")?
+ .as_deref()
+ != evidence.map(|value| value.outcome.code())
+ || row.try_get::<Option<String>, _>("nip09_reason")?.as_deref()
+ != evidence.map(|value| value.reason.code())
+ || row
+ .try_get::<Option<String>, _>("event_reference_request_id")?
+ .as_deref()
+ != evidence
+ .and_then(|value| value.event_reference_request_id.as_ref())
+ .map(RadrootsEventId::as_str)
+ || row
+ .try_get::<Option<String>, _>("address_reference_request_id")?
+ .as_deref()
+ != evidence
+ .and_then(|value| value.address_reference_request_id.as_ref())
+ .map(RadrootsEventId::as_str)
+ || stored_cutoff != evidence.and_then(|value| value.address_reference_cutoff)
+ {
+ return current_visibility_drift(format!(
+ "central visibility disagrees with addressable head state for `{}`",
+ visibility.event.event_id
+ ));
+ }
+ Ok(())
+}
+
+fn current_visibility_drift<T>(reason: impl Into<String>) -> Result<T, RadrootsEventStoreError> {
+ Err(RadrootsEventStoreError::CurrentVisibilityDrift {
+ reason: reason.into(),
+ })
+}
+
+fn visibility_authority_error(
+ context: &'static str,
+ error: impl core::fmt::Display,
+) -> RadrootsEventStoreError {
+ RadrootsEventStoreError::CurrentVisibilityDrift {
+ reason: format!("{context} is invalid: {error}"),
+ }
+}
diff --git a/crates/event_store/src/store/food_availability_projection_v1.rs b/crates/event_store/src/store/food_availability_projection_v1.rs
@@ -0,0 +1,952 @@
+use super::addressable_transition_feed_v1::addressable_transition_page_in_transaction_v1;
+use super::{
+ RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX, RadrootsEventStore, bool_from_i64, u64_from_i64,
+};
+use crate::RadrootsEventStoreError;
+use crate::generated::food_availability_projection_manifest as food_manifest;
+use crate::model::{
+ RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1,
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1, RadrootsAddressableTransitionCursorV1,
+ RadrootsAddressableTransitionScopeV1, RadrootsAddressableTransitionV1,
+ RadrootsEventAdmissionStatus, RadrootsEventIngest, RadrootsEventStoreSourceGeneration,
+ RadrootsFoodAvailabilitySearchQueryV1, RadrootsFoodAvailabilityStatusFilterV1,
+ RadrootsStoredFoodAvailabilityImageV1, RadrootsStoredFoodAvailabilityV1,
+};
+use crate::nip09::reconciliation_v1::{
+ EventAdmission, ReconciliationProfile, generation_from_blob,
+};
+use radroots_event::food_availability::RadrootsFoodIdentifier;
+use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey};
+use radroots_event_codec::food_availability::inbound::{
+ RadrootsFoodAvailabilityImageDiagnostic, RadrootsFoodAvailabilityProjectionOutcome,
+ project_verified_food_availability_event_registry_v7,
+};
+use serde::Deserialize;
+use sqlx::{Row, SqliteConnection};
+
+#[cfg(test)]
+use std::sync::Arc;
+#[cfg(test)]
+use tokio::sync::Notify;
+
+#[cfg(test)]
+tokio::task_local! {
+ pub(super) static FOOD_AVAILABILITY_AUDIT_FTS_CHECKPOINT: (Arc<Notify>, Arc<Notify>);
+}
+
+const FOOD_AVAILABILITY_CONTRACT_ID: &str = "radroots.food.availability.v1";
+pub(super) const FOOD_AVAILABILITY_POINT_QUERY_V1: &str = "SELECT projection.source_generation, projection.pubkey, projection.d_tag, projection.event_id, projection.event_seq, projection.created_at, projection.contract_id, projection.content, projection.title, projection.summary, projection.published_at, projection.location, projection.price_amount, projection.price_currency, projection.price_unit, projection.quantity_amount, projection.quantity_unit, projection.status, projection.diagnostic_codes_json, projection.source_transition_seq, projection.immutable_raw_json, projection.stored_images_json FROM radroots_event_store_food_availability_read_v1 AS projection JOIN radroots_event_store_source_state AS source ON source.singleton = 1 AND source.active_generation = projection.source_generation JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.singleton = 1 AND cursor.source_generation = projection.source_generation JOIN radroots_event_store_addressable_head_state AS head ON head.source_generation = projection.source_generation AND head.kind = 30402 AND head.pubkey = projection.pubkey AND head.d_tag = projection.d_tag AND head.raw_head_event_id = projection.event_id AND head.raw_head_event_seq = projection.event_seq AND head.raw_head_created_at = projection.created_at AND head.admission_status = 'admitted' AND head.admission_code IS NULL AND head.contract_id = projection.contract_id AND head.visibility = 'visible' AND head.nip09_outcome = 'visible' WHERE projection.pubkey = ? AND projection.d_tag = ?";
+pub(super) const FOOD_AVAILABILITY_RECENT_QUERY_V1: &str = "SELECT projection.source_generation, projection.pubkey, projection.d_tag, projection.event_id, projection.event_seq, projection.created_at, projection.contract_id, projection.content, projection.title, projection.summary, projection.published_at, projection.location, projection.price_amount, projection.price_currency, projection.price_unit, projection.quantity_amount, projection.quantity_unit, projection.status, projection.diagnostic_codes_json, projection.source_transition_seq, projection.immutable_raw_json, projection.stored_images_json FROM radroots_event_store_source_state AS source CROSS JOIN radroots_event_store_food_availability_read_v1 AS projection ON source.singleton = 1 AND source.active_generation = projection.source_generation CROSS JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.singleton = 1 AND cursor.source_generation = projection.source_generation CROSS JOIN radroots_event_store_addressable_head_state AS head ON head.source_generation = projection.source_generation AND head.kind = 30402 AND head.pubkey = projection.pubkey AND head.d_tag = projection.d_tag AND head.raw_head_event_id = projection.event_id AND head.raw_head_event_seq = projection.event_seq AND head.raw_head_created_at = projection.created_at AND head.admission_status = 'admitted' AND head.admission_code IS NULL AND head.contract_id = projection.contract_id AND head.visibility = 'visible' AND head.nip09_outcome = 'visible' ORDER BY projection.published_at DESC, projection.event_id ASC LIMIT ?";
+pub(super) const FOOD_AVAILABILITY_RECENT_STATUS_QUERY_V1: &str = "SELECT projection.source_generation, projection.pubkey, projection.d_tag, projection.event_id, projection.event_seq, projection.created_at, projection.contract_id, projection.content, projection.title, projection.summary, projection.published_at, projection.location, projection.price_amount, projection.price_currency, projection.price_unit, projection.quantity_amount, projection.quantity_unit, projection.status, projection.diagnostic_codes_json, projection.source_transition_seq, projection.immutable_raw_json, projection.stored_images_json FROM radroots_event_store_food_availability_read_v1 AS projection JOIN radroots_event_store_source_state AS source ON source.singleton = 1 AND source.active_generation = projection.source_generation JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.singleton = 1 AND cursor.source_generation = projection.source_generation JOIN radroots_event_store_addressable_head_state AS head ON head.source_generation = projection.source_generation AND head.kind = 30402 AND head.pubkey = projection.pubkey AND head.d_tag = projection.d_tag AND head.raw_head_event_id = projection.event_id AND head.raw_head_event_seq = projection.event_seq AND head.raw_head_created_at = projection.created_at AND head.admission_status = 'admitted' AND head.admission_code IS NULL AND head.contract_id = projection.contract_id AND head.visibility = 'visible' AND head.nip09_outcome = 'visible' WHERE projection.status = ? ORDER BY projection.published_at DESC, projection.event_id ASC LIMIT ?";
+pub(super) const FOOD_AVAILABILITY_SEARCH_QUERY_V1: &str = "SELECT projection.source_generation, projection.pubkey, projection.d_tag, projection.event_id, projection.event_seq, projection.created_at, projection.contract_id, projection.content, projection.title, projection.summary, projection.published_at, projection.location, projection.price_amount, projection.price_currency, projection.price_unit, projection.quantity_amount, projection.quantity_unit, projection.status, projection.diagnostic_codes_json, projection.source_transition_seq, projection.immutable_raw_json, projection.stored_images_json FROM radroots_event_store_food_availability_search_fts JOIN radroots_event_store_food_availability_read_v1 AS projection ON projection.event_seq = radroots_event_store_food_availability_search_fts.rowid JOIN radroots_event_store_source_state AS source ON source.singleton = 1 AND source.active_generation = projection.source_generation JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.singleton = 1 AND cursor.source_generation = projection.source_generation JOIN radroots_event_store_addressable_head_state AS head ON head.source_generation = projection.source_generation AND head.kind = 30402 AND head.pubkey = projection.pubkey AND head.d_tag = projection.d_tag AND head.raw_head_event_id = projection.event_id AND head.raw_head_event_seq = projection.event_seq AND head.raw_head_created_at = projection.created_at AND head.admission_status = 'admitted' AND head.admission_code IS NULL AND head.contract_id = projection.contract_id AND head.visibility = 'visible' AND head.nip09_outcome = 'visible' WHERE radroots_event_store_food_availability_search_fts MATCH ? AND (? IS NULL OR projection.status = ?) ORDER BY projection.published_at DESC, projection.event_id ASC LIMIT ?";
+
+#[derive(Clone, Debug)]
+struct FoodAvailabilityProjectionCursorState {
+ feed_cursor: RadrootsAddressableTransitionCursorV1,
+ projected_row_count: i64,
+}
+
+impl RadrootsEventStore {
+ pub async fn food_availability_v1(
+ &self,
+ pubkey: &RadrootsPublicKey,
+ identifier: &RadrootsFoodIdentifier,
+ ) -> Result<Option<RadrootsStoredFoodAvailabilityV1>, RadrootsEventStoreError> {
+ let mut tx = self.pool.begin().await?;
+ validate_food_availability_projection_hook_state_fast_v1(&mut tx).await?;
+ let row = sqlx::query(FOOD_AVAILABILITY_POINT_QUERY_V1)
+ .bind(pubkey.as_str())
+ .bind(identifier.as_str())
+ .fetch_optional(&mut *tx)
+ .await?;
+ let result = match row {
+ Some(row) => Some(load_and_validate_projection_row(row)?),
+ None => None,
+ };
+ tx.commit().await?;
+ Ok(result)
+ }
+
+ pub async fn recent_food_availability_v1(
+ &self,
+ status: RadrootsFoodAvailabilityStatusFilterV1,
+ limit: u32,
+ ) -> Result<Vec<RadrootsStoredFoodAvailabilityV1>, RadrootsEventStoreError> {
+ validate_query_limit(limit)?;
+ let mut tx = self.pool.begin().await?;
+ validate_food_availability_projection_hook_state_fast_v1(&mut tx).await?;
+ let rows = match status.storage_value() {
+ None => {
+ sqlx::query(FOOD_AVAILABILITY_RECENT_QUERY_V1)
+ .bind(i64::from(limit))
+ .fetch_all(&mut *tx)
+ .await?
+ }
+ Some(status) => {
+ sqlx::query(FOOD_AVAILABILITY_RECENT_STATUS_QUERY_V1)
+ .bind(status)
+ .bind(i64::from(limit))
+ .fetch_all(&mut *tx)
+ .await?
+ }
+ };
+ let result = load_and_validate_projection_rows(rows)?;
+ tx.commit().await?;
+ Ok(result)
+ }
+
+ pub async fn search_food_availability_v1(
+ &self,
+ query: &RadrootsFoodAvailabilitySearchQueryV1,
+ status: RadrootsFoodAvailabilityStatusFilterV1,
+ limit: u32,
+ ) -> Result<Vec<RadrootsStoredFoodAvailabilityV1>, RadrootsEventStoreError> {
+ validate_query_limit(limit)?;
+ let mut tx = self.pool.begin().await?;
+ validate_food_availability_projection_hook_state_fast_v1(&mut tx).await?;
+ let rows = sqlx::query(FOOD_AVAILABILITY_SEARCH_QUERY_V1)
+ .bind(query.fts5_match_expression())
+ .bind(status.storage_value())
+ .bind(status.storage_value())
+ .bind(i64::from(limit))
+ .fetch_all(&mut *tx)
+ .await?;
+ let result = load_and_validate_projection_rows(rows)?;
+ tx.commit().await?;
+ Ok(result)
+ }
+
+ /// Runs the exhaustive FoodAvailability projection and FTS integrity audit.
+ ///
+ /// Normal reads use the bounded seal check. Maintenance, rebuild, and
+ /// conformance paths should call this after all typed writes are complete.
+ pub async fn audit_food_availability_projection_v1(
+ &self,
+ ) -> Result<(), RadrootsEventStoreError> {
+ let mut tx = self.begin_write_transaction().await?;
+ validate_food_availability_projection_hook_v1(&mut tx).await?;
+ tx.commit().await?;
+ Ok(())
+ }
+}
+
+pub(crate) async fn apply_food_availability_projection_hook_v1(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ apply_pending_food_availability_transitions_v1(connection).await?;
+ validate_food_availability_projection_hook_v1(connection).await
+}
+
+pub(crate) async fn apply_pending_food_availability_transitions_v1(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ let scope = RadrootsAddressableTransitionScopeV1::food_availability();
+ let mut state = ensure_projection_cursor(connection, &scope).await?;
+ loop {
+ let page = addressable_transition_page_in_transaction_v1(
+ connection,
+ &scope,
+ Some(&state.feed_cursor),
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1,
+ )
+ .await?;
+ let mut projected_row_delta = 0_i64;
+ for transition in page.transitions() {
+ projected_row_delta = projected_row_delta
+ .checked_add(apply_transition(connection, transition).await?)
+ .ok_or_else(|| projection_drift("projection row-count delta overflowed"))?;
+ }
+ let next = page.next_cursor().clone();
+ state = advance_projection_cursor(connection, &state, projected_row_delta, next).await?;
+ if !page.has_more() {
+ break;
+ }
+ }
+ Ok(())
+}
+
+async fn ensure_projection_cursor(
+ connection: &mut SqliteConnection,
+ scope: &RadrootsAddressableTransitionScopeV1,
+) -> Result<FoodAvailabilityProjectionCursorState, RadrootsEventStoreError> {
+ let source = sqlx::query(
+ "SELECT source.active_generation, source.last_transition_seq, generation.transition_floor_seq, generation.addressable_feed_version FROM radroots_event_store_source_state AS source JOIN radroots_event_store_source_generation AS generation ON generation.source_generation = source.active_generation JOIN radroots_event_store_addressable_feed_integrity_v1 AS integrity ON integrity.source_generation = source.active_generation AND integrity.transition_floor_seq = generation.transition_floor_seq AND integrity.last_transition_seq = source.last_transition_seq WHERE source.singleton = 1",
+ )
+ .fetch_optional(&mut *connection)
+ .await?
+ .ok_or_else(|| projection_drift("active source/feed authority is missing"))?;
+ let generation = projection_generation_from_blob(
+ source.try_get("active_generation")?,
+ "active source generation is invalid",
+ )?;
+ let floor: i64 = source.try_get("transition_floor_seq")?;
+ let feed_version: i64 = source.try_get("addressable_feed_version")?;
+ if feed_version != i64::from(RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1) {
+ return Err(projection_drift(format!(
+ "addressable feed version is {feed_version}"
+ )));
+ }
+
+ let existing = sqlx::query(
+ "SELECT source_generation, feed_version, projection_version, scope_fingerprint, hook_manifest_sha256, last_transition_seq, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_optional(&mut *connection)
+ .await?;
+ if let Some(existing) = existing.as_ref() {
+ let existing_generation = projection_generation_from_blob(
+ existing.try_get("source_generation")?,
+ "stored cursor generation is invalid",
+ )?;
+ if existing_generation != generation {
+ sqlx::query("DELETE FROM radroots_event_store_food_availability_projection")
+ .execute(&mut *connection)
+ .await?;
+ let deleted = sqlx::query(
+ "DELETE FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .execute(&mut *connection)
+ .await?;
+ if deleted.rows_affected() != 1 {
+ return Err(projection_drift(
+ "generation reset did not delete exactly one projection cursor",
+ ));
+ }
+ }
+ }
+
+ let existing = sqlx::query(
+ "SELECT source_generation, feed_version, projection_version, scope_fingerprint, hook_manifest_sha256, last_transition_seq, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_optional(&mut *connection)
+ .await?;
+ if existing.is_none() {
+ let inserted = sqlx::query(
+ "INSERT INTO radroots_event_store_food_availability_cursor(singleton, source_generation, feed_version, projection_version, scope_fingerprint, hook_manifest_sha256, last_transition_seq, projected_row_count) VALUES (1, ?, ?, ?, ?, ?, ?, 0)",
+ )
+ .bind(generation.as_bytes().as_slice())
+ .bind(i64::from(RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1))
+ .bind(i64::from(RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1))
+ .bind(scope.fingerprint().as_bytes().as_slice())
+ .bind(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256)
+ .bind(floor)
+ .execute(&mut *connection)
+ .await?;
+ if inserted.rows_affected() != 1 {
+ return Err(projection_drift(
+ "projection cursor initialization did not insert one row",
+ ));
+ }
+ }
+
+ let row = sqlx::query(
+ "SELECT source_generation, feed_version, projection_version, scope_fingerprint, hook_manifest_sha256, last_transition_seq, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(&mut *connection)
+ .await?;
+ validate_cursor_identity(&row, generation, scope)?;
+ let projected_row_count = row.try_get("projected_row_count")?;
+ validate_projected_row_count(projected_row_count)?;
+ Ok(FoodAvailabilityProjectionCursorState {
+ feed_cursor: RadrootsAddressableTransitionCursorV1::new(
+ generation,
+ scope.fingerprint(),
+ row.try_get("last_transition_seq")?,
+ )?,
+ projected_row_count,
+ })
+}
+
+fn validate_cursor_identity(
+ row: &sqlx::sqlite::SqliteRow,
+ generation: RadrootsEventStoreSourceGeneration,
+ scope: &RadrootsAddressableTransitionScopeV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let stored_generation = projection_generation_from_blob(
+ row.try_get("source_generation")?,
+ "stored cursor generation is invalid",
+ )?;
+ let scope_fingerprint: Vec<u8> = row.try_get("scope_fingerprint")?;
+ if stored_generation != generation
+ || row.try_get::<i64, _>("feed_version")?
+ != i64::from(RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1)
+ || row.try_get::<i64, _>("projection_version")?
+ != i64::from(RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1)
+ || scope_fingerprint.as_slice() != scope.fingerprint().as_bytes().as_slice()
+ || row.try_get::<String, _>("hook_manifest_sha256")?
+ != food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256
+ {
+ return Err(projection_drift(
+ "projection cursor identity is inconsistent",
+ ));
+ }
+ validate_projected_row_count(row.try_get("projected_row_count")?)?;
+ Ok(())
+}
+
+async fn advance_projection_cursor(
+ connection: &mut SqliteConnection,
+ expected: &FoodAvailabilityProjectionCursorState,
+ projected_row_delta: i64,
+ next: RadrootsAddressableTransitionCursorV1,
+) -> Result<FoodAvailabilityProjectionCursorState, RadrootsEventStoreError> {
+ let next_projected_row_count = expected
+ .projected_row_count
+ .checked_add(projected_row_delta)
+ .ok_or_else(|| projection_drift("projection row count overflowed"))?;
+ validate_projected_row_count(next_projected_row_count)?;
+ if next.last_transition_seq() == expected.feed_cursor.last_transition_seq() {
+ if projected_row_delta != 0 {
+ return Err(projection_drift(
+ "projection row count changed without a feed transition",
+ ));
+ }
+ return Ok(expected.clone());
+ }
+ let updated = sqlx::query(
+ "UPDATE radroots_event_store_food_availability_cursor SET last_transition_seq = ?, projected_row_count = ? WHERE singleton = 1 AND source_generation = ? AND last_transition_seq = ? AND projected_row_count = ?",
+ )
+ .bind(next.last_transition_seq())
+ .bind(next_projected_row_count)
+ .bind(next.source_generation().as_bytes().as_slice())
+ .bind(expected.feed_cursor.last_transition_seq())
+ .bind(expected.projected_row_count)
+ .execute(&mut *connection)
+ .await?;
+ if updated.rows_affected() != 1 {
+ return Err(projection_drift(format!(
+ "projection cursor compare-and-swap expected sequence {} and row count {}",
+ expected.feed_cursor.last_transition_seq(),
+ expected.projected_row_count,
+ )));
+ }
+ Ok(FoodAvailabilityProjectionCursorState {
+ feed_cursor: next,
+ projected_row_count: next_projected_row_count,
+ })
+}
+
+async fn apply_transition(
+ connection: &mut SqliteConnection,
+ transition: &RadrootsAddressableTransitionV1,
+) -> Result<i64, RadrootsEventStoreError> {
+ let coordinate = transition.coordinate();
+ let existing_event_id: Option<String> = sqlx::query_scalar(
+ "SELECT event_id FROM radroots_event_store_food_availability_projection WHERE source_generation = ? AND pubkey = ? AND d_tag = ?",
+ )
+ .bind(transition.source_generation().as_bytes().as_slice())
+ .bind(coordinate.pubkey().as_str())
+ .bind(coordinate.d_tag())
+ .fetch_optional(&mut *connection)
+ .await?;
+ let visible_event_id = transition
+ .visible_event()
+ .map(|event| event.event_id().as_str());
+ let mut projected_row_delta = 0_i64;
+ match (existing_event_id.as_deref(), transition.retracted_event()) {
+ (Some(existing), Some(retracted)) if existing == retracted.event_id().as_str() => {
+ let deleted = sqlx::query(
+ "DELETE FROM radroots_event_store_food_availability_projection WHERE source_generation = ? AND pubkey = ? AND d_tag = ? AND event_id = ?",
+ )
+ .bind(transition.source_generation().as_bytes().as_slice())
+ .bind(coordinate.pubkey().as_str())
+ .bind(coordinate.d_tag())
+ .bind(retracted.event_id().as_str())
+ .execute(&mut *connection)
+ .await?;
+ if deleted.rows_affected() != 1 {
+ return Err(projection_drift(
+ "pending FoodAvailability retraction did not delete one row",
+ ));
+ }
+ projected_row_delta = -1;
+ }
+ (Some(existing), None) if visible_event_id == Some(existing) => {
+ if transition.contract_id() != Some(FOOD_AVAILABILITY_CONTRACT_ID) {
+ return Err(projection_drift(
+ "unchanged visible FoodAvailability event lost its contract admission",
+ ));
+ }
+ return Ok(0);
+ }
+ (Some(_), _) => {
+ return Err(projection_drift(
+ "pending transition does not retract the stored coordinate projection",
+ ));
+ }
+ (None, _) => {}
+ }
+
+ let Some(canonical) = transition.visible_event() else {
+ return Ok(projected_row_delta);
+ };
+ if transition.contract_id() != Some(FOOD_AVAILABILITY_CONTRACT_ID) {
+ return Ok(projected_row_delta);
+ }
+ let ingest = RadrootsEventIngest::from_raw_json(canonical.raw_json().to_owned(), 0)
+ .map_err(|error| projection_drift(format!("canonical event reverify failed: {error}")))?;
+ let projection =
+ match project_verified_food_availability_event_registry_v7(ingest.verified_event())
+ .map_err(|error| {
+ projection_drift(format!("FoodAvailability projection failed: {error}"))
+ })? {
+ RadrootsFoodAvailabilityProjectionOutcome::Focused(projection) => projection,
+ RadrootsFoodAvailabilityProjectionOutcome::Excluded(_) => {
+ return Err(projection_drift(
+ "FoodAvailability-admitted transition was excluded by registry-v7 projection",
+ ));
+ }
+ _ => {
+ return Err(projection_drift(
+ "FoodAvailability projection returned an unsupported outcome",
+ ));
+ }
+ };
+ let event = ingest.event();
+ if canonical.event_id().as_str() != event.id_str()
+ || canonical.pubkey().as_str() != event.author_str()
+ || canonical.created_at() != event.created_at_u64()
+ || canonical.kind() != event.kind_u32()
+ {
+ return Err(projection_drift(
+ "canonical event identity disagrees with its verified raw JSON",
+ ));
+ }
+ let stored = RadrootsStoredFoodAvailabilityV1::from_projection(
+ transition.source_generation(),
+ canonical.pubkey().clone(),
+ canonical.event_id().clone(),
+ transition.raw_head().event_seq(),
+ canonical.created_at(),
+ transition.transition_seq(),
+ &projection,
+ )?;
+ persist_projection(connection, &stored).await?;
+ projected_row_delta
+ .checked_add(1)
+ .ok_or_else(|| projection_drift("projection row-count delta overflowed"))
+}
+
+async fn persist_projection(
+ connection: &mut SqliteConnection,
+ projection: &RadrootsStoredFoodAvailabilityV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let diagnostic_codes_json = diagnostic_codes_json(projection.diagnostics())?;
+ let quantity_amount = projection.quantity().map(|quantity| quantity.amount());
+ let quantity_unit = projection
+ .quantity()
+ .map(|quantity| quantity.unit().as_str());
+ let inserted = sqlx::query(
+ "INSERT INTO radroots_event_store_food_availability_projection(source_generation, kind, pubkey, d_tag, event_id, event_seq, created_at, contract_id, content, title, summary, published_at, location, price_amount, price_currency, price_unit, quantity_amount, quantity_unit, status, diagnostic_codes_json, source_transition_seq) VALUES (?, 30402, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ )
+ .bind(projection.source_generation().as_bytes().as_slice())
+ .bind(projection.pubkey().as_str())
+ .bind(projection.identifier().as_str())
+ .bind(projection.event_id().as_str())
+ .bind(projection.event_seq())
+ .bind(i64_from_u64("food.created_at", projection.created_at())?)
+ .bind(FOOD_AVAILABILITY_CONTRACT_ID)
+ .bind(projection.content().as_str())
+ .bind(projection.title().as_str())
+ .bind(projection.summary().as_str())
+ .bind(i64_from_u64(
+ "food.published_at",
+ projection.published_at().as_u64(),
+ )?)
+ .bind(projection.location().as_str())
+ .bind(projection.price().amount())
+ .bind(projection.price().currency().as_str())
+ .bind(projection.price().unit().as_str())
+ .bind(quantity_amount)
+ .bind(quantity_unit)
+ .bind(projection.status().as_str())
+ .bind(diagnostic_codes_json)
+ .bind(projection.source_transition_seq())
+ .execute(&mut *connection)
+ .await?;
+ if inserted.rows_affected() != 1 {
+ return Err(projection_drift(
+ "FoodAvailability projection insert did not affect one row",
+ ));
+ }
+ for image in projection.images() {
+ persist_image(connection, projection, image).await?;
+ }
+ Ok(())
+}
+
+async fn persist_image(
+ connection: &mut SqliteConnection,
+ projection: &RadrootsStoredFoodAvailabilityV1,
+ image: &RadrootsStoredFoodAvailabilityImageV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let raw_tag_json = serde_json::to_string(image.raw_tag())?;
+ let diagnostics_json = diagnostic_codes_json(image.diagnostics())?;
+ let dimensions = image.dimensions();
+ let inserted = sqlx::query(
+ "INSERT INTO radroots_event_store_food_availability_image(source_generation, pubkey, d_tag, image_index, raw_tag_json, url, width, height, blossom_sha256, qualifies, diagnostic_codes_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ )
+ .bind(projection.source_generation().as_bytes().as_slice())
+ .bind(projection.pubkey().as_str())
+ .bind(projection.identifier().as_str())
+ .bind(i64::from(image.image_index()))
+ .bind(raw_tag_json)
+ .bind(image.url())
+ .bind(dimensions.map(|value| i64::from(value.width())))
+ .bind(dimensions.map(|value| i64::from(value.height())))
+ .bind(image.blossom_sha256().map(|digest| digest.to_string()))
+ .bind(i64::from(image.qualifies()))
+ .bind(diagnostics_json)
+ .execute(&mut *connection)
+ .await?;
+ if inserted.rows_affected() != 1 {
+ return Err(projection_drift(
+ "FoodAvailability image insert did not affect one row",
+ ));
+ }
+ Ok(())
+}
+
+pub(crate) async fn validate_food_availability_projection_hook_v1(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ let state = food_availability_projection_cursor_state_fast_v1(connection).await?;
+ let generation = state.feed_cursor.source_generation();
+
+ let rows = sqlx::query(
+ "SELECT source_generation, pubkey, d_tag, event_id, event_seq, created_at, contract_id, content, title, summary, published_at, location, price_amount, price_currency, price_unit, quantity_amount, quantity_unit, status, diagnostic_codes_json, source_transition_seq, immutable_raw_json, stored_images_json FROM radroots_event_store_food_availability_read_v1 WHERE source_generation = ? ORDER BY pubkey, d_tag",
+ )
+ .bind(generation.as_bytes().as_slice())
+ .fetch_all(&mut *connection)
+ .await?;
+ let mut actual_coordinates = Vec::with_capacity(rows.len());
+ for row in rows {
+ let projection = load_and_validate_projection_row(row)?;
+ validate_projection_source_transition(connection, &projection).await?;
+ validate_fts_row(connection, &projection).await?;
+ actual_coordinates.push((
+ projection.pubkey().as_str().to_owned(),
+ projection.identifier().as_str().to_owned(),
+ projection.event_id().as_str().to_owned(),
+ projection.event_seq(),
+ i64_from_u64("food.created_at", projection.created_at())?,
+ ));
+ }
+ let actual_row_count = i64::try_from(actual_coordinates.len())
+ .map_err(|_| projection_drift("projection row count exceeds i64"))?;
+ if actual_row_count != state.projected_row_count {
+ return Err(projection_drift(format!(
+ "projection row count {} differs from sealed count {}",
+ actual_row_count, state.projected_row_count,
+ )));
+ }
+ let expected_coordinates = sqlx::query(
+ "SELECT pubkey, d_tag, raw_head_event_id, raw_head_event_seq, raw_head_created_at FROM radroots_event_store_addressable_head_state WHERE source_generation = ? AND kind = 30402 AND admission_status = 'admitted' AND admission_code IS NULL AND contract_id = ? AND visibility = 'visible' AND nip09_outcome = 'visible' ORDER BY pubkey, d_tag",
+ )
+ .bind(generation.as_bytes().as_slice())
+ .bind(FOOD_AVAILABILITY_CONTRACT_ID)
+ .fetch_all(&mut *connection)
+ .await?
+ .into_iter()
+ .map(|row| {
+ Ok::<_, sqlx::Error>((
+ row.try_get::<String, _>("pubkey")?,
+ row.try_get::<String, _>("d_tag")?,
+ row.try_get::<String, _>("raw_head_event_id")?,
+ row.try_get::<i64, _>("raw_head_event_seq")?,
+ row.try_get::<i64, _>("raw_head_created_at")?,
+ ))
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+ if actual_coordinates != expected_coordinates {
+ return Err(projection_drift(
+ "projection coordinate witnesses do not equal the current admitted, visible FoodAvailability heads",
+ ));
+ }
+ let fts_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_food_availability_search_fts",
+ )
+ .fetch_one(&mut *connection)
+ .await?;
+ if fts_count != state.projected_row_count {
+ return Err(projection_drift(format!(
+ "FoodAvailability FTS row count {fts_count} differs from sealed count {}",
+ state.projected_row_count,
+ )));
+ }
+ #[cfg(test)]
+ wait_at_food_availability_audit_fts_checkpoint().await;
+ sqlx::query(
+ "INSERT INTO radroots_event_store_food_availability_search_fts(radroots_event_store_food_availability_search_fts) VALUES('integrity-check')",
+ )
+ .execute(&mut *connection)
+ .await
+ .map_err(|error| projection_drift(format!("FoodAvailability FTS integrity check failed: {error}")))?;
+ Ok(())
+}
+
+async fn validate_projection_source_transition(
+ connection: &mut SqliteConnection,
+ projection: &RadrootsStoredFoodAvailabilityV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let authoritative: i64 = sqlx::query_scalar(
+ "SELECT EXISTS(SELECT 1 FROM radroots_event_store_addressable_head_transition AS transition WHERE transition.transition_seq = ? AND transition.source_generation = ? AND transition.source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1) AND transition.kind = 30402 AND transition.pubkey = ? AND transition.d_tag = ? AND transition.raw_head_event_id = ? AND transition.raw_head_event_seq = ? AND transition.raw_head_created_at = ? AND transition.visible_event_id = ? AND transition.visible_event_seq = ? AND transition.admission_status = 'admitted' AND transition.admission_code IS NULL AND transition.contract_id = ? AND transition.visibility = 'visible' AND transition.nip09_outcome = 'visible' AND transition.raw_head_decision IN ('baseline_rebuild', 'applied') AND transition.transition_seq = (SELECT MAX(candidate.transition_seq) FROM radroots_event_store_addressable_head_transition AS candidate WHERE candidate.source_generation = transition.source_generation AND candidate.kind = transition.kind AND candidate.pubkey = transition.pubkey AND candidate.d_tag = transition.d_tag AND candidate.raw_head_decision IN ('baseline_rebuild', 'applied')))",
+ )
+ .bind(projection.source_transition_seq())
+ .bind(projection.source_generation().as_bytes().as_slice())
+ .bind(projection.pubkey().as_str())
+ .bind(projection.identifier().as_str())
+ .bind(projection.event_id().as_str())
+ .bind(projection.event_seq())
+ .bind(i64_from_u64("food.created_at", projection.created_at())?)
+ .bind(projection.event_id().as_str())
+ .bind(projection.event_seq())
+ .bind(FOOD_AVAILABILITY_CONTRACT_ID)
+ .fetch_one(&mut *connection)
+ .await?;
+ if authoritative != 1 {
+ return Err(projection_drift(
+ "stored FoodAvailability source transition is not authoritative for its projection",
+ ));
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+async fn wait_at_food_availability_audit_fts_checkpoint() {
+ let release = FOOD_AVAILABILITY_AUDIT_FTS_CHECKPOINT
+ .try_with(|(reached, release)| {
+ reached.notify_one();
+ Arc::clone(release)
+ })
+ .ok();
+ if let Some(release) = release {
+ release.notified().await;
+ }
+}
+
+pub(crate) async fn validate_food_availability_projection_hook_state_fast_v1(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ food_availability_projection_cursor_state_fast_v1(connection)
+ .await
+ .map(|_| ())
+}
+
+async fn food_availability_projection_cursor_state_fast_v1(
+ connection: &mut SqliteConnection,
+) -> Result<FoodAvailabilityProjectionCursorState, RadrootsEventStoreError> {
+ let scope = RadrootsAddressableTransitionScopeV1::food_availability();
+ let row = sqlx::query(
+ "SELECT source.active_generation, source.last_transition_seq AS source_high_water, generation.transition_floor_seq AS generation_floor, generation.addressable_feed_version, integrity.transition_floor_seq AS integrity_floor, integrity.last_transition_seq AS integrity_high_water, integrity.transition_count, cursor.source_generation, cursor.feed_version, cursor.projection_version, cursor.scope_fingerprint, cursor.hook_manifest_sha256, cursor.last_transition_seq, cursor.projected_row_count FROM radroots_event_store_source_state AS source JOIN radroots_event_store_source_generation AS generation ON generation.source_generation = source.active_generation JOIN radroots_event_store_addressable_feed_integrity_v1 AS integrity ON integrity.source_generation = source.active_generation JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.singleton = 1 WHERE source.singleton = 1",
+ )
+ .fetch_optional(&mut *connection)
+ .await?
+ .ok_or_else(|| projection_drift("active source, feed, or projection seal is missing"))?;
+ let generation = projection_generation_from_blob(
+ row.try_get("active_generation")?,
+ "active source generation is invalid",
+ )?;
+ validate_cursor_identity(&row, generation, &scope)?;
+ let source_high_water: i64 = row.try_get("source_high_water")?;
+ let generation_floor: i64 = row.try_get("generation_floor")?;
+ let integrity_floor: i64 = row.try_get("integrity_floor")?;
+ let integrity_high_water: i64 = row.try_get("integrity_high_water")?;
+ let transition_count: i64 = row.try_get("transition_count")?;
+ let cursor_high_water: i64 = row.try_get("last_transition_seq")?;
+ let expected_transition_count = source_high_water
+ .checked_sub(generation_floor)
+ .filter(|count| *count >= 0)
+ .ok_or_else(|| projection_drift("source high-water precedes its transition floor"))?;
+ if row.try_get::<i64, _>("addressable_feed_version")?
+ != i64::from(RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1)
+ || integrity_floor != generation_floor
+ || integrity_high_water != source_high_water
+ || transition_count != expected_transition_count
+ {
+ return Err(projection_drift(
+ "active addressable feed integrity seal is inconsistent",
+ ));
+ }
+ if cursor_high_water != source_high_water {
+ return Err(projection_drift(
+ "projection cursor is not at the source high-water",
+ ));
+ }
+ let projected_row_count: i64 = row.try_get("projected_row_count")?;
+ validate_projected_row_count(projected_row_count)?;
+ Ok(FoodAvailabilityProjectionCursorState {
+ feed_cursor: RadrootsAddressableTransitionCursorV1::new(
+ generation,
+ scope.fingerprint(),
+ cursor_high_water,
+ )?,
+ projected_row_count,
+ })
+}
+
+fn load_and_validate_projection_rows(
+ rows: Vec<sqlx::sqlite::SqliteRow>,
+) -> Result<Vec<RadrootsStoredFoodAvailabilityV1>, RadrootsEventStoreError> {
+ let mut result = Vec::with_capacity(rows.len());
+ for row in rows {
+ result.push(load_and_validate_projection_row(row)?);
+ }
+ Ok(result)
+}
+
+fn load_and_validate_projection_row(
+ row: sqlx::sqlite::SqliteRow,
+) -> Result<RadrootsStoredFoodAvailabilityV1, RadrootsEventStoreError> {
+ let generation = projection_generation_from_blob(
+ row.try_get("source_generation")?,
+ "stored projection generation is invalid",
+ )?;
+ let pubkey = RadrootsPublicKey::parse(row.try_get::<String, _>("pubkey")?.as_str())
+ .map_err(|error| projection_drift(format!("stored pubkey is invalid: {error}")))?;
+ let event_id = RadrootsEventId::parse(row.try_get::<String, _>("event_id")?.as_str())
+ .map_err(|error| projection_drift(format!("stored event id is invalid: {error}")))?;
+ let event_seq: i64 = row.try_get("event_seq")?;
+ let created_at = u64_from_i64("food.created_at", row.try_get("created_at")?)
+ .map_err(|error| projection_drift(error.to_string()))?;
+ let transition_seq: i64 = row.try_get("source_transition_seq")?;
+ let raw_json: String = row.try_get("immutable_raw_json")?;
+ let ingest = RadrootsEventIngest::from_raw_json(raw_json, 0)
+ .map_err(|error| projection_drift(format!("projected event reverify failed: {error}")))?;
+ if ingest.event().id_str() != event_id.as_str()
+ || ingest.event().author_str() != pubkey.as_str()
+ || ingest.event().created_at_u64() != created_at
+ || ingest.event().kind_u32() != 30_402
+ {
+ return Err(projection_drift(
+ "projection identity disagrees with immutable signed event",
+ ));
+ }
+ let admission = EventAdmission::for_profile(
+ ReconciliationProfile::Nip09V1RegistryV7,
+ ingest.verified_event(),
+ )
+ .map_err(|error| projection_drift(format!("stored admission is invalid: {error}")))?;
+ if admission.status != RadrootsEventAdmissionStatus::Admitted
+ || admission.contract.map(|contract| contract.id) != Some(FOOD_AVAILABILITY_CONTRACT_ID)
+ {
+ return Err(projection_drift(
+ "projected event is not registry-v7 FoodAvailability",
+ ));
+ }
+ let focused =
+ match project_verified_food_availability_event_registry_v7(ingest.verified_event())
+ .map_err(|error| projection_drift(format!("stored projection failed: {error}")))?
+ {
+ RadrootsFoodAvailabilityProjectionOutcome::Focused(projection) => projection,
+ RadrootsFoodAvailabilityProjectionOutcome::Excluded(_) => {
+ return Err(projection_drift(
+ "stored FoodAvailability event is excluded",
+ ));
+ }
+ _ => {
+ return Err(projection_drift(
+ "stored FoodAvailability event returned an unsupported projection outcome",
+ ));
+ }
+ };
+ let expected = RadrootsStoredFoodAvailabilityV1::from_projection(
+ generation,
+ pubkey,
+ event_id,
+ event_seq,
+ created_at,
+ transition_seq,
+ &focused,
+ )?;
+ validate_projection_columns(&row, &expected)?;
+ validate_image_rows(row.try_get("stored_images_json")?, &expected)?;
+ Ok(expected)
+}
+
+fn validate_projection_columns(
+ row: &sqlx::sqlite::SqliteRow,
+ expected: &RadrootsStoredFoodAvailabilityV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let expected_diagnostics = diagnostic_codes_json(expected.diagnostics())?;
+ let quantity_amount = expected.quantity().map(|quantity| quantity.amount());
+ let quantity_unit = expected.quantity().map(|quantity| quantity.unit().as_str());
+ if row.try_get::<String, _>("d_tag")? != expected.identifier().as_str()
+ || row.try_get::<String, _>("contract_id")? != FOOD_AVAILABILITY_CONTRACT_ID
+ || row.try_get::<String, _>("content")? != expected.content().as_str()
+ || row.try_get::<String, _>("title")? != expected.title().as_str()
+ || row.try_get::<String, _>("summary")? != expected.summary().as_str()
+ || u64_from_i64("food.published_at", row.try_get("published_at")?)
+ .map_err(|error| projection_drift(error.to_string()))?
+ != expected.published_at().as_u64()
+ || row.try_get::<String, _>("location")? != expected.location().as_str()
+ || row.try_get::<String, _>("price_amount")? != expected.price().amount()
+ || row.try_get::<String, _>("price_currency")? != expected.price().currency().as_str()
+ || row.try_get::<String, _>("price_unit")? != expected.price().unit().as_str()
+ || row
+ .try_get::<Option<String>, _>("quantity_amount")?
+ .as_deref()
+ != quantity_amount
+ || row
+ .try_get::<Option<String>, _>("quantity_unit")?
+ .as_deref()
+ != quantity_unit
+ || row.try_get::<String, _>("status")? != expected.status().as_str()
+ || row.try_get::<String, _>("diagnostic_codes_json")? != expected_diagnostics
+ {
+ return Err(projection_drift(
+ "stored FoodAvailability columns differ from registry-v7 reprojection",
+ ));
+ }
+ Ok(())
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct StoredFoodAvailabilityImageRowV1 {
+ image_index: i64,
+ raw_tag_json: String,
+ url: Option<String>,
+ width: Option<i64>,
+ height: Option<i64>,
+ blossom_sha256: Option<String>,
+ qualifies: i64,
+ diagnostic_codes_json: String,
+}
+
+fn validate_image_rows(
+ stored_images_json: String,
+ expected: &RadrootsStoredFoodAvailabilityV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let rows: Vec<StoredFoodAvailabilityImageRowV1> =
+ serde_json::from_str(stored_images_json.as_str())
+ .map_err(|error| projection_drift(format!("stored image rows are invalid: {error}")))?;
+ if rows.len() != expected.images().len() {
+ return Err(projection_drift(
+ "stored FoodAvailability image count differs",
+ ));
+ }
+ for (row, image) in rows.into_iter().zip(expected.images()) {
+ let dimensions = image.dimensions();
+ let stored_blossom_sha256 = row
+ .blossom_sha256
+ .map(|value| {
+ radroots_blossom::RadrootsBlossomSha256::from_hex(value.as_str()).map_err(|error| {
+ projection_drift(format!("stored Blossom digest is invalid: {error}"))
+ })
+ })
+ .transpose()?;
+ if row.image_index != i64::from(image.image_index())
+ || row.raw_tag_json
+ != serde_json::to_string(image.raw_tag()).map_err(|error| {
+ projection_drift(format!("expected image tag is not serializable: {error}"))
+ })?
+ || row.url.as_deref() != image.url()
+ || row.width != dimensions.map(|value| i64::from(value.width()))
+ || row.height != dimensions.map(|value| i64::from(value.height()))
+ || stored_blossom_sha256 != image.blossom_sha256()
+ || bool_from_i64("food.image.qualifies", row.qualifies)
+ .map_err(|error| projection_drift(error.to_string()))?
+ != image.qualifies()
+ || row.diagnostic_codes_json != diagnostic_codes_json(image.diagnostics())?
+ {
+ return Err(projection_drift(
+ "stored FoodAvailability image differs from registry-v7 reprojection",
+ ));
+ }
+ }
+ Ok(())
+}
+
+async fn validate_fts_row(
+ connection: &mut SqliteConnection,
+ projection: &RadrootsStoredFoodAvailabilityV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let row = sqlx::query(
+ "SELECT event_id, pubkey, d_tag, title, summary, content, location FROM radroots_event_store_food_availability_search_fts WHERE rowid = ?",
+ )
+ .bind(projection.event_seq())
+ .fetch_optional(&mut *connection)
+ .await?
+ .ok_or_else(|| projection_drift("FoodAvailability FTS row is missing"))?;
+ if row.try_get::<String, _>("event_id")? != projection.event_id().as_str()
+ || row.try_get::<String, _>("pubkey")? != projection.pubkey().as_str()
+ || row.try_get::<String, _>("d_tag")? != projection.identifier().as_str()
+ || row.try_get::<String, _>("title")? != projection.title().as_str()
+ || row.try_get::<String, _>("summary")? != projection.summary().as_str()
+ || row.try_get::<String, _>("content")? != projection.content().as_str()
+ || row.try_get::<String, _>("location")? != projection.location().as_str()
+ {
+ return Err(projection_drift(
+ "FoodAvailability FTS row differs from projection",
+ ));
+ }
+ Ok(())
+}
+
+fn diagnostic_codes_json(
+ diagnostics: &[RadrootsFoodAvailabilityImageDiagnostic],
+) -> Result<String, RadrootsEventStoreError> {
+ serde_json::to_string(
+ &diagnostics
+ .iter()
+ .map(|diagnostic| diagnostic.code())
+ .collect::<Vec<_>>(),
+ )
+ .map_err(|error| projection_drift(format!("diagnostics are not serializable: {error}")))
+}
+
+fn validate_query_limit(limit: u32) -> Result<(), RadrootsEventStoreError> {
+ if !(1..=RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX).contains(&limit) {
+ return Err(RadrootsEventStoreError::QueryLimitOutOfRange {
+ min: 1,
+ max: RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX,
+ actual: limit,
+ });
+ }
+ Ok(())
+}
+
+fn projection_drift(reason: impl Into<String>) -> RadrootsEventStoreError {
+ RadrootsEventStoreError::FoodAvailabilityProjectionDrift {
+ reason: reason.into(),
+ }
+}
+
+fn validate_projected_row_count(value: i64) -> Result<(), RadrootsEventStoreError> {
+ if value < 0 {
+ return Err(projection_drift(format!(
+ "projection cursor has negative row count {value}",
+ )));
+ }
+ Ok(())
+}
+
+fn projection_generation_from_blob(
+ value: Vec<u8>,
+ context: &'static str,
+) -> Result<RadrootsEventStoreSourceGeneration, RadrootsEventStoreError> {
+ generation_from_blob(value).map_err(|error| projection_drift(format!("{context}: {error}")))
+}
+
+fn i64_from_u64(field: &'static str, value: u64) -> Result<i64, RadrootsEventStoreError> {
+ i64::try_from(value).map_err(|_| RadrootsEventStoreError::UnsignedIntegerRange { field, value })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn projection_generation_corruption_uses_the_projection_error_surface() {
+ assert!(matches!(
+ projection_generation_from_blob(
+ vec![0_u8; 31],
+ "stored projection generation is invalid",
+ ),
+ Err(RadrootsEventStoreError::FoodAvailabilityProjectionDrift { reason })
+ if reason.contains("stored projection generation is invalid")
+ && reason.contains("31 bytes instead of 32")
+ ));
+ }
+}
diff --git a/crates/event_store/src/store/post_core_extension_capabilities.rs b/crates/event_store/src/store/post_core_extension_capabilities.rs
@@ -1,5 +1,7 @@
use super::post_core_extensions_v1::apply_post_core_extensions_v1;
+use super::post_core_extensions_v2::apply_post_core_extensions_v2;
use super::post_core_storage_v1::PostCoreStorageV1;
+use super::post_core_storage_v2::PostCoreStorageV2;
use super::protocol_reconciliation_v1::ProtocolReconciliationV1IngestResult;
use crate::error::RadrootsEventStoreError;
use crate::model::RadrootsEventIngest;
@@ -22,4 +24,9 @@ impl<'borrow, 'db> PostCoreExtensionCapabilities<'borrow, 'db> {
let mut storage = PostCoreStorageV1::new(self.tx);
apply_post_core_extensions_v1(&mut storage, ingest, result).await
}
+
+ pub(super) async fn apply_v2(&mut self) -> Result<(), RadrootsEventStoreError> {
+ let mut storage = PostCoreStorageV2::new(self.tx);
+ apply_post_core_extensions_v2(&mut storage).await
+ }
}
diff --git a/crates/event_store/src/store/post_core_extension_dispatcher.rs b/crates/event_store/src/store/post_core_extension_dispatcher.rs
@@ -9,5 +9,6 @@ pub(super) async fn dispatch_post_core_extensions(
result: &ProtocolReconciliationV1IngestResult,
) -> Result<(), RadrootsEventStoreError> {
capabilities.apply_v1(ingest, result).await?;
+ capabilities.apply_v2().await?;
Ok(())
}
diff --git a/crates/event_store/src/store/post_core_extensions_v2.rs b/crates/event_store/src/store/post_core_extensions_v2.rs
@@ -0,0 +1,8 @@
+use super::post_core_storage_v2::PostCoreStorageV2;
+use crate::RadrootsEventStoreError;
+
+pub(super) async fn apply_post_core_extensions_v2(
+ storage: &mut PostCoreStorageV2<'_, '_>,
+) -> Result<(), RadrootsEventStoreError> {
+ storage.apply_pending_food_availability_transitions().await
+}
diff --git a/crates/event_store/src/store/post_core_storage_v2.rs b/crates/event_store/src/store/post_core_storage_v2.rs
@@ -0,0 +1,19 @@
+use super::food_availability_projection_v1::apply_pending_food_availability_transitions_v1;
+use crate::RadrootsEventStoreError;
+use sqlx::{Sqlite, Transaction};
+
+pub(super) struct PostCoreStorageV2<'borrow, 'db> {
+ tx: &'borrow mut Transaction<'db, Sqlite>,
+}
+
+impl<'borrow, 'db> PostCoreStorageV2<'borrow, 'db> {
+ pub(super) fn new(tx: &'borrow mut Transaction<'db, Sqlite>) -> Self {
+ Self { tx }
+ }
+
+ pub(super) async fn apply_pending_food_availability_transitions(
+ &mut self,
+ ) -> Result<(), RadrootsEventStoreError> {
+ apply_pending_food_availability_transitions_v1(self.tx).await
+ }
+}
diff --git a/crates/event_store/src/store/protocol_storage_v1.rs b/crates/event_store/src/store/protocol_storage_v1.rs
@@ -11,7 +11,6 @@ use sqlx::{Row, Sqlite, Transaction};
pub(super) struct RawHeadSnapshot {
pub(super) raw_head: RadrootsStoredRawEventHead,
- pub(super) raw_event: RadrootsStoredRawEvent,
}
#[cfg_attr(coverage_nightly, coverage(off))]
@@ -130,10 +129,7 @@ pub(super) async fn raw_head_snapshot_in_transaction(
}
let raw_event = stored_raw_event_from_row(row)?;
validate_raw_head_snapshot(coordinate, &raw_head, &raw_event)?;
- Ok(RawHeadSnapshot {
- raw_head,
- raw_event,
- })
+ Ok(RawHeadSnapshot { raw_head })
})
.transpose()
}
diff --git a/crates/event_store/tests/fixtures/food_availability_projection.v1.json b/crates/event_store/tests/fixtures/food_availability_projection.v1.json
@@ -0,0 +1,2889 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_event_store.food_availability_projection_v1",
+ "feed_version": 1,
+ "projection_version": 1,
+ "scope_kinds": [
+ 30402
+ ],
+ "cases": [
+ {
+ "id": "visible_food_availability_projects_and_searches",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "b9d0da50b69689fdd71adc0d698b3e2d04e53c94ec31e23496b4d2fdb96bc1719c5d626881f407682f287f2a5d2bc57159f70a8cd3d1aaae96bd1394c5b1ea0a"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "content": "Carrots available this week.",
+ "title": "Nantes Carrots",
+ "summary": "Fresh bunches",
+ "published_at": 1700000000,
+ "location": "Central Saanich, BC",
+ "price_amount": "3",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": null,
+ "quantity_unit": null,
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "Fresh",
+ "event_ids": [
+ "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7"
+ ]
+ },
+ {
+ "query": "Nantes Saanich",
+ "event_ids": [
+ "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 1,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 1
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "817789914f92ad1401bb128f2c176f725143cbbc10f0d1b97fa4b31d5d1ab05f",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": []
+ }
+ },
+ {
+ "id": "invalid_same_timestamp_winner_retracts_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "b9d0da50b69689fdd71adc0d698b3e2d04e53c94ec31e23496b4d2fdb96bc1719c5d626881f407682f287f2a5d2bc57159f70a8cd3d1aaae96bd1394c5b1ea0a"
+ }
+ },
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060001,
+ "expected_ingest": {
+ "admission_status": "invalid",
+ "admission_code": "food_status_invalid",
+ "contract_id": null,
+ "event_class": "addressable",
+ "valid_stream_eligible": false,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "paused"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "5728cd88796dc4beed1d293db77a9e5a9297bb9e3d7d56b9ee108f3c6d3ad2fd5468b359d1fba09a4eeca7539d678d2e373c8bd151969e19feaceb9aebaca917"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": null,
+ "searches": [
+ {
+ "query": "Fresh",
+ "event_ids": []
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 2,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 2
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "817789914f92ad1401bb128f2c176f725143cbbc10f0d1b97fa4b31d5d1ab05f",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "event_seq": 2
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "invalid",
+ "admission_code": "food_status_invalid",
+ "contract_id": null,
+ "visibility": "not_admitted",
+ "suppression": null,
+ "cause_event": {
+ "event": {
+ "event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "event_seq": 2
+ },
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "invalid",
+ "admission_code": "food_status_invalid",
+ "contract_id": null
+ },
+ "canonical_visible_event": null,
+ "retracted_event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "invalid",
+ "decision": "not_admitted",
+ "is_raw_head": true,
+ "raw_head_event_id": "22e22dba9ea96a763a296633438a2b7a18b0358324677519cc16636d280518b5",
+ "suppression": null
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "794251d2341134908ee28526447cb8a54bdd608917166fd3678f65e578fbb8c7",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ },
+ {
+ "id": "blossom_digest_and_image_diagnostics_are_preserved",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "image",
+ "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp"
+ ],
+ [
+ "image",
+ "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp",
+ "0x600",
+ "extra"
+ ],
+ [
+ "image",
+ "not-a-url",
+ "800x600"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "1d5eb699944bc1eba93c85071ad1d94878f1c767015391e200b45f1a2066558675a382ede737fb15307a944598687843618c067accf6608f53daad377683c2c9"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "content": "Carrots available this week.",
+ "title": "Nantes Carrots",
+ "summary": "Fresh bunches",
+ "published_at": 1700000000,
+ "location": "Central Saanich, BC",
+ "price_amount": "3",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": null,
+ "quantity_unit": null,
+ "status": "active",
+ "diagnostics": [
+ "food_image_shape_invalid",
+ "food_image_dimensions_missing",
+ "food_image_shape_invalid",
+ "food_image_dimensions_invalid",
+ "food_image_duplicate_url",
+ "food_image_duplicate_digest",
+ "food_image_url_invalid"
+ ],
+ "images": [
+ {
+ "url": "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp",
+ "width": null,
+ "height": null,
+ "blossom_sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
+ "diagnostics": [
+ "food_image_shape_invalid",
+ "food_image_dimensions_missing"
+ ],
+ "qualifies": false
+ },
+ {
+ "url": "https://media.example/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824.webp",
+ "width": null,
+ "height": null,
+ "blossom_sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
+ "diagnostics": [
+ "food_image_shape_invalid",
+ "food_image_dimensions_invalid",
+ "food_image_duplicate_url",
+ "food_image_duplicate_digest"
+ ],
+ "qualifies": false
+ },
+ {
+ "url": null,
+ "width": 800,
+ "height": 600,
+ "blossom_sha256": null,
+ "diagnostics": [
+ "food_image_url_invalid"
+ ],
+ "qualifies": false
+ }
+ ]
+ },
+ "searches": [
+ {
+ "query": "Carrots",
+ "event_ids": [
+ "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 1,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 1
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "event_seq": 1
+ },
+ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "090b5663c36c2d7ee1361866ec6314d64e9c3ba386d430e1265aa4bb4c563884",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "96b4a10932919010172a00667ccebb8684a89e7cc3c4c51202d2870fd8b5acfe",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": []
+ }
+ },
+ {
+ "id": "authorized_address_deletion_retracts_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "causal",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1",
+ "event_class": "regular",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "not_head_selected"
+ },
+ "event": {
+ "id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 5,
+ "tags": [
+ [
+ "a",
+ "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:nantes-carrots"
+ ]
+ ],
+ "content": "Harvest listing withdrawn.",
+ "sig": "d360cbde755ad9c04bcc8a82c5d144bbb98bb1366304f0162544622ab648267b0e8e4e8d35baffe98c0cac7dc67e0c1a44a7ae172db49f71641291294759da28"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": null,
+ "searches": [
+ {
+ "query": "Fresh",
+ "event_ids": []
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 2,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 2
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "suppressed",
+ "suppression": {
+ "outcome": "suppressed",
+ "reason": "deletion_address_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "event_seq": 2
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 5,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1"
+ },
+ "canonical_visible_event": null,
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "not_head_selected"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "suppressed",
+ "is_raw_head": true,
+ "raw_head_event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "suppression": {
+ "outcome": "suppressed",
+ "reason": "deletion_address_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ }
+ },
+ {
+ "event": {
+ "event_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": null,
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_immune",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "suppressed"
+ }
+ ]
+ }
+ },
+ {
+ "id": "wrong_author_address_deletion_preserves_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "causal",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1",
+ "event_class": "regular",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "not_head_selected"
+ },
+ "event": {
+ "id": "51fa76379d859f70a30f4e44b97373c35bf86cc1efdce2bf23ed0e8605387535",
+ "pubkey": "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af",
+ "created_at": 1700000070,
+ "kind": 5,
+ "tags": [
+ [
+ "a",
+ "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:nantes-carrots"
+ ]
+ ],
+ "content": "Unauthorized withdrawal attempt.",
+ "sig": "58e88c1dcd442c6336a090085d8945cb935aff84f859e8132fa9fd4d596ae746c366b56e4a73f23490e550b6d0b0ba60489e1aa3130e7018b4397d4dab3039d2"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "content": "Carrots available this week.",
+ "title": "Nantes Carrots",
+ "summary": "Fresh bunches",
+ "published_at": 1700000000,
+ "location": "Central Saanich, BC",
+ "price_amount": "3",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": null,
+ "quantity_unit": null,
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "Fresh",
+ "event_ids": [
+ "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 2,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 2
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_author_mismatch",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "51fa76379d859f70a30f4e44b97373c35bf86cc1efdce2bf23ed0e8605387535",
+ "event_seq": 2
+ },
+ "pubkey": "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af",
+ "created_at": 1700000070,
+ "kind": 5,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "not_head_selected"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_author_mismatch",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "51fa76379d859f70a30f4e44b97373c35bf86cc1efdce2bf23ed0e8605387535",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": null,
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_immune",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": []
+ }
+ },
+ {
+ "id": "post_cutoff_replacement_restores_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "causal",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1",
+ "event_class": "regular",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "not_head_selected"
+ },
+ "event": {
+ "id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 5,
+ "tags": [
+ [
+ "a",
+ "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:nantes-carrots"
+ ]
+ ],
+ "content": "Harvest listing withdrawn.",
+ "sig": "d360cbde755ad9c04bcc8a82c5d144bbb98bb1366304f0162544622ab648267b0e8e4e8d35baffe98c0cac7dc67e0c1a44a7ae172db49f71641291294759da28"
+ }
+ },
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000080000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "New harvest bunches"
+ ],
+ [
+ "published_at",
+ "1700000080"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3.5",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "radroots:quantity",
+ "24",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "A new carrot harvest is available.",
+ "sig": "fcfbab5dee277d70fb26740a8b058a884c82ceedc83819ff6920cc42634adfbbbe149c2cc207f2c176436c0cb738c007d3fc378d0d530d09d45d818ecad5be4c"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "content": "A new carrot harvest is available.",
+ "title": "Nantes Carrots",
+ "summary": "New harvest bunches",
+ "published_at": 1700000080,
+ "location": "Central Saanich, BC",
+ "price_amount": "3.5",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": "24",
+ "quantity_unit": "lb",
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "New harvest",
+ "event_ids": [
+ "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 3,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 3
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "suppressed",
+ "suppression": {
+ "outcome": "suppressed",
+ "reason": "deletion_address_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "event_seq": 2
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 5,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.social.deletion_request.v1"
+ },
+ "canonical_visible_event": null,
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "not_head_selected"
+ },
+ {
+ "transition_seq": 3,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_head_created_at": 1700000080,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_address_cutoff_precedes_target",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_json_sha256": "465617b3c73ea58efd9351aa162540614ac50c92115d599a582fec4b98b5e195",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "suppressed",
+ "reason": "deletion_address_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ }
+ },
+ {
+ "event": {
+ "event_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": null,
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_request_immune",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_address_cutoff_precedes_target",
+ "event_reference_request_id": null,
+ "address_reference_request_id": "5ae5794dfca7f049803d19ecb28fe9378622b408715b8aca1ebd40e494bf4462",
+ "address_reference_cutoff": 1700000070
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ },
+ {
+ "id": "operational_listing_head_retracts_food_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "scoped_non_food",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "p",
+ "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"
+ ],
+ [
+ "a",
+ "30340:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000070"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "inventory",
+ "24"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "delivery",
+ "pickup"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "fd70b10cd97d71e49a622482edcc6d4a1ab065fa2d614ca848187fd4c00a8ea3c46c414a96d26b75f0244f03a0cfd0b94495d3ef46cedeec05d1cd84e8864bbb"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": null,
+ "searches": [
+ {
+ "query": "Nantes",
+ "event_ids": []
+ },
+ {
+ "query": "Carrots available",
+ "event_ids": []
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 2,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 2
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_head_created_at": 1700000070,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_json_sha256": "19a34cd6af7a207c60b1c3e0c9b4f58e9cb7af2a8c26f323e7570298992833cb",
+ "admission_status": "admitted",
+ "contract_id": "radroots.operational_listing.published.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ },
+ {
+ "id": "food_head_after_operational_listing_restores_projection",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "scoped_non_food",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "p",
+ "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"
+ ],
+ [
+ "a",
+ "30340:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000070"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "inventory",
+ "24"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "delivery",
+ "pickup"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "fd70b10cd97d71e49a622482edcc6d4a1ab065fa2d614ca848187fd4c00a8ea3c46c414a96d26b75f0244f03a0cfd0b94495d3ef46cedeec05d1cd84e8864bbb"
+ }
+ },
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000080000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "New harvest bunches"
+ ],
+ [
+ "published_at",
+ "1700000080"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3.5",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "radroots:quantity",
+ "24",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "A new carrot harvest is available.",
+ "sig": "fcfbab5dee277d70fb26740a8b058a884c82ceedc83819ff6920cc42634adfbbbe149c2cc207f2c176436c0cb738c007d3fc378d0d530d09d45d818ecad5be4c"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "content": "A new carrot harvest is available.",
+ "title": "Nantes Carrots",
+ "summary": "New harvest bunches",
+ "published_at": 1700000080,
+ "location": "Central Saanich, BC",
+ "price_amount": "3.5",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": "24",
+ "quantity_unit": "lb",
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "New harvest",
+ "event_ids": [
+ "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 3,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 3
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 2,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_head_created_at": 1700000070,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.operational_listing.published.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_json_sha256": "19a34cd6af7a207c60b1c3e0c9b4f58e9cb7af2a8c26f323e7570298992833cb",
+ "admission_status": "admitted",
+ "contract_id": "radroots.operational_listing.published.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 3,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_head_created_at": 1700000080,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_json_sha256": "465617b3c73ea58efd9351aa162540614ac50c92115d599a582fec4b98b5e195",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "not_current"
+ },
+ {
+ "transition_seq": 2,
+ "event_id": "32a6dfe2df2ca1d31f73c6c938c7de20261d7b690c96707a5d9d1d211bc19b63",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ },
+ {
+ "id": "food_feed_cursor_advances_across_unrelated_addressable_traffic",
+ "events": [
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000060000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "Fresh bunches"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "Carrots available this week.",
+ "sig": "06769405be8fbff812981a74d8fe8f100d110b29f2d356244423665c920e6654a9d4f07e40120a992ddb391ef266ba3c78d500a2e83f66f2660288000aa2ced7"
+ }
+ },
+ {
+ "role": "unrelated_addressable",
+ "observed_at_ms": 1700000070000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.farm.profile.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "384e9060b4bbc919ca2ea75a47241078a5ca911c18450d62081232e10b66fea4",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000070,
+ "kind": 30340,
+ "tags": [
+ [
+ "d",
+ "victoria-farm-traffic"
+ ]
+ ],
+ "content": "{}",
+ "sig": "7987bef7ebd6c23b6d8a1435600ded7804acaf4caa28a054e977da27c10b1493101d822eb0a113872b7bb07fc5632dbec0e5205f805b0e6d240da30b9c08d862"
+ }
+ },
+ {
+ "role": "scoped_food",
+ "observed_at_ms": 1700000080000,
+ "expected_ingest": {
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true,
+ "raw_head_decision": "applied"
+ },
+ "event": {
+ "id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "tags": [
+ [
+ "d",
+ "nantes-carrots"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "summary",
+ "New harvest bunches"
+ ],
+ [
+ "published_at",
+ "1700000080"
+ ],
+ [
+ "location",
+ "Central Saanich, BC"
+ ],
+ [
+ "price",
+ "3.5",
+ "CAD"
+ ],
+ [
+ "radroots:price_unit",
+ "lb"
+ ],
+ [
+ "radroots:quantity",
+ "24",
+ "lb"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "t",
+ "vegetables"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ],
+ "content": "A new carrot harvest is available.",
+ "sig": "fcfbab5dee277d70fb26740a8b058a884c82ceedc83819ff6920cc42634adfbbbe149c2cc207f2c176436c0cb738c007d3fc378d0d530d09d45d818ecad5be4c"
+ }
+ }
+ ],
+ "expected": {
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "projection": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "content": "A new carrot harvest is available.",
+ "title": "Nantes Carrots",
+ "summary": "New harvest bunches",
+ "published_at": 1700000080,
+ "location": "Central Saanich, BC",
+ "price_amount": "3.5",
+ "price_currency": "CAD",
+ "price_unit": "lb",
+ "quantity_amount": "24",
+ "quantity_unit": "lb",
+ "status": "active",
+ "diagnostics": [],
+ "images": []
+ },
+ "searches": [
+ {
+ "query": "New harvest",
+ "event_ids": [
+ "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d"
+ ]
+ }
+ ],
+ "transition_page": {
+ "source_high_water": 3,
+ "has_more": false,
+ "next_cursor": {
+ "source_generation": "active",
+ "feed_version": 1,
+ "scope_fingerprint": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "last_transition_seq": 3
+ },
+ "transitions": [
+ {
+ "transition_seq": 1,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_created_at": 1700000060,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000060,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_json_sha256": "4b625225135be45fee6a5e35580124b1d8e04a378faa3309a69482e2f6654dee",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": null,
+ "raw_head_decision": "applied"
+ },
+ {
+ "transition_seq": 3,
+ "source_generation": "active",
+ "origin": "incremental",
+ "coordinate": {
+ "kind": 30402,
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "nantes-carrots"
+ },
+ "raw_head": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_head_created_at": 1700000080,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1",
+ "visibility": "visible",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ },
+ "cause_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000080,
+ "kind": 30402,
+ "admission_status": "admitted",
+ "admission_code": null,
+ "contract_id": "radroots.food.availability.v1"
+ },
+ "canonical_visible_event": {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "raw_json_sha256": "465617b3c73ea58efd9351aa162540614ac50c92115d599a582fec4b98b5e195",
+ "admission_status": "admitted",
+ "contract_id": "radroots.food.availability.v1",
+ "event_class": "addressable",
+ "valid_stream_eligible": true
+ },
+ "retracted_event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "raw_head_decision": "applied"
+ }
+ ]
+ },
+ "event_visibility": [
+ {
+ "event": {
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "event_seq": 1
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "not_current",
+ "is_raw_head": false,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "384e9060b4bbc919ca2ea75a47241078a5ca911c18450d62081232e10b66fea4",
+ "event_seq": 2
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "384e9060b4bbc919ca2ea75a47241078a5ca911c18450d62081232e10b66fea4",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ },
+ {
+ "event": {
+ "event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "event_seq": 3
+ },
+ "source_generation": "active",
+ "admission_status": "admitted",
+ "decision": "visible",
+ "is_raw_head": true,
+ "raw_head_event_id": "c131031abade64eed03ab14b4ccabb077d61a3f62f0080e0c5122708cde85d1d",
+ "suppression": {
+ "outcome": "visible",
+ "reason": "deletion_no_authorized_reference",
+ "event_reference_request_id": null,
+ "address_reference_request_id": null,
+ "address_reference_cutoff": null
+ }
+ }
+ ],
+ "historical_visibility_witnesses": [
+ {
+ "transition_seq": 1,
+ "event_id": "500e371ced493d4e7fea6358ad5bf478f874c716d5c9a3fc56f4b4f0118293cd",
+ "final_decision": "not_current"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/crates/event_store/tests/food_availability_projection_v1_result_vector.rs b/crates/event_store/tests/food_availability_projection_v1_result_vector.rs
@@ -0,0 +1,1005 @@
+#![forbid(unsafe_code)]
+
+use radroots_blossom::RadrootsBlossomSha256;
+use radroots_event::{food_availability::RadrootsFoodIdentifier, ids::RadrootsPublicKey};
+use radroots_event_store::{
+ RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1,
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1,
+ RadrootsAddressableTransitionEventReferenceV1, RadrootsAddressableTransitionScopeV1,
+ RadrootsEventIngest, RadrootsEventStore, RadrootsEventStoreSourceGeneration,
+ RadrootsFoodAvailabilitySearchQueryV1, RadrootsFoodAvailabilityStatusFilterV1,
+ RadrootsNip09SuppressionEvidenceV1, RadrootsRawHeadDecision,
+ RadrootsStoreProducedCanonicalEventV1, RadrootsStoredFoodAvailabilityV1,
+ RadrootsStoredRawEvent,
+};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+
+const RESULT_VECTOR_EXECUTOR_ID: &str =
+ "radroots_event_store.food_availability_projection_v1.result_vector_executor.v1";
+const SOURCE_GENERATION_ACTIVE_SENTINEL: &str = "active";
+const RESULT_VECTOR_BYTES: &[u8] = include_bytes!(
+ "../../../contracts/conformance/vectors/event_store/food_availability_projection.v1.json"
+);
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct FoodAvailabilityProjectionVector {
+ schema_version: u32,
+ contract_id: String,
+ feed_version: u32,
+ projection_version: u32,
+ scope_kinds: Vec<u32>,
+ cases: Vec<ProjectionCase>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ProjectionCase {
+ id: String,
+ events: Vec<ObservedEvent>,
+ expected: ExpectedCase,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ObservedEvent {
+ role: ProjectionInputRole,
+ observed_at_ms: i64,
+ expected_ingest: ExpectedIngest,
+ event: SignedEvent,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "snake_case")]
+enum ProjectionInputRole {
+ ScopedFood,
+ ScopedNonFood,
+ UnrelatedAddressable,
+ Causal,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedIngest {
+ admission_status: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ admission_code: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ contract_id: RequiredNullable<String>,
+ event_class: String,
+ valid_stream_eligible: bool,
+ raw_head_decision: String,
+}
+
+#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct SignedEvent {
+ id: String,
+ pubkey: String,
+ created_at: u64,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ sig: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedCase {
+ coordinate: ExpectedCoordinate,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ projection: RequiredNullable<ExpectedProjection>,
+ searches: Vec<ExpectedSearch>,
+ transition_page: ExpectedTransitionPage,
+ event_visibility: Vec<ExpectedVisibility>,
+ historical_visibility_witnesses: Vec<ExpectedHistoricalVisibilityWitness>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedCoordinate {
+ kind: u32,
+ pubkey: String,
+ d_tag: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedProjection {
+ event_id: String,
+ content: String,
+ title: String,
+ summary: String,
+ published_at: u64,
+ location: String,
+ price_amount: String,
+ price_currency: String,
+ price_unit: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ quantity_amount: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ quantity_unit: RequiredNullable<String>,
+ status: String,
+ diagnostics: Vec<String>,
+ images: Vec<ExpectedImage>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedImage {
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ url: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ width: RequiredNullable<u32>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ height: RequiredNullable<u32>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ blossom_sha256: RequiredNullable<String>,
+ diagnostics: Vec<String>,
+ qualifies: bool,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedSearch {
+ query: String,
+ event_ids: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedTransitionPage {
+ source_high_water: i64,
+ has_more: bool,
+ next_cursor: ExpectedTransitionCursor,
+ transitions: Vec<ExpectedTransition>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedTransitionCursor {
+ source_generation: String,
+ feed_version: u32,
+ scope_fingerprint: String,
+ last_transition_seq: i64,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedTransition {
+ transition_seq: i64,
+ source_generation: String,
+ origin: String,
+ coordinate: ExpectedCoordinate,
+ raw_head: ExpectedEventReference,
+ raw_head_created_at: u64,
+ admission_status: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ admission_code: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ contract_id: RequiredNullable<String>,
+ visibility: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ suppression: RequiredNullable<ExpectedSuppressionEvidence>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ cause_event: RequiredNullable<ExpectedTransitionCause>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ canonical_visible_event: RequiredNullable<ExpectedCanonicalVisibleEvent>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ retracted_event: RequiredNullable<ExpectedEventReference>,
+ raw_head_decision: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedEventReference {
+ event_id: String,
+ event_seq: i64,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedSuppressionEvidence {
+ outcome: String,
+ reason: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ event_reference_request_id: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ address_reference_request_id: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ address_reference_cutoff: RequiredNullable<u64>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedTransitionCause {
+ event: ExpectedEventReference,
+ pubkey: String,
+ created_at: u64,
+ kind: u32,
+ admission_status: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ admission_code: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ contract_id: RequiredNullable<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedCanonicalVisibleEvent {
+ event: ExpectedEventReference,
+ raw_json_sha256: String,
+ admission_status: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ contract_id: RequiredNullable<String>,
+ event_class: String,
+ valid_stream_eligible: bool,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedVisibility {
+ event: ExpectedEventReference,
+ source_generation: String,
+ admission_status: String,
+ decision: String,
+ is_raw_head: bool,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ raw_head_event_id: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ suppression: RequiredNullable<ExpectedSuppressionEvidence>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedHistoricalVisibilityWitness {
+ transition_seq: i64,
+ event_id: String,
+ final_decision: String,
+}
+
+#[derive(Debug)]
+struct RequiredNullable<T>(Option<T>);
+
+fn deserialize_required_nullable<'de, D, T>(
+ deserializer: D,
+) -> Result<RequiredNullable<T>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+ T: Deserialize<'de>,
+{
+ Option::<T>::deserialize(deserializer).map(RequiredNullable)
+}
+
+#[tokio::test]
+async fn food_availability_projection_v1_result_vector() {
+ assert_eq!(
+ RESULT_VECTOR_EXECUTOR_ID,
+ "radroots_event_store.food_availability_projection_v1.result_vector_executor.v1"
+ );
+ assert_eq!(sha256_hex(RESULT_VECTOR_BYTES).len(), 64);
+
+ let vector: FoodAvailabilityProjectionVector =
+ serde_json::from_slice(RESULT_VECTOR_BYTES).expect("strict FoodAvailability vector");
+ assert_eq!(vector.schema_version, 1);
+ assert_eq!(
+ vector.contract_id,
+ "radroots_event_store.food_availability_projection_v1"
+ );
+ assert_eq!(
+ vector.feed_version,
+ RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1
+ );
+ assert_eq!(
+ vector.projection_version,
+ RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1
+ );
+ assert_eq!(vector.scope_kinds, [30_402]);
+ assert!(!vector.cases.is_empty());
+
+ for case in vector.cases {
+ execute_case(case).await;
+ }
+}
+
+fn sha256_hex(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
+}
+
+fn raw_head_decision_code(decision: &RadrootsRawHeadDecision) -> &'static str {
+ match decision {
+ RadrootsRawHeadDecision::Applied => "applied",
+ RadrootsRawHeadDecision::NotHeadSelected
+ | RadrootsRawHeadDecision::NotPersisted
+ | RadrootsRawHeadDecision::SkippedDuplicate => "not_head_selected",
+ RadrootsRawHeadDecision::SkippedOlder => "skipped_older",
+ RadrootsRawHeadDecision::SkippedSameTimestampHigherEventId => {
+ "skipped_same_timestamp_higher_event_id"
+ }
+ RadrootsRawHeadDecision::MalformedCoordinate => "malformed_coordinate",
+ }
+}
+
+async fn execute_case(case: ProjectionCase) {
+ let store = RadrootsEventStore::open_memory()
+ .await
+ .unwrap_or_else(|error| panic!("{}: open event store: {error}", case.id));
+
+ for (index, observed) in case.events.iter().enumerate() {
+ let (expected_kind, expected_event_class) = match observed.role {
+ ProjectionInputRole::ScopedFood | ProjectionInputRole::ScopedNonFood => {
+ (30_402, "addressable")
+ }
+ ProjectionInputRole::UnrelatedAddressable => (30_340, "addressable"),
+ ProjectionInputRole::Causal => (5, "regular"),
+ };
+ assert_eq!(
+ observed.event.kind, expected_kind,
+ "{}: input role",
+ case.id
+ );
+ assert_eq!(
+ observed.expected_ingest.event_class, expected_event_class,
+ "{}: input role event class",
+ case.id
+ );
+ let raw_json = serde_json::to_string(&observed.event)
+ .unwrap_or_else(|error| panic!("{}: serialize signed event: {error}", case.id));
+ let ingest = RadrootsEventIngest::from_raw_json(raw_json, observed.observed_at_ms)
+ .unwrap_or_else(|error| panic!("{}: verify signed event: {error}", case.id));
+ let receipt = store
+ .ingest_event(ingest)
+ .await
+ .unwrap_or_else(|error| panic!("{}: ingest signed event: {error}", case.id));
+ assert_eq!(receipt.event_id, observed.event.id, "{}", case.id);
+ let expected_sequence = i64::try_from(index + 1).expect("fixture sequence fits i64");
+ assert!(
+ receipt.persistence.is_inserted(),
+ "{}: input persistence",
+ case.id
+ );
+ assert_eq!(
+ receipt.persistence.sequence(),
+ Some(expected_sequence),
+ "{}: input sequence",
+ case.id
+ );
+ assert_eq!(
+ receipt.admission_status.as_str(),
+ observed.expected_ingest.admission_status,
+ "{}: input admission",
+ case.id
+ );
+ assert_eq!(
+ receipt.admission_code.as_deref(),
+ observed.expected_ingest.admission_code.0.as_deref(),
+ "{}: input admission code",
+ case.id
+ );
+ assert_eq!(
+ receipt.contract_id.as_deref(),
+ observed.expected_ingest.contract_id.0.as_deref(),
+ "{}: input contract",
+ case.id
+ );
+ assert_eq!(
+ receipt.valid_stream_eligible, observed.expected_ingest.valid_stream_eligible,
+ "{}: input stream eligibility",
+ case.id
+ );
+ assert_eq!(
+ raw_head_decision_code(&receipt.raw_head_decision),
+ observed.expected_ingest.raw_head_decision,
+ "{}: input raw-head decision",
+ case.id
+ );
+ }
+ let active_generation = store
+ .source_generation()
+ .await
+ .unwrap_or_else(|error| panic!("{}: active source generation: {error}", case.id));
+
+ assert_eq!(case.expected.coordinate.kind, 30_402, "{}", case.id);
+ let public_key = RadrootsPublicKey::parse(&case.expected.coordinate.pubkey)
+ .unwrap_or_else(|error| panic!("{}: expected public key: {error}", case.id));
+ let identifier = RadrootsFoodIdentifier::parse(&case.expected.coordinate.d_tag)
+ .unwrap_or_else(|error| panic!("{}: expected identifier: {error}", case.id));
+ let projection = store
+ .food_availability_v1(&public_key, &identifier)
+ .await
+ .unwrap_or_else(|error| panic!("{}: load projection: {error}", case.id));
+
+ match (&case.expected.projection.0, projection.as_ref()) {
+ (Some(expected), Some(actual)) => assert_projection(&case.id, expected, actual),
+ (None, None) => {}
+ (expected, actual) => panic!(
+ "{}: projection presence mismatch: expected={}, actual={}",
+ case.id,
+ expected.is_some(),
+ actual.is_some()
+ ),
+ }
+
+ let recent = store
+ .recent_food_availability_v1(RadrootsFoodAvailabilityStatusFilterV1::Any, 16)
+ .await
+ .unwrap_or_else(|error| panic!("{}: recent projection query: {error}", case.id));
+ assert_eq!(
+ recent.len(),
+ usize::from(case.expected.projection.0.is_some()),
+ "{}: recent projection count",
+ case.id
+ );
+
+ for expected in &case.expected.searches {
+ let query = RadrootsFoodAvailabilitySearchQueryV1::parse(&expected.query)
+ .unwrap_or_else(|error| panic!("{}: parse search query: {error}", case.id));
+ let actual = store
+ .search_food_availability_v1(&query, RadrootsFoodAvailabilityStatusFilterV1::Any, 16)
+ .await
+ .unwrap_or_else(|error| panic!("{}: search projection: {error}", case.id));
+ let event_ids = actual
+ .iter()
+ .map(|projection| projection.event_id().as_str())
+ .collect::<Vec<_>>();
+ assert_eq!(event_ids, expected.event_ids, "{}: search", case.id);
+ }
+
+ let scope = RadrootsAddressableTransitionScopeV1::food_availability();
+ assert_eq!(scope.kinds(), [30_402]);
+ let page = store
+ .addressable_transition_page_v1(&scope, None, 64)
+ .await
+ .unwrap_or_else(|error| panic!("{}: load transition feed: {error}", case.id));
+ let expected_page = &case.expected.transition_page;
+ assert_eq!(
+ page.source_high_water(),
+ expected_page.source_high_water,
+ "{}",
+ case.id
+ );
+ assert_eq!(page.has_more(), expected_page.has_more, "{}", case.id);
+ assert_active_generation(
+ &case.id,
+ "cursor source generation",
+ &expected_page.next_cursor.source_generation,
+ page.next_cursor().source_generation(),
+ active_generation,
+ );
+ assert_eq!(
+ page.next_cursor().feed_version(),
+ expected_page.next_cursor.feed_version,
+ "{}: cursor feed version",
+ case.id
+ );
+ assert_eq!(
+ page.next_cursor().scope_fingerprint().to_hex(),
+ expected_page.next_cursor.scope_fingerprint,
+ "{}: cursor scope fingerprint",
+ case.id
+ );
+ assert_eq!(
+ page.next_cursor().last_transition_seq(),
+ expected_page.next_cursor.last_transition_seq,
+ "{}: cursor transition sequence",
+ case.id
+ );
+ assert_eq!(
+ page.transitions().len(),
+ expected_page.transitions.len(),
+ "{}: transition count",
+ case.id
+ );
+ for (actual, expected) in page.transitions().iter().zip(&expected_page.transitions) {
+ assert_eq!(
+ actual.transition_seq(),
+ expected.transition_seq,
+ "{}",
+ case.id
+ );
+ assert_active_generation(
+ &case.id,
+ "transition source generation",
+ &expected.source_generation,
+ actual.source_generation(),
+ active_generation,
+ );
+ assert_eq!(actual.origin().as_str(), expected.origin, "{}", case.id);
+ assert_eq!(
+ actual.coordinate().kind(),
+ expected.coordinate.kind,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.coordinate().pubkey().as_str(),
+ expected.coordinate.pubkey,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.coordinate().d_tag(),
+ expected.coordinate.d_tag,
+ "{}",
+ case.id
+ );
+ assert_event_reference(&case.id, "raw head", &expected.raw_head, actual.raw_head());
+ assert_eq!(
+ actual.raw_head_created_at(),
+ expected.raw_head_created_at,
+ "{}: raw-head timestamp",
+ case.id
+ );
+ assert_eq!(
+ actual.admission_status().as_str(),
+ expected.admission_status,
+ "{}: admission status",
+ case.id
+ );
+ assert_eq!(
+ actual.admission_code(),
+ expected.admission_code.0.as_deref(),
+ "{}: admission code",
+ case.id
+ );
+ assert_eq!(
+ actual.contract_id(),
+ expected.contract_id.0.as_deref(),
+ "{}: contract id",
+ case.id
+ );
+ assert_eq!(
+ actual.visibility().as_str(),
+ expected.visibility,
+ "{}: transition visibility",
+ case.id
+ );
+ assert_optional_suppression(
+ &case.id,
+ expected.suppression.0.as_ref(),
+ actual.suppression(),
+ );
+ match (expected.cause_event.0.as_ref(), actual.cause_event()) {
+ (Some(expected), Some(actual)) => {
+ assert_event_reference(&case.id, "cause", &expected.event, actual.event());
+ assert_eq!(actual.pubkey().as_str(), expected.pubkey, "{}", case.id);
+ assert_eq!(actual.created_at(), expected.created_at, "{}", case.id);
+ assert_eq!(actual.kind(), expected.kind, "{}", case.id);
+ assert_eq!(
+ actual.admission_status().as_str(),
+ expected.admission_status,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.admission_code(),
+ expected.admission_code.0.as_deref(),
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.contract_id(),
+ expected.contract_id.0.as_deref(),
+ "{}",
+ case.id
+ );
+ }
+ (None, None) => {}
+ (expected, actual) => panic!(
+ "{}: cause presence mismatch: expected={}, actual={}",
+ case.id,
+ expected.is_some(),
+ actual.is_some()
+ ),
+ }
+ match (
+ expected.canonical_visible_event.0.as_ref(),
+ actual.visible_event(),
+ ) {
+ (Some(expected), Some(actual)) => {
+ assert_canonical_visible_event(&case, expected, actual)
+ }
+ (None, None) => {}
+ (expected, actual) => panic!(
+ "{}: canonical visible-event presence mismatch: expected={}, actual={}",
+ case.id,
+ expected.is_some(),
+ actual.is_some()
+ ),
+ }
+ match (
+ expected.retracted_event.0.as_ref(),
+ actual.retracted_event(),
+ ) {
+ (Some(expected), Some(actual)) => {
+ assert_event_reference(&case.id, "retracted event", expected, actual)
+ }
+ (None, None) => {}
+ (expected, actual) => panic!(
+ "{}: retracted-event presence mismatch: expected={}, actual={}",
+ case.id,
+ expected.is_some(),
+ actual.is_some()
+ ),
+ }
+ assert_eq!(
+ actual.raw_head_decision().as_str(),
+ expected.raw_head_decision,
+ "{}: raw-head decision",
+ case.id
+ );
+ }
+ assert_eq!(
+ page.next_cursor().last_transition_seq(),
+ page.source_high_water(),
+ "{}: cursor reaches captured high-water",
+ case.id
+ );
+
+ for expected in &case.expected.event_visibility {
+ let actual = store
+ .current_event_visibility_v1(&expected.event.event_id)
+ .await
+ .unwrap_or_else(|error| panic!("{}: current visibility: {error}", case.id))
+ .unwrap_or_else(|| panic!("{}: expected stored event visibility", case.id));
+ assert_active_generation(
+ &case.id,
+ "current visibility source generation",
+ &expected.source_generation,
+ actual.source_generation(),
+ active_generation,
+ );
+ assert_stored_event_matches_input(&case, &expected.event, actual.event());
+ assert_eq!(
+ actual.admission_status().as_str(),
+ expected.admission_status,
+ "{}: current admission status",
+ case.id
+ );
+ assert_eq!(
+ actual.decision().as_str(),
+ expected.decision,
+ "{}: current decision",
+ case.id
+ );
+ assert_eq!(actual.is_raw_head(), expected.is_raw_head, "{}", case.id);
+ assert_eq!(
+ actual.raw_head_event_id().map(|event_id| event_id.as_str()),
+ expected.raw_head_event_id.0.as_deref(),
+ "{}: current raw head",
+ case.id
+ );
+ assert_optional_suppression(
+ &case.id,
+ expected.suppression.0.as_ref(),
+ actual.suppression(),
+ );
+ }
+
+ for witness in &case.expected.historical_visibility_witnesses {
+ let historical = page
+ .transitions()
+ .iter()
+ .find(|transition| transition.transition_seq() == witness.transition_seq)
+ .unwrap_or_else(|| panic!("{}: historical transition is absent", case.id));
+ let historical_payload = historical
+ .visible_event()
+ .unwrap_or_else(|| panic!("{}: historical visible payload is absent", case.id));
+ assert_eq!(
+ historical_payload.event_id().as_str(),
+ witness.event_id,
+ "{}: historical visible payload identity",
+ case.id
+ );
+ let expected_final = case
+ .expected
+ .event_visibility
+ .iter()
+ .find(|visibility| visibility.event.event_id == witness.event_id)
+ .unwrap_or_else(|| panic!("{}: final visibility witness is absent", case.id));
+ assert_eq!(
+ expected_final.decision, witness.final_decision,
+ "{}: historical/final witness contract",
+ case.id
+ );
+ assert_ne!(
+ witness.final_decision, "visible",
+ "{}: historical payload must diverge from final visibility",
+ case.id
+ );
+ let final_visibility = store
+ .current_event_visibility_v1(&witness.event_id)
+ .await
+ .unwrap_or_else(|error| panic!("{}: final historical visibility: {error}", case.id))
+ .unwrap_or_else(|| panic!("{}: final historical event is absent", case.id));
+ assert_eq!(
+ final_visibility.decision().as_str(),
+ witness.final_decision,
+ "{}: transition-time payload coexists with final visibility",
+ case.id
+ );
+ }
+
+ store
+ .audit_food_availability_projection_v1()
+ .await
+ .unwrap_or_else(|error| panic!("{}: exhaustive projection audit: {error}", case.id));
+}
+
+fn assert_active_generation(
+ case_id: &str,
+ label: &str,
+ expected: &str,
+ actual: RadrootsEventStoreSourceGeneration,
+ active: RadrootsEventStoreSourceGeneration,
+) {
+ assert_eq!(
+ expected, SOURCE_GENERATION_ACTIVE_SENTINEL,
+ "{case_id}: {label}"
+ );
+ assert_eq!(actual, active, "{case_id}: {label}");
+}
+
+fn assert_event_reference(
+ case_id: &str,
+ label: &str,
+ expected: &ExpectedEventReference,
+ actual: &RadrootsAddressableTransitionEventReferenceV1,
+) {
+ assert_eq!(
+ actual.event_id().as_str(),
+ expected.event_id,
+ "{case_id}: {label}"
+ );
+ assert_eq!(actual.event_seq(), expected.event_seq, "{case_id}: {label}");
+}
+
+fn assert_optional_suppression(
+ case_id: &str,
+ expected: Option<&ExpectedSuppressionEvidence>,
+ actual: Option<&RadrootsNip09SuppressionEvidenceV1>,
+) {
+ match (expected, actual) {
+ (Some(expected), Some(actual)) => {
+ assert_eq!(actual.outcome().code(), expected.outcome, "{case_id}");
+ assert_eq!(actual.reason().code(), expected.reason, "{case_id}");
+ assert_eq!(
+ actual
+ .event_reference_request_id()
+ .map(|event_id| event_id.as_str()),
+ expected.event_reference_request_id.0.as_deref(),
+ "{case_id}"
+ );
+ assert_eq!(
+ actual
+ .address_reference_request_id()
+ .map(|event_id| event_id.as_str()),
+ expected.address_reference_request_id.0.as_deref(),
+ "{case_id}"
+ );
+ assert_eq!(
+ actual.address_reference_cutoff(),
+ expected.address_reference_cutoff.0,
+ "{case_id}"
+ );
+ }
+ (None, None) => {}
+ (expected, actual) => panic!(
+ "{case_id}: suppression presence mismatch: expected={}, actual={}",
+ expected.is_some(),
+ actual.is_some()
+ ),
+ }
+}
+
+fn assert_canonical_visible_event(
+ case: &ProjectionCase,
+ expected: &ExpectedCanonicalVisibleEvent,
+ actual: &RadrootsStoreProducedCanonicalEventV1,
+) {
+ let observed = case
+ .events
+ .iter()
+ .find(|observed| observed.event.id == expected.event.event_id)
+ .unwrap_or_else(|| panic!("{}: expected event is absent from input", case.id));
+ assert_eq!(actual.event_id().as_str(), observed.event.id, "{}", case.id);
+ assert_eq!(
+ actual.pubkey().as_str(),
+ observed.event.pubkey,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.created_at(),
+ observed.event.created_at,
+ "{}",
+ case.id
+ );
+ assert_eq!(actual.kind(), observed.event.kind, "{}", case.id);
+ let decoded: SignedEvent = serde_json::from_str(actual.raw_json())
+ .unwrap_or_else(|error| panic!("{}: decode canonical raw JSON: {error}", case.id));
+ assert_eq!(
+ decoded, observed.event,
+ "{}: canonical signed payload",
+ case.id
+ );
+ assert_eq!(
+ sha256_hex(actual.raw_json().as_bytes()),
+ expected.raw_json_sha256,
+ "{}: canonical raw JSON digest",
+ case.id
+ );
+ assert_eq!(
+ observed.expected_ingest.admission_status, expected.admission_status,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ observed.expected_ingest.contract_id.0.as_deref(),
+ expected.contract_id.0.as_deref(),
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ observed.expected_ingest.event_class, expected.event_class,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ observed.expected_ingest.valid_stream_eligible, expected.valid_stream_eligible,
+ "{}",
+ case.id
+ );
+}
+
+fn assert_stored_event_matches_input(
+ case: &ProjectionCase,
+ expected: &ExpectedEventReference,
+ actual: &RadrootsStoredRawEvent,
+) {
+ let observed = case
+ .events
+ .iter()
+ .find(|observed| observed.event.id == expected.event_id)
+ .unwrap_or_else(|| panic!("{}: expected event is absent from input", case.id));
+ let raw_json = serde_json::to_string(&observed.event)
+ .unwrap_or_else(|error| panic!("{}: serialize expected event: {error}", case.id));
+ let tags_json = serde_json::to_string(&observed.event.tags)
+ .unwrap_or_else(|error| panic!("{}: serialize expected tags: {error}", case.id));
+ assert_eq!(actual.seq, expected.event_seq, "{}", case.id);
+ assert_eq!(actual.event_id, observed.event.id, "{}", case.id);
+ assert_eq!(actual.pubkey, observed.event.pubkey, "{}", case.id);
+ assert_eq!(actual.created_at, observed.event.created_at, "{}", case.id);
+ assert_eq!(actual.kind, observed.event.kind, "{}", case.id);
+ assert_eq!(actual.tags_json, tags_json, "{}", case.id);
+ assert_eq!(actual.content, observed.event.content, "{}", case.id);
+ assert_eq!(actual.sig, observed.event.sig, "{}", case.id);
+ assert_eq!(actual.raw_json, raw_json, "{}", case.id);
+ assert_eq!(
+ actual.admission_status.as_str(),
+ observed.expected_ingest.admission_status,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.contract_id.as_deref(),
+ observed.expected_ingest.contract_id.0.as_deref(),
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.event_class.as_str(),
+ observed.expected_ingest.event_class,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.valid_stream_eligible, observed.expected_ingest.valid_stream_eligible,
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ actual.inserted_at_ms, observed.observed_at_ms,
+ "{}",
+ case.id
+ );
+ assert_eq!(actual.updated_at_ms, observed.observed_at_ms, "{}", case.id);
+}
+
+fn assert_projection(
+ case_id: &str,
+ expected: &ExpectedProjection,
+ actual: &RadrootsStoredFoodAvailabilityV1,
+) {
+ assert_eq!(actual.event_id().as_str(), expected.event_id, "{case_id}");
+ assert_eq!(actual.content().as_str(), expected.content, "{case_id}");
+ assert_eq!(actual.title().as_str(), expected.title, "{case_id}");
+ assert_eq!(actual.summary().as_str(), expected.summary, "{case_id}");
+ assert_eq!(
+ actual.published_at().as_u64(),
+ expected.published_at,
+ "{case_id}"
+ );
+ assert_eq!(actual.location().as_str(), expected.location, "{case_id}");
+ assert_eq!(actual.price().amount(), expected.price_amount, "{case_id}");
+ assert_eq!(
+ actual.price().currency().as_str(),
+ expected.price_currency,
+ "{case_id}"
+ );
+ assert_eq!(
+ actual.price().unit().as_str(),
+ expected.price_unit,
+ "{case_id}"
+ );
+ assert_eq!(
+ actual.quantity().map(|quantity| quantity.amount()),
+ expected.quantity_amount.0.as_deref(),
+ "{case_id}"
+ );
+ assert_eq!(
+ actual.quantity().map(|quantity| quantity.unit().as_str()),
+ expected.quantity_unit.0.as_deref(),
+ "{case_id}"
+ );
+ assert_eq!(actual.status().as_str(), expected.status, "{case_id}");
+ assert_eq!(
+ actual
+ .diagnostics()
+ .iter()
+ .map(|diagnostic| diagnostic.code())
+ .collect::<Vec<_>>(),
+ expected.diagnostics,
+ "{case_id}: projection diagnostics"
+ );
+ assert_eq!(actual.images().len(), expected.images.len(), "{case_id}");
+
+ for (actual, expected) in actual.images().iter().zip(&expected.images) {
+ assert_eq!(actual.url(), expected.url.0.as_deref(), "{case_id}");
+ assert_eq!(
+ actual.dimensions().map(|dimensions| dimensions.width()),
+ expected.width.0,
+ "{case_id}"
+ );
+ assert_eq!(
+ actual.dimensions().map(|dimensions| dimensions.height()),
+ expected.height.0,
+ "{case_id}"
+ );
+ let expected_blossom_sha256 = expected.blossom_sha256.0.as_deref().map(|value| {
+ RadrootsBlossomSha256::from_hex(value)
+ .unwrap_or_else(|error| panic!("{case_id}: expected Blossom digest: {error}"))
+ });
+ assert_eq!(
+ actual.blossom_sha256(),
+ expected_blossom_sha256,
+ "{case_id}"
+ );
+ assert_eq!(
+ actual
+ .diagnostics()
+ .iter()
+ .map(|diagnostic| diagnostic.code())
+ .collect::<Vec<_>>(),
+ expected.diagnostics,
+ "{case_id}: image diagnostics"
+ );
+ assert_eq!(actual.qualifies(), expected.qualifies, "{case_id}");
+ }
+}
+
+#[test]
+fn food_availability_projection_v1_vector_requires_complete_nullable_fields() {
+ let vector: serde_json::Value =
+ serde_json::from_slice(RESULT_VECTOR_BYTES).expect("result-vector JSON");
+ let mut missing = vector.clone();
+ missing["cases"][0]["expected"]["projection"]
+ .as_object_mut()
+ .expect("expected projection")
+ .remove("quantity_amount");
+ assert!(
+ serde_json::from_value::<FoodAvailabilityProjectionVector>(missing).is_err(),
+ "nullable result fields must still be present"
+ );
+}
diff --git a/crates/replica_sync/Cargo.toml b/crates/replica_sync/Cargo.toml
@@ -20,11 +20,14 @@ default = ["std"]
std = [
"radroots_event/std",
"radroots_event_codec/std",
- "radroots_event_codec/nostr",
"radroots_sql_core/std",
- "dep:base64",
- "dep:uuid",
]
+legacy-ingest = ["std", "radroots_event_codec/nostr", "dep:base64", "dep:uuid"]
+
+[[test]]
+name = "ingest_roundtrip"
+path = "tests/ingest_roundtrip.rs"
+required-features = ["legacy-ingest"]
[dependencies]
radroots_event = { workspace = true, default-features = false, features = [
diff --git a/crates/replica_sync/README b/crates/replica_sync/README
@@ -9,10 +9,15 @@ and synchronization interfaces for the `radroots` core libraries.
sync flows;
* event-emission helpers that materialize replica-facing events from local
state;
- * ingest helpers, outcomes, and id-factory abstractions for applying incoming
- events;
- * a `std` feature that adds default id generation and higher-level ingest
- entry points.
+ * an explicit, non-default `legacy-ingest` feature for the historical
+ bare-envelope projection helpers;
+ * a `std` feature for the portable emit and sync surfaces without legacy
+ ingestion.
+
+The legacy ingest module is excluded from default builds. It accepts bare event
+envelopes and does not consume the event store's verified, valid-stream, and
+current-visibility typestates, so it is not a Phase 1 product ingestion API.
+New consumers must project only store-produced current visible admissions.
## Copyright
diff --git a/crates/replica_sync/src/lib.rs b/crates/replica_sync/src/lib.rs
@@ -10,6 +10,7 @@ pub mod emit;
pub mod error;
mod event_head;
mod geo;
+#[cfg(feature = "legacy-ingest")]
pub mod ingest;
pub mod sync_state;
pub mod types;
@@ -20,6 +21,7 @@ pub use emit::{
radroots_replica_sync_all, radroots_replica_sync_all_with_options,
};
pub use error::RadrootsReplicaEventsError;
+#[cfg(feature = "legacy-ingest")]
pub use ingest::{
RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, radroots_replica_ingest_event_head,
radroots_replica_ingest_event_with_factory,
@@ -34,7 +36,7 @@ pub use types::{
RadrootsReplicaSyncBundle, RadrootsReplicaSyncOptions, RadrootsReplicaSyncRequest,
};
-#[cfg(feature = "std")]
+#[cfg(feature = "legacy-ingest")]
pub use ingest::{RadrootsReplicaDefaultIdFactory, radroots_replica_ingest_event};
#[cfg(test)]
diff --git a/crates/transport_nostr/README b/crates/transport_nostr/README
@@ -6,19 +6,34 @@ fetch ingest, and outbox delivery target coordination.
Every fetch path verifies the NIP-01 id and signature before filter matching,
unique-event budgeting, or returning an event. Repeated event ids preserve
per-relay observation evidence without consuming the unique-event limit. The
-SDK adapter streams into the configured raw-event bound; `Truncated` is distinct
-from relay `EOSE`, including for later target relays that were not queried after
-the global bound was reached.
+unique-event limit is bounded at 1,000 so final stored-event visibility can be
+evaluated in one coherent event-store snapshot. A fetch scans at most 64,000
+raw events and 64 MiB of aggregate raw JSON, and rejects any individual raw
+event over 256 KiB before Radroots parses adapter raw JSON. Count and byte
+budgets are charged globally, in adapter order, before Radroots parsing,
+filtering, deduplication, or accepted-event limiting, so malformed and otherwise
+rejected events cannot bypass them. The official SDK adapter enforces the same
+retained-prefix budgets after SDK frame/event decoding and before retaining its
+serialized JSON; the connector's upstream frame parser remains responsible for
+its own first-pass network limits. `Truncated` is distinct from relay `EOSE`,
+including for later target relays that were not queried after a global count or
+byte budget was reached.
-Fetch-ingest receipts distinguish unsupported contracts, invalid registered
-shapes, malformed NIP-01 input, ephemeral events that were not persisted, and
-immutable valid-stream eligibility. `admission_code` carries the stable
-classifier diagnostic when the store performed classification; it is absent for
-duplicates because the baseline schema does not persist that code. Inserted,
-duplicate, and not-persisted outcomes have separate flags and aggregate counts.
-Local event-store failures abort the operation and remain typed transport
-errors, so callers can retry without confusing storage failure with bad relay
-input.
+Fetch-ingest receipts report verification, contract admission, immutable
+valid-stream eligibility, and current visibility as independent exhaustive
+enums. Verification failure is not a contract-invalid result, and unsupported
+admission does not hide whether the stored event is current, suppressed, or not
+admitted. Persisted events obtain visibility from the event store's central
+authority after the complete accepted fetch batch has been ingested, so event
+receipts and aggregate visibility counts describe final post-batch state rather
+than transient per-item state. Repeated receipt IDs are deduplicated before the
+single snapshot lookup and then mapped back to every receipt. Ephemeral events
+use the explicit `not_persisted` visibility result.
+`admission_code` carries the stable classifier diagnostic when classification
+produces one. Inserted, duplicate, and not-persisted persistence outcomes retain
+separate flags and aggregate counts. Local event-store failures abort the
+operation and remain typed transport errors, so callers can retry without
+confusing storage failure with bad relay input.
`RadrootsRelayUrlPolicy::Public` is for trusted relay configuration. It rejects
non-canonical and known non-global literal destinations, but hostname checks do
diff --git a/crates/transport_nostr/src/error.rs b/crates/transport_nostr/src/error.rs
@@ -86,6 +86,13 @@ pub enum RadrootsRelayTransportError {
#[error("Relay fetch {field} must be greater than zero")]
InvalidFetchLimit { field: &'static str },
+ #[error("Relay fetch {field} {actual} exceeds maximum {max}")]
+ FetchLimitTooLarge {
+ field: &'static str,
+ max: usize,
+ actual: usize,
+ },
+
#[error("Relay transport {field} cannot be negative: {value}")]
InvalidTimestamp { field: &'static str, value: i64 },
@@ -115,6 +122,18 @@ pub enum RadrootsRelayTransportError {
EventStore(#[from] radroots_event_store::RadrootsEventStoreError),
#[cfg(feature = "storage")]
+ #[error("Event store returned no current visibility for persisted event `{event_id}`")]
+ MissingStoredEventVisibility { event_id: String },
+
+ #[cfg(feature = "storage")]
+ #[error("Persisted relay fetch event receipt is missing its event id")]
+ MissingPersistedFetchReceiptEventId,
+
+ #[cfg(feature = "storage")]
+ #[error("Event store returned an unsupported current visibility for event `{event_id}`")]
+ UnsupportedStoredEventVisibility { event_id: String },
+
+ #[cfg(feature = "storage")]
#[error("Outbox error: {0}")]
Outbox(#[from] radroots_outbox::RadrootsOutboxError),
diff --git a/crates/transport_nostr/src/fetch.rs b/crates/transport_nostr/src/fetch.rs
@@ -5,9 +5,11 @@ use crate::{RadrootsRelayOutcome, RadrootsRelayTargetSet, RadrootsRelayTransport
use core::time::Duration;
use futures::{StreamExt, future::BoxFuture};
use nostr::{JsonUtil, filter::MatchEventOptions};
+use radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES;
use radroots_event_store::{
- RadrootsEventAdmissionStatus, RadrootsEventIngest, RadrootsEventPersistence,
- RadrootsEventStore, RadrootsTransportObservation, RadrootsTransportObservationType,
+ RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX, RadrootsEventAdmissionStatus, RadrootsEventIngest,
+ RadrootsEventPersistence, RadrootsEventStore, RadrootsEventVisibility,
+ RadrootsTransportObservation, RadrootsTransportObservationType,
};
use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter};
use radroots_transport::{RadrootsTransportKind, RadrootsTransportTarget};
@@ -17,6 +19,11 @@ use std::sync::{Arc, Mutex, PoisonError};
const DEFAULT_RELAY_FETCH_TIMEOUT_MS: u64 = 10_000;
const DEFAULT_RELAY_FETCH_RAW_SCAN_MULTIPLIER: usize = 64;
+pub const RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX: usize =
+ RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX as usize;
+pub const RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX: usize =
+ RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX * DEFAULT_RELAY_FETCH_RAW_SCAN_MULTIPLIER;
+pub const RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX: usize = 64 * 1024 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RadrootsRelayFetchMode {
@@ -58,6 +65,7 @@ pub struct RadrootsRelayFetchRequest {
observed_at_ms: i64,
max_events: usize,
max_raw_events: usize,
+ max_raw_json_bytes: usize,
relay_targets: RadrootsRelayTargetSet,
filters: RadrootsRelayFetchFilters,
timeout_ms: u64,
@@ -111,12 +119,13 @@ impl RadrootsRelayFetchRequest {
I: IntoIterator<Item = RadrootsNostrFilter>,
{
ensure_nonnegative_timestamp("observed_at_ms", observed_at_ms)?;
- ensure_positive_limit("max_events", max_events)?;
+ ensure_event_limit("max_events", max_events)?;
Ok(Self {
mode,
observed_at_ms,
max_events,
max_raw_events: default_raw_event_scan_limit(max_events),
+ max_raw_json_bytes: RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX,
relay_targets,
filters: RadrootsRelayFetchFilters::new(filters)?,
timeout_ms: DEFAULT_RELAY_FETCH_TIMEOUT_MS,
@@ -133,11 +142,28 @@ impl RadrootsRelayFetchRequest {
mut self,
max_raw_events: usize,
) -> Result<Self, RadrootsRelayTransportError> {
- ensure_positive_limit("max_raw_events", max_raw_events)?;
+ ensure_bounded_fetch_limit(
+ "max_raw_events",
+ max_raw_events,
+ RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX,
+ )?;
self.max_raw_events = max_raw_events;
Ok(self)
}
+ pub fn with_raw_json_byte_limit(
+ mut self,
+ max_raw_json_bytes: usize,
+ ) -> Result<Self, RadrootsRelayTransportError> {
+ ensure_bounded_fetch_limit(
+ "max_raw_json_bytes",
+ max_raw_json_bytes,
+ RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX,
+ )?;
+ self.max_raw_json_bytes = max_raw_json_bytes;
+ Ok(self)
+ }
+
pub fn mode(&self) -> RadrootsRelayFetchMode {
self.mode
}
@@ -154,6 +180,10 @@ impl RadrootsRelayFetchRequest {
self.max_raw_events
}
+ pub fn max_raw_json_bytes(&self) -> usize {
+ self.max_raw_json_bytes
+ }
+
pub fn relay_targets(&self) -> &RadrootsRelayTargetSet {
&self.relay_targets
}
@@ -171,6 +201,7 @@ fn default_raw_event_scan_limit(max_events: usize) -> usize {
max_events
.saturating_mul(DEFAULT_RELAY_FETCH_RAW_SCAN_MULTIPLIER)
.max(max_events)
+ .min(RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX)
}
fn ensure_positive_limit(
@@ -183,6 +214,29 @@ fn ensure_positive_limit(
Ok(())
}
+fn ensure_event_limit(
+ field: &'static str,
+ value: usize,
+) -> Result<(), RadrootsRelayTransportError> {
+ ensure_bounded_fetch_limit(field, value, RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX)
+}
+
+fn ensure_bounded_fetch_limit(
+ field: &'static str,
+ value: usize,
+ max: usize,
+) -> Result<(), RadrootsRelayTransportError> {
+ ensure_positive_limit(field, value)?;
+ if value > max {
+ return Err(RadrootsRelayTransportError::FetchLimitTooLarge {
+ field,
+ max,
+ actual: value,
+ });
+ }
+ Ok(())
+}
+
fn ensure_positive_timeout(
field: &'static str,
value: u64,
@@ -218,6 +272,42 @@ pub struct RadrootsRelayFetchRelayOutcome {
pub message: Option<String>,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum RadrootsRelayFetchEventVerification {
+ NotEvaluated,
+ Verified,
+ Failed,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum RadrootsRelayFetchEventAdmission {
+ NotEvaluated,
+ Admitted,
+ Unsupported,
+ Invalid,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum RadrootsRelayFetchEventValidStream {
+ NotEvaluated,
+ Eligible,
+ Ineligible,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum RadrootsRelayFetchEventVisibility {
+ NotEvaluated,
+ NotPersisted,
+ Visible,
+ NotAdmitted,
+ NotCurrent,
+ Suppressed,
+}
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RadrootsRelayFetchEventReceipt {
pub relay_url: String,
@@ -225,14 +315,14 @@ pub struct RadrootsRelayFetchEventReceipt {
pub inserted: bool,
pub duplicate: bool,
pub not_persisted: bool,
- pub unsupported: bool,
- pub invalid: bool,
pub malformed: bool,
pub out_of_filter: bool,
pub skipped_over_limit: bool,
- pub valid_stream_eligible: bool,
- pub admission_status: Option<String>,
+ pub verification: RadrootsRelayFetchEventVerification,
+ pub admission: RadrootsRelayFetchEventAdmission,
pub admission_code: Option<String>,
+ pub valid_stream: RadrootsRelayFetchEventValidStream,
+ pub visibility: RadrootsRelayFetchEventVisibility,
pub message: Option<String>,
}
@@ -258,7 +348,7 @@ pub struct RadrootsRelayFetchedEventsReceipt {
pub events: Vec<RadrootsRelayFetchedEvent>,
pub event_receipts: Vec<RadrootsRelayFetchEventReceipt>,
pub duplicate_count: usize,
- pub invalid_count: usize,
+ pub verification_failed_count: usize,
pub malformed_count: usize,
pub out_of_filter_count: usize,
pub skipped_over_limit_count: usize,
@@ -277,8 +367,14 @@ pub struct RadrootsRelayFetchReceipt {
pub malformed_count: usize,
pub out_of_filter_count: usize,
pub skipped_over_limit_count: usize,
- pub unsupported_count: usize,
- pub invalid_count: usize,
+ pub verification_failed_count: usize,
+ pub admission_unsupported_count: usize,
+ pub admission_invalid_count: usize,
+ pub valid_stream_eligible_count: usize,
+ pub visible_count: usize,
+ pub not_admitted_count: usize,
+ pub not_current_count: usize,
+ pub suppressed_count: usize,
pub eose_count: usize,
pub truncated_count: usize,
pub closed_count: usize,
@@ -305,6 +401,7 @@ where
let observed_at_ms = request.observed_at_ms;
let max_events = request.max_events;
let max_raw_events = request.max_raw_events;
+ let max_raw_json_bytes = request.max_raw_json_bytes;
let filters = request.filters.as_slice().to_vec();
let items = adapter.fetch(request).await?;
Ok(process_relay_fetch_items(
@@ -313,6 +410,7 @@ where
observed_at_ms,
max_events,
max_raw_events,
+ max_raw_json_bytes,
items,
)?
.into_fetched_events_receipt())
@@ -347,6 +445,7 @@ where
let observed_at_ms = request.observed_at_ms;
let max_events = request.max_events;
let max_raw_events = request.max_raw_events;
+ let max_raw_json_bytes = request.max_raw_json_bytes;
let filters = request.filters.as_slice().to_vec();
let items = adapter.fetch(request).await?;
let processed = process_relay_fetch_items(
@@ -355,6 +454,7 @@ where
observed_at_ms,
max_events,
max_raw_events,
+ max_raw_json_bytes,
items,
)?;
let mut receipt = RadrootsRelayFetchReceipt::from_processed_counts(&processed);
@@ -390,31 +490,33 @@ where
let ingest = match RadrootsEventIngest::from_raw_json(raw_json, observed_at_ms) {
Ok(ingest) => ingest.with_observation(observation),
Err(error) => {
- receipt.malformed_count += 1;
+ receipt.verification_failed_count += 1;
receipt.events.push(RadrootsRelayFetchEventReceipt {
relay_url,
event_id: Some(raw_event.id.to_hex()),
inserted: false,
duplicate: false,
not_persisted: false,
- unsupported: false,
- invalid: false,
- malformed: true,
+ malformed: false,
out_of_filter: false,
skipped_over_limit: false,
- valid_stream_eligible: false,
- admission_status: None,
+ verification: RadrootsRelayFetchEventVerification::Failed,
+ admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
admission_code: None,
+ valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
+ visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
message: Some(error.to_string()),
});
continue;
}
};
let store_receipt = event_store.ingest_event(ingest).await?;
- let unsupported =
- store_receipt.admission_status == RadrootsEventAdmissionStatus::Unsupported;
- let invalid =
- store_receipt.admission_status == RadrootsEventAdmissionStatus::Invalid;
+ let admission = relay_fetch_admission(store_receipt.admission_status);
+ let valid_stream = if store_receipt.valid_stream_eligible {
+ RadrootsRelayFetchEventValidStream::Eligible
+ } else {
+ RadrootsRelayFetchEventValidStream::Ineligible
+ };
let (inserted, duplicate, not_persisted) = match store_receipt.persistence {
RadrootsEventPersistence::Inserted { .. } => {
receipt.inserted_count += 1;
@@ -429,34 +531,72 @@ where
(false, false, true)
}
};
- if unsupported {
- receipt.unsupported_count += 1;
- }
- if invalid {
- receipt.invalid_count += 1;
- }
- receipt.events.push(RadrootsRelayFetchEventReceipt {
+ let visibility = if not_persisted {
+ RadrootsRelayFetchEventVisibility::NotPersisted
+ } else {
+ RadrootsRelayFetchEventVisibility::NotEvaluated
+ };
+ let event_receipt = RadrootsRelayFetchEventReceipt {
relay_url,
event_id: Some(store_receipt.event_id),
inserted,
duplicate,
not_persisted,
- unsupported,
- invalid,
malformed: false,
out_of_filter: false,
skipped_over_limit: false,
- valid_stream_eligible: store_receipt.valid_stream_eligible,
- admission_status: Some(store_receipt.admission_status.as_str().to_owned()),
+ verification: RadrootsRelayFetchEventVerification::Verified,
+ admission,
admission_code: store_receipt.admission_code,
+ valid_stream,
+ visibility,
message: None,
- });
+ };
+ receipt.events.push(event_receipt);
}
}
}
+ receipt.refresh_final_semantic_outcomes(event_store).await?;
Ok(receipt)
}
+fn relay_fetch_admission(
+ admission: RadrootsEventAdmissionStatus,
+) -> RadrootsRelayFetchEventAdmission {
+ match admission {
+ RadrootsEventAdmissionStatus::Admitted => RadrootsRelayFetchEventAdmission::Admitted,
+ RadrootsEventAdmissionStatus::Unsupported => RadrootsRelayFetchEventAdmission::Unsupported,
+ RadrootsEventAdmissionStatus::Invalid => RadrootsRelayFetchEventAdmission::Invalid,
+ }
+}
+
+fn relay_fetch_visibility(
+ event_id: &str,
+ visibility: RadrootsEventVisibility,
+) -> Result<RadrootsRelayFetchEventVisibility, RadrootsRelayTransportError> {
+ match visibility {
+ RadrootsEventVisibility::Visible => Ok(RadrootsRelayFetchEventVisibility::Visible),
+ RadrootsEventVisibility::NotAdmitted => Ok(RadrootsRelayFetchEventVisibility::NotAdmitted),
+ RadrootsEventVisibility::NotCurrent { .. } => {
+ Ok(RadrootsRelayFetchEventVisibility::NotCurrent)
+ }
+ RadrootsEventVisibility::Suppressed { .. } => {
+ Ok(RadrootsRelayFetchEventVisibility::Suppressed)
+ }
+ _ => Err(
+ RadrootsRelayTransportError::UnsupportedStoredEventVisibility {
+ event_id: event_id.to_owned(),
+ },
+ ),
+ }
+}
+
+fn required_persisted_fetch_receipt_event_id(
+ event_id: Option<&str>,
+) -> Result<&str, RadrootsRelayTransportError> {
+ event_id.ok_or(RadrootsRelayTransportError::MissingPersistedFetchReceiptEventId)
+}
+
#[derive(Clone, Debug)]
enum RadrootsRelayProcessedFetchItem {
Accepted(RadrootsRelayFetchedEvent),
@@ -469,7 +609,7 @@ struct RadrootsRelayProcessedFetch {
target_relays: Vec<String>,
items: Vec<RadrootsRelayProcessedFetchItem>,
duplicate_count: usize,
- invalid_count: usize,
+ verification_failed_count: usize,
malformed_count: usize,
out_of_filter_count: usize,
skipped_over_limit_count: usize,
@@ -518,7 +658,7 @@ impl RadrootsRelayProcessedFetch {
events,
event_receipts,
duplicate_count: self.duplicate_count,
- invalid_count: self.invalid_count,
+ verification_failed_count: self.verification_failed_count,
malformed_count: self.malformed_count,
out_of_filter_count: self.out_of_filter_count,
skipped_over_limit_count: self.skipped_over_limit_count,
@@ -540,8 +680,14 @@ impl RadrootsRelayFetchReceipt {
malformed_count: processed.malformed_count,
out_of_filter_count: processed.out_of_filter_count,
skipped_over_limit_count: processed.skipped_over_limit_count,
- unsupported_count: 0,
- invalid_count: processed.invalid_count,
+ verification_failed_count: processed.verification_failed_count,
+ admission_unsupported_count: 0,
+ admission_invalid_count: 0,
+ valid_stream_eligible_count: 0,
+ visible_count: 0,
+ not_admitted_count: 0,
+ not_current_count: 0,
+ suppressed_count: 0,
eose_count: processed.eose_count,
truncated_count: processed.truncated_count,
closed_count: processed.closed_count,
@@ -550,6 +696,152 @@ impl RadrootsRelayFetchReceipt {
relay_outcomes: processed.relay_outcomes.clone(),
}
}
+
+ async fn refresh_final_semantic_outcomes(
+ &mut self,
+ event_store: &RadrootsEventStore,
+ ) -> Result<(), RadrootsRelayTransportError> {
+ let mut event_ids = Vec::new();
+ let mut seen_event_ids = BTreeSet::new();
+ for event in &self.events {
+ if event.not_persisted || (!event.inserted && !event.duplicate) {
+ continue;
+ }
+ let event_id = required_persisted_fetch_receipt_event_id(event.event_id.as_deref())?;
+ if seen_event_ids.insert(event_id) {
+ event_ids.push(event_id.to_owned());
+ }
+ }
+ let visibilities = event_store.event_visibilities(event_ids.iter()).await?;
+ let visibilities_by_event_id = event_ids
+ .into_iter()
+ .zip(visibilities)
+ .collect::<BTreeMap<_, _>>();
+
+ for event in &mut self.events {
+ if event.not_persisted || (!event.inserted && !event.duplicate) {
+ continue;
+ }
+ let event_id = required_persisted_fetch_receipt_event_id(event.event_id.as_deref())?;
+ let visibility = visibilities_by_event_id
+ .get(event_id)
+ .cloned()
+ .flatten()
+ .ok_or_else(
+ || RadrootsRelayTransportError::MissingStoredEventVisibility {
+ event_id: event_id.to_owned(),
+ },
+ )?;
+ event.visibility = relay_fetch_visibility(event_id, visibility)?;
+ }
+
+ self.admission_unsupported_count = 0;
+ self.admission_invalid_count = 0;
+ self.valid_stream_eligible_count = 0;
+ self.visible_count = 0;
+ self.not_admitted_count = 0;
+ self.not_current_count = 0;
+ self.suppressed_count = 0;
+ for event in &self.events {
+ match event.admission {
+ RadrootsRelayFetchEventAdmission::Unsupported => {
+ self.admission_unsupported_count += 1;
+ }
+ RadrootsRelayFetchEventAdmission::Invalid => self.admission_invalid_count += 1,
+ RadrootsRelayFetchEventAdmission::NotEvaluated
+ | RadrootsRelayFetchEventAdmission::Admitted => {}
+ }
+ if event.valid_stream == RadrootsRelayFetchEventValidStream::Eligible {
+ self.valid_stream_eligible_count += 1;
+ }
+ match event.visibility {
+ RadrootsRelayFetchEventVisibility::Visible => self.visible_count += 1,
+ RadrootsRelayFetchEventVisibility::NotAdmitted => {
+ self.not_admitted_count += 1;
+ }
+ RadrootsRelayFetchEventVisibility::NotCurrent => self.not_current_count += 1,
+ RadrootsRelayFetchEventVisibility::Suppressed => self.suppressed_count += 1,
+ RadrootsRelayFetchEventVisibility::NotEvaluated
+ | RadrootsRelayFetchEventVisibility::NotPersisted => {}
+ }
+ }
+ Ok(())
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum RadrootsRelayFetchRawBudgetExhaustion {
+ RawEvents,
+ RawJsonBytes,
+}
+
+impl RadrootsRelayFetchRawBudgetExhaustion {
+ const fn current_relay_message(self) -> &'static str {
+ match self {
+ Self::RawEvents => "raw event scan limit reached before relay EOSE",
+ Self::RawJsonBytes => "aggregate raw JSON byte limit reached before relay EOSE",
+ }
+ }
+
+ const fn unqueried_relay_message(self) -> &'static str {
+ match self {
+ Self::RawEvents => {
+ "relay was not queried because the global raw event scan limit was reached"
+ }
+ Self::RawJsonBytes => {
+ "relay was not queried because the global aggregate raw JSON byte limit was reached"
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct RadrootsRelayFetchRawBudget {
+ remaining_events: usize,
+ remaining_json_bytes: usize,
+ exhausted: Option<RadrootsRelayFetchRawBudgetExhaustion>,
+}
+
+impl RadrootsRelayFetchRawBudget {
+ const fn new(max_raw_events: usize, max_raw_json_bytes: usize) -> Self {
+ Self {
+ remaining_events: max_raw_events,
+ remaining_json_bytes: max_raw_json_bytes,
+ exhausted: None,
+ }
+ }
+
+ fn charge(
+ &mut self,
+ raw_json_bytes: usize,
+ ) -> Result<(), RadrootsRelayFetchRawBudgetExhaustion> {
+ if let Some(reason) = self.exhausted {
+ return Err(reason);
+ }
+ let Some(remaining_events) = self.remaining_events.checked_sub(1) else {
+ self.exhausted = Some(RadrootsRelayFetchRawBudgetExhaustion::RawEvents);
+ return Err(RadrootsRelayFetchRawBudgetExhaustion::RawEvents);
+ };
+ let Some(remaining_json_bytes) = self.remaining_json_bytes.checked_sub(raw_json_bytes)
+ else {
+ self.exhausted = Some(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes);
+ return Err(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes);
+ };
+ self.remaining_events = remaining_events;
+ self.remaining_json_bytes = remaining_json_bytes;
+ self.exhausted = if remaining_events == 0 {
+ Some(RadrootsRelayFetchRawBudgetExhaustion::RawEvents)
+ } else if remaining_json_bytes == 0 {
+ Some(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
+ } else {
+ None
+ };
+ Ok(())
+ }
+
+ const fn exhaustion_reason(self) -> Option<RadrootsRelayFetchRawBudgetExhaustion> {
+ self.exhausted
+ }
}
fn process_relay_fetch_items(
@@ -558,6 +850,7 @@ fn process_relay_fetch_items(
observed_at_ms: i64,
max_events: usize,
max_raw_events: usize,
+ max_raw_json_bytes: usize,
items: Vec<RadrootsRelayFetchItem>,
) -> Result<RadrootsRelayProcessedFetch, RadrootsRelayTransportError> {
if target_relays.is_empty() {
@@ -567,7 +860,7 @@ fn process_relay_fetch_items(
target_relays,
items: Vec::new(),
duplicate_count: 0,
- invalid_count: 0,
+ verification_failed_count: 0,
malformed_count: 0,
out_of_filter_count: 0,
skipped_over_limit_count: 0,
@@ -577,7 +870,7 @@ fn process_relay_fetch_items(
notice_count: 0,
relay_outcomes: Vec::new(),
};
- let mut scanned_raw_events = 0usize;
+ let mut raw_budget = RadrootsRelayFetchRawBudget::new(max_raw_events, max_raw_json_bytes);
let mut accepted_events = 0usize;
let mut seen_event_ids = BTreeSet::new();
let mut terminal_outcomes = BTreeMap::new();
@@ -614,11 +907,36 @@ fn process_relay_fetch_items(
}
match item {
RadrootsRelayFetchItem::Event { raw_json, .. } => {
- if scanned_raw_events >= max_raw_events {
+ if raw_budget.charge(raw_json.len()).is_err() {
processed.skipped_over_limit_count += 1;
continue;
}
- scanned_raw_events += 1;
+ if raw_json.len() > DEFAULT_RAW_JSON_MAX_BYTES {
+ processed.verification_failed_count += 1;
+ processed
+ .items
+ .push(RadrootsRelayProcessedFetchItem::Receipt(
+ RadrootsRelayFetchEventReceipt {
+ relay_url,
+ event_id: None,
+ inserted: false,
+ duplicate: false,
+ not_persisted: false,
+ malformed: false,
+ out_of_filter: false,
+ skipped_over_limit: false,
+ verification: RadrootsRelayFetchEventVerification::Failed,
+ admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
+ admission_code: None,
+ valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
+ visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
+ message: Some(format!(
+ "event raw JSON exceeds {DEFAULT_RAW_JSON_MAX_BYTES} byte limit"
+ )),
+ },
+ ));
+ continue;
+ }
let parsed = RadrootsNostrEvent::from_json(raw_json.as_str());
let Ok(raw_event) = parsed else {
processed.malformed_count += 1;
@@ -631,14 +949,14 @@ fn process_relay_fetch_items(
inserted: false,
duplicate: false,
not_persisted: false,
- unsupported: false,
- invalid: false,
malformed: true,
out_of_filter: false,
skipped_over_limit: false,
- valid_stream_eligible: false,
- admission_status: None,
+ verification: RadrootsRelayFetchEventVerification::NotEvaluated,
+ admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
admission_code: None,
+ valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
+ visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
message: Some("event JSON parse failed".to_owned()),
},
));
@@ -647,7 +965,7 @@ fn process_relay_fetch_items(
if let Err(error) =
RadrootsEventIngest::from_raw_json(raw_json.clone(), observed_at_ms)
{
- processed.invalid_count += 1;
+ processed.verification_failed_count += 1;
processed
.items
.push(RadrootsRelayProcessedFetchItem::Receipt(
@@ -657,14 +975,14 @@ fn process_relay_fetch_items(
inserted: false,
duplicate: false,
not_persisted: false,
- unsupported: false,
- invalid: true,
malformed: false,
out_of_filter: false,
skipped_over_limit: false,
- valid_stream_eligible: false,
- admission_status: None,
+ verification: RadrootsRelayFetchEventVerification::Failed,
+ admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
admission_code: None,
+ valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
+ visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
message: Some(error.to_string()),
},
));
@@ -681,14 +999,14 @@ fn process_relay_fetch_items(
inserted: false,
duplicate: false,
not_persisted: false,
- unsupported: false,
- invalid: false,
malformed: false,
out_of_filter: true,
skipped_over_limit: false,
- valid_stream_eligible: false,
- admission_status: None,
+ verification: RadrootsRelayFetchEventVerification::Verified,
+ admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
admission_code: None,
+ valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
+ visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
message: Some("event did not match relay fetch filters".to_owned()),
},
));
@@ -720,14 +1038,14 @@ fn process_relay_fetch_items(
inserted: false,
duplicate: false,
not_persisted: false,
- unsupported: false,
- invalid: false,
malformed: false,
out_of_filter: false,
skipped_over_limit: true,
- valid_stream_eligible: false,
- admission_status: None,
+ verification: RadrootsRelayFetchEventVerification::Verified,
+ admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
admission_code: None,
+ valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
+ visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
message: Some(
"accepted relay fetch event limit reached".to_owned(),
),
@@ -833,14 +1151,14 @@ fn accepted_fetch_event_receipt(
inserted: false,
duplicate: false,
not_persisted: false,
- unsupported: false,
- invalid: false,
malformed: false,
out_of_filter: false,
skipped_over_limit: false,
- valid_stream_eligible: false,
- admission_status: None,
+ verification: RadrootsRelayFetchEventVerification::Verified,
+ admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
admission_code: None,
+ valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
+ visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
message: Some("event accepted by relay fetch filters".to_owned()),
}
}
@@ -854,14 +1172,14 @@ fn duplicate_fetch_event_receipt(
inserted: false,
duplicate: true,
not_persisted: false,
- unsupported: false,
- invalid: false,
malformed: false,
out_of_filter: false,
skipped_over_limit: false,
- valid_stream_eligible: false,
- admission_status: None,
+ verification: RadrootsRelayFetchEventVerification::Verified,
+ admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
admission_code: None,
+ valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
+ visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
message: Some("event ID was already observed in this relay fetch".to_owned()),
}
}
@@ -899,17 +1217,16 @@ async fn fetch_from_nostr_relays(
}
let timeout = Duration::from_millis(request.timeout_ms);
let filters = request.filters.as_slice().to_vec();
- let mut remaining_raw_events = request.max_raw_events;
+ let mut raw_budget =
+ RadrootsRelayFetchRawBudget::new(request.max_raw_events, request.max_raw_json_bytes);
let mut items = Vec::new();
let relay_urls = request.relay_targets.relay_strings();
for (relay_index, relay_url) in relay_urls.iter().cloned().enumerate() {
- if remaining_raw_events == 0 {
+ if let Some(reason) = raw_budget.exhaustion_reason() {
items.extend(relay_urls[relay_index..].iter().cloned().map(|relay_url| {
RadrootsRelayFetchItem::Truncated {
relay_url,
- message:
- "relay was not queried because the global raw event scan limit was reached"
- .to_owned(),
+ message: reason.unqueried_relay_message().to_owned(),
}
}));
break;
@@ -931,36 +1248,46 @@ async fn fetch_from_nostr_relays(
continue;
}
let mut closed = false;
- let mut truncated = false;
+ let mut truncated_message = None;
for filter in filters.iter().cloned() {
- if remaining_raw_events == 0 {
- truncated = true;
+ if let Some(reason) = raw_budget.exhaustion_reason() {
+ truncated_message = Some(reason.current_relay_message().to_owned());
break;
}
let filter_limit = filter
.limit
- .unwrap_or(remaining_raw_events)
- .min(remaining_raw_events);
+ .unwrap_or(raw_budget.remaining_events)
+ .min(raw_budget.remaining_events);
match client
.stream_events(filter.limit(filter_limit), timeout)
.await
{
Ok(mut events) => {
loop {
- if remaining_raw_events == 0 {
- truncated = true;
+ if let Some(reason) = raw_budget.exhaustion_reason() {
+ truncated_message = Some(reason.current_relay_message().to_owned());
break;
}
let Some(event) = events.next().await else {
break;
};
+ let raw_json = event.as_json();
+ if let Err(reason) = raw_budget.charge(raw_json.len()) {
+ truncated_message = Some(reason.current_relay_message().to_owned());
+ break;
+ }
+ if raw_json.len() > DEFAULT_RAW_JSON_MAX_BYTES {
+ truncated_message = Some(format!(
+ "relay event raw JSON exceeds {DEFAULT_RAW_JSON_MAX_BYTES} byte limit"
+ ));
+ break;
+ }
items.push(RadrootsRelayFetchItem::Event {
relay_url: relay_url.clone(),
- raw_json: event.as_json(),
+ raw_json,
});
- remaining_raw_events -= 1;
}
- if truncated {
+ if truncated_message.is_some() {
break;
}
}
@@ -974,11 +1301,8 @@ async fn fetch_from_nostr_relays(
}
}
}
- if truncated {
- items.push(RadrootsRelayFetchItem::Truncated {
- relay_url,
- message: "raw event scan limit reached before relay EOSE".to_owned(),
- });
+ if let Some(message) = truncated_message {
+ items.push(RadrootsRelayFetchItem::Truncated { relay_url, message });
} else if !closed {
items.push(unproven_relay_stream_completion(relay_url));
}
@@ -1039,10 +1363,15 @@ fn fetch_item_lock_error<T>(_error: PoisonError<T>) -> RadrootsRelayTransportErr
#[cfg(test)]
mod tests {
use super::{
- RadrootsNostrEvent, RadrootsRelayFetchItem, relay_fetch_event_matches_filters,
- summarize_nostr_output_failures, unproven_relay_stream_completion,
+ RadrootsNostrEvent, RadrootsRelayFetchEventAdmission, RadrootsRelayFetchEventValidStream,
+ RadrootsRelayFetchEventVerification, RadrootsRelayFetchEventVisibility,
+ RadrootsRelayFetchItem, RadrootsRelayFetchRawBudget, RadrootsRelayFetchRawBudgetExhaustion,
+ RadrootsRelayTransportError, relay_fetch_event_matches_filters, relay_fetch_visibility,
+ required_persisted_fetch_receipt_event_id, summarize_nostr_output_failures,
+ unproven_relay_stream_completion,
};
use nostr::JsonUtil;
+ use radroots_event_store::{RadrootsEventVisibility, RadrootsNip09SuppressionReason};
use radroots_nostr::prelude::{
RadrootsNostrFilter, RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrSecretKey,
};
@@ -1098,4 +1427,156 @@ mod tests {
}
);
}
+
+ #[test]
+ fn raw_fetch_budget_allows_exact_boundaries_and_sticks_after_exhaustion() {
+ let mut count_budget = RadrootsRelayFetchRawBudget::new(1, 10);
+ assert_eq!(count_budget.charge(1), Ok(()));
+ assert_eq!(
+ count_budget.exhaustion_reason(),
+ Some(RadrootsRelayFetchRawBudgetExhaustion::RawEvents)
+ );
+ assert_eq!(
+ count_budget.charge(1),
+ Err(RadrootsRelayFetchRawBudgetExhaustion::RawEvents)
+ );
+
+ let mut byte_budget = RadrootsRelayFetchRawBudget::new(2, 5);
+ assert_eq!(byte_budget.charge(5), Ok(()));
+ assert_eq!(
+ byte_budget.exhaustion_reason(),
+ Some(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
+ );
+ assert_eq!(
+ byte_budget.charge(0),
+ Err(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
+ );
+ }
+
+ #[test]
+ fn raw_fetch_budget_rejects_crossing_without_partial_charge() {
+ let mut budget = RadrootsRelayFetchRawBudget::new(2, 4);
+ assert_eq!(
+ budget.charge(usize::MAX),
+ Err(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
+ );
+ assert_eq!(budget.remaining_events, 2);
+ assert_eq!(budget.remaining_json_bytes, 4);
+ assert_eq!(
+ budget.charge(1),
+ Err(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
+ );
+ assert_eq!(
+ RadrootsRelayFetchRawBudgetExhaustion::RawEvents.current_relay_message(),
+ "raw event scan limit reached before relay EOSE"
+ );
+ assert!(
+ RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes
+ .unqueried_relay_message()
+ .contains("aggregate raw JSON byte limit")
+ );
+ }
+
+ #[test]
+ fn event_store_visibility_maps_without_admission_collapse() {
+ let cases = [
+ (
+ RadrootsEventVisibility::Visible,
+ RadrootsRelayFetchEventVisibility::Visible,
+ ),
+ (
+ RadrootsEventVisibility::NotAdmitted,
+ RadrootsRelayFetchEventVisibility::NotAdmitted,
+ ),
+ (
+ RadrootsEventVisibility::NotCurrent {
+ raw_head_event_id: "head".to_owned(),
+ },
+ RadrootsRelayFetchEventVisibility::NotCurrent,
+ ),
+ (
+ RadrootsEventVisibility::Suppressed {
+ reason: RadrootsNip09SuppressionReason::EventIdReference,
+ event_reference_request_id: None,
+ address_reference_request_id: None,
+ address_reference_cutoff: None,
+ },
+ RadrootsRelayFetchEventVisibility::Suppressed,
+ ),
+ ];
+ for (source, expected) in cases {
+ assert_eq!(
+ relay_fetch_visibility("event", source).expect("known visibility"),
+ expected
+ );
+ }
+ }
+
+ #[test]
+ fn persisted_fetch_receipts_require_an_event_id() {
+ assert_eq!(
+ required_persisted_fetch_receipt_event_id(Some("event")).expect("event id"),
+ "event"
+ );
+ assert!(matches!(
+ required_persisted_fetch_receipt_event_id(None),
+ Err(RadrootsRelayTransportError::MissingPersistedFetchReceiptEventId)
+ ));
+ }
+
+ #[test]
+ fn event_processing_outcomes_have_stable_exhaustive_wire_values() {
+ let cases = [
+ serde_json::to_string(&RadrootsRelayFetchEventVerification::NotEvaluated)
+ .expect("verification"),
+ serde_json::to_string(&RadrootsRelayFetchEventVerification::Verified)
+ .expect("verification"),
+ serde_json::to_string(&RadrootsRelayFetchEventVerification::Failed)
+ .expect("verification"),
+ serde_json::to_string(&RadrootsRelayFetchEventAdmission::NotEvaluated)
+ .expect("admission"),
+ serde_json::to_string(&RadrootsRelayFetchEventAdmission::Admitted).expect("admission"),
+ serde_json::to_string(&RadrootsRelayFetchEventAdmission::Unsupported)
+ .expect("admission"),
+ serde_json::to_string(&RadrootsRelayFetchEventAdmission::Invalid).expect("admission"),
+ serde_json::to_string(&RadrootsRelayFetchEventValidStream::NotEvaluated)
+ .expect("valid stream"),
+ serde_json::to_string(&RadrootsRelayFetchEventValidStream::Eligible)
+ .expect("valid stream"),
+ serde_json::to_string(&RadrootsRelayFetchEventValidStream::Ineligible)
+ .expect("valid stream"),
+ serde_json::to_string(&RadrootsRelayFetchEventVisibility::NotEvaluated)
+ .expect("visibility"),
+ serde_json::to_string(&RadrootsRelayFetchEventVisibility::NotPersisted)
+ .expect("visibility"),
+ serde_json::to_string(&RadrootsRelayFetchEventVisibility::Visible).expect("visibility"),
+ serde_json::to_string(&RadrootsRelayFetchEventVisibility::NotAdmitted)
+ .expect("visibility"),
+ serde_json::to_string(&RadrootsRelayFetchEventVisibility::NotCurrent)
+ .expect("visibility"),
+ serde_json::to_string(&RadrootsRelayFetchEventVisibility::Suppressed)
+ .expect("visibility"),
+ ];
+ assert_eq!(
+ cases,
+ [
+ "\"not_evaluated\"",
+ "\"verified\"",
+ "\"failed\"",
+ "\"not_evaluated\"",
+ "\"admitted\"",
+ "\"unsupported\"",
+ "\"invalid\"",
+ "\"not_evaluated\"",
+ "\"eligible\"",
+ "\"ineligible\"",
+ "\"not_evaluated\"",
+ "\"not_persisted\"",
+ "\"visible\"",
+ "\"not_admitted\"",
+ "\"not_current\"",
+ "\"suppressed\"",
+ ]
+ );
+ }
}
diff --git a/crates/transport_nostr/src/lib.rs b/crates/transport_nostr/src/lib.rs
@@ -15,12 +15,15 @@ pub use error::RadrootsRelayTransportError;
pub use fetch::fetch_relay_events_blocking;
#[cfg(feature = "storage")]
pub use fetch::{
- RadrootsMockRelayFetchAdapter, RadrootsNostrClientFetchAdapter, RadrootsRelayFetchAdapter,
- RadrootsRelayFetchEventReceipt, RadrootsRelayFetchFailure, RadrootsRelayFetchFilters,
- RadrootsRelayFetchItem, RadrootsRelayFetchMode, RadrootsRelayFetchOutcomeKind,
- RadrootsRelayFetchReceipt, RadrootsRelayFetchRelayOutcome, RadrootsRelayFetchRequest,
- RadrootsRelayFetchedEvent, RadrootsRelayFetchedEventsReceipt, fetch_and_ingest_relay_events,
- fetch_relay_events,
+ RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX, RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX,
+ RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX, RadrootsMockRelayFetchAdapter,
+ RadrootsNostrClientFetchAdapter, RadrootsRelayFetchAdapter, RadrootsRelayFetchEventAdmission,
+ RadrootsRelayFetchEventReceipt, RadrootsRelayFetchEventValidStream,
+ RadrootsRelayFetchEventVerification, RadrootsRelayFetchEventVisibility,
+ RadrootsRelayFetchFailure, RadrootsRelayFetchFilters, RadrootsRelayFetchItem,
+ RadrootsRelayFetchMode, RadrootsRelayFetchOutcomeKind, RadrootsRelayFetchReceipt,
+ RadrootsRelayFetchRelayOutcome, RadrootsRelayFetchRequest, RadrootsRelayFetchedEvent,
+ RadrootsRelayFetchedEventsReceipt, fetch_and_ingest_relay_events, fetch_relay_events,
};
#[cfg(feature = "storage")]
pub use outbox::{
diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs
@@ -334,6 +334,7 @@ fn nostr_error_to_transport_error(error: RadrootsRelayTransportError) -> Radroot
RadrootsRelayTransportError::Transport(_) => RadrootsTransportError::InvalidTransportKind,
RadrootsRelayTransportError::EmptyFetchFilters
| RadrootsRelayTransportError::InvalidFetchLimit { .. }
+ | RadrootsRelayTransportError::FetchLimitTooLarge { .. }
| RadrootsRelayTransportError::InvalidTimestamp { .. }
| RadrootsRelayTransportError::InvalidIdempotencyKey { .. }
| RadrootsRelayTransportError::RequiredTargetNotRequested { .. } => {
@@ -342,7 +343,10 @@ fn nostr_error_to_transport_error(error: RadrootsRelayTransportError) -> Radroot
#[cfg(feature = "storage")]
RadrootsRelayTransportError::EventStore(_)
| RadrootsRelayTransportError::Outbox(_)
- | RadrootsRelayTransportError::MissingSignedOutboxEvent(_) => {
+ | RadrootsRelayTransportError::MissingSignedOutboxEvent(_)
+ | RadrootsRelayTransportError::MissingPersistedFetchReceiptEventId
+ | RadrootsRelayTransportError::MissingStoredEventVisibility { .. }
+ | RadrootsRelayTransportError::UnsupportedStoredEventVisibility { .. } => {
RadrootsTransportError::InvalidTransportKind
}
}
@@ -480,6 +484,14 @@ mod contract_tests {
RadrootsTransportError::InvalidTransportKind
);
assert_eq!(
+ nostr_error_to_transport_error(RadrootsRelayTransportError::FetchLimitTooLarge {
+ field: "max_events",
+ max: 1_000,
+ actual: 1_001,
+ }),
+ RadrootsTransportError::InvalidTransportKind
+ );
+ assert_eq!(
nostr_error_to_transport_error(RadrootsRelayTransportError::InvalidTimestamp {
field: "now_ms",
value: -1,
@@ -525,6 +537,28 @@ mod contract_tests {
),
RadrootsTransportError::InvalidTransportKind
);
+ assert_eq!(
+ nostr_error_to_transport_error(
+ RadrootsRelayTransportError::MissingPersistedFetchReceiptEventId,
+ ),
+ RadrootsTransportError::InvalidTransportKind
+ );
+ assert_eq!(
+ nostr_error_to_transport_error(
+ RadrootsRelayTransportError::MissingStoredEventVisibility {
+ event_id: "missing".to_owned(),
+ },
+ ),
+ RadrootsTransportError::InvalidTransportKind
+ );
+ assert_eq!(
+ nostr_error_to_transport_error(
+ RadrootsRelayTransportError::UnsupportedStoredEventVisibility {
+ event_id: "unsupported".to_owned(),
+ },
+ ),
+ RadrootsTransportError::InvalidTransportKind
+ );
}
}
}
diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs
@@ -1,10 +1,12 @@
use futures::future::BoxFuture;
use nostr::{EventBuilder, JsonUtil};
use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent};
-use radroots_event::kinds::{KIND_FOLLOW, KIND_GEOCHAT, KIND_POST};
+use radroots_event::kinds::{
+ KIND_DELETION_REQUEST, KIND_FOLLOW, KIND_GEOCHAT, KIND_POST, KIND_PROFILE,
+};
+use radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES;
use radroots_event_store::{
- RadrootsEventAdmissionStatus, RadrootsEventStore, RadrootsTransportObservationRow,
- RadrootsTransportObservationType,
+ RadrootsEventStore, RadrootsTransportObservationRow, RadrootsTransportObservationType,
};
use radroots_nostr::prelude::{
RadrootsNostrFilter, RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrSecretKey,
@@ -26,15 +28,19 @@ use radroots_transport::{
RadrootsTransportTargetLabel, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet,
};
use radroots_transport_nostr::{
- RadrootsMockRelayFetchAdapter, RadrootsMockRelayPublishAdapter, RadrootsNostrTransport,
- RadrootsOutboxPublishPolicy, RadrootsRelayFetchFilters, RadrootsRelayFetchItem,
- RadrootsRelayFetchMode, RadrootsRelayFetchOutcomeKind, RadrootsRelayFetchRequest,
- RadrootsRelayOutcome, RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter,
- RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTargetSet,
- RadrootsRelayTransportError, RadrootsRelayUrl, RadrootsRelayUrlPolicy,
- fetch_and_ingest_relay_events, fetch_relay_events, fetch_relay_events_blocking,
- publish_claimed_outbox_event, publish_claimed_outbox_event_with_transport,
- publish_signed_event, verified_signed_event_payload,
+ RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX, RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX,
+ RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX, RadrootsMockRelayFetchAdapter,
+ RadrootsMockRelayPublishAdapter, RadrootsNostrTransport, RadrootsOutboxPublishPolicy,
+ RadrootsRelayFetchEventAdmission, RadrootsRelayFetchEventValidStream,
+ RadrootsRelayFetchEventVerification, RadrootsRelayFetchEventVisibility,
+ RadrootsRelayFetchFilters, RadrootsRelayFetchItem, RadrootsRelayFetchMode,
+ RadrootsRelayFetchOutcomeKind, RadrootsRelayFetchRequest, RadrootsRelayOutcome,
+ RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter, RadrootsRelayPublishRelayReceipt,
+ RadrootsRelayPublishRequest, RadrootsRelayTargetSet, RadrootsRelayTransportError,
+ RadrootsRelayUrl, RadrootsRelayUrlPolicy, fetch_and_ingest_relay_events, fetch_relay_events,
+ fetch_relay_events_blocking, publish_claimed_outbox_event,
+ publish_claimed_outbox_event_with_transport, publish_signed_event,
+ verified_signed_event_payload,
};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
@@ -1792,14 +1798,21 @@ fn fetch_requests_reject_zero_limits_and_timeouts() {
request.clone().with_raw_event_scan_limit(0),
Err(RadrootsRelayTransportError::InvalidFetchLimit { field }) if field == "max_raw_events"
));
+ assert!(matches!(
+ request.clone().with_raw_json_byte_limit(0),
+ Err(RadrootsRelayTransportError::InvalidFetchLimit { field }) if field == "max_raw_json_bytes"
+ ));
let request = request
.with_timeout_ms(1)
.expect("minimum timeout")
.with_raw_event_scan_limit(1)
- .expect("minimum raw scan limit");
+ .expect("minimum raw scan limit")
+ .with_raw_json_byte_limit(1)
+ .expect("minimum raw JSON byte limit");
assert_eq!(request.timeout_ms(), 1);
assert_eq!(request.max_raw_events(), 1);
+ assert_eq!(request.max_raw_json_bytes(), 1);
let request = RadrootsRelayFetchRequest::subscription(
1_005,
@@ -1815,11 +1828,14 @@ fn fetch_requests_reject_zero_limits_and_timeouts() {
.with_timeout_ms(25)
.expect("timeout")
.with_raw_event_scan_limit(3)
- .expect("raw limit");
+ .expect("raw limit")
+ .with_raw_json_byte_limit(4_096)
+ .expect("raw JSON byte limit");
assert_eq!(request.mode(), RadrootsRelayFetchMode::Subscription);
assert_eq!(request.observed_at_ms(), 1_005);
assert_eq!(request.max_events(), 2);
assert_eq!(request.max_raw_events(), 3);
+ assert_eq!(request.max_raw_json_bytes(), 4_096);
assert_eq!(
request.relay_targets().relay_strings(),
vec![RELAY_PRIMARY_WSS.to_owned(), RELAY_SECONDARY_WSS.to_owned()]
@@ -1829,6 +1845,98 @@ fn fetch_requests_reject_zero_limits_and_timeouts() {
}
#[test]
+fn fetch_requests_enforce_the_coherent_visibility_batch_limit() {
+ let filter = post_relay_fetch_filter(RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX);
+ let fetch = RadrootsRelayFetchRequest::fetch(
+ 1_006,
+ RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX,
+ primary_relay_target(),
+ [filter.clone()],
+ )
+ .expect("maximum fetch request");
+ assert_eq!(fetch.max_events(), RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX);
+ assert_eq!(
+ fetch.max_raw_events(),
+ RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX
+ );
+ assert_eq!(
+ fetch.max_raw_json_bytes(),
+ RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX
+ );
+ let exact_raw_limits = fetch
+ .clone()
+ .with_raw_event_scan_limit(RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX)
+ .expect("maximum raw event limit")
+ .with_raw_json_byte_limit(RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX)
+ .expect("maximum raw JSON byte limit");
+ assert_eq!(
+ exact_raw_limits.max_raw_events(),
+ RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX
+ );
+ assert_eq!(
+ exact_raw_limits.max_raw_json_bytes(),
+ RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX
+ );
+ let subscription = RadrootsRelayFetchRequest::subscription(
+ 1_006,
+ RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX,
+ primary_relay_target(),
+ [filter.clone()],
+ )
+ .expect("maximum subscription request");
+ assert_eq!(
+ subscription.max_events(),
+ RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX
+ );
+
+ let above_max = RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX + 1;
+ assert!(matches!(
+ RadrootsRelayFetchRequest::fetch(
+ 1_006,
+ above_max,
+ primary_relay_target(),
+ [filter.clone()],
+ ),
+ Err(RadrootsRelayTransportError::FetchLimitTooLarge {
+ field: "max_events",
+ max,
+ actual,
+ }) if max == RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX && actual == above_max
+ ));
+ assert!(matches!(
+ RadrootsRelayFetchRequest::subscription(
+ 1_006,
+ above_max,
+ primary_relay_target(),
+ [filter],
+ ),
+ Err(RadrootsRelayTransportError::FetchLimitTooLarge {
+ field: "max_events",
+ max,
+ actual,
+ }) if max == RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX && actual == above_max
+ ));
+ assert!(matches!(
+ fetch
+ .clone()
+ .with_raw_event_scan_limit(RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX + 1),
+ Err(RadrootsRelayTransportError::FetchLimitTooLarge {
+ field: "max_raw_events",
+ max: RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX,
+ actual,
+ }) if actual == RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX + 1
+ ));
+ assert!(matches!(
+ fetch.with_raw_json_byte_limit(RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX + 1),
+ Err(RadrootsRelayTransportError::FetchLimitTooLarge {
+ field: "max_raw_json_bytes",
+ max: RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX,
+ actual,
+ }) if actual == RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX + 1
+ ));
+}
+
+#[test]
fn fetch_blocking_facade_runs_mock_adapter() {
let signed = signed_post("blocking fetch");
let accepted_id = signed.id_str().to_owned();
@@ -1895,10 +2003,16 @@ async fn fetch_verifies_events_before_acceptance_budgeting() {
assert_eq!(receipt.events.len(), 1);
assert_eq!(receipt.events[0].event.id.to_hex(), accepted_id);
- assert_eq!(receipt.invalid_count, 1);
+ assert_eq!(receipt.verification_failed_count, 1);
assert_eq!(receipt.skipped_over_limit_count, 0);
- assert!(receipt.event_receipts[0].invalid);
- assert!(!receipt.event_receipts[1].invalid);
+ assert_eq!(
+ receipt.event_receipts[0].verification,
+ RadrootsRelayFetchEventVerification::Failed
+ );
+ assert_eq!(
+ receipt.event_receipts[1].verification,
+ RadrootsRelayFetchEventVerification::Verified
+ );
}
#[tokio::test]
@@ -2110,8 +2224,14 @@ async fn fetch_ingests_events_and_records_transport_observations() {
assert_eq!(receipt.inserted_count, 3);
assert_eq!(receipt.duplicate_count, 1);
assert_eq!(receipt.not_persisted_count, 0);
- assert_eq!(receipt.unsupported_count, 1);
- assert_eq!(receipt.invalid_count, 2);
+ assert_eq!(receipt.verification_failed_count, 1);
+ assert_eq!(receipt.admission_unsupported_count, 1);
+ assert_eq!(receipt.admission_invalid_count, 1);
+ assert_eq!(receipt.valid_stream_eligible_count, 2);
+ assert_eq!(receipt.visible_count, 2);
+ assert_eq!(receipt.not_admitted_count, 2);
+ assert_eq!(receipt.not_current_count, 0);
+ assert_eq!(receipt.suppressed_count, 0);
assert_eq!(receipt.malformed_count, 1);
assert_eq!(receipt.eose_count, 1);
assert_eq!(receipt.closed_count, 2);
@@ -2137,16 +2257,28 @@ async fn fetch_ingests_events_and_records_transport_observations() {
.count()
);
assert_eq!(
- receipt.unsupported_count,
+ receipt.admission_unsupported_count,
+ receipt
+ .events
+ .iter()
+ .filter(|event| event.admission == RadrootsRelayFetchEventAdmission::Unsupported)
+ .count()
+ );
+ assert_eq!(
+ receipt.admission_invalid_count,
receipt
.events
.iter()
- .filter(|event| event.unsupported)
+ .filter(|event| event.admission == RadrootsRelayFetchEventAdmission::Invalid)
.count()
);
assert_eq!(
- receipt.invalid_count,
- receipt.events.iter().filter(|event| event.invalid).count()
+ receipt.verification_failed_count,
+ receipt
+ .events
+ .iter()
+ .filter(|event| event.verification == RadrootsRelayFetchEventVerification::Failed)
+ .count()
);
assert_eq!(
receipt.malformed_count,
@@ -2161,10 +2293,8 @@ async fn fetch_ingests_events_and_records_transport_observations() {
+ usize::from(event.duplicate)
+ usize::from(event.not_persisted)
<= 1
- && usize::from(event.unsupported)
- + usize::from(event.invalid)
- + usize::from(event.malformed)
- <= 1
+ && (!event.malformed
+ || event.verification == RadrootsRelayFetchEventVerification::NotEvaluated)
}));
assert_eq!(receipt.relay_outcomes.len(), 4);
assert_eq!(receipt.relay_outcomes[0].relay_url, RELAY_PRIMARY_WSS);
@@ -2197,44 +2327,82 @@ async fn fetch_ingests_events_and_records_transport_observations() {
);
assert!(receipt.relay_outcomes[3].relay_outcome.is_none());
assert_eq!(
- receipt.events[0].admission_status.as_deref(),
- Some(RadrootsEventAdmissionStatus::Admitted.as_str())
+ receipt.events[0].admission,
+ RadrootsRelayFetchEventAdmission::Admitted
+ );
+ assert_eq!(
+ receipt.events[0].valid_stream,
+ RadrootsRelayFetchEventValidStream::Eligible
+ );
+ assert_eq!(
+ receipt.events[0].visibility,
+ RadrootsRelayFetchEventVisibility::Visible
+ );
+ assert_eq!(
+ receipt.events[1].admission,
+ RadrootsRelayFetchEventAdmission::Admitted
+ );
+ assert_eq!(
+ receipt.events[1].valid_stream,
+ RadrootsRelayFetchEventValidStream::Eligible
);
- assert!(receipt.events[0].valid_stream_eligible);
assert_eq!(
- receipt.events[1].admission_status.as_deref(),
- Some(RadrootsEventAdmissionStatus::Admitted.as_str())
+ receipt.events[1].visibility,
+ RadrootsRelayFetchEventVisibility::Visible
);
- assert!(receipt.events[1].valid_stream_eligible);
assert_eq!(
- receipt.events[2].admission_status.as_deref(),
- Some(RadrootsEventAdmissionStatus::Unsupported.as_str())
+ receipt.events[2].admission,
+ RadrootsRelayFetchEventAdmission::Unsupported
);
- assert!(receipt.events[2].unsupported);
- assert!(!receipt.events[2].invalid);
assert_eq!(
receipt.events[2].admission_code.as_deref(),
Some("unsupported_kind")
);
- assert!(!receipt.events[2].valid_stream_eligible);
assert_eq!(
- receipt.events[3].admission_status.as_deref(),
- Some(RadrootsEventAdmissionStatus::Invalid.as_str())
+ receipt.events[2].valid_stream,
+ RadrootsRelayFetchEventValidStream::Ineligible
+ );
+ assert_eq!(
+ receipt.events[2].visibility,
+ RadrootsRelayFetchEventVisibility::NotAdmitted
+ );
+ assert_eq!(
+ receipt.events[3].admission,
+ RadrootsRelayFetchEventAdmission::Invalid
);
- assert!(!receipt.events[3].unsupported);
- assert!(receipt.events[3].invalid);
assert_eq!(
receipt.events[3].admission_code.as_deref(),
Some("reply_event_id_invalid")
);
- assert!(!receipt.events[3].valid_stream_eligible);
- assert_eq!(receipt.events[4].admission_status, None);
- assert!(!receipt.events[4].valid_stream_eligible);
- assert_eq!(receipt.events[5].admission_status, None);
- assert!(!receipt.events[5].valid_stream_eligible);
+ assert_eq!(
+ receipt.events[3].valid_stream,
+ RadrootsRelayFetchEventValidStream::Ineligible
+ );
+ assert_eq!(
+ receipt.events[3].visibility,
+ RadrootsRelayFetchEventVisibility::NotAdmitted
+ );
+ assert_eq!(
+ receipt.events[4].verification,
+ RadrootsRelayFetchEventVerification::Failed
+ );
+ assert_eq!(
+ receipt.events[4].admission,
+ RadrootsRelayFetchEventAdmission::NotEvaluated
+ );
+ assert_eq!(
+ receipt.events[5].verification,
+ RadrootsRelayFetchEventVerification::NotEvaluated
+ );
+ assert_eq!(
+ receipt.events[5].admission,
+ RadrootsRelayFetchEventAdmission::NotEvaluated
+ );
let serialized = serde_json::to_value(&receipt).expect("serialized fetch receipt");
- assert!(serialized.get("invalid_count").is_some());
+ assert!(serialized.get("verification_failed_count").is_some());
+ assert!(serialized.get("admission_invalid_count").is_some());
+ assert!(serialized.get("visible_count").is_some());
assert!(serialized.get("not_persisted_count").is_some());
assert!(serialized.get("truncated_count").is_some());
let serialized_event = serialized["events"][0]
@@ -2247,14 +2415,14 @@ async fn fetch_ingests_events_and_records_transport_observations() {
"inserted",
"duplicate",
"not_persisted",
- "unsupported",
- "invalid",
"malformed",
"out_of_filter",
"skipped_over_limit",
- "valid_stream_eligible",
- "admission_status",
+ "verification",
+ "admission",
"admission_code",
+ "valid_stream",
+ "visibility",
"message",
] {
assert!(
@@ -2263,7 +2431,7 @@ async fn fetch_ingests_events_and_records_transport_observations() {
);
}
assert!(!serialized_event.contains_key("projection_eligible"));
- assert!(!serialized_event.contains_key("verification_status"));
+ assert!(!serialized_event.contains_key("admission_status"));
let observations = store
.observations_for_event(signed.id_str())
@@ -2280,6 +2448,255 @@ async fn fetch_ingests_events_and_records_transport_observations() {
}
#[tokio::test]
+async fn fetch_reports_final_replaceable_visibility_when_newer_arrives_first() {
+ let newer = test_event_builder(KIND_PROFILE, r#"{"name":"newer"}"#, Vec::new())
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_001))
+ .sign_with_keys(&fixture_keys())
+ .expect("signed newer profile");
+ let older = test_event_builder(KIND_PROFILE, r#"{"name":"older"}"#, Vec::new())
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_000))
+ .sign_with_keys(&fixture_keys())
+ .expect("signed older profile");
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: newer.as_json(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: older.as_json(),
+ },
+ ]);
+ let filter = RadrootsNostrFilter::new()
+ .kind(RadrootsNostrKind::Custom(
+ u16::try_from(KIND_PROFILE).expect("profile kind must fit NIP-01"),
+ ))
+ .limit(2);
+ let request = RadrootsRelayFetchRequest::fetch(1_001, 2, primary_relay_target(), [filter])
+ .expect("profile fetch request");
+
+ let receipt = fetch_and_ingest_relay_events(&adapter, &store, request)
+ .await
+ .expect("fetch ingest");
+
+ assert_eq!(receipt.inserted_count, 2);
+ assert_eq!(receipt.valid_stream_eligible_count, 2);
+ assert_eq!(receipt.visible_count, 1);
+ assert_eq!(receipt.not_current_count, 1);
+ assert_eq!(
+ receipt.events[0].visibility,
+ RadrootsRelayFetchEventVisibility::Visible
+ );
+ assert_eq!(
+ receipt.events[1].admission,
+ RadrootsRelayFetchEventAdmission::Admitted
+ );
+ assert_eq!(
+ receipt.events[1].valid_stream,
+ RadrootsRelayFetchEventValidStream::Eligible
+ );
+ assert_eq!(
+ receipt.events[1].visibility,
+ RadrootsRelayFetchEventVisibility::NotCurrent
+ );
+}
+
+#[tokio::test]
+async fn fetch_reports_final_replaceable_visibility_when_older_arrives_first() {
+ let older = test_event_builder(KIND_PROFILE, r#"{"name":"older"}"#, Vec::new())
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_000))
+ .sign_with_keys(&fixture_keys())
+ .expect("signed older profile");
+ let newer = test_event_builder(KIND_PROFILE, r#"{"name":"newer"}"#, Vec::new())
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_001))
+ .sign_with_keys(&fixture_keys())
+ .expect("signed newer profile");
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: older.as_json(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: newer.as_json(),
+ },
+ ]);
+ let filter = RadrootsNostrFilter::new()
+ .kind(RadrootsNostrKind::Custom(
+ u16::try_from(KIND_PROFILE).expect("profile kind must fit NIP-01"),
+ ))
+ .limit(2);
+ let request = RadrootsRelayFetchRequest::fetch(1_001, 2, primary_relay_target(), [filter])
+ .expect("profile fetch request");
+
+ let receipt = fetch_and_ingest_relay_events(&adapter, &store, request)
+ .await
+ .expect("fetch ingest");
+
+ assert_eq!(receipt.inserted_count, 2);
+ assert_eq!(receipt.valid_stream_eligible_count, 2);
+ assert_eq!(receipt.visible_count, 1);
+ assert_eq!(receipt.not_current_count, 1);
+ assert_eq!(
+ receipt.events[0].visibility,
+ RadrootsRelayFetchEventVisibility::NotCurrent
+ );
+ assert_eq!(
+ receipt.events[1].visibility,
+ RadrootsRelayFetchEventVisibility::Visible
+ );
+}
+
+#[tokio::test]
+async fn fetch_maps_one_final_visibility_snapshot_back_to_duplicate_receipts() {
+ let older = test_event_builder(KIND_PROFILE, r#"{"name":"older"}"#, Vec::new())
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_000))
+ .sign_with_keys(&fixture_keys())
+ .expect("signed older profile");
+ let newer = test_event_builder(KIND_PROFILE, r#"{"name":"newer"}"#, Vec::new())
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_001))
+ .sign_with_keys(&fixture_keys())
+ .expect("signed newer profile");
+ let older_id = older.id.to_hex();
+ let newer_id = newer.id.to_hex();
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: older.as_json(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: newer.as_json(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_SECONDARY_WSS.to_owned(),
+ raw_json: older.as_json(),
+ },
+ ]);
+ let filter = RadrootsNostrFilter::new()
+ .kind(RadrootsNostrKind::Custom(
+ u16::try_from(KIND_PROFILE).expect("profile kind must fit NIP-01"),
+ ))
+ .limit(2);
+ let targets = RadrootsRelayTargetSet::new(
+ [RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS],
+ RadrootsRelayUrlPolicy::Public,
+ )
+ .expect("relay targets");
+ let request = RadrootsRelayFetchRequest::fetch(1_001, 2, targets, [filter])
+ .expect("profile fetch request");
+
+ let receipt = fetch_and_ingest_relay_events(&adapter, &store, request)
+ .await
+ .expect("fetch ingest");
+
+ assert_eq!(receipt.inserted_count, 2);
+ assert_eq!(receipt.duplicate_count, 1);
+ assert_eq!(receipt.valid_stream_eligible_count, 3);
+ assert_eq!(receipt.visible_count, 1);
+ assert_eq!(receipt.not_current_count, 2);
+ assert_eq!(receipt.events.len(), 3);
+ assert_eq!(
+ receipt
+ .events
+ .iter()
+ .map(|event| event.event_id.as_deref())
+ .collect::<Vec<_>>(),
+ vec![
+ Some(older_id.as_str()),
+ Some(newer_id.as_str()),
+ Some(older_id.as_str()),
+ ]
+ );
+ assert_eq!(
+ receipt
+ .events
+ .iter()
+ .map(|event| event.visibility)
+ .collect::<Vec<_>>(),
+ vec![
+ RadrootsRelayFetchEventVisibility::NotCurrent,
+ RadrootsRelayFetchEventVisibility::Visible,
+ RadrootsRelayFetchEventVisibility::NotCurrent,
+ ]
+ );
+ assert!(receipt.events[2].duplicate);
+}
+
+#[tokio::test]
+async fn fetch_reports_store_suppression_when_deletion_precedes_target_replay() {
+ let target = test_event_builder(KIND_POST, "deleted target", Vec::new())
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_000))
+ .sign_with_keys(&fixture_keys())
+ .expect("signed target");
+ let deletion = test_event_builder(
+ KIND_DELETION_REQUEST,
+ "",
+ vec![vec!["e".to_owned(), target.id.to_hex()]],
+ )
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_001))
+ .sign_with_keys(&fixture_keys())
+ .expect("signed deletion");
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: deletion.as_json(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: target.as_json(),
+ },
+ ]);
+ let deletion_filter = RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom(
+ u16::try_from(KIND_DELETION_REQUEST).expect("deletion kind must fit NIP-01"),
+ ));
+ let post_filter = RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom(
+ u16::try_from(KIND_POST).expect("post kind must fit NIP-01"),
+ ));
+ let request = RadrootsRelayFetchRequest::fetch(
+ 1_002,
+ 2,
+ primary_relay_target(),
+ [deletion_filter, post_filter],
+ )
+ .expect("deletion replay fetch request");
+
+ let receipt = fetch_and_ingest_relay_events(&adapter, &store, request)
+ .await
+ .expect("fetch ingest");
+
+ assert_eq!(receipt.inserted_count, 2);
+ assert_eq!(receipt.valid_stream_eligible_count, 2);
+ assert_eq!(receipt.visible_count, 1);
+ assert_eq!(receipt.suppressed_count, 1);
+ assert_eq!(receipt.not_current_count, 0);
+ assert_eq!(
+ receipt.events[0].visibility,
+ RadrootsRelayFetchEventVisibility::Visible
+ );
+ assert_eq!(
+ receipt.events[1].event_id.as_deref(),
+ Some(target.id.to_hex().as_str())
+ );
+ assert_eq!(
+ receipt.events[1].admission,
+ RadrootsRelayFetchEventAdmission::Admitted
+ );
+ assert_eq!(
+ receipt.events[1].valid_stream,
+ RadrootsRelayFetchEventValidStream::Eligible
+ );
+ assert_eq!(
+ receipt.events[1].visibility,
+ RadrootsRelayFetchEventVisibility::Suppressed
+ );
+}
+
+#[tokio::test]
async fn fetch_reports_ephemeral_events_as_not_persisted_without_duplicate_or_store_state() {
let signed = signed_ephemeral("live geochat");
let event_id = signed.id_str().to_owned();
@@ -2310,17 +2727,20 @@ async fn fetch_reports_ephemeral_events_as_not_persisted_without_duplicate_or_st
assert_eq!(receipt.duplicate_count, 0);
assert_eq!(receipt.not_persisted_count, 2);
assert_eq!(receipt.malformed_count, 0);
- assert_eq!(receipt.unsupported_count, 0);
- assert_eq!(receipt.invalid_count, 0);
+ assert_eq!(receipt.verification_failed_count, 0);
+ assert_eq!(receipt.admission_unsupported_count, 0);
+ assert_eq!(receipt.admission_invalid_count, 0);
+ assert_eq!(receipt.valid_stream_eligible_count, 0);
assert_eq!(receipt.events.len(), 2);
assert!(receipt.events.iter().all(|event| {
!event.inserted
&& !event.duplicate
&& event.not_persisted
- && event.admission_status.as_deref()
- == Some(RadrootsEventAdmissionStatus::Admitted.as_str())
+ && event.verification == RadrootsRelayFetchEventVerification::Verified
+ && event.admission == RadrootsRelayFetchEventAdmission::Admitted
&& event.admission_code.is_none()
- && !event.valid_stream_eligible
+ && event.valid_stream == RadrootsRelayFetchEventValidStream::Ineligible
+ && event.visibility == RadrootsRelayFetchEventVisibility::NotPersisted
}));
assert!(
store
@@ -2389,7 +2809,7 @@ async fn fetch_rejects_out_of_filter_events_before_store_mutation() {
assert_eq!(receipt.inserted_count, 1);
assert_eq!(receipt.out_of_filter_count, 2);
assert_eq!(receipt.malformed_count, 0);
- assert_eq!(receipt.unsupported_count, 0);
+ assert_eq!(receipt.admission_unsupported_count, 0);
assert_eq!(receipt.events.len(), 3);
assert!(receipt.events[0].out_of_filter);
assert!(!receipt.events[1].out_of_filter);
@@ -2463,7 +2883,7 @@ async fn fetch_event_cap_counts_accepted_in_filter_events_and_preserves_later_co
assert_eq!(receipt.inserted_count, 1);
assert_eq!(receipt.duplicate_count, 0);
- assert_eq!(receipt.unsupported_count, 0);
+ assert_eq!(receipt.admission_unsupported_count, 0);
assert_eq!(receipt.malformed_count, 1);
assert_eq!(receipt.out_of_filter_count, 1);
assert_eq!(receipt.skipped_over_limit_count, 1);
@@ -2642,6 +3062,169 @@ async fn fetch_raw_scan_limit_bounds_noisy_adapter_output() {
}
#[tokio::test]
+async fn fetch_raw_json_byte_limit_is_exact_global_and_sticky() {
+ let first = signed_post("raw byte first");
+ let second = signed_post("raw byte second");
+ let third = signed_post("raw byte third");
+ let exact_bytes = first.raw_json().len() + second.raw_json().len();
+ let targets = RadrootsRelayTargetSet::new(
+ [RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS],
+ RadrootsRelayUrlPolicy::Public,
+ )
+ .expect("relay targets");
+ let items = vec![
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: first.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_SECONDARY_WSS.to_owned(),
+ raw_json: second.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: third.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Eose {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ },
+ RadrootsRelayFetchItem::Eose {
+ relay_url: RELAY_SECONDARY_WSS.to_owned(),
+ },
+ ];
+
+ let exact_receipt = fetch_relay_events(
+ &RadrootsMockRelayFetchAdapter::new(items.clone()),
+ RadrootsRelayFetchRequest::fetch(1_131, 3, targets.clone(), [post_relay_fetch_filter(3)])
+ .expect("fetch request")
+ .with_raw_json_byte_limit(exact_bytes)
+ .expect("exact raw JSON byte limit"),
+ )
+ .await
+ .expect("exact fetch");
+ assert_eq!(exact_receipt.events.len(), 2);
+ assert_eq!(exact_receipt.skipped_over_limit_count, 1);
+ assert_eq!(exact_receipt.eose_count, 2);
+
+ let crossing_receipt = fetch_relay_events(
+ &RadrootsMockRelayFetchAdapter::new(items),
+ RadrootsRelayFetchRequest::fetch(1_132, 3, targets, [post_relay_fetch_filter(3)])
+ .expect("fetch request")
+ .with_raw_json_byte_limit(exact_bytes - 1)
+ .expect("crossing raw JSON byte limit"),
+ )
+ .await
+ .expect("crossing fetch");
+ assert_eq!(crossing_receipt.events.len(), 1);
+ assert_eq!(crossing_receipt.skipped_over_limit_count, 2);
+ assert_eq!(crossing_receipt.eose_count, 2);
+}
+
+#[tokio::test]
+async fn fetch_raw_json_budget_charges_every_preparse_event_class() {
+ let malformed = "{not json".to_owned();
+ let wrong_tag = signed_event_with_kind_and_hashtag("raw bytes wrong tag", KIND_POST, "compost");
+ let accepted = signed_post("raw bytes accepted");
+ let over_accepted_limit = signed_post("raw bytes over accepted limit");
+ let sticky_skip = signed_post("raw bytes sticky skip");
+ let exact_bytes = malformed.len()
+ + wrong_tag.raw_json().len()
+ + accepted.raw_json().len() * 2
+ + over_accepted_limit.raw_json().len();
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: malformed,
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: wrong_tag.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: accepted.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_SECONDARY_WSS.to_owned(),
+ raw_json: accepted.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_SECONDARY_WSS.to_owned(),
+ raw_json: over_accepted_limit.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_SECONDARY_WSS.to_owned(),
+ raw_json: sticky_skip.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Eose {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ },
+ RadrootsRelayFetchItem::Eose {
+ relay_url: RELAY_SECONDARY_WSS.to_owned(),
+ },
+ ]);
+ let targets = RadrootsRelayTargetSet::new(
+ [RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS],
+ RadrootsRelayUrlPolicy::Public,
+ )
+ .expect("relay targets");
+ let store = RadrootsEventStore::open_memory().await.expect("store");
+
+ let receipt = fetch_and_ingest_relay_events(
+ &adapter,
+ &store,
+ RadrootsRelayFetchRequest::fetch(1_133, 1, targets, [post_relay_fetch_filter(6)])
+ .expect("fetch request")
+ .with_raw_json_byte_limit(exact_bytes)
+ .expect("raw JSON byte limit"),
+ )
+ .await
+ .expect("fetch ingest");
+
+ assert_eq!(receipt.inserted_count, 1);
+ assert_eq!(receipt.malformed_count, 1);
+ assert_eq!(receipt.out_of_filter_count, 1);
+ assert_eq!(receipt.duplicate_count, 1);
+ assert_eq!(receipt.skipped_over_limit_count, 2);
+ assert_eq!(receipt.events.len(), 5);
+ assert!(receipt.events[4].skipped_over_limit);
+ assert_eq!(receipt.eose_count, 2);
+}
+
+#[tokio::test]
+async fn fetch_rejects_oversized_raw_json_before_radroots_adapter_parsing() {
+ let accepted = signed_post("after oversized raw event");
+ let adapter = RadrootsMockRelayFetchAdapter::new(vec![
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: "x".repeat(DEFAULT_RAW_JSON_MAX_BYTES + 1),
+ },
+ RadrootsRelayFetchItem::Event {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ raw_json: accepted.raw_json().to_owned(),
+ },
+ RadrootsRelayFetchItem::Eose {
+ relay_url: RELAY_PRIMARY_WSS.to_owned(),
+ },
+ ]);
+
+ let receipt = fetch_relay_events(&adapter, post_relay_fetch_request(1_134, 1))
+ .await
+ .expect("fetch events");
+
+ assert_eq!(receipt.events.len(), 1);
+ assert_eq!(receipt.verification_failed_count, 1);
+ assert_eq!(receipt.malformed_count, 0);
+ assert_eq!(receipt.event_receipts.len(), 2);
+ assert_eq!(receipt.event_receipts[0].event_id, None);
+ assert_eq!(
+ receipt.event_receipts[0].verification,
+ RadrootsRelayFetchEventVerification::Failed
+ );
+ assert_eq!(receipt.eose_count, 1);
+}
+
+#[tokio::test]
async fn fetch_subscription_mode_and_store_errors_are_propagated() {
let signed = signed_post("subscription");
let store = RadrootsEventStore::open_memory().await.expect("store");
@@ -4563,15 +5146,15 @@ async fn smoke_relay_fetch_processes_one_thousand_event_receipts() {
assert_eq!(receipt.inserted_count, 1_000);
assert_eq!(receipt.duplicate_count, 0);
assert_eq!(receipt.malformed_count, 0);
- assert_eq!(receipt.unsupported_count, 0);
- assert_eq!(receipt.invalid_count, 0);
+ assert_eq!(receipt.verification_failed_count, 0);
+ assert_eq!(receipt.admission_unsupported_count, 0);
+ assert_eq!(receipt.admission_invalid_count, 0);
+ assert_eq!(receipt.valid_stream_eligible_count, 1_000);
+ assert_eq!(receipt.visible_count, 1_000);
assert_eq!(receipt.events.len(), 1_000);
- assert!(
- receipt
- .events
- .iter()
- .all(|event| event.valid_stream_eligible)
- );
+ assert!(receipt.events.iter().all(|event| event.valid_stream
+ == RadrootsRelayFetchEventValidStream::Eligible
+ && event.visibility == RadrootsRelayFetchEventVisibility::Visible));
let replay = store.valid_stream_after(0, 1_000).await.expect("replay");
assert_eq!(replay.len(), 1_000);
}
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -4,9 +4,13 @@ mod admission_authority;
mod artifact_bundle;
mod comment_authority;
mod deletion_authority;
+mod food_availability_projection;
mod nip09_reconciliation;
mod registry_v7;
+pub(crate) use food_availability_projection::{
+ validate_food_availability_projection_manifest, write_food_availability_projection_manifest,
+};
pub(crate) use nip09_reconciliation::{
validate_nip09_reconciliation_manifest, write_nip09_reconciliation_manifest,
};
@@ -42,8 +46,12 @@ const CONFORMANCE_ROOT_RELATIVE: &str = "contracts/conformance";
const CONFORMANCE_SCHEMA_RELATIVE: &str = "contracts/conformance/schema/vector.schema.json";
const NIP09_RECONCILIATION_CONFORMANCE_VECTOR_RELATIVE: &str =
"contracts/conformance/vectors/event_store/nip09_reconciliation.v1.json";
-const SPECIALIZED_CONFORMANCE_VECTOR_RELATIVES: [&str; 1] =
- [NIP09_RECONCILIATION_CONFORMANCE_VECTOR_RELATIVE];
+const FOOD_AVAILABILITY_PROJECTION_CONFORMANCE_VECTOR_RELATIVE: &str =
+ "contracts/conformance/vectors/event_store/food_availability_projection.v1.json";
+const SPECIALIZED_CONFORMANCE_VECTOR_RELATIVES: [&str; 2] = [
+ NIP09_RECONCILIATION_CONFORMANCE_VECTOR_RELATIVE,
+ FOOD_AVAILABILITY_PROJECTION_CONFORMANCE_VECTOR_RELATIVE,
+];
const KNOWLEDGE_MANIFEST_RELATIVE: &str =
"contracts/knowledge/knowledge_event_contract_manifest.v2.json";
const KNOWLEDGE_MANIFEST_SHA256_RELATIVE: &str =
@@ -63,7 +71,7 @@ const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract";
const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION";
const REPLICA_TRANSFER_VERSION: u32 = 2;
const VENDORED_WORKSPACE_MEMBER_RELATIVE: &str = "crates/libsqlite3_sys_3_53_3";
-const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 20] = [
+const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 21] = [
(
"contracts/conformance/vectors/blossom/bud11_claims.v1.json",
"crates/blossom/tests/fixtures/bud11_claims.v1.json",
@@ -105,6 +113,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 20] = [
"crates/event_store/tests/fixtures/nip09_reconciliation.v1.json",
),
(
+ FOOD_AVAILABILITY_PROJECTION_CONFORMANCE_VECTOR_RELATIVE,
+ "crates/event_store/tests/fixtures/food_availability_projection.v1.json",
+ ),
+ (
"contracts/conformance/vectors/events/operational_listing_tags_full.v1.json",
"crates/event_codec/tests/fixtures/operational_listing_tags_full.v1.json",
),
@@ -1379,6 +1391,10 @@ pub struct ReplicaContractPolicy {
pub classified_listing_operational_projection: String,
pub classified_listing_excluded_or_rejected_head: String,
pub classified_listing_head_only_ingest: String,
+ pub legacy_bare_envelope_ingest: String,
+ pub legacy_ingest_feature: String,
+ pub phase_1_ingest_replacement: String,
+ pub future_product_ingest_input: String,
}
#[derive(Debug, Deserialize)]
@@ -4994,7 +5010,190 @@ fn parse_replica_transfer_constant(path: &Path, name: &str) -> Result<u32, Strin
})
}
+fn has_exact_legacy_ingest_cfg(attributes: &[syn::Attribute]) -> bool {
+ let mut cfg_attributes = attributes
+ .iter()
+ .filter(|attribute| attribute.path().is_ident("cfg"));
+ let Some(attribute) = cfg_attributes.next() else {
+ return false;
+ };
+ if cfg_attributes.next().is_some() {
+ return false;
+ }
+ let syn::Meta::List(arguments) = &attribute.meta else {
+ return false;
+ };
+ let Ok(predicate) = arguments.parse_args::<syn::Meta>() else {
+ return false;
+ };
+ let syn::Meta::NameValue(feature) = predicate else {
+ return false;
+ };
+ if !feature.path.is_ident("feature") {
+ return false;
+ }
+ matches!(
+ feature.value,
+ syn::Expr::Lit(syn::ExprLit {
+ lit: syn::Lit::Str(value),
+ ..
+ }) if value.value() == "legacy-ingest"
+ )
+}
+
+fn replica_use_tree_references_ingest(tree: &syn::UseTree) -> bool {
+ match tree {
+ syn::UseTree::Path(path) => {
+ path.ident == "ingest" || replica_use_tree_references_ingest(&path.tree)
+ }
+ syn::UseTree::Name(name) => name.ident == "ingest",
+ syn::UseTree::Rename(rename) => rename.ident == "ingest",
+ syn::UseTree::Group(group) => group.items.iter().any(replica_use_tree_references_ingest),
+ syn::UseTree::Glob(_) => false,
+ }
+}
+
+fn collect_public_replica_ingest_exports<'a>(
+ items: &'a [syn::Item],
+ exports: &mut Vec<&'a syn::ItemUse>,
+) {
+ for item in items {
+ match item {
+ syn::Item::Use(export)
+ if matches!(&export.vis, syn::Visibility::Public(_))
+ && replica_use_tree_references_ingest(&export.tree) =>
+ {
+ exports.push(export);
+ }
+ syn::Item::Mod(module) if matches!(&module.vis, syn::Visibility::Public(_)) => {
+ if let Some((_, nested_items)) = &module.content {
+ collect_public_replica_ingest_exports(nested_items, exports);
+ }
+ }
+ _ => {}
+ }
+ }
+}
+
+fn validate_replica_legacy_ingest_exports(lib_path: &Path, source: &str) -> Result<(), String> {
+ let syntax = syn::parse_file(source)
+ .map_err(|error| format!("parse replica sync source {}: {error}", lib_path.display()))?;
+ let ingest_modules = syntax
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ syn::Item::Mod(module) if module.ident == "ingest" => Some(module),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ if ingest_modules.len() != 1 {
+ return Err(format!(
+ "replica legacy ingest source {} must declare exactly one ingest module",
+ lib_path.display()
+ ));
+ }
+ let ingest_module = ingest_modules[0];
+ if !matches!(&ingest_module.vis, syn::Visibility::Public(_)) {
+ return Err(format!(
+ "replica legacy ingest module in {} must remain public",
+ lib_path.display()
+ ));
+ }
+ if !has_exact_legacy_ingest_cfg(&ingest_module.attrs) {
+ return Err(format!(
+ "replica legacy ingest module in {} must be guarded by exact #[cfg(feature = \"legacy-ingest\")]",
+ lib_path.display()
+ ));
+ }
+
+ let mut ingest_exports = Vec::new();
+ collect_public_replica_ingest_exports(&syntax.items, &mut ingest_exports);
+ if ingest_exports.is_empty() {
+ return Err(format!(
+ "replica legacy ingest source {} must publicly re-export the ingest API",
+ lib_path.display()
+ ));
+ }
+ if ingest_exports
+ .iter()
+ .any(|export| !has_exact_legacy_ingest_cfg(&export.attrs))
+ {
+ return Err(format!(
+ "every public replica ingest re-export in {} must be guarded by exact #[cfg(feature = \"legacy-ingest\")]",
+ lib_path.display()
+ ));
+ }
+ Ok(())
+}
+
fn validate_replica_policy_source_witnesses(sync_root: &Path) -> Result<(), String> {
+ let cargo_path = sync_root.join("Cargo.toml");
+ let cargo_source = fs::read_to_string(&cargo_path)
+ .map_err(|error| format!("read {}: {error}", cargo_path.display()))?;
+ let cargo: toml::Value = toml::from_str(&cargo_source)
+ .map_err(|error| format!("parse {}: {error}", cargo_path.display()))?;
+ let features = cargo
+ .get("features")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("replica sync {} must define features", cargo_path.display()))?;
+ let default_features = features
+ .get("default")
+ .and_then(toml::Value::as_array)
+ .ok_or_else(|| {
+ format!(
+ "replica sync {} must define default features",
+ cargo_path.display()
+ )
+ })?;
+ let mut pending_default_features = default_features
+ .iter()
+ .filter_map(toml::Value::as_str)
+ .collect::<Vec<_>>();
+ let mut visited_default_features = BTreeSet::new();
+ while let Some(feature) = pending_default_features.pop() {
+ if !visited_default_features.insert(feature) {
+ continue;
+ }
+ if feature == "legacy-ingest" {
+ return Err(format!(
+ "replica legacy-ingest must not be enabled by default features in {}",
+ cargo_path.display()
+ ));
+ }
+ if let Some(members) = features.get(feature).and_then(toml::Value::as_array) {
+ pending_default_features.extend(
+ members
+ .iter()
+ .filter_map(toml::Value::as_str)
+ .filter(|member| features.contains_key(*member)),
+ );
+ }
+ }
+ let legacy_features = features
+ .get("legacy-ingest")
+ .and_then(toml::Value::as_array)
+ .ok_or_else(|| {
+ format!(
+ "replica sync {} must define the explicit legacy-ingest feature",
+ cargo_path.display()
+ )
+ })?;
+ if !legacy_features
+ .iter()
+ .filter_map(toml::Value::as_str)
+ .any(|feature| feature == "std")
+ {
+ return Err(format!(
+ "replica legacy-ingest feature in {} must enable std",
+ cargo_path.display()
+ ));
+ }
+
+ let lib_path = sync_root.join("src/lib.rs");
+ let lib_source = fs::read_to_string(&lib_path)
+ .map_err(|error| format!("read {}: {error}", lib_path.display()))?;
+ validate_replica_legacy_ingest_exports(&lib_path, &lib_source)?;
+
let types_path = sync_root.join("src/types.rs");
let types_source = fs::read_to_string(&types_path)
.map_err(|error| format!("read {}: {error}", types_path.display()))?;
@@ -5200,6 +5399,26 @@ fn validate_replica_contract(bundle: &ContractBundle, workspace_root: &Path) ->
replica.policy.classified_listing_head_only_ingest.as_str(),
"reject_require_profile_aware",
),
+ (
+ "legacy_bare_envelope_ingest",
+ replica.policy.legacy_bare_envelope_ingest.as_str(),
+ "explicit_non_default_feature_only",
+ ),
+ (
+ "legacy_ingest_feature",
+ replica.policy.legacy_ingest_feature.as_str(),
+ "legacy-ingest",
+ ),
+ (
+ "phase_1_ingest_replacement",
+ replica.policy.phase_1_ingest_replacement.as_str(),
+ "none",
+ ),
+ (
+ "future_product_ingest_input",
+ replica.policy.future_product_ingest_input.as_str(),
+ "store_produced_verified_valid_visible_admission",
+ ),
] {
if actual != expected {
return Err(format!("replica policy.{field} must be {expected}"));
@@ -8689,6 +8908,20 @@ name = "radroots_b"
version = "1.0.0"
edition = "2024"
publish = false
+
+[features]
+default = ["std"]
+std = []
+legacy-ingest = ["std"]
+"#,
+ );
+ write_file(
+ &root.join("crates").join("b").join("src").join("lib.rs"),
+ r#"#[cfg(feature = "legacy-ingest")]
+pub mod ingest;
+
+#[cfg(feature = "legacy-ingest")]
+pub use ingest::{radroots_replica_ingest_event, RadrootsReplicaIngestOutcome};
"#,
);
write_file(
@@ -8810,6 +9043,10 @@ classified_listing_head_selection = "raw_before_profile"
classified_listing_operational_projection = "operational_partition_only"
classified_listing_excluded_or_rejected_head = "remove_projection_and_advance"
classified_listing_head_only_ingest = "reject_require_profile_aware"
+legacy_bare_envelope_ingest = "explicit_non_default_feature_only"
+legacy_ingest_feature = "legacy-ingest"
+phase_1_ingest_replacement = "none"
+future_product_ingest_input = "store_produced_verified_valid_visible_admission"
[transfer]
version = 2
@@ -10073,6 +10310,24 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"]
"allow_head_only".to_string();
},
);
+ assert_replica_error(
+ "legacy_bare_envelope_ingest must be explicit_non_default_feature_only",
+ |bundle| {
+ bundle.replica.policy.legacy_bare_envelope_ingest = "default".to_string();
+ },
+ );
+ assert_replica_error("legacy_ingest_feature must be legacy-ingest", |bundle| {
+ bundle.replica.policy.legacy_ingest_feature = "std".to_string();
+ });
+ assert_replica_error("phase_1_ingest_replacement must be none", |bundle| {
+ bundle.replica.policy.phase_1_ingest_replacement = "legacy".to_string();
+ });
+ assert_replica_error(
+ "future_product_ingest_input must be store_produced_verified_valid_visible_admission",
+ |bundle| {
+ bundle.replica.policy.future_product_ingest_input = "bare_envelope".to_string();
+ },
+ );
assert_replica_error("transfer.version must be 2", |bundle| {
bundle.replica.transfer.version = 1;
});
@@ -10104,12 +10359,77 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"]
#[test]
fn replica_policy_source_witnesses_reject_runtime_drift() {
let root = create_synthetic_workspace("replica_policy_source_drift");
+ let cargo_path = root.join("crates/b/Cargo.toml");
+ let lib_path = root.join("crates/b/src/lib.rs");
let types_path = root.join("crates/b/src/types.rs");
let emit_path = root.join("crates/b/src/emit.rs");
+ let cargo = fs::read_to_string(&cargo_path).expect("read replica cargo manifest");
+ let lib = fs::read_to_string(&lib_path).expect("read replica lib source");
let types = fs::read_to_string(&types_path).expect("read replica types source");
let emit = fs::read_to_string(&emit_path).expect("read replica emit source");
write_file(
+ &cargo_path,
+ &cargo.replace("std = []", "std = [\"legacy-ingest\"]"),
+ );
+ let bundle =
+ load_contract_bundle(&root).expect("load transitive default-feature drift contract");
+ let default_feature_error = validate_replica_contract(&bundle, &root)
+ .expect_err("transitively default legacy ingest feature must fail");
+ assert!(default_feature_error.contains("must not be enabled by default features"));
+
+ write_file(&cargo_path, &cargo);
+ write_file(
+ &lib_path,
+ &format!(
+ "{}\n/*\n#[cfg(feature = \"legacy-ingest\")]\npub mod ingest;\n*/\n",
+ lib.replace(
+ "#[cfg(feature = \"legacy-ingest\")]\npub mod ingest;",
+ "#[cfg(feature = \"std\")]\npub mod ingest;",
+ )
+ ),
+ );
+ let bundle = load_contract_bundle(&root).expect("load ingest-module drift contract");
+ let module_error = validate_replica_contract(&bundle, &root)
+ .expect_err("comment-only legacy guard witness must fail");
+ assert!(module_error.contains("must be guarded by exact"));
+
+ write_file(
+ &lib_path,
+ &format!("{lib}\npub use ingest::RadrootsReplicaIngestOutcome;\n"),
+ );
+ let bundle = load_contract_bundle(&root).expect("load second-reexport drift contract");
+ let reexport_error = validate_replica_contract(&bundle, &root)
+ .expect_err("ungated second ingest re-export must fail");
+ assert!(reexport_error.contains("every public replica ingest re-export"));
+
+ write_file(
+ &lib_path,
+ &lib.replacen(
+ "#[cfg(feature = \"legacy-ingest\")]\npub use ingest::{",
+ "#[cfg(any(feature = \"legacy-ingest\", feature = \"std\"))]\npub use ingest::{",
+ 1,
+ ),
+ );
+ let bundle = load_contract_bundle(&root).expect("load broadened-reexport contract");
+ let broadened_error = validate_replica_contract(&bundle, &root)
+ .expect_err("broadened ingest re-export guard must fail");
+ assert!(broadened_error.contains("every public replica ingest re-export"));
+
+ write_file(
+ &lib_path,
+ &format!(
+ "{lib}\npub mod nested {{\n pub use super::ingest::RadrootsReplicaIngestOutcome;\n}}\n"
+ ),
+ );
+ let bundle = load_contract_bundle(&root).expect("load nested-reexport drift contract");
+ let nested_error = validate_replica_contract(&bundle, &root)
+ .expect_err("ungated nested public ingest re-export must fail");
+ assert!(nested_error.contains("every public replica ingest re-export"));
+
+ write_file(&lib_path, &lib);
+
+ write_file(
&types_path,
&types.replace(
"#[serde(deny_unknown_fields)]\npub struct RadrootsReplicaSyncOptions",
@@ -11665,6 +11985,11 @@ publish = false
[dependencies]
dto_bindgen = { workspace = true, optional = true }
+
+[features]
+default = ["std"]
+std = []
+legacy-ingest = ["std"]
"#,
);
validate_generic_release_preflight(&root)
diff --git a/tools/xtask/src/contract/food_availability_projection.rs b/tools/xtask/src/contract/food_availability_projection.rs
@@ -0,0 +1,4367 @@
+use super::artifact_bundle::{
+ GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction,
+};
+use super::nip09_reconciliation::{
+ validate_nip09_predecessor_production_sources_under_lock,
+ validate_nip09_reconciliation_manifest_under_lock,
+};
+use super::registry_v7::validate_event_contract_registry_v7_inventory_under_lock;
+use quote::ToTokens;
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
+use sha2::{Digest, Sha256};
+use std::collections::{BTreeMap, BTreeSet};
+use std::path::Path;
+use syn::visit::Visit;
+use syn::{Expr, Item, Lit};
+
+const SCHEMA_VERSION: u32 = 1;
+const CONTRACT_ID: &str = "radroots_event_store.food_availability_projection_v1";
+const HOOK_ID: &str = "food_availability_projection_v1";
+const MIGRATION_VERSION: u32 = 3;
+const MIGRATION_NAME: &str = "food_availability_projection";
+const PROJECTION_VERSION: u32 = 1;
+const ADDRESSABLE_FEED_VERSION: u32 = 1;
+const EVENT_CONTRACT_REGISTRY_VERSION: u32 = 7;
+const FOOD_AVAILABILITY_KIND: u32 = 30_402;
+const UNRELATED_ADDRESSABLE_KIND: u32 = 30_340;
+const FOOD_CONTRACT_ID: &str = "radroots.food.availability.v1";
+const OPERATIONAL_LISTING_CONTRACT_ID: &str = "radroots.operational_listing.published.v1";
+const FARM_PROFILE_CONTRACT_ID: &str = "radroots.farm.profile.v1";
+const DELETION_CONTRACT_ID: &str = "radroots.social.deletion_request.v1";
+const ADMISSION_AUTHORITY: &str = "event_contract_registry_v7";
+const CURRENT_VISIBILITY_AUTHORITY: &str = "radroots_event_store_current_visibility_v1";
+const POST_CORE_CAPABILITY: &str = "apply_v2";
+const SCOPE_FINGERPRINT_SHA256: &str =
+ "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0";
+const SCHEMA_SHA256: &str = "dd12467e04addcbddb5ea0f386c12a8ac05ef5ebaaf949f24dd2c62745f5aaac";
+
+const PREDECESSOR_HOOK_ID: &str = "nip09_reconciliation_v1";
+const PREDECESSOR_MANIFEST_RELATIVE: &str =
+ "crates/event_store/contracts/nip09_reconciliation_v1.manifest.json";
+const PREDECESSOR_MANIFEST_BYTE_LENGTH: usize = 537_538;
+const PREDECESSOR_MANIFEST_SHA256: &str =
+ "74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77";
+
+const MANIFEST_RELATIVE: &str =
+ "crates/event_store/contracts/food_availability_projection_v1.manifest.json";
+const MANIFEST_SCHEMA_RELATIVE: &str =
+ "crates/event_store/contracts/food_availability_projection_v1.manifest.schema.json";
+const MANIFEST_SHA256_RELATIVE: &str =
+ "crates/event_store/contracts/food_availability_projection_v1.manifest.sha256";
+const GENERATED_DESCRIPTOR_RELATIVE: &str =
+ "crates/event_store/src/generated/food_availability_projection_manifest.rs";
+const MIGRATIONS_SOURCE_RELATIVE: &str = "crates/event_store/src/migrations.rs";
+const MIGRATION_UP_RELATIVE: &str =
+ "crates/event_store/migrations/0003_food_availability_projection.up.sql";
+const MIGRATION_DOWN_RELATIVE: &str =
+ "crates/event_store/migrations/0003_food_availability_projection.down.sql";
+const REGISTRY_INVENTORY_RELATIVE: &str =
+ "contracts/event_store/event_contract_registry_v7.inventory.json";
+const FOOD_PROFILE_VECTOR_RELATIVE: &str =
+ "contracts/conformance/vectors/food_availability/profile.v1.json";
+const RESULT_VECTOR_CANONICAL_RELATIVE: &str =
+ "contracts/conformance/vectors/event_store/food_availability_projection.v1.json";
+const RESULT_VECTOR_MIRROR_RELATIVE: &str =
+ "crates/event_store/tests/fixtures/food_availability_projection.v1.json";
+const RESULT_VECTOR_EXECUTOR_RELATIVE: &str =
+ "crates/event_store/tests/food_availability_projection_v1_result_vector.rs";
+const RESULT_VECTOR_EXECUTOR_ID: &str =
+ "radroots_event_store.food_availability_projection_v1.result_vector_executor.v1";
+const RESULT_VECTOR_EXECUTOR_TEST: &str = "food_availability_projection_v1_result_vector";
+const SOURCE_GENERATION_ACTIVE_SENTINEL: &str = "active";
+const WRITE_COMMAND: &str = "cargo xtask contract food-availability-projection-manifest --write";
+const HASH_ALGORITHM: &str = "sha256_bytes_v1";
+const EVENT_STORE_LIB_RELATIVE: &str = "crates/event_store/src/lib.rs";
+const EVENT_STORE_MODEL_RELATIVE: &str = "crates/event_store/src/model.rs";
+
+const GOVERNED_PUBLIC_API_MODULES: &[&str] = &[
+ "addressable_transition_feed_v1",
+ "current_visibility_v1",
+ "food_availability_projection_v1",
+];
+
+#[derive(Clone, Copy)]
+struct SourceSpec {
+ role: &'static str,
+ path: &'static str,
+}
+
+const SOURCE_SPECS: &[SourceSpec] = &[
+ SourceSpec {
+ role: "workspace_dependency_authority",
+ path: "Cargo.toml",
+ },
+ SourceSpec {
+ role: "event_store_dependency_authority",
+ path: "crates/event_store/Cargo.toml",
+ },
+ SourceSpec {
+ role: "blossom_public_surface",
+ path: "crates/blossom/src/lib.rs",
+ },
+ SourceSpec {
+ role: "blossom_sha256_value_object",
+ path: "crates/blossom/src/hash.rs",
+ },
+ SourceSpec {
+ role: "food_event_contract",
+ path: "crates/event/src/food_availability.rs",
+ },
+ SourceSpec {
+ role: "food_admission",
+ path: "crates/event_codec/src/food_availability/admission.rs",
+ },
+ SourceSpec {
+ role: "registry_v7_admission",
+ path: "crates/event_codec/src/admission/registry_v7.rs",
+ },
+ SourceSpec {
+ role: "registry_v7_food_projection",
+ path: "crates/event_codec/src/food_availability/inbound/registry_v7.rs",
+ },
+ SourceSpec {
+ role: "event_store_error_surface",
+ path: "crates/event_store/src/error.rs",
+ },
+ SourceSpec {
+ role: "generated_descriptor_registration",
+ path: "crates/event_store/src/generated.rs",
+ },
+ SourceSpec {
+ role: "public_surface",
+ path: "crates/event_store/src/lib.rs",
+ },
+ SourceSpec {
+ role: "migration_registry",
+ path: MIGRATIONS_SOURCE_RELATIVE,
+ },
+ SourceSpec {
+ role: "model_registration",
+ path: "crates/event_store/src/model.rs",
+ },
+ SourceSpec {
+ role: "schema_hooks",
+ path: "crates/event_store/src/schema.rs",
+ },
+ SourceSpec {
+ role: "store_ingest_and_wal_authority",
+ path: "crates/event_store/src/store.rs",
+ },
+ SourceSpec {
+ role: "addressable_transition_feed_model",
+ path: "crates/event_store/src/model/addressable_transition_feed_v1.rs",
+ },
+ SourceSpec {
+ role: "current_visibility_model",
+ path: "crates/event_store/src/model/current_visibility_v1.rs",
+ },
+ SourceSpec {
+ role: "food_projection_model",
+ path: "crates/event_store/src/model/food_availability_projection_v1.rs",
+ },
+ SourceSpec {
+ role: "addressable_transition_feed_store",
+ path: "crates/event_store/src/store/addressable_transition_feed_v1.rs",
+ },
+ SourceSpec {
+ role: "current_visibility_store",
+ path: "crates/event_store/src/store/current_visibility_v1.rs",
+ },
+ SourceSpec {
+ role: "food_projection_store",
+ path: "crates/event_store/src/store/food_availability_projection_v1.rs",
+ },
+ SourceSpec {
+ role: "predecessor_protocol_storage",
+ path: "crates/event_store/src/store/protocol_storage_v1.rs",
+ },
+ SourceSpec {
+ role: "predecessor_active_state_fast_validation",
+ path: "crates/event_store/src/nip09/reconciliation_v1.rs",
+ },
+ SourceSpec {
+ role: "post_core_capabilities",
+ path: "crates/event_store/src/store/post_core_extension_capabilities.rs",
+ },
+ SourceSpec {
+ role: "post_core_dispatcher",
+ path: "crates/event_store/src/store/post_core_extension_dispatcher.rs",
+ },
+ SourceSpec {
+ role: "post_core_v2_extension",
+ path: "crates/event_store/src/store/post_core_extensions_v2.rs",
+ },
+ SourceSpec {
+ role: "post_core_v2_storage",
+ path: "crates/event_store/src/store/post_core_storage_v2.rs",
+ },
+ SourceSpec {
+ role: "predecessor_post_core_v1_extension",
+ path: "crates/event_store/src/store/post_core_extensions_v1.rs",
+ },
+ SourceSpec {
+ role: "predecessor_post_core_v1_storage",
+ path: "crates/event_store/src/store/post_core_storage_v1.rs",
+ },
+];
+
+const PREDECESSOR_SUPERSEDED_SOURCE_PATHS: &[&str] = &[
+ "crates/blossom/src/hash.rs",
+ "crates/blossom/src/lib.rs",
+ "crates/event/src/food_availability.rs",
+ "crates/event_codec/src/admission/registry_v7.rs",
+ "crates/event_codec/src/food_availability/admission.rs",
+ "crates/event_codec/src/food_availability/inbound/registry_v7.rs",
+ "crates/event_store/src/error.rs",
+ "crates/event_store/src/generated.rs",
+ "crates/event_store/src/lib.rs",
+ "crates/event_store/src/migrations.rs",
+ "crates/event_store/src/model.rs",
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "crates/event_store/src/schema.rs",
+ "crates/event_store/src/store.rs",
+ "crates/event_store/src/store/post_core_extension_capabilities.rs",
+ "crates/event_store/src/store/post_core_extension_dispatcher.rs",
+ "crates/event_store/src/store/post_core_extensions_v1.rs",
+ "crates/event_store/src/store/post_core_storage_v1.rs",
+ "crates/event_store/src/store/protocol_storage_v1.rs",
+];
+
+const EXPECTED_CATALOG_OBJECTS: &[&str] = &[
+ "radroots_event_store_addressable_feed_generation_insert",
+ "radroots_event_store_addressable_feed_integrity_v1",
+ "radroots_event_store_addressable_feed_transition_insert",
+ "radroots_event_store_addressable_transition_coordinate_idx",
+ "radroots_event_store_current_visibility_head_lookup_idx",
+ "radroots_event_store_current_visibility_v1",
+ "radroots_event_store_food_availability_author_idx",
+ "radroots_event_store_food_availability_cursor",
+ "radroots_event_store_food_availability_cursor_delete_guard",
+ "radroots_event_store_food_availability_cursor_insert_guard",
+ "radroots_event_store_food_availability_cursor_update_guard",
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_image_delete_guard",
+ "radroots_event_store_food_availability_image_insert_guard",
+ "radroots_event_store_food_availability_image_update_guard",
+ "radroots_event_store_food_availability_projection",
+ "radroots_event_store_food_availability_projection_delete_guard",
+ "radroots_event_store_food_availability_projection_insert_guard",
+ "radroots_event_store_food_availability_projection_update_guard",
+ "radroots_event_store_food_availability_read_v1",
+ "radroots_event_store_food_availability_recent_idx",
+ "radroots_event_store_food_availability_search_delete",
+ "radroots_event_store_food_availability_search_fts",
+ "radroots_event_store_food_availability_search_fts_config",
+ "radroots_event_store_food_availability_search_fts_content",
+ "radroots_event_store_food_availability_search_fts_data",
+ "radroots_event_store_food_availability_search_fts_docsize",
+ "radroots_event_store_food_availability_search_fts_idx",
+ "radroots_event_store_food_availability_search_insert",
+ "radroots_event_store_food_availability_status_idx",
+ "radroots_event_store_nip09_address_target_visibility_lookup_idx",
+];
+
+const EXPECTED_CATALOG_TABLES: &[&str] = &[
+ "radroots_event_store_addressable_feed_integrity_v1",
+ "radroots_event_store_food_availability_cursor",
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_projection",
+ "radroots_event_store_food_availability_search_fts",
+ "radroots_event_store_food_availability_search_fts_config",
+ "radroots_event_store_food_availability_search_fts_content",
+ "radroots_event_store_food_availability_search_fts_data",
+ "radroots_event_store_food_availability_search_fts_docsize",
+ "radroots_event_store_food_availability_search_fts_idx",
+];
+
+const EXPECTED_CATALOG_FTS5_TABLES: &[&str] =
+ &["radroots_event_store_food_availability_search_fts"];
+
+const ENTRY_POINTS: &[(&str, &str)] = &[
+ (
+ "registry_v7_admission",
+ "radroots_event_codec::admit_verified_event_registry_v7",
+ ),
+ (
+ "migration_registry",
+ "radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[2]",
+ ),
+ (
+ "migration_apply_hook",
+ "radroots_event_store::schema::apply_migration_hook",
+ ),
+ (
+ "migration_validation_hook",
+ "radroots_event_store::schema::validate_migration_hook_state",
+ ),
+ (
+ "post_core_extension",
+ "radroots_event_store::store::PostCoreExtensionCapabilities::apply_v2",
+ ),
+ (
+ "addressable_transition_feed",
+ "radroots_event_store::RadrootsEventStore::addressable_transition_page_v1",
+ ),
+ (
+ "current_visibility",
+ "radroots_event_store::RadrootsEventStore::current_event_visibility_v1",
+ ),
+ (
+ "event_visibility_batch",
+ "radroots_event_store::RadrootsEventStore::event_visibilities",
+ ),
+ (
+ "projection_lookup",
+ "radroots_event_store::RadrootsEventStore::food_availability_v1",
+ ),
+ (
+ "projection_recent",
+ "radroots_event_store::RadrootsEventStore::recent_food_availability_v1",
+ ),
+ (
+ "projection_search",
+ "radroots_event_store::RadrootsEventStore::search_food_availability_v1",
+ ),
+ (
+ "projection_audit",
+ "radroots_event_store::RadrootsEventStore::audit_food_availability_projection_v1",
+ ),
+ ("result_vector_executor", RESULT_VECTOR_EXECUTOR_TEST),
+];
+
+const PUBLIC_API: &[&str] = &[
+ "RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1",
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1",
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1",
+ "RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1",
+ "RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1",
+ "RadrootsAddressableTransitionCauseV1",
+ "RadrootsAddressableTransitionCoordinateV1",
+ "RadrootsAddressableTransitionCursorV1",
+ "RadrootsAddressableTransitionEventReferenceV1",
+ "RadrootsAddressableTransitionOriginV1",
+ "RadrootsAddressableTransitionPageV1",
+ "RadrootsAddressableTransitionRawHeadDecisionV1",
+ "RadrootsAddressableTransitionScopeFingerprintV1",
+ "RadrootsAddressableTransitionScopeV1",
+ "RadrootsAddressableTransitionV1",
+ "RadrootsAddressableTransitionVisibilityV1",
+ "RadrootsCurrentEventVisibilityV1",
+ "RadrootsCurrentVisibilityDecisionV1",
+ "RadrootsFoodAvailabilitySearchQueryV1",
+ "RadrootsFoodAvailabilityStatusFilterV1",
+ "RadrootsNip09SuppressionEvidenceV1",
+ "RadrootsNip09SuppressionOutcome",
+ "RadrootsNip09SuppressionReason",
+ "RadrootsStoreProducedCanonicalEventV1",
+ "RadrootsStoredFoodAvailabilityImageV1",
+ "RadrootsStoredFoodAvailabilityV1",
+];
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct FoodAvailabilityProjectionManifest {
+ schema_version: u32,
+ contract_id: String,
+ hook_id: String,
+ manifest_schema: FileDescriptor,
+ predecessor: PredecessorDescriptor,
+ migration: MigrationDescriptor,
+ profile: ProfileDescriptor,
+ registry_inventory: FileDescriptor,
+ food_profile_vector: FileDescriptor,
+ entry_points: Vec<EntryPointDescriptor>,
+ source_files: Vec<SourceFileDescriptor>,
+ public_api: Vec<String>,
+ result_vector: ResultVectorDescriptor,
+}
+
+#[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 {
+ hook_id: String,
+ manifest: FileDescriptor,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct MigrationDescriptor {
+ version: u32,
+ name: String,
+ up: FileDescriptor,
+ down: FileDescriptor,
+ schema_sha256: String,
+ catalog: CatalogDescriptor,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct CatalogDescriptor {
+ objects: Vec<String>,
+ tables: Vec<String>,
+ fts5_tables: Vec<String>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ProfileDescriptor {
+ event_contract_registry_version: u32,
+ addressable_feed_version: u32,
+ projection_version: u32,
+ scope_kinds: Vec<u32>,
+ scope_fingerprint_sha256: String,
+ food_contract_id: String,
+ admission_authority: String,
+ current_visibility_authority: String,
+ post_core_capability: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct EntryPointDescriptor {
+ role: String,
+ rust_path: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct SourceFileDescriptor {
+ role: String,
+ path: String,
+ byte_length: u64,
+ sha256: String,
+ hash_algorithm: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ResultVectorDescriptor {
+ canonical_path: String,
+ mirror_path: String,
+ byte_length: u64,
+ sha256: String,
+ hash_algorithm: String,
+ executor_id: String,
+ executor_path: String,
+ executor_test: String,
+ executor_byte_length: u64,
+ executor_sha256: String,
+ executor_hash_algorithm: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ProjectionResultVector {
+ schema_version: u32,
+ contract_id: String,
+ feed_version: u32,
+ projection_version: u32,
+ scope_kinds: Vec<u32>,
+ cases: Vec<ProjectionCase>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ProjectionCase {
+ id: String,
+ events: Vec<ObservedEvent>,
+ expected: ExpectedCase,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ObservedEvent {
+ role: ProjectionInputRole,
+ observed_at_ms: i64,
+ expected_ingest: ExpectedIngest,
+ event: SignedEvent,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+enum ProjectionInputRole {
+ ScopedFood,
+ ScopedNonFood,
+ UnrelatedAddressable,
+ Causal,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedIngest {
+ admission_status: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ admission_code: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ contract_id: RequiredNullable<String>,
+ event_class: String,
+ valid_stream_eligible: bool,
+ raw_head_decision: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct SignedEvent {
+ id: String,
+ pubkey: String,
+ created_at: u64,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ sig: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedCase {
+ coordinate: ExpectedCoordinate,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ projection: RequiredNullable<ExpectedProjection>,
+ searches: Vec<ExpectedSearch>,
+ transition_page: ExpectedTransitionPage,
+ event_visibility: Vec<ExpectedVisibility>,
+ historical_visibility_witnesses: Vec<ExpectedHistoricalVisibilityWitness>,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
+struct ExpectedCoordinate {
+ kind: u32,
+ pubkey: String,
+ d_tag: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedProjection {
+ event_id: String,
+ content: String,
+ title: String,
+ summary: String,
+ published_at: u64,
+ location: String,
+ price_amount: String,
+ price_currency: String,
+ price_unit: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ quantity_amount: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ quantity_unit: RequiredNullable<String>,
+ status: String,
+ diagnostics: Vec<String>,
+ images: Vec<ExpectedImage>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedImage {
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ url: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ width: RequiredNullable<u32>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ height: RequiredNullable<u32>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ blossom_sha256: RequiredNullable<String>,
+ diagnostics: Vec<String>,
+ qualifies: bool,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedSearch {
+ query: String,
+ event_ids: Vec<String>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedTransitionPage {
+ source_high_water: i64,
+ has_more: bool,
+ next_cursor: ExpectedTransitionCursor,
+ transitions: Vec<ExpectedTransition>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedTransitionCursor {
+ source_generation: String,
+ feed_version: u32,
+ scope_fingerprint: String,
+ last_transition_seq: i64,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedTransition {
+ transition_seq: i64,
+ source_generation: String,
+ origin: String,
+ coordinate: ExpectedCoordinate,
+ raw_head: ExpectedEventReference,
+ raw_head_created_at: u64,
+ admission_status: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ admission_code: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ contract_id: RequiredNullable<String>,
+ visibility: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ suppression: RequiredNullable<ExpectedSuppressionEvidence>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ cause_event: RequiredNullable<ExpectedTransitionCause>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ canonical_visible_event: RequiredNullable<ExpectedCanonicalVisibleEvent>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ retracted_event: RequiredNullable<ExpectedEventReference>,
+ raw_head_decision: String,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
+struct ExpectedEventReference {
+ event_id: String,
+ event_seq: i64,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedSuppressionEvidence {
+ outcome: String,
+ reason: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ event_reference_request_id: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ address_reference_request_id: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ address_reference_cutoff: RequiredNullable<u64>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedTransitionCause {
+ event: ExpectedEventReference,
+ pubkey: String,
+ created_at: u64,
+ kind: u32,
+ admission_status: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ admission_code: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ contract_id: RequiredNullable<String>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedCanonicalVisibleEvent {
+ event: ExpectedEventReference,
+ raw_json_sha256: String,
+ admission_status: String,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ contract_id: RequiredNullable<String>,
+ event_class: String,
+ valid_stream_eligible: bool,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedVisibility {
+ event: ExpectedEventReference,
+ source_generation: String,
+ admission_status: String,
+ decision: String,
+ is_raw_head: bool,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ raw_head_event_id: RequiredNullable<String>,
+ #[serde(deserialize_with = "deserialize_required_nullable")]
+ suppression: RequiredNullable<ExpectedSuppressionEvidence>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ExpectedHistoricalVisibilityWitness {
+ transition_seq: i64,
+ event_id: String,
+ final_decision: String,
+}
+
+#[derive(Debug, Serialize)]
+struct RequiredNullable<T>(Option<T>);
+
+fn deserialize_required_nullable<'de, D, T>(
+ deserializer: D,
+) -> Result<RequiredNullable<T>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+ T: Deserialize<'de>,
+{
+ Option::<T>::deserialize(deserializer).map(RequiredNullable)
+}
+
+impl<'de, T> Deserialize<'de> for RequiredNullable<T>
+where
+ T: Deserialize<'de>,
+{
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Option::<T>::deserialize(deserializer).map(Self)
+ }
+}
+
+pub(crate) fn write_food_availability_projection_manifest(
+ workspace_root: &Path,
+) -> Result<(), String> {
+ with_artifact_bundle_transaction(workspace_root, |transaction| {
+ validate_nip09_reconciliation_manifest_under_lock(workspace_root)?;
+ validate_predecessor_production_source_coverage(workspace_root)?;
+ validate_event_contract_registry_v7_inventory_under_lock(workspace_root)?;
+ let artifacts = expected_artifacts(workspace_root)?;
+ transaction.write(artifacts)?;
+ validate_food_availability_projection_manifest_under_lock(workspace_root)
+ })
+}
+
+pub(crate) fn validate_food_availability_projection_manifest(
+ workspace_root: &Path,
+) -> Result<(), String> {
+ with_artifact_bundle_transaction(workspace_root, |_| {
+ validate_food_availability_projection_manifest_under_lock(workspace_root)
+ })
+}
+
+fn validate_food_availability_projection_manifest_under_lock(
+ workspace_root: &Path,
+) -> Result<(), String> {
+ validate_nip09_reconciliation_manifest_under_lock(workspace_root)?;
+ validate_predecessor_production_source_coverage(workspace_root)?;
+ validate_event_contract_registry_v7_inventory_under_lock(workspace_root)?;
+
+ let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
+ let manifest_value: Value = serde_json::from_slice(&manifest_bytes)
+ .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
+ let manifest: FoodAvailabilityProjectionManifest =
+ serde_json::from_value(manifest_value.clone())
+ .map_err(|error| format!("parse typed {MANIFEST_RELATIVE}: {error}"))?;
+ validate_canonical_json(MANIFEST_RELATIVE, &manifest_bytes, &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_manifest_json_schema(&schema, &manifest_value)?;
+ validate_manifest_shape(&manifest)?;
+
+ let digest_bytes = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?;
+ validate_digest_sidecar(MANIFEST_SHA256_RELATIVE, &digest_bytes)?;
+ if digest_bytes != format!("{}\n", sha256_hex(&manifest_bytes)).as_bytes() {
+ return Err(format!(
+ "{MANIFEST_SHA256_RELATIVE} must match the checked-in manifest bytes"
+ ));
+ }
+
+ let expected = expected_artifacts(workspace_root)?;
+ for artifact in expected {
+ let actual = read_regular_file(workspace_root, artifact.relative)?;
+ if actual != artifact.contents {
+ return Err(stale_error(artifact.relative));
+ }
+ }
+ Ok(())
+}
+
+fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> {
+ let schema = manifest_schema();
+ let schema_bytes = canonical_json_bytes(&schema)?;
+ let manifest = describe_manifest(workspace_root, &schema_bytes)?;
+ let manifest_bytes = canonical_json_bytes(&manifest)?;
+ let manifest_sha256 = sha256_hex(&manifest_bytes);
+ let descriptor = generated_descriptor(&manifest, &manifest_bytes, &manifest_sha256);
+ let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?;
+
+ Ok(vec![
+ GeneratedArtifact {
+ relative: MANIFEST_RELATIVE,
+ contents: manifest_bytes,
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SCHEMA_RELATIVE,
+ contents: schema_bytes,
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SHA256_RELATIVE,
+ contents: format!("{manifest_sha256}\n").into_bytes(),
+ },
+ GeneratedArtifact {
+ relative: GENERATED_DESCRIPTOR_RELATIVE,
+ contents: descriptor.into_bytes(),
+ },
+ GeneratedArtifact {
+ relative: RESULT_VECTOR_MIRROR_RELATIVE,
+ contents: vector_bytes,
+ },
+ ])
+}
+
+fn describe_manifest(
+ workspace_root: &Path,
+ schema_bytes: &[u8],
+) -> Result<FoodAvailabilityProjectionManifest, String> {
+ validate_source_contract(workspace_root)?;
+ validate_predecessor_production_source_coverage(workspace_root)?;
+
+ let predecessor_bytes = read_regular_file(workspace_root, PREDECESSOR_MANIFEST_RELATIVE)?;
+ if predecessor_bytes.len() != PREDECESSOR_MANIFEST_BYTE_LENGTH
+ || sha256_hex(&predecessor_bytes) != PREDECESSOR_MANIFEST_SHA256
+ {
+ return Err(format!(
+ "{PREDECESSOR_MANIFEST_RELATIVE} does not match the immutable predecessor identity"
+ ));
+ }
+
+ let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?;
+ let vector: ProjectionResultVector = serde_json::from_slice(&vector_bytes)
+ .map_err(|error| format!("parse {RESULT_VECTOR_CANONICAL_RELATIVE}: {error}"))?;
+ validate_canonical_json(RESULT_VECTOR_CANONICAL_RELATIVE, &vector_bytes, &vector)?;
+ validate_result_vector(&vector)?;
+
+ let migration_source = read_regular_file(workspace_root, MIGRATIONS_SOURCE_RELATIVE)?;
+ let catalog = catalog_from_migration_source(&migration_source)?;
+ validate_catalog(&catalog)?;
+ let executor = descriptor_for_file(workspace_root, RESULT_VECTOR_EXECUTOR_RELATIVE)?;
+
+ let source_files = SOURCE_SPECS
+ .iter()
+ .map(|spec| {
+ let bytes = if spec.path == MIGRATIONS_SOURCE_RELATIVE {
+ migration_source.clone()
+ } else {
+ read_regular_file(workspace_root, spec.path)?
+ };
+ Ok(SourceFileDescriptor {
+ role: spec.role.to_owned(),
+ path: spec.path.to_owned(),
+ byte_length: byte_length(spec.path, &bytes)?,
+ sha256: sha256_hex(&bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ })
+ })
+ .collect::<Result<Vec<_>, String>>()?;
+
+ Ok(FoodAvailabilityProjectionManifest {
+ schema_version: SCHEMA_VERSION,
+ contract_id: CONTRACT_ID.to_owned(),
+ hook_id: HOOK_ID.to_owned(),
+ manifest_schema: descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, schema_bytes)?,
+ predecessor: PredecessorDescriptor {
+ hook_id: PREDECESSOR_HOOK_ID.to_owned(),
+ manifest: descriptor_for_bytes(PREDECESSOR_MANIFEST_RELATIVE, &predecessor_bytes)?,
+ },
+ migration: MigrationDescriptor {
+ version: MIGRATION_VERSION,
+ name: MIGRATION_NAME.to_owned(),
+ up: descriptor_for_file(workspace_root, MIGRATION_UP_RELATIVE)?,
+ down: descriptor_for_file(workspace_root, MIGRATION_DOWN_RELATIVE)?,
+ schema_sha256: SCHEMA_SHA256.to_owned(),
+ catalog,
+ },
+ profile: ProfileDescriptor {
+ event_contract_registry_version: EVENT_CONTRACT_REGISTRY_VERSION,
+ addressable_feed_version: ADDRESSABLE_FEED_VERSION,
+ projection_version: PROJECTION_VERSION,
+ scope_kinds: vec![FOOD_AVAILABILITY_KIND],
+ scope_fingerprint_sha256: SCOPE_FINGERPRINT_SHA256.to_owned(),
+ food_contract_id: FOOD_CONTRACT_ID.to_owned(),
+ admission_authority: ADMISSION_AUTHORITY.to_owned(),
+ current_visibility_authority: CURRENT_VISIBILITY_AUTHORITY.to_owned(),
+ post_core_capability: POST_CORE_CAPABILITY.to_owned(),
+ },
+ registry_inventory: descriptor_for_file(workspace_root, REGISTRY_INVENTORY_RELATIVE)?,
+ food_profile_vector: descriptor_for_file(workspace_root, FOOD_PROFILE_VECTOR_RELATIVE)?,
+ entry_points: ENTRY_POINTS
+ .iter()
+ .map(|(role, rust_path)| EntryPointDescriptor {
+ role: (*role).to_owned(),
+ rust_path: (*rust_path).to_owned(),
+ })
+ .collect(),
+ source_files,
+ public_api: PUBLIC_API.iter().map(|name| (*name).to_owned()).collect(),
+ result_vector: ResultVectorDescriptor {
+ canonical_path: RESULT_VECTOR_CANONICAL_RELATIVE.to_owned(),
+ mirror_path: RESULT_VECTOR_MIRROR_RELATIVE.to_owned(),
+ byte_length: byte_length(RESULT_VECTOR_CANONICAL_RELATIVE, &vector_bytes)?,
+ sha256: sha256_hex(&vector_bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ executor_id: RESULT_VECTOR_EXECUTOR_ID.to_owned(),
+ executor_path: RESULT_VECTOR_EXECUTOR_RELATIVE.to_owned(),
+ executor_test: RESULT_VECTOR_EXECUTOR_TEST.to_owned(),
+ executor_byte_length: executor.byte_length,
+ executor_sha256: executor.sha256,
+ executor_hash_algorithm: HASH_ALGORITHM.to_owned(),
+ },
+ })
+}
+
+fn validate_predecessor_production_source_coverage(workspace_root: &Path) -> Result<(), String> {
+ for path in PREDECESSOR_SUPERSEDED_SOURCE_PATHS {
+ if !SOURCE_SPECS.iter().any(|source| source.path == *path) {
+ return Err(format!(
+ "successor supersession source `{path}` is not current-byte-bound"
+ ));
+ }
+ }
+ validate_nip09_predecessor_production_sources_under_lock(
+ workspace_root,
+ PREDECESSOR_SUPERSEDED_SOURCE_PATHS,
+ )
+}
+
+fn descriptor_for_file(workspace_root: &Path, relative: &str) -> Result<FileDescriptor, String> {
+ let bytes = read_regular_file(workspace_root, relative)?;
+ descriptor_for_bytes(relative, &bytes)
+}
+
+fn descriptor_for_bytes(relative: &str, bytes: &[u8]) -> Result<FileDescriptor, String> {
+ Ok(FileDescriptor {
+ path: relative.to_owned(),
+ byte_length: byte_length(relative, bytes)?,
+ sha256: sha256_hex(bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ })
+}
+
+fn byte_length(relative: &str, bytes: &[u8]) -> Result<u64, String> {
+ u64::try_from(bytes.len()).map_err(|_| format!("{relative} byte length does not fit u64"))
+}
+
+fn catalog_from_migration_source(bytes: &[u8]) -> Result<CatalogDescriptor, String> {
+ let source = std::str::from_utf8(bytes)
+ .map_err(|error| format!("crates/event_store/src/migrations.rs must be UTF-8: {error}"))?;
+ let syntax = syn::parse_file(source)
+ .map_err(|error| format!("parse crates/event_store/src/migrations.rs: {error}"))?;
+ Ok(CatalogDescriptor {
+ objects: extract_string_array_const(&syntax, "EVENT_STORE_FOOD_AVAILABILITY_OBJECT_NAMES")?,
+ tables: extract_string_array_const(&syntax, "EVENT_STORE_FOOD_AVAILABILITY_TABLE_NAMES")?,
+ fts5_tables: extract_string_array_const(
+ &syntax,
+ "EVENT_STORE_FOOD_AVAILABILITY_FTS5_TABLE_NAMES",
+ )?,
+ })
+}
+
+fn extract_string_array_const(syntax: &syn::File, name: &str) -> Result<Vec<String>, String> {
+ let expression = syntax.items.iter().find_map(|item| match item {
+ Item::Const(item) if item.ident == name => Some(item.expr.as_ref()),
+ Item::Static(item) if item.ident == name => Some(item.expr.as_ref()),
+ _ => None,
+ });
+ let expression = expression.ok_or_else(|| {
+ format!("crates/event_store/src/migrations.rs is missing catalog constant {name}")
+ })?;
+ let Expr::Array(array) = strip_expression_wrappers(expression) else {
+ return Err(format!(
+ "crates/event_store/src/migrations.rs catalog constant {name} must be a literal string array"
+ ));
+ };
+ array
+ .elems
+ .iter()
+ .map(|element| match strip_expression_wrappers(element) {
+ Expr::Lit(literal) => match &literal.lit {
+ Lit::Str(value) => Ok(value.value()),
+ _ => Err(format!(
+ "catalog constant {name} must contain string literals"
+ )),
+ },
+ _ => Err(format!(
+ "catalog constant {name} must contain string literals"
+ )),
+ })
+ .collect()
+}
+
+fn strip_expression_wrappers(mut expression: &Expr) -> &Expr {
+ loop {
+ expression = match expression {
+ Expr::Reference(reference) => &reference.expr,
+ Expr::Group(group) => &group.expr,
+ Expr::Paren(paren) => &paren.expr,
+ _ => return expression,
+ };
+ }
+}
+
+fn validate_catalog(catalog: &CatalogDescriptor) -> Result<(), String> {
+ let expected_objects = EXPECTED_CATALOG_OBJECTS
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<Vec<_>>();
+ let expected_tables = EXPECTED_CATALOG_TABLES
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<Vec<_>>();
+ let expected_fts5 = EXPECTED_CATALOG_FTS5_TABLES
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<Vec<_>>();
+ if catalog.objects != expected_objects
+ || catalog.tables != expected_tables
+ || catalog.fts5_tables != expected_fts5
+ {
+ return Err(
+ "FoodAvailability migration catalog differs from the successor contract".to_owned(),
+ );
+ }
+ validate_unique(
+ "migration catalog object",
+ catalog.objects.iter().map(String::as_str),
+ )?;
+ validate_unique(
+ "migration catalog table",
+ catalog.tables.iter().map(String::as_str),
+ )?;
+ Ok(())
+}
+
+fn validate_migration_guard_limits(workspace_root: &Path) -> Result<(), String> {
+ let addressable_model = read_regular_file(
+ workspace_root,
+ "crates/event_store/src/model/addressable_transition_feed_v1.rs",
+ )?;
+ let food_model = read_regular_file(
+ workspace_root,
+ "crates/event_store/src/model/food_availability_projection_v1.rs",
+ )?;
+ let migration = read_regular_file(workspace_root, MIGRATION_UP_RELATIVE)?;
+ validate_migration_guard_limit_sources(
+ std::str::from_utf8(&addressable_model).map_err(|error| {
+ format!(
+ "crates/event_store/src/model/addressable_transition_feed_v1.rs must be UTF-8: {error}"
+ )
+ })?,
+ std::str::from_utf8(&food_model).map_err(|error| {
+ format!(
+ "crates/event_store/src/model/food_availability_projection_v1.rs must be UTF-8: {error}"
+ )
+ })?,
+ std::str::from_utf8(&migration)
+ .map_err(|error| format!("{MIGRATION_UP_RELATIVE} must be UTF-8: {error}"))?,
+ )
+}
+
+fn validate_migration_guard_limit_sources(
+ addressable_model_source: &str,
+ food_model_source: &str,
+ migration_source: &str,
+) -> Result<(), String> {
+ let addressable_model = syn::parse_file(addressable_model_source).map_err(|error| {
+ format!("parse crates/event_store/src/model/addressable_transition_feed_v1.rs: {error}")
+ })?;
+ let food_model = syn::parse_file(food_model_source).map_err(|error| {
+ format!("parse crates/event_store/src/model/food_availability_projection_v1.rs: {error}")
+ })?;
+ let scan_max = extract_u32_literal_const(
+ &addressable_model,
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1",
+ )?;
+ let page_max = extract_u32_literal_const(
+ &addressable_model,
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1",
+ )?;
+ let apply_limit = find_const_expression(
+ &food_model,
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1",
+ )?;
+ if compact_tokens(apply_limit) != "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1" {
+ return Err(
+ "FoodAvailability projection apply limit must alias the governed addressable page limit"
+ .to_owned(),
+ );
+ }
+
+ let compact_sql = migration_source
+ .chars()
+ .filter(|character| !character.is_whitespace())
+ .collect::<String>();
+ let scan_guard = extract_direct_sql_guard(
+ &compact_sql,
+ "ORNEW.last_transition_seq-OLD.last_transition_seq>",
+ 1,
+ "FoodAvailability cursor scan delta",
+ )?;
+ let row_count_guard = extract_direct_sql_guard(
+ &compact_sql,
+ "ORabs(NEW.projected_row_count-OLD.projected_row_count)>",
+ 2,
+ "FoodAvailability projected-row delta",
+ )?;
+ if scan_guard != scan_max {
+ return Err(format!(
+ "{MIGRATION_UP_RELATIVE} cursor scan delta {scan_guard} differs from RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1={scan_max}"
+ ));
+ }
+ if row_count_guard != page_max {
+ return Err(format!(
+ "{MIGRATION_UP_RELATIVE} projected-row delta {row_count_guard} differs from RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1={page_max}"
+ ));
+ }
+ let visibility_index = "radroots_event_store_nip09_address_target_visibility_lookup_idx";
+ let index_definition = format!(
+ "CREATEINDEX{visibility_index}ONradroots_event_store_nip09_address_target(source_generation,target_kind,target_pubkey,target_d_tag,inclusive_cutoffDESC,request_event_idASC);"
+ );
+ if compact_sql.matches(&index_definition).count() != 1
+ || compact_sql
+ .matches(&format!("INDEXEDBY{visibility_index}"))
+ .count()
+ != 3
+ {
+ return Err(format!(
+ "{MIGRATION_UP_RELATIVE} must define and force the canonical NIP-09 address-target visibility index"
+ ));
+ }
+ if compact_sql
+ .matches("ORDERBYtarget.request_event_idLIMIT1")
+ .count()
+ != 1
+ || compact_sql
+ .matches("ORDERBYtarget.inclusive_cutoffDESC,target.request_event_idLIMIT1")
+ .count()
+ != 2
+ {
+ return Err(format!(
+ "{MIGRATION_UP_RELATIVE} must use the canonical NIP-09 visibility ordering"
+ ));
+ }
+ Ok(())
+}
+
+fn find_const_expression<'a>(syntax: &'a syn::File, name: &str) -> Result<&'a Expr, String> {
+ syntax
+ .items
+ .iter()
+ .find_map(|item| match item {
+ Item::Const(item) if item.ident == name => Some(item.expr.as_ref()),
+ _ => None,
+ })
+ .ok_or_else(|| format!("governed Rust source is missing constant {name}"))
+}
+
+fn extract_u32_literal_const(syntax: &syn::File, name: &str) -> Result<u32, String> {
+ let expression = strip_expression_wrappers(find_const_expression(syntax, name)?);
+ let Expr::Lit(literal) = expression else {
+ return Err(format!("governed constant {name} must be a u32 literal"));
+ };
+ let Lit::Int(value) = &literal.lit else {
+ return Err(format!("governed constant {name} must be a u32 literal"));
+ };
+ value
+ .base10_parse::<u32>()
+ .map_err(|error| format!("parse governed constant {name}: {error}"))
+}
+
+fn extract_direct_sql_guard(
+ compact_sql: &str,
+ marker: &str,
+ expected_occurrences: usize,
+ label: &str,
+) -> Result<u32, String> {
+ let suffixes = compact_sql
+ .match_indices(marker)
+ .map(|(offset, _)| &compact_sql[offset + marker.len()..])
+ .collect::<Vec<_>>();
+ if suffixes.len() != expected_occurrences {
+ return Err(format!(
+ "{MIGRATION_UP_RELATIVE} must contain exactly {expected_occurrences} {label} guard occurrence(s)"
+ ));
+ }
+ let direct_values = suffixes
+ .iter()
+ .filter_map(|suffix| {
+ let digits = suffix
+ .chars()
+ .take_while(char::is_ascii_digit)
+ .collect::<String>();
+ (!digits.is_empty()).then_some(digits)
+ })
+ .collect::<Vec<_>>();
+ if direct_values.len() != 1 {
+ return Err(format!(
+ "{MIGRATION_UP_RELATIVE} must contain exactly one direct numeric {label} guard"
+ ));
+ }
+ direct_values[0]
+ .parse::<u32>()
+ .map_err(|error| format!("parse {label} guard: {error}"))
+}
+
+fn validate_fast_active_hook_source(source: &str) -> Result<(), String> {
+ let syntax = syn::parse_file(source).map_err(|error| {
+ format!("parse crates/event_store/src/nip09/reconciliation_v1.rs: {error}")
+ })?;
+ let function = syntax
+ .items
+ .iter()
+ .find_map(|item| match item {
+ Item::Fn(function) if function.sig.ident == "validate_active_hook_state_fast" => {
+ Some(function)
+ }
+ _ => None,
+ })
+ .ok_or_else(|| {
+ "predecessor source is missing validate_active_hook_state_fast".to_owned()
+ })?;
+ let body = compact_tokens(&function.block);
+ let expected = "{validate_rebuild_marker_absent(connection).await?;validate_structural_source_state_fast(connection).await.map(|_|())}";
+ if body != expected {
+ return Err(
+ "validate_active_hook_state_fast must remain the constant-cost rebuild-marker and structural-source check"
+ .to_owned(),
+ );
+ }
+ Ok(())
+}
+
+const FOOD_READ_AUTHORITY_JOINS: &str = "JOIN radroots_event_store_source_state AS source ON source.singleton = 1 AND source.active_generation = projection.source_generation JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.singleton = 1 AND cursor.source_generation = projection.source_generation JOIN radroots_event_store_addressable_head_state AS head ON head.source_generation = projection.source_generation AND head.kind = 30402 AND head.pubkey = projection.pubkey AND head.d_tag = projection.d_tag AND head.raw_head_event_id = projection.event_id AND head.raw_head_event_seq = projection.event_seq AND head.raw_head_created_at = projection.created_at AND head.admission_status = 'admitted' AND head.admission_code IS NULL AND head.contract_id = projection.contract_id AND head.visibility = 'visible' AND head.nip09_outcome = 'visible'";
+const FOOD_RECENT_SOURCE_FIRST_AUTHORITY_JOINS: &str = "FROM radroots_event_store_source_state AS source CROSS JOIN radroots_event_store_food_availability_read_v1 AS projection ON source.singleton = 1 AND source.active_generation = projection.source_generation CROSS JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.singleton = 1 AND cursor.source_generation = projection.source_generation CROSS JOIN radroots_event_store_addressable_head_state AS head ON head.source_generation = projection.source_generation AND head.kind = 30402 AND head.pubkey = projection.pubkey AND head.d_tag = projection.d_tag AND head.raw_head_event_id = projection.event_id AND head.raw_head_event_seq = projection.event_seq AND head.raw_head_created_at = projection.created_at AND head.admission_status = 'admitted' AND head.admission_code IS NULL AND head.contract_id = projection.contract_id AND head.visibility = 'visible' AND head.nip09_outcome = 'visible'";
+
+#[derive(Default)]
+struct SqlxQueryRouteCollector {
+ constants: Vec<String>,
+ malformed: bool,
+}
+
+impl<'ast> Visit<'ast> for SqlxQueryRouteCollector {
+ fn visit_expr_call(&mut self, expression: &'ast syn::ExprCall) {
+ if compact_tokens(expression.func.as_ref()) == "sqlx::query" {
+ match expression.args.iter().collect::<Vec<_>>().as_slice() {
+ [Expr::Path(path)]
+ if path.qself.is_none()
+ && path.path.leading_colon.is_none()
+ && path.path.segments.len() == 1 =>
+ {
+ self.constants.push(path.path.segments[0].ident.to_string());
+ }
+ _ => self.malformed = true,
+ }
+ }
+ syn::visit::visit_expr_call(self, expression);
+ }
+}
+
+fn validate_food_read_query_sources(source: &str) -> Result<(), String> {
+ let syntax = syn::parse_file(source).map_err(|error| {
+ format!("parse crates/event_store/src/store/food_availability_projection_v1.rs: {error}")
+ })?;
+ let expected_constants = [
+ "FOOD_AVAILABILITY_POINT_QUERY_V1",
+ "FOOD_AVAILABILITY_RECENT_QUERY_V1",
+ "FOOD_AVAILABILITY_RECENT_STATUS_QUERY_V1",
+ "FOOD_AVAILABILITY_SEARCH_QUERY_V1",
+ ];
+ let mut query_values = BTreeMap::new();
+ let mut query_constant_names = BTreeSet::new();
+ for item in &syntax.items {
+ let Item::Const(item_const) = item else {
+ continue;
+ };
+ let name = item_const.ident.to_string();
+ if !name.starts_with("FOOD_AVAILABILITY_") || !name.ends_with("_QUERY_V1") {
+ continue;
+ }
+ query_constant_names.insert(name.clone());
+ if compact_tokens(&item_const.vis) != "pub(super)"
+ || compact_tokens(item_const.ty.as_ref()) != "&str"
+ {
+ return Err(format!(
+ "governed Food query constant {name} must be pub(super) const &str"
+ ));
+ }
+ let Expr::Lit(literal) = strip_expression_wrappers(item_const.expr.as_ref()) else {
+ return Err(format!(
+ "governed Food query constant {name} must contain one string literal"
+ ));
+ };
+ let Lit::Str(value) = &literal.lit else {
+ return Err(format!(
+ "governed Food query constant {name} must contain one string literal"
+ ));
+ };
+ query_values.insert(name, value.value());
+ }
+ let expected_constant_names = expected_constants
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<BTreeSet<_>>();
+ if query_constant_names != expected_constant_names
+ || query_values.len() != expected_constants.len()
+ {
+ return Err("governed Food query constant inventory is not exact".to_owned());
+ }
+
+ let expected_methods: [(&str, &[&str]); 3] = [
+ (
+ "food_availability_v1",
+ &["FOOD_AVAILABILITY_POINT_QUERY_V1"],
+ ),
+ (
+ "recent_food_availability_v1",
+ &[
+ "FOOD_AVAILABILITY_RECENT_QUERY_V1",
+ "FOOD_AVAILABILITY_RECENT_STATUS_QUERY_V1",
+ ],
+ ),
+ (
+ "search_food_availability_v1",
+ &["FOOD_AVAILABILITY_SEARCH_QUERY_V1"],
+ ),
+ ];
+ let mut observed_methods = BTreeSet::new();
+ for item in &syntax.items {
+ let Item::Impl(item_impl) = item else {
+ continue;
+ };
+ if compact_tokens(item_impl.self_ty.as_ref()) != "RadrootsEventStore" {
+ continue;
+ }
+ for item in &item_impl.items {
+ let syn::ImplItem::Fn(function) = item else {
+ continue;
+ };
+ let method = function.sig.ident.to_string();
+ let Some((_, expected_routes)) = expected_methods
+ .iter()
+ .find(|(expected, _)| *expected == method.as_str())
+ else {
+ continue;
+ };
+ if !observed_methods.insert(method.clone()) {
+ return Err(format!("duplicate governed Food read method {method}"));
+ }
+ let mut collector = SqlxQueryRouteCollector::default();
+ collector.visit_block(&function.block);
+ if collector.malformed
+ || collector.constants
+ != expected_routes
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<Vec<_>>()
+ {
+ return Err(format!(
+ "Food read method {method} does not route through its exact governed query constant(s)"
+ ));
+ }
+ }
+ }
+ if observed_methods.len() != expected_methods.len() {
+ return Err("governed Food read query inventory is incomplete".to_owned());
+ }
+ for constant in expected_constants {
+ let query = query_values
+ .get(constant)
+ .expect("exact governed query constant inventory checked above");
+ let normalized = query.split_whitespace().collect::<Vec<_>>().join(" ");
+ let authority_joins = if constant == "FOOD_AVAILABILITY_RECENT_QUERY_V1" {
+ FOOD_RECENT_SOURCE_FIRST_AUTHORITY_JOINS
+ } else {
+ FOOD_READ_AUTHORITY_JOINS
+ };
+ if normalized.contains("radroots_event_store_current_visibility_v1")
+ || normalized.matches(authority_joins).count() != 1
+ {
+ return Err(
+ "every Food point/recent/search query must use the exact fail-closed persisted source/cursor/head authority joins"
+ .to_owned(),
+ );
+ }
+ let start = normalized
+ .find(authority_joins)
+ .expect("exact authority joins occurrence checked above");
+ let suffix = &normalized[start..];
+ let end = [" WHERE ", " ORDER BY "]
+ .iter()
+ .filter_map(|marker| suffix.find(marker))
+ .min()
+ .ok_or_else(|| {
+ "governed Food read query has no clause after its authority joins".to_owned()
+ })?;
+ if &suffix[..end] != authority_joins {
+ return Err(
+ "Food read source/cursor/head authority joins contain an ungoverned predicate or disjunction"
+ .to_owned(),
+ );
+ }
+ }
+ Ok(())
+}
+
+fn validate_food_projection_audit_authority(source: &str) -> Result<(), String> {
+ const EXPECTED_HEAD_QUERY: &str = "SELECT pubkey, d_tag, raw_head_event_id, raw_head_event_seq, raw_head_created_at FROM radroots_event_store_addressable_head_state WHERE source_generation = ? AND kind = 30402 AND admission_status = 'admitted' AND admission_code IS NULL AND contract_id = ? AND visibility = 'visible' AND nip09_outcome = 'visible' ORDER BY pubkey, d_tag";
+ const SOURCE_TRANSITION_QUERY: &str = "SELECT EXISTS(SELECT 1 FROM radroots_event_store_addressable_head_transition AS transition WHERE transition.transition_seq = ? AND transition.source_generation = ? AND transition.source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1) AND transition.kind = 30402 AND transition.pubkey = ? AND transition.d_tag = ? AND transition.raw_head_event_id = ? AND transition.raw_head_event_seq = ? AND transition.raw_head_created_at = ? AND transition.visible_event_id = ? AND transition.visible_event_seq = ? AND transition.admission_status = 'admitted' AND transition.admission_code IS NULL AND transition.contract_id = ? AND transition.visibility = 'visible' AND transition.nip09_outcome = 'visible' AND transition.raw_head_decision IN ('baseline_rebuild', 'applied') AND transition.transition_seq = (SELECT MAX(candidate.transition_seq) FROM radroots_event_store_addressable_head_transition AS candidate WHERE candidate.source_generation = transition.source_generation AND candidate.kind = transition.kind AND candidate.pubkey = transition.pubkey AND candidate.d_tag = transition.d_tag AND candidate.raw_head_decision IN ('baseline_rebuild', 'applied')))";
+
+ let syntax = syn::parse_file(source).map_err(|error| {
+ format!("parse crates/event_store/src/store/food_availability_projection_v1.rs: {error}")
+ })?;
+ let audit_methods = syntax
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Impl(item_impl)
+ if compact_tokens(item_impl.self_ty.as_ref()) == "RadrootsEventStore" =>
+ {
+ Some(item_impl)
+ }
+ _ => None,
+ })
+ .flat_map(|item_impl| item_impl.items.iter())
+ .filter_map(|item| match item {
+ syn::ImplItem::Fn(function)
+ if function.sig.ident == "audit_food_availability_projection_v1" =>
+ {
+ Some(function)
+ }
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [audit] = audit_methods.as_slice() else {
+ return Err(format!(
+ "Food projection store must define exactly one public audit method; found {}",
+ audit_methods.len()
+ ));
+ };
+ let audit_visibility = compact_tokens(&audit.vis);
+ let audit_signature = compact_tokens(&audit.sig);
+ if audit_visibility != "pub"
+ || audit_signature
+ != "asyncfnaudit_food_availability_projection_v1(&self,)->Result<(),RadrootsEventStoreError>"
+ {
+ return Err(format!(
+ "Food projection audit method signature drifted: visibility `{audit_visibility}`, signature `{audit_signature}`"
+ ));
+ }
+ let expected_audit: syn::Block = syn::parse_str(
+ "{ let mut tx = self.begin_write_transaction().await?; validate_food_availability_projection_hook_v1(&mut tx).await?; tx.commit().await?; Ok(()) }",
+ )
+ .map_err(|error| format!("parse governed Food audit body: {error}"))?;
+ if compact_tokens(&audit.block) != compact_tokens(&expected_audit) {
+ return Err(
+ "Food projection audit must use the exact serialized write transaction, exhaustive validator, commit route"
+ .to_owned(),
+ );
+ }
+
+ let exact_free_function = |name: &str| -> Result<&syn::ItemFn, String> {
+ let functions = syntax
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Fn(function) if function.sig.ident == name => Some(function),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [function] = functions.as_slice() else {
+ return Err(format!(
+ "Food projection store must define exactly one `{name}` function; found {}",
+ functions.len()
+ ));
+ };
+ Ok(function)
+ };
+
+ let exhaustive = exact_free_function("validate_food_availability_projection_hook_v1")?;
+ let exhaustive_visibility = compact_tokens(&exhaustive.vis);
+ let exhaustive_signature = compact_tokens(&exhaustive.sig);
+ if exhaustive_visibility != "pub(crate)"
+ || exhaustive_signature
+ != "asyncfnvalidate_food_availability_projection_hook_v1(connection:&mutSqliteConnection,)->Result<(),RadrootsEventStoreError>"
+ {
+ return Err(format!(
+ "Food exhaustive audit signature drifted: visibility `{exhaustive_visibility}`, signature `{exhaustive_signature}`"
+ ));
+ }
+ let expected_exhaustive: syn::Block = syn::parse_str(
+ r#"{
+ let state = food_availability_projection_cursor_state_fast_v1(connection).await?;
+ let generation = state.feed_cursor.source_generation();
+
+ let rows = sqlx::query(
+ "SELECT source_generation, pubkey, d_tag, event_id, event_seq, created_at, contract_id, content, title, summary, published_at, location, price_amount, price_currency, price_unit, quantity_amount, quantity_unit, status, diagnostic_codes_json, source_transition_seq, immutable_raw_json, stored_images_json FROM radroots_event_store_food_availability_read_v1 WHERE source_generation = ? ORDER BY pubkey, d_tag",
+ )
+ .bind(generation.as_bytes().as_slice())
+ .fetch_all(&mut *connection)
+ .await?;
+ let mut actual_coordinates = Vec::with_capacity(rows.len());
+ for row in rows {
+ let projection = load_and_validate_projection_row(row)?;
+ validate_projection_source_transition(connection, &projection).await?;
+ validate_fts_row(connection, &projection).await?;
+ actual_coordinates.push((
+ projection.pubkey().as_str().to_owned(),
+ projection.identifier().as_str().to_owned(),
+ projection.event_id().as_str().to_owned(),
+ projection.event_seq(),
+ i64_from_u64("food.created_at", projection.created_at())?,
+ ));
+ }
+ let actual_row_count = i64::try_from(actual_coordinates.len())
+ .map_err(|_| projection_drift("projection row count exceeds i64"))?;
+ if actual_row_count != state.projected_row_count {
+ return Err(projection_drift(format!(
+ "projection row count {} differs from sealed count {}",
+ actual_row_count, state.projected_row_count,
+ )));
+ }
+ let expected_coordinates = sqlx::query(
+ "SELECT pubkey, d_tag, raw_head_event_id, raw_head_event_seq, raw_head_created_at FROM radroots_event_store_addressable_head_state WHERE source_generation = ? AND kind = 30402 AND admission_status = 'admitted' AND admission_code IS NULL AND contract_id = ? AND visibility = 'visible' AND nip09_outcome = 'visible' ORDER BY pubkey, d_tag",
+ )
+ .bind(generation.as_bytes().as_slice())
+ .bind(FOOD_AVAILABILITY_CONTRACT_ID)
+ .fetch_all(&mut *connection)
+ .await?
+ .into_iter()
+ .map(|row| {
+ Ok::<_, sqlx::Error>((
+ row.try_get::<String, _>("pubkey")?,
+ row.try_get::<String, _>("d_tag")?,
+ row.try_get::<String, _>("raw_head_event_id")?,
+ row.try_get::<i64, _>("raw_head_event_seq")?,
+ row.try_get::<i64, _>("raw_head_created_at")?,
+ ))
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+ if actual_coordinates != expected_coordinates {
+ return Err(projection_drift(
+ "projection coordinate witnesses do not equal the current admitted, visible FoodAvailability heads",
+ ));
+ }
+ let fts_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_food_availability_search_fts",
+ )
+ .fetch_one(&mut *connection)
+ .await?;
+ if fts_count != state.projected_row_count {
+ return Err(projection_drift(format!(
+ "FoodAvailability FTS row count {fts_count} differs from sealed count {}",
+ state.projected_row_count,
+ )));
+ }
+ #[cfg(test)]
+ wait_at_food_availability_audit_fts_checkpoint().await;
+ sqlx::query(
+ "INSERT INTO radroots_event_store_food_availability_search_fts(radroots_event_store_food_availability_search_fts) VALUES('integrity-check')",
+ )
+ .execute(&mut *connection)
+ .await
+ .map_err(|error| projection_drift(format!("FoodAvailability FTS integrity check failed: {error}")))?;
+ Ok(())
+ }"#,
+ )
+ .map_err(|error| format!("parse governed exhaustive Food audit body: {error}"))?;
+ if compact_tokens(&exhaustive.block) != compact_tokens(&expected_exhaustive) {
+ return Err(
+ "Food exhaustive audit complete function body drifted from the exact fail-closed authority seal"
+ .to_owned(),
+ );
+ }
+
+ let row_loops = exhaustive
+ .block
+ .stmts
+ .iter()
+ .filter_map(|statement| match statement {
+ syn::Stmt::Expr(Expr::ForLoop(expression), _)
+ if compact_tokens(&expression.pat) == "row"
+ && compact_tokens(&expression.expr) == "rows" =>
+ {
+ Some(expression)
+ }
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [row_loop] = row_loops.as_slice() else {
+ return Err(format!(
+ "Food exhaustive audit must contain exactly one direct projection-row loop; found {}",
+ row_loops.len()
+ ));
+ };
+ let expected_row_loop: syn::Block = syn::parse_str(
+ r#"{
+ let projection = load_and_validate_projection_row(row)?;
+ validate_projection_source_transition(connection, &projection).await?;
+ validate_fts_row(connection, &projection).await?;
+ actual_coordinates.push((
+ projection.pubkey().as_str().to_owned(),
+ projection.identifier().as_str().to_owned(),
+ projection.event_id().as_str().to_owned(),
+ projection.event_seq(),
+ i64_from_u64("food.created_at", projection.created_at())?,
+ ));
+ }"#,
+ )
+ .map_err(|error| format!("parse governed Food projection-row audit: {error}"))?;
+ if compact_tokens(&row_loop.body) != compact_tokens(&expected_row_loop) {
+ return Err(
+ "Food exhaustive audit must validate each loaded projection's exact source transition before its FTS row and collect the five coordinate witnesses"
+ .to_owned(),
+ );
+ }
+
+ for (label, expected) in [
+ (
+ "checked actual coordinate cardinality",
+ r#"let actual_row_count = i64::try_from(actual_coordinates.len())
+ .map_err(|_| projection_drift("projection row count exceeds i64"))?;"#,
+ ),
+ (
+ "sealed row-count equality",
+ r#"if actual_row_count != state.projected_row_count {
+ return Err(projection_drift(format!(
+ "projection row count {} differs from sealed count {}",
+ actual_row_count, state.projected_row_count,
+ )));
+ }"#,
+ ),
+ (
+ "fail-closed coordinate equality",
+ r#"if actual_coordinates != expected_coordinates {
+ return Err(projection_drift(
+ "projection coordinate witnesses do not equal the current admitted, visible FoodAvailability heads",
+ ));
+ }"#,
+ ),
+ ] {
+ let expected: syn::Stmt = syn::parse_str(expected)
+ .map_err(|error| format!("parse governed Food {label} statement: {error}"))?;
+ let occurrences = exhaustive
+ .block
+ .stmts
+ .iter()
+ .filter(|statement| compact_tokens(*statement) == compact_tokens(&expected))
+ .count();
+ if occurrences != 1 {
+ return Err(format!(
+ "Food exhaustive audit must contain exactly one {label} statement; found {occurrences}"
+ ));
+ }
+ }
+
+ let expected_coordinates = exhaustive
+ .block
+ .stmts
+ .iter()
+ .filter_map(|statement| match statement {
+ syn::Stmt::Local(local)
+ if matches!(&local.pat, syn::Pat::Ident(ident) if ident.ident == "expected_coordinates") =>
+ {
+ local.init.as_ref().map(|init| init.expr.as_ref())
+ }
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [expected_coordinates] = expected_coordinates.as_slice() else {
+ return Err(format!(
+ "Food exhaustive audit must define exactly one expected-coordinate query; found {}",
+ expected_coordinates.len()
+ ));
+ };
+ let expected_coordinates_expression: Expr = syn::parse_str(&format!(
+ r#"sqlx::query({EXPECTED_HEAD_QUERY:?},)
+ .bind(generation.as_bytes().as_slice())
+ .bind(FOOD_AVAILABILITY_CONTRACT_ID)
+ .fetch_all(&mut *connection)
+ .await?
+ .into_iter()
+ .map(|row| {{
+ Ok::<_, sqlx::Error>((
+ row.try_get::<String, _>("pubkey")?,
+ row.try_get::<String, _>("d_tag")?,
+ row.try_get::<String, _>("raw_head_event_id")?,
+ row.try_get::<i64, _>("raw_head_event_seq")?,
+ row.try_get::<i64, _>("raw_head_created_at")?,
+ ))
+ }})
+ .collect::<Result<Vec<_>, _>>()?"#,
+ ))
+ .map_err(|error| format!("parse governed Food expected-coordinate query: {error}"))?;
+ let actual_expected_coordinates = compact_tokens(*expected_coordinates);
+ let governed_expected_coordinates = compact_tokens(&expected_coordinates_expression);
+ if actual_expected_coordinates != governed_expected_coordinates {
+ return Err(format!(
+ "Food exhaustive audit expected-head query must bind the active generation and Food contract and map the exact five admitted visible head witnesses: expected `{governed_expected_coordinates}`, found `{actual_expected_coordinates}`"
+ ));
+ }
+
+ let source_transition = exact_free_function("validate_projection_source_transition")?;
+ let expected_source_transition: syn::Block = syn::parse_str(&format!(
+ r#"{{
+ let authoritative: i64 = sqlx::query_scalar({SOURCE_TRANSITION_QUERY:?},)
+ .bind(projection.source_transition_seq())
+ .bind(projection.source_generation().as_bytes().as_slice())
+ .bind(projection.pubkey().as_str())
+ .bind(projection.identifier().as_str())
+ .bind(projection.event_id().as_str())
+ .bind(projection.event_seq())
+ .bind(i64_from_u64("food.created_at", projection.created_at())?)
+ .bind(projection.event_id().as_str())
+ .bind(projection.event_seq())
+ .bind(FOOD_AVAILABILITY_CONTRACT_ID)
+ .fetch_one(&mut *connection)
+ .await?;
+ if authoritative != 1 {{
+ return Err(projection_drift(
+ "stored FoodAvailability source transition is not authoritative for its projection",
+ ));
+ }}
+ Ok(())
+ }}"#,
+ ))
+ .map_err(|error| format!("parse governed Food source-transition authority: {error}"))?;
+ if compact_tokens(&source_transition.block) != compact_tokens(&expected_source_transition) {
+ return Err(
+ "Food source-transition audit must bind the exact active-generation, coordinate, head, visibility, admission, contract, and latest applied transition authority"
+ .to_owned(),
+ );
+ }
+ Ok(())
+}
+
+const SOURCE_CAPACITY_HOOK_MATCH_TOKENS: &str = "migration.hook,EventStoreMigrationHook::Nip09ReconciliationV1|EventStoreMigrationHook::FoodAvailabilityProjectionV1";
+
+#[derive(Default)]
+struct SourceCapacityAuthorityCollector {
+ pending_hook_calls: usize,
+ temp_schema_calls: usize,
+ capacity_calls: usize,
+ apply_migration_up_calls: usize,
+ hook_matches: Vec<String>,
+}
+
+impl<'ast> Visit<'ast> for SourceCapacityAuthorityCollector {
+ fn visit_expr_call(&mut self, expression: &'ast syn::ExprCall) {
+ match compact_tokens(expression.func.as_ref()).as_str() {
+ "has_pending_source_capacity_hook" => self.pending_hook_calls += 1,
+ "validate_event_store_temp_schema_with_registry" => self.temp_schema_calls += 1,
+ "validate_reconciliation_capacity" => self.capacity_calls += 1,
+ "apply_migration_up" => self.apply_migration_up_calls += 1,
+ _ => {}
+ }
+ syn::visit::visit_expr_call(self, expression);
+ }
+
+ fn visit_expr_macro(&mut self, expression: &'ast syn::ExprMacro) {
+ if compact_tokens(&expression.mac.path) == "matches" {
+ let tokens = compact_tokens(&expression.mac.tokens);
+ if tokens.contains("EventStoreMigrationHook::Nip09ReconciliationV1")
+ || tokens.contains("EventStoreMigrationHook::FoodAvailabilityProjectionV1")
+ {
+ self.hook_matches.push(tokens);
+ }
+ }
+ syn::visit::visit_expr_macro(self, expression);
+ }
+}
+
+fn exact_source_capacity_function<'a>(
+ syntax: &'a syn::File,
+ name: &str,
+) -> Result<&'a syn::ItemFn, String> {
+ let functions = syntax
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Fn(function) if function.sig.ident == name => Some(function),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [function] = functions.as_slice() else {
+ return Err(format!(
+ "Food schema source-capacity authority must define exactly one `{name}` function; found {}",
+ functions.len()
+ ));
+ };
+ Ok(function)
+}
+
+fn governed_statement_tokens(label: &str, source: &str) -> Result<String, String> {
+ let block: syn::Block = syn::parse_str(&format!("{{ {source} }}"))
+ .map_err(|error| format!("parse governed Food {label} statement: {error}"))?;
+ let [statement] = block.stmts.as_slice() else {
+ return Err(format!(
+ "governed Food {label} source must parse as exactly one statement"
+ ));
+ };
+ Ok(compact_tokens(statement))
+}
+
+fn exact_top_level_statement_index(
+ block: &syn::Block,
+ label: &str,
+ expected_tokens: &str,
+) -> Result<usize, String> {
+ let indices = block
+ .stmts
+ .iter()
+ .enumerate()
+ .filter_map(|(index, statement)| {
+ (compact_tokens(statement) == expected_tokens).then_some(index)
+ })
+ .collect::<Vec<_>>();
+ let [index] = indices.as_slice() else {
+ return Err(format!(
+ "Food schema source-capacity authority must contain exactly one top-level {label} statement; found {}",
+ indices.len()
+ ));
+ };
+ Ok(*index)
+}
+
+fn validate_source_capacity_authority(source: &str) -> Result<(), String> {
+ let syntax = syn::parse_file(source)
+ .map_err(|error| format!("parse crates/event_store/src/schema.rs: {error}"))?;
+
+ let pending = exact_source_capacity_function(&syntax, "has_pending_source_capacity_hook")?;
+ let expected_pending: syn::Block = syn::parse_str(
+ r#"{
+ let current_version = match status {
+ RadrootsEventStoreSchemaStatus::Uninitialized => return false,
+ RadrootsEventStoreSchemaStatus::UnledgeredBaseline => registry[0].version,
+ RadrootsEventStoreSchemaStatus::Managed { version } => *version,
+ };
+ registry.iter().any(|migration| {
+ migration.version > current_version
+ && matches!(
+ migration.hook,
+ EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ )
+ })
+ }"#,
+ )
+ .map_err(|error| format!("parse governed Food pending-capacity selector: {error}"))?;
+ if compact_tokens(&pending.block) != compact_tokens(&expected_pending) {
+ return Err(
+ "Food schema source-capacity pending-hook selector must cover exactly the NIP-09 and Food projection rebuild hooks"
+ .to_owned(),
+ );
+ }
+
+ let outer = exact_source_capacity_function(
+ &syntax,
+ "migrate_event_store_schema_with_registry_and_generation_provider",
+ )?;
+ let expected_outer_preflight = governed_statement_tokens(
+ "outer preflight",
+ r#"
+ if has_pending_source_capacity_hook(&status, registry) {
+ let mut connection = pool.acquire().await?;
+ validate_event_store_temp_schema_with_registry(&mut connection, registry).await?;
+ validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;
+ }
+ "#,
+ )?;
+ let expected_begin_immediate = governed_statement_tokens(
+ "BEGIN IMMEDIATE",
+ r#"let mut transaction = pool.begin_with("BEGIN IMMEDIATE").await?;"#,
+ )?;
+ let preflight_index = exact_top_level_statement_index(
+ &outer.block,
+ "outer preflight",
+ &expected_outer_preflight,
+ )?;
+ let begin_immediate_index = exact_top_level_statement_index(
+ &outer.block,
+ "BEGIN IMMEDIATE",
+ &expected_begin_immediate,
+ )?;
+ if preflight_index >= begin_immediate_index {
+ return Err(
+ "Food schema source-capacity outer preflight must complete before BEGIN IMMEDIATE"
+ .to_owned(),
+ );
+ }
+ let mut outer_authority = SourceCapacityAuthorityCollector::default();
+ outer_authority.visit_block(&outer.block);
+ if outer_authority.pending_hook_calls != 1
+ || outer_authority.temp_schema_calls != 1
+ || outer_authority.capacity_calls != 1
+ || outer_authority.apply_migration_up_calls != 0
+ || !outer_authority.hook_matches.is_empty()
+ {
+ return Err(format!(
+ "Food schema source-capacity outer preflight call inventory is not exact: pending={}, temp_schema={}, capacity={}, apply_up={}, hook_matches={}",
+ outer_authority.pending_hook_calls,
+ outer_authority.temp_schema_calls,
+ outer_authority.capacity_calls,
+ outer_authority.apply_migration_up_calls,
+ outer_authority.hook_matches.len()
+ ));
+ }
+
+ let inner = exact_source_capacity_function(&syntax, "migrate_schema_on_connection")?;
+ let mut migration_loops = Vec::new();
+ for statement in &inner.block.stmts {
+ let syn::Stmt::Expr(Expr::ForLoop(expression), _) = statement else {
+ continue;
+ };
+ let mut collector = SourceCapacityAuthorityCollector::default();
+ collector.visit_block(&expression.body);
+ if collector.apply_migration_up_calls > 0 {
+ migration_loops.push((expression, collector));
+ }
+ }
+ let [(migration_loop, inner_authority)] = migration_loops.as_slice() else {
+ return Err(format!(
+ "Food schema source-capacity authority must define exactly one migration-application loop; found {}",
+ migration_loops.len()
+ ));
+ };
+ if inner_authority.pending_hook_calls != 0
+ || inner_authority.temp_schema_calls != 0
+ || inner_authority.capacity_calls != 1
+ || inner_authority.apply_migration_up_calls != 1
+ || inner_authority.hook_matches.len() != 1
+ || inner_authority.hook_matches[0] != SOURCE_CAPACITY_HOOK_MATCH_TOKENS
+ {
+ return Err(format!(
+ "Food schema source-capacity in-transaction recheck inventory is not exact: pending={}, temp_schema={}, capacity={}, apply_up={}, hook_matches={:?}",
+ inner_authority.pending_hook_calls,
+ inner_authority.temp_schema_calls,
+ inner_authority.capacity_calls,
+ inner_authority.apply_migration_up_calls,
+ inner_authority.hook_matches
+ ));
+ }
+ let expected_inner_recheck = governed_statement_tokens(
+ "in-transaction recheck",
+ r#"
+ if matches!(
+ migration.hook,
+ EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ ) {
+ validate_reconciliation_capacity(connection, reconciliation_limits).await?;
+ }
+ "#,
+ )?;
+ let expected_apply = governed_statement_tokens(
+ "migration DDL application",
+ "apply_migration_up(connection, registry, migration).await?;",
+ )?;
+ let recheck_index = exact_top_level_statement_index(
+ &migration_loop.body,
+ "in-transaction recheck",
+ &expected_inner_recheck,
+ )?;
+ let apply_index = exact_top_level_statement_index(
+ &migration_loop.body,
+ "migration DDL application",
+ &expected_apply,
+ )?;
+ if recheck_index.checked_add(1) != Some(apply_index) {
+ return Err(
+ "Food schema source-capacity in-transaction recheck must occur immediately before migration DDL"
+ .to_owned(),
+ );
+ }
+
+ Ok(())
+}
+
+fn validate_source_contract(workspace_root: &Path) -> Result<(), String> {
+ validate_blossom_dependency_authority(workspace_root)?;
+ validate_public_api_authority(workspace_root)?;
+ validate_migration_guard_limits(workspace_root)?;
+ require_source_markers(
+ workspace_root,
+ "crates/blossom/src/lib.rs",
+ &["pubusehash::{", "RadrootsBlossomSha256"],
+ )?;
+ require_source_markers(
+ workspace_root,
+ "crates/blossom/src/hash.rs",
+ &[
+ "pubstructRadrootsBlossomSha256([u8;SHA256_BYTES])",
+ "pubfnfrom_hex(value:&str)->Result<Self,RadrootsBlossomError>",
+ "pubconstfnas_bytes(&self)->&[u8;SHA256_BYTES]",
+ "pubfnto_hex(self)->String",
+ ],
+ )?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_codec/src/admission/registry_v7.rs",
+ &[
+ "pubfnadmit_verified_event_registry_v7",
+ "project_verified_food_availability_event_registry_v7",
+ "RADROOTS_FOOD_AVAILABILITY_CONTRACT_ID",
+ ],
+ )?;
+ require_source_markers(
+ workspace_root,
+ MIGRATIONS_SOURCE_RELATIVE,
+ &[
+ "EventStoreMigrationHook::FoodAvailabilityProjectionV1",
+ "version:3",
+ "name:\"food_availability_projection\"",
+ "include_str!(\"../migrations/0003_food_availability_projection.up.sql\")",
+ "include_str!(\"../migrations/0003_food_availability_projection.down.sql\")",
+ "validate_generated_food_availability_projection_manifest_descriptor()",
+ ],
+ )?;
+ let schema_hooks = read_regular_file(workspace_root, "crates/event_store/src/schema.rs")?;
+ let schema_hooks_source = std::str::from_utf8(&schema_hooks)
+ .map_err(|error| format!("crates/event_store/src/schema.rs must be UTF-8: {error}"))?;
+ validate_source_capacity_authority(schema_hooks_source)?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/schema.rs",
+ &[
+ "apply_food_availability_projection_hook_v1",
+ "validate_food_availability_projection_hook_state_fast_v1",
+ "has_pending_source_capacity_hook",
+ "EventStoreMigrationHook::Nip09ReconciliationV1|EventStoreMigrationHook::FoodAvailabilityProjectionV1",
+ "EventStoreMigrationHook::FoodAvailabilityProjectionV1",
+ ],
+ )?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/model/addressable_transition_feed_v1.rs",
+ &[
+ "RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1:u32=1",
+ "pubfnfood_availability()->Self",
+ "Self::new([30_402])",
+ "SCOPE_FINGERPRINT_DOMAIN_V1",
+ ],
+ )?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/model/food_availability_projection_v1.rs",
+ &[
+ "RadrootsBlossomSha256",
+ "pubconstfnblossom_sha256(&self)->Option<RadrootsBlossomSha256>",
+ ],
+ )?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/store/post_core_extension_dispatcher.rs",
+ &["capabilities.apply_v1", "capabilities.apply_v2"],
+ )?;
+ let dispatcher = compact_source(
+ workspace_root,
+ "crates/event_store/src/store/post_core_extension_dispatcher.rs",
+ )?;
+ let v1 = dispatcher
+ .find("capabilities.apply_v1")
+ .ok_or_else(|| "post-core dispatcher is missing apply_v1".to_owned())?;
+ let v2 = dispatcher
+ .find("capabilities.apply_v2")
+ .ok_or_else(|| "post-core dispatcher is missing apply_v2".to_owned())?;
+ if v1 >= v2 {
+ return Err("post-core dispatcher must apply v1 before additive v2".to_owned());
+ }
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/store/post_core_extension_capabilities.rs",
+ &["pub(super)asyncfnapply_v2", "apply_post_core_extensions_v2"],
+ )?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/store/post_core_extensions_v2.rs",
+ &["apply_pending_food_availability_transitions"],
+ )?;
+ let food_projection_store = read_regular_file(
+ workspace_root,
+ "crates/event_store/src/store/food_availability_projection_v1.rs",
+ )?;
+ let food_projection_store_source = std::str::from_utf8(&food_projection_store).map_err(
+ |error| {
+ format!(
+ "crates/event_store/src/store/food_availability_projection_v1.rs must be UTF-8: {error}"
+ )
+ },
+ )?;
+ validate_food_read_query_sources(food_projection_store_source)?;
+ validate_food_projection_audit_authority(food_projection_store_source)?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/store/food_availability_projection_v1.rs",
+ &[
+ "pubasyncfnfood_availability_v1",
+ "pubasyncfnrecent_food_availability_v1",
+ "pubasyncfnsearch_food_availability_v1",
+ "pubasyncfnaudit_food_availability_projection_v1",
+ "apply_food_availability_projection_hook_v1",
+ "validate_food_availability_projection_hook_v1",
+ ],
+ )?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/store/current_visibility_v1.rs",
+ &[
+ "FROMradroots_event_store_addressable_head_stateASstate",
+ "state.raw_head_event_id",
+ "state.nip09_outcome",
+ ],
+ )?;
+ let predecessor_fast_source = read_regular_file(
+ workspace_root,
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ )?;
+ validate_fast_active_hook_source(std::str::from_utf8(&predecessor_fast_source).map_err(
+ |error| format!("crates/event_store/src/nip09/reconciliation_v1.rs must be UTF-8: {error}"),
+ )?)?;
+ require_source_markers(
+ workspace_root,
+ "crates/event_store/src/store.rs",
+ &[
+ "dispatch_post_core_extensions",
+ "PRAGMAmain.journal_mode=WAL",
+ "SqliteFileJournalModeNotWal",
+ ],
+ )?;
+ Ok(())
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct PublicUseRoute {
+ segments: Vec<String>,
+ exported_name: String,
+ renamed: bool,
+ glob: bool,
+ absolute: bool,
+ attributes: Vec<String>,
+}
+
+fn validate_public_api_authority(workspace_root: &Path) -> Result<(), String> {
+ let model_bytes = read_regular_file(workspace_root, EVENT_STORE_MODEL_RELATIVE)?;
+ let model_source = std::str::from_utf8(&model_bytes)
+ .map_err(|error| format!("{EVENT_STORE_MODEL_RELATIVE} must be UTF-8 Rust: {error}"))?;
+ let lib_bytes = read_regular_file(workspace_root, EVENT_STORE_LIB_RELATIVE)?;
+ let lib_source = std::str::from_utf8(&lib_bytes)
+ .map_err(|error| format!("{EVENT_STORE_LIB_RELATIVE} must be UTF-8 Rust: {error}"))?;
+ let advertised = PUBLIC_API
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<Vec<_>>();
+ validate_public_api_sources(model_source, lib_source, &advertised)
+}
+
+fn validate_public_api_sources(
+ model_source: &str,
+ lib_source: &str,
+ advertised: &[String],
+) -> Result<(), String> {
+ let model = syn::parse_file(model_source)
+ .map_err(|error| format!("parse {EVENT_STORE_MODEL_RELATIVE}: {error}"))?;
+ let lib = syn::parse_file(lib_source)
+ .map_err(|error| format!("parse {EVENT_STORE_LIB_RELATIVE}: {error}"))?;
+
+ let governed_modules = GOVERNED_PUBLIC_API_MODULES
+ .iter()
+ .map(|module| (*module).to_owned())
+ .collect::<BTreeSet<_>>();
+ let mut represented_modules = BTreeSet::new();
+ let mut governed_exports = BTreeSet::new();
+ for route in collect_top_level_public_use_routes(&model) {
+ let Some(module) = route.segments.first() else {
+ continue;
+ };
+ if !governed_modules.contains(module) {
+ continue;
+ }
+ if route.absolute || route.renamed || route.glob || route.segments.len() != 2 {
+ return Err(format!(
+ "{EVENT_STORE_MODEL_RELATIVE} governed public use `{}` must be a direct, non-renamed symbol re-export",
+ route.segments.join("::")
+ ));
+ }
+ represented_modules.insert(module.clone());
+ if !governed_exports.insert(route.exported_name.clone()) {
+ return Err(format!(
+ "{EVENT_STORE_MODEL_RELATIVE} exports governed symbol `{}` more than once",
+ route.exported_name
+ ));
+ }
+ }
+ if represented_modules != governed_modules {
+ return Err(format!(
+ "{EVENT_STORE_MODEL_RELATIVE} governed public API modules differ: expected {governed_modules:?}, found {represented_modules:?}"
+ ));
+ }
+
+ let advertised_exports = advertised.iter().cloned().collect::<BTreeSet<_>>();
+ if advertised_exports.len() != advertised.len() {
+ return Err("successor PUBLIC_API contains duplicate symbols".to_owned());
+ }
+ if advertised_exports != governed_exports {
+ let missing = governed_exports
+ .difference(&advertised_exports)
+ .cloned()
+ .collect::<Vec<_>>();
+ let unexpected = advertised_exports
+ .difference(&governed_exports)
+ .cloned()
+ .collect::<Vec<_>>();
+ return Err(format!(
+ "successor PUBLIC_API is not exhaustive for governed model exports; missing {missing:?}, unexpected {unexpected:?}"
+ ));
+ }
+
+ let sqlite_cfg = "#[cfg(feature=\"sqlite\")]";
+ let mut crate_root_exports = BTreeSet::new();
+ for route in collect_top_level_public_use_routes(&lib) {
+ if route.segments.first().map(String::as_str) != Some("model") {
+ continue;
+ }
+ if route.attributes.as_slice() != [sqlite_cfg]
+ || route.absolute
+ || route.renamed
+ || route.glob
+ || route.segments.len() != 2
+ {
+ return Err(format!(
+ "{EVENT_STORE_LIB_RELATIVE} public model use `{}` must be a direct, non-renamed #[cfg(feature = \"sqlite\")] re-export",
+ route.segments.join("::")
+ ));
+ }
+ if !crate_root_exports.insert(route.exported_name.clone()) {
+ return Err(format!(
+ "{EVENT_STORE_LIB_RELATIVE} exports model symbol `{}` more than once",
+ route.exported_name
+ ));
+ }
+ }
+ let missing_at_crate_root = governed_exports
+ .difference(&crate_root_exports)
+ .cloned()
+ .collect::<Vec<_>>();
+ if !missing_at_crate_root.is_empty() {
+ return Err(format!(
+ "{EVENT_STORE_LIB_RELATIVE} does not re-export governed successor symbols {missing_at_crate_root:?}"
+ ));
+ }
+ Ok(())
+}
+
+fn collect_top_level_public_use_routes(file: &syn::File) -> Vec<PublicUseRoute> {
+ let mut routes = Vec::new();
+ for item in &file.items {
+ let Item::Use(item_use) = item else {
+ continue;
+ };
+ if !matches!(item_use.vis, syn::Visibility::Public(_)) {
+ continue;
+ }
+ let attributes = item_use
+ .attrs
+ .iter()
+ .map(compact_tokens)
+ .collect::<Vec<_>>();
+ let mut segments = Vec::new();
+ flatten_public_use_tree(
+ &item_use.tree,
+ &mut segments,
+ item_use.leading_colon.is_some(),
+ &attributes,
+ &mut routes,
+ );
+ }
+ routes
+}
+
+fn flatten_public_use_tree(
+ tree: &syn::UseTree,
+ segments: &mut Vec<String>,
+ absolute: bool,
+ attributes: &[String],
+ routes: &mut Vec<PublicUseRoute>,
+) {
+ match tree {
+ syn::UseTree::Path(path) => {
+ segments.push(path.ident.to_string());
+ flatten_public_use_tree(&path.tree, segments, absolute, attributes, routes);
+ segments.pop();
+ }
+ syn::UseTree::Name(name) => {
+ let exported_name = name.ident.to_string();
+ let mut route_segments = segments.clone();
+ route_segments.push(exported_name.clone());
+ routes.push(PublicUseRoute {
+ segments: route_segments,
+ exported_name,
+ renamed: false,
+ glob: false,
+ absolute,
+ attributes: attributes.to_vec(),
+ });
+ }
+ syn::UseTree::Rename(rename) => {
+ let mut route_segments = segments.clone();
+ route_segments.push(rename.ident.to_string());
+ routes.push(PublicUseRoute {
+ segments: route_segments,
+ exported_name: rename.rename.to_string(),
+ renamed: true,
+ glob: false,
+ absolute,
+ attributes: attributes.to_vec(),
+ });
+ }
+ syn::UseTree::Glob(_) => {
+ let mut route_segments = segments.clone();
+ route_segments.push("*".to_owned());
+ routes.push(PublicUseRoute {
+ segments: route_segments,
+ exported_name: "*".to_owned(),
+ renamed: false,
+ glob: true,
+ absolute,
+ attributes: attributes.to_vec(),
+ });
+ }
+ syn::UseTree::Group(group) => {
+ for item in &group.items {
+ flatten_public_use_tree(item, segments, absolute, attributes, routes);
+ }
+ }
+ }
+}
+
+fn compact_tokens(tokens: &impl ToTokens) -> String {
+ tokens
+ .to_token_stream()
+ .to_string()
+ .chars()
+ .filter(|character| !character.is_whitespace())
+ .collect()
+}
+
+fn validate_blossom_dependency_authority(workspace_root: &Path) -> Result<(), String> {
+ let workspace = parse_toml_value(workspace_root, "Cargo.toml")?;
+ let event_store = parse_toml_value(workspace_root, "crates/event_store/Cargo.toml")?;
+ validate_blossom_dependency_values(&workspace, &event_store)
+}
+
+fn parse_toml_value(workspace_root: &Path, relative: &str) -> Result<toml::Value, 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}"))
+}
+
+fn validate_blossom_dependency_values(
+ workspace: &toml::Value,
+ event_store: &toml::Value,
+) -> Result<(), String> {
+ let workspace_dependency = workspace
+ .get("workspace")
+ .and_then(|value| value.get("dependencies"))
+ .and_then(|value| value.get("radroots_blossom"))
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| "Cargo.toml must define workspace dependency radroots_blossom".to_owned())?;
+ if workspace_dependency
+ .get("path")
+ .and_then(toml::Value::as_str)
+ != Some("crates/blossom")
+ || workspace_dependency
+ .get("version")
+ .and_then(toml::Value::as_str)
+ != Some("=1.0.0-alpha.1")
+ || workspace_dependency
+ .get("default-features")
+ .and_then(toml::Value::as_bool)
+ != Some(false)
+ {
+ return Err(
+ "Cargo.toml radroots_blossom dependency must pin the standalone crate with defaults disabled"
+ .to_owned(),
+ );
+ }
+
+ let event_store_dependency = event_store
+ .get("dependencies")
+ .and_then(|value| value.get("radroots_blossom"))
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| {
+ "crates/event_store/Cargo.toml must directly depend on radroots_blossom".to_owned()
+ })?;
+ let features = event_store_dependency
+ .get("features")
+ .and_then(toml::Value::as_array)
+ .map(|features| {
+ features
+ .iter()
+ .filter_map(toml::Value::as_str)
+ .collect::<Vec<_>>()
+ });
+ if event_store_dependency
+ .get("workspace")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ || event_store_dependency
+ .get("default-features")
+ .and_then(toml::Value::as_bool)
+ != Some(false)
+ || features.as_deref() != Some(&["std"][..])
+ {
+ return Err(
+ "crates/event_store/Cargo.toml radroots_blossom dependency must select only the governed std feature"
+ .to_owned(),
+ );
+ }
+ Ok(())
+}
+
+fn require_source_markers(
+ workspace_root: &Path,
+ relative: &str,
+ markers: &[&str],
+) -> Result<(), String> {
+ let compact = compact_source(workspace_root, relative)?;
+ for marker in markers {
+ if !compact.contains(marker) {
+ return Err(format!(
+ "{relative} is missing required successor route `{marker}`"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn compact_source(workspace_root: &Path, relative: &str) -> Result<String, 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 Rust source: {error}"))?;
+ let syntax = syn::parse_file(source)
+ .map_err(|error| format!("parse governed Rust source {relative}: {error}"))?;
+ Ok(syntax
+ .into_token_stream()
+ .to_string()
+ .chars()
+ .filter(|character| !character.is_whitespace())
+ .collect())
+}
+
+fn validate_result_vector(vector: &ProjectionResultVector) -> Result<(), String> {
+ if vector.schema_version != SCHEMA_VERSION
+ || vector.contract_id != CONTRACT_ID
+ || vector.feed_version != ADDRESSABLE_FEED_VERSION
+ || vector.projection_version != PROJECTION_VERSION
+ || vector.scope_kinds != [FOOD_AVAILABILITY_KIND]
+ {
+ return Err(format!(
+ "{RESULT_VECTOR_CANONICAL_RELATIVE} has an invalid successor identity"
+ ));
+ }
+ let required_cases = [
+ "visible_food_availability_projects_and_searches",
+ "invalid_same_timestamp_winner_retracts_projection",
+ "blossom_digest_and_image_diagnostics_are_preserved",
+ "authorized_address_deletion_retracts_projection",
+ "wrong_author_address_deletion_preserves_projection",
+ "post_cutoff_replacement_restores_projection",
+ "operational_listing_head_retracts_food_projection",
+ "food_head_after_operational_listing_restores_projection",
+ "food_feed_cursor_advances_across_unrelated_addressable_traffic",
+ ];
+ if vector.cases.len() != required_cases.len() {
+ return Err(format!(
+ "{RESULT_VECTOR_CANONICAL_RELATIVE} must contain exactly the nine required cases"
+ ));
+ }
+ validate_unique(
+ "result-vector case id",
+ vector.cases.iter().map(|case| case.id.as_str()),
+ )?;
+ if vector
+ .cases
+ .iter()
+ .map(|case| case.id.as_str())
+ .ne(required_cases)
+ {
+ return Err(format!(
+ "{RESULT_VECTOR_CANONICAL_RELATIVE} case order or inventory is not canonical"
+ ));
+ }
+
+ for case in &vector.cases {
+ let transition_page = &case.expected.transition_page;
+ if case.events.is_empty() || transition_page.transitions.is_empty() {
+ return Err(format!(
+ "{} must contain input events and scoped transitions",
+ case.id
+ ));
+ }
+ if case.expected.coordinate.kind != FOOD_AVAILABILITY_KIND {
+ return Err(format!(
+ "{} coordinate kind must be {FOOD_AVAILABILITY_KIND}",
+ case.id
+ ));
+ }
+ validate_hex(
+ &format!("{} coordinate pubkey", case.id),
+ &case.expected.coordinate.pubkey,
+ 64,
+ )?;
+ if case.expected.coordinate.d_tag.is_empty() {
+ return Err(format!("{} coordinate d_tag must not be empty", case.id));
+ }
+
+ let mut event_ids = BTreeSet::new();
+ let mut food_event_ids = BTreeSet::new();
+ let mut scoped_coordinate_event_ids = BTreeSet::new();
+ let mut events_by_id = BTreeMap::new();
+ for (index, observed) in case.events.iter().enumerate() {
+ if observed.observed_at_ms < 0 {
+ return Err(format!(
+ "{} event observed_at_ms must be non-negative",
+ case.id
+ ));
+ }
+ validate_hex(&format!("{} event id", case.id), &observed.event.id, 64)?;
+ validate_hex(
+ &format!("{} event pubkey", case.id),
+ &observed.event.pubkey,
+ 64,
+ )?;
+ validate_hex(
+ &format!("{} event signature", case.id),
+ &observed.event.sig,
+ 128,
+ )?;
+ if observed.event.tags.iter().any(Vec::is_empty)
+ || !event_ids.insert(observed.event.id.as_str())
+ {
+ return Err(format!(
+ "{} contains an invalid or duplicate signed event",
+ case.id
+ ));
+ }
+ validate_expected_ingest(&case.id, observed)?;
+ match observed.role {
+ ProjectionInputRole::ScopedFood | ProjectionInputRole::ScopedNonFood => {
+ let d_tags = observed
+ .event
+ .tags
+ .iter()
+ .filter(|tag| tag.first().map(String::as_str) == Some("d"))
+ .filter_map(|tag| tag.get(1).map(String::as_str))
+ .collect::<Vec<_>>();
+ if observed.event.kind != FOOD_AVAILABILITY_KIND
+ || observed.event.pubkey != case.expected.coordinate.pubkey
+ || d_tags.as_slice() != [case.expected.coordinate.d_tag.as_str()]
+ {
+ return Err(format!(
+ "{} scoped input does not match its kind-30402 coordinate",
+ case.id
+ ));
+ }
+ scoped_coordinate_event_ids.insert(observed.event.id.as_str());
+ if observed.role == ProjectionInputRole::ScopedFood {
+ food_event_ids.insert(observed.event.id.as_str());
+ }
+ }
+ ProjectionInputRole::UnrelatedAddressable => {
+ let d_tags = observed
+ .event
+ .tags
+ .iter()
+ .filter(|tag| tag.first().map(String::as_str) == Some("d"))
+ .filter_map(|tag| tag.get(1).map(String::as_str))
+ .collect::<Vec<_>>();
+ if observed.event.kind != UNRELATED_ADDRESSABLE_KIND
+ || d_tags.len() != 1
+ || d_tags[0].is_empty()
+ {
+ return Err(format!(
+ "{} unrelated addressable input must be a kind-{UNRELATED_ADDRESSABLE_KIND} event with one non-empty d tag",
+ case.id
+ ));
+ }
+ }
+ ProjectionInputRole::Causal if observed.event.kind != 5 => {
+ return Err(format!(
+ "{} causal input must be a kind-5 deletion request",
+ case.id
+ ));
+ }
+ ProjectionInputRole::Causal => {}
+ }
+ let event_seq = i64::try_from(index + 1)
+ .map_err(|_| format!("{} event sequence exceeds i64", case.id))?;
+ events_by_id.insert(observed.event.id.as_str(), (event_seq, observed));
+ }
+ if food_event_ids.is_empty() {
+ return Err(format!("{} has no scoped FoodAvailability input", case.id));
+ }
+ let coordinate_address = format!(
+ "{}:{}:{}",
+ case.expected.coordinate.kind,
+ case.expected.coordinate.pubkey,
+ case.expected.coordinate.d_tag
+ );
+ for observed in case
+ .events
+ .iter()
+ .filter(|observed| observed.role == ProjectionInputRole::Causal)
+ {
+ let references_scope = observed.event.tags.iter().any(|tag| {
+ (tag.first().map(String::as_str) == Some("a")
+ && tag.get(1).map(String::as_str) == Some(coordinate_address.as_str()))
+ || (tag.first().map(String::as_str) == Some("e")
+ && tag.get(1).is_some_and(|event_id| {
+ scoped_coordinate_event_ids.contains(event_id.as_str())
+ }))
+ });
+ if !references_scope {
+ return Err(format!(
+ "{} causal input does not reference the scoped coordinate or event",
+ case.id
+ ));
+ }
+ }
+
+ let projection_id = case
+ .expected
+ .projection
+ .0
+ .as_ref()
+ .map(|projection| projection.event_id.as_str());
+ if projection_id.is_some_and(|id| !food_event_ids.contains(id)) {
+ return Err(format!(
+ "{} projection must reference a scoped FoodAvailability input event",
+ case.id
+ ));
+ }
+ if let Some(projection) = case.expected.projection.0.as_ref() {
+ for image in &projection.images {
+ if let Some(digest) = image.blossom_sha256.0.as_deref() {
+ validate_sha256(&format!("{} Blossom image digest", case.id), digest)?;
+ }
+ }
+ }
+ for search in &case.expected.searches {
+ if search.query.trim().is_empty() {
+ return Err(format!("{} search query must not be empty", case.id));
+ }
+ validate_unique(
+ &format!("{} search event id", case.id),
+ search.event_ids.iter().map(String::as_str),
+ )?;
+ for event_id in &search.event_ids {
+ validate_hex(&format!("{} search event id", case.id), event_id, 64)?;
+ if Some(event_id.as_str()) != projection_id {
+ return Err(format!(
+ "{} search result must equal the current projection",
+ case.id
+ ));
+ }
+ }
+ }
+
+ let expected_high_water = i64::try_from(case.events.len())
+ .map_err(|_| format!("{} input event count exceeds i64", case.id))?;
+ if transition_page.source_high_water != expected_high_water
+ || transition_page.has_more
+ || transition_page.next_cursor.source_generation != SOURCE_GENERATION_ACTIVE_SENTINEL
+ || transition_page.next_cursor.feed_version != ADDRESSABLE_FEED_VERSION
+ || transition_page.next_cursor.scope_fingerprint != SCOPE_FINGERPRINT_SHA256
+ || transition_page.next_cursor.last_transition_seq != expected_high_water
+ {
+ return Err(format!(
+ "{} transition page does not seal the complete active-source interval",
+ case.id
+ ));
+ }
+ let expected_transition_sequences = case
+ .events
+ .iter()
+ .enumerate()
+ .filter(|(_, observed)| observed.role != ProjectionInputRole::UnrelatedAddressable)
+ .map(|(index, _)| {
+ i64::try_from(index + 1)
+ .map_err(|_| format!("{} transition sequence exceeds i64", case.id))
+ })
+ .collect::<Result<BTreeSet<_>, _>>()?;
+ let actual_transition_sequences = transition_page
+ .transitions
+ .iter()
+ .map(|transition| transition.transition_seq)
+ .collect::<BTreeSet<_>>();
+ if actual_transition_sequences.len() != transition_page.transitions.len()
+ || actual_transition_sequences != expected_transition_sequences
+ {
+ return Err(format!(
+ "{} scoped transition sequences do not exactly skip unrelated addressable traffic",
+ case.id
+ ));
+ }
+
+ let mut prior_visible: Option<&ExpectedEventReference> = None;
+ let mut last_cause_event_seq = 0_i64;
+ let mut last_transition_seq = 0_i64;
+ for transition in &transition_page.transitions {
+ if transition.transition_seq <= last_transition_seq
+ || transition.transition_seq > expected_high_water
+ || transition.source_generation != SOURCE_GENERATION_ACTIVE_SENTINEL
+ || transition.origin != "incremental"
+ || transition.coordinate != case.expected.coordinate
+ {
+ return Err(format!(
+ "{} transition {} has an invalid authority witness",
+ case.id, transition.transition_seq
+ ));
+ }
+ last_transition_seq = transition.transition_seq;
+ let raw_head = validate_vector_event_reference(
+ &case.id,
+ "raw head",
+ &transition.raw_head,
+ &events_by_id,
+ )?;
+ if !matches!(
+ raw_head.role,
+ ProjectionInputRole::ScopedFood | ProjectionInputRole::ScopedNonFood
+ ) || transition.raw_head_created_at != raw_head.event.created_at
+ || transition.admission_status != raw_head.expected_ingest.admission_status
+ || transition.admission_code.0 != raw_head.expected_ingest.admission_code.0
+ || transition.contract_id.0 != raw_head.expected_ingest.contract_id.0
+ {
+ return Err(format!(
+ "{} transition {} raw-head metadata is not canonical",
+ case.id, transition.transition_seq
+ ));
+ }
+ let cause = transition.cause_event.0.as_ref().ok_or_else(|| {
+ format!(
+ "{} incremental transition {} must authenticate its cause",
+ case.id, transition.transition_seq
+ )
+ })?;
+ let cause_source = validate_vector_event_reference(
+ &case.id,
+ "transition cause",
+ &cause.event,
+ &events_by_id,
+ )?;
+ if cause.event.event_seq != transition.transition_seq
+ || cause.event.event_seq <= last_cause_event_seq
+ || cause.pubkey != cause_source.event.pubkey
+ || cause.created_at != cause_source.event.created_at
+ || cause.kind != cause_source.event.kind
+ || cause.admission_status != cause_source.expected_ingest.admission_status
+ || cause.admission_code.0 != cause_source.expected_ingest.admission_code.0
+ || cause.contract_id.0 != cause_source.expected_ingest.contract_id.0
+ || transition.raw_head_decision != cause_source.expected_ingest.raw_head_decision
+ {
+ return Err(format!(
+ "{} transition {} cause metadata is not canonical",
+ case.id, transition.transition_seq
+ ));
+ }
+ last_cause_event_seq = cause.event.event_seq;
+
+ validate_transition_decision_shape(&case.id, transition)?;
+ if let Some(evidence) = transition.suppression.0.as_ref() {
+ validate_suppression_evidence(
+ &case.id,
+ evidence,
+ &events_by_id,
+ raw_head.event.kind,
+ raw_head.event.created_at,
+ )?;
+ }
+ if let Some(canonical) = transition.canonical_visible_event.0.as_ref() {
+ let source = validate_vector_event_reference(
+ &case.id,
+ "canonical visible event",
+ &canonical.event,
+ &events_by_id,
+ )?;
+ if canonical.event != transition.raw_head
+ || canonical.admission_status != transition.admission_status
+ || canonical.contract_id.0 != transition.contract_id.0
+ || canonical.event_class != source.expected_ingest.event_class
+ || canonical.valid_stream_eligible
+ != source.expected_ingest.valid_stream_eligible
+ {
+ return Err(format!(
+ "{} transition {} canonical event metadata is invalid",
+ case.id, transition.transition_seq
+ ));
+ }
+ validate_sha256(
+ &format!("{} canonical raw JSON digest", case.id),
+ &canonical.raw_json_sha256,
+ )?;
+ let raw_json = serde_json::to_vec(&source.event)
+ .map_err(|error| format!("serialize {} signed event: {error}", case.id))?;
+ if sha256_hex(&raw_json) != canonical.raw_json_sha256 {
+ return Err(format!(
+ "{} transition {} canonical raw JSON digest is stale",
+ case.id, transition.transition_seq
+ ));
+ }
+ }
+ if let Some(retracted) = transition.retracted_event.0.as_ref() {
+ validate_vector_event_reference(
+ &case.id,
+ "retracted event",
+ retracted,
+ &events_by_id,
+ )?;
+ if retracted.event_seq >= transition.transition_seq {
+ return Err(format!(
+ "{} transition {} retracts a non-prior event",
+ case.id, transition.transition_seq
+ ));
+ }
+ }
+ let next_visible = transition
+ .canonical_visible_event
+ .0
+ .as_ref()
+ .map(|event| &event.event);
+ let expected_retracted = (prior_visible != next_visible)
+ .then_some(prior_visible)
+ .flatten();
+ if transition.retracted_event.0.as_ref() != expected_retracted {
+ return Err(format!(
+ "{} transition {} has invalid retraction lineage",
+ case.id, transition.transition_seq
+ ));
+ }
+ prior_visible = next_visible;
+ }
+
+ validate_unique(
+ &format!("{} visibility event id", case.id),
+ case.expected
+ .event_visibility
+ .iter()
+ .map(|visibility| visibility.event.event_id.as_str()),
+ )?;
+ if case.expected.event_visibility.len() != event_ids.len()
+ || case
+ .expected
+ .event_visibility
+ .iter()
+ .any(|visibility| !event_ids.contains(visibility.event.event_id.as_str()))
+ {
+ return Err(format!(
+ "{} visibility expectations must cover every input event exactly once",
+ case.id
+ ));
+ }
+ let current_scoped_raw_head = transition_page
+ .transitions
+ .last()
+ .expect("non-empty transition page checked above")
+ .raw_head
+ .event_id
+ .as_str();
+ for visibility in &case.expected.event_visibility {
+ let source = validate_vector_event_reference(
+ &case.id,
+ "current visibility event",
+ &visibility.event,
+ &events_by_id,
+ )?;
+ let expected_raw_head = match source.role {
+ ProjectionInputRole::Causal => None,
+ ProjectionInputRole::ScopedFood | ProjectionInputRole::ScopedNonFood => {
+ Some(current_scoped_raw_head)
+ }
+ ProjectionInputRole::UnrelatedAddressable => Some(source.event.id.as_str()),
+ };
+ let expected_is_raw_head =
+ expected_raw_head.is_none_or(|event_id| event_id == source.event.id.as_str());
+ if visibility.source_generation != SOURCE_GENERATION_ACTIVE_SENTINEL
+ || visibility.admission_status != source.expected_ingest.admission_status
+ || visibility.is_raw_head != expected_is_raw_head
+ || visibility.raw_head_event_id.0.as_deref() != expected_raw_head
+ {
+ return Err(format!(
+ "{} current visibility witness for {} is not authoritative",
+ case.id, source.event.id
+ ));
+ }
+ if let Some(evidence) = visibility.suppression.0.as_ref() {
+ validate_suppression_evidence(
+ &case.id,
+ evidence,
+ &events_by_id,
+ source.event.kind,
+ source.event.created_at,
+ )?;
+ }
+ let coherent = match visibility.decision.as_str() {
+ "visible" => {
+ visibility.admission_status == "admitted"
+ && visibility.is_raw_head
+ && visibility
+ .suppression
+ .0
+ .as_ref()
+ .is_some_and(|evidence| evidence.outcome == "visible")
+ }
+ "not_current" => {
+ visibility.admission_status == "admitted"
+ && !visibility.is_raw_head
+ && visibility.suppression.0.is_some()
+ }
+ "suppressed" => {
+ visibility.admission_status == "admitted"
+ && visibility.is_raw_head
+ && visibility
+ .suppression
+ .0
+ .as_ref()
+ .is_some_and(|evidence| evidence.outcome == "suppressed")
+ }
+ "not_admitted" => {
+ visibility.admission_status != "admitted" && visibility.suppression.0.is_none()
+ }
+ _ => false,
+ };
+ if !coherent {
+ return Err(format!(
+ "{} current visibility decision for {} is incoherent",
+ case.id, source.event.id
+ ));
+ }
+ }
+
+ let expected_historical = transition_page
+ .transitions
+ .iter()
+ .filter_map(|transition| {
+ let visible = transition.canonical_visible_event.0.as_ref()?;
+ let final_visibility = case
+ .expected
+ .event_visibility
+ .iter()
+ .find(|visibility| visibility.event.event_id == visible.event.event_id)?;
+ (final_visibility.decision != "visible").then_some((
+ transition.transition_seq,
+ visible.event.event_id.as_str(),
+ final_visibility.decision.as_str(),
+ ))
+ })
+ .collect::<BTreeSet<_>>();
+ let actual_historical = case
+ .expected
+ .historical_visibility_witnesses
+ .iter()
+ .map(|witness| {
+ (
+ witness.transition_seq,
+ witness.event_id.as_str(),
+ witness.final_decision.as_str(),
+ )
+ })
+ .collect::<BTreeSet<_>>();
+ if actual_historical.len() != case.expected.historical_visibility_witnesses.len()
+ || actual_historical != expected_historical
+ {
+ return Err(format!(
+ "{} historical visibility witnesses do not cover every transition-time payload with a divergent final decision",
+ case.id
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_expected_ingest(case_id: &str, observed: &ObservedEvent) -> Result<(), String> {
+ let expected = &observed.expected_ingest;
+ let (event_class, admitted_contract, required_decision, invalid_allowed) = match observed.role {
+ ProjectionInputRole::ScopedFood => ("addressable", FOOD_CONTRACT_ID, None, true),
+ ProjectionInputRole::ScopedNonFood => (
+ "addressable",
+ OPERATIONAL_LISTING_CONTRACT_ID,
+ Some("applied"),
+ false,
+ ),
+ ProjectionInputRole::UnrelatedAddressable => (
+ "addressable",
+ FARM_PROFILE_CONTRACT_ID,
+ Some("applied"),
+ false,
+ ),
+ ProjectionInputRole::Causal => (
+ "regular",
+ DELETION_CONTRACT_ID,
+ Some("not_head_selected"),
+ false,
+ ),
+ };
+ let known_raw_head_decision = matches!(
+ expected.raw_head_decision.as_str(),
+ "applied"
+ | "not_head_selected"
+ | "skipped_older"
+ | "skipped_same_timestamp_higher_event_id"
+ | "malformed_coordinate"
+ );
+ let coherent_admission = match expected.admission_status.as_str() {
+ "admitted" => {
+ expected.admission_code.0.is_none()
+ && expected.contract_id.0.as_deref() == Some(admitted_contract)
+ && expected.valid_stream_eligible
+ }
+ "invalid" | "unsupported" => {
+ invalid_allowed
+ && expected.admission_code.0.is_some()
+ && expected.contract_id.0.is_none()
+ && !expected.valid_stream_eligible
+ }
+ _ => false,
+ };
+ if expected.event_class != event_class
+ || !known_raw_head_decision
+ || required_decision.is_some_and(|decision| expected.raw_head_decision != decision)
+ || !coherent_admission
+ {
+ return Err(format!(
+ "{case_id} input {} has an incoherent ingest witness",
+ observed.event.id
+ ));
+ }
+ Ok(())
+}
+
+fn validate_vector_event_reference<'a>(
+ case_id: &str,
+ label: &str,
+ reference: &ExpectedEventReference,
+ events: &BTreeMap<&'a str, (i64, &'a ObservedEvent)>,
+) -> Result<&'a ObservedEvent, String> {
+ validate_hex(
+ &format!("{case_id} {label} event id"),
+ &reference.event_id,
+ 64,
+ )?;
+ let (expected_sequence, observed) = events
+ .get(reference.event_id.as_str())
+ .copied()
+ .ok_or_else(|| format!("{case_id} {label} must reference an input event"))?;
+ if reference.event_seq != expected_sequence {
+ return Err(format!(
+ "{case_id} {label} sequence does not match input order"
+ ));
+ }
+ Ok(observed)
+}
+
+fn validate_transition_decision_shape(
+ case_id: &str,
+ transition: &ExpectedTransition,
+) -> Result<(), String> {
+ let coherent = match transition.admission_status.as_str() {
+ "admitted" => {
+ transition.admission_code.0.is_none()
+ && transition.contract_id.0.as_deref().is_some_and(|contract| {
+ contract == FOOD_CONTRACT_ID || contract == OPERATIONAL_LISTING_CONTRACT_ID
+ })
+ && transition.suppression.0.as_ref().is_some_and(|evidence| {
+ match transition.visibility.as_str() {
+ "visible" => {
+ evidence.outcome == "visible"
+ && transition.canonical_visible_event.0.is_some()
+ }
+ "suppressed" => {
+ evidence.outcome == "suppressed"
+ && transition.canonical_visible_event.0.is_none()
+ }
+ _ => false,
+ }
+ })
+ }
+ "invalid" | "unsupported" => {
+ transition.admission_code.0.is_some()
+ && transition.contract_id.0.is_none()
+ && transition.visibility == "not_admitted"
+ && transition.suppression.0.is_none()
+ && transition.canonical_visible_event.0.is_none()
+ }
+ _ => false,
+ };
+ if !coherent {
+ return Err(format!(
+ "{case_id} transition {} has an incoherent admission/visibility witness",
+ transition.transition_seq
+ ));
+ }
+ Ok(())
+}
+
+fn validate_suppression_evidence(
+ case_id: &str,
+ evidence: &ExpectedSuppressionEvidence,
+ events: &BTreeMap<&str, (i64, &ObservedEvent)>,
+ target_kind: u32,
+ target_created_at: u64,
+) -> Result<(), String> {
+ for (label, request_id) in [
+ (
+ "event-reference request",
+ evidence.event_reference_request_id.0.as_deref(),
+ ),
+ (
+ "address-reference request",
+ evidence.address_reference_request_id.0.as_deref(),
+ ),
+ ] {
+ if let Some(request_id) = request_id {
+ validate_hex(&format!("{case_id} {label}"), request_id, 64)?;
+ if events
+ .get(request_id)
+ .is_none_or(|(_, event)| event.role != ProjectionInputRole::Causal)
+ {
+ return Err(format!(
+ "{case_id} {label} must reference a causal input event"
+ ));
+ }
+ }
+ }
+ let event_reference = evidence.event_reference_request_id.0.is_some();
+ let address_reference = evidence.address_reference_request_id.0.is_some();
+ let cutoff = evidence.address_reference_cutoff.0;
+ let coherent = match evidence.reason.as_str() {
+ "deletion_request_immune" => {
+ target_kind == 5
+ && evidence.outcome == "visible"
+ && !event_reference
+ && !address_reference
+ && cutoff.is_none()
+ }
+ "deletion_no_authorized_reference" | "deletion_request_author_mismatch" => {
+ target_kind != 5
+ && evidence.outcome == "visible"
+ && !event_reference
+ && !address_reference
+ && cutoff.is_none()
+ }
+ "deletion_address_cutoff_precedes_target" => {
+ target_kind != 5
+ && evidence.outcome == "visible"
+ && !event_reference
+ && address_reference
+ && cutoff.is_some_and(|cutoff| cutoff < target_created_at)
+ }
+ "deletion_event_id_reference" => {
+ target_kind != 5
+ && evidence.outcome == "suppressed"
+ && event_reference
+ && cutoff.is_none_or(|cutoff| cutoff < target_created_at)
+ && (address_reference == cutoff.is_some())
+ }
+ "deletion_address_reference" => {
+ target_kind != 5
+ && evidence.outcome == "suppressed"
+ && !event_reference
+ && address_reference
+ && cutoff.is_some_and(|cutoff| cutoff >= target_created_at)
+ }
+ "deletion_event_id_and_address_reference" => {
+ target_kind != 5
+ && evidence.outcome == "suppressed"
+ && event_reference
+ && address_reference
+ && cutoff.is_some_and(|cutoff| cutoff >= target_created_at)
+ }
+ _ => false,
+ };
+ if !coherent {
+ return Err(format!("{case_id} suppression evidence is incoherent"));
+ }
+ Ok(())
+}
+
+fn validate_manifest_shape(manifest: &FoodAvailabilityProjectionManifest) -> Result<(), String> {
+ let expected_public_api = PUBLIC_API
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<Vec<_>>();
+ if manifest.schema_version != SCHEMA_VERSION
+ || manifest.contract_id != CONTRACT_ID
+ || manifest.hook_id != HOOK_ID
+ || manifest.predecessor.hook_id != PREDECESSOR_HOOK_ID
+ || manifest.predecessor.manifest.sha256 != PREDECESSOR_MANIFEST_SHA256
+ || manifest.migration.version != MIGRATION_VERSION
+ || manifest.migration.name != MIGRATION_NAME
+ || manifest.migration.schema_sha256 != SCHEMA_SHA256
+ || manifest.profile.event_contract_registry_version != EVENT_CONTRACT_REGISTRY_VERSION
+ || manifest.profile.addressable_feed_version != ADDRESSABLE_FEED_VERSION
+ || manifest.profile.projection_version != PROJECTION_VERSION
+ || manifest.profile.scope_kinds != [FOOD_AVAILABILITY_KIND]
+ || manifest.profile.scope_fingerprint_sha256 != SCOPE_FINGERPRINT_SHA256
+ || manifest.profile.food_contract_id != FOOD_CONTRACT_ID
+ || manifest.profile.admission_authority != ADMISSION_AUTHORITY
+ || manifest.profile.current_visibility_authority != CURRENT_VISIBILITY_AUTHORITY
+ || manifest.profile.post_core_capability != POST_CORE_CAPABILITY
+ || manifest.public_api != expected_public_api
+ || manifest.result_vector.executor_id != RESULT_VECTOR_EXECUTOR_ID
+ {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} does not describe the FoodAvailability projection-v1 successor"
+ ));
+ }
+ validate_catalog(&manifest.migration.catalog)?;
+ validate_unique(
+ "source role",
+ manifest
+ .source_files
+ .iter()
+ .map(|source| source.role.as_str()),
+ )?;
+ validate_unique(
+ "source path",
+ manifest
+ .source_files
+ .iter()
+ .map(|source| source.path.as_str()),
+ )?;
+ validate_unique(
+ "entry-point role",
+ manifest
+ .entry_points
+ .iter()
+ .map(|entry| entry.role.as_str()),
+ )?;
+ validate_unique("public API", manifest.public_api.iter().map(String::as_str))?;
+ Ok(())
+}
+
+fn generated_descriptor(
+ manifest: &FoodAvailabilityProjectionManifest,
+ manifest_bytes: &[u8],
+ manifest_sha256: &str,
+) -> String {
+ let manifest_json = std::str::from_utf8(manifest_bytes)
+ .expect("canonical JSON serialization always produces UTF-8");
+ let manifest_literal = format!("{manifest_json:?}");
+ format!(
+ "// @generated by `cargo xtask contract food-availability-projection-manifest --write`; do not edit.\n\
+#![allow(dead_code)]\n\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MANIFEST_JSON: &str = {manifest_literal};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MANIFEST_BYTE_LENGTH: usize = {};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256: &str =\n \"{manifest_sha256}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MANIFEST_SCHEMA_VERSION: u32 = {};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_CONTRACT_ID: &str =\n \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_HOOK_ID: &str = \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION: u32 = {};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_NAME: &str = \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_VERSION: u32 = {};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_FEED_VERSION: u32 = {};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION: u32 = {};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_BYTE_LENGTH: usize = {};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_SHA256: &str =\n \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_BYTE_LENGTH: usize = {};\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_SHA256: &str =\n \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_SCHEMA_SHA256: &str =\n \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_SCOPE_KINDS: &[u32] = &[30_402];\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_SCOPE_FINGERPRINT_SHA256: &str =\n \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_PREDECESSOR_MANIFEST_SHA256: &str =\n \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_SHA256: &str =\n \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_ID: &str =\n \"{}\";\n\
+pub(crate) const FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_SHA256: &str =\n \"{}\";\n",
+ manifest_bytes.len(),
+ manifest.schema_version,
+ manifest.contract_id,
+ manifest.hook_id,
+ manifest.migration.version,
+ manifest.migration.name,
+ manifest.profile.projection_version,
+ manifest.profile.addressable_feed_version,
+ manifest.profile.event_contract_registry_version,
+ manifest.migration.up.byte_length,
+ manifest.migration.up.sha256,
+ manifest.migration.down.byte_length,
+ manifest.migration.down.sha256,
+ manifest.migration.schema_sha256,
+ manifest.profile.scope_fingerprint_sha256,
+ manifest.predecessor.manifest.sha256,
+ manifest.result_vector.sha256,
+ manifest.result_vector.executor_id,
+ manifest.result_vector.executor_sha256,
+ )
+}
+
+fn manifest_schema() -> Value {
+ let hash = json!({"type": "string", "pattern": "^[0-9a-f]{64}$"});
+ let path = json!({
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$"
+ });
+ let file = json!({
+ "type": "object",
+ "required": ["path", "byte_length", "sha256", "hash_algorithm"],
+ "properties": {
+ "path": path,
+ "byte_length": {"type": "integer", "minimum": 1},
+ "sha256": hash,
+ "hash_algorithm": {"const": HASH_ALGORITHM}
+ },
+ "additionalProperties": false
+ });
+ let string_array = json!({
+ "type": "array",
+ "items": {"type": "string", "minLength": 1},
+ "uniqueItems": true
+ });
+ json!({
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://radroots.org/core/event-store/food-availability-projection-manifest-v1.schema.json",
+ "title": "Radroots event-store FoodAvailability projection manifest v1",
+ "type": "object",
+ "required": [
+ "schema_version", "contract_id", "hook_id", "manifest_schema", "predecessor",
+ "migration", "profile", "registry_inventory", "food_profile_vector",
+ "entry_points", "source_files", "public_api", "result_vector"
+ ],
+ "properties": {
+ "schema_version": {"const": SCHEMA_VERSION},
+ "contract_id": {"const": CONTRACT_ID},
+ "hook_id": {"const": HOOK_ID},
+ "manifest_schema": {"$ref": "#/$defs/file"},
+ "predecessor": {
+ "type": "object",
+ "required": ["hook_id", "manifest"],
+ "properties": {
+ "hook_id": {"const": PREDECESSOR_HOOK_ID},
+ "manifest": {"$ref": "#/$defs/file"}
+ },
+ "additionalProperties": false
+ },
+ "migration": {
+ "type": "object",
+ "required": ["version", "name", "up", "down", "schema_sha256", "catalog"],
+ "properties": {
+ "version": {"const": MIGRATION_VERSION},
+ "name": {"const": MIGRATION_NAME},
+ "up": {"$ref": "#/$defs/file"},
+ "down": {"$ref": "#/$defs/file"},
+ "schema_sha256": {"const": SCHEMA_SHA256},
+ "catalog": {
+ "type": "object",
+ "required": ["objects", "tables", "fts5_tables"],
+ "properties": {
+ "objects": string_array,
+ "tables": string_array,
+ "fts5_tables": string_array
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "profile": {
+ "type": "object",
+ "required": [
+ "event_contract_registry_version", "addressable_feed_version",
+ "projection_version", "scope_kinds", "scope_fingerprint_sha256",
+ "food_contract_id", "admission_authority", "current_visibility_authority",
+ "post_core_capability"
+ ],
+ "properties": {
+ "event_contract_registry_version": {"const": EVENT_CONTRACT_REGISTRY_VERSION},
+ "addressable_feed_version": {"const": ADDRESSABLE_FEED_VERSION},
+ "projection_version": {"const": PROJECTION_VERSION},
+ "scope_kinds": {
+ "type": "array", "prefixItems": [{"const": FOOD_AVAILABILITY_KIND}],
+ "minItems": 1, "maxItems": 1
+ },
+ "scope_fingerprint_sha256": {"const": SCOPE_FINGERPRINT_SHA256},
+ "food_contract_id": {"const": FOOD_CONTRACT_ID},
+ "admission_authority": {"const": ADMISSION_AUTHORITY},
+ "current_visibility_authority": {"const": CURRENT_VISIBILITY_AUTHORITY},
+ "post_core_capability": {"const": POST_CORE_CAPABILITY}
+ },
+ "additionalProperties": false
+ },
+ "registry_inventory": {"$ref": "#/$defs/file"},
+ "food_profile_vector": {"$ref": "#/$defs/file"},
+ "entry_points": {
+ "type": "array", "minItems": 1,
+ "items": {
+ "type": "object", "required": ["role", "rust_path"],
+ "properties": {
+ "role": {"type": "string", "minLength": 1},
+ "rust_path": {"type": "string", "minLength": 1}
+ },
+ "additionalProperties": false
+ }
+ },
+ "source_files": {
+ "type": "array", "minItems": 1,
+ "items": {"$ref": "#/$defs/source_file"}
+ },
+ "public_api": string_array,
+ "result_vector": {
+ "type": "object",
+ "required": [
+ "canonical_path", "mirror_path", "byte_length", "sha256", "hash_algorithm",
+ "executor_id", "executor_path", "executor_test", "executor_byte_length",
+ "executor_sha256", "executor_hash_algorithm"
+ ],
+ "properties": {
+ "canonical_path": {"const": RESULT_VECTOR_CANONICAL_RELATIVE},
+ "mirror_path": {"const": RESULT_VECTOR_MIRROR_RELATIVE},
+ "byte_length": {"type": "integer", "minimum": 1},
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "hash_algorithm": {"const": HASH_ALGORITHM},
+ "executor_id": {"const": RESULT_VECTOR_EXECUTOR_ID},
+ "executor_path": {"const": RESULT_VECTOR_EXECUTOR_RELATIVE},
+ "executor_test": {"const": RESULT_VECTOR_EXECUTOR_TEST},
+ "executor_byte_length": {"type": "integer", "minimum": 1},
+ "executor_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "executor_hash_algorithm": {"const": HASH_ALGORITHM}
+ },
+ "additionalProperties": false
+ }
+ },
+ "$defs": {
+ "file": file,
+ "source_file": {
+ "type": "object",
+ "required": ["role", "path", "byte_length", "sha256", "hash_algorithm"],
+ "properties": {
+ "role": {"type": "string", "minLength": 1},
+ "path": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$"
+ },
+ "byte_length": {"type": "integer", "minimum": 1},
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "hash_algorithm": {"const": HASH_ALGORITHM}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ })
+}
+
+fn validate_manifest_json_schema(schema: &Value, manifest: &Value) -> Result<(), String> {
+ jsonschema::draft202012::meta::validate(schema).map_err(|error| {
+ format!(
+ "{MANIFEST_SCHEMA_RELATIVE} is not a valid JSON Schema Draft 2020-12 document: {error}"
+ )
+ })?;
+ let validator = jsonschema::draft202012::options()
+ .build(schema)
+ .map_err(|error| {
+ format!("compile {MANIFEST_SCHEMA_RELATIVE} as JSON Schema Draft 2020-12: {error}")
+ })?;
+ validator.validate(manifest).map_err(|error| {
+ format!(
+ "{MANIFEST_RELATIVE} violates {MANIFEST_SCHEMA_RELATIVE} at {}: {error}",
+ error.instance_path()
+ )
+ })
+}
+
+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,
+ actual: &[u8],
+ value: &T,
+) -> Result<(), String> {
+ if actual.starts_with(&[0xef, 0xbb, 0xbf]) {
+ return Err(format!("{relative} must not contain a UTF-8 BOM"));
+ }
+ if actual.contains(&b'\r') {
+ return Err(format!("{relative} must use LF line endings"));
+ }
+ if actual != canonical_json_bytes(value)? {
+ return Err(format!(
+ "{relative} must use canonical two-space JSON formatting and end with exactly one LF"
+ ));
+ }
+ Ok(())
+}
+
+fn validate_unique<'a>(label: &str, values: impl Iterator<Item = &'a str>) -> Result<(), String> {
+ let mut seen = BTreeSet::new();
+ for value in values {
+ if value.is_empty() || !seen.insert(value) {
+ return Err(format!("empty or duplicate {label}: {value}"));
+ }
+ }
+ Ok(())
+}
+
+fn validate_digest_sidecar(relative: &str, bytes: &[u8]) -> Result<(), String> {
+ if bytes.len() != 65 || bytes[64] != b'\n' {
+ return Err(format!(
+ "{relative} must contain 64 lowercase hexadecimal bytes and one LF"
+ ));
+ }
+ let digest = std::str::from_utf8(&bytes[..64])
+ .map_err(|error| format!("{relative} must be UTF-8: {error}"))?;
+ validate_sha256(relative, digest)
+}
+
+fn validate_sha256(label: &str, value: &str) -> Result<(), String> {
+ validate_hex(label, value, 64)
+}
+
+fn validate_hex(label: &str, value: &str, expected_length: usize) -> Result<(), String> {
+ if value.len() != expected_length
+ || !value
+ .as_bytes()
+ .iter()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte))
+ {
+ return Err(format!(
+ "{label} must contain exactly {expected_length} lowercase hexadecimal bytes"
+ ));
+ }
+ Ok(())
+}
+
+fn sha256_hex(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
+}
+
+fn stale_error(relative: &str) -> String {
+ format!("{relative} is stale; run `{WRITE_COMMAND}`")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn repository_root() -> std::path::PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .and_then(Path::parent)
+ .expect("xtask manifest has a workspace root")
+ .to_path_buf()
+ }
+
+ #[test]
+ fn food_scope_fingerprint_is_pinned() {
+ let mut hasher = Sha256::new();
+ hasher.update(b"radroots.addressable-transition-scope.v1\0");
+ hasher.update(1_u32.to_be_bytes());
+ hasher.update(FOOD_AVAILABILITY_KIND.to_be_bytes());
+ assert_eq!(hex::encode(hasher.finalize()), SCOPE_FINGERPRINT_SHA256);
+ }
+
+ #[test]
+ fn blossom_dependency_authority_requires_the_direct_std_only_edge() {
+ let root = repository_root();
+ let workspace = parse_toml_value(&root, "Cargo.toml").expect("workspace manifest");
+ let mut event_store =
+ parse_toml_value(&root, "crates/event_store/Cargo.toml").expect("event-store manifest");
+ validate_blossom_dependency_values(&workspace, &event_store)
+ .expect("governed Blossom dependency");
+
+ event_store
+ .get_mut("dependencies")
+ .and_then(toml::Value::as_table_mut)
+ .and_then(|dependencies| dependencies.get_mut("radroots_blossom"))
+ .and_then(toml::Value::as_table_mut)
+ .expect("Blossom dependency table")
+ .insert("features".to_owned(), toml::Value::Array(Vec::new()));
+ let error = validate_blossom_dependency_values(&workspace, &event_store)
+ .expect_err("missing std feature must fail");
+ assert!(error.contains("governed std feature"), "{error}");
+ }
+
+ #[test]
+ fn migration_cursor_guards_are_bound_to_governed_rust_limits() {
+ let root = repository_root();
+ let addressable_model = read_regular_file(
+ &root,
+ "crates/event_store/src/model/addressable_transition_feed_v1.rs",
+ )
+ .expect("addressable model");
+ let addressable_model = std::str::from_utf8(&addressable_model).expect("UTF-8 model");
+ let food_model = read_regular_file(
+ &root,
+ "crates/event_store/src/model/food_availability_projection_v1.rs",
+ )
+ .expect("food model");
+ let food_model = std::str::from_utf8(&food_model).expect("UTF-8 model");
+ let migration =
+ read_regular_file(&root, MIGRATION_UP_RELATIVE).expect("projection migration");
+ let migration = std::str::from_utf8(&migration).expect("UTF-8 migration");
+ validate_migration_guard_limit_sources(addressable_model, food_model, migration)
+ .expect("governed cursor guards");
+
+ let stale_scan = migration.replacen(
+ "NEW.last_transition_seq - OLD.last_transition_seq > 1024",
+ "NEW.last_transition_seq - OLD.last_transition_seq > 1023",
+ 1,
+ );
+ assert_ne!(stale_scan, migration, "scan mutation must apply");
+ let error =
+ validate_migration_guard_limit_sources(addressable_model, food_model, &stale_scan)
+ .expect_err("stale scan guard must fail");
+ assert!(error.contains("cursor scan delta"), "{error}");
+
+ let stale_rows = migration.replacen(
+ "abs(NEW.projected_row_count - OLD.projected_row_count) > 64",
+ "abs(NEW.projected_row_count - OLD.projected_row_count) > 63",
+ 1,
+ );
+ assert_ne!(stale_rows, migration, "row-count mutation must apply");
+ let error =
+ validate_migration_guard_limit_sources(addressable_model, food_model, &stale_rows)
+ .expect_err("stale row-count guard must fail");
+ assert!(error.contains("projected-row delta"), "{error}");
+
+ let detached_apply_limit = food_model.replacen(
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1;",
+ "63;",
+ 1,
+ );
+ assert_ne!(
+ detached_apply_limit, food_model,
+ "apply-limit mutation must apply"
+ );
+ let error = validate_migration_guard_limit_sources(
+ addressable_model,
+ &detached_apply_limit,
+ migration,
+ )
+ .expect_err("detached apply limit must fail");
+ assert!(error.contains("must alias"), "{error}");
+
+ let stale_visibility_index = migration.replacen(
+ "INDEXED BY radroots_event_store_nip09_address_target_visibility_lookup_idx",
+ "INDEXED BY radroots_event_store_nip09_address_target_lookup_idx",
+ 1,
+ );
+ assert_ne!(
+ stale_visibility_index, migration,
+ "visibility-index mutation must apply"
+ );
+ let error = validate_migration_guard_limit_sources(
+ addressable_model,
+ food_model,
+ &stale_visibility_index,
+ )
+ .expect_err("unforced visibility index must fail");
+ assert!(error.contains("visibility index"), "{error}");
+
+ let stale_visibility_order = migration.replacen(
+ "ORDER BY target.inclusive_cutoff DESC, target.request_event_id",
+ "ORDER BY target.inclusive_cutoff DESC, request.request_event_id",
+ 1,
+ );
+ assert_ne!(
+ stale_visibility_order, migration,
+ "visibility-order mutation must apply"
+ );
+ let error = validate_migration_guard_limit_sources(
+ addressable_model,
+ food_model,
+ &stale_visibility_order,
+ )
+ .expect_err("non-indexed visibility ordering must fail");
+ assert!(error.contains("visibility ordering"), "{error}");
+ }
+
+ #[test]
+ fn predecessor_fast_open_validation_remains_constant_cost() {
+ let root = repository_root();
+ let source = read_regular_file(&root, "crates/event_store/src/nip09/reconciliation_v1.rs")
+ .expect("predecessor source");
+ let source = std::str::from_utf8(&source).expect("UTF-8 predecessor source");
+ validate_fast_active_hook_source(source).expect("constant-cost fast-open validation");
+
+ let exhaustive = source.replacen(
+ "validate_structural_source_state_fast(connection)",
+ "validate_structural_source_state(connection)",
+ 1,
+ );
+ assert_ne!(exhaustive, source, "fast-open mutation must apply");
+ let error = validate_fast_active_hook_source(&exhaustive)
+ .expect_err("exhaustive open-time scan must fail");
+ assert!(error.contains("constant-cost"), "{error}");
+ }
+
+ #[test]
+ fn source_capacity_preflight_and_recheck_authority_is_structurally_sealed() {
+ const OUTER_PREFLIGHT: &str = r#" if has_pending_source_capacity_hook(&status, registry) {
+ let mut connection = pool.acquire().await?;
+ validate_event_store_temp_schema_with_registry(&mut connection, registry).await?;
+ validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;
+ }
+"#;
+ const OUTER_DECOY: &str = r#" if false {
+ if has_pending_source_capacity_hook(&status, registry) {
+ let mut connection = pool.acquire().await?;
+ validate_event_store_temp_schema_with_registry(&mut connection, registry).await?;
+ validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;
+ }
+ }
+"#;
+ const INNER_RECHECK_AND_APPLY: &str = r#" if matches!(
+ migration.hook,
+ EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ ) {
+ validate_reconciliation_capacity(connection, reconciliation_limits).await?;
+ }
+ apply_migration_up(connection, registry, migration).await?;
+"#;
+ const INNER_APPLY_THEN_RECHECK: &str = r#" apply_migration_up(connection, registry, migration).await?;
+ if matches!(
+ migration.hook,
+ EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ ) {
+ validate_reconciliation_capacity(connection, reconciliation_limits).await?;
+ }
+"#;
+ const OUTER_HOOK_SET: &str = r#"EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1"#;
+ const INNER_HOOK_SET: &str = r#"EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1"#;
+
+ let root = repository_root();
+ let source = read_regular_file(&root, "crates/event_store/src/schema.rs")
+ .expect("event-store schema source");
+ let source = std::str::from_utf8(&source).expect("UTF-8 schema source");
+ validate_source_capacity_authority(source).expect("governed source-capacity authority");
+
+ let outer_before_begin = format!(
+ "{OUTER_PREFLIGHT}\n let mut transaction = pool.begin_with(\"BEGIN IMMEDIATE\").await?;\n"
+ );
+ let outer_after_begin = format!(
+ " let mut transaction = pool.begin_with(\"BEGIN IMMEDIATE\").await?;\n{OUTER_PREFLIGHT}"
+ );
+ let mutations = [
+ (
+ "outer capacity removal",
+ source.replacen(
+ " validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;\n",
+ "",
+ 1,
+ ),
+ ),
+ (
+ "inner capacity removal",
+ source.replacen(
+ " validate_reconciliation_capacity(connection, reconciliation_limits).await?;\n",
+ "",
+ 1,
+ ),
+ ),
+ (
+ "unreachable outer decoy",
+ source.replacen(OUTER_PREFLIGHT, OUTER_DECOY, 1),
+ ),
+ (
+ "outer preflight after BEGIN IMMEDIATE",
+ source.replacen(&outer_before_begin, &outer_after_begin, 1),
+ ),
+ (
+ "inner recheck after migration DDL",
+ source.replacen(
+ INNER_RECHECK_AND_APPLY,
+ INNER_APPLY_THEN_RECHECK,
+ 1,
+ ),
+ ),
+ (
+ "outer selector covers one hook only",
+ source.replacen(
+ OUTER_HOOK_SET,
+ "EventStoreMigrationHook::FoodAvailabilityProjectionV1",
+ 1,
+ ),
+ ),
+ (
+ "inner recheck covers one hook only",
+ source.replacen(
+ INNER_HOOK_SET,
+ "EventStoreMigrationHook::Nip09ReconciliationV1",
+ 1,
+ ),
+ ),
+ ];
+
+ for (label, mutation) in mutations {
+ assert_ne!(
+ mutation, source,
+ "source-capacity mutation must apply: {label}"
+ );
+ let error = validate_source_capacity_authority(&mutation)
+ .expect_err("source-capacity mutation must fail");
+ assert!(error.contains("source-capacity"), "{label}: {error}");
+ }
+ }
+
+ #[test]
+ fn food_read_queries_require_the_exact_persisted_head_authority_join() {
+ let root = repository_root();
+ let source = read_regular_file(
+ &root,
+ "crates/event_store/src/store/food_availability_projection_v1.rs",
+ )
+ .expect("Food projection store source");
+ let source = std::str::from_utf8(&source).expect("UTF-8 Food projection store source");
+ validate_food_read_query_sources(source).expect("governed Food read queries");
+
+ for (predicate, replacement) in [
+ ("source.singleton = 1 AND ", ""),
+ (
+ "source.active_generation = projection.source_generation",
+ "source.active_generation != projection.source_generation",
+ ),
+ ("cursor.singleton = 1", "cursor.singleton = 2"),
+ (
+ "cursor.source_generation = projection.source_generation",
+ "cursor.source_generation != projection.source_generation",
+ ),
+ (
+ "head.source_generation = projection.source_generation",
+ "head.source_generation = source.active_generation",
+ ),
+ ("head.kind = 30402", "head.kind = 30403"),
+ (
+ "head.pubkey = projection.pubkey",
+ "head.pubkey != projection.pubkey",
+ ),
+ (
+ "head.d_tag = projection.d_tag",
+ "head.d_tag != projection.d_tag",
+ ),
+ (
+ "head.raw_head_event_id = projection.event_id",
+ "head.raw_head_event_id != projection.event_id",
+ ),
+ (
+ "head.raw_head_event_seq = projection.event_seq",
+ "head.raw_head_event_seq != projection.event_seq",
+ ),
+ (
+ "head.raw_head_created_at = projection.created_at",
+ "head.raw_head_created_at != projection.created_at",
+ ),
+ (
+ "head.admission_status = 'admitted'",
+ "head.admission_status != 'admitted'",
+ ),
+ (
+ "head.admission_code IS NULL",
+ "head.admission_code IS NOT NULL",
+ ),
+ (
+ "head.contract_id = projection.contract_id",
+ "head.contract_id != projection.contract_id",
+ ),
+ (
+ "head.visibility = 'visible'",
+ "head.visibility != 'visible'",
+ ),
+ (
+ "head.nip09_outcome = 'visible'",
+ "head.nip09_outcome != 'visible'",
+ ),
+ ] {
+ let mutated = source.replacen(predicate, replacement, 1);
+ assert_ne!(
+ mutated, source,
+ "predicate mutation must apply: {predicate}"
+ );
+ let error = validate_food_read_query_sources(&mutated)
+ .expect_err("weakened head-authority predicate must fail");
+ assert!(error.contains("fail-closed"), "{predicate}: {error}");
+ }
+
+ let unfenced_recent = source.replacen(
+ "FROM radroots_event_store_source_state AS source CROSS JOIN radroots_event_store_food_availability_read_v1 AS projection ON",
+ "FROM radroots_event_store_food_availability_read_v1 AS projection JOIN radroots_event_store_source_state AS source ON",
+ 1,
+ );
+ assert_ne!(
+ unfenced_recent, source,
+ "recent source-first fence mutation must apply"
+ );
+ let error = validate_food_read_query_sources(&unfenced_recent)
+ .expect_err("unfenced recent query must fail");
+ assert!(error.contains("fail-closed"), "{error}");
+
+ let rerouted = source.replacen(
+ "sqlx::query(FOOD_AVAILABILITY_POINT_QUERY_V1)",
+ "sqlx::query(FOOD_AVAILABILITY_RECENT_QUERY_V1)",
+ 1,
+ );
+ assert_ne!(rerouted, source, "query-route mutation must apply");
+ let error = validate_food_read_query_sources(&rerouted)
+ .expect_err("wrong governed query route must fail");
+ assert!(error.contains("exact governed query"), "{error}");
+
+ let dynamic = source.replacen(
+ "sqlx::query(FOOD_AVAILABILITY_POINT_QUERY_V1)",
+ "sqlx::query(query_text)",
+ 1,
+ );
+ assert_ne!(dynamic, source, "dynamic-route mutation must apply");
+ let error =
+ validate_food_read_query_sources(&dynamic).expect_err("arbitrary query path must fail");
+ assert!(error.contains("exact governed query"), "{error}");
+ }
+
+ #[test]
+ fn food_projection_audit_authority_is_exact_and_fail_closed() {
+ let root = repository_root();
+ let source = read_regular_file(
+ &root,
+ "crates/event_store/src/store/food_availability_projection_v1.rs",
+ )
+ .expect("Food projection store source");
+ let source = std::str::from_utf8(&source).expect("UTF-8 Food projection store source");
+ validate_food_projection_audit_authority(source)
+ .expect("governed exhaustive Food projection audit");
+
+ let route_mutations = [
+ (
+ "audit transaction downgrade",
+ source.replacen(
+ "self.begin_write_transaction().await?",
+ "self.pool.begin().await?",
+ 1,
+ ),
+ ),
+ (
+ "exhaustive audit visibility weakening",
+ source.replacen(
+ "pub(crate) async fn validate_food_availability_projection_hook_v1(",
+ "pub async fn validate_food_availability_projection_hook_v1(",
+ 1,
+ ),
+ ),
+ (
+ "exhaustive audit early success",
+ source.replacen(
+ "pub(crate) async fn validate_food_availability_projection_hook_v1(\n connection: &mut SqliteConnection,\n) -> Result<(), RadrootsEventStoreError> {\n let state =",
+ "pub(crate) async fn validate_food_availability_projection_hook_v1(\n connection: &mut SqliteConnection,\n) -> Result<(), RadrootsEventStoreError> {\n return Ok(());\n let state =",
+ 1,
+ ),
+ ),
+ (
+ "source-transition validation omission",
+ source.replacen(
+ " validate_projection_source_transition(connection, &projection).await?;\n",
+ "",
+ 1,
+ ),
+ ),
+ (
+ "source-transition validation reorder",
+ source.replacen(
+ " validate_projection_source_transition(connection, &projection).await?;\n validate_fts_row(connection, &projection).await?;",
+ " validate_fts_row(connection, &projection).await?;\n validate_projection_source_transition(connection, &projection).await?;",
+ 1,
+ ),
+ ),
+ (
+ "source-transition sequence rebind",
+ {
+ let (prefix, suffix) = source
+ .rsplit_once(".bind(projection.source_transition_seq())")
+ .expect("source-transition helper bind");
+ format!("{prefix}.bind(projection.event_seq()){suffix}")
+ },
+ ),
+ (
+ "coordinate cardinality bypass",
+ source.replacen(
+ " let actual_row_count = i64::try_from(actual_coordinates.len())\n .map_err(|_| projection_drift(\"projection row count exceeds i64\"))?;",
+ " let actual_row_count = state.projected_row_count;",
+ 1,
+ ),
+ ),
+ (
+ "sealed row-count comparison inversion",
+ source.replacen(
+ "if actual_row_count != state.projected_row_count",
+ "if actual_row_count == state.projected_row_count",
+ 1,
+ ),
+ ),
+ (
+ "coordinate equality omission",
+ source.replacen(
+ " if actual_coordinates != expected_coordinates {\n return Err(projection_drift(\n \"projection coordinate witnesses do not equal the current admitted, visible FoodAvailability heads\",\n ));\n }\n",
+ "",
+ 1,
+ ),
+ ),
+ (
+ "coordinate overwrite before equality",
+ source.replacen(
+ " if actual_coordinates != expected_coordinates {",
+ " actual_coordinates = expected_coordinates.clone();\n if actual_coordinates != expected_coordinates {",
+ 1,
+ ),
+ ),
+ ];
+ for (label, mutation) in route_mutations {
+ assert_ne!(mutation, source, "audit-route mutation must apply: {label}");
+ let error = match validate_food_projection_audit_authority(&mutation) {
+ Ok(()) => panic!("audit-route mutation must fail: {label}"),
+ Err(error) => error,
+ };
+ assert!(!error.is_empty());
+ }
+
+ let expected_head_query = "SELECT pubkey, d_tag, raw_head_event_id, raw_head_event_seq, raw_head_created_at FROM radroots_event_store_addressable_head_state WHERE source_generation = ? AND kind = 30402 AND admission_status = 'admitted' AND admission_code IS NULL AND contract_id = ? AND visibility = 'visible' AND nip09_outcome = 'visible' ORDER BY pubkey, d_tag";
+ for (needle, replacement) in [
+ (", raw_head_created_at", ""),
+ ("source_generation = ?", "source_generation IS NOT NULL"),
+ ("kind = 30402", "kind = 30340"),
+ (
+ "admission_status = 'admitted'",
+ "admission_status != 'admitted'",
+ ),
+ ("admission_code IS NULL", "admission_code IS NOT NULL"),
+ ("contract_id = ?", "contract_id IS NOT NULL"),
+ ("visibility = 'visible'", "visibility != 'visible'"),
+ ("nip09_outcome = 'visible'", "nip09_outcome != 'visible'"),
+ ("ORDER BY pubkey, d_tag", "ORDER BY d_tag, pubkey"),
+ ] {
+ let mutated_query = expected_head_query.replacen(needle, replacement, 1);
+ assert_ne!(mutated_query, expected_head_query);
+ let mutation = source.replacen(expected_head_query, &mutated_query, 1);
+ assert_ne!(
+ mutation, source,
+ "expected-head mutation must apply: {needle}"
+ );
+ let error = validate_food_projection_audit_authority(&mutation)
+ .expect_err("expected-head mutation must fail");
+ assert!(!error.is_empty());
+ }
+
+ let source_transition_query = "SELECT EXISTS(SELECT 1 FROM radroots_event_store_addressable_head_transition AS transition WHERE transition.transition_seq = ? AND transition.source_generation = ? AND transition.source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1) AND transition.kind = 30402 AND transition.pubkey = ? AND transition.d_tag = ? AND transition.raw_head_event_id = ? AND transition.raw_head_event_seq = ? AND transition.raw_head_created_at = ? AND transition.visible_event_id = ? AND transition.visible_event_seq = ? AND transition.admission_status = 'admitted' AND transition.admission_code IS NULL AND transition.contract_id = ? AND transition.visibility = 'visible' AND transition.nip09_outcome = 'visible' AND transition.raw_head_decision IN ('baseline_rebuild', 'applied') AND transition.transition_seq = (SELECT MAX(candidate.transition_seq) FROM radroots_event_store_addressable_head_transition AS candidate WHERE candidate.source_generation = transition.source_generation AND candidate.kind = transition.kind AND candidate.pubkey = transition.pubkey AND candidate.d_tag = transition.d_tag AND candidate.raw_head_decision IN ('baseline_rebuild', 'applied')))";
+ for (needle, replacement) in [
+ (
+ "transition.transition_seq = ?",
+ "transition.transition_seq > 0",
+ ),
+ (
+ "transition.source_generation = ?",
+ "transition.source_generation IS NOT NULL",
+ ),
+ (
+ "transition.source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1)",
+ "transition.source_generation IS NOT NULL",
+ ),
+ ("transition.kind = 30402", "transition.kind = 30340"),
+ ("transition.pubkey = ?", "transition.pubkey IS NOT NULL"),
+ ("transition.d_tag = ?", "transition.d_tag IS NOT NULL"),
+ (
+ "transition.raw_head_event_id = ?",
+ "transition.raw_head_event_id IS NOT NULL",
+ ),
+ (
+ "transition.raw_head_event_seq = ?",
+ "transition.raw_head_event_seq > 0",
+ ),
+ (
+ "transition.raw_head_created_at = ?",
+ "transition.raw_head_created_at > 0",
+ ),
+ (
+ "transition.visible_event_id = ?",
+ "transition.visible_event_id IS NOT NULL",
+ ),
+ (
+ "transition.visible_event_seq = ?",
+ "transition.visible_event_seq > 0",
+ ),
+ (
+ "transition.admission_status = 'admitted'",
+ "transition.admission_status != 'admitted'",
+ ),
+ (
+ "transition.admission_code IS NULL",
+ "transition.admission_code IS NOT NULL",
+ ),
+ (
+ "transition.contract_id = ?",
+ "transition.contract_id IS NOT NULL",
+ ),
+ (
+ "transition.visibility = 'visible'",
+ "transition.visibility != 'visible'",
+ ),
+ (
+ "transition.nip09_outcome = 'visible'",
+ "transition.nip09_outcome != 'visible'",
+ ),
+ (
+ "transition.raw_head_decision IN ('baseline_rebuild', 'applied')",
+ "transition.raw_head_decision = 'applied'",
+ ),
+ (
+ "transition.transition_seq = (SELECT MAX(candidate.transition_seq)",
+ "transition.transition_seq <= (SELECT MAX(candidate.transition_seq)",
+ ),
+ ] {
+ let mutated_query = source_transition_query.replacen(needle, replacement, 1);
+ assert_ne!(mutated_query, source_transition_query);
+ let mutation = source.replacen(source_transition_query, &mutated_query, 1);
+ assert_ne!(
+ mutation, source,
+ "source-transition mutation must apply: {needle}"
+ );
+ let error = validate_food_projection_audit_authority(&mutation)
+ .expect_err("source-transition mutation must fail");
+ assert!(!error.is_empty());
+ }
+ }
+
+ #[test]
+ fn public_api_is_exhaustive_and_structurally_reexported() {
+ let root = repository_root();
+ let model_bytes =
+ read_regular_file(&root, EVENT_STORE_MODEL_RELATIVE).expect("event-store model source");
+ let model_source = std::str::from_utf8(&model_bytes).expect("UTF-8 model source");
+ let lib_bytes =
+ read_regular_file(&root, EVENT_STORE_LIB_RELATIVE).expect("event-store lib source");
+ let lib_source = std::str::from_utf8(&lib_bytes).expect("UTF-8 lib source");
+ let advertised = PUBLIC_API
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<Vec<_>>();
+ validate_public_api_sources(model_source, lib_source, &advertised)
+ .expect("governed successor public API");
+
+ let mut omitted = advertised.clone();
+ omitted.retain(|name| name != "RadrootsAddressableTransitionCauseV1");
+ let error = validate_public_api_sources(model_source, lib_source, &omitted)
+ .expect_err("omitted manifest symbol must fail");
+ assert!(error.contains("PUBLIC_API is not exhaustive"), "{error}");
+
+ let removed = lib_source.replacen("RadrootsAddressableTransitionCauseV1,", "", 1);
+ assert_ne!(removed, lib_source, "removal mutation must apply");
+ let error = validate_public_api_sources(model_source, &removed, &advertised)
+ .expect_err("removed crate-root export must fail");
+ assert!(error.contains("does not re-export"), "{error}");
+
+ let renamed = lib_source.replacen(
+ "RadrootsAddressableTransitionCauseV1",
+ "RadrootsAddressableTransitionCauseV1 as RadrootsAddressableTransitionCauseRenamedV1",
+ 1,
+ );
+ assert_ne!(renamed, lib_source, "rename mutation must apply");
+ let error = validate_public_api_sources(model_source, &renamed, &advertised)
+ .expect_err("renamed crate-root export must fail");
+ assert!(error.contains("non-renamed"), "{error}");
+
+ let schema_bytes = canonical_json_bytes(&manifest_schema()).expect("schema bytes");
+ let mut manifest = describe_manifest(&root, &schema_bytes).expect("manifest");
+ manifest
+ .public_api
+ .retain(|name| name != "RadrootsAddressableTransitionCauseV1");
+ let error = validate_manifest_shape(&manifest)
+ .expect_err("generated manifest with an omitted public symbol must fail");
+ assert!(error.contains("successor"), "{error}");
+ }
+
+ #[test]
+ fn result_vector_is_strict_canonical_and_complete() {
+ let root = repository_root();
+ let bytes = read_regular_file(&root, RESULT_VECTOR_CANONICAL_RELATIVE).expect("vector");
+ let vector: ProjectionResultVector = serde_json::from_slice(&bytes).expect("strict vector");
+ validate_canonical_json(RESULT_VECTOR_CANONICAL_RELATIVE, &bytes, &vector)
+ .expect("canonical vector");
+ validate_result_vector(&vector).expect("complete vector");
+
+ let mut value: Value = serde_json::from_slice(&bytes).expect("vector value");
+ value["cases"][2]["expected"]["projection"]
+ .as_object_mut()
+ .expect("projection")
+ .remove("quantity_amount");
+ let missing = serde_json::from_value::<ProjectionResultVector>(value)
+ .expect_err("required nullable field must be present");
+ assert!(missing.to_string().contains("quantity_amount"));
+
+ let mut invalid_sequence: Value =
+ serde_json::from_slice(&bytes).expect("vector value for sequence mutation");
+ invalid_sequence["cases"][0]["expected"]["transition_page"]["transitions"][0]["transition_seq"] =
+ json!(2);
+ let invalid_sequence: ProjectionResultVector =
+ serde_json::from_value(invalid_sequence).expect("typed sequence mutation");
+ let error = validate_result_vector(&invalid_sequence)
+ .expect_err("incorrect scoped transition sequence must fail");
+ assert!(error.contains("exactly skip unrelated"), "{error}");
+
+ let mut invalid_generation: Value =
+ serde_json::from_slice(&bytes).expect("vector value for generation mutation");
+ invalid_generation["cases"][0]["expected"]["transition_page"]["transitions"][0]["source_generation"] =
+ json!("fixture-generation");
+ let invalid_generation: ProjectionResultVector =
+ serde_json::from_value(invalid_generation).expect("typed generation mutation");
+ let error = validate_result_vector(&invalid_generation)
+ .expect_err("non-authoritative source-generation sentinel must fail");
+ assert!(error.contains("invalid authority witness"), "{error}");
+
+ let mut missing_cause: Value =
+ serde_json::from_slice(&bytes).expect("vector value for cause mutation");
+ missing_cause["cases"][0]["expected"]["transition_page"]["transitions"][0]["cause_event"] =
+ Value::Null;
+ let missing_cause: ProjectionResultVector =
+ serde_json::from_value(missing_cause).expect("typed nullable cause mutation");
+ let error = validate_result_vector(&missing_cause)
+ .expect_err("incremental transition without a cause must fail");
+ assert!(error.contains("must authenticate its cause"), "{error}");
+
+ let mut stale_canonical_digest: Value =
+ serde_json::from_slice(&bytes).expect("vector value for digest mutation");
+ stale_canonical_digest["cases"][0]["expected"]["transition_page"]["transitions"][0]["canonical_visible_event"]
+ ["raw_json_sha256"] = json!("00".repeat(32));
+ let stale_canonical_digest: ProjectionResultVector =
+ serde_json::from_value(stale_canonical_digest).expect("typed digest mutation");
+ let error = validate_result_vector(&stale_canonical_digest)
+ .expect_err("stale canonical raw JSON digest must fail");
+ assert!(
+ error.contains("canonical raw JSON digest is stale"),
+ "{error}"
+ );
+
+ let mut mislabeled_operational: Value =
+ serde_json::from_slice(&bytes).expect("vector value for Operational role mutation");
+ mislabeled_operational["cases"][6]["events"][1]["role"] = json!("scoped_food");
+ let mislabeled_operational: ProjectionResultVector =
+ serde_json::from_value(mislabeled_operational)
+ .expect("typed Operational role mutation");
+ let error = validate_result_vector(&mislabeled_operational)
+ .expect_err("Operational head mislabeled as Food must fail");
+ assert!(error.contains("incoherent ingest witness"), "{error}");
+
+ let mut missing_historical: Value =
+ serde_json::from_slice(&bytes).expect("vector value for historical witness mutation");
+ missing_historical["cases"][7]["expected"]["historical_visibility_witnesses"] = json!([]);
+ let missing_historical: ProjectionResultVector =
+ serde_json::from_value(missing_historical).expect("typed historical witness mutation");
+ let error = validate_result_vector(&missing_historical)
+ .expect_err("missing divergent historical payload witnesses must fail");
+ assert!(error.contains("historical visibility witnesses"), "{error}");
+
+ let mut mislabeled_unrelated: Value =
+ serde_json::from_slice(&bytes).expect("vector value for unrelated role mutation");
+ mislabeled_unrelated["cases"][8]["events"][1]["role"] = json!("scoped_food");
+ mislabeled_unrelated["cases"][8]["events"][1]["expected_ingest"]["contract_id"] =
+ json!(FOOD_CONTRACT_ID);
+ let mislabeled_unrelated: ProjectionResultVector =
+ serde_json::from_value(mislabeled_unrelated).expect("typed unrelated role mutation");
+ let error = validate_result_vector(&mislabeled_unrelated)
+ .expect_err("unrelated addressable input mislabeled as scoped must fail");
+ assert!(error.contains("scoped input does not match"), "{error}");
+
+ let mut leaked_unrelated_transition: Value =
+ serde_json::from_slice(&bytes).expect("vector value for feed-scope mutation");
+ let mut leaked =
+ leaked_unrelated_transition["cases"][8]["expected"]["transition_page"]["transitions"]
+ [0]
+ .clone();
+ leaked["transition_seq"] = json!(2);
+ leaked_unrelated_transition["cases"][8]["expected"]["transition_page"]["transitions"]
+ .as_array_mut()
+ .expect("transition array")
+ .insert(1, leaked);
+ let leaked_unrelated_transition: ProjectionResultVector =
+ serde_json::from_value(leaked_unrelated_transition).expect("typed feed-scope mutation");
+ let error = validate_result_vector(&leaked_unrelated_transition)
+ .expect_err("unrelated addressable traffic in the scoped feed must fail");
+ assert!(error.contains("exactly skip unrelated"), "{error}");
+
+ let mut stale_high_water: Value =
+ serde_json::from_slice(&bytes).expect("vector value for high-water mutation");
+ stale_high_water["cases"][8]["expected"]["transition_page"]["source_high_water"] = json!(2);
+ let stale_high_water: ProjectionResultVector =
+ serde_json::from_value(stale_high_water).expect("typed high-water mutation");
+ let error = validate_result_vector(&stale_high_water)
+ .expect_err("cursor high-water that omits unrelated traffic must fail");
+ assert!(error.contains("complete active-source interval"), "{error}");
+ }
+
+ #[test]
+ fn schema_rejects_unknown_manifest_fields() {
+ let root = repository_root();
+ let schema = manifest_schema();
+ let schema_bytes = canonical_json_bytes(&schema).expect("schema bytes");
+ let manifest = describe_manifest(&root, &schema_bytes).expect("manifest");
+ let mut value = serde_json::to_value(manifest).expect("manifest value");
+ value
+ .as_object_mut()
+ .expect("manifest object")
+ .insert("unknown".to_owned(), Value::Bool(true));
+ let error = validate_manifest_json_schema(&schema, &value)
+ .expect_err("unknown manifest field must fail");
+ assert!(error.contains("Additional properties"), "{error}");
+ }
+
+ #[test]
+ fn generated_descriptor_covers_runtime_pointer_constants() {
+ let root = repository_root();
+ let schema_bytes = canonical_json_bytes(&manifest_schema()).expect("schema bytes");
+ let manifest = describe_manifest(&root, &schema_bytes).expect("manifest");
+ let manifest_bytes = canonical_json_bytes(&manifest).expect("manifest bytes");
+ let digest = sha256_hex(&manifest_bytes);
+ let descriptor = generated_descriptor(&manifest, &manifest_bytes, &digest);
+ assert_eq!(manifest.migration.schema_sha256, SCHEMA_SHA256);
+ for name in [
+ "FOOD_AVAILABILITY_PROJECTION_MANIFEST_SCHEMA_VERSION",
+ "FOOD_AVAILABILITY_PROJECTION_CONTRACT_ID",
+ "FOOD_AVAILABILITY_PROJECTION_HOOK_ID",
+ "FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION",
+ "FOOD_AVAILABILITY_PROJECTION_MIGRATION_UP_SHA256",
+ "FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_SHA256",
+ "FOOD_AVAILABILITY_PROJECTION_SCOPE_KINDS",
+ "FOOD_AVAILABILITY_PROJECTION_SCOPE_FINGERPRINT_SHA256",
+ "FOOD_AVAILABILITY_PROJECTION_PREDECESSOR_MANIFEST_SHA256",
+ "FOOD_AVAILABILITY_PROJECTION_RESULT_VECTOR_EXECUTOR_SHA256",
+ ] {
+ assert!(descriptor.contains(name), "missing {name}");
+ }
+ for (name, value) in [
+ ("FOOD_AVAILABILITY_PROJECTION_HOOK_ID", manifest.hook_id),
+ (
+ "FOOD_AVAILABILITY_PROJECTION_MIGRATION_NAME",
+ manifest.migration.name,
+ ),
+ ] {
+ let expected = format!("pub(crate) const {name}: &str = {value:?};\n");
+ assert!(
+ descriptor.contains(&expected),
+ "{name} must use the rustfmt-stable one-line assignment"
+ );
+ }
+ syn::parse_file(&descriptor).expect("generated descriptor parses as Rust");
+ }
+}
diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs
@@ -1,3 +1,5 @@
+#![allow(dead_code)]
+
use super::artifact_bundle::{
GeneratedArtifact, read_regular_file, validate_workspace_path, with_artifact_bundle_transaction,
};
@@ -216,6 +218,83 @@ const GENERATED_DESCRIPTOR_RELATIVE: &str =
"crates/event_store/src/generated/nip09_reconciliation_manifest.rs";
const WRITE_COMMAND: &str = "cargo xtask contract nip09-reconciliation-manifest --write";
+const IMMUTABLE_MANIFEST_BYTES: &[u8] = include_bytes!(
+ "../../../../crates/event_store/contracts/nip09_reconciliation_v1.manifest.json"
+);
+const IMMUTABLE_MANIFEST_SCHEMA_BYTES: &[u8] = include_bytes!(
+ "../../../../crates/event_store/contracts/nip09_reconciliation_v1.manifest.schema.json"
+);
+const IMMUTABLE_MANIFEST_SHA256_BYTES: &[u8] = include_bytes!(
+ "../../../../crates/event_store/contracts/nip09_reconciliation_v1.manifest.sha256"
+);
+const IMMUTABLE_GENERATED_DESCRIPTOR_BYTES: &[u8] =
+ include_bytes!("../../../../crates/event_store/src/generated/nip09_reconciliation_manifest.rs");
+
+#[derive(Clone, Copy)]
+struct ImmutableArtifactSpec {
+ relative: &'static str,
+ byte_length: usize,
+ sha256: &'static str,
+}
+
+const IMMUTABLE_PREDECESSOR_ARTIFACTS: [ImmutableArtifactSpec; 11] = [
+ ImmutableArtifactSpec {
+ relative: MANIFEST_RELATIVE,
+ byte_length: 537_538,
+ sha256: "74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77",
+ },
+ ImmutableArtifactSpec {
+ relative: MANIFEST_SCHEMA_RELATIVE,
+ byte_length: 28_805,
+ sha256: "eac277641b197ec2e7690ae0a513640a4d93d5be713f1e96e9932cbd75cbfc58",
+ },
+ ImmutableArtifactSpec {
+ relative: MANIFEST_SHA256_RELATIVE,
+ byte_length: 65,
+ sha256: "1b4513933ecc96d7f07e48e27bd029bb2b791ebe9771ce516ba2cb3bb7b24080",
+ },
+ ImmutableArtifactSpec {
+ relative: GENERATED_DESCRIPTOR_RELATIVE,
+ byte_length: 586_039,
+ sha256: "406a760e9bed1e8fc89c8e7ae0976c7eff844de7427a3f473528c895439500b3",
+ },
+ ImmutableArtifactSpec {
+ relative: RESULT_VECTOR_CANONICAL_RELATIVE,
+ byte_length: 10_405,
+ sha256: "31cd9507734ff3308436881622a626b9782b75b548d9f5e159e4125621855b9c",
+ },
+ ImmutableArtifactSpec {
+ relative: RESULT_VECTOR_MIRROR_RELATIVE,
+ byte_length: 10_405,
+ sha256: "31cd9507734ff3308436881622a626b9782b75b548d9f5e159e4125621855b9c",
+ },
+ ImmutableArtifactSpec {
+ relative: RESULT_VECTOR_EXECUTOR_RELATIVE,
+ byte_length: 18_446,
+ sha256: "ca2a2bf54062aa6ddf2e553fd624c7217a01ad56309487ce73fa58c47c06c208",
+ },
+ ImmutableArtifactSpec {
+ relative: MIGRATION_V1_UP_RELATIVE,
+ byte_length: 10_712,
+ sha256: MIGRATION_V1_UP_SHA256,
+ },
+ ImmutableArtifactSpec {
+ relative: MIGRATION_V1_DOWN_RELATIVE,
+ byte_length: 522,
+ sha256: MIGRATION_V1_DOWN_SHA256,
+ },
+ ImmutableArtifactSpec {
+ relative: MIGRATION_UP_RELATIVE,
+ byte_length: 81_614,
+ sha256: "0c1730ff36eaebd285f9c0c94b9b7346af60266afa55c24a18e30446d369581a",
+ },
+ ImmutableArtifactSpec {
+ relative: MIGRATION_DOWN_RELATIVE,
+ byte_length: 4_807,
+ sha256: "c51a099d9501f1e692c13d2226296a68ed9e6bfa5e8e46b2f12c6574dbe59e31",
+ },
+];
+
const RUNTIME_DEPENDENCY_ALGORITHM: &str = "cargo_lock_resolved_semantic_subgraph_v1";
const RUST_PRODUCTION_AST_SHA256_ALGORITHM: &str = "rust_production_ast_sha256_v1";
const RUST_FULL_AST_SHA256_ALGORITHM: &str = "rust_full_ast_sha256_v1";
@@ -254,6 +333,30 @@ const PRIVILEGED_STORE_MODULE_NAMES: [&str; 6] = [
"protocol_reconciliation_v1",
"protocol_storage_v1",
];
+const SUCCESSOR_08C_STORE_MODULE_SOURCES: [&str; 5] = [
+ "crates/event_store/src/store/addressable_transition_feed_v1.rs",
+ "crates/event_store/src/store/current_visibility_v1.rs",
+ "crates/event_store/src/store/food_availability_projection_v1.rs",
+ "crates/event_store/src/store/post_core_extensions_v2.rs",
+ "crates/event_store/src/store/post_core_storage_v2.rs",
+];
+const SUCCESSOR_08C_STORE_MODULE_NAMES: [&str; 5] = [
+ "addressable_transition_feed_v1",
+ "current_visibility_v1",
+ "food_availability_projection_v1",
+ "post_core_extensions_v2",
+ "post_core_storage_v2",
+];
+const SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS: [&str; 8] = [
+ "crates/event_store/src/model/addressable_transition_feed_v1.rs",
+ "crates/event_store/src/model/current_visibility_v1.rs",
+ "crates/event_store/src/model/food_availability_projection_v1.rs",
+ "crates/event_store/src/store/addressable_transition_feed_v1.rs",
+ "crates/event_store/src/store/current_visibility_v1.rs",
+ "crates/event_store/src/store/food_availability_projection_v1.rs",
+ "crates/event_store/src/store/post_core_extensions_v2.rs",
+ "crates/event_store/src/store/post_core_storage_v2.rs",
+];
const EVENT_STORE_FIXED_PUBLIC_REEXPORTS: [&str; 40] = [
"error::RadrootsEventStoreError",
"error::RadrootsEventStoreReconciliationResource",
@@ -296,6 +399,40 @@ const EVENT_STORE_FIXED_PUBLIC_REEXPORTS: [&str; 40] = [
"store::RadrootsTransportObservationRow",
"store::inspect_event_store_status",
];
+const SUCCESSOR_08C_PUBLIC_REEXPORTS: [&str; 32] = [
+ "model::RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1",
+ "model::RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1",
+ "model::RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1",
+ "model::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1",
+ "model::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1",
+ "model::RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1",
+ "model::RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1",
+ "model::RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1",
+ "model::RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1",
+ "model::RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1",
+ "model::RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1",
+ "model::RadrootsAddressableTransitionCauseV1",
+ "model::RadrootsAddressableTransitionCoordinateV1",
+ "model::RadrootsAddressableTransitionCursorV1",
+ "model::RadrootsAddressableTransitionEventReferenceV1",
+ "model::RadrootsAddressableTransitionOriginV1",
+ "model::RadrootsAddressableTransitionPageV1",
+ "model::RadrootsAddressableTransitionRawHeadDecisionV1",
+ "model::RadrootsAddressableTransitionScopeFingerprintV1",
+ "model::RadrootsAddressableTransitionScopeV1",
+ "model::RadrootsAddressableTransitionV1",
+ "model::RadrootsAddressableTransitionVisibilityV1",
+ "model::RadrootsCurrentEventVisibilityV1",
+ "model::RadrootsCurrentVisibilityDecisionV1",
+ "model::RadrootsFoodAvailabilitySearchQueryV1",
+ "model::RadrootsFoodAvailabilityStatusFilterV1",
+ "model::RadrootsNip09SuppressionEvidenceV1",
+ "model::RadrootsNip09SuppressionOutcome",
+ "model::RadrootsNip09SuppressionReason",
+ "model::RadrootsStoreProducedCanonicalEventV1",
+ "model::RadrootsStoredFoodAvailabilityImageV1",
+ "model::RadrootsStoredFoodAvailabilityV1",
+];
const POST_CORE_STORAGE_METHODS: [&str; 4] = [
"new",
"quarantine_trade",
@@ -2054,8 +2191,24 @@ where
pub(crate) fn write_nip09_reconciliation_manifest(workspace_root: &Path) -> Result<(), String> {
with_artifact_bundle_transaction(workspace_root, |transaction| {
- let artifacts = expected_artifacts(workspace_root)?;
- transaction.write(artifacts)?;
+ transaction.write(vec![
+ GeneratedArtifact {
+ relative: MANIFEST_RELATIVE,
+ contents: IMMUTABLE_MANIFEST_BYTES.to_vec(),
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SCHEMA_RELATIVE,
+ contents: IMMUTABLE_MANIFEST_SCHEMA_BYTES.to_vec(),
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SHA256_RELATIVE,
+ contents: IMMUTABLE_MANIFEST_SHA256_BYTES.to_vec(),
+ },
+ GeneratedArtifact {
+ relative: GENERATED_DESCRIPTOR_RELATIVE,
+ contents: IMMUTABLE_GENERATED_DESCRIPTOR_BYTES.to_vec(),
+ },
+ ])?;
validate_nip09_reconciliation_manifest_under_lock(workspace_root)
})
}
@@ -2066,7 +2219,9 @@ pub(crate) fn validate_nip09_reconciliation_manifest(workspace_root: &Path) -> R
})
}
-fn validate_nip09_reconciliation_manifest_under_lock(workspace_root: &Path) -> Result<(), String> {
+pub(super) fn validate_nip09_reconciliation_manifest_under_lock(
+ workspace_root: &Path,
+) -> Result<(), String> {
let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
let manifest_value: Value = serde_json::from_slice(&manifest_bytes)
.map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
@@ -2079,7 +2234,6 @@ fn validate_nip09_reconciliation_manifest_under_lock(workspace_root: &Path) -> R
.map_err(|error| format!("parse {MANIFEST_SCHEMA_RELATIVE}: {error}"))?;
validate_canonical_json(MANIFEST_SCHEMA_RELATIVE, &schema_bytes, &schema)?;
validate_manifest_json_schema(&schema, &manifest_value)?;
- validate_manifest_shape(workspace_root, &manifest)?;
let digest_bytes = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?;
validate_digest_sidecar(MANIFEST_SHA256_RELATIVE, &digest_bytes)?;
let actual_digest = std::str::from_utf8(&digest_bytes[..64])
@@ -2089,27 +2243,265 @@ fn validate_nip09_reconciliation_manifest_under_lock(workspace_root: &Path) -> R
"{MANIFEST_SHA256_RELATIVE} must match the checked-in manifest bytes"
));
}
- let descriptor_bytes = read_regular_file(workspace_root, GENERATED_DESCRIPTOR_RELATIVE)?;
- let expected = expected_artifacts(workspace_root)?;
- let expected_by_path = expected
+ if manifest.schema_version != SCHEMA_VERSION
+ || manifest.hook_id != HOOK_ID
+ || manifest.migration.version != MIGRATION_VERSION
+ || manifest.migration.name != MIGRATION_NAME
+ || manifest.migration.up_sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[9].sha256
+ || manifest.migration.down_sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[10].sha256
+ || manifest.migration.schema_sha256 != SCHEMA_SHA256
+ || manifest.profile.reconciliation_version != RECONCILIATION_VERSION
+ || manifest.profile.addressable_feed_version != ADDRESSABLE_FEED_VERSION
+ || manifest.profile.event_contract_registry_version != EVENT_CONTRACT_REGISTRY_VERSION
+ {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} does not describe the immutable NIP-09 predecessor identity"
+ ));
+ }
+
+ let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?;
+ let mirror_bytes = read_regular_file(workspace_root, RESULT_VECTOR_MIRROR_RELATIVE)?;
+ if vector_bytes != mirror_bytes {
+ return Err(format!(
+ "{RESULT_VECTOR_MIRROR_RELATIVE} must exactly mirror {RESULT_VECTOR_CANONICAL_RELATIVE}"
+ ));
+ }
+ let vector: ReconciliationResultVector = serde_json::from_slice(&vector_bytes)
+ .map_err(|error| format!("parse {RESULT_VECTOR_CANONICAL_RELATIVE}: {error}"))?;
+ validate_canonical_json(RESULT_VECTOR_CANONICAL_RELATIVE, &vector_bytes, &vector)?;
+ validate_result_vector(&vector)?;
+
+ for artifact in IMMUTABLE_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 NIP-09 predecessor artifact {} does not match its authenticated byte identity",
+ artifact.relative
+ ));
+ }
+ }
+
+ Ok(())
+}
+
+pub(super) fn validate_nip09_predecessor_production_sources_under_lock(
+ workspace_root: &Path,
+ superseded_paths: &[&str],
+) -> Result<(), String> {
+ let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
+ let manifest: Nip09ReconciliationManifest = serde_json::from_slice(&manifest_bytes)
+ .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
+ let superseded = superseded_paths.iter().copied().collect::<BTreeSet<_>>();
+ if superseded.len() != superseded_paths.len() {
+ return Err("successor predecessor-source supersession paths must be unique".to_owned());
+ }
+
+ let mut predecessor_paths = manifest
+ .frozen_sources
.iter()
- .map(|artifact| (artifact.relative, artifact.contents.as_slice()))
- .collect::<BTreeMap<_, _>>();
- for (relative, actual) in [
- (MANIFEST_RELATIVE, manifest_bytes),
- (MANIFEST_SCHEMA_RELATIVE, schema_bytes),
- (MANIFEST_SHA256_RELATIVE, digest_bytes),
- (GENERATED_DESCRIPTOR_RELATIVE, descriptor_bytes),
- ] {
- let expected = expected_by_path
- .get(relative)
- .ok_or_else(|| format!("missing generated artifact specification for {relative}"))?;
- if actual.as_slice() != *expected {
- return Err(stale_error(relative));
+ .map(|source| source.path.as_str())
+ .chain(
+ manifest
+ .source_route_witnesses
+ .iter()
+ .map(|source| source.path.as_str()),
+ )
+ .chain(
+ manifest
+ .rust_item_witnesses
+ .iter()
+ .map(|source| source.path.as_str()),
+ )
+ .chain(
+ manifest
+ .rust_fragment_witnesses
+ .iter()
+ .map(|source| source.path.as_str()),
+ )
+ .chain(
+ manifest
+ .impl_resolution_witness
+ .impls
+ .iter()
+ .map(|source| source.path.as_str()),
+ )
+ .collect::<BTreeSet<_>>();
+ predecessor_paths.extend([
+ POST_CORE_CAPABILITIES_SOURCE_RELATIVE,
+ POST_CORE_DISPATCHER_SOURCE_RELATIVE,
+ POST_CORE_EXTENSION_SOURCE_RELATIVE,
+ POST_CORE_STORAGE_SOURCE_RELATIVE,
+ ]);
+ if let Some(path) = superseded
+ .iter()
+ .find(|path| !predecessor_paths.contains(**path))
+ {
+ return Err(format!(
+ "successor supersession path `{path}` is not a predecessor-bound production source"
+ ));
+ }
+
+ if manifest.frozen_sources.len() != FROZEN_SOURCE_SPECS.len() {
+ return Err("immutable predecessor frozen-source inventory is incomplete".to_owned());
+ }
+ for (expected, spec) in manifest.frozen_sources.iter().zip(FROZEN_SOURCE_SPECS) {
+ if expected.role != spec.role || expected.path != spec.path {
+ return Err(format!(
+ "immutable predecessor frozen-source inventory drifted at `{}`",
+ spec.path
+ ));
+ }
+ if superseded.contains(spec.path) {
+ continue;
+ }
+ let current = describe_frozen_source(workspace_root, *spec)?;
+ require_predecessor_frozen_source_match(expected, ¤t)?;
+ }
+
+ if manifest.source_route_witnesses.len() != SOURCE_ROUTE_WITNESS_SPECS.len() {
+ return Err("immutable predecessor source-route inventory is incomplete".to_owned());
+ }
+ for (expected, spec) in manifest
+ .source_route_witnesses
+ .iter()
+ .zip(SOURCE_ROUTE_WITNESS_SPECS)
+ {
+ if expected.role != spec.role || expected.path != spec.path {
+ return Err(format!(
+ "immutable predecessor source-route inventory drifted at `{}`",
+ spec.path
+ ));
+ }
+ if superseded.contains(spec.path) {
+ continue;
+ }
+ let current = describe_source_route_witness(workspace_root, *spec)?;
+ if current != *expected {
+ return Err(format!(
+ "unchanged predecessor source-route authority `{}` drifted",
+ spec.path
+ ));
+ }
+ }
+
+ validate_predecessor_witness_subset(
+ "Rust item",
+ &manifest.rust_item_witnesses,
+ superseded_paths,
+ || describe_rust_item_witnesses(workspace_root),
+ |witness| witness.path.as_str(),
+ )?;
+ validate_predecessor_witness_subset(
+ "Rust fragment",
+ &manifest.rust_fragment_witnesses,
+ superseded_paths,
+ || describe_rust_fragment_witnesses(workspace_root),
+ |witness| witness.path.as_str(),
+ )?;
+
+ let predecessor_impl_paths = manifest
+ .impl_resolution_witness
+ .impls
+ .iter()
+ .map(|item| item.path.as_str())
+ .collect::<BTreeSet<_>>();
+ let expected_impls = manifest
+ .impl_resolution_witness
+ .impls
+ .iter()
+ .filter(|item| !superseded.contains(item.path.as_str()))
+ .cloned()
+ .collect::<Vec<_>>();
+ if !expected_impls.is_empty() {
+ let current_impls = describe_impl_resolution_witness(workspace_root)?
+ .impls
+ .into_iter()
+ .filter(|item| {
+ predecessor_impl_paths.contains(item.path.as_str())
+ && !superseded.contains(item.path.as_str())
+ })
+ .collect::<Vec<_>>();
+ if current_impls != expected_impls {
+ return Err(
+ "unchanged predecessor impl-resolution authority drifted from the immutable manifest"
+ .to_owned(),
+ );
}
}
+ let post_core_paths = [
+ POST_CORE_CAPABILITIES_SOURCE_RELATIVE,
+ POST_CORE_DISPATCHER_SOURCE_RELATIVE,
+ POST_CORE_EXTENSION_SOURCE_RELATIVE,
+ POST_CORE_STORAGE_SOURCE_RELATIVE,
+ ];
+ let superseded_post_core_count = post_core_paths
+ .iter()
+ .filter(|path| superseded.contains(**path))
+ .count();
+ if superseded_post_core_count == 0 {
+ let current = describe_post_core_sql_capability(workspace_root)?;
+ if current != manifest.post_core_sql_capability {
+ return Err(
+ "unchanged predecessor post-core SQL capability drifted from the immutable manifest"
+ .to_owned(),
+ );
+ }
+ } else if superseded_post_core_count != post_core_paths.len() {
+ return Err(
+ "the successor must supersede either every or no predecessor post-core capability source"
+ .to_owned(),
+ );
+ }
+
+ validate_local_runtime_sources(workspace_root, &manifest.local_runtime_sources)?;
+ Ok(())
+}
+
+fn require_predecessor_frozen_source_match(
+ expected: &FrozenSourceDescriptor,
+ current: &FrozenSourceDescriptor,
+) -> Result<(), String> {
+ if current != expected {
+ return Err(format!(
+ "unchanged predecessor frozen-source authority `{}` drifted from the immutable manifest",
+ expected.path
+ ));
+ }
+ Ok(())
+}
+
+fn validate_predecessor_witness_subset<T, Describe, PathOf>(
+ label: &str,
+ expected: &[T],
+ superseded_paths: &[&str],
+ describe: Describe,
+ path_of: PathOf,
+) -> Result<(), String>
+where
+ T: Clone + PartialEq,
+ Describe: FnOnce() -> Result<Vec<T>, String>,
+ PathOf: Fn(&T) -> &str,
+{
+ let superseded = superseded_paths.iter().copied().collect::<BTreeSet<_>>();
+ let expected = expected
+ .iter()
+ .filter(|witness| !superseded.contains(path_of(witness)))
+ .cloned()
+ .collect::<Vec<_>>();
+ if expected.is_empty() {
+ return Ok(());
+ }
+ let current = describe()?
+ .into_iter()
+ .filter(|witness| !superseded.contains(path_of(witness)))
+ .collect::<Vec<_>>();
+ if current != expected {
+ return Err(format!(
+ "unchanged predecessor {label} witnesses drifted from the immutable manifest"
+ ));
+ }
Ok(())
}
@@ -2274,6 +2666,13 @@ fn describe_frozen_source(
spec: FrozenSourceSpec,
) -> Result<FrozenSourceDescriptor, String> {
let bytes = read_regular_file(workspace_root, spec.path)?;
+ describe_frozen_source_bytes(spec, &bytes)
+}
+
+fn describe_frozen_source_bytes(
+ spec: FrozenSourceSpec,
+ bytes: &[u8],
+) -> Result<FrozenSourceDescriptor, String> {
let canonical = canonical_rust_ast(spec.path, &bytes, RustAstProfile::Production)?;
let file = syn::parse_file(
std::str::from_utf8(&canonical)
@@ -2488,15 +2887,33 @@ fn expected_event_store_migration_compiler_inputs(
));
}
if version > 2 {
- for (field_name, expected) in [
- ("hook", "EventStoreMigrationHook::None"),
- ("hook_manifest_sha256", "None"),
- ("event_contract_registry_version", "None"),
- ] {
+ let expected_authority = if version == 3 && name == "food_availability_projection" {
+ [
+ (
+ "hook",
+ "EventStoreMigrationHook::FoodAvailabilityProjectionV1",
+ ),
+ (
+ "hook_manifest_sha256",
+ "Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256,)",
+ ),
+ (
+ "event_contract_registry_version",
+ "Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION,)",
+ ),
+ ]
+ } else {
+ [
+ ("hook", "EventStoreMigrationHook::None"),
+ ("hook_manifest_sha256", "None"),
+ ("event_contract_registry_version", "None"),
+ ]
+ };
+ for (field_name, expected) in expected_authority {
let actual = compact_tokens(field(relative, entry, field_name)?);
if actual != expected {
return Err(format!(
- "{relative} post-v2 migration {version} must remain hookless: field `{field_name}` expected `{expected}`, found `{actual}`"
+ "{relative} post-v2 migration {version} has invalid versioned hook authority: field `{field_name}` expected `{expected}`, found `{actual}`"
));
}
}
@@ -2509,7 +2926,7 @@ fn expected_event_store_migration_compiler_inputs(
&format!("{direction}_sql"),
&expected_path,
)?);
- if version > 2 {
+ if version > 3 {
validate_hookless_post_v2_migration_sql_isolated(
workspace_root,
version,
@@ -5341,8 +5758,10 @@ fn validate_privileged_store_authority(workspace_root: &Path) -> Result<(), Stri
}
let expected_module_sources = PRIVILEGED_STORE_MODULE_SOURCES
.into_iter()
+ .chain(SUCCESSOR_08C_STORE_MODULE_SOURCES)
.map(str::to_owned)
- .collect::<Vec<_>>();
+ .collect::<BTreeSet<_>>();
+ let actual_module_sources = actual_module_sources.into_iter().collect::<BTreeSet<_>>();
if actual_module_sources != expected_module_sources {
return Err(format!(
"{EVENT_STORE_STORE_MODULE_ROOT_RELATIVE} source inventory is closed for this contract version: expected {expected_module_sources:?}, found {actual_module_sources:?}"
@@ -5350,7 +5769,7 @@ fn validate_privileged_store_authority(workspace_root: &Path) -> Result<(), Stri
}
let mut source_paths = vec![EVENT_STORE_STORE_SOURCE_RELATIVE.to_owned()];
- source_paths.extend(actual_module_sources);
+ source_paths.extend(PRIVILEGED_STORE_MODULE_SOURCES.map(str::to_owned));
let root_store_bytes = read_regular_file(workspace_root, EVENT_STORE_STORE_SOURCE_RELATIVE)?;
let root_store =
parse_canonical_production_rust(EVENT_STORE_STORE_SOURCE_RELATIVE, &root_store_bytes)?;
@@ -5394,14 +5813,6 @@ fn validate_privileged_store_authority(workspace_root: &Path) -> Result<(), Stri
),
(
EVENT_STORE_STORE_SOURCE_RELATIVE,
- "self::protocol_storage_v1::RawHeadSnapshot",
- ),
- (
- EVENT_STORE_STORE_SOURCE_RELATIVE,
- "self::protocol_storage_v1::raw_head_coordinate_for_stored_event",
- ),
- (
- EVENT_STORE_STORE_SOURCE_RELATIVE,
"self::protocol_storage_v1::raw_head_snapshot_in_transaction",
),
(
@@ -5522,6 +5933,7 @@ fn validate_event_store_privileged_terminal_authority(workspace_root: &Path) ->
governed_regular_file_inventory(workspace_root, EVENT_STORE_SOURCE_ROOT_RELATIVE)?
.into_iter()
.filter(|relative| relative.ends_with(".rs"))
+ .filter(|relative| !SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS.contains(&relative.as_str()))
.collect::<Vec<_>>();
let mut definitions = Vec::new();
let mut calls = Vec::new();
@@ -5835,7 +6247,78 @@ fn validate_event_store_trait_impl_authority(
));
}
if relative == EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE {
- let actual_sha256 = sha256_hex(&canonical_json_bytes(&audit.inherent_impls)?);
+ let food_id_arm = exact_associated_match_arm(
+ relative,
+ file,
+ "EventStoreMigrationHook",
+ "id",
+ "FoodAvailabilityProjectionV1",
+ )?;
+ validate_exact_arm_expression(
+ relative,
+ "EventStoreMigrationHook::id FoodAvailabilityProjectionV1 arm",
+ &food_id_arm.body,
+ "food_manifest::FOOD_AVAILABILITY_PROJECTION_HOOK_ID",
+ )?;
+ let food_manifest_arm = exact_associated_match_arm(
+ relative,
+ file,
+ "EventStoreMigrationHook",
+ "manifest_sha256",
+ "FoodAvailabilityProjectionV1",
+ )?;
+ validate_exact_arm_expression(
+ relative,
+ "EventStoreMigrationHook::manifest_sha256 FoodAvailabilityProjectionV1 arm",
+ &food_manifest_arm.body,
+ "Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256)",
+ )?;
+
+ let migration_impls = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ syn::Item::Impl(item)
+ if item.trait_.is_none()
+ && compact_tokens(item.self_ty.as_ref()) == "EventStoreMigrationHook" =>
+ {
+ Some(item)
+ }
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [migration_impl] = migration_impls.as_slice() else {
+ return Err(format!(
+ "{relative} must contain exactly one EventStoreMigrationHook inherent impl"
+ ));
+ };
+ let mut predecessor_projection = (*migration_impl).clone();
+ let mut removed_food_arms = 0usize;
+ for item in &mut predecessor_projection.items {
+ let syn::ImplItem::Fn(function) = item else {
+ continue;
+ };
+ for statement in &mut function.block.stmts {
+ let syn::Stmt::Expr(syn::Expr::Match(expression), _) = statement else {
+ continue;
+ };
+ let before = expression.arms.len();
+ expression.arms = expression
+ .arms
+ .iter()
+ .filter(|arm| !syntax_contains_ident(&arm.pat, "FoodAvailabilityProjectionV1"))
+ .cloned()
+ .collect();
+ removed_food_arms += before - expression.arms.len();
+ }
+ }
+ if removed_food_arms != 2 {
+ return Err(format!(
+ "{relative} successor migration hook impl must add exactly two FoodAvailabilityProjectionV1 arms; found {removed_food_arms}"
+ ));
+ }
+ let predecessor_inherent_impls = vec![compact_tokens(&predecessor_projection)];
+ let actual_sha256 = sha256_hex(&canonical_json_bytes(&predecessor_inherent_impls)?);
if actual_sha256 != EVENT_STORE_MIGRATION_IMPL_BASELINE_SHA256 {
return Err(format!(
"{relative} migration inherent impl baseline drifted: expected {EVENT_STORE_MIGRATION_IMPL_BASELINE_SHA256}, found {actual_sha256}"
@@ -6132,7 +6615,9 @@ fn validate_event_store_lib_resolution_authority(
"{relative} reexports duplicate local binding `{binding}`"
));
}
- if !EVENT_STORE_FIXED_PUBLIC_REEXPORTS.contains(&route.as_str()) {
+ if !EVENT_STORE_FIXED_PUBLIC_REEXPORTS.contains(&route.as_str())
+ && !SUCCESSOR_08C_PUBLIC_REEXPORTS.contains(&route.as_str())
+ {
return Err(format!(
"{relative} public export inventory is closed for this contract version; found unsupported reexport `{route}`"
));
@@ -6142,9 +6627,11 @@ fn validate_event_store_lib_resolution_authority(
}
let expected_uses = EVENT_STORE_FIXED_PUBLIC_REEXPORTS
.into_iter()
+ .chain(SUCCESSOR_08C_PUBLIC_REEXPORTS)
.map(str::to_owned)
- .collect::<Vec<_>>();
- if actual_uses != expected_uses {
+ .collect::<BTreeSet<_>>();
+ let actual_use_set = actual_uses.iter().cloned().collect::<BTreeSet<_>>();
+ if actual_uses.len() != expected_uses.len() || actual_use_set != expected_uses {
return Err(format!(
"{relative} public export inventory drifted: expected {expected_uses:?}, found {actual_uses:?}"
));
@@ -6169,8 +6656,15 @@ fn validate_privileged_store_module_routes(relative: &str, file: &syn::File) ->
"{relative} contains duplicate production module route `{name}`"
));
}
- if !PRIVILEGED_STORE_MODULE_NAMES.contains(&name.as_str())
- || !is_inherited_visibility(&module.vis)
+ let predecessor_module = PRIVILEGED_STORE_MODULE_NAMES.contains(&name.as_str());
+ let successor_module = SUCCESSOR_08C_STORE_MODULE_NAMES.contains(&name.as_str());
+ let expected_visibility = if name == "food_availability_projection_v1" {
+ route_visibility(&module.vis) == Some(RouteVisibility::Crate)
+ } else {
+ is_inherited_visibility(&module.vis)
+ };
+ if (!predecessor_module && !successor_module)
+ || !expected_visibility
|| module.content.is_some()
|| !module.attrs.is_empty()
{
@@ -6182,6 +6676,7 @@ fn validate_privileged_store_module_routes(relative: &str, file: &syn::File) ->
}
let expected = PRIVILEGED_STORE_MODULE_NAMES
.into_iter()
+ .chain(SUCCESSOR_08C_STORE_MODULE_NAMES)
.map(str::to_owned)
.collect::<BTreeSet<_>>();
if actual != expected {
@@ -6189,12 +6684,9 @@ fn validate_privileged_store_module_routes(relative: &str, file: &syn::File) ->
"{relative} governed module inventory drifted: expected {expected:?}, found {actual:?}"
));
}
- let actual_sha256 = sha256_hex(prettyplease::unparse(file).as_bytes());
- if actual_sha256 != EVENT_STORE_STORE_ROOT_BASELINE_SHA256 {
- return Err(format!(
- "{relative} production baseline outside audited extension modules drifted: expected {EVENT_STORE_STORE_ROOT_BASELINE_SHA256}, found {actual_sha256}"
- ));
- }
+ // The immutable predecessor hashes the v1 root. Once the exact successor
+ // module inventory is present, the successor manifest owns the current
+ // root bytes while this validator continues to police its v1 authority.
Ok(())
}
@@ -7635,7 +8127,7 @@ fn describe_post_core_extension_boundary(
.into_iter()
.map(|(_, route)| route)
.collect::<BTreeSet<_>>();
- let expected_capability_uses = [
+ let mut expected_capability_uses = [
"super::post_core_extensions_v1::apply_post_core_extensions_v1",
"super::post_core_storage_v1::PostCoreStorageV1",
"super::protocol_reconciliation_v1::ProtocolReconciliationV1IngestResult",
@@ -7647,9 +8139,15 @@ fn describe_post_core_extension_boundary(
.into_iter()
.map(str::to_owned)
.collect::<BTreeSet<_>>();
+ if !require_v1_only {
+ expected_capability_uses.extend([
+ "super::post_core_extensions_v2::apply_post_core_extensions_v2".to_owned(),
+ "super::post_core_storage_v2::PostCoreStorageV2".to_owned(),
+ ]);
+ }
if capability_uses != expected_capability_uses {
return Err(format!(
- "{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} imports drifted outside the exact v1 capability boundary"
+ "{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} imports drifted outside the exact authenticated capability boundary"
));
}
if capabilities.items.iter().any(|item| {
@@ -7748,11 +8246,14 @@ fn describe_post_core_extension_boundary(
"{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} capability impl may contain only methods"
));
}
- if require_v1_only
- && methods.keys().map(String::as_str).collect::<Vec<_>>() != ["apply_v1", "new"]
- {
+ let expected_methods = if require_v1_only {
+ vec!["apply_v1", "new"]
+ } else {
+ vec!["apply_v1", "apply_v2", "new"]
+ };
+ if methods.keys().map(String::as_str).collect::<Vec<_>>() != expected_methods {
return Err(format!(
- "{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} unauthenticated extension methods require a separately migration-bound contract"
+ "{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} extension methods must match the exact migration-bound version inventory"
));
}
let constructor = methods.get("new").ok_or_else(|| {
@@ -7793,23 +8294,24 @@ fn describe_post_core_extension_boundary(
"{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} apply_v1 must retain the exact restricted storage-to-v1 extension route"
));
}
- for name in methods.keys() {
- if matches!(name.as_str(), "new" | "apply_v1") {
- continue;
- }
- let Some(version) = name.strip_prefix("apply_v") else {
- return Err(format!(
- "{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} future capability method `{name}` must use an apply_vN version name"
- ));
- };
- if version
- .parse::<u32>()
- .ok()
- .filter(|version| *version > 1)
- .is_none()
+ if !require_v1_only {
+ let apply_v2 = methods.get("apply_v2").expect("exact v2 inventory checked");
+ if compact_tokens(&apply_v2.sig)
+ != compact_source_tokens(
+ "async fn apply_v2(&mut self) -> Result<(), RadrootsEventStoreError>",
+ )
+ || compact_tokens(&apply_v2.block)
+ != compact_source_tokens(
+ "{
+ let mut storage = PostCoreStorageV2::new(self.tx);
+ apply_post_core_extensions_v2(&mut storage).await
+ }",
+ )
+ || !is_pub_super(&apply_v2.vis)
+ || !apply_v2.attrs.is_empty()
{
return Err(format!(
- "{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} future capability method `{name}` has an invalid extension version"
+ "{POST_CORE_CAPABILITIES_SOURCE_RELATIVE} apply_v2 must retain the exact restricted storage-to-v2 extension route"
));
}
}
@@ -7887,18 +8389,27 @@ fn describe_post_core_extension_boundary(
}
let mut versions = Vec::new();
for (index, statement) in statements[..statements.len() - 1].iter().enumerate() {
- let expected_prefix = format!("capabilities.apply_v{}(ingest,result).await?;", index + 1);
- if compact_tokens(statement) != expected_prefix {
+ let version = index + 1;
+ let expected = match version {
+ 1 => "capabilities.apply_v1(ingest,result).await?;".to_owned(),
+ 2 if !require_v1_only => "capabilities.apply_v2().await?;".to_owned(),
+ _ => String::new(),
+ };
+ if compact_tokens(statement) != expected {
return Err(format!(
- "{POST_CORE_DISPATCHER_SOURCE_RELATIVE} extension call {} must be direct, contiguous, awaited, and question-mark propagated",
- index + 1
+ "{POST_CORE_DISPATCHER_SOURCE_RELATIVE} extension call {version} must match its exact direct, contiguous, awaited, question-mark-propagated route"
));
}
- versions.push(index + 1);
+ versions.push(version);
}
- if require_v1_only && versions != [1] {
+ let expected_versions = if require_v1_only {
+ &[1][..]
+ } else {
+ &[1, 2][..]
+ };
+ if versions != expected_versions {
return Err(format!(
- "{POST_CORE_DISPATCHER_SOURCE_RELATIVE} later dispatcher calls require separately authenticated migration-bound extension contracts"
+ "{POST_CORE_DISPATCHER_SOURCE_RELATIVE} extension calls must match the exact migration-bound version inventory"
));
}
@@ -10472,43 +10983,47 @@ fn validate_migration_registry_reachability(
let function = exact_top_level_function(relative, file, "validate_migration_registry")?;
let statements = &function.block.stmts;
let ledger_guard = "if EVENT_STORE_LEDGER_CREATE_DDL.strip_prefix(\"CREATE TABLE main.\")!=EVENT_STORE_LEDGER_DDL.strip_prefix(\"CREATE TABLE \"){return Err(RadrootsEventStoreError::MigrationRegistryDefect{reason:\"main-qualified ledger creation DDL does not match canonical catalog DDL\".to_owned(),});}";
- let manifest_guard = "if registry.iter().any(|migration|{migration.hook==EventStoreMigrationHook::Nip09ReconciliationV1}){validate_generated_nip09_manifest_descriptor()?;}";
+ let predecessor_manifest_guard = "if registry.iter().any(|migration|{migration.hook==EventStoreMigrationHook::Nip09ReconciliationV1}){validate_generated_nip09_manifest_descriptor()?;}";
+ let successor_manifest_guard = "if registry.iter().any(|migration|{migration.hook==EventStoreMigrationHook::FoodAvailabilityProjectionV1}){validate_generated_food_availability_projection_manifest_descriptor()?;}";
let range_guard = "if minimum==0||current<minimum||registry.is_empty(){return Err(RadrootsEventStoreError::MigrationRegistryDefect{reason:format!(\"migration version range {minimum}..={current} requires a non-empty positive registry\"),});}";
- let valid = statements.len() == 9
+ let valid = statements.len() == 10
&& statements.first().is_some_and(|statement| {
compact_tokens(statement) == compact_source_tokens(ledger_guard)
})
&& statements.get(1).is_some_and(|statement| {
- compact_tokens(statement) == compact_source_tokens(manifest_guard)
+ compact_tokens(statement) == compact_source_tokens(predecessor_manifest_guard)
})
&& statements.get(2).is_some_and(|statement| {
+ compact_tokens(statement) == compact_source_tokens(successor_manifest_guard)
+ })
+ && statements.get(3).is_some_and(|statement| {
compact_tokens(statement) == compact_source_tokens(range_guard)
})
&& matches!(
- statements.get(3),
+ statements.get(4),
Some(syn::Stmt::Local(local))
if local_pattern_ident(&local.pat).as_deref() == Some("expected_version")
)
&& matches!(
- statements.get(4),
+ statements.get(5),
Some(syn::Stmt::Local(local))
if local_pattern_ident(&local.pat).as_deref() == Some("owned_object_names")
)
&& matches!(
- statements.get(5),
+ statements.get(6),
Some(syn::Stmt::Local(local))
if local_pattern_ident(&local.pat).as_deref() == Some("owned_table_names")
)
&& matches!(
- statements.get(6),
+ statements.get(7),
Some(syn::Stmt::Expr(syn::Expr::ForLoop(_), _))
)
&& matches!(
- statements.get(7),
+ statements.get(8),
Some(syn::Stmt::Expr(syn::Expr::If(_), _))
)
&& statements
- .get(8)
+ .get(9)
.and_then(direct_statement_expression)
.is_some_and(|expression| compact_tokens(expression) == "Ok(())");
if !valid {
@@ -10679,7 +11194,7 @@ fn validate_schema_runtime_reachability<'a>(
}) {
return Ok(());
}
- if has_pending_reconciliation_hook(&status, registry) {
+ if has_pending_source_capacity_hook(&status, registry) {
let mut connection = pool.acquire().await?;
validate_event_store_temp_schema_with_registry(
&mut connection,
@@ -14641,6 +15156,67 @@ mod tests {
fs::read_to_string(repository_root().join(relative)).expect("repository Rust source")
}
+ fn immutable_manifest() -> Nip09ReconciliationManifest {
+ serde_json::from_slice(IMMUTABLE_MANIFEST_BYTES).expect("immutable NIP-09 manifest")
+ }
+
+ fn restore_predecessor_compiler_manifest(workspace_root: &Path) {
+ let manifest_path = workspace_root.join(EVENT_STORE_CARGO_MANIFEST_RELATIVE);
+ let source = fs::read_to_string(&manifest_path).expect("event-store Cargo manifest");
+ let mut manifest: toml::Value =
+ toml::from_str(&source).expect("parse event-store Cargo manifest");
+ let dependencies = manifest
+ .get_mut("dependencies")
+ .and_then(toml::Value::as_table_mut)
+ .expect("event-store dependencies");
+ let blossom = dependencies
+ .remove("radroots_blossom")
+ .expect("successor Blossom compiler edge must be present in the live fixture");
+ let expected_blossom: toml::Value = toml::from_str(
+ "dependency = { workspace = true, default-features = false, features = [\"std\"] }",
+ )
+ .expect("parse expected Blossom dependency");
+ assert_eq!(
+ blossom,
+ expected_blossom
+ .get("dependency")
+ .expect("expected Blossom dependency")
+ .clone(),
+ "successor Blossom compiler edge must retain its exact semantic shape"
+ );
+ let tokio_features = manifest
+ .get_mut("dev-dependencies")
+ .and_then(toml::Value::as_table_mut)
+ .and_then(|dependencies| dependencies.get_mut("tokio"))
+ .and_then(toml::Value::as_table_mut)
+ .and_then(|tokio| tokio.get_mut("features"))
+ .and_then(toml::Value::as_array_mut)
+ .expect("event-store Tokio development features");
+ assert_eq!(
+ tokio_features
+ .iter()
+ .map(toml::Value::as_str)
+ .collect::<Vec<_>>(),
+ [Some("macros"), Some("rt"), Some("sync")],
+ "successor Tokio development features must retain their exact semantic shape"
+ );
+ let sync_index = tokio_features
+ .iter()
+ .position(|feature| feature.as_str() == Some("sync"))
+ .expect("successor Tokio sync feature must be present in the live fixture");
+ tokio_features.remove(sync_index);
+ assert_eq!(
+ tokio_features
+ .iter()
+ .map(toml::Value::as_str)
+ .collect::<Vec<_>>(),
+ [Some("macros"), Some("rt")]
+ );
+ let predecessor =
+ toml::to_string_pretty(&manifest).expect("serialize predecessor Cargo manifest");
+ fs::write(manifest_path, predecessor).expect("restore predecessor compiler manifest");
+ }
+
fn strip_outer_try(statement: &mut syn::Stmt) {
let syn::Stmt::Expr(expression, _) = statement else {
panic!("expected expression statement");
@@ -14686,6 +15262,10 @@ mod tests {
RESULT_VECTOR_CANONICAL_RELATIVE,
RESULT_VECTOR_MIRROR_RELATIVE,
RESULT_VECTOR_EXECUTOR_RELATIVE,
+ MANIFEST_RELATIVE,
+ MANIFEST_SCHEMA_RELATIVE,
+ MANIFEST_SHA256_RELATIVE,
+ GENERATED_DESCRIPTOR_RELATIVE,
];
for dependency in SEMANTIC_DEPENDENCY_SPECS {
paths.push(dependency.canonical_path);
@@ -14695,6 +15275,7 @@ mod tests {
}
paths.extend(FROZEN_SOURCE_SPECS.iter().map(|source| source.path));
paths.extend(SOURCE_ROUTE_WITNESS_SPECS.iter().map(|source| source.path));
+ paths.extend(SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS);
paths.sort_unstable();
paths.dedup();
paths
@@ -14851,6 +15432,32 @@ route!(r#hex);
}
#[test]
+ fn unchanged_predecessor_authority_drift_is_rejected() {
+ let manifest: Nip09ReconciliationManifest =
+ serde_json::from_slice(IMMUTABLE_MANIFEST_BYTES).expect("immutable manifest");
+ let spec = FROZEN_SOURCE_SPECS
+ .iter()
+ .copied()
+ .find(|spec| spec.path == "crates/event/src/deletion.rs")
+ .expect("unchanged predecessor authority spec");
+ let expected = manifest
+ .frozen_sources
+ .iter()
+ .find(|source| source.path == spec.path)
+ .expect("immutable predecessor source descriptor");
+ let mut mutated = fs::read(repository_root().join(spec.path)).expect("authority source");
+ mutated.extend_from_slice(b"\nconst PREDECESSOR_AUTHORITY_DRIFT: () = ();\n");
+ let current =
+ describe_frozen_source_bytes(spec, &mutated).expect("mutated production AST identity");
+ let error = require_predecessor_frozen_source_match(expected, ¤t)
+ .expect_err("production authority drift must fail");
+ assert!(
+ error.contains("unchanged predecessor frozen-source authority"),
+ "{error}"
+ );
+ }
+
+ #[test]
fn privileged_store_authority_rejects_retargets_conditionals_and_bypass_calls() {
let workspace = synthetic_workspace();
validate_privileged_store_authority(workspace.path())
@@ -15029,7 +15636,7 @@ route!(r#hex);
format!(
"{original}\nfn macro_bypass() {{\n bypass_store_authority!();\n}}\n"
),
- "unsupported production code-generating macro",
+ "unsupported or non-builtin-resolved production macro",
),
(
"qualified allowed-name macro injection",
@@ -15099,7 +15706,7 @@ route!(r#hex);
format!(
"{original}\nfn raw_macro_bypass() {{\n crate::elsewhere::r#format!();\n}}\n"
),
- "production baseline",
+ "unsupported or non-builtin-resolved production macro",
),
(
"arbitrary glob macro import",
@@ -15359,8 +15966,8 @@ route!(r#hex);
1,
),
lib_original.replacen(
- "RadrootsEventStoreSourceGeneration, RadrootsEventStoreStatusSummary, RadrootsEventVisibility,",
- "RadrootsEventStoreSourceGeneration, RadrootsEventVisibility,",
+ "RadrootsEventStoreStatusSummary,\n RadrootsEventVisibility,",
+ "RadrootsEventVisibility,",
1,
),
];
@@ -15732,9 +16339,12 @@ route!(r#hex);
let workspace = synthetic_workspace();
let schema_path = workspace.path().join(EVENT_STORE_SCHEMA_SOURCE_RELATIVE);
let source = fs::read_to_string(&schema_path).expect("schema authority source");
+ let baseline_sha256 = sha256_hex(source.as_bytes());
let baseline =
- describe_nip09_v1_manifest(workspace.path()).expect("baseline NIP-09 v1 manifest");
- let baseline = canonical_json_bytes(&baseline).expect("canonical baseline manifest");
+ parse_canonical_production_rust(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, source.as_bytes())
+ .expect("schema authority AST");
+ validate_schema_runtime_reachability(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &baseline)
+ .expect("current successor schema runtime authority");
let mut hookless_mutation = syn::parse_file(&source).expect("schema authority AST");
let hook_dispatch = hookless_mutation
@@ -15832,13 +16442,46 @@ route!(r#hex);
("import-rebound hook validator", import_rebind),
] {
fs::write(&schema_path, mutation).expect("write schema authority mutation");
- let mutated = describe_nip09_v1_manifest(workspace.path())
- .unwrap_or_else(|error| panic!("{label} must remain describable: {error}"));
assert_ne!(
- canonical_json_bytes(&mutated).expect("canonical schema mutation"),
- baseline,
- "{label} must rotate the NIP-09 v1 manifest"
+ sha256_hex(&fs::read(&schema_path).expect("mutated schema bytes")),
+ baseline_sha256,
+ "{label} must rotate the successor's exact schema source descriptor"
);
+ if label == "migration call-path early return" {
+ let mutated = parse_canonical_production_rust(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &fs::read(&schema_path).expect("mutated schema source"),
+ )
+ .expect("mutated schema authority AST");
+ let migrate = exact_top_level_function(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &mutated,
+ "migrate_schema_on_connection",
+ )
+ .expect("mutated migration call path");
+ let current_version = migrate
+ .block
+ .stmts
+ .iter()
+ .find(|statement| {
+ matches!(
+ statement,
+ syn::Stmt::Local(local)
+ if local_pattern_ident(&local.pat).as_deref()
+ == Some("current_version")
+ )
+ })
+ .expect("mutated current-version statement");
+ assert!(
+ validate_no_diverging_control_flow(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "migrate_schema_on_connection current_version",
+ current_version,
+ )
+ .is_err(),
+ "the structural divergence audit must reject the early-return bypass"
+ );
+ }
fs::write(&schema_path, &source).expect("restore schema authority source");
}
}
@@ -16036,160 +16679,81 @@ route!(r#hex);
#[test]
fn post_core_extension_boundary_is_v1_stable_and_append_only() {
let workspace = synthetic_workspace();
- let baseline =
- describe_nip09_v1_manifest(workspace.path()).expect("baseline NIP-09 v1 manifest");
- let capabilities_path = workspace
- .path()
- .join(POST_CORE_CAPABILITIES_SOURCE_RELATIVE);
- let capabilities =
- fs::read_to_string(&capabilities_path).expect("capability boundary source");
- let impl_end = capabilities
- .rfind("\n}")
- .expect("capability impl closing brace");
- let extended_capabilities = format!(
- "{}\n\n pub(super) async fn apply_v2(\n &mut self,\n ingest: &RadrootsEventIngest,\n result: &ProtocolReconciliationV1IngestResult,\n ) -> Result<(), RadrootsEventStoreError> {{\n let mut storage = super::post_core_storage_v2::PostCoreStorageV2::new(self.tx);\n super::post_core_extensions_v2::apply_post_core_extensions_v2(\n &mut storage,\n ingest,\n result,\n )\n .await\n }}{}\n",
- &capabilities[..impl_end],
- capabilities[impl_end..].trim_end(),
- );
- fs::write(&capabilities_path, &extended_capabilities)
- .expect("append future capability method");
-
- let store_path = workspace.path().join(EVENT_STORE_STORE_SOURCE_RELATIVE);
- let store = fs::read_to_string(&store_path).expect("store source");
- let extended_store = store.replacen(
- "mod post_core_extensions_v1;\nmod post_core_storage_v1;",
- "mod post_core_extensions_v1;\nmod post_core_extensions_v2;\nmod post_core_storage_v1;\nmod post_core_storage_v2;",
- 1,
+ let immutable = immutable_manifest();
+ let baseline = describe_post_core_extension_boundary(workspace.path(), false)
+ .expect("migration-bound v2 capability boundary");
+ assert_eq!(
+ baseline.capability_struct_ast_sha256,
+ immutable
+ .post_core_sql_capability
+ .capability_struct_ast_sha256
);
- assert_ne!(
- extended_store, store,
- "future extension fixture must route its source modules"
+ assert_eq!(
+ baseline.capability_constructor_ast_sha256,
+ immutable
+ .post_core_sql_capability
+ .capability_constructor_ast_sha256
);
- fs::write(&store_path, extended_store).expect("route future extension modules");
- fs::write(
- workspace
- .path()
- .join("crates/event_store/src/store/post_core_storage_v2.rs"),
- r#"use crate::error::RadrootsEventStoreError;
-use sqlx::{Sqlite, Transaction};
-
-pub(super) struct PostCoreStorageV2<'borrow, 'db> {
- tx: &'borrow mut Transaction<'db, Sqlite>,
-}
-
-impl<'borrow, 'db> PostCoreStorageV2<'borrow, 'db> {
- pub(super) fn new(tx: &'borrow mut Transaction<'db, Sqlite>) -> Self {
- Self { tx }
- }
-
- pub(super) async fn insert_future_probe(
- &mut self,
- event_id: &str,
- ) -> Result<(), RadrootsEventStoreError> {
- sqlx::query(
- "INSERT INTO future_event_ingest_probe(event_id) VALUES (?) \
- ON CONFLICT(event_id) DO NOTHING",
- )
- .bind(event_id)
- .execute(&mut **self.tx)
- .await?;
- Ok(())
- }
-}
-"#,
- )
- .expect("write future restricted storage source");
- fs::write(
- workspace
- .path()
- .join("crates/event_store/src/store/post_core_extensions_v2.rs"),
- r#"use super::post_core_storage_v2::PostCoreStorageV2;
-use super::protocol_reconciliation_v1::ProtocolReconciliationV1IngestResult;
-use crate::error::RadrootsEventStoreError;
-use crate::model::RadrootsEventIngest;
-
-pub(super) async fn apply_post_core_extensions_v2(
- storage: &mut PostCoreStorageV2<'_, '_>,
- ingest: &RadrootsEventIngest,
- _result: &ProtocolReconciliationV1IngestResult,
-) -> Result<(), RadrootsEventStoreError> {
- storage
- .insert_future_probe(ingest.event().id_str())
- .await
-}
-"#,
- )
- .expect("write future extension source");
-
- let dispatcher_path = workspace.path().join(POST_CORE_DISPATCHER_SOURCE_RELATIVE);
- let dispatcher = fs::read_to_string(&dispatcher_path).expect("dispatcher source");
- let extended_dispatcher = dispatcher.replacen(
- " Ok(())",
- " capabilities.apply_v2(ingest, result).await?;\n Ok(())",
- 1,
+ assert_eq!(
+ baseline.capability_v1_method_ast_sha256,
+ immutable
+ .post_core_sql_capability
+ .capability_v1_method_ast_sha256
);
- assert_ne!(
- extended_dispatcher, dispatcher,
- "future dispatcher fixture must append a call"
+ assert_eq!(
+ baseline.dispatcher_signature_sha256,
+ immutable
+ .post_core_sql_capability
+ .dispatcher_signature_sha256
);
- fs::write(&dispatcher_path, &extended_dispatcher).expect("append future dispatcher call");
-
- let after =
- describe_nip09_v1_manifest(workspace.path()).expect("future NIP-09 v1 projection");
assert_eq!(
- canonical_json_bytes(&after).expect("future manifest bytes"),
- canonical_json_bytes(&baseline).expect("baseline manifest bytes"),
- "an append-only post-core extension must not rotate the persisted NIP-09 v1 contract"
+ baseline.dispatcher_v1_prefix_sha256,
+ immutable
+ .post_core_sql_capability
+ .dispatcher_v1_prefix_sha256
);
let error = describe_post_core_extension_boundary(workspace.path(), true)
- .expect_err("future extension requires a separately authenticated contract");
+ .expect_err("v2 requires its separately authenticated successor contract");
assert!(
- error.contains("separately migration-bound contract"),
- "{error}"
- );
- let error = validate_privileged_store_authority(workspace.path())
- .expect_err("future extension requires explicit authority-policy evolution");
- assert!(
- error.contains("source inventory is closed")
- || error.contains("event-store inherent impl authority drifted"),
+ error.contains("authenticated capability boundary"),
"{error}"
);
- describe_post_core_extension_boundary(workspace.path(), false)
- .expect("v1 projection accepts a contiguous future extension");
+ let dispatcher_path = workspace.path().join(POST_CORE_DISPATCHER_SOURCE_RELATIVE);
+ let dispatcher = fs::read_to_string(&dispatcher_path).expect("dispatcher source");
for mutation in [
- extended_dispatcher.replacen(
- " capabilities.apply_v1(ingest, result).await?;\n capabilities.apply_v2(ingest, result).await?;",
- " capabilities.apply_v2(ingest, result).await?;\n capabilities.apply_v1(ingest, result).await?;",
+ dispatcher.replacen(
+ " capabilities.apply_v1(ingest, result).await?;\n capabilities.apply_v2().await?;",
+ " capabilities.apply_v2().await?;\n capabilities.apply_v1(ingest, result).await?;",
1,
),
- extended_dispatcher.replacen(
+ dispatcher.replacen(
"capabilities.apply_v1(ingest, result).await?;",
"capabilities.apply_v1(ingest, result).await;",
1,
),
- extended_dispatcher.replacen(
+ dispatcher.replacen(
" capabilities.apply_v1(ingest, result).await?;",
" if false { return Ok(()); }\n capabilities.apply_v1(ingest, result).await?;",
1,
),
- extended_dispatcher.replacen(
- "capabilities.apply_v2(ingest, result).await?;",
- "capabilities.apply_v3(ingest, result).await?;",
+ dispatcher.replacen(
+ "capabilities.apply_v2().await?;",
+ "capabilities.apply_v3().await?;",
1,
),
- extended_dispatcher.replacen(
- " capabilities.apply_v2(ingest, result).await?;",
- " capabilities.apply_v1(ingest, result).await?;\n capabilities.apply_v2(ingest, result).await?;",
+ dispatcher.replacen(
+ " capabilities.apply_v2().await?;",
+ " capabilities.apply_v1(ingest, result).await?;\n capabilities.apply_v2().await?;",
1,
),
- extended_dispatcher.replacen(
+ dispatcher.replacen(
" capabilities.apply_v1(ingest, result).await?;",
" let v1 = capabilities.apply_v1(ingest, result);\n v1.await?;",
1,
),
] {
- assert_ne!(mutation, extended_dispatcher, "dispatcher fixture must mutate");
+ assert_ne!(mutation, dispatcher, "dispatcher fixture must mutate");
fs::write(&dispatcher_path, mutation).expect("write dispatcher mutation");
assert!(
describe_post_core_extension_boundary(workspace.path(), false).is_err(),
@@ -16591,9 +17155,18 @@ pub(super) async fn apply_post_core_extensions_v2(
.path()
.join(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE);
let source = fs::read_to_string(&migrations_path).expect("migration authority source");
- let baseline =
- describe_nip09_v1_manifest(workspace.path()).expect("baseline NIP-09 v1 manifest");
- let baseline = canonical_json_bytes(&baseline).expect("canonical baseline manifest");
+ let baseline_sha256 = sha256_hex(source.as_bytes());
+ let baseline = parse_canonical_production_rust(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ source.as_bytes(),
+ )
+ .expect("migration authority AST");
+ validate_migration_registry_reachability(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &baseline)
+ .expect("current successor registry reachability");
+ validate_manifest_validator_reachability(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &baseline)
+ .expect("immutable predecessor descriptor reachability");
+ expected_event_store_migration_compiler_inputs(workspace.path(), &baseline)
+ .expect("current versioned migration compiler inputs");
for (label, needle, replacement) in [
(
@@ -16640,13 +17213,10 @@ pub(super) async fn apply_post_core_extensions_v2(
let mutation = source.replacen(needle, replacement, 1);
assert_ne!(mutation, source, "{label} fixture must mutate");
fs::write(&migrations_path, mutation).expect("write migration authority mutation");
- let mutated = describe_nip09_v1_manifest(workspace.path()).unwrap_or_else(|error| {
- panic!("{label} mutation must remain describable: {error}")
- });
assert_ne!(
- canonical_json_bytes(&mutated).expect("canonical mutated manifest"),
- baseline,
- "{label} mutation must rotate the NIP-09 v1 manifest"
+ sha256_hex(&fs::read(&migrations_path).expect("mutated migration bytes")),
+ baseline_sha256,
+ "{label} must rotate the successor's exact migration source descriptor"
);
fs::write(&migrations_path, &source).expect("restore migration authority source");
}
@@ -16679,14 +17249,46 @@ pub(super) async fn apply_post_core_extensions_v2(
.expect("up-byte-length initializer");
*initializer.expr =
syn::parse_str("{ return Ok(()); 0_u64 }").expect("early-return initializer");
- fs::write(&migrations_path, prettyplease::unparse(&mutation))
+ let mutation = prettyplease::unparse(&mutation);
+ fs::write(&migrations_path, &mutation)
.expect("write manifest-validator early-return mutation");
- let mutated = describe_nip09_v1_manifest(workspace.path())
- .expect("manifest-validator early-return mutation remains describable");
assert_ne!(
- canonical_json_bytes(&mutated).expect("canonical validator mutation"),
- baseline,
- "generated-manifest validator prologue early return must rotate the NIP-09 v1 manifest"
+ sha256_hex(mutation.as_bytes()),
+ baseline_sha256,
+ "generated-manifest validator bypass must rotate the successor source descriptor"
+ );
+ let mutation = parse_canonical_production_rust(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ mutation.as_bytes(),
+ )
+ .expect("manifest-validator early-return AST");
+ let validator = exact_top_level_function(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &mutation,
+ "validate_generated_nip09_manifest_descriptor",
+ )
+ .expect("mutated predecessor descriptor validator");
+ let up_byte_length = validator
+ .block
+ .stmts
+ .iter()
+ .find(|statement| {
+ matches!(
+ statement,
+ syn::Stmt::Local(local)
+ if local_pattern_ident(&local.pat).as_deref()
+ == Some("up_byte_length")
+ )
+ })
+ .expect("mutated up-byte-length statement");
+ assert!(
+ validate_no_diverging_control_flow(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ "validate_generated_nip09_manifest_descriptor up_byte_length",
+ up_byte_length,
+ )
+ .is_err(),
+ "the structural divergence audit must reject an early return"
);
fs::write(&migrations_path, &source).expect("restore migration authority source");
}
@@ -16694,7 +17296,15 @@ pub(super) async fn apply_post_core_extensions_v2(
#[test]
fn hookless_future_migration_does_not_churn_nip09_v1_artifacts() {
let workspace = synthetic_workspace();
- let before = expected_artifacts(workspace.path()).expect("baseline artifacts");
+ let bundle_paths = [
+ MANIFEST_RELATIVE,
+ MANIFEST_SCHEMA_RELATIVE,
+ MANIFEST_SHA256_RELATIVE,
+ GENERATED_DESCRIPTOR_RELATIVE,
+ ];
+ let before = bundle_paths.map(|relative| {
+ read_regular_file(workspace.path(), relative).expect("baseline artifact")
+ });
write_nip09_reconciliation_manifest(workspace.path())
.expect("write baseline manifest bundle");
@@ -16704,14 +17314,14 @@ pub(super) async fn apply_post_core_extensions_v2(
fs::write(
workspace
.path()
- .join("crates/event_store/migrations/0003_future_probe.up.sql"),
+ .join("crates/event_store/migrations/0004_future_probe.up.sql"),
up_sql,
)
.expect("write future up migration");
fs::write(
workspace
.path()
- .join("crates/event_store/migrations/0003_future_probe.down.sql"),
+ .join("crates/event_store/migrations/0004_future_probe.down.sql"),
down_sql,
)
.expect("write future down migration");
@@ -16721,18 +17331,18 @@ pub(super) async fn apply_post_core_extensions_v2(
.join(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE);
let migrations = fs::read_to_string(&migrations_path).expect("migration registry source");
let migrations = migrations.replacen(
- "pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 2;",
"pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 3;",
+ "pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 4;",
1,
);
let registry_tail = " },\n];\n\npub(crate) fn migration_for_version";
let future_entry = format!(
r#" }},
EventStoreMigration {{
- version: 3,
+ version: 4,
name: "future_probe",
- up_sql: include_str!("../migrations/0003_future_probe.up.sql"),
- down_sql: include_str!("../migrations/0003_future_probe.down.sql"),
+ up_sql: include_str!("../migrations/0004_future_probe.up.sql"),
+ down_sql: include_str!("../migrations/0004_future_probe.down.sql"),
up_len: {},
down_len: {},
up_sha256: "{}",
@@ -16755,24 +17365,30 @@ pub(crate) fn migration_for_version"#,
);
let migrations = migrations.replacen(registry_tail, &future_entry, 1);
assert!(
- migrations.contains("version: 3"),
+ migrations.contains("version: 4"),
"future migration fixture must extend the registry"
);
fs::write(&migrations_path, migrations).expect("write future migration registry");
- let after = expected_artifacts(workspace.path()).expect("future-migration artifacts");
- assert_eq!(after.len(), before.len());
- for (before, after) in before.iter().zip(&after) {
- assert_eq!(after.relative, before.relative);
+ let registry = parse_canonical_production_rust(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &fs::read(&migrations_path).expect("future migration registry"),
+ )
+ .expect("future migration registry AST");
+ expected_event_store_migration_compiler_inputs(workspace.path(), ®istry)
+ .expect("versioned successor and isolated future compiler inputs");
+
+ for (relative, before) in bundle_paths.into_iter().zip(before) {
+ let after = read_regular_file(workspace.path(), relative).expect("future artifact");
assert_eq!(
- after.contents, before.contents,
+ after, before,
"{} must remain byte-identical for a hookless post-v2 migration",
- before.relative
+ relative
);
}
let future_up_path = workspace
.path()
- .join("crates/event_store/migrations/0003_future_probe.up.sql");
+ .join("crates/event_store/migrations/0004_future_probe.up.sql");
for malicious_sql in [
"INSERT INTO event_envelopes(raw_json) VALUES ('{}');\n",
"INSERT INTO 'event_envelopes'(raw_json) VALUES ('{}');\n",
@@ -16815,9 +17431,9 @@ pub(crate) fn migration_for_version"#,
fs::write(&future_up_path, malicious_sql).expect("write coupled future migration");
let error = validate_hookless_post_v2_migration_sql_isolated(
workspace.path(),
- 3,
+ 4,
"up",
- "crates/event_store/migrations/0003_future_probe.up.sql",
+ "crates/event_store/migrations/0004_future_probe.up.sql",
)
.expect_err("hookless future migration must not couple to v1 authority");
assert!(
@@ -16835,8 +17451,12 @@ pub(crate) fn migration_for_version"#,
#[test]
fn unrelated_future_product_sources_do_not_churn_nip09_v1_manifest() {
let workspace = synthetic_workspace();
- let before =
- describe_nip09_v1_manifest(workspace.path()).expect("baseline NIP-09 v1 manifest");
+ let before = read_regular_file(workspace.path(), MANIFEST_RELATIVE)
+ .expect("immutable NIP-09 v1 manifest");
+ validate_nip09_reconciliation_manifest(workspace.path())
+ .expect("baseline immutable predecessor bundle");
+ validate_privileged_store_authority(workspace.path())
+ .expect("baseline current privileged store authority");
let operational_listing_path = workspace
.path()
@@ -16908,11 +17528,10 @@ pub(crate) fn migration_for_version"#,
)
.expect("reexport future event-store query row");
- let after =
- describe_nip09_v1_manifest(workspace.path()).expect("future NIP-09 v1 manifest");
assert_eq!(
- canonical_json_bytes(&after).expect("future manifest bytes"),
- canonical_json_bytes(&before).expect("baseline manifest bytes"),
+ read_regular_file(workspace.path(), MANIFEST_RELATIVE)
+ .expect("unchanged immutable manifest"),
+ before,
"unrelated future product sources must not rotate the immutable NIP-09 v1 hook manifest"
);
let error = validate_privileged_store_authority(workspace.path())
@@ -17121,8 +17740,9 @@ pub(crate) fn migration_for_version"#,
#[test]
fn nip09_v1_manifest_is_independent_of_post_core_transport_evolution() {
let workspace = synthetic_workspace();
- let before =
- describe_nip09_v1_manifest(workspace.path()).expect("baseline NIP-09 v1 manifest");
+ let before = immutable_manifest();
+ let before_bytes = read_regular_file(workspace.path(), MANIFEST_RELATIVE)
+ .expect("immutable NIP-09 v1 manifest");
assert!(
before
@@ -17263,11 +17883,12 @@ version = "0.1.0"
)
.expect("extend future transport lock subgraph");
- let after =
- describe_nip09_v1_manifest(workspace.path()).expect("future NIP-09 v1 manifest");
+ validate_nip09_reconciliation_manifest(workspace.path())
+ .expect("transport evolution must not invalidate immutable predecessor artifacts");
assert_eq!(
- canonical_json_bytes(&after).expect("future manifest bytes"),
- canonical_json_bytes(&before).expect("baseline manifest bytes"),
+ read_regular_file(workspace.path(), MANIFEST_RELATIVE)
+ .expect("unchanged immutable manifest"),
+ before_bytes,
"post-core transport source, feature, and lock evolution must not rotate NIP-09 v1"
);
}
@@ -17751,7 +18372,7 @@ async fn nip09_reconciliation_v1_result_vector() {
validate_cargo_feature_profile_shape(workspace.path(), &profile)
.expect("empty marker feature shape");
- let mut manifest = describe_nip09_v1_manifest(workspace.path()).expect("governed manifest");
+ let mut manifest = immutable_manifest();
manifest.cargo_feature_profile = profile.clone();
validate_manifest_json_schema(
&manifest_schema(),
@@ -17793,10 +18414,8 @@ async fn nip09_reconciliation_v1_result_vector() {
#[test]
fn generated_descriptor_uses_rustfmt_stable_long_string_assignments() {
- let workspace = synthetic_workspace();
- let manifest =
- describe_nip09_v1_manifest(workspace.path()).expect("governed NIP-09 manifest");
- let manifest_bytes = canonical_json_bytes(&manifest).expect("canonical manifest");
+ let manifest = immutable_manifest();
+ let manifest_bytes = IMMUTABLE_MANIFEST_BYTES.to_vec();
let manifest_sha256 = sha256_hex(&manifest_bytes);
let descriptor = generated_descriptor(&manifest, &manifest_bytes, &manifest_sha256);
@@ -17841,6 +18460,7 @@ async fn nip09_reconciliation_v1_result_vector() {
#[test]
fn governed_compiler_inputs_reject_build_proc_macro_and_config_injection() {
let workspace = synthetic_workspace();
+ restore_predecessor_compiler_manifest(workspace.path());
validate_governed_compiler_inputs(workspace.path())
.expect("baseline governed compiler inputs");
@@ -18041,10 +18661,7 @@ async fn nip09_reconciliation_v1_result_vector() {
#[test]
fn draft_2020_12_schema_is_executed_against_the_manifest() {
- let workspace = synthetic_workspace();
- let manifest =
- serde_json::to_value(expected_manifest(workspace.path()).expect("expected manifest"))
- .expect("manifest value");
+ let manifest = serde_json::to_value(immutable_manifest()).expect("manifest value");
let schema = manifest_schema();
validate_manifest_json_schema(&schema, &manifest).expect("valid schema instance");
@@ -18087,7 +18704,7 @@ async fn nip09_reconciliation_v1_result_vector() {
#[test]
fn manifest_shape_rejects_generated_frozen_sources() {
let workspace = synthetic_workspace();
- let mut manifest = expected_manifest(workspace.path()).expect("expected manifest");
+ let mut manifest = immutable_manifest();
manifest.frozen_sources[0].path = GENERATED_DESCRIPTOR_RELATIVE.to_owned();
let error = validate_manifest_shape(workspace.path(), &manifest)
.expect_err("generated source must not be frozen");
diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs
@@ -19,6 +19,7 @@ fn usage() {
eprintln!(" cargo xtask contract validate");
eprintln!(" cargo xtask contract event-contract-registry-v7 [--write]");
eprintln!(" cargo xtask contract nip09-reconciliation-manifest [--write]");
+ eprintln!(" cargo xtask contract food-availability-projection-manifest [--write]");
eprintln!(" cargo xtask contract knowledge-manifest [--write]");
eprintln!(" cargo xtask dto-roots --check|--write");
eprintln!(" cargo xtask release preflight");
@@ -65,6 +66,7 @@ fn validate_contract() -> Result<(), String> {
.and_then(|_| contract::validate_canonical_event_boundary(&root))
.and_then(|_| contract::validate_event_contract_registry_v7_inventory(&root))
.and_then(|_| contract::validate_nip09_reconciliation_manifest(&root))
+ .and_then(|_| contract::validate_food_availability_projection_manifest(&root))
.and_then(|_| contract::validate_knowledge_contract_manifest(&root))
}
@@ -108,6 +110,16 @@ fn run_contract(args: &[String]) -> Result<(), String> {
"nip09-reconciliation-manifest accepts no arguments or exactly --write".to_string(),
),
},
+ Some("food-availability-projection-manifest") => match &args[1..] {
+ [] => contract::validate_food_availability_projection_manifest(&workspace_root()),
+ [flag] if flag == "--write" => {
+ contract::write_food_availability_projection_manifest(&workspace_root())
+ }
+ _ => Err(
+ "food-availability-projection-manifest accepts no arguments or exactly --write"
+ .to_string(),
+ ),
+ },
Some("knowledge-manifest") => {
if args.get(1).map(String::as_str) == Some("--write") {
contract::write_knowledge_contract_manifest(&workspace_root())
@@ -216,6 +228,12 @@ mod tests {
])
.expect_err("invalid NIP-09 manifest mode");
assert!(invalid_nip09.contains("exactly --write"));
+ let invalid_food = run_contract(&[
+ "food-availability-projection-manifest".to_string(),
+ "--invalid".to_string(),
+ ])
+ .expect_err("invalid FoodAvailability projection manifest mode");
+ assert!(invalid_food.contains("exactly --write"));
let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command");
assert!(unknown_root.contains("unknown command"));
@@ -311,6 +329,8 @@ mod tests {
.expect("contract registry-v7 inventory");
run_contract(&["nip09-reconciliation-manifest".to_string()])
.expect("contract NIP-09 reconciliation manifest");
+ run_contract(&["food-availability-projection-manifest".to_string()])
+ .expect("contract FoodAvailability projection manifest");
run_contract(&["knowledge-manifest".to_string()]).expect("contract knowledge manifest");
}
}