lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 5e91441795b5a838631af610a42d0ae367045bea
parent 08d7134408cd95c9d3fbda1aa60ae4f46682f5e4
Author: triesap <tyson@radroots.org>
Date:   Tue, 21 Jul 2026 22:57:56 +0000

event-store: add atomic raw-source rebuild

- rebuild visibility and Food projections from immutable envelopes
- support governed cold repair with rollback-safe maintenance authority
- bind raw and normalized state digests to executable contracts
- verify cursor, WAL, schema, and caller-state isolation

Diffstat:
MCHANGELOG.md | 34++++++++++++++++++++++++++++++++++
MCargo.lock | 1+
Mbuild/nix/common.nix | 5+++++
Acontracts/conformance/vectors/event_store/raw_source_rebuild.v1.json | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/releases/1.0.0-alpha.1.toml | 15+++++++++++++++
Mcrates/event_store/Cargo.toml | 3++-
Mcrates/event_store/README | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Acrates/event_store/contracts/raw_source_rebuild_v1.manifest.json | 1301+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json | 655+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256 | 1+
Mcrates/event_store/src/error.rs | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/event_store/src/generated.rs | 1+
Acrates/event_store/src/generated/raw_source_rebuild_manifest.rs | 18++++++++++++++++++
Mcrates/event_store/src/lib.rs | 18+++++++++++-------
Mcrates/event_store/src/model.rs | 5+++++
Acrates/event_store/src/model/raw_source_rebuild_v1.rs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event_store/src/nip09/reconciliation_v1.rs | 667+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Acrates/event_store/src/nip09/reconciliation_v1/raw_source_rebuild.rs | 1418+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs | 940+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event_store/src/schema.rs | 184++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/event_store/src/store.rs | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mcrates/event_store/src/store/food_availability_projection_v1.rs | 42+++++++++++++++++++++++++++++++++++++++++-
Acrates/event_store/src/store/raw_source_rebuild_v1_tests.rs | 2534+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_store/tests/fixtures/raw_source_rebuild.v1.json | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_store/tests/raw_source_rebuild_v1_result_vector.rs | 656+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/Cargo.toml | 4++++
Mtools/xtask/src/contract.rs | 16++++++++++++++--
Mtools/xtask/src/contract/food_availability_projection.rs | 10++++++----
Mtools/xtask/src/contract/nip09_reconciliation.rs | 619+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Atools/xtask/src/contract/raw_source_rebuild.rs | 7433+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/contract/source_maintenance.rs | 1455++++++-------------------------------------------------------------------------
Mtools/xtask/src/main.rs | 18++++++++++++++++++
32 files changed, 17787 insertions(+), 1735 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -137,6 +137,40 @@ publish policy both pass for the same source revision. `RadrootsEventStoreReconciliationResource` type and `ReconciliationCapacityExceeded` error variant are replaced by the versioned source-capacity resource and typed capacity/history errors. +<!-- release-change: event-store-raw-source-rebuild-authority --> +- Event-store schema v4 now exposes a versioned raw-source rebuild operation + that repairs governed derived NIP-09, current-visibility, and focused + FoodAvailability state from reverified immutable raw envelopes in one + `BEGIN IMMEDIATE` transaction. The typed report returns prior and new source + generations, the rebound capacity/high-water seal, a domain-separated + immutable-raw digest, and a generation-normalized active-product digest. + Rebuild drift exposes six stable typed authority categories while retaining + non-contractual diagnostic detail for operators. + The file-only repair entry point requires an existing exact managed-v4 WAL + database and returns a store only after repaired state commits. It creates a + deterministic single-connection pool and proves a fresh canonical-path + connection shares the validated SQLite writer-lock domain. Callers must + quiesce every alias, independent pool, direct SQL user, and filesystem path, + symlink, or file-replacement operation for the repair duration. The public + event-store error enum is now non-exhaustive so future typed recovery errors + do not repeatedly break downstream matches. + Rebuild preserves unrelated caller-owned tables with no schema dependency on + any rebuild-mutated parent, generic projection cursors, and unrelated SQLite + sequence row triples. Transition replay performs one shared + target-alias cleanup, places the governed target first, and validates that + exact row after replay; generic cursor inventory is instead + prospectively bounded at 4,096 identities and invalidates lazily after a + generation change. + Before entropy or mutation, rebuild also bounds caller-owned main tables and + cumulative foreign-key rows at 4,096 each and refuses every caller-owned + inbound foreign key to every directly or indirectly mutated parent, + regardless of its SQLite action, so derived replacement cannot cascade into + caller rows or triggers. The sealed parent inventory includes the Food FTS5 + virtual table and all five shadows plus the governed `sqlite_sequence` row; + it remains separate from the narrower scoped-integrity inventory. + The executable raw-rebuild successor contract freezes the SourceMaintenance + predecessor and the `0001` through `0004` migration inventory; no schema + migration is added. - Bare-envelope replica ingestion is quarantined behind the explicit, non-default `legacy-ingest` feature. Default replica APIs expose emit and sync surfaces only; a future product ingest boundary must consume a store-produced diff --git a/Cargo.lock b/Cargo.lock @@ -4593,6 +4593,7 @@ dependencies = [ name = "radroots_event_store" version = "1.0.0-alpha.1" dependencies = [ + "futures", "getrandom 0.2.17", "hex", "nostr", diff --git a/build/nix/common.nix b/build/nix/common.nix @@ -19,8 +19,13 @@ let ../../.cargo ../../Cargo.toml ../../Cargo.lock + ../../flake.lock ../../CHANGELOG.md ../../README + ../../flake.nix + ../../build/nix/apps.nix + ../../build/nix/common.nix + ../../build/nix/toolchains.nix ../../dto_bindgen.toml ../../rust-toolchain.toml ../../contracts diff --git a/contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json b/contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json @@ -0,0 +1,462 @@ +{ + "schema_version": 1, + "contract_id": "radroots_event_store.raw_source_rebuild_v1", + "delegated_suite": { + "id": "radroots_event_store.raw_source_rebuild_v1.delegated_rust_test_suite.v1", + "lane": "nix run .#contract", + "package": "radroots_event_store", + "authorities": [ + { + "authority": "raw_source_rebuild_incremental_reopen_and_repeat_parity_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "projection_cursor_capacity_accepts_exact_and_rejects_one_over_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_invalidates_generic_cursors_without_enumerating_or_mutating_them_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_normalizes_only_transition_sqlite_sequence_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_rejects_unrelated_minimum_transition_sequence_rowid_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_reuses_target_alias_at_minimum_sequence_rowid_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_repairs_active_transition_high_water_metadata_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_repairs_empty_transition_high_water_metadata_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_repairs_derived_drift_and_refuses_raw_drift_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_refuses_transition_history_gap_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_refuses_historical_generation_lineage_corruption_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_rollback_failure_preserves_primary_and_rollback_errors_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_wal_readers_observe_only_committed_generation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_rejects_caller_inbound_foreign_keys_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_caller_schema_inventory_limits_are_typed_and_atomic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_scoped_integrity_preserves_caller_state_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_generation_exhaustion_precedes_entropy_and_mutation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_entropy_failure_is_atomic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_empty_source_without_transitions_is_deterministic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_cold_file_repair_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_repair_preflights_reject_bounded_authority_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_repair_rejects_delete_mode_exact_v4_without_mutation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_repair_rejects_canonical_path_lock_domain_mismatch_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_repair_post_preflight_failures_preserve_wal_and_state_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_snapshot_visibility_oracle_covers_regular_replaceable_addressable_and_deletion_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" + }, + { + "authority": "raw_snapshot_visibility_oracle_matches_wide_event_and_address_requests_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" + }, + { + "authority": "raw_snapshot_visibility_oracle_matches_all_protocol_decision_branches_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" + }, + { + "authority": "raw_snapshot_visibility_oracle_is_order_and_repeat_invariant_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" + } + ] + }, + "cases": [ + { + "id": "empty_source_repeat_digest_parity", + "execution": "direct_executor", + "authority": "raw_source_rebuild_v1_result_vector", + "authority_path": "crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs", + "expected_outcome": "two committed rebuilds rotate generations while preserving zero capacity, raw high-water, immutable-raw digest, and normalized product digest", + "expected_immutable_raw_digest": "73e66ea95452e902176d701311e6e82b3cd7895ae77f3d73fd1cbb67bcf9d321", + "expected_active_product_state_digest": "bf20fc2ba0e7c64bb0958829e118d87a86efe17e2848bb098d3d9ab2a78c2245" + }, + { + "id": "signed_food_fixture_typed_digest_parity", + "execution": "direct_executor", + "authority": "raw_source_rebuild_v1_result_vector", + "authority_path": "crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs", + "expected_outcome": "one signed admitted FoodAvailability fixture freezes exact immutable-raw and generation-normalized product digests across text, i64, boolean, optional, and blob framing and preserves them across repeated rebuild", + "expected_immutable_raw_digest": "336a6a6cf1d84b0fcb185c4a7550cf5a8d8047c5a3f89df40e0d8f1ead543c68", + "expected_active_product_state_digest": "1ed8a22036091f0a492f3848027b313be4ef1202a9cdfa6c3ff35e12ab10f15c" + }, + { + "id": "incremental_reopen_repeat_product_parity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_incremental_reopen_and_repeat_parity_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "incremental, file reopen, first rebuild, and repeated rebuild expose identical generation-normalized current visibility, Food image, and logical FTS product witnesses", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "projection_cursor_capacity_exact_and_one_over", + "execution": "delegated_rust_test", + "authority": "projection_cursor_capacity_accepts_exact_and_rejects_one_over_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "4,096 unique generic cursors are accepted, the next unique insert and a 4,097-row migration/reconciliation inventory probe fail typed, and an existing identity at capacity remains updateable", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "generic_cursor_lazy_generation_invalidation", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_invalidates_generic_cursors_without_enumerating_or_mutating_them_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "generic cursor rows remain byte-identical and become invalid only through active-generation mismatch", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "target_first_transition_sequence_normalization", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_normalizes_only_transition_sqlite_sequence_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "missing, low, high, duplicate, and case-aliased target rows normalize once to a canonical target-first row at the retained transition maximum while every unrelated sqlite_sequence row triple remains unchanged", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "unrelated_minimum_transition_sequence_rowid_refusal", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_rejects_unrelated_minimum_transition_sequence_rowid_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "an unrelated sqlite_sequence row at the minimum SQLite rowid exhausts target-first placement and rejects atomically", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "minimum_target_alias_sequence_reuse", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_reuses_target_alias_at_minimum_sequence_rowid_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "a case-aliased target already at the minimum SQLite rowid is reused, canonicalized, and advanced to the replay high-water", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "active_transition_high_water_metadata_repair", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_repairs_active_transition_high_water_metadata_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "low and high active transition high-water metadata drift repairs to the exact retained transition maximum while preserving immutable-raw, normalized product, and logical product parity across repeat rebuild", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "empty_transition_high_water_metadata_repair", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_repairs_empty_transition_high_water_metadata_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "nonzero active transition high-water metadata on an empty source repairs to zero while preserving immutable-raw, normalized product, and logical product parity across reopen validation and repeat rebuild", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "derived_repair_and_raw_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_repairs_derived_drift_and_refuses_raw_drift_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "managed-v4 derived corruption is repaired, while immutable raw, capacity, governed catalog, or migration-ledger drift is refused before mutation", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "transition_history_gap_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_refuses_transition_history_gap_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "a retained transition-history gap is refused as addressable-transition authority drift before any rebuild-owned state mutates", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "historical_generation_lineage_corruption_refusal", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_refuses_historical_generation_lineage_corruption_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "historical generation baselines and generation-bound transition lineage are authenticated before mutation, and any mismatch is refused atomically", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_marker_open", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_marker_open restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_generation_rotation", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_generation_rotation restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_core_replay", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_core_replay restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_visibility_audit", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_visibility_audit restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_food_reset_replay", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_food_reset_replay restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_food_audit", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_food_audit restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_marker_close", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_marker_close restores the exact prior committed database and leaves no marker residue", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_failure_preserves_both_errors", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_rollback_failure_preserves_primary_and_rollback_errors_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "the typed rollback error preserves both the primary rebuild failure and the SQL rollback failure", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "wal_reader_commit_visibility", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_wal_readers_observe_only_committed_generation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "concurrent WAL readers observe only the prior committed generation until rebuild commit", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "caller_inbound_foreign_key_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_rejects_caller_inbound_foreign_keys_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "caller-owned inbound foreign keys to directly or indirectly mutated parents are rejected before entropy or mutation; representative managed-parent cases cover CASCADE, SET NULL, SET DEFAULT, RESTRICT, and NO ACTION, while explicit parent-inventory cases cover the Food FTS5 virtual table, all five shadows, and sqlite_sequence, preserving caller rows, schema, triggers, side effects, and rebuild authority", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "caller_schema_inventory_capacity_exact_and_one_over", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_caller_schema_inventory_limits_are_typed_and_atomic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "bounded caller main-table and cumulative foreign-key-row inventories accept their exact limits and return typed atomic refusal one row over before entropy or mutation", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "scoped_integrity_preserves_caller_state", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_scoped_integrity_preserves_caller_state_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "unrelated caller rows, indices, foreign-key violations, and AUTOINCREMENT counters with no dependency on rebuild-owned tables remain outside rebuild authority and byte-identical", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "generation_exhaustion_preflight", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_generation_exhaustion_precedes_entropy_and_mutation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "the ninth retained generation fails before entropy, marker, sequence, raw, or derived mutation", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "generation_entropy_failure_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_entropy_failure_is_atomic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "source-generation entropy failure returns the typed error before marker, sequence, raw, or derived mutation", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "empty_source_without_transitions", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_empty_source_without_transitions_is_deterministic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "an empty source and absent target transition sequence rebuild deterministically without creating unrelated sequence state", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_file_repair", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_cold_file_repair_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "maintenance-only file repair restores exact managed-v4 derived corruption through a fresh governed connection while refusing nonexistent, unmanaged, and non-v4 databases without weakening ordinary constructors", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_bounded_preflight_authority_drift_refusal", + "execution": "delegated_rust_test", + "authority": "raw_source_repair_preflights_reject_bounded_authority_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "bounded catalog, migration-history, temporary-schema, and encoding preflights refuse authority drift without creating rebuild state or changing persistent database authority", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_non_wal_file_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_repair_rejects_delete_mode_exact_v4_without_mutation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "cold repair requires an existing WAL database and rejects an exact managed-v4 DELETE-mode file without changing journal mode or governed state", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_canonical_path_lock_domain_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_repair_rejects_canonical_path_lock_domain_mismatch_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "cold repair proves the caller path shares the validated SQLite writer-lock domain and rejects a mismatched canonical path without mutating either database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_post_preflight_failure_wal_state_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_repair_post_preflight_failures_preserve_wal_and_state_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "raw reconciliation and source-capacity failures after cold preflight preserve the exact prior rebuild-owned state and WAL journal mode", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "pure_raw_snapshot_visibility_oracle", + "execution": "delegated_rust_test", + "authority": "raw_snapshot_visibility_oracle_covers_regular_replaceable_addressable_and_deletion_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "expected_outcome": "the independent pure oracle covers regular, replaceable, addressable, empty-identifier replaceable address targets, and kind-5 visibility without consulting derived SQL views", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "direct_indexed_visibility_oracle_wide_targets", + "execution": "delegated_rust_test", + "authority": "raw_snapshot_visibility_oracle_matches_wide_event_and_address_requests_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "expected_outcome": "one wide deletion request is reduced once into direct indexed event and address evidence whose per-target decisions exactly match the frozen NIP-09 evaluator without projection rescans", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "direct_indexed_visibility_oracle_protocol_matrix", + "execution": "delegated_rust_test", + "authority": "raw_snapshot_visibility_oracle_matches_all_protocol_decision_branches_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "expected_outcome": "the direct indexed reducer exactly matches all seven frozen NIP-09 outcomes, preserves stale and winning evidence, and applies authorized-evidence precedence over mismatches", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "direct_indexed_visibility_oracle_order_repeat_invariance", + "execution": "delegated_rust_test", + "authority": "raw_snapshot_visibility_oracle_is_order_and_repeat_invariant_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "expected_outcome": "canonical exact-event and address evidence is invariant under reversed request order and repeated admitted requests, including cutoff ties", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + } + ] +} diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml @@ -420,6 +420,21 @@ semver_impacts = [ summary = "Advance the event store to schema version 4 with prospective retained-source capacity enforcement across independent file pools, UTF-8 preflight before schema or journal mutation, bounded reopen recounts, rollback-protected finite generation history, coherent NIP-09 and FoodAvailability rebuild seals, typed capacity and recovery failures, and an authenticated executable SourceMaintenance successor contract that replaces exactly radroots_event_store_food_availability_image_delete_guard, radroots_event_store_food_availability_projection_delete_guard, and radroots_event_store_source_rebuild_marker_insert_guard; rejects drifted v3 upgrades atomically; restores the exact predecessor trigger SQL on rollback; and reserves future derived-state repair for an exact managed-v4 catalog, ledger, migration history, immutable raw/source lineage, and capacity without requiring derived hook health as a precondition." [[changes]] +id = "event-store-raw-source-rebuild-authority" +classification = "breaking" +semver_impacts = [ + "add_exported_type", + "add_exported_function", + "add_exported_constant", + "add_exported_field", + "add_enum_variant", + "add_conformance_vector", + "change_exported_enum_variant", + "change_exported_algorithm_behavior", +] +summary = "Add an authenticated managed-v4 raw-source rebuild and file-only cold-repair authority with stable typed drift categories, serialized generation rotation, independent immutable-raw visibility audit, typed generation-normalized product-state digests, bounded generic projection cursors, a bounded caller-schema dependency preflight over every directly or indirectly mutated parent including the full Food FTS5 table family and sqlite_sequence, target-first transition sequence normalization, separately scoped integrity checks, exact rollback failpoints, a canonical-path SQLite lock-domain probe, deterministic crate-owned connection policy, and an executable successor contract while freezing the SourceMaintenance predecessor and migration inventory." + +[[changes]] id = "transport-event-outcomes-and-replica-quarantine" classification = "breaking" semver_impacts = [ diff --git a/crates/event_store/Cargo.toml b/crates/event_store/Cargo.toml @@ -13,7 +13,7 @@ readme = "README" [features] default = ["sqlite", "runtime-tokio"] -sqlite = ["dep:getrandom", "dep:sqlx", "sqlx/sqlite-bundled"] +sqlite = ["dep:futures", "dep:getrandom", "dep:sqlx", "sqlx/sqlite-bundled"] runtime-tokio = ["sqlx/runtime-tokio"] [dependencies] @@ -31,6 +31,7 @@ radroots_event_codec = { workspace = true, default-features = false, features = ] } radroots_transport = { workspace = true, default-features = false } hex = { workspace = true } +futures = { workspace = true, optional = true } getrandom = { workspace = true, optional = true, features = ["std"] } serde = { workspace = true, features = ["std"] } serde_json = { workspace = true, features = ["std"] } diff --git a/crates/event_store/README b/crates/event_store/README @@ -179,9 +179,19 @@ history access explicitly to SQLite's `main` database and reject governed temporary-schema collisions on the connection before authority reads or mutation. Reconciliation and migration integrity checks fail on foreign-key violations whose child table is declared as event-store-owned or uses the -reserved namespace. Violations in unrelated shared-schema child tables remain -caller-owned and visible through SQLite's full `foreign_key_check`; they do not -block event-store migration or source rebuild. +reserved namespace. Violations in unrelated shared-schema child tables with no +dependency on event-store-owned tables remain caller-owned and do not block +event-store migration or source rebuild. Raw-source rebuild bounds its +caller-owned main-table and cumulative foreign-key-row inventories at 4,096 +each, then rejects every caller-owned inbound foreign key to the exact +rebuild-mutated parent set before generation entropy or mutation. This refusal +applies regardless of whether the declared action is `CASCADE`, `SET NULL`, +`SET DEFAULT`, +`RESTRICT`, or `NO ACTION`, preventing caller rows and their triggers from +being changed as a side effect of derived-state replacement. The dependency +inventory covers direct mutations, Food FTS5's virtual table and all five +shadow tables, and the transition row in `sqlite_sequence`; this inventory is +separate from the narrower post-rebuild scoped-integrity table set. Migration `0002_nip09` re-verifies the frozen raw JSON and immutable columns, rebuilds derived registry-v7 admission and raw-head facts, then creates one @@ -192,15 +202,62 @@ 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. Repair of -derived transition high-water or Food projection state is authorized only -inside that future rebuild after the exact managed-v4 catalog, ledger, and -migration history plus immutable raw/source lineage and capacity validate. -Derived hook state is the repair target, not a repair precondition. A drifted -managed-v3 database is rejected atomically before v4 changes begin; schema -upgrade is not a repair path for corrupt v3 authority. +generation and validated on open. `rebuild_from_raw_v1` is the supported +current-schema maintenance operation. It validates an exact managed-v4 catalog, +ledger, migration history, immutable raw/source lineage, and capacity without +requiring derived hook health; derived NIP-09, current-visibility, transition, +and FoodAvailability state is the repair target. The serialized transaction +reverifies raw NIP-01 envelopes, appends a fresh generation, rebuilds and audits +core visibility through an independent raw-snapshot oracle, explicitly resets +and replays Food rows, images, cursor, and FTS, then closes the guarded marker +and commits. `repair_file_from_raw_v1` provides the same operation for cold +derived-state corruption without weakening ordinary constructors; the file +path must already exist and no migration is attempted. Cold repair requires +every store alias, independent pool, and direct SQL user of the database to +remain quiesced. The canonical path, symlink targets, and file replacement or +rename operations must also remain quiesced for the repair duration. The file +must already use WAL journal mode; repair validates +this with a read-only query and never changes journal mode on the validation +connection. The API does not accept caller-provided pools because their +connection callbacks and session state cannot be sealed. It instead +canonicalizes the file identity and creates a fresh governed single-connection +pool with deterministic future options. After validating that connection, it +reserves the writer transaction. A second independent connection must resolve +to the same canonical identity and fail a write probe against that reservation, +proving the path shares the same SQLite lock domain before rebuild proceeds in +the original validated transaction. Live in-memory stores use +`rebuild_from_raw_v1`. +A drifted managed-v3 database is rejected atomically before v4 changes begin; +schema upgrade is not a repair path for corrupt v3 authority. +`RawSourceRebuildStateDrift` exposes a +`RadrootsEventStoreRawSourceRebuildDriftV1` category whose `code()` value is +stable for programmatic handling: `managed_schema_authority`, +`immutable_raw_authority`, `source_generation_lineage`, +`addressable_transition_authority`, `derived_product_state_authority`, or +`rebuild_postcondition`. Its `detail` text is non-contractual diagnostic +context and must not be parsed by callers. + +The rebuild report contains prior and new source generations, the rebound +capacity and raw high-water seal, a domain-separated digest of ordered immutable +raw rows, and a generation-normalized digest of active product state. Repeated +rebuilds preserve both logical digests while rotating generation identity. +Every successful rebuild irreversibly appends one of the eight retained source +generations and can return `SourceGenerationHistoryLimitReached`; generation +rotation invalidates generic projection cursors lazily and rebinds the governed +FoodAvailability cursor in the same transaction. +Only the `radroots_event_store_addressable_head_transition` SQLite sequence is +normalized. One case-insensitive target-alias cleanup is the sole shared +sequence-table scan; the canonical target row is placed first so transition +replay does not repeatedly scan unrelated AUTOINCREMENT rows, then that exact +row is validated after replay. Every unrelated sequence row triple is +preserved. Unrelated caller-owned tables, indices, foreign-key violations, +generic projection cursors, and AUTOINCREMENT rows that have no schema +dependency on a rebuild-mutated parent remain outside rebuild authority. Scoped +post-rebuild integrity checks cover only event-store-owned tables and indices +plus the owned Food FTS table. Caller-schema inventory excess returns a typed +capacity refusal; a bounded inbound dependency returns its caller table, +foreign-key id and sequence, columns, managed parent, and SQLite actions in a +typed error. Every pending rebuild-bound migration, including `0002_nip09` and `0003_food_availability_projection`, preflights and then rechecks under the @@ -265,6 +322,15 @@ returns a cursor bound to the active source generation. mismatch, sequence regression, a sequence beyond the raw high-water mark, and conflicting writers. +Supported APIs and migrations enforce a 4,096-identity bound for caller-owned +generic projection cursors. Prospective insertion of a new identity fails with +`ProjectionCursorCapacityExceeded` before cursor mutation at that boundary; +updating an existing identity remains valid. Migration-time reconciliation +inventory validation uses bounded cap-plus-one probes rather than an unbounded +cursor inventory scan. Ordinary current-v4 reopen and raw-source rebuild never +enumerate or mutate this inventory. Existing cursors invalidate lazily when +their source generation no longer matches the active generation. + A consumer that needs a different projection version or encounters an unbound legacy cursor must call `prepare_projection_cursor_rebuild`, rebuild derived state through the ticket's target raw high-water sequence, and pass diff --git a/crates/event_store/contracts/raw_source_rebuild_v1.manifest.json b/crates/event_store/contracts/raw_source_rebuild_v1.manifest.json @@ -0,0 +1,1301 @@ +{ + "schema_version": 1, + "contract_id": "radroots_event_store.raw_source_rebuild_v1", + "authority_id": "raw_source_rebuild_v1", + "manifest_schema": { + "path": "crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json", + "byte_length": 17896, + "sha256": "f9d210967e54b66f39c8bb965d97b2001a0ebc0927e7c2c14edb8e474bfda695", + "hash_algorithm": "sha256_bytes_v1" + }, + "predecessor": { + "contract_id": "radroots_event_store.source_maintenance_v1", + "manifest": { + "path": "crates/event_store/contracts/source_maintenance_v1.manifest.json", + "byte_length": 14216, + "sha256": "e8911e6e5710278969cbd15557a5b856b1575dfd11a655711403598370b41221", + "hash_algorithm": "sha256_bytes_v1" + } + }, + "migration_inventory": [ + { + "path": "crates/event_store/migrations/0001_event_store.down.sql", + "byte_length": 522, + "sha256": "fa84d587f657f601947eaeb9cd239c962a48f6fcdce723588476e8d22f3c1f53", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_store/migrations/0001_event_store.up.sql", + "byte_length": 10712, + "sha256": "4c03906a1cffd418a48d40907aa9a1ca51bb41766cff7250c4dfc7c2fd6eddde", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_store/migrations/0002_nip09.down.sql", + "byte_length": 4807, + "sha256": "c51a099d9501f1e692c13d2226296a68ed9e6bfa5e8e46b2f12c6574dbe59e31", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_store/migrations/0002_nip09.up.sql", + "byte_length": 81614, + "sha256": "0c1730ff36eaebd285f9c0c94b9b7346af60266afa55c24a18e30446d369581a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_store/migrations/0003_food_availability_projection.down.sql", + "byte_length": 1755, + "sha256": "29d663320109d9dd0df6a00b6a53d8d988438d01f7a66960a9d4ba3482ffffb8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_store/migrations/0003_food_availability_projection.up.sql", + "byte_length": 23683, + "sha256": "4e7edfb981b25f76055efc7802ec30b4034eeae9b9c0809ea4ea7c574678748a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_store/migrations/0004_source_maintenance.down.sql", + "byte_length": 5172, + "sha256": "fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_store/migrations/0004_source_maintenance.up.sql", + "byte_length": 19841, + "sha256": "425dc799f392b87f265a6fb81f89c4a1c7a5db8391ab0380970708cb9c66704d", + "hash_algorithm": "sha256_bytes_v1" + } + ], + "runtime": { + "event_store_schema_version": 4, + "event_contract_registry_version": 7, + "transaction_mode": "begin_immediate_v1", + "projection_cursor_count_limit": 4096, + "projection_cursor_rejection_probe_limit": 4097, + "caller_main_table_count_limit": 4096, + "caller_foreign_key_row_count_limit": 4096, + "caller_inbound_foreign_key_policy": "reject_all_rebuild_mutated_parent_dependencies_before_entropy_v1", + "caller_inbound_foreign_key_parent_tables": [ + "event_envelopes", + "event_envelope_tags", + "event_envelope_head", + "radroots_event_store_source_generation", + "radroots_event_store_source_rebuild_commit_barrier", + "radroots_event_store_source_rebuild_marker", + "radroots_event_store_source_state", + "radroots_event_store_write_lock", + "radroots_event_store_source_capacity_v1", + "radroots_event_store_event_coordinate", + "radroots_event_store_nip09_request", + "radroots_event_store_nip09_event_target", + "radroots_event_store_nip09_address_target", + "radroots_event_store_addressable_head_state", + "radroots_event_store_addressable_head_transition", + "radroots_event_store_addressable_feed_integrity_v1", + "radroots_event_store_food_availability_cursor", + "radroots_event_store_food_availability_projection", + "radroots_event_store_food_availability_image", + "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", + "sqlite_sequence" + ], + "cold_repair_mode": "canonical_file_only_single_connection_lock_domain_probe_v1", + "immutable_raw_digest": { + "algorithm": "sha256_domain_nul_typed_fields_v1", + "domain_utf8": "radroots:event-store:immutable-raw-digest:v1", + "domain_terminator": "nul_byte", + "framing": { + "section": "S_then_N_then_u64be_length_then_utf8_name", + "row": "R", + "signed_i64": "I_then_i64be", + "boolean": "B_then_u8_0_or_1", + "optional": "O_then_presence_u8_then_nested_value_when_present", + "text": "T_then_u64be_length_then_utf8_bytes", + "blob": "X_then_u64be_length_then_bytes" + }, + "output_bytes": 32, + "source_queries": [ + { + "section": "event_envelopes", + "sql": "SELECT seq, event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, inserted_at_ms FROM event_envelopes ORDER BY seq", + "fields": [ + { + "name": "seq", + "framing": "i64" + }, + { + "name": "event_id", + "framing": "text" + }, + { + "name": "pubkey", + "framing": "text" + }, + { + "name": "created_at", + "framing": "i64" + }, + { + "name": "kind", + "framing": "i64" + }, + { + "name": "tags_json", + "framing": "text" + }, + { + "name": "content", + "framing": "text" + }, + { + "name": "sig", + "framing": "text" + }, + { + "name": "raw_json", + "framing": "text" + }, + { + "name": "inserted_at_ms", + "framing": "i64" + } + ] + }, + { + "section": "event_envelope_tags", + "sql": "SELECT event.seq, tag.event_id, tag.tag_index, tag.tag_name, tag.tag_value, tag.tag_json FROM event_envelope_tags AS tag JOIN event_envelopes AS event ON event.event_id = tag.event_id ORDER BY event.seq, tag.tag_index", + "fields": [ + { + "name": "seq", + "framing": "i64" + }, + { + "name": "event_id", + "framing": "text" + }, + { + "name": "tag_index", + "framing": "i64" + }, + { + "name": "tag_name", + "framing": "text" + }, + { + "name": "tag_value", + "framing": "optional_text" + }, + { + "name": "tag_json", + "framing": "text" + } + ] + } + ] + }, + "active_product_state_digest": { + "algorithm": "sha256_domain_nul_typed_fields_v1", + "domain_utf8": "radroots:event-store:active-product-state-digest:v1", + "domain_terminator": "nul_byte", + "framing": { + "section": "S_then_N_then_u64be_length_then_utf8_name", + "row": "R", + "signed_i64": "I_then_i64be", + "boolean": "B_then_u8_0_or_1", + "optional": "O_then_presence_u8_then_nested_value_when_present", + "text": "T_then_u64be_length_then_utf8_bytes", + "blob": "X_then_u64be_length_then_bytes" + }, + "output_bytes": 32, + "components": [ + "logical_current_classifications", + "raw_heads", + "active_addressable_head_state", + "active_nip09_facts", + "current_visibility", + "food_availability_rows", + "food_availability_images", + "logical_food_fts_rows", + "stable_food_cursor_metadata" + ], + "exclusions": [ + "source_generation", + "absolute_transition_sequence", + "transition_history", + "rebuild_origin", + "rebuild_cause", + "operational_timestamps", + "generic_projection_cursors", + "caller_owned_state" + ], + "component_queries": [ + { + "section": "envelope_classification", + "sql": "SELECT event_id, verification_status, contract_status, contract_id, event_class, projection_eligible FROM event_envelopes ORDER BY event_id", + "fields": [ + { + "name": "event_id", + "framing": "text" + }, + { + "name": "verification_status", + "framing": "text" + }, + { + "name": "contract_status", + "framing": "text" + }, + { + "name": "contract_id", + "framing": "optional_text" + }, + { + "name": "event_class", + "framing": "optional_text" + }, + { + "name": "projection_eligible", + "framing": "boolean" + } + ] + }, + { + "section": "tag_classification", + "sql": "SELECT event_id, tag_index, contract_semantic, contract_value_type, relay_indexed FROM event_envelope_tags ORDER BY event_id, tag_index", + "fields": [ + { + "name": "event_id", + "framing": "text" + }, + { + "name": "tag_index", + "framing": "i64" + }, + { + "name": "contract_semantic", + "framing": "optional_text" + }, + { + "name": "contract_value_type", + "framing": "optional_text" + }, + { + "name": "relay_indexed", + "framing": "boolean" + } + ] + }, + { + "section": "raw_heads", + "sql": "SELECT coordinate_type, kind, pubkey, d_tag, event_id, created_at FROM event_envelope_head ORDER BY coordinate_type, kind, pubkey, d_tag", + "fields": [ + { + "name": "coordinate_type", + "framing": "text" + }, + { + "name": "kind", + "framing": "i64" + }, + { + "name": "pubkey", + "framing": "text" + }, + { + "name": "d_tag", + "framing": "optional_text" + }, + { + "name": "event_id", + "framing": "text" + }, + { + "name": "created_at", + "framing": "i64" + } + ] + }, + { + "section": "event_coordinates", + "sql": "SELECT event_id, coordinate_type, kind, pubkey, created_at, admission_status, admission_code, contract_id, raw_d_tag, nip09_matchable, nip09_d_tag FROM radroots_event_store_event_coordinate WHERE source_generation = ? ORDER BY event_id", + "fields": [ + { + "name": "event_id", + "framing": "text" + }, + { + "name": "coordinate_type", + "framing": "text" + }, + { + "name": "kind", + "framing": "i64" + }, + { + "name": "pubkey", + "framing": "text" + }, + { + "name": "created_at", + "framing": "i64" + }, + { + "name": "admission_status", + "framing": "text" + }, + { + "name": "admission_code", + "framing": "optional_text" + }, + { + "name": "contract_id", + "framing": "optional_text" + }, + { + "name": "raw_d_tag", + "framing": "text" + }, + { + "name": "nip09_matchable", + "framing": "boolean" + }, + { + "name": "nip09_d_tag", + "framing": "optional_text" + } + ] + }, + { + "section": "nip09_requests", + "sql": "SELECT request_event_id, request_pubkey, request_created_at FROM radroots_event_store_nip09_request WHERE source_generation = ? ORDER BY request_event_id", + "fields": [ + { + "name": "request_event_id", + "framing": "text" + }, + { + "name": "request_pubkey", + "framing": "text" + }, + { + "name": "request_created_at", + "framing": "i64" + } + ] + }, + { + "section": "nip09_event_targets", + "sql": "SELECT request_event_id, target_event_id, source_tag_index, source_tag_value FROM radroots_event_store_nip09_event_target WHERE source_generation = ? ORDER BY request_event_id, target_event_id, source_tag_index", + "fields": [ + { + "name": "request_event_id", + "framing": "text" + }, + { + "name": "target_event_id", + "framing": "text" + }, + { + "name": "source_tag_index", + "framing": "i64" + }, + { + "name": "source_tag_value", + "framing": "text" + } + ] + }, + { + "section": "nip09_address_targets", + "sql": "SELECT request_event_id, target_kind, target_pubkey, target_d_tag, inclusive_cutoff, source_tag_index, source_tag_value, source_kind_text, source_pubkey_text, source_d_tag FROM radroots_event_store_nip09_address_target WHERE source_generation = ? ORDER BY request_event_id, target_kind, target_pubkey, target_d_tag, source_tag_index", + "fields": [ + { + "name": "request_event_id", + "framing": "text" + }, + { + "name": "target_kind", + "framing": "i64" + }, + { + "name": "target_pubkey", + "framing": "text" + }, + { + "name": "target_d_tag", + "framing": "text" + }, + { + "name": "inclusive_cutoff", + "framing": "i64" + }, + { + "name": "source_tag_index", + "framing": "i64" + }, + { + "name": "source_tag_value", + "framing": "text" + }, + { + "name": "source_kind_text", + "framing": "text" + }, + { + "name": "source_pubkey_text", + "framing": "text" + }, + { + "name": "source_d_tag", + "framing": "text" + } + ] + }, + { + "section": "addressable_heads", + "sql": "SELECT kind, pubkey, d_tag, raw_head_event_id, 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_state WHERE source_generation = ? ORDER BY kind, pubkey, d_tag", + "fields": [ + { + "name": "kind", + "framing": "i64" + }, + { + "name": "pubkey", + "framing": "text" + }, + { + "name": "d_tag", + "framing": "text" + }, + { + "name": "raw_head_event_id", + "framing": "text" + }, + { + "name": "raw_head_created_at", + "framing": "i64" + }, + { + "name": "admission_status", + "framing": "text" + }, + { + "name": "admission_code", + "framing": "optional_text" + }, + { + "name": "contract_id", + "framing": "optional_text" + }, + { + "name": "visibility", + "framing": "text" + }, + { + "name": "nip09_outcome", + "framing": "optional_text" + }, + { + "name": "nip09_reason", + "framing": "optional_text" + }, + { + "name": "event_reference_request_id", + "framing": "optional_text" + }, + { + "name": "address_reference_request_id", + "framing": "optional_text" + }, + { + "name": "address_reference_cutoff", + "framing": "optional_i64" + } + ] + }, + { + "section": "current_visibility", + "sql": "SELECT event_id, admission_status, contract_id, event_class, raw_d_tag, is_raw_head, raw_head_event_id, 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 source_generation = ? ORDER BY event_id", + "fields": [ + { + "name": "event_id", + "framing": "text" + }, + { + "name": "admission_status", + "framing": "text" + }, + { + "name": "contract_id", + "framing": "optional_text" + }, + { + "name": "event_class", + "framing": "text" + }, + { + "name": "raw_d_tag", + "framing": "optional_text" + }, + { + "name": "is_raw_head", + "framing": "boolean" + }, + { + "name": "raw_head_event_id", + "framing": "optional_text" + }, + { + "name": "suppression_outcome", + "framing": "optional_text" + }, + { + "name": "suppression_reason", + "framing": "optional_text" + }, + { + "name": "event_reference_request_id", + "framing": "optional_text" + }, + { + "name": "address_reference_request_id", + "framing": "optional_text" + }, + { + "name": "address_reference_cutoff", + "framing": "optional_i64" + }, + { + "name": "current_visibility", + "framing": "text" + } + ] + }, + { + "section": "food_projection", + "sql": "SELECT kind, pubkey, d_tag, event_id, created_at, contract_id, content, title, summary, published_at, location, price_amount, price_currency, price_unit, quantity_amount, quantity_unit, status, diagnostic_codes_json FROM radroots_event_store_food_availability_projection WHERE source_generation = ? ORDER BY pubkey, d_tag", + "fields": [ + { + "name": "kind", + "framing": "i64" + }, + { + "name": "pubkey", + "framing": "text" + }, + { + "name": "d_tag", + "framing": "text" + }, + { + "name": "event_id", + "framing": "text" + }, + { + "name": "created_at", + "framing": "i64" + }, + { + "name": "contract_id", + "framing": "text" + }, + { + "name": "content", + "framing": "text" + }, + { + "name": "title", + "framing": "text" + }, + { + "name": "summary", + "framing": "text" + }, + { + "name": "published_at", + "framing": "i64" + }, + { + "name": "location", + "framing": "text" + }, + { + "name": "price_amount", + "framing": "text" + }, + { + "name": "price_currency", + "framing": "text" + }, + { + "name": "price_unit", + "framing": "text" + }, + { + "name": "quantity_amount", + "framing": "optional_text" + }, + { + "name": "quantity_unit", + "framing": "optional_text" + }, + { + "name": "status", + "framing": "text" + }, + { + "name": "diagnostic_codes_json", + "framing": "text" + } + ] + }, + { + "section": "food_images", + "sql": "SELECT pubkey, d_tag, image_index, raw_tag_json, url, width, height, blossom_sha256, qualifies, diagnostic_codes_json FROM radroots_event_store_food_availability_image WHERE source_generation = ? ORDER BY pubkey, d_tag, image_index", + "fields": [ + { + "name": "pubkey", + "framing": "text" + }, + { + "name": "d_tag", + "framing": "text" + }, + { + "name": "image_index", + "framing": "i64" + }, + { + "name": "raw_tag_json", + "framing": "text" + }, + { + "name": "url", + "framing": "optional_text" + }, + { + "name": "width", + "framing": "optional_i64" + }, + { + "name": "height", + "framing": "optional_i64" + }, + { + "name": "blossom_sha256", + "framing": "optional_text" + }, + { + "name": "qualifies", + "framing": "boolean" + }, + { + "name": "diagnostic_codes_json", + "framing": "text" + } + ] + }, + { + "section": "food_search", + "sql": "SELECT event_id, pubkey, d_tag, title, summary, content, location FROM radroots_event_store_food_availability_search_fts ORDER BY event_id", + "fields": [ + { + "name": "event_id", + "framing": "text" + }, + { + "name": "pubkey", + "framing": "text" + }, + { + "name": "d_tag", + "framing": "text" + }, + { + "name": "title", + "framing": "text" + }, + { + "name": "summary", + "framing": "text" + }, + { + "name": "content", + "framing": "text" + }, + { + "name": "location", + "framing": "text" + } + ] + }, + { + "section": "food_cursor", + "sql": "SELECT feed_version, projection_version, scope_fingerprint, hook_manifest_sha256, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1", + "fields": [ + { + "name": "feed_version", + "framing": "i64" + }, + { + "name": "projection_version", + "framing": "i64" + }, + { + "name": "scope_fingerprint", + "framing": "blob" + }, + { + "name": "hook_manifest_sha256", + "framing": "text" + }, + { + "name": "projected_row_count", + "framing": "i64" + } + ] + } + ] + }, + "visibility_oracle": "pure_verified_raw_snapshot_direct_indexed_evidence_v1", + "scoped_integrity_mode": "event_store_owned_tables_and_indices_v1", + "scoped_integrity_tables": [ + "event_envelopes", + "event_envelope_tags", + "event_envelope_head", + "radroots_event_store_source_generation", + "radroots_event_store_source_rebuild_commit_barrier", + "radroots_event_store_source_rebuild_marker", + "radroots_event_store_source_state", + "radroots_event_store_write_lock", + "radroots_event_store_source_capacity_v1", + "radroots_event_store_event_coordinate", + "radroots_event_store_nip09_request", + "radroots_event_store_nip09_event_target", + "radroots_event_store_nip09_address_target", + "radroots_event_store_addressable_head_state", + "radroots_event_store_addressable_head_transition", + "radroots_event_store_addressable_feed_integrity_v1", + "radroots_event_store_food_availability_cursor", + "radroots_event_store_food_availability_projection", + "radroots_event_store_food_availability_image" + ], + "sqlite_sequence_scope": "target_first_after_single_shared_sequence_scan_v1", + "stages": [ + "after_marker_open", + "after_generation_rotation", + "after_core_replay", + "after_visibility_audit", + "after_food_reset_replay", + "after_food_audit", + "after_marker_close" + ], + "failpoints": [ + "after_marker_open", + "after_generation_rotation", + "after_core_replay", + "after_visibility_audit", + "after_food_reset_replay", + "after_food_audit", + "after_marker_close" + ], + "preserved_authorities": [ + "legacy_listing", + "trade", + "transport_observation", + "generic_projection_cursor", + "unrelated_caller_state_without_dependencies_on_rebuild_owned_tables" + ] + }, + "entry_points": [ + { + "role": "live_rebuild", + "rust_path": "radroots_event_store::RadrootsEventStore::rebuild_from_raw_v1" + }, + { + "role": "cold_file_repair", + "rust_path": "radroots_event_store::RadrootsEventStore::repair_file_from_raw_v1" + }, + { + "role": "projection_cursor_insert_preflight", + "rust_path": "radroots_event_store::nip09::reconciliation_v1::preflight_projection_cursor_insert_v1" + }, + { + "role": "serialized_rebuild_runtime", + "rust_path": "radroots_event_store::nip09::reconciliation_v1::raw_source_rebuild::rebuild_from_raw_v1_on_pool" + }, + { + "role": "independent_visibility_oracle", + "rust_path": "radroots_event_store::nip09::reconciliation_v1::visibility_oracle_v1::audit_current_visibility_from_raw_v1" + }, + { + "role": "result_vector_executor", + "rust_path": "raw_source_rebuild_v1_result_vector" + } + ], + "source_files": [ + { + "role": "workspace_manifest_authority", + "path": "Cargo.toml", + "byte_length": 10836, + "sha256": "285532dbb0894204843a832880f136ceac5ee312a3203ff951fb0551fac63ec4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "workspace_lockfile_authority", + "path": "Cargo.lock", + "byte_length": 216965, + "sha256": "f26bf62f77e48914c89c15e689fdbc6799928e9603cab38f50c0c65a0c405edf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "nix_flake_app_export_authority", + "path": "flake.nix", + "byte_length": 1835, + "sha256": "0251b26040cf5338c12dc777a4deaadb8f63eb4e88bc05929dcec67db88ff2bf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "nix_input_lock_authority", + "path": "flake.lock", + "byte_length": 3031, + "sha256": "41b569739bfa0c488625326f4f0a874561601787951cdf7a3f171e60572fa20e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "nix_contract_app_routing_authority", + "path": "build/nix/apps.nix", + "byte_length": 2836, + "sha256": "41a185ac87379e24c1ede09c0f1aac820653dffc09f99cd803b145b44bed982c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "nix_contract_test_lane_authority", + "path": "build/nix/common.nix", + "byte_length": 11127, + "sha256": "b3340e1b4973e6a1e02899d164ca74842757f22b6b1a03f90461532fcd844df5", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "nix_toolchain_routing_authority", + "path": "build/nix/toolchains.nix", + "byte_length": 178, + "sha256": "cd664be945e28bf6c25c7758182ff8d01e03248832dfc2c045c01b4f4aff960f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "rust_toolchain_authority", + "path": "rust-toolchain.toml", + "byte_length": 132, + "sha256": "c33aa38292bab6513bf79ed2f69c1525b736dd738b15ca78af713b70b29265c9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "xtask_manifest_authority", + "path": "tools/xtask/Cargo.toml", + "byte_length": 1097, + "sha256": "7e858f4f33913f986c565be2a31c41615ea0585c9e19572363ef5cae36cafdc9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "event_store_dependency_feature_authority", + "path": "crates/event_store/Cargo.toml", + "byte_length": 1529, + "sha256": "4bddb3462a7543c9a7981ead5cf1027988fc381457432f4b04b0e9c43f6d51ca", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "event_store_error_surface", + "path": "crates/event_store/src/error.rs", + "byte_length": 24687, + "sha256": "404f3f91b1b4aed345faf23a2bfd8a59cdf475d5f411d416dd26418c71ea9a89", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "generated_descriptor_registration", + "path": "crates/event_store/src/generated.rs", + "byte_length": 188, + "sha256": "05328d38ebb6f827f6986b384fefb834948652dfd77fc29c9633d8a1a0d5947e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "food_generated_descriptor_input", + "path": "crates/event_store/src/generated/food_availability_projection_manifest.rs", + "byte_length": 21437, + "sha256": "90908da53ab9572f45f5916ccc2652736b7ea26ba6dd202a4f69af1e651b564b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "nip09_generated_descriptor_input", + "path": "crates/event_store/src/generated/nip09_reconciliation_manifest.rs", + "byte_length": 586039, + "sha256": "406a760e9bed1e8fc89c8e7ae0976c7eff844de7427a3f473528c895439500b3", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "source_maintenance_generated_descriptor_input", + "path": "crates/event_store/src/generated/source_maintenance_manifest.rs", + "byte_length": 18723, + "sha256": "5f988f800425cf36d4327c828b30943c2f79c1fa577ce80730dc13383a1466b1", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_surface", + "path": "crates/event_store/src/lib.rs", + "byte_length": 4133, + "sha256": "7cc60495cd26d1f3d8147b1c6b39db83a170f934f74226a617263c0c13c25ada", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "migration_runtime_registry", + "path": "crates/event_store/src/migrations.rs", + "byte_length": 73585, + "sha256": "a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "model_registration", + "path": "crates/event_store/src/model.rs", + "byte_length": 33818, + "sha256": "66d0b7b8d9966084c76d85aa7f79e9ec0d68cde464ea8b0a327404e61eadd8ff", + "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_availability_projection_model", + "path": "crates/event_store/src/model/food_availability_projection_v1.rs", + "byte_length": 17493, + "sha256": "1e5ff9c05a81fda223ed1a27ff18a1b08bcdeaec9047a13fdd577390b3e0fdb9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "ingest_reconciliation_model", + "path": "crates/event_store/src/model/ingest_reconciliation_v1.rs", + "byte_length": 1626, + "sha256": "47bf13b3fc0f8a913a660f7d655413de0f6b90568bc4acc510aa6bd741bab47b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "rebuild_report_and_digest_models", + "path": "crates/event_store/src/model/raw_source_rebuild_v1.rs", + "byte_length": 2804, + "sha256": "a59459b5566f4450576fc5412e3c8ac0153954b653be376ccd925b19fc647345", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "reconciliation_model", + "path": "crates/event_store/src/model/reconciliation_v1.rs", + "byte_length": 11138, + "sha256": "8a26bc373035878ef9b41767ceea7b681896e17d88bedce118de1d622125e1d6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "nip09_module_registration", + "path": "crates/event_store/src/nip09.rs", + "byte_length": 34, + "sha256": "fbd8a3b36d7f36e7b0d301aee0847d42c3908659f066cafcae3e247d67a75845", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "reconciliation_runtime_registration", + "path": "crates/event_store/src/nip09/reconciliation_v1.rs", + "byte_length": 194622, + "sha256": "4c14df2bd3af7bfefb002917dc7549f6ada155be3a748acb9cd6d78199ee6f76", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "serialized_raw_source_rebuild", + "path": "crates/event_store/src/nip09/reconciliation_v1/raw_source_rebuild.rs", + "byte_length": 60973, + "sha256": "a8db92dfbfa420b545038502c2a04547bed41b76e283f09758faa248c597c781", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "nip09_result_vector_executor_input", + "path": "crates/event_store/src/nip09/reconciliation_v1/result_vector_executor.rs", + "byte_length": 18446, + "sha256": "ca2a2bf54062aa6ddf2e553fd624c7217a01ad56309487ce73fa58c47c06c208", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "independent_raw_visibility_oracle", + "path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "byte_length": 36998, + "sha256": "48b60aba869d804ad7b3b120d7479c7ff45dd3758502a90b4fbce312d9848a99", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "managed_v4_validation_and_scoped_integrity", + "path": "crates/event_store/src/schema.rs", + "byte_length": 153682, + "sha256": "df92fc509b44e40dae5a48d03ad9bf5cc556c4319a78215460ca26b899c610a2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "source_capacity_rebuild_authority", + "path": "crates/event_store/src/source_maintenance_v1.rs", + "byte_length": 51756, + "sha256": "f8d5b62f0613104aa86658d5bf1baade92c7df83f00ef0cddadd734b9797afca", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_rebuild_and_cold_repair_boundary", + "path": "crates/event_store/src/store.rs", + "byte_length": 402744, + "sha256": "56a84cc05208a335cbb6bad41b024c20ed77db5000fa69e684611e196ae461f4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "addressable_transition_feed_storage", + "path": "crates/event_store/src/store/addressable_transition_feed_v1.rs", + "byte_length": 40253, + "sha256": "fe23424aa1e6b39f9aba2dfa4470652b26b4990f91204a2bdfe379c03da9b610", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "current_visibility_storage", + "path": "crates/event_store/src/store/current_visibility_v1.rs", + "byte_length": 15860, + "sha256": "8615086e674c30700305debcef11de5b3dbfe5aec735c0f58b4ac11caa518596", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "raw_source_rebuild_focused_tests", + "path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "byte_length": 103772, + "sha256": "383ca6f8aac6418d1d4460603d50746d224011c16367c2b86d8342818567ae2a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "signed_food_digest_fixture", + "path": "crates/event_store/tests/fixtures/food_availability_projection.v1.json", + "byte_length": 103659, + "sha256": "fca2b71b47736ed04ed1e908823b65b3fc3cf0366cb162128369fe328295bb63", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "food_projection_reset_and_replay", + "path": "crates/event_store/src/store/food_availability_projection_v1.rs", + "byte_length": 50858, + "sha256": "adc8a3eb59f5bccb4c0d0ba4c5319dbf55cffb5e0c333db8235db63fd0df5f21", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "post_core_extension_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_extension_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_extensions_v1", + "path": "crates/event_store/src/store/post_core_extensions_v1.rs", + "byte_length": 6935, + "sha256": "fb165704c64d982cf3be0a880c44985be6b375758451e94b2aaaf30881769f18", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "post_core_extensions_v2", + "path": "crates/event_store/src/store/post_core_extensions_v2.rs", + "byte_length": 294, + "sha256": "8dcbc503ed9ea6fb06ed9a2a83b0804d928f9590b5706de25a057ad72c0d38d2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "post_core_storage_v1", + "path": "crates/event_store/src/store/post_core_storage_v1.rs", + "byte_length": 16871, + "sha256": "a6dca0884762cec3c32e460d17662ced9d0335f30e79b3d5fdb3461259d3ec19", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "post_core_storage_v2", + "path": "crates/event_store/src/store/post_core_storage_v2.rs", + "byte_length": 632, + "sha256": "4b672770f3c34bf887e4cc949c068cb0c87396cb4af8efb6e13d39aa4e0d973a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "protocol_reconciliation_storage", + "path": "crates/event_store/src/store/protocol_reconciliation_v1.rs", + "byte_length": 30140, + "sha256": "210112eeaa6975a3b4fbb97d5c52588f8c6d8d07975e531d39737fd11235de51", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "protocol_storage_boundary", + "path": "crates/event_store/src/store/protocol_storage_v1.rs", + "byte_length": 10975, + "sha256": "155c74d27eee5db1d6f0f844f9d319604eefbbf640f4b2371ac5d0e370816e50", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "event_store_package_readme", + "path": "crates/event_store/README", + "byte_length": 22209, + "sha256": "9e1cf2ec9ba58c2028d78eb33e6355fc2dff3507837b6e8640941dccd5608dc7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "signed_nip09_reconciliation_fixture", + "path": "crates/event_store/tests/fixtures/nip09_reconciliation.v1.json", + "byte_length": 10405, + "sha256": "31cd9507734ff3308436881622a626b9782b75b548d9f5e159e4125621855b9c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "transitive_food_predecessor_governance", + "path": "tools/xtask/src/contract/food_availability_projection.rs", + "byte_length": 194995, + "sha256": "02f8b70b3885267b09fd5241ec89bcf020d2975e1eb1c6c533656a036723395b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "immutable_predecessor_governance", + "path": "tools/xtask/src/contract/source_maintenance.rs", + "byte_length": 123766, + "sha256": "f10962e0cc0fa44dc109d87b707f02be11fe6dad1113707eef820ff3c5ae97ee", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "transitive_nip09_predecessor_governance", + "path": "tools/xtask/src/contract/nip09_reconciliation.rs", + "byte_length": 850763, + "sha256": "852697eaaffcfe99391ffafd0c7c390c8eeb175377ac7e5cd5f490050b57ed58", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "raw_source_rebuild_governance", + "path": "tools/xtask/src/contract/raw_source_rebuild.rs", + "byte_length": 294540, + "sha256": "543c21131346381a833f9e063fd535efd0d0e192419aefa1f60e9b0f68475866", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "contract_command_authority", + "path": "tools/xtask/src/contract.rs", + "byte_length": 479703, + "sha256": "72fbd457b0cfdff1e30f07bf2452a0c71c23cef93bdaefffc114defa616d6342", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "xtask_dispatch_and_release_preflight", + "path": "tools/xtask/src/main.rs", + "byte_length": 15018, + "sha256": "9aab8db1186b776ba8dcac90cc46c29d96928b610850b084be0e3a25d3e4cb0f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "release_breaking_change_authority", + "path": "contracts/releases/1.0.0-alpha.1.toml", + "byte_length": 19840, + "sha256": "946d90dacc9db522825898dbb5d9d424b020a22fe53b80ec15eb67c5f1d8cd2d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "release_note_authority", + "path": "CHANGELOG.md", + "byte_length": 28939, + "sha256": "61b9d2a9050e4123bc5324aebf6e54393b9b1efdc2145a4a2cf6c009a4345b23", + "hash_algorithm": "sha256_bytes_v1" + } + ], + "public_api": { + "added_symbols": [ + "RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1", + "RadrootsEventStoreCallerInboundForeignKeyV1", + "RadrootsEventStoreActiveProductStateDigestV1", + "RadrootsEventStoreImmutableRawDigestV1", + "RadrootsEventStoreRawSourceRebuildDriftV1", + "RadrootsEventStoreRawSourceRebuildReportV1" + ], + "methods": [ + "RadrootsEventStore::rebuild_from_raw_v1", + "RadrootsEventStore::repair_file_from_raw_v1", + "RadrootsEventStoreRawSourceRebuildReportV1::prior_source_generation", + "RadrootsEventStoreRawSourceRebuildReportV1::new_source_generation", + "RadrootsEventStoreRawSourceRebuildReportV1::source_capacity", + "RadrootsEventStoreRawSourceRebuildReportV1::raw_high_water_seq", + "RadrootsEventStoreRawSourceRebuildReportV1::immutable_raw_digest", + "RadrootsEventStoreRawSourceRebuildReportV1::active_product_state_digest", + "RadrootsEventStoreImmutableRawDigestV1::as_bytes", + "RadrootsEventStoreActiveProductStateDigestV1::as_bytes", + "RadrootsEventStoreRawSourceRebuildDriftV1::code" + ], + "error_variants": [ + "ProjectionCursorCapacityExceeded", + "RawSourceRepairDatabaseIdentityMismatch", + "RawSourceRepairCanonicalPathLockDomainMismatch", + "RawSourceRepairMainDatabaseCanonicalizationFailed", + "RawSourceRebuildCallerForeignKeyCapacityExceeded", + "RawSourceRebuildCallerInboundForeignKeyUnsupported", + "RawSourceRebuildCallerTableCapacityExceeded", + "RawSourceRebuildStateDrift", + "RawSourceRebuildTransactionRollbackFailed" + ], + "drift_kinds": [ + { + "variant": "ManagedSchemaAuthority", + "code": "managed_schema_authority" + }, + { + "variant": "ImmutableRawAuthority", + "code": "immutable_raw_authority" + }, + { + "variant": "SourceGenerationLineage", + "code": "source_generation_lineage" + }, + { + "variant": "AddressableTransitionAuthority", + "code": "addressable_transition_authority" + }, + { + "variant": "DerivedProductStateAuthority", + "code": "derived_product_state_authority" + }, + { + "variant": "RebuildPostcondition", + "code": "rebuild_postcondition" + } + ] + }, + "result_vector": { + "canonical_path": "contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json", + "mirror_path": "crates/event_store/tests/fixtures/raw_source_rebuild.v1.json", + "byte_length": 26833, + "sha256": "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15", + "hash_algorithm": "sha256_bytes_v1", + "executor_id": "radroots_event_store.raw_source_rebuild_v1.result_vector_executor.v1", + "executor_path": "crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs", + "executor_test": "raw_source_rebuild_v1_result_vector", + "executor_byte_length": 25542, + "executor_sha256": "51647259efdd0d99689ef1db0defb139c8d1f60f2ead69b793ddb2733a28e832", + "executor_hash_algorithm": "sha256_bytes_v1" + } +} diff --git a/crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json b/crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json @@ -0,0 +1,655 @@ +{ + "$id": "https://radroots.org/contracts/event-store/raw-source-rebuild-v1-manifest.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "authority_id": { + "const": "raw_source_rebuild_v1" + }, + "contract_id": { + "const": "radroots_event_store.raw_source_rebuild_v1" + }, + "entry_points": { + "items": { + "additionalProperties": false, + "properties": { + "role": { + "minLength": 1, + "type": "string" + }, + "rust_path": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "role", + "rust_path" + ], + "type": "object" + }, + "type": "array" + }, + "manifest_schema": { + "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" + }, + "migration_inventory": { + "items": { + "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" + }, + "maxItems": 8, + "minItems": 8, + "type": "array" + }, + "predecessor": { + "additionalProperties": false, + "properties": { + "contract_id": { + "const": "radroots_event_store.source_maintenance_v1" + }, + "manifest": { + "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" + } + }, + "required": [ + "contract_id", + "manifest" + ], + "type": "object" + }, + "public_api": { + "additionalProperties": false, + "properties": { + "added_symbols": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "drift_kinds": { + "items": { + "additionalProperties": false, + "properties": { + "code": { + "pattern": "^[a-z][a-z0-9_]*$", + "type": "string" + }, + "variant": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "variant", + "code" + ], + "type": "object" + }, + "maxItems": 6, + "minItems": 6, + "type": "array" + }, + "error_variants": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "methods": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "added_symbols", + "methods", + "error_variants", + "drift_kinds" + ], + "type": "object" + }, + "result_vector": { + "additionalProperties": false, + "properties": { + "byte_length": { + "minimum": 1, + "type": "integer" + }, + "canonical_path": { + "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$", + "type": "string" + }, + "executor_byte_length": { + "minimum": 1, + "type": "integer" + }, + "executor_hash_algorithm": { + "const": "sha256_bytes_v1" + }, + "executor_id": { + "minLength": 1, + "type": "string" + }, + "executor_path": { + "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$", + "type": "string" + }, + "executor_sha256": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + "executor_test": { + "minLength": 1, + "type": "string" + }, + "hash_algorithm": { + "const": "sha256_bytes_v1" + }, + "mirror_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": [ + "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" + }, + "runtime": { + "additionalProperties": false, + "properties": { + "active_product_state_digest": { + "additionalProperties": false, + "properties": { + "algorithm": { + "const": "sha256_domain_nul_typed_fields_v1" + }, + "component_queries": { + "items": { + "additionalProperties": false, + "properties": { + "fields": { + "items": { + "additionalProperties": false, + "properties": { + "framing": { + "enum": [ + "i64", + "boolean", + "optional_i64", + "text", + "optional_text", + "blob" + ] + }, + "name": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "framing" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "section": { + "minLength": 1, + "type": "string" + }, + "sql": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "section", + "sql", + "fields" + ], + "type": "object" + }, + "maxItems": 13, + "minItems": 13, + "type": "array" + }, + "components": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "domain_terminator": { + "const": "nul_byte" + }, + "domain_utf8": { + "const": "radroots:event-store:active-product-state-digest:v1" + }, + "exclusions": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "framing": { + "additionalProperties": false, + "properties": { + "blob": { + "const": "X_then_u64be_length_then_bytes" + }, + "boolean": { + "const": "B_then_u8_0_or_1" + }, + "optional": { + "const": "O_then_presence_u8_then_nested_value_when_present" + }, + "row": { + "const": "R" + }, + "section": { + "const": "S_then_N_then_u64be_length_then_utf8_name" + }, + "signed_i64": { + "const": "I_then_i64be" + }, + "text": { + "const": "T_then_u64be_length_then_utf8_bytes" + } + }, + "required": [ + "section", + "row", + "signed_i64", + "boolean", + "optional", + "text", + "blob" + ], + "type": "object" + }, + "output_bytes": { + "const": 32 + } + }, + "required": [ + "algorithm", + "domain_utf8", + "domain_terminator", + "framing", + "output_bytes", + "components", + "exclusions", + "component_queries" + ], + "type": "object" + }, + "caller_foreign_key_row_count_limit": { + "const": 4096 + }, + "caller_inbound_foreign_key_parent_tables": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "caller_inbound_foreign_key_policy": { + "const": "reject_all_rebuild_mutated_parent_dependencies_before_entropy_v1" + }, + "caller_main_table_count_limit": { + "const": 4096 + }, + "cold_repair_mode": { + "const": "canonical_file_only_single_connection_lock_domain_probe_v1" + }, + "event_contract_registry_version": { + "const": 7 + }, + "event_store_schema_version": { + "const": 4 + }, + "failpoints": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "immutable_raw_digest": { + "additionalProperties": false, + "properties": { + "algorithm": { + "const": "sha256_domain_nul_typed_fields_v1" + }, + "domain_terminator": { + "const": "nul_byte" + }, + "domain_utf8": { + "const": "radroots:event-store:immutable-raw-digest:v1" + }, + "framing": { + "additionalProperties": false, + "properties": { + "blob": { + "const": "X_then_u64be_length_then_bytes" + }, + "boolean": { + "const": "B_then_u8_0_or_1" + }, + "optional": { + "const": "O_then_presence_u8_then_nested_value_when_present" + }, + "row": { + "const": "R" + }, + "section": { + "const": "S_then_N_then_u64be_length_then_utf8_name" + }, + "signed_i64": { + "const": "I_then_i64be" + }, + "text": { + "const": "T_then_u64be_length_then_utf8_bytes" + } + }, + "required": [ + "section", + "row", + "signed_i64", + "boolean", + "optional", + "text", + "blob" + ], + "type": "object" + }, + "output_bytes": { + "const": 32 + }, + "source_queries": { + "items": { + "additionalProperties": false, + "properties": { + "fields": { + "items": { + "additionalProperties": false, + "properties": { + "framing": { + "enum": [ + "i64", + "boolean", + "optional_i64", + "text", + "optional_text", + "blob" + ] + }, + "name": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "framing" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "section": { + "minLength": 1, + "type": "string" + }, + "sql": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "section", + "sql", + "fields" + ], + "type": "object" + }, + "maxItems": 2, + "minItems": 2, + "type": "array" + } + }, + "required": [ + "algorithm", + "domain_utf8", + "domain_terminator", + "framing", + "output_bytes", + "source_queries" + ], + "type": "object" + }, + "preserved_authorities": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "projection_cursor_count_limit": { + "const": 4096 + }, + "projection_cursor_rejection_probe_limit": { + "const": 4097 + }, + "scoped_integrity_mode": { + "const": "event_store_owned_tables_and_indices_v1" + }, + "scoped_integrity_tables": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "sqlite_sequence_scope": { + "const": "target_first_after_single_shared_sequence_scan_v1" + }, + "stages": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "transaction_mode": { + "const": "begin_immediate_v1" + }, + "visibility_oracle": { + "const": "pure_verified_raw_snapshot_direct_indexed_evidence_v1" + } + }, + "required": [ + "event_store_schema_version", + "event_contract_registry_version", + "transaction_mode", + "projection_cursor_count_limit", + "projection_cursor_rejection_probe_limit", + "caller_main_table_count_limit", + "caller_foreign_key_row_count_limit", + "caller_inbound_foreign_key_policy", + "caller_inbound_foreign_key_parent_tables", + "cold_repair_mode", + "immutable_raw_digest", + "active_product_state_digest", + "visibility_oracle", + "scoped_integrity_mode", + "scoped_integrity_tables", + "sqlite_sequence_scope", + "stages", + "failpoints", + "preserved_authorities" + ], + "type": "object" + }, + "schema_version": { + "const": 1 + }, + "source_files": { + "items": { + "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" + }, + "type": "array" + } + }, + "required": [ + "schema_version", + "contract_id", + "authority_id", + "manifest_schema", + "predecessor", + "migration_inventory", + "runtime", + "entry_points", + "source_files", + "public_api", + "result_vector" + ], + "title": "Radroots event-store raw-source rebuild v1 manifest", + "type": "object" +} diff --git a/crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256 b/crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256 @@ -0,0 +1 @@ +b8737a9c5836517114e7df6c2194c46e3c200093e12c4e6297165d2b9dae56a1 diff --git a/crates/event_store/src/error.rs b/crates/event_store/src/error.rs @@ -14,6 +14,8 @@ pub const RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1: u64 = 64 * 1024 * pub const RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1: u64 = 32 * 1024 * 1024; /// Maximum append-only source generations retained before fresh-store resync. pub const RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1: u32 = 8; +/// Maximum caller-owned generic projection cursor identities in one store. +pub const RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1: u32 = 4_096; /// Governed retained raw-source resource dimension. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -47,6 +49,86 @@ impl core::fmt::Display for RadrootsEventStoreSourceCapacityResourceV1 { } } +/// Stable category for raw-source rebuild authority drift. +/// +/// The category code is contractual. Error detail remains diagnostic context +/// and must not be parsed by callers. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsEventStoreRawSourceRebuildDriftV1 { + /// The database is not the exact managed schema supported by repair. + ManagedSchemaAuthority, + /// Retained immutable raw events or tags are internally inconsistent. + ImmutableRawAuthority, + /// Retained source-generation history or baselines are inconsistent. + SourceGenerationLineage, + /// Addressable transition sequence authority is inconsistent. + AddressableTransitionAuthority, + /// Rebuilt visibility or projection state is inconsistent. + DerivedProductStateAuthority, + /// The rebuild could not establish its final atomic postconditions. + RebuildPostcondition, +} + +impl RadrootsEventStoreRawSourceRebuildDriftV1 { + /// Stable machine-readable category code. + pub const fn code(self) -> &'static str { + match self { + Self::ManagedSchemaAuthority => "managed_schema_authority", + Self::ImmutableRawAuthority => "immutable_raw_authority", + Self::SourceGenerationLineage => "source_generation_lineage", + Self::AddressableTransitionAuthority => "addressable_transition_authority", + Self::DerivedProductStateAuthority => "derived_product_state_authority", + Self::RebuildPostcondition => "rebuild_postcondition", + } + } +} + +impl core::fmt::Display for RadrootsEventStoreRawSourceRebuildDriftV1 { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(self.code()) + } +} + +/// Caller-owned foreign-key dependency that makes raw-source rebuild unsafe. +#[non_exhaustive] +#[derive(Debug, PartialEq, Eq)] +pub struct RadrootsEventStoreCallerInboundForeignKeyV1 { + pub child_table: String, + pub foreign_key_id: i64, + pub foreign_key_sequence: i64, + pub child_column: String, + pub parent_table: String, + pub parent_column: Option<String>, + pub on_update: String, + pub on_delete: String, + pub match_clause: String, +} + +impl core::fmt::Display for RadrootsEventStoreCallerInboundForeignKeyV1 { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + formatter, + "{}:{} on `{}` (`{}` -> `{}`.", + self.foreign_key_id, + self.foreign_key_sequence, + self.child_table, + self.child_column, + self.parent_table, + )?; + match self.parent_column.as_deref() { + Some(parent_column) => write!(formatter, "`{parent_column}`")?, + None => formatter.write_str("<implicit primary key>")?, + } + write!( + formatter, + ", on update {}, on delete {}, match {})", + self.on_update, self.on_delete, self.match_clause, + ) + } +} + +#[non_exhaustive] #[derive(Debug, thiserror::Error)] pub enum RadrootsEventStoreError { #[error("sqlx error: {0}")] @@ -120,6 +202,20 @@ pub enum RadrootsEventStoreError { )] UnsafeInMemoryPoolConnectionCount { actual: u32 }, #[error( + "raw-source repair SQLite main database identity mismatch: expected `{expected}`, found `{actual}`" + )] + RawSourceRepairDatabaseIdentityMismatch { expected: String, actual: String }, + #[error( + "raw-source repair canonical path `{canonical_path}` does not share the validated SQLite main lock domain" + )] + RawSourceRepairCanonicalPathLockDomainMismatch { canonical_path: String }, + #[error("raw-source repair could not canonicalize SQLite main database `{filename}`: {source}")] + RawSourceRepairMainDatabaseCanonicalizationFailed { + filename: String, + #[source] + source: std::io::Error, + }, + #[error( "event-store pool backing mismatch: file_backed={file_backed}, configured filename `{filename}`" )] SqlitePoolBackingMismatch { file_backed: bool, filename: String }, @@ -127,9 +223,7 @@ pub enum RadrootsEventStoreError { SqliteMainDatabaseUnavailable, #[error("event-store SQLite main database must use UTF-8 encoding; reported `{actual}`")] SqliteMainDatabaseEncodingNotUtf8 { actual: String }, - #[error( - "event-store SQLite file connection did not enter WAL journal mode; reported `{actual}`" - )] + #[error("event-store SQLite file connection must use WAL journal mode; reported `{actual}`")] SqliteFileJournalModeNotWal { actual: String }, #[error( "temporary schema object `{name}` ({object_type}, table `{table_name}`) collides with event-store authority" @@ -228,6 +322,17 @@ pub enum RadrootsEventStoreError { primary: Box<RadrootsEventStoreError>, rollback: sqlx::Error, }, + #[error("event-store raw-source rebuild authority is inconsistent ({kind}): {detail}")] + RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1, + detail: String, + }, + #[error("raw-source rebuild failed: {primary}; transaction rollback also failed: {rollback}")] + RawSourceRebuildTransactionRollbackFailed { + #[source] + primary: Box<RadrootsEventStoreError>, + rollback: sqlx::Error, + }, #[error("event-store source generation entropy is unavailable")] SourceGenerationEntropyUnavailable, #[error( @@ -291,6 +396,20 @@ pub enum RadrootsEventStoreError { parent: String, foreign_key_index: i64, }, + #[error( + "event-store raw-source rebuild caller main-table inventory exceeds bounded preflight capacity: observed at least {observed_at_least}, limit {limit}" + )] + RawSourceRebuildCallerTableCapacityExceeded { observed_at_least: u64, limit: u64 }, + #[error( + "event-store raw-source rebuild caller foreign-key inventory exceeds bounded preflight capacity: observed at least {observed_at_least} rows, limit {limit}" + )] + RawSourceRebuildCallerForeignKeyCapacityExceeded { observed_at_least: u64, limit: u64 }, + #[error( + "event-store raw-source rebuild does not support caller-owned foreign key {dependency}" + )] + RawSourceRebuildCallerInboundForeignKeyUnsupported { + dependency: Box<RadrootsEventStoreCallerInboundForeignKeyV1>, + }, #[error("invalid stored enum value `{value}` for {field}")] InvalidStoredEnum { field: &'static str, value: String }, #[error("invalid stored boolean value `{value}` for {field}; expected 0 or 1")] @@ -349,6 +468,10 @@ pub enum RadrootsEventStoreError { high_water: i64, }, #[error( + "event-store generic projection cursor capacity exceeded: current {current}, limit {limit}" + )] + ProjectionCursorCapacityExceeded { current: u32, limit: u32 }, + #[error( "projection `{projection_id}` version {projection_version} is already current for the active source generation" )] ProjectionRebuildNotRequired { diff --git a/crates/event_store/src/generated.rs b/crates/event_store/src/generated.rs @@ -1,3 +1,4 @@ pub(crate) mod food_availability_projection_manifest; pub(crate) mod nip09_reconciliation_manifest; +pub(crate) mod raw_source_rebuild_manifest; pub(crate) mod source_maintenance_manifest; diff --git a/crates/event_store/src/generated/raw_source_rebuild_manifest.rs b/crates/event_store/src/generated/raw_source_rebuild_manifest.rs @@ -0,0 +1,18 @@ +// @generated by `cargo xtask contract raw-source-rebuild-manifest --write`; do not edit. +#![allow(dead_code)] + +pub(crate) const RAW_SOURCE_REBUILD_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.raw_source_rebuild_v1\",\n \"authority_id\": \"raw_source_rebuild_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json\",\n \"byte_length\": 17896,\n \"sha256\": \"f9d210967e54b66f39c8bb965d97b2001a0ebc0927e7c2c14edb8e474bfda695\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.json\",\n \"byte_length\": 14216,\n \"sha256\": \"e8911e6e5710278969cbd15557a5b856b1575dfd11a655711403598370b41221\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration_inventory\": [\n {\n \"path\": \"crates/event_store/migrations/0001_event_store.down.sql\",\n \"byte_length\": 522,\n \"sha256\": \"fa84d587f657f601947eaeb9cd239c962a48f6fcdce723588476e8d22f3c1f53\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"path\": \"crates/event_store/migrations/0001_event_store.up.sql\",\n \"byte_length\": 10712,\n \"sha256\": \"4c03906a1cffd418a48d40907aa9a1ca51bb41766cff7250c4dfc7c2fd6eddde\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"path\": \"crates/event_store/migrations/0002_nip09.down.sql\",\n \"byte_length\": 4807,\n \"sha256\": \"c51a099d9501f1e692c13d2226296a68ed9e6bfa5e8e46b2f12c6574dbe59e31\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"path\": \"crates/event_store/migrations/0002_nip09.up.sql\",\n \"byte_length\": 81614,\n \"sha256\": \"0c1730ff36eaebd285f9c0c94b9b7346af60266afa55c24a18e30446d369581a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\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 {\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 {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"425dc799f392b87f265a6fb81f89c4a1c7a5db8391ab0380970708cb9c66704d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"runtime\": {\n \"event_store_schema_version\": 4,\n \"event_contract_registry_version\": 7,\n \"transaction_mode\": \"begin_immediate_v1\",\n \"projection_cursor_count_limit\": 4096,\n \"projection_cursor_rejection_probe_limit\": 4097,\n \"caller_main_table_count_limit\": 4096,\n \"caller_foreign_key_row_count_limit\": 4096,\n \"caller_inbound_foreign_key_policy\": \"reject_all_rebuild_mutated_parent_dependencies_before_entropy_v1\",\n \"caller_inbound_foreign_key_parent_tables\": [\n \"event_envelopes\",\n \"event_envelope_tags\",\n \"event_envelope_head\",\n \"radroots_event_store_source_generation\",\n \"radroots_event_store_source_rebuild_commit_barrier\",\n \"radroots_event_store_source_rebuild_marker\",\n \"radroots_event_store_source_state\",\n \"radroots_event_store_write_lock\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_event_coordinate\",\n \"radroots_event_store_nip09_request\",\n \"radroots_event_store_nip09_event_target\",\n \"radroots_event_store_nip09_address_target\",\n \"radroots_event_store_addressable_head_state\",\n \"radroots_event_store_addressable_head_transition\",\n \"radroots_event_store_addressable_feed_integrity_v1\",\n \"radroots_event_store_food_availability_cursor\",\n \"radroots_event_store_food_availability_projection\",\n \"radroots_event_store_food_availability_image\",\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 \"sqlite_sequence\"\n ],\n \"cold_repair_mode\": \"canonical_file_only_single_connection_lock_domain_probe_v1\",\n \"immutable_raw_digest\": {\n \"algorithm\": \"sha256_domain_nul_typed_fields_v1\",\n \"domain_utf8\": \"radroots:event-store:immutable-raw-digest:v1\",\n \"domain_terminator\": \"nul_byte\",\n \"framing\": {\n \"section\": \"S_then_N_then_u64be_length_then_utf8_name\",\n \"row\": \"R\",\n \"signed_i64\": \"I_then_i64be\",\n \"boolean\": \"B_then_u8_0_or_1\",\n \"optional\": \"O_then_presence_u8_then_nested_value_when_present\",\n \"text\": \"T_then_u64be_length_then_utf8_bytes\",\n \"blob\": \"X_then_u64be_length_then_bytes\"\n },\n \"output_bytes\": 32,\n \"source_queries\": [\n {\n \"section\": \"event_envelopes\",\n \"sql\": \"SELECT seq, event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, inserted_at_ms FROM event_envelopes ORDER BY seq\",\n \"fields\": [\n {\n \"name\": \"seq\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"created_at\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"kind\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"tags_json\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"content\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"sig\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"raw_json\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"inserted_at_ms\",\n \"framing\": \"i64\"\n }\n ]\n },\n {\n \"section\": \"event_envelope_tags\",\n \"sql\": \"SELECT event.seq, tag.event_id, tag.tag_index, tag.tag_name, tag.tag_value, tag.tag_json FROM event_envelope_tags AS tag JOIN event_envelopes AS event ON event.event_id = tag.event_id ORDER BY event.seq, tag.tag_index\",\n \"fields\": [\n {\n \"name\": \"seq\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"tag_index\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"tag_name\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"tag_value\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"tag_json\",\n \"framing\": \"text\"\n }\n ]\n }\n ]\n },\n \"active_product_state_digest\": {\n \"algorithm\": \"sha256_domain_nul_typed_fields_v1\",\n \"domain_utf8\": \"radroots:event-store:active-product-state-digest:v1\",\n \"domain_terminator\": \"nul_byte\",\n \"framing\": {\n \"section\": \"S_then_N_then_u64be_length_then_utf8_name\",\n \"row\": \"R\",\n \"signed_i64\": \"I_then_i64be\",\n \"boolean\": \"B_then_u8_0_or_1\",\n \"optional\": \"O_then_presence_u8_then_nested_value_when_present\",\n \"text\": \"T_then_u64be_length_then_utf8_bytes\",\n \"blob\": \"X_then_u64be_length_then_bytes\"\n },\n \"output_bytes\": 32,\n \"components\": [\n \"logical_current_classifications\",\n \"raw_heads\",\n \"active_addressable_head_state\",\n \"active_nip09_facts\",\n \"current_visibility\",\n \"food_availability_rows\",\n \"food_availability_images\",\n \"logical_food_fts_rows\",\n \"stable_food_cursor_metadata\"\n ],\n \"exclusions\": [\n \"source_generation\",\n \"absolute_transition_sequence\",\n \"transition_history\",\n \"rebuild_origin\",\n \"rebuild_cause\",\n \"operational_timestamps\",\n \"generic_projection_cursors\",\n \"caller_owned_state\"\n ],\n \"component_queries\": [\n {\n \"section\": \"envelope_classification\",\n \"sql\": \"SELECT event_id, verification_status, contract_status, contract_id, event_class, projection_eligible FROM event_envelopes ORDER BY event_id\",\n \"fields\": [\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"verification_status\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"contract_status\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"contract_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"event_class\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"projection_eligible\",\n \"framing\": \"boolean\"\n }\n ]\n },\n {\n \"section\": \"tag_classification\",\n \"sql\": \"SELECT event_id, tag_index, contract_semantic, contract_value_type, relay_indexed FROM event_envelope_tags ORDER BY event_id, tag_index\",\n \"fields\": [\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"tag_index\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"contract_semantic\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"contract_value_type\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"relay_indexed\",\n \"framing\": \"boolean\"\n }\n ]\n },\n {\n \"section\": \"raw_heads\",\n \"sql\": \"SELECT coordinate_type, kind, pubkey, d_tag, event_id, created_at FROM event_envelope_head ORDER BY coordinate_type, kind, pubkey, d_tag\",\n \"fields\": [\n {\n \"name\": \"coordinate_type\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"kind\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"d_tag\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"created_at\",\n \"framing\": \"i64\"\n }\n ]\n },\n {\n \"section\": \"event_coordinates\",\n \"sql\": \"SELECT event_id, coordinate_type, kind, pubkey, created_at, admission_status, admission_code, contract_id, raw_d_tag, nip09_matchable, nip09_d_tag FROM radroots_event_store_event_coordinate WHERE source_generation = ? ORDER BY event_id\",\n \"fields\": [\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"coordinate_type\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"kind\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"created_at\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"admission_status\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"admission_code\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"contract_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"raw_d_tag\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"nip09_matchable\",\n \"framing\": \"boolean\"\n },\n {\n \"name\": \"nip09_d_tag\",\n \"framing\": \"optional_text\"\n }\n ]\n },\n {\n \"section\": \"nip09_requests\",\n \"sql\": \"SELECT request_event_id, request_pubkey, request_created_at FROM radroots_event_store_nip09_request WHERE source_generation = ? ORDER BY request_event_id\",\n \"fields\": [\n {\n \"name\": \"request_event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"request_pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"request_created_at\",\n \"framing\": \"i64\"\n }\n ]\n },\n {\n \"section\": \"nip09_event_targets\",\n \"sql\": \"SELECT request_event_id, target_event_id, source_tag_index, source_tag_value FROM radroots_event_store_nip09_event_target WHERE source_generation = ? ORDER BY request_event_id, target_event_id, source_tag_index\",\n \"fields\": [\n {\n \"name\": \"request_event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"target_event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"source_tag_index\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"source_tag_value\",\n \"framing\": \"text\"\n }\n ]\n },\n {\n \"section\": \"nip09_address_targets\",\n \"sql\": \"SELECT request_event_id, target_kind, target_pubkey, target_d_tag, inclusive_cutoff, source_tag_index, source_tag_value, source_kind_text, source_pubkey_text, source_d_tag FROM radroots_event_store_nip09_address_target WHERE source_generation = ? ORDER BY request_event_id, target_kind, target_pubkey, target_d_tag, source_tag_index\",\n \"fields\": [\n {\n \"name\": \"request_event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"target_kind\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"target_pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"target_d_tag\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"inclusive_cutoff\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"source_tag_index\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"source_tag_value\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"source_kind_text\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"source_pubkey_text\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"source_d_tag\",\n \"framing\": \"text\"\n }\n ]\n },\n {\n \"section\": \"addressable_heads\",\n \"sql\": \"SELECT kind, pubkey, d_tag, raw_head_event_id, 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_state WHERE source_generation = ? ORDER BY kind, pubkey, d_tag\",\n \"fields\": [\n {\n \"name\": \"kind\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"d_tag\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"raw_head_event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"raw_head_created_at\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"admission_status\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"admission_code\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"contract_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"visibility\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"nip09_outcome\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"nip09_reason\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"event_reference_request_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"address_reference_request_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"address_reference_cutoff\",\n \"framing\": \"optional_i64\"\n }\n ]\n },\n {\n \"section\": \"current_visibility\",\n \"sql\": \"SELECT event_id, admission_status, contract_id, event_class, raw_d_tag, is_raw_head, raw_head_event_id, 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 source_generation = ? ORDER BY event_id\",\n \"fields\": [\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"admission_status\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"contract_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"event_class\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"raw_d_tag\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"is_raw_head\",\n \"framing\": \"boolean\"\n },\n {\n \"name\": \"raw_head_event_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"suppression_outcome\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"suppression_reason\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"event_reference_request_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"address_reference_request_id\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"address_reference_cutoff\",\n \"framing\": \"optional_i64\"\n },\n {\n \"name\": \"current_visibility\",\n \"framing\": \"text\"\n }\n ]\n },\n {\n \"section\": \"food_projection\",\n \"sql\": \"SELECT kind, pubkey, d_tag, event_id, created_at, contract_id, content, title, summary, published_at, location, price_amount, price_currency, price_unit, quantity_amount, quantity_unit, status, diagnostic_codes_json FROM radroots_event_store_food_availability_projection WHERE source_generation = ? ORDER BY pubkey, d_tag\",\n \"fields\": [\n {\n \"name\": \"kind\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"d_tag\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"created_at\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"contract_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"content\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"title\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"summary\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"published_at\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"location\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"price_amount\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"price_currency\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"price_unit\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"quantity_amount\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"quantity_unit\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"status\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"diagnostic_codes_json\",\n \"framing\": \"text\"\n }\n ]\n },\n {\n \"section\": \"food_images\",\n \"sql\": \"SELECT pubkey, d_tag, image_index, raw_tag_json, url, width, height, blossom_sha256, qualifies, diagnostic_codes_json FROM radroots_event_store_food_availability_image WHERE source_generation = ? ORDER BY pubkey, d_tag, image_index\",\n \"fields\": [\n {\n \"name\": \"pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"d_tag\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"image_index\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"raw_tag_json\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"url\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"width\",\n \"framing\": \"optional_i64\"\n },\n {\n \"name\": \"height\",\n \"framing\": \"optional_i64\"\n },\n {\n \"name\": \"blossom_sha256\",\n \"framing\": \"optional_text\"\n },\n {\n \"name\": \"qualifies\",\n \"framing\": \"boolean\"\n },\n {\n \"name\": \"diagnostic_codes_json\",\n \"framing\": \"text\"\n }\n ]\n },\n {\n \"section\": \"food_search\",\n \"sql\": \"SELECT event_id, pubkey, d_tag, title, summary, content, location FROM radroots_event_store_food_availability_search_fts ORDER BY event_id\",\n \"fields\": [\n {\n \"name\": \"event_id\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"pubkey\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"d_tag\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"title\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"summary\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"content\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"location\",\n \"framing\": \"text\"\n }\n ]\n },\n {\n \"section\": \"food_cursor\",\n \"sql\": \"SELECT feed_version, projection_version, scope_fingerprint, hook_manifest_sha256, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1\",\n \"fields\": [\n {\n \"name\": \"feed_version\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"projection_version\",\n \"framing\": \"i64\"\n },\n {\n \"name\": \"scope_fingerprint\",\n \"framing\": \"blob\"\n },\n {\n \"name\": \"hook_manifest_sha256\",\n \"framing\": \"text\"\n },\n {\n \"name\": \"projected_row_count\",\n \"framing\": \"i64\"\n }\n ]\n }\n ]\n },\n \"visibility_oracle\": \"pure_verified_raw_snapshot_direct_indexed_evidence_v1\",\n \"scoped_integrity_mode\": \"event_store_owned_tables_and_indices_v1\",\n \"scoped_integrity_tables\": [\n \"event_envelopes\",\n \"event_envelope_tags\",\n \"event_envelope_head\",\n \"radroots_event_store_source_generation\",\n \"radroots_event_store_source_rebuild_commit_barrier\",\n \"radroots_event_store_source_rebuild_marker\",\n \"radroots_event_store_source_state\",\n \"radroots_event_store_write_lock\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_event_coordinate\",\n \"radroots_event_store_nip09_request\",\n \"radroots_event_store_nip09_event_target\",\n \"radroots_event_store_nip09_address_target\",\n \"radroots_event_store_addressable_head_state\",\n \"radroots_event_store_addressable_head_transition\",\n \"radroots_event_store_addressable_feed_integrity_v1\",\n \"radroots_event_store_food_availability_cursor\",\n \"radroots_event_store_food_availability_projection\",\n \"radroots_event_store_food_availability_image\"\n ],\n \"sqlite_sequence_scope\": \"target_first_after_single_shared_sequence_scan_v1\",\n \"stages\": [\n \"after_marker_open\",\n \"after_generation_rotation\",\n \"after_core_replay\",\n \"after_visibility_audit\",\n \"after_food_reset_replay\",\n \"after_food_audit\",\n \"after_marker_close\"\n ],\n \"failpoints\": [\n \"after_marker_open\",\n \"after_generation_rotation\",\n \"after_core_replay\",\n \"after_visibility_audit\",\n \"after_food_reset_replay\",\n \"after_food_audit\",\n \"after_marker_close\"\n ],\n \"preserved_authorities\": [\n \"legacy_listing\",\n \"trade\",\n \"transport_observation\",\n \"generic_projection_cursor\",\n \"unrelated_caller_state_without_dependencies_on_rebuild_owned_tables\"\n ]\n },\n \"entry_points\": [\n {\n \"role\": \"live_rebuild\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::rebuild_from_raw_v1\"\n },\n {\n \"role\": \"cold_file_repair\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::repair_file_from_raw_v1\"\n },\n {\n \"role\": \"projection_cursor_insert_preflight\",\n \"rust_path\": \"radroots_event_store::nip09::reconciliation_v1::preflight_projection_cursor_insert_v1\"\n },\n {\n \"role\": \"serialized_rebuild_runtime\",\n \"rust_path\": \"radroots_event_store::nip09::reconciliation_v1::raw_source_rebuild::rebuild_from_raw_v1_on_pool\"\n },\n {\n \"role\": \"independent_visibility_oracle\",\n \"rust_path\": \"radroots_event_store::nip09::reconciliation_v1::visibility_oracle_v1::audit_current_visibility_from_raw_v1\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"raw_source_rebuild_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_manifest_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 10836,\n \"sha256\": \"285532dbb0894204843a832880f136ceac5ee312a3203ff951fb0551fac63ec4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"workspace_lockfile_authority\",\n \"path\": \"Cargo.lock\",\n \"byte_length\": 216965,\n \"sha256\": \"f26bf62f77e48914c89c15e689fdbc6799928e9603cab38f50c0c65a0c405edf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nix_flake_app_export_authority\",\n \"path\": \"flake.nix\",\n \"byte_length\": 1835,\n \"sha256\": \"0251b26040cf5338c12dc777a4deaadb8f63eb4e88bc05929dcec67db88ff2bf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nix_input_lock_authority\",\n \"path\": \"flake.lock\",\n \"byte_length\": 3031,\n \"sha256\": \"41b569739bfa0c488625326f4f0a874561601787951cdf7a3f171e60572fa20e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nix_contract_app_routing_authority\",\n \"path\": \"build/nix/apps.nix\",\n \"byte_length\": 2836,\n \"sha256\": \"41a185ac87379e24c1ede09c0f1aac820653dffc09f99cd803b145b44bed982c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nix_contract_test_lane_authority\",\n \"path\": \"build/nix/common.nix\",\n \"byte_length\": 11127,\n \"sha256\": \"b3340e1b4973e6a1e02899d164ca74842757f22b6b1a03f90461532fcd844df5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nix_toolchain_routing_authority\",\n \"path\": \"build/nix/toolchains.nix\",\n \"byte_length\": 178,\n \"sha256\": \"cd664be945e28bf6c25c7758182ff8d01e03248832dfc2c045c01b4f4aff960f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"rust_toolchain_authority\",\n \"path\": \"rust-toolchain.toml\",\n \"byte_length\": 132,\n \"sha256\": \"c33aa38292bab6513bf79ed2f69c1525b736dd738b15ca78af713b70b29265c9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_manifest_authority\",\n \"path\": \"tools/xtask/Cargo.toml\",\n \"byte_length\": 1097,\n \"sha256\": \"7e858f4f33913f986c565be2a31c41615ea0585c9e19572363ef5cae36cafdc9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_dependency_feature_authority\",\n \"path\": \"crates/event_store/Cargo.toml\",\n \"byte_length\": 1529,\n \"sha256\": \"4bddb3462a7543c9a7981ead5cf1027988fc381457432f4b04b0e9c43f6d51ca\",\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\": 24687,\n \"sha256\": \"404f3f91b1b4aed345faf23a2bfd8a59cdf475d5f411d416dd26418c71ea9a89\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 188,\n \"sha256\": \"05328d38ebb6f827f6986b384fefb834948652dfd77fc29c9633d8a1a0d5947e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_generated_descriptor_input\",\n \"path\": \"crates/event_store/src/generated/food_availability_projection_manifest.rs\",\n \"byte_length\": 21437,\n \"sha256\": \"90908da53ab9572f45f5916ccc2652736b7ea26ba6dd202a4f69af1e651b564b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nip09_generated_descriptor_input\",\n \"path\": \"crates/event_store/src/generated/nip09_reconciliation_manifest.rs\",\n \"byte_length\": 586039,\n \"sha256\": \"406a760e9bed1e8fc89c8e7ae0976c7eff844de7427a3f473528c895439500b3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_generated_descriptor_input\",\n \"path\": \"crates/event_store/src/generated/source_maintenance_manifest.rs\",\n \"byte_length\": 18723,\n \"sha256\": \"5f988f800425cf36d4327c828b30943c2f79c1fa577ce80730dc13383a1466b1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 4133,\n \"sha256\": \"7cc60495cd26d1f3d8147b1c6b39db83a170f934f74226a617263c0c13c25ada\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_runtime_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"model_registration\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 33818,\n \"sha256\": \"66d0b7b8d9966084c76d85aa7f79e9ec0d68cde464ea8b0a327404e61eadd8ff\",\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_availability_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\": \"ingest_reconciliation_model\",\n \"path\": \"crates/event_store/src/model/ingest_reconciliation_v1.rs\",\n \"byte_length\": 1626,\n \"sha256\": \"47bf13b3fc0f8a913a660f7d655413de0f6b90568bc4acc510aa6bd741bab47b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"rebuild_report_and_digest_models\",\n \"path\": \"crates/event_store/src/model/raw_source_rebuild_v1.rs\",\n \"byte_length\": 2804,\n \"sha256\": \"a59459b5566f4450576fc5412e3c8ac0153954b653be376ccd925b19fc647345\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"reconciliation_model\",\n \"path\": \"crates/event_store/src/model/reconciliation_v1.rs\",\n \"byte_length\": 11138,\n \"sha256\": \"8a26bc373035878ef9b41767ceea7b681896e17d88bedce118de1d622125e1d6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nip09_module_registration\",\n \"path\": \"crates/event_store/src/nip09.rs\",\n \"byte_length\": 34,\n \"sha256\": \"fbd8a3b36d7f36e7b0d301aee0847d42c3908659f066cafcae3e247d67a75845\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"reconciliation_runtime_registration\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 194622,\n \"sha256\": \"4c14df2bd3af7bfefb002917dc7549f6ada155be3a748acb9cd6d78199ee6f76\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"serialized_raw_source_rebuild\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1/raw_source_rebuild.rs\",\n \"byte_length\": 60973,\n \"sha256\": \"a8db92dfbfa420b545038502c2a04547bed41b76e283f09758faa248c597c781\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nip09_result_vector_executor_input\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1/result_vector_executor.rs\",\n \"byte_length\": 18446,\n \"sha256\": \"ca2a2bf54062aa6ddf2e553fd624c7217a01ad56309487ce73fa58c47c06c208\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"independent_raw_visibility_oracle\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs\",\n \"byte_length\": 36998,\n \"sha256\": \"48b60aba869d804ad7b3b120d7479c7ff45dd3758502a90b4fbce312d9848a99\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"managed_v4_validation_and_scoped_integrity\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 153682,\n \"sha256\": \"df92fc509b44e40dae5a48d03ad9bf5cc556c4319a78215460ca26b899c610a2\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_capacity_rebuild_authority\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51756,\n \"sha256\": \"f8d5b62f0613104aa86658d5bf1baade92c7df83f00ef0cddadd734b9797afca\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_rebuild_and_cold_repair_boundary\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 402744,\n \"sha256\": \"56a84cc05208a335cbb6bad41b024c20ed77db5000fa69e684611e196ae461f4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_storage\",\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_storage\",\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\": \"raw_source_rebuild_focused_tests\",\n \"path\": \"crates/event_store/src/store/raw_source_rebuild_v1_tests.rs\",\n \"byte_length\": 103772,\n \"sha256\": \"383ca6f8aac6418d1d4460603d50746d224011c16367c2b86d8342818567ae2a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"signed_food_digest_fixture\",\n \"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 },\n {\n \"role\": \"food_projection_reset_and_replay\",\n \"path\": \"crates/event_store/src/store/food_availability_projection_v1.rs\",\n \"byte_length\": 50858,\n \"sha256\": \"adc8a3eb59f5bccb4c0d0ba4c5319dbf55cffb5e0c333db8235db63fd0df5f21\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"post_core_extension_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_extension_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_extensions_v1\",\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\": \"post_core_extensions_v2\",\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_storage_v1\",\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 \"role\": \"post_core_storage_v2\",\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\": \"protocol_reconciliation_storage\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 30140,\n \"sha256\": \"210112eeaa6975a3b4fbb97d5c52588f8c6d8d07975e531d39737fd11235de51\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"protocol_storage_boundary\",\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\": \"event_store_package_readme\",\n \"path\": \"crates/event_store/README\",\n \"byte_length\": 22209,\n \"sha256\": \"9e1cf2ec9ba58c2028d78eb33e6355fc2dff3507837b6e8640941dccd5608dc7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"signed_nip09_reconciliation_fixture\",\n \"path\": \"crates/event_store/tests/fixtures/nip09_reconciliation.v1.json\",\n \"byte_length\": 10405,\n \"sha256\": \"31cd9507734ff3308436881622a626b9782b75b548d9f5e159e4125621855b9c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_food_predecessor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 194995,\n \"sha256\": \"02f8b70b3885267b09fd5241ec89bcf020d2975e1eb1c6c533656a036723395b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"immutable_predecessor_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 123766,\n \"sha256\": \"f10962e0cc0fa44dc109d87b707f02be11fe6dad1113707eef820ff3c5ae97ee\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_nip09_predecessor_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 850763,\n \"sha256\": \"852697eaaffcfe99391ffafd0c7c390c8eeb175377ac7e5cd5f490050b57ed58\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_source_rebuild_governance\",\n \"path\": \"tools/xtask/src/contract/raw_source_rebuild.rs\",\n \"byte_length\": 294540,\n \"sha256\": \"543c21131346381a833f9e063fd535efd0d0e192419aefa1f60e9b0f68475866\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 479703,\n \"sha256\": \"72fbd457b0cfdff1e30f07bf2452a0c71c23cef93bdaefffc114defa616d6342\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 15018,\n \"sha256\": \"9aab8db1186b776ba8dcac90cc46c29d96928b610850b084be0e3a25d3e4cb0f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"release_breaking_change_authority\",\n \"path\": \"contracts/releases/1.0.0-alpha.1.toml\",\n \"byte_length\": 19840,\n \"sha256\": \"946d90dacc9db522825898dbb5d9d424b020a22fe53b80ec15eb67c5f1d8cd2d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"release_note_authority\",\n \"path\": \"CHANGELOG.md\",\n \"byte_length\": 28939,\n \"sha256\": \"61b9d2a9050e4123bc5324aebf6e54393b9b1efdc2145a4a2cf6c009a4345b23\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1\",\n \"RadrootsEventStoreCallerInboundForeignKeyV1\",\n \"RadrootsEventStoreActiveProductStateDigestV1\",\n \"RadrootsEventStoreImmutableRawDigestV1\",\n \"RadrootsEventStoreRawSourceRebuildDriftV1\",\n \"RadrootsEventStoreRawSourceRebuildReportV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::rebuild_from_raw_v1\",\n \"RadrootsEventStore::repair_file_from_raw_v1\",\n \"RadrootsEventStoreRawSourceRebuildReportV1::prior_source_generation\",\n \"RadrootsEventStoreRawSourceRebuildReportV1::new_source_generation\",\n \"RadrootsEventStoreRawSourceRebuildReportV1::source_capacity\",\n \"RadrootsEventStoreRawSourceRebuildReportV1::raw_high_water_seq\",\n \"RadrootsEventStoreRawSourceRebuildReportV1::immutable_raw_digest\",\n \"RadrootsEventStoreRawSourceRebuildReportV1::active_product_state_digest\",\n \"RadrootsEventStoreImmutableRawDigestV1::as_bytes\",\n \"RadrootsEventStoreActiveProductStateDigestV1::as_bytes\",\n \"RadrootsEventStoreRawSourceRebuildDriftV1::code\"\n ],\n \"error_variants\": [\n \"ProjectionCursorCapacityExceeded\",\n \"RawSourceRepairDatabaseIdentityMismatch\",\n \"RawSourceRepairCanonicalPathLockDomainMismatch\",\n \"RawSourceRepairMainDatabaseCanonicalizationFailed\",\n \"RawSourceRebuildCallerForeignKeyCapacityExceeded\",\n \"RawSourceRebuildCallerInboundForeignKeyUnsupported\",\n \"RawSourceRebuildCallerTableCapacityExceeded\",\n \"RawSourceRebuildStateDrift\",\n \"RawSourceRebuildTransactionRollbackFailed\"\n ],\n \"drift_kinds\": [\n {\n \"variant\": \"ManagedSchemaAuthority\",\n \"code\": \"managed_schema_authority\"\n },\n {\n \"variant\": \"ImmutableRawAuthority\",\n \"code\": \"immutable_raw_authority\"\n },\n {\n \"variant\": \"SourceGenerationLineage\",\n \"code\": \"source_generation_lineage\"\n },\n {\n \"variant\": \"AddressableTransitionAuthority\",\n \"code\": \"addressable_transition_authority\"\n },\n {\n \"variant\": \"DerivedProductStateAuthority\",\n \"code\": \"derived_product_state_authority\"\n },\n {\n \"variant\": \"RebuildPostcondition\",\n \"code\": \"rebuild_postcondition\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/raw_source_rebuild.v1.json\",\n \"byte_length\": 26833,\n \"sha256\": \"c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.raw_source_rebuild_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs\",\n \"executor_test\": \"raw_source_rebuild_v1_result_vector\",\n \"executor_byte_length\": 25542,\n \"executor_sha256\": \"51647259efdd0d99689ef1db0defb139c8d1f60f2ead69b793ddb2733a28e832\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n"; +pub(crate) const RAW_SOURCE_REBUILD_MANIFEST_BYTE_LENGTH: usize = 45449; +pub(crate) const RAW_SOURCE_REBUILD_MANIFEST_SHA256: &str = + "b8737a9c5836517114e7df6c2194c46e3c200093e12c4e6297165d2b9dae56a1"; +pub(crate) const RAW_SOURCE_REBUILD_CONTRACT_ID: &str = + "radroots_event_store.raw_source_rebuild_v1"; +pub(crate) const RAW_SOURCE_REBUILD_AUTHORITY_ID: &str = "raw_source_rebuild_v1"; +pub(crate) const RAW_SOURCE_REBUILD_PREDECESSOR_MANIFEST_SHA256: &str = + "e8911e6e5710278969cbd15557a5b856b1575dfd11a655711403598370b41221"; +pub(crate) const RAW_SOURCE_REBUILD_EVENT_STORE_SCHEMA_VERSION: u32 = 4; +pub(crate) const RAW_SOURCE_REBUILD_EVENT_CONTRACT_REGISTRY_VERSION: u32 = 7; +pub(crate) const RAW_SOURCE_REBUILD_RESULT_VECTOR_SHA256: &str = + "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15"; +pub(crate) const RAW_SOURCE_REBUILD_RESULT_VECTOR_EXECUTOR_SHA256: &str = + "51647259efdd0d99689ef1db0defb139c8d1f60f2ead69b793ddb2733a28e832"; diff --git a/crates/event_store/src/lib.rs b/crates/event_store/src/lib.rs @@ -20,11 +20,13 @@ mod store; #[cfg(feature = "sqlite")] pub use error::{ + RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1, - RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1, RadrootsEventStoreError, - RadrootsEventStoreSourceCapacityResourceV1, + RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1, + RadrootsEventStoreCallerInboundForeignKeyV1, RadrootsEventStoreError, + RadrootsEventStoreRawSourceRebuildDriftV1, RadrootsEventStoreSourceCapacityResourceV1, }; #[cfg(feature = "sqlite")] pub use migrations::{ @@ -51,11 +53,13 @@ pub use model::{ RadrootsAddressableTransitionV1, RadrootsAddressableTransitionVisibilityV1, RadrootsCurrentEventVisibilityV1, RadrootsCurrentVisibilityDecisionV1, RadrootsEventAdmissionStatus, RadrootsEventIngest, RadrootsEventIngestReceipt, - RadrootsEventPersistence, RadrootsEventStoreSourceGeneration, RadrootsEventStoreStatusSummary, - RadrootsEventVisibility, RadrootsFoodAvailabilitySearchQueryV1, - RadrootsFoodAvailabilityStatusFilterV1, RadrootsNip09SuppressionEvidenceV1, - RadrootsNip09SuppressionOutcome, RadrootsNip09SuppressionReason, RadrootsProjectionCursor, - RadrootsProjectionRebuildPrior, RadrootsProjectionRebuildTicket, RadrootsRawHeadDecision, + RadrootsEventPersistence, RadrootsEventStoreActiveProductStateDigestV1, + RadrootsEventStoreImmutableRawDigestV1, RadrootsEventStoreRawSourceRebuildReportV1, + RadrootsEventStoreSourceGeneration, RadrootsEventStoreStatusSummary, RadrootsEventVisibility, + RadrootsFoodAvailabilitySearchQueryV1, RadrootsFoodAvailabilityStatusFilterV1, + RadrootsNip09SuppressionEvidenceV1, RadrootsNip09SuppressionOutcome, + RadrootsNip09SuppressionReason, RadrootsProjectionCursor, RadrootsProjectionRebuildPrior, + RadrootsProjectionRebuildTicket, RadrootsRawHeadDecision, RadrootsStoreProducedCanonicalEventV1, RadrootsStoredEventTag, RadrootsStoredFoodAvailabilityImageV1, RadrootsStoredFoodAvailabilityV1, RadrootsStoredRawEvent, RadrootsStoredRawEventHead, RadrootsStoredSellerReservation, diff --git a/crates/event_store/src/model.rs b/crates/event_store/src/model.rs @@ -2,6 +2,7 @@ mod addressable_transition_feed_v1; mod current_visibility_v1; mod food_availability_projection_v1; mod ingest_reconciliation_v1; +mod raw_source_rebuild_v1; pub(crate) mod reconciliation_v1; pub use addressable_transition_feed_v1::{ @@ -32,6 +33,10 @@ pub use food_availability_projection_v1::{ RadrootsFoodAvailabilityStatusFilterV1, RadrootsStoredFoodAvailabilityImageV1, RadrootsStoredFoodAvailabilityV1, }; +pub use raw_source_rebuild_v1::{ + RadrootsEventStoreActiveProductStateDigestV1, RadrootsEventStoreImmutableRawDigestV1, + RadrootsEventStoreRawSourceRebuildReportV1, +}; use crate::RadrootsEventStoreError; use radroots_event::RadrootsEventKind; diff --git a/crates/event_store/src/model/raw_source_rebuild_v1.rs b/crates/event_store/src/model/raw_source_rebuild_v1.rs @@ -0,0 +1,76 @@ +use super::RadrootsEventStoreSourceGeneration; +use crate::RadrootsEventStoreSourceCapacityV1; + +/// SHA-256 digest of the ordered immutable raw-event and raw-tag authority. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct RadrootsEventStoreImmutableRawDigestV1(pub(crate) [u8; 32]); + +impl RadrootsEventStoreImmutableRawDigestV1 { + pub(crate) const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Returns the fixed-width digest bytes. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// SHA-256 digest of generation-normalized active product state. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct RadrootsEventStoreActiveProductStateDigestV1(pub(crate) [u8; 32]); + +impl RadrootsEventStoreActiveProductStateDigestV1 { + pub(crate) const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Returns the fixed-width digest bytes. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// Committed result of rebuilding all active product state from immutable raw rows. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RadrootsEventStoreRawSourceRebuildReportV1 { + pub(crate) prior_source_generation: RadrootsEventStoreSourceGeneration, + pub(crate) new_source_generation: RadrootsEventStoreSourceGeneration, + pub(crate) source_capacity: RadrootsEventStoreSourceCapacityV1, + pub(crate) immutable_raw_digest: RadrootsEventStoreImmutableRawDigestV1, + pub(crate) active_product_state_digest: RadrootsEventStoreActiveProductStateDigestV1, +} + +impl RadrootsEventStoreRawSourceRebuildReportV1 { + /// Returns the active generation replaced by this rebuild. + pub const fn prior_source_generation(&self) -> RadrootsEventStoreSourceGeneration { + self.prior_source_generation + } + + /// Returns the generation committed by this rebuild. + pub const fn new_source_generation(&self) -> RadrootsEventStoreSourceGeneration { + self.new_source_generation + } + + /// Returns the raw-source capacity seal committed for the new generation. + pub const fn source_capacity(&self) -> RadrootsEventStoreSourceCapacityV1 { + self.source_capacity + } + + /// Returns the greatest retained raw event sequence. + pub const fn raw_high_water_seq(&self) -> i64 { + self.source_capacity.raw_high_water_seq() + } + + /// Returns the digest of ordered immutable raw authority. + pub const fn immutable_raw_digest(&self) -> RadrootsEventStoreImmutableRawDigestV1 { + self.immutable_raw_digest + } + + /// Returns the generation-normalized active product-state digest. + pub const fn active_product_state_digest( + &self, + ) -> RadrootsEventStoreActiveProductStateDigestV1 { + self.active_product_state_digest + } +} diff --git a/crates/event_store/src/nip09/reconciliation_v1.rs b/crates/event_store/src/nip09/reconciliation_v1.rs @@ -12,6 +12,7 @@ use crate::model::reconciliation_v1::{ RadrootsRawHeadDecision, StoredEventClass, tag_semantic_name, tag_value_type_name, }; use crate::{ + RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1, @@ -31,8 +32,10 @@ use radroots_event_codec::admission::registry_v7::{ use radroots_event_codec::deletion::reconciliation_v1::admission::{ RadrootsAdmittedNip09DeletionRequestEventV1, admit_verified_nip09_deletion_request_event_v1, }; +#[cfg(test)] +use radroots_event_codec::deletion::reconciliation_v1::evaluator::evaluate_nip09_suppression_from_borrowed_requests_v1; use radroots_event_codec::deletion::reconciliation_v1::evaluator::{ - RadrootsNip09SuppressionOutcome, evaluate_nip09_suppression_from_borrowed_requests_v1, + RadrootsNip09SuppressionOutcome, RadrootsNip09SuppressionReason, }; use radroots_event_codec::verification::v1::RadrootsSignatureVerifiedEvent; #[cfg(test)] @@ -40,8 +43,20 @@ use sqlx::SqlitePool; use sqlx::{Row, SqliteConnection}; use std::collections::{BTreeMap, BTreeSet}; +mod raw_source_rebuild; #[cfg(test)] mod result_vector_executor; +mod visibility_oracle_v1; + +#[cfg(test)] +pub(crate) use raw_source_rebuild::{ + RawSourceRebuildFailpointV1, preserve_raw_source_rebuild_primary_failure_for_test, + rebuild_from_raw_v1_in_transaction_for_test, rebuild_from_raw_v1_on_pool_for_test, + rebuild_from_raw_v1_on_pool_with_caller_schema_limits_for_test, +}; +pub(crate) use raw_source_rebuild::{ + rebuild_from_raw_v1_in_existing_transaction, rebuild_from_raw_v1_on_pool, +}; const RECONCILIATION_SNAPSHOT_BATCH_SIZE: i64 = 512; const RECONCILIATION_SNAPSHOT_BATCH_LEN: usize = 512; @@ -318,17 +333,25 @@ struct SourceRebuildPlan { prior: Option<SourceState>, } -struct RequestIndex<'a> { - requests: &'a [RadrootsAdmittedNip09DeletionRequestEventV1], - event_targets: BTreeMap<String, Vec<usize>>, - address_targets: BTreeMap<(i64, String, String), Vec<usize>>, +struct SourceRebuildMarkerTokenV1 { + generation: RadrootsEventStoreSourceGeneration, } -struct MergedRequestIndices<'a> { - event_indices: &'a [usize], - address_indices: &'a [usize], - event_position: usize, - address_position: usize, +struct RequestIndex { + event_targets: BTreeMap<String, BTreeMap<String, String>>, + address_targets: BTreeMap<(i64, String, String), AddressRequestEvidence>, +} + +#[derive(Clone)] +struct IndexedRequestEvidence { + request_id: String, + created_at: u64, +} + +#[derive(Default)] +struct AddressRequestEvidence { + authorized: Option<IndexedRequestEvidence>, + unauthorized: bool, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -389,6 +412,7 @@ struct EventCoordinateFact { nip09_d_tag: Option<String>, } +#[derive(Debug, PartialEq, Eq)] struct StoredSuppressionDecision { outcome: RadrootsNip09SuppressionOutcome, reason: &'static str, @@ -397,95 +421,125 @@ struct StoredSuppressionDecision { address_reference_cutoff: Option<i64>, } -impl<'a> RequestIndex<'a> { - fn new(requests: &'a [RadrootsAdmittedNip09DeletionRequestEventV1]) -> Self { - let mut event_targets = BTreeMap::<String, Vec<usize>>::new(); - let mut address_targets = BTreeMap::<(i64, String, String), Vec<usize>>::new(); - for (index, request) in requests.iter().enumerate() { - for target in request.projection().event_targets() { - event_targets - .entry(target.event_id().as_str().to_owned()) - .or_default() - .push(index); - } - for target in request.projection().address_targets() { - address_targets - .entry(( - i64::from(target.coordinate().kind()), - target.coordinate().pubkey().as_str().to_owned(), - target.coordinate().identifier().to_owned(), - )) - .or_default() - .push(index); - } +impl RequestIndex { + fn new(requests: &[RadrootsAdmittedNip09DeletionRequestEventV1]) -> Self { + let mut index = Self { + event_targets: BTreeMap::new(), + address_targets: BTreeMap::new(), + }; + for request in requests { + index.insert(request); } - Self { - requests, - event_targets, - address_targets, + index + } + + fn insert(&mut self, request: &RadrootsAdmittedNip09DeletionRequestEventV1) { + let request_event = request.event(); + let request_author = request_event.author_str(); + let request_id = request_event.id_str(); + for target in request.projection().event_targets() { + self.event_targets + .entry(target.event_id().as_str().to_owned()) + .or_default() + .entry(request_author.to_owned()) + .and_modify(|current| { + if request_id < current.as_str() { + *current = request_id.to_owned(); + } + }) + .or_insert_with(|| request_id.to_owned()); + } + for target in request.projection().address_targets() { + let coordinate = ( + i64::from(target.coordinate().kind()), + target.coordinate().pubkey().as_str().to_owned(), + target.coordinate().identifier().to_owned(), + ); + let evidence = self.address_targets.entry(coordinate.clone()).or_default(); + if request_author == coordinate.1 { + let replace = evidence.authorized.as_ref().is_none_or(|current| { + request_event.created_at_u64() > current.created_at + || (request_event.created_at_u64() == current.created_at + && request_id < current.request_id.as_str()) + }); + if replace { + evidence.authorized = Some(IndexedRequestEvidence { + request_id: request_id.to_owned(), + created_at: request_event.created_at_u64(), + }); + } + } else { + evidence.unauthorized = true; + } } } - fn matching<'index>( - &'index self, + fn decision( + &self, event: &RadrootsEventEnvelope, - ) -> impl Iterator<Item = &'index RadrootsAdmittedNip09DeletionRequestEventV1> + 'index { - let event_indices = self + ) -> Result<StoredSuppressionDecision, RadrootsEventStoreError> { + if event.kind_u32() == 5 { + return Ok(StoredSuppressionDecision { + outcome: RadrootsNip09SuppressionOutcome::Visible, + reason: RadrootsNip09SuppressionReason::DeletionRequestImmune.code(), + event_reference_request_id: None, + address_reference_request_id: None, + address_reference_cutoff: None, + }); + } + + let (event_reference_request_id, unauthorized_event_reference) = self .event_targets .get(event.id_str()) - .map(Vec::as_slice) - .unwrap_or_default(); - let address_indices = nip01_coordinate_key(event) + .map_or((None, false), |by_author| { + let authorized = by_author.get(event.author_str()).cloned(); + let unauthorized = by_author.len() > usize::from(authorized.is_some()); + (authorized, unauthorized) + }); + let address_evidence = nip01_coordinate_key(event) .as_ref() - .and_then(|coordinate| self.address_targets.get(coordinate)) - .map(Vec::as_slice) - .unwrap_or_default(); - MergedRequestIndices::new(event_indices, address_indices).map(|index| &self.requests[index]) - } -} - -impl<'a> MergedRequestIndices<'a> { - const fn new(event_indices: &'a [usize], address_indices: &'a [usize]) -> Self { - Self { - event_indices, - address_indices, - event_position: 0, - address_position: 0, - } - } -} - -impl Iterator for MergedRequestIndices<'_> { - type Item = usize; - - fn next(&mut self) -> Option<Self::Item> { - match ( - self.event_indices.get(self.event_position).copied(), - self.address_indices.get(self.address_position).copied(), - ) { - (Some(event_index), Some(address_index)) if event_index < address_index => { - self.event_position += 1; - Some(event_index) - } - (Some(event_index), Some(address_index)) if address_index < event_index => { - self.address_position += 1; - Some(address_index) - } - (Some(index), Some(_)) => { - self.event_position += 1; - self.address_position += 1; - Some(index) - } - (Some(index), None) => { - self.event_position += 1; - Some(index) - } - (None, Some(index)) => { - self.address_position += 1; - Some(index) - } - (None, None) => None, - } + .and_then(|coordinate| self.address_targets.get(coordinate)); + let address_reference = address_evidence.and_then(|evidence| evidence.authorized.as_ref()); + let has_unauthorized_reference = unauthorized_event_reference + || address_evidence.is_some_and(|evidence| evidence.unauthorized); + let address_applies = + address_reference.is_some_and(|evidence| event.created_at_u64() <= evidence.created_at); + let (outcome, reason) = match (event_reference_request_id.is_some(), address_applies) { + (true, true) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdAndAddressReference, + ), + (true, false) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdReference, + ), + (false, true) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, + ), + (false, false) if address_reference.is_some() => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, + ), + (false, false) if has_unauthorized_reference => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::RequestAuthorMismatch, + ), + (false, false) => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::NoAuthorizedReference, + ), + }; + Ok(StoredSuppressionDecision { + outcome, + reason: reason.code(), + event_reference_request_id, + address_reference_request_id: address_reference + .map(|evidence| evidence.request_id.clone()), + address_reference_cutoff: address_reference + .map(|evidence| i64_from_u64("address_reference_cutoff", evidence.created_at)) + .transpose()?, + }) } } @@ -595,7 +649,7 @@ pub(crate) async fn apply_reconciliation_hook( prior, }; - open_source_rebuild_marker(connection, &plan).await?; + let marker = open_source_rebuild_marker(connection, &plan).await?; append_source_generation(connection, &plan).await?; rotate_source_state(connection, &plan).await?; reconcile_raw_events(connection, &events).await?; @@ -631,7 +685,7 @@ pub(crate) async fn apply_reconciliation_hook( ) .await?; } - close_source_rebuild_marker(connection, plan.generation).await?; + close_source_rebuild_marker(connection, marker).await?; validate_sqlite_integrity_after_rebuild(connection).await?; validate_active_hook_state_fast(connection).await } @@ -639,7 +693,7 @@ pub(crate) async fn apply_reconciliation_hook( async fn open_source_rebuild_marker( connection: &mut SqliteConnection, plan: &SourceRebuildPlan, -) -> Result<(), RadrootsEventStoreError> { +) -> Result<SourceRebuildMarkerTokenV1, RadrootsEventStoreError> { let prior_generation = plan .prior .as_ref() @@ -667,7 +721,10 @@ async fn open_source_rebuild_marker( .bind(plan.prior.as_ref().map(|state| state.last_transition_seq)) .execute(&mut *connection) .await?; - require_expected_insert(inserted.rows_affected(), "source rebuild marker") + require_expected_insert(inserted.rows_affected(), "source rebuild marker")?; + Ok(SourceRebuildMarkerTokenV1 { + generation: plan.generation, + }) } async fn append_source_generation( @@ -732,12 +789,12 @@ async fn rotate_source_state( async fn close_source_rebuild_marker( connection: &mut SqliteConnection, - generation: RadrootsEventStoreSourceGeneration, + marker: SourceRebuildMarkerTokenV1, ) -> Result<(), RadrootsEventStoreError> { let deleted = sqlx::query( "DELETE FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1 AND target_generation = ?", ) - .bind(generation.as_bytes().as_slice()) + .bind(marker.generation.as_bytes().as_slice()) .execute(&mut *connection) .await?; if deleted.rows_affected() != 1 { @@ -808,6 +865,29 @@ async fn validate_rebuild_hook_state_with_events( validate_hook_state_with_events(connection, &state, events).await } +async fn validate_raw_source_rebuild_core_with_events_v1( + connection: &mut SqliteConnection, + generation: RadrootsEventStoreSourceGeneration, + events: &[ReconciledEvent], +) -> Result<(), RadrootsEventStoreError> { + validate_active_rebuild_marker(connection, generation).await?; + let state = read_source_state(connection).await?; + if state.generation != generation { + return hook_drift( + "open rebuild marker target does not match active source generation".to_owned(), + ); + } + validate_source_raw_authority_with_state(connection, &state).await?; + validate_transition_interval_full(connection, &state).await?; + validate_derived_event_storage(connection, events).await?; + validate_raw_heads(connection, events).await?; + validate_event_coordinate_facts(connection, state.generation, events).await?; + let requests = validate_nip09_fact_graph(connection, state.generation, events).await?; + validate_addressable_state(connection, state.generation, events, &requests).await?; + validate_transition_history(connection, &state, events).await?; + validate_latest_transitions_match_state(connection, state.generation).await +} + async fn validate_hook_state_with_events( connection: &mut SqliteConnection, state: &SourceState, @@ -828,8 +908,8 @@ pub(crate) async fn validate_active_hook_state_fast( connection: &mut SqliteConnection, ) -> Result<(), RadrootsEventStoreError> { // 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. + // constant-cost authority bounds; full history/state checks remain part of + // migration and rebuild audits, while cursor inventory is migration-only. validate_rebuild_marker_absent(connection).await?; validate_structural_source_state_fast(connection) .await @@ -976,12 +1056,25 @@ async fn validate_active_rebuild_marker( async fn validate_projection_cursor_authority( connection: &mut SqliteConnection, ) -> Result<(), RadrootsEventStoreError> { + let probe_limit = i64::from(RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1) + 1; + let cursor_probe = sqlx::query("SELECT 1 FROM projection_cursor LIMIT ?") + .bind(probe_limit) + .fetch_all(&mut *connection) + .await?; + validate_projection_cursor_cardinality_v1(cursor_probe.len())?; + let identity_probe = + sqlx::query("SELECT 1 FROM radroots_event_store_projection_cursor_source LIMIT ?") + .bind(probe_limit) + .fetch_all(&mut *connection) + .await?; + validate_projection_cursor_cardinality_v1(identity_probe.len())?; + let raw_high_water: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(seq), 0) FROM event_envelopes") .fetch_one(&mut *connection) .await?; - let invalid_count: i64 = sqlx::query_scalar( - "SELECT COUNT(*) + let invalid: Option<i64> = sqlx::query_scalar( + "SELECT 1 FROM projection_cursor AS cursor LEFT JOIN radroots_event_store_projection_cursor_source AS source ON source.projection_id = cursor.projection_id @@ -1004,29 +1097,61 @@ async fn validate_projection_cursor_authority( typeof(source.source_generation) != 'blob' OR length(source.source_generation) != 32 ) - )", + ) + LIMIT 1", ) .bind(raw_high_water) - .fetch_one(&mut *connection) + .fetch_optional(&mut *connection) .await?; - if invalid_count != 0 { - return hook_drift(format!( - "{invalid_count} projection cursor identities are invalid or ahead of raw source authority" - )); + if invalid.is_some() { + return hook_drift( + "a projection cursor identity is invalid or ahead of raw source authority".to_owned(), + ); } - let orphan_identity_count: i64 = sqlx::query_scalar( - "SELECT COUNT(*) + let orphan_identity: Option<i64> = sqlx::query_scalar( + "SELECT 1 FROM radroots_event_store_projection_cursor_source AS source LEFT JOIN projection_cursor AS cursor ON cursor.projection_id = source.projection_id - WHERE cursor.projection_id IS NULL", + WHERE cursor.projection_id IS NULL + LIMIT 1", ) - .fetch_one(&mut *connection) + .fetch_optional(&mut *connection) .await?; - if orphan_identity_count != 0 { - return hook_drift(format!( - "{orphan_identity_count} projection cursor source identities have no cursor" - )); + if orphan_identity.is_some() { + return hook_drift("a projection cursor source identity has no cursor".to_owned()); + } + Ok(()) +} + +pub(crate) async fn preflight_projection_cursor_insert_v1( + connection: &mut SqliteConnection, +) -> Result<(), RadrootsEventStoreError> { + let rows = sqlx::query("SELECT 1 FROM projection_cursor LIMIT ?") + .bind(i64::from( + RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1, + )) + .fetch_all(&mut *connection) + .await?; + if rows.len() + >= usize::try_from(RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1) + .unwrap_or(usize::MAX) + { + return Err(RadrootsEventStoreError::ProjectionCursorCapacityExceeded { + current: RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1, + limit: RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1, + }); + } + Ok(()) +} + +fn validate_projection_cursor_cardinality_v1( + observed: usize, +) -> Result<(), RadrootsEventStoreError> { + let limit = RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1; + let current = u32::try_from(observed).unwrap_or(u32::MAX); + if current > limit { + return Err(RadrootsEventStoreError::ProjectionCursorCapacityExceeded { current, limit }); } Ok(()) } @@ -2609,7 +2734,7 @@ fn desired_addressable_states( d_tag.as_str(), winner.event_seq, event, - request_index.matching(event.verified_event.event()), + &request_index, )?; desired.insert((i64::from(kind), pubkey.to_string(), d_tag), state); } @@ -2803,7 +2928,8 @@ fn expected_transition_history( .iter() .map(|event| (event.verified_event.event().id_str(), event)) .collect::<BTreeMap<_, _>>(); - let mut requests = admitted_nip09_requests(&baseline_events)?; + let requests = admitted_nip09_requests(&baseline_events)?; + let mut request_index = RequestIndex::new(&requests); let mut winners = select_raw_head_winners(&baseline_events); let mut states = desired_addressable_states(&baseline_events, &requests)?; let mut transitions = Vec::new(); @@ -2828,34 +2954,12 @@ fn expected_transition_history( && event.verified_event.event().kind_u32() == 5 { let request = admitted_nip09_request(event)?; - for (coordinate, winner) in &winners { - let RadrootsEventHeadCoordinate::Addressable { - kind, - pubkey, - d_tag, - } = coordinate - else { - continue; - }; - let target = event_by_id - .get(winner.candidate.event_id.as_str()) - .ok_or_else(|| RadrootsEventStoreError::MigrationHookStateDrift { - hook_id: NIP09_HOOK_ID, - reason: format!( - "raw head `{}` has no reconciled event", - winner.candidate.event_id - ), - })?; - if request_references_event(&request, target.verified_event.event()) { - affected_coordinates.insert(( - i64::from(*kind), - pubkey.to_string(), - d_tag.clone(), - )); - } - } - requests.push(request); - requests.sort_by(|left, right| left.event().id().cmp(right.event().id())); + affected_coordinates.extend(request_affected_addressable_coordinates( + &request, + &winners, + &event_by_id, + )); + request_index.insert(&request); } else if matches!(raw_head_decision, RadrootsRawHeadDecision::Applied) && let RadrootsEventHeadCandidateResult::Candidate(candidate) = event_head_candidate_for_nip01_event_v1(event.verified_event.event()) @@ -2868,7 +2972,6 @@ fn expected_transition_history( affected_coordinates.insert((i64::from(kind), pubkey.to_string(), d_tag)); } - let request_index = RequestIndex::new(&requests); for (kind, pubkey, d_tag) in affected_coordinates { let key = (kind, pubkey.clone(), d_tag.clone()); let coordinate = RadrootsEventHeadCoordinate::Addressable { @@ -2896,14 +2999,13 @@ fn expected_transition_history( winner.candidate.event_id ), })?; - let matching = request_index.matching(target.verified_event.event()); let desired = addressable_state_for_event( kind, &pubkey, &d_tag, winner.event_seq, target, - matching, + &request_index, )?; let prior = states.get(&key); if prior == Some(&desired) { @@ -2962,22 +3064,56 @@ fn admitted_nip09_request( }) } -fn request_references_event( +fn request_affected_addressable_coordinates<'a>( request: &RadrootsAdmittedNip09DeletionRequestEventV1, - event: &RadrootsEventEnvelope, -) -> bool { - request - .projection() - .event_targets() - .iter() - .any(|target| target.event_id() == event.id()) - || nip01_coordinate_key(event).is_some_and(|(kind, pubkey, d_tag)| { - request.projection().address_targets().iter().any(|target| { - i64::from(target.coordinate().kind()) == kind - && target.coordinate().pubkey().as_str() == pubkey - && target.coordinate().identifier() == d_tag - }) - }) + winners: &BTreeMap<RadrootsEventHeadCoordinate, RawHeadWinner>, + event_by_id: &BTreeMap<&'a str, &'a ReconciledEvent>, +) -> BTreeSet<(i64, String, String)> { + let mut affected = BTreeSet::new(); + for target in request.projection().event_targets() { + let Some(event) = event_by_id.get(target.event_id().as_str()) else { + continue; + }; + let RadrootsEventHeadCandidateResult::Candidate(candidate) = + event_head_candidate_for_nip01_event_v1(event.verified_event.event()) + else { + continue; + }; + let coordinate = &candidate.coordinate; + let RadrootsEventHeadCoordinate::Addressable { + kind, + pubkey, + d_tag, + } = coordinate + else { + continue; + }; + if winners + .get(coordinate) + .is_some_and(|winner| winner.candidate.event_id == candidate.event_id) + { + affected.insert((i64::from(*kind), pubkey.to_string(), d_tag.clone())); + } + } + for target in request.projection().address_targets() { + let kind = target.coordinate().kind(); + if !(30_000..=39_999).contains(&kind) { + continue; + } + let coordinate = RadrootsEventHeadCoordinate::Addressable { + kind, + pubkey: target.coordinate().pubkey().clone(), + d_tag: target.coordinate().identifier().to_owned(), + }; + if winners.contains_key(&coordinate) { + affected.insert(( + i64::from(kind), + target.coordinate().pubkey().as_str().to_owned(), + target.coordinate().identifier().to_owned(), + )); + } + } + affected } fn addressable_transition_fact( @@ -3018,34 +3154,26 @@ fn addressable_transition_fact( } } -fn addressable_state_for_event<'a>( +fn addressable_state_for_event( kind: i64, pubkey: &str, d_tag: &str, event_seq: i64, event: &ReconciledEvent, - requests: impl IntoIterator<Item = &'a RadrootsAdmittedNip09DeletionRequestEventV1>, + request_index: &RequestIndex, ) -> Result<AddressableHeadState, RadrootsEventStoreError> { let mut state = addressable_state_base(kind, pubkey, d_tag, event_seq, event)?; if event.admission.status != RadrootsEventAdmissionStatus::Admitted { return Ok(state); } - let decision = - evaluate_nip09_suppression_from_borrowed_requests_v1(&event.verified_event, requests); - state.nip09_outcome = Some(decision.outcome().code().to_owned()); - state.nip09_reason = Some(decision.reason().code().to_owned()); - state.event_reference_request_id = decision - .event_reference() - .map(|evidence| evidence.request_id().as_str().to_owned()); - if let Some(evidence) = decision.address_reference() { - state.address_reference_request_id = Some(evidence.request_id().as_str().to_owned()); - state.address_reference_cutoff = Some(i64_from_u64( - "address_reference_cutoff", - evidence.inclusive_cutoff(), - )?); - } - state.visibility = match decision.outcome() { + let decision = request_index.decision(event.verified_event.event())?; + state.nip09_outcome = Some(decision.outcome.code().to_owned()); + state.nip09_reason = Some(decision.reason.to_owned()); + state.event_reference_request_id = decision.event_reference_request_id; + state.address_reference_request_id = decision.address_reference_request_id; + state.address_reference_cutoff = decision.address_reference_cutoff; + state.visibility = match decision.outcome { RadrootsNip09SuppressionOutcome::Visible => "visible", RadrootsNip09SuppressionOutcome::Suppressed => "suppressed", } @@ -4003,9 +4131,9 @@ INSERT INTO radroots_event_store_owned_child_probe(id, parent_id) VALUES (1, 999 } #[test] - fn request_index_borrows_one_maximum_shape_request_across_all_target_heads() { + fn request_index_reduces_maximum_shape_requests_once_and_decides_by_lookup() { let author = fixture_author(); - let request_tags = (0..RADROOTS_NIP09_DELETION_TAG_MAX_COUNT) + let address_tags = (0..RADROOTS_NIP09_DELETION_TAG_MAX_COUNT) .map(|index| { vec![ "a".to_owned(), @@ -4013,39 +4141,59 @@ INSERT INTO radroots_event_store_owned_child_probe(id, parent_id) VALUES (1, 999 ] }) .collect::<Vec<_>>(); - let request = admitted_request(REQUEST_CREATED_AT, request_tags, "maximum fanout"); + let address_request = + admitted_request(REQUEST_CREATED_AT, address_tags, "maximum address fanout"); + let event_tags = (0..RADROOTS_NIP09_DELETION_TAG_MAX_COUNT) + .map(|index| vec!["e".to_owned(), format!("{:064x}", index + 1)]) + .collect::<Vec<_>>(); + let event_request = + admitted_request(REQUEST_CREATED_AT + 1, event_tags, "maximum event fanout"); + assert_eq!( + address_request.event().tag_slices().len(), + RADROOTS_NIP09_DELETION_TAG_MAX_COUNT + ); assert_eq!( - request.event().tag_slices().len(), + address_request.projection().address_targets().len(), RADROOTS_NIP09_DELETION_TAG_MAX_COUNT ); assert_eq!( - request.projection().address_targets().len(), + event_request.projection().event_targets().len(), RADROOTS_NIP09_DELETION_TAG_MAX_COUNT ); - let request_id = request.event().id_str().to_owned(); - let requests = vec![request]; + let request_id = address_request.event().id_str().to_owned(); + let requests = vec![address_request, event_request]; let request_index = RequestIndex::new(&requests); - assert!(request_index.event_targets.is_empty()); assert_eq!( - request_index.address_targets.len(), + request_index.event_targets.len(), RADROOTS_NIP09_DELETION_TAG_MAX_COUNT ); assert_eq!( + request_index.address_targets.len(), + RADROOTS_NIP09_DELETION_TAG_MAX_COUNT + ); + assert!( request_index .address_targets .values() - .map(Vec::len) - .sum::<usize>(), - RADROOTS_NIP09_DELETION_TAG_MAX_COUNT + .all(|evidence| { evidence.authorized.is_some() && !evidence.unauthorized }) ); for index in 0..RADROOTS_NIP09_DELETION_TAG_MAX_COUNT { let target = unsigned_addressable_target(author.as_str(), index); - let mut matching = request_index.matching(&target); - let matched = matching.next().expect("indexed request"); - assert!(core::ptr::eq(matched, &requests[0])); - assert!(matching.next().is_none()); + let decision = request_index.decision(&target).expect("indexed decision"); + assert_eq!( + decision.outcome, + RadrootsNip09SuppressionOutcome::Suppressed + ); + assert_eq!( + decision.reason, + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff.code() + ); + assert_eq!( + decision.address_reference_request_id.as_deref(), + Some(request_id.as_str()) + ); } let identifier = fanout_identifier(0); @@ -4072,7 +4220,7 @@ INSERT INTO radroots_event_store_owned_child_probe(id, parent_id) VALUES (1, 999 identifier.as_str(), event.seq, &event, - request_index.matching(event.verified_event.event()), + &request_index, ) .expect("suppressed addressable state"); @@ -4092,22 +4240,28 @@ INSERT INTO radroots_event_store_owned_child_probe(id, parent_id) VALUES (1, 999 } #[test] - fn request_index_merges_event_and_address_indices_once_in_canonical_order() { + fn request_index_reduces_event_and_address_evidence_canonically() { let author = fixture_author(); - let target = unsigned_addressable_target(author.as_str(), 0); + let target = verify_nip01_event_v1(signed_event( + TARGET_CREATED_AT, + KIND_LIST_SET_RELAY, + vec![vec!["d".to_owned(), fanout_identifier(0)]], + "{}", + )) + .expect("verified target"); let target_coordinate = coordinate(author.as_str(), fanout_identifier(0).as_str()); let mut requests = vec![ admitted_request( REQUEST_CREATED_AT, vec![ - vec!["e".to_owned(), target.id_str().to_owned()], + vec!["e".to_owned(), target.event().id_str().to_owned()], vec!["a".to_owned(), target_coordinate.clone()], ], "both", ), admitted_request( REQUEST_CREATED_AT + 1, - vec![vec!["e".to_owned(), target.id_str().to_owned()]], + vec![vec!["e".to_owned(), target.event().id_str().to_owned()]], "event", ), admitted_request( @@ -4118,29 +4272,83 @@ INSERT INTO radroots_event_store_owned_child_probe(id, parent_id) VALUES (1, 999 ]; requests.sort_by(|left, right| left.event().id().cmp(right.event().id())); let request_index = RequestIndex::new(&requests); - let coordinate_key = nip01_coordinate_key(&target).expect("target coordinate"); + let coordinate_key = nip01_coordinate_key(target.event()).expect("target coordinate"); assert_eq!( request_index .event_targets - .get(target.id_str()) + .get(target.event().id_str()) .expect("event indices") .len(), - 2 + 1 + ); + let address_evidence = request_index + .address_targets + .get(&coordinate_key) + .expect("address evidence"); + assert!(address_evidence.authorized.is_some()); + assert!(!address_evidence.unauthorized); + let expected_event_request_id = requests + .iter() + .filter(|request| { + request + .projection() + .event_targets() + .iter() + .any(|event_target| event_target.event_id().as_str() == target.event().id_str()) + }) + .map(|request| request.event().id_str()) + .min() + .expect("event evidence"); + let expected_address_request_id = requests + .iter() + .filter(|request| { + request + .projection() + .address_targets() + .iter() + .any(|address_target| { + i64::from(address_target.coordinate().kind()) == coordinate_key.0 + && address_target.coordinate().pubkey().as_str() + == coordinate_key.1.as_str() + && address_target.coordinate().identifier() == coordinate_key.2.as_str() + }) + }) + .max_by(|left, right| { + left.event() + .created_at_u64() + .cmp(&right.event().created_at_u64()) + .then_with(|| right.event().id().cmp(left.event().id())) + }) + .map(|request| request.event().id_str()) + .expect("address evidence"); + let decision = request_index + .decision(target.event()) + .expect("indexed decision"); + assert_eq!( + decision.reason, + RadrootsNip09SuppressionReason::EventIdAndAddressReference.code() ); assert_eq!( - request_index - .address_targets - .get(&coordinate_key) - .expect("address indices") - .len(), - 2 + decision.event_reference_request_id.as_deref(), + Some(expected_event_request_id) ); - let matching = request_index.matching(&target).collect::<Vec<_>>(); - assert_eq!(matching.len(), 3); - for (expected, actual) in requests.iter().zip(matching) { - assert!(core::ptr::eq(expected, actual)); - } + assert_eq!( + decision.address_reference_request_id.as_deref(), + Some(expected_address_request_id) + ); + + let mut reversed = requests.clone(); + reversed.reverse(); + reversed.push(requests[0].clone()); + assert_eq!( + RequestIndex::new(&reversed) + .decision(target.event()) + .expect("reversed repeated decision"), + decision + ); + assert_request_index_matches_protocol(&target, &requests, &request_index); + assert_request_index_matches_protocol(&target, &reversed, &RequestIndex::new(&reversed)); } type RawEventRows = Vec<( @@ -4508,6 +4716,35 @@ INSERT INTO caller_child(id, parent_id) VALUES (1, 999);", admit_verified_nip09_deletion_request_event_v1(verified).expect("admitted request") } + fn assert_request_index_matches_protocol( + target: &RadrootsSignatureVerifiedEvent, + requests: &[RadrootsAdmittedNip09DeletionRequestEventV1], + index: &RequestIndex, + ) { + let expected = evaluate_nip09_suppression_from_borrowed_requests_v1(target, requests); + let actual = index.decision(target.event()).expect("indexed decision"); + assert_eq!(actual.outcome, expected.outcome()); + assert_eq!(actual.reason, expected.reason().code()); + assert_eq!( + actual.event_reference_request_id.as_deref(), + expected + .event_reference() + .map(|evidence| evidence.request_id().as_str()) + ); + assert_eq!( + actual.address_reference_request_id.as_deref(), + expected + .address_reference() + .map(|evidence| evidence.request_id().as_str()) + ); + assert_eq!( + actual.address_reference_cutoff, + expected + .address_reference() + .map(|evidence| i64::try_from(evidence.inclusive_cutoff()).expect("cutoff range")) + ); + } + fn unsigned_addressable_target(author: &str, index: usize) -> RadrootsEventEnvelope { let tags = vec![vec!["d".to_owned(), fanout_identifier(index)]]; let id = compute_canonical_nip01_event_id( diff --git a/crates/event_store/src/nip09/reconciliation_v1/raw_source_rebuild.rs b/crates/event_store/src/nip09/reconciliation_v1/raw_source_rebuild.rs @@ -0,0 +1,1418 @@ +use super::visibility_oracle_v1::{VisibilityOracleFactV1, audit_current_visibility_from_raw_v1}; +use super::{ + OsSourceGenerationProvider, ReconciledEvent, ReconciliationCapacityLimits, + SourceGenerationProvider, SourceRebuildPlan, SourceState, TransitionOrigin, + append_source_generation, close_source_rebuild_marker, generation_from_blob, + load_reconciliation_snapshot, open_source_rebuild_marker, persist_event_coordinate_facts, + persist_nip09_facts, read_source_state, rebuild_raw_heads, reconcile_raw_events, + reconciliation_profile, rotate_source_state, synchronize_addressable_heads, + update_source_authority, validate_active_hook_state_fast, validate_baseline_authority, + validate_raw_source_rebuild_core_with_events_v1, validate_rebuild_marker_absent, + validate_source_raw_authority_with_state, +}; +use crate::migrations::{ + EVENT_STORE_LEDGER_NAME, EVENT_STORE_MIGRATIONS, EVENT_STORE_RESERVED_PREFIX, +}; +use crate::model::{ + RadrootsEventStoreActiveProductStateDigestV1, RadrootsEventStoreImmutableRawDigestV1, + RadrootsEventStoreRawSourceRebuildReportV1, +}; +use crate::schema::validate_exact_managed_v4_for_raw_source_rebuild_v1; +use crate::source_maintenance_v1::{ + preflight_source_generation_append_v1, validate_source_capacity_authority_fast_v1, + validate_source_capacity_authority_full_v1, +}; +use crate::store::food_availability_projection_v1::{ + reset_and_replay_food_availability_from_raw_v1, + validate_food_availability_projection_hook_state_fast_v1, + validate_food_availability_projection_hook_v1, +}; +use futures::TryStreamExt; +use sha2::{Digest, Sha256}; +use sqlx::{Row, Sqlite, SqliteConnection, SqlitePool, Transaction}; +use std::collections::BTreeSet; + +use crate::{ + RadrootsEventStoreCallerInboundForeignKeyV1, RadrootsEventStoreError, + RadrootsEventStoreRawSourceRebuildDriftV1, RadrootsEventStoreSourceGeneration, +}; + +const IMMUTABLE_RAW_DIGEST_DOMAIN_V1: &[u8] = b"radroots:event-store:immutable-raw-digest:v1\0"; +const ACTIVE_PRODUCT_STATE_DIGEST_DOMAIN_V1: &[u8] = + b"radroots:event-store:active-product-state-digest:v1\0"; +const TRANSITION_SEQUENCE_NAME: &str = "radroots_event_store_addressable_head_transition"; +const RAW_SOURCE_REBUILD_CALLER_MAIN_TABLE_COUNT_LIMIT_V1: u32 = 4_096; +const RAW_SOURCE_REBUILD_CALLER_FOREIGN_KEY_ROW_COUNT_LIMIT_V1: u32 = 4_096; +const REBUILD_OWNED_TABLES_V1: &[&str] = &[ + "event_envelopes", + "event_envelope_tags", + "event_envelope_head", + "radroots_event_store_source_generation", + "radroots_event_store_source_rebuild_commit_barrier", + "radroots_event_store_source_rebuild_marker", + "radroots_event_store_source_state", + "radroots_event_store_write_lock", + "radroots_event_store_source_capacity_v1", + "radroots_event_store_event_coordinate", + "radroots_event_store_nip09_request", + "radroots_event_store_nip09_event_target", + "radroots_event_store_nip09_address_target", + "radroots_event_store_addressable_head_state", + "radroots_event_store_addressable_head_transition", + "radroots_event_store_addressable_feed_integrity_v1", + "radroots_event_store_food_availability_cursor", + "radroots_event_store_food_availability_projection", + "radroots_event_store_food_availability_image", +]; +const RAW_SOURCE_REBUILD_MUTATED_PARENT_TABLES_V1: &[&str] = &[ + "event_envelopes", + "event_envelope_tags", + "event_envelope_head", + "radroots_event_store_source_generation", + "radroots_event_store_source_rebuild_commit_barrier", + "radroots_event_store_source_rebuild_marker", + "radroots_event_store_source_state", + "radroots_event_store_write_lock", + "radroots_event_store_source_capacity_v1", + "radroots_event_store_event_coordinate", + "radroots_event_store_nip09_request", + "radroots_event_store_nip09_event_target", + "radroots_event_store_nip09_address_target", + "radroots_event_store_addressable_head_state", + "radroots_event_store_addressable_head_transition", + "radroots_event_store_addressable_feed_integrity_v1", + "radroots_event_store_food_availability_cursor", + "radroots_event_store_food_availability_projection", + "radroots_event_store_food_availability_image", + "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", + "sqlite_sequence", +]; + +#[derive(Clone, Copy)] +struct RawSourceRebuildCallerSchemaLimitsV1 { + main_tables: u32, + foreign_key_rows: u32, +} + +impl RawSourceRebuildCallerSchemaLimitsV1 { + const fn production() -> Self { + Self { + main_tables: RAW_SOURCE_REBUILD_CALLER_MAIN_TABLE_COUNT_LIMIT_V1, + foreign_key_rows: RAW_SOURCE_REBUILD_CALLER_FOREIGN_KEY_ROW_COUNT_LIMIT_V1, + } + } +} + +#[cfg(test)] +#[allow(clippy::enum_variant_names)] // Variants mirror the governed after_* failpoint IDs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum RawSourceRebuildFailpointV1 { + AfterMarkerOpen, + AfterGenerationRotation, + AfterCoreReplay, + AfterVisibilityAudit, + AfterFoodResetAndReplay, + AfterFoodAudit, + AfterMarkerClose, +} + +#[cfg(not(test))] +type RawSourceRebuildFailpointV1 = (); + +#[cfg(test)] +impl RawSourceRebuildFailpointV1 { + const fn as_str(self) -> &'static str { + match self { + Self::AfterMarkerOpen => "after_marker_open", + Self::AfterGenerationRotation => "after_generation_rotation", + Self::AfterCoreReplay => "after_core_replay", + Self::AfterVisibilityAudit => "after_visibility_audit", + Self::AfterFoodResetAndReplay => "after_food_reset_replay", + Self::AfterFoodAudit => "after_food_audit", + Self::AfterMarkerClose => "after_marker_close", + } + } +} + +pub(crate) async fn rebuild_from_raw_v1_on_pool( + pool: &SqlitePool, +) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + rebuild_from_raw_v1_on_pool_inner( + pool, + &OsSourceGenerationProvider, + None, + RawSourceRebuildCallerSchemaLimitsV1::production(), + ) + .await +} + +#[cfg(test)] +pub(crate) async fn rebuild_from_raw_v1_on_pool_for_test( + pool: &SqlitePool, + generation_provider: &dyn SourceGenerationProvider, + failpoint: Option<RawSourceRebuildFailpointV1>, +) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + rebuild_from_raw_v1_on_pool_inner( + pool, + generation_provider, + failpoint, + RawSourceRebuildCallerSchemaLimitsV1::production(), + ) + .await +} + +#[cfg(test)] +pub(crate) async fn rebuild_from_raw_v1_on_pool_with_caller_schema_limits_for_test( + pool: &SqlitePool, + generation_provider: &dyn SourceGenerationProvider, + main_table_limit: u32, + foreign_key_row_limit: u32, +) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + rebuild_from_raw_v1_on_pool_inner( + pool, + generation_provider, + None, + RawSourceRebuildCallerSchemaLimitsV1 { + main_tables: main_table_limit, + foreign_key_rows: foreign_key_row_limit, + }, + ) + .await +} + +#[cfg(test)] +pub(crate) async fn rebuild_from_raw_v1_in_transaction_for_test( + connection: &mut SqliteConnection, + generation_provider: &dyn SourceGenerationProvider, +) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + rebuild_from_raw_v1_in_transaction_inner( + connection, + generation_provider, + None, + RawSourceRebuildCallerSchemaLimitsV1::production(), + ) + .await +} + +async fn rebuild_from_raw_v1_on_pool_inner( + pool: &SqlitePool, + generation_provider: &dyn SourceGenerationProvider, + failpoint: Option<RawSourceRebuildFailpointV1>, + caller_schema_limits: RawSourceRebuildCallerSchemaLimitsV1, +) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + let mut transaction = pool.begin_with("BEGIN IMMEDIATE").await?; + let result = rebuild_from_raw_v1_in_transaction_inner( + &mut transaction, + generation_provider, + failpoint, + caller_schema_limits, + ) + .await; + finish_raw_source_rebuild_transaction(transaction, result).await +} + +pub(crate) async fn rebuild_from_raw_v1_in_existing_transaction( + mut transaction: Transaction<'_, Sqlite>, +) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + let result = rebuild_from_raw_v1_in_transaction_inner( + &mut transaction, + &OsSourceGenerationProvider, + None, + RawSourceRebuildCallerSchemaLimitsV1::production(), + ) + .await; + finish_raw_source_rebuild_transaction(transaction, result).await +} + +async fn rebuild_from_raw_v1_in_transaction_inner( + connection: &mut SqliteConnection, + generation_provider: &dyn SourceGenerationProvider, + _failpoint: Option<RawSourceRebuildFailpointV1>, + caller_schema_limits: RawSourceRebuildCallerSchemaLimitsV1, +) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + validate_exact_managed_v4_for_raw_source_rebuild_v1(connection).await?; + preflight_caller_owned_schema_dependencies_v1(connection, caller_schema_limits).await?; + validate_rebuild_marker_absent(connection).await?; + validate_source_capacity_authority_full_v1(connection).await?; + preflight_source_generation_append_v1(connection).await?; + + let snapshot = + load_reconciliation_snapshot(connection, ReconciliationCapacityLimits::production()) + .await?; + let transition_floor_seq = transition_high_water_v1(connection).await?; + let prior = + validate_source_lineage_for_rebuild_v1(connection, &snapshot.events, transition_floor_seq) + .await?; + let immutable_raw_digest = immutable_raw_digest_v1(connection).await?; + if transition_floor_seq == i64::MAX { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + "addressable transition sequence space is exhausted at SQLite INTEGER maximum", + ); + } + let mut generation_bytes = [0_u8; 32]; + generation_provider.fill_generation(&mut generation_bytes)?; + let generation = RadrootsEventStoreSourceGeneration::from_bytes(generation_bytes); + let generation_exists: i64 = sqlx::query_scalar( + "SELECT EXISTS (SELECT 1 FROM radroots_event_store_source_generation WHERE source_generation = ?)", + ) + .bind(generation.as_bytes().as_slice()) + .fetch_one(&mut *connection) + .await?; + if generation_exists != 0 { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "fresh source generation collided with retained lineage", + ); + } + + let raw_event_count = i64::try_from(snapshot.capacity.raw_events).map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::ImmutableRawAuthority, + "raw event count exceeds SQLite integer range", + ) + })?; + let raw_tag_count = i64::try_from(snapshot.capacity.raw_tags).map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::ImmutableRawAuthority, + "raw tag count exceeds SQLite integer range", + ) + })?; + let raw_high_water_seq = snapshot.events.last().map(|event| event.seq).unwrap_or(0); + let generation_ordinal: i64 = sqlx::query_scalar( + "SELECT COALESCE(MAX(generation_ordinal), 0) + 1 FROM radroots_event_store_source_generation", + ) + .fetch_one(&mut *connection) + .await?; + let plan = SourceRebuildPlan { + generation, + generation_ordinal, + transition_floor_seq, + raw_event_count, + raw_tag_count, + raw_high_water_seq, + prior: Some(prior.clone()), + }; + + let marker = open_source_rebuild_marker(connection, &plan).await?; + #[cfg(test)] + inject_raw_source_rebuild_failpoint_v1( + _failpoint, + RawSourceRebuildFailpointV1::AfterMarkerOpen, + )?; + append_source_generation(connection, &plan).await?; + rotate_source_state(connection, &plan).await?; + #[cfg(test)] + inject_raw_source_rebuild_failpoint_v1( + _failpoint, + RawSourceRebuildFailpointV1::AfterGenerationRotation, + )?; + let transition_sequence_rowid = + prepare_transition_sqlite_sequence_v1(connection, transition_floor_seq).await?; + + reconcile_raw_events(connection, &snapshot.events).await?; + persist_event_coordinate_facts(connection, generation, &snapshot.events).await?; + rebuild_raw_heads(connection, &snapshot.events).await?; + let requests = persist_nip09_facts(connection, generation, &snapshot.events).await?; + synchronize_addressable_heads( + connection, + generation, + &snapshot.events, + &requests, + TransitionOrigin::Baseline, + None, + "baseline_rebuild", + ) + .await?; + update_source_authority( + connection, + raw_event_count, + raw_tag_count, + raw_high_water_seq, + ) + .await?; + let replay_transition_high_water = transition_high_water_v1(connection).await?; + validate_transition_sqlite_sequence_v1( + connection, + transition_sequence_rowid, + replay_transition_high_water, + ) + .await?; + validate_raw_source_rebuild_core_with_events_v1(connection, generation, &snapshot.events) + .await?; + #[cfg(test)] + inject_raw_source_rebuild_failpoint_v1( + _failpoint, + RawSourceRebuildFailpointV1::AfterCoreReplay, + )?; + + let derived_visibility = load_derived_visibility_rows_v1(connection, generation).await?; + audit_current_visibility_from_raw_v1(&snapshot.events, derived_visibility).await?; + #[cfg(test)] + inject_raw_source_rebuild_failpoint_v1( + _failpoint, + RawSourceRebuildFailpointV1::AfterVisibilityAudit, + )?; + + crate::source_maintenance_v1::bind_source_capacity_to_generation_v1(connection, generation) + .await?; + reset_and_replay_food_availability_from_raw_v1(connection, generation).await?; + #[cfg(test)] + inject_raw_source_rebuild_failpoint_v1( + _failpoint, + RawSourceRebuildFailpointV1::AfterFoodResetAndReplay, + )?; + validate_food_availability_projection_hook_v1(connection).await?; + #[cfg(test)] + inject_raw_source_rebuild_failpoint_v1( + _failpoint, + RawSourceRebuildFailpointV1::AfterFoodAudit, + )?; + + let final_raw_digest = immutable_raw_digest_v1(connection).await?; + if final_raw_digest != immutable_raw_digest { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::ImmutableRawAuthority, + "immutable raw digest changed during source rebuild", + ); + } + close_source_rebuild_marker(connection, marker).await?; + #[cfg(test)] + inject_raw_source_rebuild_failpoint_v1( + _failpoint, + RawSourceRebuildFailpointV1::AfterMarkerClose, + )?; + + validate_active_hook_state_fast(connection).await?; + let source_capacity = validate_source_capacity_authority_fast_v1(connection).await?; + validate_food_availability_projection_hook_state_fast_v1(connection).await?; + validate_scoped_integrity_v1(connection).await?; + let active_product_state_digest = + active_product_state_digest_v1(connection, generation).await?; + if source_capacity.source_generation() != generation + || source_capacity.raw_event_count() != snapshot.capacity.raw_events + || source_capacity.raw_tag_count() != snapshot.capacity.raw_tags + || source_capacity.raw_event_text_bytes() != snapshot.capacity.raw_event_bytes + || source_capacity.raw_tag_text_bytes() != snapshot.capacity.raw_tag_bytes + || source_capacity.raw_high_water_seq() != raw_high_water_seq + || source_capacity.retained_generation_count() + != u32::try_from(generation_ordinal).map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "generation ordinal exceeds u32 range", + ) + })? + || prior.generation == generation + { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::RebuildPostcondition, + "committed rebuild report authority is inconsistent", + ); + } + Ok(RadrootsEventStoreRawSourceRebuildReportV1 { + prior_source_generation: prior.generation, + new_source_generation: generation, + source_capacity, + immutable_raw_digest, + active_product_state_digest, + }) +} + +async fn preflight_caller_owned_schema_dependencies_v1( + connection: &mut SqliteConnection, + limits: RawSourceRebuildCallerSchemaLimitsV1, +) -> Result<(), RadrootsEventStoreError> { + let governed_names_json = governed_schema_names_json_v1()?; + let mutated_parent_tables_json = + serde_json::to_string(RAW_SOURCE_REBUILD_MUTATED_PARENT_TABLES_V1)?; + + let caller_main_table_count: i64 = sqlx::query_scalar( + "WITH governed(name) AS ( + SELECT CAST(value AS TEXT) COLLATE NOCASE FROM main.json_each(?) + ) + SELECT COUNT(*) + FROM ( + SELECT 1 + FROM main.sqlite_schema AS child + WHERE child.type = 'table' + AND lower(substr(child.name, 1, 7)) != 'sqlite_' + AND child.name COLLATE NOCASE NOT IN (SELECT name FROM governed) + AND lower(substr(child.name, 1, length(?))) != lower(?) + LIMIT ? + )", + ) + .bind(&governed_names_json) + .bind(EVENT_STORE_RESERVED_PREFIX) + .bind(EVENT_STORE_RESERVED_PREFIX) + .bind(i64::from(limits.main_tables) + 1) + .fetch_one(&mut *connection) + .await?; + let caller_main_table_count = caller_schema_count_v1(caller_main_table_count)?; + if caller_main_table_count > u64::from(limits.main_tables) { + return Err( + RadrootsEventStoreError::RawSourceRebuildCallerTableCapacityExceeded { + observed_at_least: caller_main_table_count, + limit: u64::from(limits.main_tables), + }, + ); + } + + let caller_foreign_key_row_count: i64 = sqlx::query_scalar( + "WITH governed(name) AS ( + SELECT CAST(value AS TEXT) COLLATE NOCASE FROM main.json_each(?) + ) + SELECT COUNT(*) + FROM ( + SELECT 1 + FROM main.sqlite_schema AS child + JOIN main.pragma_foreign_key_list(child.name, 'main') AS foreign_key + WHERE child.type = 'table' + AND lower(substr(child.name, 1, 7)) != 'sqlite_' + AND child.name COLLATE NOCASE NOT IN (SELECT name FROM governed) + AND lower(substr(child.name, 1, length(?))) != lower(?) + LIMIT ? + )", + ) + .bind(&governed_names_json) + .bind(EVENT_STORE_RESERVED_PREFIX) + .bind(EVENT_STORE_RESERVED_PREFIX) + .bind(i64::from(limits.foreign_key_rows) + 1) + .fetch_one(&mut *connection) + .await?; + let caller_foreign_key_row_count = caller_schema_count_v1(caller_foreign_key_row_count)?; + if caller_foreign_key_row_count > u64::from(limits.foreign_key_rows) { + return Err( + RadrootsEventStoreError::RawSourceRebuildCallerForeignKeyCapacityExceeded { + observed_at_least: caller_foreign_key_row_count, + limit: u64::from(limits.foreign_key_rows), + }, + ); + } + + let dependency = sqlx::query( + "WITH governed(name) AS ( + SELECT CAST(value AS TEXT) COLLATE NOCASE FROM main.json_each(?) + ), rebuild_parent(name) AS ( + SELECT CAST(value AS TEXT) COLLATE NOCASE FROM main.json_each(?) + ) + SELECT + child.name AS child_table, + foreign_key.id AS foreign_key_id, + foreign_key.seq AS foreign_key_sequence, + foreign_key.\"from\" AS child_column, + rebuild_parent.name AS parent_table, + foreign_key.\"to\" AS parent_column, + foreign_key.on_update AS on_update, + foreign_key.on_delete AS on_delete, + foreign_key.\"match\" AS match_clause + FROM main.sqlite_schema AS child + JOIN main.pragma_foreign_key_list(child.name, 'main') AS foreign_key + JOIN rebuild_parent + ON foreign_key.\"table\" COLLATE NOCASE = rebuild_parent.name + WHERE child.type = 'table' + AND lower(substr(child.name, 1, 7)) != 'sqlite_' + AND child.name COLLATE NOCASE NOT IN (SELECT name FROM governed) + AND lower(substr(child.name, 1, length(?))) != lower(?) + ORDER BY child.name COLLATE NOCASE, child.name, foreign_key.id, foreign_key.seq + LIMIT 1", + ) + .bind(&governed_names_json) + .bind(&mutated_parent_tables_json) + .bind(EVENT_STORE_RESERVED_PREFIX) + .bind(EVENT_STORE_RESERVED_PREFIX) + .fetch_optional(&mut *connection) + .await?; + if let Some(dependency) = dependency { + return Err( + RadrootsEventStoreError::RawSourceRebuildCallerInboundForeignKeyUnsupported { + dependency: Box::new(RadrootsEventStoreCallerInboundForeignKeyV1 { + child_table: dependency.try_get("child_table")?, + foreign_key_id: dependency.try_get("foreign_key_id")?, + foreign_key_sequence: dependency.try_get("foreign_key_sequence")?, + child_column: dependency.try_get("child_column")?, + parent_table: dependency.try_get("parent_table")?, + parent_column: dependency.try_get("parent_column")?, + on_update: dependency.try_get("on_update")?, + on_delete: dependency.try_get("on_delete")?, + match_clause: dependency.try_get("match_clause")?, + }), + }, + ); + } + Ok(()) +} + +fn governed_schema_names_json_v1() -> Result<String, RadrootsEventStoreError> { + let mut names = EVENT_STORE_MIGRATIONS + .iter() + .flat_map(|migration| migration.owned_object_names.iter().copied()) + .collect::<BTreeSet<_>>(); + names.insert(EVENT_STORE_LEDGER_NAME); + Ok(serde_json::to_string(&names)?) +} + +fn caller_schema_count_v1(count: i64) -> Result<u64, RadrootsEventStoreError> { + u64::try_from(count).map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + "caller-owned schema inventory returned a negative row count", + ) + }) +} + +async fn load_derived_visibility_rows_v1( + connection: &mut SqliteConnection, + generation: RadrootsEventStoreSourceGeneration, +) -> Result<Vec<VisibilityOracleFactV1>, RadrootsEventStoreError> { + let rows = sqlx::query( + "SELECT event.event_id, event.contract_status AS admission_status, event.contract_id, event.event_class, 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, visibility.source_generation FROM event_envelopes AS event JOIN radroots_event_store_current_visibility_v1 AS visibility ON visibility.event_id = event.event_id WHERE visibility.source_generation = ? ORDER BY event.seq", + ) + .bind(generation.as_bytes().as_slice()) + .fetch_all(&mut *connection) + .await?; + let mut actual = Vec::with_capacity(rows.len()); + for row in rows { + let stored_generation: Vec<u8> = row.try_get("source_generation")?; + if stored_generation.as_slice() != generation.as_bytes().as_slice() { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::DerivedProductStateAuthority, + "current visibility exposed a foreign source generation", + ); + } + actual.push(VisibilityOracleFactV1 { + event_id: row.try_get("event_id")?, + admission_status: row.try_get("admission_status")?, + contract_id: row.try_get("contract_id")?, + event_class: row.try_get("event_class")?, + raw_d_tag: row.try_get("raw_d_tag")?, + is_raw_head: row.try_get("is_raw_head")?, + raw_head_event_id: row.try_get("raw_head_event_id")?, + suppression_outcome: row.try_get("suppression_outcome")?, + suppression_reason: row.try_get("suppression_reason")?, + event_reference_request_id: row.try_get("event_reference_request_id")?, + address_reference_request_id: row.try_get("address_reference_request_id")?, + address_reference_cutoff: row.try_get("address_reference_cutoff")?, + current_visibility: row.try_get("current_visibility")?, + }); + } + Ok(actual) +} + +async fn validate_source_lineage_for_rebuild_v1( + connection: &mut SqliteConnection, + events: &[ReconciledEvent], + terminal_transition_high_water: i64, +) -> Result<SourceState, RadrootsEventStoreError> { + let state = read_source_state(connection).await?; + validate_source_raw_authority_with_state(connection, &state).await?; + validate_baseline_authority(connection, &state, events).await?; + let rows = sqlx::query( + "SELECT source_generation, generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq FROM radroots_event_store_source_generation ORDER BY generation_ordinal", + ) + .fetch_all(&mut *connection) + .await?; + let capacity = validate_source_capacity_authority_fast_v1(connection).await?; + if rows.len() != usize::try_from(capacity.retained_generation_count()).unwrap_or(usize::MAX) { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "retained source-generation count does not match lineage rows", + ); + } + if rows.is_empty() { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "retained source-generation lineage is empty", + ); + } + let mut prior_baseline = (0_i64, 0_i64, 0_i64); + for (index, row) in rows.iter().enumerate() { + let ordinal: i64 = row.try_get("generation_ordinal")?; + let expected_ordinal = i64::try_from(index + 1).map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "generation lineage exceeds SQLite range", + ) + })?; + let generation = generation_from_blob(row.try_get("source_generation")?)?; + let hook_id: String = row.try_get("hook_id")?; + let hook_manifest_sha256: String = row.try_get("hook_manifest_sha256")?; + reconciliation_profile( + row.try_get("reconciliation_version")?, + row.try_get("addressable_feed_version")?, + row.try_get("event_contract_registry_version")?, + hook_id.as_str(), + hook_manifest_sha256.as_str(), + )?; + let floor: i64 = row.try_get("transition_floor_seq")?; + let baseline_events: i64 = row.try_get("baseline_raw_event_count")?; + let baseline_tags: i64 = row.try_get("baseline_raw_tag_count")?; + let baseline_high_water: i64 = row.try_get("baseline_raw_high_water_seq")?; + let expected_baseline_events = + i64::try_from(events.partition_point(|event| event.seq <= baseline_high_water)) + .map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "historical raw baseline exceeds SQLite range", + ) + })?; + let expected_baseline_high_water = if expected_baseline_events == 0 { + 0 + } else { + let final_index = usize::try_from(expected_baseline_events - 1).map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "historical raw baseline is negative", + ) + })?; + events + .get(final_index) + .map(|event| event.seq) + .ok_or_else(|| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "historical raw baseline is out of range", + ) + })? + }; + let expected_baseline_tags: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM event_envelope_tags AS tag JOIN event_envelopes AS event ON event.event_id = tag.event_id WHERE event.seq <= ?", + ) + .bind(baseline_high_water) + .fetch_one(&mut *connection) + .await?; + let transition_end: i64 = if let Some(next) = rows.get(index + 1) { + next.try_get("transition_floor_seq")? + } else { + terminal_transition_high_water + }; + let transition_bounds = sqlx::query( + "SELECT COUNT(*) AS transition_count, MIN(transition_seq) AS first_transition_seq, MAX(transition_seq) AS last_transition_seq FROM radroots_event_store_addressable_head_transition WHERE source_generation = ?", + ) + .bind(generation.as_bytes().as_slice()) + .fetch_one(&mut *connection) + .await?; + let transition_count: i64 = transition_bounds.try_get("transition_count")?; + let first_transition_seq: Option<i64> = + transition_bounds.try_get("first_transition_seq")?; + let last_transition_seq: Option<i64> = transition_bounds.try_get("last_transition_seq")?; + let expected_transition_count = transition_end.checked_sub(floor).ok_or_else(|| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + format!( + "source-generation lineage row {expected_ordinal} has an inverted transition interval" + ), + ) + })?; + let expected_first_transition = if expected_transition_count == 0 { + None + } else { + Some(floor.checked_add(1).ok_or_else(|| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + "historical transition sequence overflow", + ) + })?) + }; + let expected_last_transition = (expected_transition_count != 0).then_some(transition_end); + if ordinal != expected_ordinal + || baseline_events < 0 + || baseline_tags < 0 + || baseline_high_water < 0 + || baseline_events > state.raw_event_count + || baseline_tags > state.raw_tag_count + || baseline_high_water > state.raw_high_water_seq + || baseline_events < prior_baseline.0 + || baseline_tags < prior_baseline.1 + || baseline_high_water < prior_baseline.2 + || baseline_events != expected_baseline_events + || baseline_tags != expected_baseline_tags + || baseline_high_water != expected_baseline_high_water + { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + format!("source-generation lineage row {expected_ordinal} is inconsistent"), + ); + } + if (index == 0 && floor != 0) + || transition_count != expected_transition_count + || first_transition_seq != expected_first_transition + || last_transition_seq != expected_last_transition + { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + format!( + "source-generation lineage row {expected_ordinal} has inconsistent addressable transition authority" + ), + ); + } + if index + 1 == rows.len() && generation != state.generation { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "active source generation is not the terminal lineage row", + ); + } + prior_baseline = (baseline_events, baseline_tags, baseline_high_water); + } + let transition_summary = sqlx::query( + "SELECT COUNT(*) AS transition_count, MIN(transition_seq) AS first_transition_seq, MAX(transition_seq) AS last_transition_seq FROM radroots_event_store_addressable_head_transition", + ) + .fetch_one(&mut *connection) + .await?; + let transition_count: i64 = transition_summary.try_get("transition_count")?; + let first_transition_seq: Option<i64> = transition_summary.try_get("first_transition_seq")?; + let last_transition_seq: Option<i64> = transition_summary.try_get("last_transition_seq")?; + let expected_first_transition = (terminal_transition_high_water != 0).then_some(1); + let expected_last_transition = + (terminal_transition_high_water != 0).then_some(terminal_transition_high_water); + if transition_count != terminal_transition_high_water + || first_transition_seq != expected_first_transition + || last_transition_seq != expected_last_transition + { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + "retained source-generation transition lineage has gaps or foreign rows", + ); + } + Ok(state) +} + +async fn transition_high_water_v1( + connection: &mut SqliteConnection, +) -> Result<i64, RadrootsEventStoreError> { + Ok(sqlx::query_scalar( + "SELECT COALESCE(MAX(transition_seq), 0) FROM radroots_event_store_addressable_head_transition", + ) + .fetch_one(&mut *connection) + .await?) +} + +async fn prepare_transition_sqlite_sequence_v1( + connection: &mut SqliteConnection, + transition_max: i64, +) -> Result<i64, RadrootsEventStoreError> { + if transition_max < 0 || transition_max == i64::MAX { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + format!( + "addressable transition sequence high-water {transition_max} cannot be normalized" + ), + ); + } + let first: Option<(i64, Option<i64>)> = sqlx::query_as( + "SELECT rowid, name = ? COLLATE NOCASE FROM main.sqlite_sequence ORDER BY rowid LIMIT 1", + ) + .bind(TRANSITION_SEQUENCE_NAME) + .fetch_optional(&mut *connection) + .await?; + let target_rowid = match first { + None => -1, + Some((rowid, Some(1))) => rowid, + Some((i64::MIN, _)) => { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + "unrelated sqlite_sequence authority exhausts target-first rowid space", + ); + } + Some((rowid, _)) => rowid - 1, + }; + sqlx::query("DELETE FROM main.sqlite_sequence WHERE name COLLATE NOCASE = ?") + .bind(TRANSITION_SEQUENCE_NAME) + .execute(&mut *connection) + .await?; + sqlx::query("INSERT INTO main.sqlite_sequence(rowid, name, seq) VALUES (?, ?, ?)") + .bind(target_rowid) + .bind(TRANSITION_SEQUENCE_NAME) + .bind(transition_max) + .execute(&mut *connection) + .await?; + validate_transition_sqlite_sequence_v1(connection, target_rowid, transition_max).await?; + Ok(target_rowid) +} + +async fn validate_transition_sqlite_sequence_v1( + connection: &mut SqliteConnection, + target_rowid: i64, + transition_max: i64, +) -> Result<(), RadrootsEventStoreError> { + let normalized: Option<(String, Option<i64>)> = + sqlx::query_as("SELECT name, seq FROM main.sqlite_sequence WHERE rowid = ?") + .bind(target_rowid) + .fetch_optional(&mut *connection) + .await?; + let first_rowid: Option<i64> = + sqlx::query_scalar("SELECT rowid FROM main.sqlite_sequence ORDER BY rowid LIMIT 1") + .fetch_optional(&mut *connection) + .await?; + if normalized != Some((TRANSITION_SEQUENCE_NAME.to_owned(), Some(transition_max))) + || first_rowid != Some(target_rowid) + { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + "addressable transition sqlite_sequence target-first authority is inconsistent", + ); + } + Ok(()) +} + +async fn immutable_raw_digest_v1( + connection: &mut SqliteConnection, +) -> Result<RadrootsEventStoreImmutableRawDigestV1, RadrootsEventStoreError> { + let mut digest = Sha256::new(); + digest.update(IMMUTABLE_RAW_DIGEST_DOMAIN_V1); + digest_section(&mut digest, b"event_envelopes")?; + let mut events = sqlx::query( + "SELECT seq, event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, inserted_at_ms FROM event_envelopes ORDER BY seq", + ) + .fetch(&mut *connection); + while let Some(row) = events.try_next().await? { + digest_row_start(&mut digest); + digest_i64(&mut digest, row.try_get("seq")?); + digest_text( + &mut digest, + row.try_get::<String, _>("event_id")?.as_bytes(), + )?; + digest_text(&mut digest, row.try_get::<String, _>("pubkey")?.as_bytes())?; + digest_i64(&mut digest, row.try_get("created_at")?); + digest_i64(&mut digest, row.try_get("kind")?); + digest_text( + &mut digest, + row.try_get::<String, _>("tags_json")?.as_bytes(), + )?; + digest_text(&mut digest, row.try_get::<String, _>("content")?.as_bytes())?; + digest_text(&mut digest, row.try_get::<String, _>("sig")?.as_bytes())?; + digest_text( + &mut digest, + row.try_get::<String, _>("raw_json")?.as_bytes(), + )?; + digest_i64(&mut digest, row.try_get("inserted_at_ms")?); + } + drop(events); + digest_section(&mut digest, b"event_envelope_tags")?; + let mut tags = sqlx::query( + "SELECT event.seq, tag.event_id, tag.tag_index, tag.tag_name, tag.tag_value, tag.tag_json FROM event_envelope_tags AS tag JOIN event_envelopes AS event ON event.event_id = tag.event_id ORDER BY event.seq, tag.tag_index", + ) + .fetch(&mut *connection); + while let Some(row) = tags.try_next().await? { + digest_row_start(&mut digest); + digest_i64(&mut digest, row.try_get("seq")?); + digest_text( + &mut digest, + row.try_get::<String, _>("event_id")?.as_bytes(), + )?; + digest_i64(&mut digest, row.try_get("tag_index")?); + digest_text( + &mut digest, + row.try_get::<String, _>("tag_name")?.as_bytes(), + )?; + digest_optional_text( + &mut digest, + row.try_get::<Option<String>, _>("tag_value")?.as_deref(), + )?; + digest_text( + &mut digest, + row.try_get::<String, _>("tag_json")?.as_bytes(), + )?; + } + drop(tags); + Ok(RadrootsEventStoreImmutableRawDigestV1::from_bytes( + digest.finalize().into(), + )) +} + +async fn active_product_state_digest_v1( + connection: &mut SqliteConnection, + generation: RadrootsEventStoreSourceGeneration, +) -> Result<RadrootsEventStoreActiveProductStateDigestV1, RadrootsEventStoreError> { + let mut digest = Sha256::new(); + digest.update(ACTIVE_PRODUCT_STATE_DIGEST_DOMAIN_V1); + digest_section(&mut digest, b"envelope_classification")?; + let mut envelope_classification = sqlx::query( + "SELECT event_id, verification_status, contract_status, contract_id, event_class, projection_eligible FROM event_envelopes ORDER BY event_id", + ) + .fetch(&mut *connection); + while let Some(row) = envelope_classification.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "event_id")?; + digest_text_field(&mut digest, &row, "verification_status")?; + digest_text_field(&mut digest, &row, "contract_status")?; + digest_optional_text_field(&mut digest, &row, "contract_id")?; + digest_optional_text_field(&mut digest, &row, "event_class")?; + digest_bool_field(&mut digest, &row, "projection_eligible")?; + } + drop(envelope_classification); + + digest_section(&mut digest, b"tag_classification")?; + let mut tag_classification = sqlx::query( + "SELECT event_id, tag_index, contract_semantic, contract_value_type, relay_indexed FROM event_envelope_tags ORDER BY event_id, tag_index", + ) + .fetch(&mut *connection); + while let Some(row) = tag_classification.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "event_id")?; + digest_i64_field(&mut digest, &row, "tag_index")?; + digest_optional_text_field(&mut digest, &row, "contract_semantic")?; + digest_optional_text_field(&mut digest, &row, "contract_value_type")?; + digest_bool_field(&mut digest, &row, "relay_indexed")?; + } + drop(tag_classification); + + digest_section(&mut digest, b"raw_heads")?; + let mut raw_heads = sqlx::query( + "SELECT coordinate_type, kind, pubkey, d_tag, event_id, created_at FROM event_envelope_head ORDER BY coordinate_type, kind, pubkey, d_tag", + ) + .fetch(&mut *connection); + while let Some(row) = raw_heads.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "coordinate_type")?; + digest_i64_field(&mut digest, &row, "kind")?; + digest_text_field(&mut digest, &row, "pubkey")?; + digest_optional_text_field(&mut digest, &row, "d_tag")?; + digest_text_field(&mut digest, &row, "event_id")?; + digest_i64_field(&mut digest, &row, "created_at")?; + } + drop(raw_heads); + + digest_section(&mut digest, b"event_coordinates")?; + let mut event_coordinates = sqlx::query( + "SELECT event_id, coordinate_type, kind, pubkey, created_at, admission_status, admission_code, contract_id, raw_d_tag, nip09_matchable, nip09_d_tag FROM radroots_event_store_event_coordinate WHERE source_generation = ? ORDER BY event_id", + ) + .bind(generation.as_bytes().as_slice()) + .fetch(&mut *connection); + while let Some(row) = event_coordinates.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "event_id")?; + digest_text_field(&mut digest, &row, "coordinate_type")?; + digest_i64_field(&mut digest, &row, "kind")?; + digest_text_field(&mut digest, &row, "pubkey")?; + digest_i64_field(&mut digest, &row, "created_at")?; + digest_text_field(&mut digest, &row, "admission_status")?; + digest_optional_text_field(&mut digest, &row, "admission_code")?; + digest_optional_text_field(&mut digest, &row, "contract_id")?; + digest_text_field(&mut digest, &row, "raw_d_tag")?; + digest_bool_field(&mut digest, &row, "nip09_matchable")?; + digest_optional_text_field(&mut digest, &row, "nip09_d_tag")?; + } + drop(event_coordinates); + + digest_section(&mut digest, b"nip09_requests")?; + let mut nip09_requests = sqlx::query( + "SELECT request_event_id, request_pubkey, request_created_at FROM radroots_event_store_nip09_request WHERE source_generation = ? ORDER BY request_event_id", + ) + .bind(generation.as_bytes().as_slice()) + .fetch(&mut *connection); + while let Some(row) = nip09_requests.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "request_event_id")?; + digest_text_field(&mut digest, &row, "request_pubkey")?; + digest_i64_field(&mut digest, &row, "request_created_at")?; + } + drop(nip09_requests); + + digest_section(&mut digest, b"nip09_event_targets")?; + let mut nip09_event_targets = sqlx::query( + "SELECT request_event_id, target_event_id, source_tag_index, source_tag_value FROM radroots_event_store_nip09_event_target WHERE source_generation = ? ORDER BY request_event_id, target_event_id, source_tag_index", + ) + .bind(generation.as_bytes().as_slice()) + .fetch(&mut *connection); + while let Some(row) = nip09_event_targets.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "request_event_id")?; + digest_text_field(&mut digest, &row, "target_event_id")?; + digest_i64_field(&mut digest, &row, "source_tag_index")?; + digest_text_field(&mut digest, &row, "source_tag_value")?; + } + drop(nip09_event_targets); + + digest_section(&mut digest, b"nip09_address_targets")?; + let mut nip09_address_targets = sqlx::query( + "SELECT request_event_id, target_kind, target_pubkey, target_d_tag, inclusive_cutoff, source_tag_index, source_tag_value, source_kind_text, source_pubkey_text, source_d_tag FROM radroots_event_store_nip09_address_target WHERE source_generation = ? ORDER BY request_event_id, target_kind, target_pubkey, target_d_tag, source_tag_index", + ) + .bind(generation.as_bytes().as_slice()) + .fetch(&mut *connection); + while let Some(row) = nip09_address_targets.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "request_event_id")?; + digest_i64_field(&mut digest, &row, "target_kind")?; + digest_text_field(&mut digest, &row, "target_pubkey")?; + digest_text_field(&mut digest, &row, "target_d_tag")?; + digest_i64_field(&mut digest, &row, "inclusive_cutoff")?; + digest_i64_field(&mut digest, &row, "source_tag_index")?; + digest_text_field(&mut digest, &row, "source_tag_value")?; + digest_text_field(&mut digest, &row, "source_kind_text")?; + digest_text_field(&mut digest, &row, "source_pubkey_text")?; + digest_text_field(&mut digest, &row, "source_d_tag")?; + } + drop(nip09_address_targets); + + digest_section(&mut digest, b"addressable_heads")?; + let mut addressable_heads = sqlx::query( + "SELECT kind, pubkey, d_tag, raw_head_event_id, 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_state WHERE source_generation = ? ORDER BY kind, pubkey, d_tag", + ) + .bind(generation.as_bytes().as_slice()) + .fetch(&mut *connection); + while let Some(row) = addressable_heads.try_next().await? { + digest_row_start(&mut digest); + digest_i64_field(&mut digest, &row, "kind")?; + digest_text_field(&mut digest, &row, "pubkey")?; + digest_text_field(&mut digest, &row, "d_tag")?; + digest_text_field(&mut digest, &row, "raw_head_event_id")?; + digest_i64_field(&mut digest, &row, "raw_head_created_at")?; + digest_text_field(&mut digest, &row, "admission_status")?; + digest_optional_text_field(&mut digest, &row, "admission_code")?; + digest_optional_text_field(&mut digest, &row, "contract_id")?; + digest_text_field(&mut digest, &row, "visibility")?; + digest_optional_text_field(&mut digest, &row, "nip09_outcome")?; + digest_optional_text_field(&mut digest, &row, "nip09_reason")?; + digest_optional_text_field(&mut digest, &row, "event_reference_request_id")?; + digest_optional_text_field(&mut digest, &row, "address_reference_request_id")?; + digest_optional_i64_field(&mut digest, &row, "address_reference_cutoff")?; + } + drop(addressable_heads); + + digest_section(&mut digest, b"current_visibility")?; + let mut current_visibility = sqlx::query( + "SELECT event_id, admission_status, contract_id, event_class, raw_d_tag, is_raw_head, raw_head_event_id, 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 source_generation = ? ORDER BY event_id", + ) + .bind(generation.as_bytes().as_slice()) + .fetch(&mut *connection); + while let Some(row) = current_visibility.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "event_id")?; + digest_text_field(&mut digest, &row, "admission_status")?; + digest_optional_text_field(&mut digest, &row, "contract_id")?; + digest_text_field(&mut digest, &row, "event_class")?; + digest_optional_text_field(&mut digest, &row, "raw_d_tag")?; + digest_bool_field(&mut digest, &row, "is_raw_head")?; + digest_optional_text_field(&mut digest, &row, "raw_head_event_id")?; + digest_optional_text_field(&mut digest, &row, "suppression_outcome")?; + digest_optional_text_field(&mut digest, &row, "suppression_reason")?; + digest_optional_text_field(&mut digest, &row, "event_reference_request_id")?; + digest_optional_text_field(&mut digest, &row, "address_reference_request_id")?; + digest_optional_i64_field(&mut digest, &row, "address_reference_cutoff")?; + digest_text_field(&mut digest, &row, "current_visibility")?; + } + drop(current_visibility); + + digest_section(&mut digest, b"food_projection")?; + let mut food_projection = sqlx::query( + "SELECT kind, pubkey, d_tag, event_id, created_at, contract_id, content, title, summary, published_at, location, price_amount, price_currency, price_unit, quantity_amount, quantity_unit, status, diagnostic_codes_json FROM radroots_event_store_food_availability_projection WHERE source_generation = ? ORDER BY pubkey, d_tag", + ) + .bind(generation.as_bytes().as_slice()) + .fetch(&mut *connection); + while let Some(row) = food_projection.try_next().await? { + digest_row_start(&mut digest); + digest_i64_field(&mut digest, &row, "kind")?; + for field in ["pubkey", "d_tag", "event_id"] { + digest_text_field(&mut digest, &row, field)?; + } + digest_i64_field(&mut digest, &row, "created_at")?; + for field in ["contract_id", "content", "title", "summary"] { + digest_text_field(&mut digest, &row, field)?; + } + digest_i64_field(&mut digest, &row, "published_at")?; + for field in ["location", "price_amount", "price_currency", "price_unit"] { + digest_text_field(&mut digest, &row, field)?; + } + digest_optional_text_field(&mut digest, &row, "quantity_amount")?; + digest_optional_text_field(&mut digest, &row, "quantity_unit")?; + digest_text_field(&mut digest, &row, "status")?; + digest_text_field(&mut digest, &row, "diagnostic_codes_json")?; + } + drop(food_projection); + + digest_section(&mut digest, b"food_images")?; + let mut food_images = sqlx::query( + "SELECT pubkey, d_tag, image_index, raw_tag_json, url, width, height, blossom_sha256, qualifies, diagnostic_codes_json FROM radroots_event_store_food_availability_image WHERE source_generation = ? ORDER BY pubkey, d_tag, image_index", + ) + .bind(generation.as_bytes().as_slice()) + .fetch(&mut *connection); + while let Some(row) = food_images.try_next().await? { + digest_row_start(&mut digest); + digest_text_field(&mut digest, &row, "pubkey")?; + digest_text_field(&mut digest, &row, "d_tag")?; + digest_i64_field(&mut digest, &row, "image_index")?; + digest_text_field(&mut digest, &row, "raw_tag_json")?; + digest_optional_text_field(&mut digest, &row, "url")?; + digest_optional_i64_field(&mut digest, &row, "width")?; + digest_optional_i64_field(&mut digest, &row, "height")?; + digest_optional_text_field(&mut digest, &row, "blossom_sha256")?; + digest_bool_field(&mut digest, &row, "qualifies")?; + digest_text_field(&mut digest, &row, "diagnostic_codes_json")?; + } + drop(food_images); + + digest_section(&mut digest, b"food_search")?; + let mut food_search = sqlx::query( + "SELECT event_id, pubkey, d_tag, title, summary, content, location FROM radroots_event_store_food_availability_search_fts ORDER BY event_id", + ) + .fetch(&mut *connection); + while let Some(row) = food_search.try_next().await? { + digest_row_start(&mut digest); + for field in [ + "event_id", "pubkey", "d_tag", "title", "summary", "content", "location", + ] { + digest_text_field(&mut digest, &row, field)?; + } + } + drop(food_search); + + digest_section(&mut digest, b"food_cursor")?; + let mut food_cursor = sqlx::query( + "SELECT feed_version, projection_version, scope_fingerprint, hook_manifest_sha256, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1", + ) + .fetch(&mut *connection); + while let Some(row) = food_cursor.try_next().await? { + digest_row_start(&mut digest); + digest_i64_field(&mut digest, &row, "feed_version")?; + digest_i64_field(&mut digest, &row, "projection_version")?; + digest_blob_field(&mut digest, &row, "scope_fingerprint")?; + digest_text_field(&mut digest, &row, "hook_manifest_sha256")?; + digest_i64_field(&mut digest, &row, "projected_row_count")?; + } + drop(food_cursor); + Ok(RadrootsEventStoreActiveProductStateDigestV1::from_bytes( + digest.finalize().into(), + )) +} + +fn digest_section(digest: &mut Sha256, name: &[u8]) -> Result<(), RadrootsEventStoreError> { + digest.update(b"S"); + digest_bytes(digest, b'N', name) +} + +fn digest_row_start(digest: &mut Sha256) { + digest.update(b"R"); +} + +fn digest_i64(digest: &mut Sha256, value: i64) { + digest.update(b"I"); + digest.update(value.to_be_bytes()); +} + +fn digest_bytes( + digest: &mut Sha256, + marker: u8, + value: &[u8], +) -> Result<(), RadrootsEventStoreError> { + let length = u64::try_from(value.len()).map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::RebuildPostcondition, + "digest field length exceeds u64", + ) + })?; + digest.update([marker]); + digest.update(length.to_be_bytes()); + digest.update(value); + Ok(()) +} + +fn digest_text(digest: &mut Sha256, value: &[u8]) -> Result<(), RadrootsEventStoreError> { + digest_bytes(digest, b'T', value) +} + +fn digest_optional_text( + digest: &mut Sha256, + value: Option<&str>, +) -> Result<(), RadrootsEventStoreError> { + match value { + Some(value) => { + digest.update([b'O', 1]); + digest_text(digest, value.as_bytes()) + } + None => { + digest.update([b'O', 0]); + Ok(()) + } + } +} + +fn digest_optional_i64(digest: &mut Sha256, value: Option<i64>) { + match value { + Some(value) => { + digest.update([b'O', 1]); + digest_i64(digest, value); + } + None => digest.update([b'O', 0]), + } +} + +fn digest_bool(digest: &mut Sha256, value: i64) -> Result<(), RadrootsEventStoreError> { + match value { + 0 | 1 => { + digest.update([b'B', if value == 0 { 0 } else { 1 }]); + Ok(()) + } + _ => rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::DerivedProductStateAuthority, + format!("digest boolean field has invalid value {value}"), + ), + } +} + +fn digest_text_field( + digest: &mut Sha256, + row: &sqlx::sqlite::SqliteRow, + field: &'static str, +) -> Result<(), RadrootsEventStoreError> { + let value: String = row.try_get(field)?; + digest_text(digest, value.as_bytes()) +} + +fn digest_optional_text_field( + digest: &mut Sha256, + row: &sqlx::sqlite::SqliteRow, + field: &'static str, +) -> Result<(), RadrootsEventStoreError> { + let value: Option<String> = row.try_get(field)?; + digest_optional_text(digest, value.as_deref()) +} + +fn digest_i64_field( + digest: &mut Sha256, + row: &sqlx::sqlite::SqliteRow, + field: &'static str, +) -> Result<(), RadrootsEventStoreError> { + digest_i64(digest, row.try_get(field)?); + Ok(()) +} + +fn digest_optional_i64_field( + digest: &mut Sha256, + row: &sqlx::sqlite::SqliteRow, + field: &'static str, +) -> Result<(), RadrootsEventStoreError> { + digest_optional_i64(digest, row.try_get(field)?); + Ok(()) +} + +fn digest_bool_field( + digest: &mut Sha256, + row: &sqlx::sqlite::SqliteRow, + field: &'static str, +) -> Result<(), RadrootsEventStoreError> { + digest_bool(digest, row.try_get(field)?) +} + +fn digest_blob_field( + digest: &mut Sha256, + row: &sqlx::sqlite::SqliteRow, + field: &'static str, +) -> Result<(), RadrootsEventStoreError> { + let value: Vec<u8> = row.try_get(field)?; + digest_bytes(digest, b'X', &value) +} + +async fn validate_scoped_integrity_v1( + connection: &mut SqliteConnection, +) -> Result<(), RadrootsEventStoreError> { + for table in REBUILD_OWNED_TABLES_V1 { + let integrity_sql = format!("PRAGMA main.integrity_check('{table}')"); + let rows = sqlx::query(sqlx::AssertSqlSafe(integrity_sql)) + .fetch_all(&mut *connection) + .await?; + for row in rows { + let detail: String = row.try_get(0)?; + if detail != "ok" { + return Err(RadrootsEventStoreError::IntegrityCheckFailed { detail }); + } + } + + let foreign_key_sql = format!("PRAGMA main.foreign_key_check('{table}')"); + if let Some(row) = sqlx::query(sqlx::AssertSqlSafe(foreign_key_sql)) + .fetch_optional(&mut *connection) + .await? + { + return Err(RadrootsEventStoreError::ForeignKeyViolation { + table: row.try_get("table")?, + rowid: row.try_get("rowid")?, + parent: row.try_get("parent")?, + foreign_key_index: row.try_get("fkid")?, + }); + } + } + 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(|source| RadrootsEventStoreError::Fts5IntegrityCheckFailed { + table: "radroots_event_store_food_availability_search_fts", + source, + })?; + Ok(()) +} + +async fn finish_raw_source_rebuild_transaction( + transaction: Transaction<'_, Sqlite>, + result: Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError>, +) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + match result { + Ok(report) => { + transaction.commit().await?; + Ok(report) + } + Err(primary) => { + let rollback = transaction.rollback().await; + preserve_raw_source_rebuild_primary_failure(primary, rollback) + } + } +} + +fn preserve_raw_source_rebuild_primary_failure<T>( + primary: RadrootsEventStoreError, + rollback: Result<(), sqlx::Error>, +) -> Result<T, RadrootsEventStoreError> { + match rollback { + Ok(()) => Err(primary), + Err(rollback) => Err( + RadrootsEventStoreError::RawSourceRebuildTransactionRollbackFailed { + primary: Box::new(primary), + rollback, + }, + ), + } +} + +#[cfg(test)] +fn inject_raw_source_rebuild_failpoint_v1( + selected: Option<RawSourceRebuildFailpointV1>, + stage: RawSourceRebuildFailpointV1, +) -> Result<(), RadrootsEventStoreError> { + if selected == Some(stage) { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::RebuildPostcondition, + format!("injected raw-source rebuild failure at {}", stage.as_str()), + ); + } + Ok(()) +} + +fn rebuild_state_error( + kind: RadrootsEventStoreRawSourceRebuildDriftV1, + detail: impl Into<String>, +) -> RadrootsEventStoreError { + RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind, + detail: detail.into(), + } +} + +fn rebuild_drift<T>( + kind: RadrootsEventStoreRawSourceRebuildDriftV1, + detail: impl Into<String>, +) -> Result<T, RadrootsEventStoreError> { + Err(rebuild_state_error(kind, detail)) +} + +#[cfg(test)] +pub(crate) fn preserve_raw_source_rebuild_primary_failure_for_test<T>( + primary: RadrootsEventStoreError, + rollback: Result<(), sqlx::Error>, +) -> Result<T, RadrootsEventStoreError> { + preserve_raw_source_rebuild_primary_failure(primary, rollback) +} diff --git a/crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs b/crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs @@ -0,0 +1,940 @@ +use super::ReconciledEvent; +use crate::model::reconciliation_v1::{RadrootsEventAdmissionStatus, StoredEventClass}; +use crate::{RadrootsEventStoreError, RadrootsEventStoreRawSourceRebuildDriftV1}; +use radroots_event::envelope::RadrootsEventEnvelope; +use radroots_event::event_head::v1::{ + RadrootsCurrentEventHead, RadrootsEventHeadCandidate, RadrootsEventHeadCandidateResult, + RadrootsEventHeadCoordinate, RadrootsEventHeadDecision, + event_head_candidate_for_nip01_event_v1, select_event_head_v1, +}; +use radroots_event::ids::RadrootsNip01Coordinate; +use radroots_event_codec::deletion::reconciliation_v1::admission::{ + RadrootsAdmittedNip09DeletionRequestEventV1, admit_verified_nip09_deletion_request_event_v1, +}; +#[cfg(test)] +use radroots_event_codec::deletion::reconciliation_v1::evaluator::evaluate_nip09_suppression_from_borrowed_requests_v1; +use radroots_event_codec::deletion::reconciliation_v1::evaluator::{ + RadrootsNip09SuppressionOutcome, RadrootsNip09SuppressionReason, +}; +use std::collections::BTreeMap; +#[cfg(test)] +use std::collections::BTreeSet; + +#[derive(Debug, PartialEq, Eq)] +pub(super) struct VisibilityOracleFactV1 { + pub(super) event_id: String, + pub(super) admission_status: String, + pub(super) contract_id: Option<String>, + pub(super) event_class: String, + pub(super) raw_d_tag: Option<String>, + pub(super) is_raw_head: i64, + pub(super) raw_head_event_id: Option<String>, + pub(super) suppression_outcome: Option<String>, + pub(super) suppression_reason: Option<String>, + pub(super) event_reference_request_id: Option<String>, + pub(super) address_reference_request_id: Option<String>, + pub(super) address_reference_cutoff: Option<i64>, + pub(super) current_visibility: String, +} + +pub(super) async fn audit_current_visibility_from_raw_v1( + events: &[ReconciledEvent], + actual: Vec<VisibilityOracleFactV1>, +) -> Result<(), RadrootsEventStoreError> { + let expected = expected_visibility(events)?; + if actual != expected { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::DerivedProductStateAuthority, + "current visibility does not equal the independent immutable-raw oracle", + ); + } + Ok(()) +} + +fn expected_visibility( + events: &[ReconciledEvent], +) -> Result<Vec<VisibilityOracleFactV1>, RadrootsEventStoreError> { + let winners = oracle_head_winners(events); + let requests = oracle_deletion_requests(events)?; + let request_index = OracleRequestIndexV1::new(&requests); + let mut expected = Vec::with_capacity(events.len()); + for event in events { + let envelope = event.verified_event.event(); + let event_class = StoredEventClass::from_event_kind_class(envelope.kind_class()); + let raw_d_tag = oracle_raw_d_tag(envelope, event_class); + let raw_head_event_id = match event_class { + StoredEventClass::Regular => None, + StoredEventClass::Replaceable => winners + .get(&RadrootsEventHeadCoordinate::Replaceable { + kind: envelope.kind_u32(), + pubkey: envelope.author().clone(), + }) + .map(|winner| winner.event_id.to_string()), + StoredEventClass::Addressable => winners + .get(&RadrootsEventHeadCoordinate::Addressable { + kind: envelope.kind_u32(), + pubkey: envelope.author().clone(), + d_tag: raw_d_tag.clone().unwrap_or_default(), + }) + .map(|winner| winner.event_id.to_string()), + StoredEventClass::Ephemeral => { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::ImmutableRawAuthority, + "the immutable-raw visibility oracle found an ephemeral row", + ); + } + }; + let is_raw_head = event_class == StoredEventClass::Regular + || raw_head_event_id.as_deref() == Some(envelope.id_str()); + let admitted = event.admission.status == RadrootsEventAdmissionStatus::Admitted; + let ( + suppression_outcome, + suppression_reason, + event_reference_request_id, + address_reference_request_id, + address_reference_cutoff, + ) = if admitted { + let decision = request_index.decision(envelope); + ( + Some(decision.outcome.code().to_owned()), + Some(decision.reason.code().to_owned()), + decision.event_reference_request_id, + decision.address_reference_request_id, + decision + .address_reference_cutoff + .map(i64::try_from) + .transpose() + .map_err(|_| RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: + RadrootsEventStoreRawSourceRebuildDriftV1::DerivedProductStateAuthority, + detail: format!( + "deletion cutoff for `{}` exceeds SQLite integer range", + envelope.id_str() + ), + })?, + ) + } else { + (None, None, None, None, None) + }; + let current_visibility = if !admitted { + "not_admitted" + } else if !is_raw_head { + "not_current" + } else if suppression_outcome.as_deref() + == Some(RadrootsNip09SuppressionOutcome::Suppressed.code()) + { + "suppressed" + } else { + "visible" + }; + expected.push(VisibilityOracleFactV1 { + event_id: envelope.id_str().to_owned(), + admission_status: event.admission.status.as_str().to_owned(), + contract_id: event + .admission + .contract + .map(|contract| contract.id.to_owned()), + event_class: event_class.as_str().to_owned(), + raw_d_tag, + is_raw_head: i64::from(is_raw_head), + raw_head_event_id, + suppression_outcome, + suppression_reason, + event_reference_request_id, + address_reference_request_id, + address_reference_cutoff, + current_visibility: current_visibility.to_owned(), + }); + } + Ok(expected) +} + +fn oracle_head_winners( + events: &[ReconciledEvent], +) -> BTreeMap<RadrootsEventHeadCoordinate, RadrootsEventHeadCandidate> { + let mut winners = BTreeMap::new(); + for event in events { + let RadrootsEventHeadCandidateResult::Candidate(candidate) = + event_head_candidate_for_nip01_event_v1(event.verified_event.event()) + else { + continue; + }; + let current = + winners + .get(&candidate.coordinate) + .map( + |winner: &RadrootsEventHeadCandidate| RadrootsCurrentEventHead { + coordinate: winner.coordinate.clone(), + event_id: winner.event_id.clone(), + created_at: winner.created_at, + }, + ); + if matches!( + select_event_head_v1(candidate.clone(), current.as_ref()), + RadrootsEventHeadDecision::Applied(_) + ) { + winners.insert(candidate.coordinate.clone(), candidate); + } + } + winners +} + +fn oracle_raw_d_tag( + event: &RadrootsEventEnvelope, + event_class: StoredEventClass, +) -> Option<String> { + match event_class { + StoredEventClass::Regular | StoredEventClass::Ephemeral => None, + StoredEventClass::Replaceable => Some(String::new()), + StoredEventClass::Addressable => Some( + event + .tag_slices() + .iter() + .find(|tag| tag.as_slice().first().is_some_and(|name| name == "d")) + .and_then(|tag| tag.as_slice().get(1)) + .cloned() + .unwrap_or_default(), + ), + } +} + +fn oracle_deletion_requests( + events: &[ReconciledEvent], +) -> Result<Vec<RadrootsAdmittedNip09DeletionRequestEventV1>, RadrootsEventStoreError> { + let mut requests = events + .iter() + .filter(|event| { + event.admission.status == RadrootsEventAdmissionStatus::Admitted + && event.verified_event.event().kind_u32() == 5 + }) + .map(|event| { + admit_verified_nip09_deletion_request_event_v1(event.verified_event.clone()).map_err( + |error| RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::DerivedProductStateAuthority, + detail: format!( + "oracle could not type admitted deletion request `{}`: {error}", + event.verified_event.event().id_str() + ), + }, + ) + }) + .collect::<Result<Vec<_>, _>>()?; + requests.sort_by(|left, right| left.event().id().cmp(right.event().id())); + Ok(requests) +} + +struct OracleRequestIndexV1<'a> { + requests: &'a [RadrootsAdmittedNip09DeletionRequestEventV1], + event_targets: BTreeMap<String, BTreeMap<String, usize>>, + address_targets: BTreeMap<(u32, String, String), OracleAddressRequestEvidenceV1>, +} + +#[derive(Debug, PartialEq, Eq)] +struct OracleSuppressionDecisionV1 { + outcome: RadrootsNip09SuppressionOutcome, + reason: RadrootsNip09SuppressionReason, + event_reference_request_id: Option<String>, + address_reference_request_id: Option<String>, + address_reference_cutoff: Option<u64>, +} + +#[derive(Default)] +struct OracleAddressRequestEvidenceV1 { + authorized: Option<usize>, + unauthorized: Option<usize>, +} + +impl<'a> OracleRequestIndexV1<'a> { + fn new(requests: &'a [RadrootsAdmittedNip09DeletionRequestEventV1]) -> Self { + let mut event_targets = BTreeMap::<String, BTreeMap<String, usize>>::new(); + let mut address_targets = + BTreeMap::<(u32, String, String), OracleAddressRequestEvidenceV1>::new(); + for (index, request) in requests.iter().enumerate() { + let request_author = request.event().author_str(); + for target in request.projection().event_targets() { + event_targets + .entry(target.event_id().as_str().to_owned()) + .or_default() + .entry(request_author.to_owned()) + .and_modify(|current| { + if request.event().id() < requests[*current].event().id() { + *current = index; + } + }) + .or_insert(index); + } + for target in request.projection().address_targets() { + let coordinate = ( + target.coordinate().kind(), + target.coordinate().pubkey().as_str().to_owned(), + target.coordinate().identifier().to_owned(), + ); + let evidence = address_targets.entry(coordinate.clone()).or_default(); + if request_author == coordinate.1 { + let replace = evidence.authorized.is_none_or(|current| { + let current = requests[current].event(); + request.event().created_at_u64() > current.created_at_u64() + || (request.event().created_at_u64() == current.created_at_u64() + && request.event().id() < current.id()) + }); + if replace { + evidence.authorized = Some(index); + } + } else if evidence.unauthorized.is_none() { + evidence.unauthorized = Some(index); + } + } + } + Self { + requests, + event_targets, + address_targets, + } + } + + fn decision(&self, event: &RadrootsEventEnvelope) -> OracleSuppressionDecisionV1 { + if event.kind_u32() == 5 { + return OracleSuppressionDecisionV1 { + outcome: RadrootsNip09SuppressionOutcome::Visible, + reason: RadrootsNip09SuppressionReason::DeletionRequestImmune, + event_reference_request_id: None, + address_reference_request_id: None, + address_reference_cutoff: None, + }; + } + + let (event_reference, unauthorized_event_reference) = self + .event_targets + .get(event.id_str()) + .map_or((None, false), |by_author| { + let authorized = by_author.get(event.author_str()).copied(); + ( + authorized, + by_author.len() > usize::from(authorized.is_some()), + ) + }); + let address_evidence = oracle_nip01_coordinate_key(event) + .as_ref() + .and_then(|coordinate| self.address_targets.get(coordinate)); + let address_reference = address_evidence.and_then(|evidence| evidence.authorized); + let has_unauthorized_reference = unauthorized_event_reference + || address_evidence.is_some_and(|evidence| evidence.unauthorized.is_some()); + + let event_reference_request_id = + event_reference.map(|index| self.requests[index].event().id().as_str().to_owned()); + let (address_reference_request_id, address_reference_cutoff) = address_reference + .map(|index| { + let request = self.requests[index].event(); + ( + Some(request.id().as_str().to_owned()), + Some(request.created_at_u64()), + ) + }) + .unwrap_or((None, None)); + let address_applies = + address_reference_cutoff.is_some_and(|cutoff| event.created_at_u64() <= cutoff); + let (outcome, reason) = match (event_reference.is_some(), address_applies) { + (true, true) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdAndAddressReference, + ), + (true, false) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdReference, + ), + (false, true) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, + ), + (false, false) if address_reference.is_some() => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, + ), + (false, false) if has_unauthorized_reference => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::RequestAuthorMismatch, + ), + (false, false) => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::NoAuthorizedReference, + ), + }; + OracleSuppressionDecisionV1 { + outcome, + reason, + event_reference_request_id, + address_reference_request_id, + address_reference_cutoff, + } + } +} + +fn oracle_nip01_coordinate_key(event: &RadrootsEventEnvelope) -> Option<(u32, String, String)> { + let kind = event.kind_u32(); + let identifier = match kind { + 0 | 3 | 10_000..=19_999 => String::new(), + 30_000..=39_999 => event + .tag_slices() + .iter() + .find(|tag| tag.as_slice().first().is_some_and(|name| name == "d"))? + .as_slice() + .get(1)? + .clone(), + _ => return None, + }; + let coordinate = + RadrootsNip01Coordinate::parse(format!("{kind}:{}:{identifier}", event.author_str())) + .ok()?; + Some(( + coordinate.kind(), + coordinate.pubkey().as_str().to_owned(), + coordinate.identifier().to_owned(), + )) +} + +fn rebuild_drift<T>( + kind: RadrootsEventStoreRawSourceRebuildDriftV1, + detail: impl Into<String>, +) -> Result<T, RadrootsEventStoreError> { + Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind, + detail: detail.into(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nip09::reconciliation_v1::{ + ReconciliationCapacityLimits, load_reconciliation_snapshot, + }; + use crate::{RadrootsEventIngest, RadrootsEventStore}; + use nostr::{EventBuilder, Keys, Kind, SecretKey, Tag, TagKind, Timestamp}; + use radroots_event_codec::verification::v1::RadrootsSignatureVerifiedEvent; + use serde_json::Value; + + const FOOD_FIXTURE: &[u8] = + include_bytes!("../../../tests/fixtures/food_availability_projection.v1.json"); + const FIXTURE_SECRET_KEY_HEX: &str = + "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; + const OTHER_SECRET_KEY_HEX: &str = + "0000000000000000000000000000000000000000000000000000000000000002"; + + fn signed_ingest(kind: u16, created_at: u64, content: &str) -> RadrootsEventIngest { + signed_ingest_with_tags(kind, created_at, content, Vec::new()) + } + + fn signed_ingest_with_tags( + kind: u16, + created_at: u64, + content: &str, + tags: Vec<Vec<String>>, + ) -> RadrootsEventIngest { + signed_ingest_with_tags_and_key(kind, created_at, content, tags, FIXTURE_SECRET_KEY_HEX) + } + + fn signed_ingest_with_tags_and_key( + kind: u16, + created_at: u64, + content: &str, + tags: Vec<Vec<String>>, + secret_key_hex: &str, + ) -> RadrootsEventIngest { + let keys = Keys::new(SecretKey::from_hex(secret_key_hex).expect("fixture secret key")); + let event = EventBuilder::new(Kind::Custom(kind), content) + .tags( + tags.into_iter() + .map(|mut values| { + let name = values.remove(0); + Tag::custom(TagKind::Custom(name.into()), values) + }) + .collect::<Vec<_>>(), + ) + .custom_created_at(Timestamp::from_secs(created_at)) + .sign_with_keys(&keys) + .expect("signed oracle event"); + RadrootsEventIngest::from_raw_json( + serde_json::to_string(&event).expect("oracle event JSON"), + i64::try_from(created_at * 1_000).expect("oracle observed time"), + ) + .expect("verified oracle ingest") + } + + fn fixture_ingests(case_id: &str) -> Vec<RadrootsEventIngest> { + let fixture: Value = serde_json::from_slice(FOOD_FIXTURE).expect("Food fixture JSON"); + let case = fixture["cases"] + .as_array() + .expect("Food fixture cases") + .iter() + .find(|case| case["id"].as_str() == Some(case_id)) + .expect("oracle fixture case"); + case["events"] + .as_array() + .expect("oracle fixture events") + .iter() + .map(|observed| { + RadrootsEventIngest::from_raw_json( + serde_json::to_string(&observed["event"]).expect("fixture event JSON"), + observed["observed_at_ms"] + .as_i64() + .expect("fixture observed time"), + ) + .expect("verified fixture ingest") + }) + .collect() + } + + fn assert_indexed_decision_matches_protocol_v1( + target: &RadrootsSignatureVerifiedEvent, + requests: &[RadrootsAdmittedNip09DeletionRequestEventV1], + index: &OracleRequestIndexV1<'_>, + ) -> OracleSuppressionDecisionV1 { + let expected = evaluate_nip09_suppression_from_borrowed_requests_v1(target, requests); + let actual = index.decision(target.event()); + assert_eq!(actual.outcome, expected.outcome()); + assert_eq!(actual.reason, expected.reason()); + assert_eq!( + actual.event_reference_request_id.as_deref(), + expected + .event_reference() + .map(|evidence| evidence.request_id().as_str()) + ); + assert_eq!( + actual.address_reference_request_id.as_deref(), + expected + .address_reference() + .map(|evidence| evidence.request_id().as_str()) + ); + assert_eq!( + actual.address_reference_cutoff, + expected + .address_reference() + .map(|evidence| evidence.inclusive_cutoff()) + ); + actual + } + + fn admitted_request( + ingest: RadrootsEventIngest, + ) -> RadrootsAdmittedNip09DeletionRequestEventV1 { + admit_verified_nip09_deletion_request_event_v1(ingest.verified_event().clone()) + .expect("admitted deletion request") + } + + #[test] + fn raw_snapshot_visibility_oracle_bounds_high_fan_in_evidence_v1() { + const REQUEST_COUNT: usize = 512; + let target = signed_ingest_with_tags( + 30_402, + 1_700_000_000, + "{}", + vec![vec!["d".to_owned(), "high-fan-in".to_owned()]], + ); + let coordinate = format!("30402:{}:high-fan-in", target.event().author_str()); + let mut requests = (0..REQUEST_COUNT) + .map(|index| { + let ingest = signed_ingest_with_tags( + 5, + 1_700_001_000 + u64::try_from(index).expect("request timestamp"), + "high fan-in", + vec![vec!["a".to_owned(), coordinate.clone()]], + ); + admit_verified_nip09_deletion_request_event_v1(ingest.verified_event().clone()) + .expect("admitted deletion request") + }) + .collect::<Vec<_>>(); + requests.sort_by(|left, right| left.event().id().cmp(right.event().id())); + + let index = OracleRequestIndexV1::new(&requests); + let actual = index.decision(target.event()); + assert_eq!( + actual.address_reference_cutoff, + Some(1_700_001_000 + u64::try_from(REQUEST_COUNT - 1).expect("request count")) + ); + assert_indexed_decision_matches_protocol_v1(target.verified_event(), &requests, &index); + } + + #[test] + fn raw_snapshot_visibility_oracle_matches_wide_event_and_address_requests_v1() { + const TARGETS_PER_REFERENCE_KIND: usize = 128; + let mut targets = Vec::with_capacity(TARGETS_PER_REFERENCE_KIND * 2); + let mut request_tags = Vec::with_capacity(TARGETS_PER_REFERENCE_KIND * 2); + for index in 0..TARGETS_PER_REFERENCE_KIND { + let offset = u64::try_from(index).expect("target timestamp"); + let event_target = signed_ingest( + 1, + 1_700_010_000 + offset, + &format!("wide event target {index}"), + ); + request_tags.push(vec![ + "e".to_owned(), + event_target.event().id_str().to_owned(), + ]); + targets.push(event_target); + + let identifier = format!("wide-address-{index}"); + let address_target = signed_ingest_with_tags( + 30_402, + 1_700_020_000 + offset, + "{}", + vec![vec!["d".to_owned(), identifier.clone()]], + ); + request_tags.push(vec![ + "a".to_owned(), + format!("30402:{}:{identifier}", address_target.event().author_str()), + ]); + targets.push(address_target); + } + let request = signed_ingest_with_tags(5, 1_700_030_000, "wide request", request_tags); + let requests = vec![ + admit_verified_nip09_deletion_request_event_v1(request.verified_event().clone()) + .expect("admitted wide deletion request"), + ]; + let index = OracleRequestIndexV1::new(&requests); + for target in &targets { + assert_indexed_decision_matches_protocol_v1(target.verified_event(), &requests, &index); + } + } + + #[test] + fn raw_snapshot_visibility_oracle_matches_all_protocol_decision_branches_v1() { + let no_reference = signed_ingest(1, 1_700_100_000, "no reference"); + let no_reference_requests = Vec::new(); + let no_reference_index = OracleRequestIndexV1::new(&no_reference_requests); + assert_eq!( + assert_indexed_decision_matches_protocol_v1( + no_reference.verified_event(), + &no_reference_requests, + &no_reference_index, + ) + .reason, + RadrootsNip09SuppressionReason::NoAuthorizedReference + ); + + let immune = signed_ingest(5, 1_700_100_010, "immune"); + let immune_requests = vec![admitted_request(signed_ingest_with_tags( + 5, + 1_700_100_020, + "references deletion request", + vec![vec!["e".to_owned(), immune.event().id_str().to_owned()]], + ))]; + let immune_index = OracleRequestIndexV1::new(&immune_requests); + assert_eq!( + assert_indexed_decision_matches_protocol_v1( + immune.verified_event(), + &immune_requests, + &immune_index, + ) + .reason, + RadrootsNip09SuppressionReason::DeletionRequestImmune + ); + + let unauthorized = signed_ingest(1, 1_700_100_030, "unauthorized target"); + let unauthorized_requests = vec![admitted_request(signed_ingest_with_tags_and_key( + 5, + 1_700_100_040, + "wrong author", + vec![vec![ + "e".to_owned(), + unauthorized.event().id_str().to_owned(), + ]], + OTHER_SECRET_KEY_HEX, + ))]; + let unauthorized_index = OracleRequestIndexV1::new(&unauthorized_requests); + assert_eq!( + assert_indexed_decision_matches_protocol_v1( + unauthorized.verified_event(), + &unauthorized_requests, + &unauthorized_index, + ) + .reason, + RadrootsNip09SuppressionReason::RequestAuthorMismatch + ); + + let stale = signed_ingest_with_tags( + 30_402, + 1_700_100_100, + "{}", + vec![vec!["d".to_owned(), "stale".to_owned()]], + ); + let stale_requests = vec![ + admitted_request(signed_ingest_with_tags( + 5, + 1_700_100_090, + "stale address", + vec![vec![ + "a".to_owned(), + format!("30402:{}:stale", stale.event().author_str()), + ]], + )), + admitted_request(signed_ingest_with_tags_and_key( + 5, + 1_700_100_110, + "unauthorized exact reference", + vec![vec!["e".to_owned(), stale.event().id_str().to_owned()]], + OTHER_SECRET_KEY_HEX, + )), + ]; + let stale_index = OracleRequestIndexV1::new(&stale_requests); + let stale_decision = assert_indexed_decision_matches_protocol_v1( + stale.verified_event(), + &stale_requests, + &stale_index, + ); + assert_eq!( + stale_decision.reason, + RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget + ); + assert!(stale_decision.address_reference_request_id.is_some()); + + let exact = signed_ingest_with_tags( + 30_402, + 1_700_100_200, + "{}", + vec![vec!["d".to_owned(), "exact".to_owned()]], + ); + let exact_requests = vec![ + admitted_request(signed_ingest_with_tags( + 5, + 1_700_100_190, + "stale address", + vec![vec![ + "a".to_owned(), + format!("30402:{}:exact", exact.event().author_str()), + ]], + )), + admitted_request(signed_ingest_with_tags( + 5, + 1_700_100_210, + "exact event", + vec![vec!["e".to_owned(), exact.event().id_str().to_owned()]], + )), + ]; + let exact_index = OracleRequestIndexV1::new(&exact_requests); + let exact_decision = assert_indexed_decision_matches_protocol_v1( + exact.verified_event(), + &exact_requests, + &exact_index, + ); + assert_eq!( + exact_decision.reason, + RadrootsNip09SuppressionReason::EventIdReference + ); + assert!(exact_decision.event_reference_request_id.is_some()); + assert!(exact_decision.address_reference_request_id.is_some()); + + let address = signed_ingest_with_tags( + 30_402, + 1_700_100_300, + "{}", + vec![vec!["d".to_owned(), "address".to_owned()]], + ); + let address_requests = vec![admitted_request(signed_ingest_with_tags( + 5, + 1_700_100_310, + "address reference", + vec![vec![ + "a".to_owned(), + format!("30402:{}:address", address.event().author_str()), + ]], + ))]; + let address_index = OracleRequestIndexV1::new(&address_requests); + assert_eq!( + assert_indexed_decision_matches_protocol_v1( + address.verified_event(), + &address_requests, + &address_index, + ) + .reason, + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff + ); + + let both = signed_ingest_with_tags( + 30_402, + 1_700_100_400, + "{}", + vec![vec!["d".to_owned(), "both".to_owned()]], + ); + let both_requests = vec![admitted_request(signed_ingest_with_tags( + 5, + 1_700_100_410, + "both references", + vec![ + vec!["e".to_owned(), both.event().id_str().to_owned()], + vec![ + "a".to_owned(), + format!("30402:{}:both", both.event().author_str()), + ], + ], + ))]; + let both_index = OracleRequestIndexV1::new(&both_requests); + let both_decision = assert_indexed_decision_matches_protocol_v1( + both.verified_event(), + &both_requests, + &both_index, + ); + assert_eq!( + both_decision.reason, + RadrootsNip09SuppressionReason::EventIdAndAddressReference + ); + assert!(both_decision.event_reference_request_id.is_some()); + assert!(both_decision.address_reference_request_id.is_some()); + } + + #[test] + fn raw_snapshot_visibility_oracle_is_order_and_repeat_invariant_v1() { + let target = signed_ingest_with_tags( + 30_402, + 1_700_200_000, + "{}", + vec![vec!["d".to_owned(), "invariant".to_owned()]], + ); + let coordinate = format!("30402:{}:invariant", target.event().author_str()); + let first = admitted_request(signed_ingest_with_tags( + 5, + 1_700_200_010, + "first exact", + vec![vec!["e".to_owned(), target.event().id_str().to_owned()]], + )); + let second = admitted_request(signed_ingest_with_tags( + 5, + 1_700_200_020, + "second exact and address", + vec![ + vec!["e".to_owned(), target.event().id_str().to_owned()], + vec!["a".to_owned(), coordinate.clone()], + ], + )); + let third = admitted_request(signed_ingest_with_tags( + 5, + 1_700_200_020, + "address tie", + vec![vec!["a".to_owned(), coordinate]], + )); + let canonical_requests = vec![first.clone(), second.clone(), third.clone()]; + let repeated_reverse_requests = vec![ + third.clone(), + second.clone(), + first.clone(), + third, + second, + first, + ]; + let canonical_index = OracleRequestIndexV1::new(&canonical_requests); + let repeated_reverse_index = OracleRequestIndexV1::new(&repeated_reverse_requests); + let canonical = assert_indexed_decision_matches_protocol_v1( + target.verified_event(), + &canonical_requests, + &canonical_index, + ); + let repeated_reverse = assert_indexed_decision_matches_protocol_v1( + target.verified_event(), + &repeated_reverse_requests, + &repeated_reverse_index, + ); + assert_eq!(repeated_reverse, canonical); + } + + #[tokio::test] + async fn raw_snapshot_visibility_oracle_covers_regular_replaceable_addressable_and_deletion_v1() + { + let store = RadrootsEventStore::open_memory().await.expect("open store"); + let fixture_pubkey = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + for ingest in [ + signed_ingest(1, 1_700_000_010, "Victoria harvest update"), + signed_ingest(0, 1_700_000_020, "{}"), + signed_ingest_with_tags( + 5, + 1_700_000_030, + "Profile withdrawn", + vec![vec!["a".to_owned(), format!("0:{fixture_pubkey}:")]], + ), + ] + .into_iter() + .chain(fixture_ingests( + "authorized_address_deletion_retracts_projection", + )) { + store.ingest_event(ingest).await.expect("ingest oracle row"); + } + + let mut connection = store.pool().acquire().await.expect("connection"); + let snapshot = load_reconciliation_snapshot( + &mut connection, + ReconciliationCapacityLimits::production(), + ) + .await + .expect("load immutable raw snapshot"); + let requests = oracle_deletion_requests(&snapshot.events).expect("oracle requests"); + let request_index = OracleRequestIndexV1::new(&requests); + for event in &snapshot.events { + assert_indexed_decision_matches_protocol_v1( + &event.verified_event, + &requests, + &request_index, + ); + } + let expected = expected_visibility(&snapshot.events).expect("oracle facts"); + let classes = expected + .iter() + .map(|fact| fact.event_class.as_str()) + .collect::<BTreeSet<_>>(); + assert_eq!( + classes, + BTreeSet::from(["regular", "replaceable", "addressable"]) + ); + + let deletion_id = snapshot + .events + .iter() + .find(|event| event.verified_event.event().kind_u32() == 5) + .map(|event| event.verified_event.event().id_str()) + .expect("deletion request"); + let deletion_fact = expected + .iter() + .find(|fact| fact.event_id == deletion_id) + .expect("deletion oracle fact"); + assert_eq!(deletion_fact.current_visibility, "visible"); + assert_eq!( + deletion_fact.suppression_reason.as_deref(), + Some("deletion_request_immune") + ); + + let addressable_id = snapshot + .events + .iter() + .find(|event| event.verified_event.event().kind_u32() == 30_402) + .map(|event| event.verified_event.event().id_str()) + .expect("addressable Food event"); + assert_eq!( + expected + .iter() + .find(|fact| fact.event_id == addressable_id) + .expect("addressable oracle fact") + .current_visibility, + "suppressed" + ); + let replaceable_id = snapshot + .events + .iter() + .find(|event| event.verified_event.event().kind_u32() == 0) + .map(|event| event.verified_event.event().id_str()) + .expect("replaceable profile event"); + let replaceable_fact = expected + .iter() + .find(|fact| fact.event_id == replaceable_id) + .expect("replaceable oracle fact"); + assert_eq!(replaceable_fact.current_visibility, "suppressed"); + assert!(replaceable_fact.address_reference_request_id.is_some()); + audit_current_visibility_from_raw_v1(&snapshot.events, expected) + .await + .expect("matching oracle audit"); + + let mut drift = expected_visibility(&snapshot.events).expect("second oracle facts"); + drift[0].current_visibility = "forged".to_owned(); + assert!( + audit_current_visibility_from_raw_v1(&snapshot.events, drift) + .await + .is_err() + ); + } +} diff --git a/crates/event_store/src/schema.rs b/crates/event_store/src/schema.rs @@ -1,4 +1,3 @@ -use crate::RadrootsEventStoreError; use crate::migrations::{ EVENT_STORE_LEDGER_CREATE_DDL, EVENT_STORE_LEDGER_DDL, EVENT_STORE_LEDGER_NAME, EVENT_STORE_MIGRATIONS, EventStoreMigration, EventStoreMigrationHook, @@ -7,6 +6,7 @@ use crate::migrations::{ sqlite_identifier_starts_with, validate_embedded_migration_registry, validate_migration_registry, }; +use crate::{RadrootsEventStoreError, RadrootsEventStoreRawSourceRebuildDriftV1}; use sha2::{Digest, Sha256}; use sqlx::{Row, Sqlite, SqliteConnection, SqlitePool, Transaction}; use std::collections::{BTreeMap, BTreeSet}; @@ -24,6 +24,8 @@ use crate::store::food_availability_projection_v1::{ validate_food_availability_projection_hook_state_fast_v1, }; +const RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1: u32 = 4; + #[cfg(test)] const EMPTY_SCHEMA_SHA256: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; @@ -88,6 +90,54 @@ pub(crate) async fn migrate_event_store_schema( migrate_event_store_schema_with_generation_provider(pool, &OsSourceGenerationProvider).await } +/// Validates the exact current managed catalog and ledger without consulting +/// derived hook state. Raw-source repair uses this before it starts replacing +/// derived authority; ordinary open continues through the stricter hook path. +pub(crate) async fn validate_exact_managed_v4_for_raw_source_rebuild_v1( + connection: &mut SqliteConnection, +) -> Result<(), RadrootsEventStoreError> { + validate_embedded_migration_registry()?; + validate_repair_temp_schema_bounded_v1(connection, EVENT_STORE_MIGRATIONS).await?; + let catalog = read_repair_catalog_bounded_v1(connection, EVENT_STORE_MIGRATIONS).await?; + if !validate_ledger_catalog(&catalog)? { + return Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + detail: "maintenance repair requires an exact managed-v4 migration ledger".to_owned(), + }); + } + let history = + read_repair_history_bounded_v1(connection, RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1).await?; + let current = validate_history_against_registry( + &history, + EVENT_STORE_MIGRATIONS, + RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1, + )?; + if current != RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1 { + return Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + detail: format!( + "maintenance repair requires managed schema version {}, found {current}", + RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1 + ), + }); + } + let migration = + migration_for_version(EVENT_STORE_MIGRATIONS, RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1).ok_or( + RadrootsEventStoreError::UnknownMigration { + version: RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1, + }, + )?; + let actual = catalog_fingerprint(&governed_catalog(&catalog, EVENT_STORE_MIGRATIONS)); + if actual != migration.schema_sha256 { + return Err(RadrootsEventStoreError::SchemaFingerprintMismatch { + version: migration.version, + expected: migration.schema_sha256, + actual, + }); + } + Ok(()) +} + pub(crate) async fn migrate_event_store_schema_with_generation_provider( pool: &SqlitePool, generation_provider: &dyn SourceGenerationProvider, @@ -697,6 +747,106 @@ pub(crate) async fn validate_event_store_temp_schema( validate_event_store_temp_schema_with_registry(connection, EVENT_STORE_MIGRATIONS).await } +fn repair_governed_catalog_authority_v1( + registry: &[EventStoreMigration], +) -> Result<(String, i64), RadrootsEventStoreError> { + let mut names = registry + .iter() + .flat_map(|migration| migration.owned_object_names.iter().copied()) + .collect::<BTreeSet<_>>(); + names.insert(EVENT_STORE_LEDGER_NAME); + let canonical_row_count = i64::try_from(names.len()).map_err(|_| { + RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + detail: "managed catalog authority exceeds the SQLite row-count range".to_owned(), + } + })?; + let row_limit = canonical_row_count.checked_add(1).ok_or_else(|| { + RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + detail: "managed catalog authority cannot reserve a collision row".to_owned(), + } + })?; + Ok((serde_json::to_string(&names)?, row_limit)) +} + +async fn read_repair_catalog_bounded_v1( + connection: &mut SqliteConnection, + registry: &[EventStoreMigration], +) -> Result<Vec<CatalogRow>, RadrootsEventStoreError> { + let (governed_names_json, row_limit) = repair_governed_catalog_authority_v1(registry)?; + let rows = sqlx::query( + "WITH governed(name) AS ( + SELECT CAST(value AS TEXT) COLLATE NOCASE FROM json_each(?) + ) + SELECT type, name, tbl_name, sql + FROM main.sqlite_schema + WHERE lower(substr(name, 1, 7)) != 'sqlite_' + AND ( + name COLLATE NOCASE IN (SELECT name FROM governed) + OR tbl_name COLLATE NOCASE IN (SELECT name FROM governed) + OR lower(substr(name, 1, length(?))) = lower(?) + OR lower(substr(tbl_name, 1, length(?))) = lower(?) + ) + ORDER BY type, name, tbl_name + LIMIT ?", + ) + .bind(&governed_names_json) + .bind(crate::migrations::EVENT_STORE_RESERVED_PREFIX) + .bind(crate::migrations::EVENT_STORE_RESERVED_PREFIX) + .bind(crate::migrations::EVENT_STORE_RESERVED_PREFIX) + .bind(crate::migrations::EVENT_STORE_RESERVED_PREFIX) + .bind(row_limit) + .fetch_all(&mut *connection) + .await?; + rows.into_iter() + .map(|row| { + Ok(CatalogRow { + object_type: row.try_get("type")?, + name: row.try_get("name")?, + table_name: row.try_get("tbl_name")?, + sql: row.try_get("sql")?, + }) + }) + .collect() +} + +pub(crate) async fn validate_repair_temp_schema_bounded_v1( + connection: &mut SqliteConnection, + registry: &[EventStoreMigration], +) -> Result<(), RadrootsEventStoreError> { + let (governed_names_json, _) = repair_governed_catalog_authority_v1(registry)?; + let collision = sqlx::query( + "WITH governed(name) AS ( + SELECT CAST(value AS TEXT) COLLATE NOCASE FROM json_each(?) + ) + SELECT type, name, tbl_name + FROM temp.sqlite_schema + WHERE type IN ('trigger', 'view') + OR name COLLATE NOCASE IN (SELECT name FROM governed) + OR tbl_name COLLATE NOCASE IN (SELECT name FROM governed) + OR lower(substr(name, 1, length(?))) = lower(?) + OR lower(substr(tbl_name, 1, length(?))) = lower(?) + ORDER BY type, name, tbl_name + LIMIT 1", + ) + .bind(&governed_names_json) + .bind(crate::migrations::EVENT_STORE_RESERVED_PREFIX) + .bind(crate::migrations::EVENT_STORE_RESERVED_PREFIX) + .bind(crate::migrations::EVENT_STORE_RESERVED_PREFIX) + .bind(crate::migrations::EVENT_STORE_RESERVED_PREFIX) + .fetch_optional(&mut *connection) + .await?; + if let Some(row) = collision { + return Err(RadrootsEventStoreError::TemporarySchemaCollision { + object_type: row.try_get("type")?, + name: row.try_get("name")?, + table_name: row.try_get("tbl_name")?, + }); + } + Ok(()) +} + async fn validate_event_store_temp_schema_with_registry( connection: &mut SqliteConnection, registry: &[EventStoreMigration], @@ -896,6 +1046,38 @@ async fn read_history( .collect() } +async fn read_repair_history_bounded_v1( + connection: &mut SqliteConnection, + supported_current: u32, +) -> Result<Vec<AppliedMigration>, RadrootsEventStoreError> { + let row_limit = i64::from(supported_current).checked_add(1).ok_or_else(|| { + RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + detail: "managed migration-history authority cannot reserve a drift row".to_owned(), + } + })?; + let rows = sqlx::query( + "SELECT version, name, up_sha256, down_sha256, schema_sha256 + FROM main.radroots_event_store_schema_migrations + ORDER BY version + LIMIT ?", + ) + .bind(row_limit) + .fetch_all(&mut *connection) + .await?; + rows.into_iter() + .map(|row| { + Ok(AppliedMigration { + version: row.try_get("version")?, + name: row.try_get("name")?, + up_sha256: row.try_get("up_sha256")?, + down_sha256: row.try_get("down_sha256")?, + schema_sha256: row.try_get("schema_sha256")?, + }) + }) + .collect() +} + fn validate_history_against_registry( history: &[AppliedMigration], registry: &[EventStoreMigration], diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs @@ -9,6 +9,8 @@ mod post_core_storage_v1; mod post_core_storage_v2; mod protocol_reconciliation_v1; mod protocol_storage_v1; +#[cfg(test)] +mod raw_source_rebuild_v1_tests; use self::current_visibility_v1::current_visibility_in_transaction; use self::post_core_extension_capabilities::PostCoreExtensionCapabilities; @@ -37,14 +39,15 @@ use self::protocol_storage_v1::{raw_head_snapshot_in_transaction, stored_raw_eve use crate::RadrootsEventStoreError; use crate::model::{ RadrootsCurrentVisibilityDecisionV1, RadrootsEventIngest, RadrootsEventIngestReceipt, - RadrootsEventStoreSourceGeneration, RadrootsEventStoreStatusSummary, RadrootsEventVisibility, - RadrootsProjectionCursor, RadrootsProjectionRebuildPrior, RadrootsProjectionRebuildTicket, - RadrootsStoredEventTag, RadrootsStoredRawEvent, RadrootsStoredRawEventHead, - RadrootsStoredSellerReservation, RadrootsStoredSellerReservationLine, - RadrootsStoredTradeMissingParent, RadrootsStoredTradeMutation, - RadrootsStoredTradeMutationParent, RadrootsStoredTradeTransportEnvelope, - RadrootsStoredValidEvent, RadrootsStoredVisibleEvent, RadrootsStoredVisibleEventHead, - RadrootsTradeProjectionCheckpoint, RadrootsTransportObservationType, + RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreSourceGeneration, + RadrootsEventStoreStatusSummary, RadrootsEventVisibility, RadrootsProjectionCursor, + RadrootsProjectionRebuildPrior, RadrootsProjectionRebuildTicket, RadrootsStoredEventTag, + RadrootsStoredRawEvent, RadrootsStoredRawEventHead, RadrootsStoredSellerReservation, + RadrootsStoredSellerReservationLine, RadrootsStoredTradeMissingParent, + RadrootsStoredTradeMutation, RadrootsStoredTradeMutationParent, + RadrootsStoredTradeTransportEnvelope, RadrootsStoredValidEvent, RadrootsStoredVisibleEvent, + RadrootsStoredVisibleEventHead, RadrootsTradeProjectionCheckpoint, + RadrootsTransportObservationType, }; #[cfg(test)] use crate::model::{ @@ -53,7 +56,9 @@ use crate::model::{ }; #[cfg(test)] use crate::nip09::reconciliation_v1::ReconciliationProfile; -use crate::nip09::reconciliation_v1::{active_source_generation, generation_from_blob}; +use crate::nip09::reconciliation_v1::{ + active_source_generation, generation_from_blob, preflight_projection_cursor_insert_v1, +}; use crate::schema::{ RadrootsEventStoreSchemaStatus, inspect_event_store_schema_status, migrate_event_store_schema, rollback_event_store_schema_offline, @@ -82,7 +87,7 @@ 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::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; @@ -209,6 +214,64 @@ impl RadrootsEventStore { Ok(capacity) } + /// Rebuilds active product state solely from retained immutable raw rows. + /// + /// Every successful call appends one irreversible retained source + /// generation, up to the governed history limit, and invalidates generic + /// projection cursors by rotating the active generation. Calls at the + /// history limit return + /// [`RadrootsEventStoreError::SourceGenerationHistoryLimitReached`]. + pub async fn rebuild_from_raw_v1( + &self, + ) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError> { + crate::nip09::reconciliation_v1::rebuild_from_raw_v1_on_pool(&self.pool).await + } + + /// Repairs an exact managed-v4 file without exposing its invalid state. + /// + /// The database file must already exist, and the caller must quiesce every + /// store alias, independent pool, and direct SQL user of that file for the + /// duration of repair. The canonical path, symlink targets, and file + /// replacement or rename operations must also remain quiesced. + /// Caller-provided pools are intentionally unavailable; + /// their callbacks and session state cannot be sealed. This path creates a + /// fresh governed pool, never creates or migrates a schema, requires the + /// existing file to use WAL journal mode, and proves its canonical path + /// shares the reserved SQLite writer-lock domain before rebuilding in the + /// same validated transaction. It returns a usable store only after rebuild + /// commit. Every successful call appends one irreversible retained source + /// generation, up to the governed history limit, and invalidates generic + /// projection cursors by rotating the active generation. Calls at the + /// history limit return + /// [`RadrootsEventStoreError::SourceGenerationHistoryLimitReached`]. + pub async fn repair_file_from_raw_v1( + path: impl AsRef<Path>, + ) -> Result<(Self, RadrootsEventStoreRawSourceRebuildReportV1), RadrootsEventStoreError> { + let canonical_path = canonical_raw_source_repair_main_path_v1(path.as_ref())?; + let options = SqliteConnectOptions::new() + .filename(&canonical_path) + .create_if_missing(false); + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(options) + .await?; + pool.set_connect_options(raw_source_repair_connect_options_v1(&canonical_path)); + let mut connection = pool.acquire().await?; + prepare_raw_source_repair_connection_v1(&mut connection, &canonical_path).await?; + let transaction = connection.begin_with("BEGIN IMMEDIATE").await?; + if let Err(primary) = + validate_raw_source_repair_canonical_lock_domain_v1(&canonical_path).await + { + return preserve_raw_source_repair_probe_failure(primary, transaction.rollback().await); + } + let report = crate::nip09::reconciliation_v1::rebuild_from_raw_v1_in_existing_transaction( + transaction, + ) + .await?; + drop(connection); + Ok((Self { pool }, report)) + } + /// Begins a serialized write transaction suitable for composed event-store writes. /// /// Call this before performing any reads that will precede @@ -540,9 +603,13 @@ impl RadrootsEventStore { }, ); } - projection_cursor_unchecked(&mut tx, cursor.projection_id(), active_generation).await?; + let existing = + projection_cursor_unchecked(&mut tx, cursor.projection_id(), active_generation).await?; match expected_prior_sequence { None => { + if existing.is_none() { + preflight_projection_cursor_insert_v1(&mut tx).await?; + } let inserted = sqlx::query( "INSERT OR IGNORE INTO projection_cursor(projection_id, projection_version, last_event_seq, updated_at_ms) VALUES (?, ?, ?, ?)", ) @@ -724,6 +791,7 @@ impl RadrootsEventStore { .await?; match (expected_prior, actual_prior) { (RadrootsProjectionRebuildPrior::Missing, None) => { + preflight_projection_cursor_insert_v1(&mut tx).await?; let inserted = sqlx::query( "INSERT OR IGNORE INTO projection_cursor(projection_id, projection_version, last_event_seq, updated_at_ms) VALUES (?, ?, ?, ?)", ) @@ -1167,7 +1235,6 @@ async fn configure_pool( file_backed: bool, ) -> Result<(), RadrootsEventStoreError> { let max_connections = pool.options().get_max_connections(); - let existing_options = pool.connect_options(); if !file_backed && max_connections != 1 { return Err(RadrootsEventStoreError::UnsafeInMemoryPoolConnectionCount { actual: max_connections, @@ -1189,19 +1256,6 @@ async fn configure_pool( } validate_main_database_encoding(connection).await?; crate::schema::validate_event_store_temp_schema(connection).await?; - } - - let mut connect_options = existing_options - .as_ref() - .clone() - .foreign_keys(true) - .busy_timeout(Duration::from_millis(5_000)); - if file_backed { - connect_options = connect_options.journal_mode(SqliteJournalMode::Wal); - } - pool.set_connect_options(connect_options); - - for connection in &mut connections { sqlx::query("PRAGMA foreign_keys = ON") .execute(&mut **connection) .await?; @@ -1212,9 +1266,133 @@ async fn configure_pool( configure_file_journal_mode(connection).await?; } } + let existing_options = pool.connect_options(); + let connect_options = existing_options + .as_ref() + .clone() + .foreign_keys(true) + .busy_timeout(Duration::from_millis(5_000)); + let connect_options = if file_backed { + connect_options.journal_mode(SqliteJournalMode::Wal) + } else { + connect_options + }; + pool.set_connect_options(connect_options); + Ok(()) +} + +async fn prepare_raw_source_repair_connection_v1( + connection: &mut SqliteConnection, + canonical_path: &Path, +) -> Result<(), RadrootsEventStoreError> { + let main_filename = main_database_filename(connection).await?; + let actual = canonical_raw_source_repair_main_path_v1(Path::new(&main_filename))?; + if actual != canonical_path { + return Err( + RadrootsEventStoreError::RawSourceRepairDatabaseIdentityMismatch { + expected: canonical_path.display().to_string(), + actual: actual.display().to_string(), + }, + ); + } + validate_main_database_encoding(connection).await?; + crate::schema::validate_exact_managed_v4_for_raw_source_rebuild_v1(connection).await?; + validate_file_journal_mode_is_wal(connection).await?; + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&mut *connection) + .await?; + sqlx::query("PRAGMA busy_timeout = 5000") + .execute(&mut *connection) + .await?; Ok(()) } +fn raw_source_repair_connect_options_v1(canonical_path: &Path) -> SqliteConnectOptions { + SqliteConnectOptions::new() + .filename(canonical_path) + .create_if_missing(false) + .journal_mode(SqliteJournalMode::Wal) + .foreign_keys(true) + .busy_timeout(Duration::from_millis(5_000)) +} + +async fn validate_raw_source_repair_canonical_lock_domain_v1( + canonical_path: &Path, +) -> Result<(), RadrootsEventStoreError> { + let mut candidate = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(canonical_path) + .create_if_missing(false) + .foreign_keys(true) + .busy_timeout(Duration::ZERO), + ) + .await?; + let candidate_filename = main_database_filename(&mut candidate).await?; + let candidate_path = canonical_raw_source_repair_main_path_v1(Path::new(&candidate_filename))?; + if candidate_path != canonical_path { + return Err( + RadrootsEventStoreError::RawSourceRepairDatabaseIdentityMismatch { + expected: canonical_path.display().to_string(), + actual: candidate_path.display().to_string(), + }, + ); + } + validate_main_database_encoding(&mut candidate).await?; + crate::schema::validate_exact_managed_v4_for_raw_source_rebuild_v1(&mut candidate).await?; + validate_file_journal_mode_is_wal(&mut candidate).await?; + + let mut probe = candidate.begin().await?; + let write = sqlx::query( + "UPDATE main.radroots_event_store_write_lock SET lock_version = lock_version WHERE singleton = 1", + ) + .execute(&mut *probe) + .await; + let rollback = probe.rollback().await; + match write { + Ok(_) => preserve_raw_source_repair_probe_failure( + RadrootsEventStoreError::RawSourceRepairCanonicalPathLockDomainMismatch { + canonical_path: canonical_path.display().to_string(), + }, + rollback, + ), + Err(error) => { + if sqlite_error_is_busy_or_locked(&error) { + rollback?; + Ok(()) + } else { + preserve_raw_source_repair_probe_failure(error.into(), rollback) + } + } + } +} + +fn preserve_raw_source_repair_probe_failure<T>( + primary: RadrootsEventStoreError, + rollback: Result<(), sqlx::Error>, +) -> Result<T, RadrootsEventStoreError> { + match rollback { + Ok(()) => Err(primary), + Err(rollback) => Err( + RadrootsEventStoreError::RawSourceRebuildTransactionRollbackFailed { + primary: Box::new(primary), + rollback, + }, + ), + } +} + +fn canonical_raw_source_repair_main_path_v1( + path: &Path, +) -> Result<PathBuf, RadrootsEventStoreError> { + let filename = path.display().to_string(); + std::fs::canonicalize(path).map_err(|source| { + RadrootsEventStoreError::RawSourceRepairMainDatabaseCanonicalizationFailed { + filename, + source, + } + }) +} + async fn validate_main_database_encoding( connection: &mut SqliteConnection, ) -> Result<(), RadrootsEventStoreError> { @@ -1253,6 +1431,18 @@ async fn configure_file_journal_mode( } } +async fn validate_file_journal_mode_is_wal( + connection: &mut SqliteConnection, +) -> Result<(), RadrootsEventStoreError> { + let actual: String = sqlx::query_scalar("PRAGMA main.journal_mode") + .fetch_one(&mut *connection) + .await?; + if actual == "wal" { + return Ok(()); + } + Err(RadrootsEventStoreError::SqliteFileJournalModeNotWal { actual }) +} + fn sqlite_error_is_busy(error: &sqlx::Error) -> bool { let sqlx::Error::Database(error) = error else { return false; @@ -1263,6 +1453,16 @@ fn sqlite_error_is_busy(error: &sqlx::Error) -> bool { .is_some_and(|code| code & 0xff == 5) } +fn sqlite_error_is_busy_or_locked(error: &sqlx::Error) -> bool { + let sqlx::Error::Database(error) = error else { + return false; + }; + error + .code() + .and_then(|code| code.parse::<i32>().ok()) + .is_some_and(|code| code & 0xff == 5 || code & 0xff == 6) +} + async fn main_database_filename( connection: &mut SqliteConnection, ) -> Result<String, RadrootsEventStoreError> { diff --git a/crates/event_store/src/store/food_availability_projection_v1.rs b/crates/event_store/src/store/food_availability_projection_v1.rs @@ -2,7 +2,6 @@ use super::addressable_transition_feed_v1::addressable_transition_page_in_transa 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, @@ -16,6 +15,7 @@ use crate::model::{ use crate::nip09::reconciliation_v1::{ EventAdmission, ReconciliationProfile, generation_from_blob, }; +use crate::{RadrootsEventStoreError, RadrootsEventStoreRawSourceRebuildDriftV1}; use radroots_event::food_availability::RadrootsFoodIdentifier; use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey}; use radroots_event_codec::food_availability::inbound::{ @@ -138,6 +138,46 @@ pub(crate) async fn apply_food_availability_projection_hook_v1( validate_food_availability_projection_hook_v1(connection).await } +pub(crate) async fn reset_and_replay_food_availability_from_raw_v1( + connection: &mut SqliteConnection, + active_generation: RadrootsEventStoreSourceGeneration, +) -> Result<(), RadrootsEventStoreError> { + sqlx::query( + "DELETE FROM radroots_event_store_food_availability_image WHERE source_generation != ?", + ) + .bind(active_generation.as_bytes().as_slice()) + .execute(&mut *connection) + .await?; + sqlx::query( + "DELETE FROM radroots_event_store_food_availability_projection WHERE source_generation != ?", + ) + .bind(active_generation.as_bytes().as_slice()) + .execute(&mut *connection) + .await?; + sqlx::query("DELETE FROM radroots_event_store_food_availability_search_fts") + .execute(&mut *connection) + .await?; + sqlx::query( + "DELETE FROM radroots_event_store_food_availability_cursor WHERE source_generation != ?", + ) + .bind(active_generation.as_bytes().as_slice()) + .execute(&mut *connection) + .await?; + + let residual_count: i64 = sqlx::query_scalar( + "SELECT (SELECT COUNT(*) FROM radroots_event_store_food_availability_image) + (SELECT COUNT(*) FROM radroots_event_store_food_availability_projection) + (SELECT COUNT(*) FROM radroots_event_store_food_availability_cursor) + (SELECT COUNT(*) FROM radroots_event_store_food_availability_search_fts)", + ) + .fetch_one(&mut *connection) + .await?; + if residual_count != 0 { + return Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::DerivedProductStateAuthority, + detail: format!("FoodAvailability reset left {residual_count} stale derived row(s)"), + }); + } + apply_pending_food_availability_transitions_v1(connection).await +} + pub(crate) async fn apply_pending_food_availability_transitions_v1( connection: &mut SqliteConnection, ) -> Result<(), RadrootsEventStoreError> { diff --git a/crates/event_store/src/store/raw_source_rebuild_v1_tests.rs b/crates/event_store/src/store/raw_source_rebuild_v1_tests.rs @@ -0,0 +1,2534 @@ +use super::RadrootsEventStore; +use crate::model::{RadrootsEventIngest, RadrootsProjectionCursor}; +use crate::nip09::reconciliation_v1::{ + RawSourceRebuildFailpointV1, SourceGenerationProvider, + preserve_raw_source_rebuild_primary_failure_for_test, + rebuild_from_raw_v1_in_transaction_for_test, rebuild_from_raw_v1_on_pool_for_test, + rebuild_from_raw_v1_on_pool_with_caller_schema_limits_for_test, +}; +use crate::schema::rollback_event_store_schema_offline_destructive_for_migration_test; +use crate::{ + RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1, + RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1, RadrootsEventStoreError, + RadrootsEventStoreRawSourceRebuildDriftV1, +}; +use serde_json::Value; +use sqlx::Connection; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use sqlx::{SqliteConnection, SqlitePool}; +use std::path::Path; + +const FOOD_FIXTURE: &[u8] = + include_bytes!("../../tests/fixtures/food_availability_projection.v1.json"); +const NIP09_FIXTURE: &[u8] = include_bytes!("../../tests/fixtures/nip09_reconciliation.v1.json"); +const TRANSITION_SEQUENCE_NAME: &str = "radroots_event_store_addressable_head_transition"; + +struct FixedGeneration(u8); + +impl SourceGenerationProvider for FixedGeneration { + fn fill_generation(&self, generation: &mut [u8; 32]) -> Result<(), RadrootsEventStoreError> { + generation.fill(self.0); + Ok(()) + } +} + +struct PanickingGeneration; + +impl SourceGenerationProvider for PanickingGeneration { + fn fill_generation(&self, _generation: &mut [u8; 32]) -> Result<(), RadrootsEventStoreError> { + panic!("generation entropy was requested after the retained-history preflight") + } +} + +struct FailingGeneration; + +impl SourceGenerationProvider for FailingGeneration { + fn fill_generation(&self, _generation: &mut [u8; 32]) -> Result<(), RadrootsEventStoreError> { + Err(RadrootsEventStoreError::SourceGenerationEntropyUnavailable) + } +} + +#[derive(Debug, PartialEq, Eq)] +struct RebuildAuthoritySnapshot { + source_state: Vec<String>, + source_capacity: Vec<String>, + migration_history: Vec<String>, + commit_barrier: Vec<String>, + write_lock: Vec<String>, + feed_integrity: Vec<String>, + generations: Vec<String>, + markers: Vec<String>, + envelopes: Vec<String>, + tags: Vec<String>, + raw_heads: Vec<String>, + coordinates: Vec<String>, + nip09_requests: Vec<String>, + nip09_event_targets: Vec<String>, + nip09_address_targets: Vec<String>, + addressable_heads: Vec<String>, + transitions: Vec<String>, + food_cursor: Vec<String>, + food_projection: Vec<String>, + food_images: Vec<String>, + food_search: Vec<String>, + sqlite_sequences: Vec<String>, +} + +fn food_fixture_ingest() -> RadrootsEventIngest { + fixture_case_ingests( + FOOD_FIXTURE, + "visible_food_availability_projects_and_searches", + "events", + ) + .into_iter() + .next() + .expect("Food fixture event") +} + +fn fixture_case_ingests( + bytes: &[u8], + case_id: &str, + events_field: &str, +) -> Vec<RadrootsEventIngest> { + let fixture: Value = serde_json::from_slice(bytes).expect("parse event fixture"); + let case = fixture["cases"] + .as_array() + .expect("fixture cases") + .iter() + .find(|case| case["id"].as_str() == Some(case_id)) + .unwrap_or_else(|| panic!("missing fixture case {case_id}")); + case[events_field] + .as_array() + .expect("fixture events") + .iter() + .map(|observed| { + let raw_json = + serde_json::to_string(&observed["event"]).expect("serialize fixture event"); + let observed_at_ms = observed["observed_at_ms"] + .as_i64() + .expect("fixture observed_at_ms"); + RadrootsEventIngest::from_raw_json(raw_json, observed_at_ms) + .expect("verify fixture event") + }) + .collect() +} + +async fn seed_food_fixture(store: &RadrootsEventStore) { + let receipt = store + .ingest_event(food_fixture_ingest()) + .await + .expect("ingest Food fixture"); + assert!(receipt.persistence.is_inserted()); +} + +async fn seed_fixture_case( + store: &RadrootsEventStore, + bytes: &[u8], + case_id: &str, + events_field: &str, +) { + for ingest in fixture_case_ingests(bytes, case_id, events_field) { + store + .ingest_event(ingest) + .await + .unwrap_or_else(|error| panic!("ingest fixture case {case_id}: {error}")); + } +} + +async fn query_string_rows(pool: &SqlitePool, sql: &'static str) -> Vec<String> { + sqlx::query_scalar(sql) + .fetch_all(pool) + .await + .expect("snapshot query") +} + +async fn rebuild_authority_snapshot(store: &RadrootsEventStore) -> RebuildAuthoritySnapshot { + RebuildAuthoritySnapshot { + source_state: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%d|%d|%d', hex(active_generation), raw_event_count, raw_tag_count, raw_high_water_seq, last_transition_seq) FROM radroots_event_store_source_state ORDER BY singleton", + ) + .await, + source_capacity: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%d|%d|%d|%d|%d|%d', hex(source_generation), raw_event_count, raw_tag_count, raw_event_bytes, raw_tag_bytes, raw_high_water_seq, retained_generation_count, retained_generation_limit) FROM radroots_event_store_source_capacity_v1 ORDER BY singleton", + ) + .await, + migration_history: query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s|%s|%s', version, name, up_sha256, down_sha256, schema_sha256) FROM radroots_event_store_schema_migrations ORDER BY version", + ) + .await, + commit_barrier: query_string_rows( + store.pool(), + "SELECT printf('%d', barrier_key) FROM radroots_event_store_source_rebuild_commit_barrier ORDER BY barrier_key", + ) + .await, + write_lock: query_string_rows( + store.pool(), + "SELECT printf('%d|%d', singleton, lock_version) FROM radroots_event_store_write_lock ORDER BY singleton", + ) + .await, + feed_integrity: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%d|%d', hex(source_generation), transition_floor_seq, last_transition_seq, transition_count) FROM radroots_event_store_addressable_feed_integrity_v1 ORDER BY hex(source_generation)", + ) + .await, + generations: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%d|%d|%d|%s|%s|%d|%d|%d|%d', hex(source_generation), generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq) FROM radroots_event_store_source_generation ORDER BY generation_ordinal", + ) + .await, + markers: query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%d', singleton, hex(target_generation), target_generation_ordinal) FROM radroots_event_store_source_rebuild_marker ORDER BY singleton", + ) + .await, + envelopes: query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%d|%d|%d', seq, quote(event_id), quote(pubkey), quote(tags_json), quote(content), quote(sig), quote(raw_json), quote(verification_status), quote(contract_status), quote(contract_id), quote(event_class), quote(created_at), quote(kind), projection_eligible, inserted_at_ms, updated_at_ms) FROM event_envelopes ORDER BY seq", + ) + .await, + tags: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%s|%s|%s|%s|%d', quote(event_id), tag_index, quote(tag_name), quote(tag_value), quote(tag_json), quote(contract_semantic), quote(contract_value_type), relay_indexed) FROM event_envelope_tags ORDER BY event_id, tag_index", + ) + .await, + raw_heads: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%s|%s|%d|%d', coordinate_type, kind, pubkey, quote(d_tag), event_id, created_at, updated_at_ms) FROM event_envelope_head ORDER BY coordinate_type, kind, pubkey, d_tag", + ) + .await, + coordinates: query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%d|%s|%s|%s', hex(source_generation), event_id, event_seq, coordinate_type, admission_status, quote(nip09_d_tag)) FROM radroots_event_store_event_coordinate ORDER BY hex(source_generation), event_id", + ) + .await, + nip09_requests: query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%d|%d', request_event_id, request_pubkey, request_created_at, request_event_seq) FROM radroots_event_store_nip09_request ORDER BY hex(source_generation), request_event_id", + ) + .await, + nip09_event_targets: query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%d|%s', request_event_id, target_event_id, source_tag_index, source_tag_value) FROM radroots_event_store_nip09_event_target ORDER BY hex(source_generation), request_event_id, target_event_id, source_tag_index", + ) + .await, + nip09_address_targets: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%s|%d|%d', request_event_id, target_kind, target_pubkey, target_d_tag, inclusive_cutoff, source_tag_index) FROM radroots_event_store_nip09_address_target ORDER BY hex(source_generation), request_event_id, target_kind, target_pubkey, target_d_tag, source_tag_index", + ) + .await, + addressable_heads: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%s|%s|%s|%s', hex(source_generation), kind, pubkey, d_tag, raw_head_event_id, visibility, quote(nip09_reason)) FROM radroots_event_store_addressable_head_state ORDER BY hex(source_generation), kind, pubkey, d_tag", + ) + .await, + transitions: query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s|%d|%s|%s|%s|%s', transition_seq, hex(source_generation), origin, kind, pubkey, d_tag, raw_head_event_id, visibility) FROM radroots_event_store_addressable_head_transition ORDER BY transition_seq", + ) + .await, + food_cursor: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%d|%s|%s|%d', hex(source_generation), feed_version, projection_version, hex(scope_fingerprint), hook_manifest_sha256, projected_row_count) FROM radroots_event_store_food_availability_cursor ORDER BY singleton", + ) + .await, + food_projection: query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%s|%s|%s|%s|%s', hex(source_generation), kind, pubkey, d_tag, event_id, title, location, status) FROM radroots_event_store_food_availability_projection ORDER BY pubkey, d_tag", + ) + .await, + food_images: query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%d|%s|%d', hex(source_generation), pubkey, d_tag, image_index, quote(url), qualifies) FROM radroots_event_store_food_availability_image ORDER BY pubkey, d_tag, image_index", + ) + .await, + food_search: query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s|%s|%s', event_id, pubkey, d_tag, title, summary, content, location) FROM radroots_event_store_food_availability_search_fts ORDER BY event_id", + ) + .await, + sqlite_sequences: query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s', rowid, quote(name), quote(seq)) FROM main.sqlite_sequence ORDER BY rowid", + ) + .await, + } +} + +async fn cold_file_authority_snapshot(path: &Path) -> (RebuildAuthoritySnapshot, String) { + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with( + SqliteConnectOptions::new() + .filename(path) + .create_if_missing(false), + ) + .await + .expect("open cold snapshot pool"); + let store = RadrootsEventStore { pool }; + let snapshot = rebuild_authority_snapshot(&store).await; + let journal_mode = sqlx::query_scalar("PRAGMA main.journal_mode") + .fetch_one(store.pool()) + .await + .expect("cold snapshot journal mode"); + store.pool().close().await; + (snapshot, journal_mode) +} + +async fn logical_product_snapshot(store: &RadrootsEventStore) -> Vec<Vec<String>> { + vec![ + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%d', event_id, contract_status, quote(contract_id), quote(event_class), projection_eligible) FROM event_envelopes ORDER BY event_id", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%s|%d', event_id, tag_index, quote(contract_semantic), quote(contract_value_type), relay_indexed) FROM event_envelope_tags ORDER BY event_id, tag_index", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%s|%s', coordinate_type, kind, pubkey, quote(d_tag), event_id) FROM event_envelope_head ORDER BY coordinate_type, kind, pubkey, d_tag", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%d|%s|%s|%s|%s|%d|%s', event_id, event_seq, coordinate_type, kind, pubkey, admission_status, quote(admission_code), quote(contract_id), nip09_matchable, quote(nip09_d_tag)) FROM radroots_event_store_event_coordinate WHERE source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1) ORDER BY event_id", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s|%s|%d|%s|%s|%s|%s', kind, pubkey, d_tag, raw_head_event_id, raw_head_created_at, admission_status, quote(admission_code), quote(contract_id), visibility) FROM radroots_event_store_addressable_head_state WHERE source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1) ORDER BY kind, pubkey, d_tag", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s', event_id, admission_status, quote(contract_id), current_visibility, quote(suppression_reason)) FROM radroots_event_store_current_visibility_v1 ORDER BY event_id", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%d', request_event_id, request_pubkey, request_created_at) FROM radroots_event_store_nip09_request WHERE source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1) ORDER BY request_event_id", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%d|%s', request_event_id, target_event_id, source_tag_index, source_tag_value) FROM radroots_event_store_nip09_event_target WHERE source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1) ORDER BY request_event_id, target_event_id, source_tag_index", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%s|%s|%d|%d', request_event_id, target_kind, target_pubkey, target_d_tag, inclusive_cutoff, source_tag_index) FROM radroots_event_store_nip09_address_target WHERE source_generation = (SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1) ORDER BY request_event_id, target_kind, target_pubkey, target_d_tag, source_tag_index", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s|%s|%s|%s|%s', kind, pubkey, d_tag, event_id, title, location, status) FROM radroots_event_store_food_availability_projection ORDER BY pubkey, d_tag", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%d|%s|%s|%s|%s|%d', pubkey, d_tag, image_index, raw_tag_json, quote(url), quote(width), quote(height), qualifies) FROM radroots_event_store_food_availability_image ORDER BY pubkey, d_tag, image_index", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s|%s|%s', event_id, pubkey, d_tag, title, summary, content, location) FROM radroots_event_store_food_availability_search_fts ORDER BY event_id", + ) + .await, + ] +} + +async fn set_trigger_guarded_drift( + store: &RadrootsEventStore, + trigger: &'static str, + mutation: &'static str, +) { + let trigger_sql: String = sqlx::query_scalar( + "SELECT sql FROM main.sqlite_schema WHERE type = 'trigger' AND name = ?", + ) + .bind(trigger) + .fetch_one(store.pool()) + .await + .expect("load guard SQL"); + sqlx::query(sqlx::AssertSqlSafe(format!("DROP TRIGGER main.{trigger}"))) + .execute(store.pool()) + .await + .expect("drop guard"); + sqlx::query(mutation) + .execute(store.pool()) + .await + .expect("forge drift"); + sqlx::query(sqlx::AssertSqlSafe(trigger_sql)) + .execute(store.pool()) + .await + .expect("restore guard"); +} + +async fn transition_high_water(store: &RadrootsEventStore) -> i64 { + sqlx::query_scalar( + "SELECT COALESCE(MAX(transition_seq), 0) FROM radroots_event_store_addressable_head_transition", + ) + .fetch_one(store.pool()) + .await + .expect("transition high-water") +} + +async fn assert_nonempty_transition_high_water_drift_is_repaired( + drift_sql: &'static str, + pristine_generation: u8, + repair_generation: u8, + repeat_generation: u8, +) { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + let pristine = rebuild_from_raw_v1_on_pool_for_test( + store.pool(), + &FixedGeneration(pristine_generation), + None, + ) + .await + .expect("establish pristine rebuild"); + let pristine_product = logical_product_snapshot(&store).await; + let prior_transition_high_water = transition_high_water(&store).await; + assert!(prior_transition_high_water > 0); + + set_trigger_guarded_drift( + &store, + "radroots_event_store_source_state_authority_update_guard", + drift_sql, + ) + .await; + let drifted_high_water: i64 = sqlx::query_scalar( + "SELECT last_transition_seq FROM radroots_event_store_source_state WHERE singleton = 1", + ) + .fetch_one(store.pool()) + .await + .expect("drifted source-state high-water"); + assert_ne!(drifted_high_water, prior_transition_high_water); + + let repaired = rebuild_from_raw_v1_on_pool_for_test( + store.pool(), + &FixedGeneration(repair_generation), + None, + ) + .await + .expect("repair active transition high-water drift"); + assert_eq!( + repaired.immutable_raw_digest(), + pristine.immutable_raw_digest() + ); + assert_eq!( + repaired.active_product_state_digest(), + pristine.active_product_state_digest() + ); + assert_eq!(logical_product_snapshot(&store).await, pristine_product); + + let repaired_generation = repaired.new_source_generation(); + let repaired_floor: i64 = sqlx::query_scalar( + "SELECT transition_floor_seq FROM radroots_event_store_source_generation WHERE source_generation = ?", + ) + .bind(repaired_generation.as_bytes().as_slice()) + .fetch_one(store.pool()) + .await + .expect("repaired generation transition floor"); + assert_eq!(repaired_floor, prior_transition_high_water); + let repaired_state_high_water: i64 = sqlx::query_scalar( + "SELECT last_transition_seq FROM radroots_event_store_source_state WHERE singleton = 1", + ) + .fetch_one(store.pool()) + .await + .expect("repaired source-state high-water"); + assert_eq!( + repaired_state_high_water, + transition_high_water(&store).await + ); + + store + .migrate_to_current_schema() + .await + .expect("ordinary reopen validation after repair"); + let repeated = rebuild_from_raw_v1_on_pool_for_test( + store.pool(), + &FixedGeneration(repeat_generation), + None, + ) + .await + .expect("repeat rebuild after repair"); + assert_eq!( + repeated.immutable_raw_digest(), + repaired.immutable_raw_digest() + ); + assert_eq!( + repeated.active_product_state_digest(), + repaired.active_product_state_digest() + ); +} + +async fn insert_projection_cursor_capacity(store: &RadrootsEventStore) { + sqlx::query( + "WITH digits(n) AS (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15)) INSERT INTO projection_cursor(projection_id, projection_version, last_event_seq, updated_at_ms) SELECT printf('projection-%04d', high.n * 256 + middle.n * 16 + low.n), 1, 0, 1 FROM digits AS high CROSS JOIN digits AS middle CROSS JOIN digits AS low", + ) + .execute(store.pool()) + .await + .expect("fill governed cursor capacity"); +} + +#[tokio::test] +async fn raw_source_rebuild_incremental_reopen_and_repeat_parity_v1() { + let tempdir = tempfile::tempdir().expect("tempdir"); + let path = tempdir.path().join("raw-rebuild-parity.sqlite"); + let store = RadrootsEventStore::open_file(&path) + .await + .expect("open file"); + seed_fixture_case( + &store, + FOOD_FIXTURE, + "post_cutoff_replacement_restores_projection", + "events", + ) + .await; + let incremental = logical_product_snapshot(&store).await; + + let first = rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x21), None) + .await + .expect("first rebuild"); + assert_eq!(logical_product_snapshot(&store).await, incremental); + store.pool().close().await; + + let reopened = RadrootsEventStore::open_file(&path) + .await + .expect("strict reopen"); + assert_eq!(logical_product_snapshot(&reopened).await, incremental); + let second = + rebuild_from_raw_v1_on_pool_for_test(reopened.pool(), &FixedGeneration(0x22), None) + .await + .expect("second rebuild"); + let third = rebuild_from_raw_v1_on_pool_for_test(reopened.pool(), &FixedGeneration(0x23), None) + .await + .expect("repeat rebuild"); + assert_eq!(logical_product_snapshot(&reopened).await, incremental); + assert_eq!(first.immutable_raw_digest(), second.immutable_raw_digest()); + assert_eq!(second.immutable_raw_digest(), third.immutable_raw_digest()); + assert_eq!( + first.active_product_state_digest(), + second.active_product_state_digest() + ); + assert_eq!( + second.active_product_state_digest(), + third.active_product_state_digest() + ); + + for (index, (bytes, case_id, events_field)) in [ + ( + FOOD_FIXTURE, + "invalid_same_timestamp_winner_retracts_projection", + "events", + ), + ( + FOOD_FIXTURE, + "blossom_digest_and_image_diagnostics_are_preserved", + "events", + ), + ( + FOOD_FIXTURE, + "wrong_author_address_deletion_preserves_projection", + "events", + ), + ( + FOOD_FIXTURE, + "operational_listing_head_retracts_food_projection", + "events", + ), + ( + NIP09_FIXTURE, + "maximum_address_cutoff_is_order_independent", + "input_events", + ), + ( + NIP09_FIXTURE, + "later_revision_survives_maximum_address_cutoff", + "input_events", + ), + ( + NIP09_FIXTURE, + "unauthorized_exact_reference_does_not_override_authorized_stale_cutoff", + "input_events", + ), + ] + .into_iter() + .enumerate() + { + let case_store = RadrootsEventStore::open_memory() + .await + .expect("open case store"); + seed_fixture_case(&case_store, bytes, case_id, events_field).await; + let incremental = logical_product_snapshot(&case_store).await; + let generation = u8::try_from(0x80 + index).expect("fixture generation"); + let rebuilt = rebuild_from_raw_v1_on_pool_for_test( + case_store.pool(), + &FixedGeneration(generation), + None, + ) + .await + .unwrap_or_else(|error| panic!("rebuild fixture case {case_id}: {error}")); + assert_eq!( + logical_product_snapshot(&case_store).await, + incremental, + "fixture case {case_id}" + ); + let repeated = rebuild_from_raw_v1_on_pool_for_test( + case_store.pool(), + &FixedGeneration(generation + 0x10), + None, + ) + .await + .unwrap_or_else(|error| panic!("repeat fixture case {case_id}: {error}")); + assert_eq!( + rebuilt.immutable_raw_digest(), + repeated.immutable_raw_digest(), + "fixture case {case_id} raw digest" + ); + assert_eq!( + rebuilt.active_product_state_digest(), + repeated.active_product_state_digest(), + "fixture case {case_id} product digest" + ); + let deletion_count: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM event_envelopes WHERE kind = 5") + .fetch_one(case_store.pool()) + .await + .expect("deletion count"); + let visible_deletion_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM radroots_event_store_current_visibility_v1 AS visibility JOIN event_envelopes AS event USING (event_id) WHERE event.kind = 5 AND visibility.current_visibility = 'visible'", + ) + .fetch_one(case_store.pool()) + .await + .expect("visible deletion count"); + assert_eq!(visible_deletion_count, deletion_count, "kind-5 immunity"); + } +} + +#[tokio::test] +async fn projection_cursor_capacity_accepts_exact_and_rejects_one_over_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + rollback_event_store_schema_offline_destructive_for_migration_test(store.pool(), 1) + .await + .expect("rollback exact-cap store to v1"); + insert_projection_cursor_capacity(&store).await; + assert_eq!( + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM projection_cursor") + .fetch_one(store.pool()) + .await + .expect("cursor count"), + i64::from(RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1) + ); + store + .migrate_to_current_schema() + .await + .expect("v1 migration accepts exact cursor capacity"); + + let generation = store.source_generation().await.expect("generation"); + let existing_ticket = store + .prepare_projection_cursor_rebuild("projection-0000", 1) + .await + .expect("prepare existing cursor binding at exact capacity"); + store + .reset_projection_cursor_after_rebuild(existing_ticket, 2) + .await + .expect("bind existing cursor at exact capacity"); + let existing = RadrootsProjectionCursor::new("projection-0000", 1, generation, 0, 3) + .expect("existing cursor update"); + store + .compare_and_swap_projection_cursor(&existing, Some(0)) + .await + .expect("existing cursor remains updateable at exact capacity"); + assert_eq!( + store + .projection_cursor("projection-0000", 1) + .await + .expect("read existing cursor") + .expect("existing cursor") + .updated_at_ms(), + 3 + ); + let overflow = RadrootsProjectionCursor::new("projection-overflow", 1, generation, 0, 2) + .expect("overflow cursor"); + assert!(matches!( + store + .compare_and_swap_projection_cursor(&overflow, None) + .await, + Err(RadrootsEventStoreError::ProjectionCursorCapacityExceeded { current, limit }) + if current == limit && limit == RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1 + )); + let ticket = store + .prepare_projection_cursor_rebuild("projection-reset-overflow", 1) + .await + .expect("missing cursor ticket"); + assert!(matches!( + store.reset_projection_cursor_after_rebuild(ticket, 3).await, + Err(RadrootsEventStoreError::ProjectionCursorCapacityExceeded { current, limit }) + if current == limit && limit == RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1 + )); + + let one_over = RadrootsEventStore::open_memory() + .await + .expect("open one-over store"); + rollback_event_store_schema_offline_destructive_for_migration_test(one_over.pool(), 1) + .await + .expect("rollback one-over store to v1"); + insert_projection_cursor_capacity(&one_over).await; + sqlx::query( + "INSERT INTO projection_cursor(projection_id, projection_version, last_event_seq, updated_at_ms) VALUES ('projection-direct-overflow', 1, 0, 4)", + ) + .execute(one_over.pool()) + .await + .expect("forge one-over cursor inventory"); + assert!(matches!( + one_over.migrate_to_current_schema().await, + Err(RadrootsEventStoreError::ProjectionCursorCapacityExceeded { current, limit }) + if current == limit + 1 + && limit == RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1 + )); +} + +#[tokio::test] +async fn raw_source_rebuild_invalidates_generic_cursors_without_enumerating_or_mutating_them_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + let generation = store.source_generation().await.expect("generation"); + let cursor = + RadrootsProjectionCursor::new("generic", 1, generation, 0, 10).expect("generic cursor"); + store + .compare_and_swap_projection_cursor(&cursor, None) + .await + .expect("insert cursor"); + let before = query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%d|%d|%s|%d', cursor.projection_id, cursor.projection_version, cursor.last_event_seq, cursor.updated_at_ms, hex(source.source_generation), source.source_revision) FROM projection_cursor AS cursor JOIN radroots_event_store_projection_cursor_source AS source USING (projection_id) ORDER BY cursor.projection_id", + ) + .await; + + store.rebuild_from_raw_v1().await.expect("rebuild"); + let after = query_string_rows( + store.pool(), + "SELECT printf('%s|%d|%d|%d|%s|%d', cursor.projection_id, cursor.projection_version, cursor.last_event_seq, cursor.updated_at_ms, hex(source.source_generation), source.source_revision) FROM projection_cursor AS cursor JOIN radroots_event_store_projection_cursor_source AS source USING (projection_id) ORDER BY cursor.projection_id", + ) + .await; + assert_eq!(after, before); + assert!(matches!( + store.projection_cursor("generic", 1).await, + Err(RadrootsEventStoreError::ProjectionSourceGenerationMismatch { projection_id }) + if projection_id == "generic" + )); +} + +#[tokio::test] +async fn raw_source_rebuild_normalizes_only_transition_sqlite_sequence_v1() { + for (index, corruption) in ["missing", "low", "high", "duplicate", "case_alias"] + .into_iter() + .enumerate() + { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + for caller_index in 0..64 { + let create = format!( + "CREATE TABLE caller_autoincrement_{caller_index}(id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL)" + ); + sqlx::query(sqlx::AssertSqlSafe(create)) + .execute(store.pool()) + .await + .expect("caller table"); + let insert = format!( + "INSERT INTO caller_autoincrement_{caller_index}(value) VALUES ('preserve')" + ); + sqlx::query(sqlx::AssertSqlSafe(insert)) + .execute(store.pool()) + .await + .expect("caller row"); + } + match corruption { + "missing" => { + sqlx::query("DELETE FROM main.sqlite_sequence WHERE name = ?") + .bind(TRANSITION_SEQUENCE_NAME) + .execute(store.pool()) + .await + .expect("remove target sequence"); + } + "low" => { + sqlx::query("UPDATE main.sqlite_sequence SET seq = 0 WHERE name = ?") + .bind(TRANSITION_SEQUENCE_NAME) + .execute(store.pool()) + .await + .expect("lower target sequence"); + } + "high" => { + sqlx::query("UPDATE main.sqlite_sequence SET seq = 99 WHERE name = ?") + .bind(TRANSITION_SEQUENCE_NAME) + .execute(store.pool()) + .await + .expect("raise target sequence"); + } + "duplicate" => { + sqlx::query("INSERT INTO main.sqlite_sequence(name, seq) VALUES (?, 99)") + .bind(TRANSITION_SEQUENCE_NAME) + .execute(store.pool()) + .await + .expect("duplicate target sequence"); + } + "case_alias" => { + sqlx::query("UPDATE main.sqlite_sequence SET name = upper(name) WHERE name = ?") + .bind(TRANSITION_SEQUENCE_NAME) + .execute(store.pool()) + .await + .expect("retarget sequence name casing"); + } + _ => unreachable!("closed sequence corruption matrix"), + } + let unrelated_before = query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s', rowid, quote(name), quote(seq)) FROM main.sqlite_sequence WHERE name IS NULL OR name COLLATE NOCASE != 'radroots_event_store_addressable_head_transition' ORDER BY rowid", + ) + .await; + + rebuild_from_raw_v1_on_pool_for_test( + store.pool(), + &FixedGeneration(u8::try_from(0x31 + index).expect("test generation")), + None, + ) + .await + .unwrap_or_else(|error| panic!("rebuild {corruption} sequence: {error}")); + let target_sequences: Vec<(i64, String, Option<i64>)> = sqlx::query_as( + "SELECT rowid, name, seq FROM main.sqlite_sequence WHERE name COLLATE NOCASE = ? ORDER BY rowid", + ) + .bind(TRANSITION_SEQUENCE_NAME) + .fetch_all(store.pool()) + .await + .expect("target sequence rows"); + assert_eq!(target_sequences.len(), 1, "{corruption}"); + let target = &target_sequences[0]; + assert_eq!(target.1, TRANSITION_SEQUENCE_NAME, "{corruption}"); + assert_eq!(target.2, Some(2), "{corruption}"); + assert_eq!( + sqlx::query_as::<_, (i64, String)>( + "SELECT rowid, name FROM main.sqlite_sequence ORDER BY rowid LIMIT 1", + ) + .fetch_one(store.pool()) + .await + .expect("first sequence row"), + (target.0, TRANSITION_SEQUENCE_NAME.to_owned()), + "{corruption}" + ); + let unrelated_after = query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s', rowid, quote(name), quote(seq)) FROM main.sqlite_sequence WHERE name IS NULL OR name COLLATE NOCASE != 'radroots_event_store_addressable_head_transition' ORDER BY rowid", + ) + .await; + assert_eq!(unrelated_after, unrelated_before, "{corruption}"); + } +} + +#[tokio::test] +async fn raw_source_rebuild_rejects_unrelated_minimum_transition_sequence_rowid_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + sqlx::query("INSERT INTO main.sqlite_sequence(rowid, name, seq) VALUES (?, ?, 0)") + .bind(i64::MIN) + .bind("caller_minimum_sequence") + .execute(store.pool()) + .await + .expect("occupy reserved sequence rowid"); + let before = rebuild_authority_snapshot(&store).await; + + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x3f), None,).await, + Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + .. + }) + )); + assert_eq!(rebuild_authority_snapshot(&store).await, before); +} + +#[tokio::test] +async fn raw_source_rebuild_reuses_target_alias_at_minimum_sequence_rowid_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + sqlx::query("DELETE FROM main.sqlite_sequence WHERE name COLLATE NOCASE = ?") + .bind(TRANSITION_SEQUENCE_NAME) + .execute(store.pool()) + .await + .expect("remove canonical target row"); + sqlx::query("INSERT INTO main.sqlite_sequence(rowid, name, seq) VALUES (?, upper(?), 99)") + .bind(i64::MIN) + .bind(TRANSITION_SEQUENCE_NAME) + .execute(store.pool()) + .await + .expect("insert minimum target alias"); + + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x40), None) + .await + .expect("rebuild target alias"); + assert_eq!( + sqlx::query_as::<_, (i64, String, i64)>( + "SELECT rowid, name, seq FROM main.sqlite_sequence ORDER BY rowid LIMIT 1", + ) + .fetch_one(store.pool()) + .await + .expect("canonical minimum target row"), + (i64::MIN, TRANSITION_SEQUENCE_NAME.to_owned(), 2) + ); +} + +#[tokio::test] +async fn raw_source_rebuild_repairs_derived_drift_and_refuses_raw_drift_atomically_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_fixture_case( + &store, + FOOD_FIXTURE, + "post_cutoff_replacement_restores_projection", + "events", + ) + .await; + let pristine = logical_product_snapshot(&store).await; + set_trigger_guarded_drift( + &store, + "radroots_event_store_event_envelopes_derived_update_guard", + "UPDATE event_envelopes SET contract_status = 'invalid', contract_id = NULL, event_class = NULL, projection_eligible = 0", + ) + .await; + set_trigger_guarded_drift( + &store, + "radroots_event_store_nip09_request_update_guard", + "UPDATE radroots_event_store_nip09_request SET request_created_at = request_created_at + 1", + ) + .await; + set_trigger_guarded_drift( + &store, + "radroots_event_store_food_availability_cursor_update_guard", + "UPDATE radroots_event_store_food_availability_cursor SET projected_row_count = 0", + ) + .await; + set_trigger_guarded_drift( + &store, + "radroots_event_store_food_availability_projection_delete_guard", + "DELETE FROM radroots_event_store_food_availability_projection", + ) + .await; + set_trigger_guarded_drift( + &store, + "radroots_event_store_addressable_state_delete_guard", + "DELETE FROM radroots_event_store_addressable_head_state", + ) + .await; + set_trigger_guarded_drift( + &store, + "radroots_event_store_event_head_delete_guard", + "DELETE FROM event_envelope_head", + ) + .await; + sqlx::query("DELETE FROM radroots_event_store_food_availability_search_fts") + .execute(store.pool()) + .await + .expect("forge Food search drift"); + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x32), None) + .await + .expect("repair derived drift"); + assert_eq!(logical_product_snapshot(&store).await, pristine); + + set_trigger_guarded_drift( + &store, + "radroots_event_store_event_envelopes_raw_update_guard", + "UPDATE event_envelopes SET content = 'Parsnip available this week.' WHERE content = 'Carrots available this week.'", + ) + .await; + let before = rebuild_authority_snapshot(&store).await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x33), None).await, + Err(RadrootsEventStoreError::RawEventReconciliationMismatch { field, .. }) + if field == "content" + )); + assert_eq!(rebuild_authority_snapshot(&store).await, before); + + let capacity_store = RadrootsEventStore::open_memory() + .await + .expect("open capacity-drift store"); + seed_food_fixture(&capacity_store).await; + set_trigger_guarded_drift( + &capacity_store, + "radroots_event_store_source_capacity_update_guard", + "UPDATE radroots_event_store_source_capacity_v1 SET raw_event_count = raw_event_count + 1", + ) + .await; + let capacity_before = rebuild_authority_snapshot(&capacity_store).await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(capacity_store.pool(), &FixedGeneration(0x34), None,) + .await, + Err(RadrootsEventStoreError::SourceCapacityStateDrift { reason }) + if reason == "capacity seal does not match active source state and generation history" + )); + assert_eq!( + rebuild_authority_snapshot(&capacity_store).await, + capacity_before + ); + + let catalog_store = RadrootsEventStore::open_memory() + .await + .expect("open catalog-drift store"); + seed_food_fixture(&catalog_store).await; + sqlx::query("DROP INDEX event_envelope_kind_created_idx") + .execute(catalog_store.pool()) + .await + .expect("forge governed catalog drift"); + let catalog_before = rebuild_authority_snapshot(&catalog_store).await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(catalog_store.pool(), &FixedGeneration(0x35), None,) + .await, + Err(RadrootsEventStoreError::SchemaFingerprintMismatch { .. }) + )); + assert_eq!( + rebuild_authority_snapshot(&catalog_store).await, + catalog_before + ); + + let ledger_store = RadrootsEventStore::open_memory() + .await + .expect("open ledger-drift store"); + seed_food_fixture(&ledger_store).await; + sqlx::query( + "UPDATE radroots_event_store_schema_migrations SET up_sha256 = ? WHERE version = 4", + ) + .bind("0".repeat(64)) + .execute(ledger_store.pool()) + .await + .expect("forge migration-ledger drift"); + let ledger_before = rebuild_authority_snapshot(&ledger_store).await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(ledger_store.pool(), &FixedGeneration(0x36), None,) + .await, + Err(RadrootsEventStoreError::MigrationHistoryChecksumDrift { + version: 4, + field: "up_sha256", + .. + }) + )); + assert_eq!( + rebuild_authority_snapshot(&ledger_store).await, + ledger_before + ); +} + +#[tokio::test] +async fn raw_source_rebuild_failpoints_roll_back_every_stage_v1() { + let tempdir = tempfile::tempdir().expect("tempdir"); + for (index, failpoint) in [ + RawSourceRebuildFailpointV1::AfterMarkerOpen, + RawSourceRebuildFailpointV1::AfterGenerationRotation, + RawSourceRebuildFailpointV1::AfterCoreReplay, + RawSourceRebuildFailpointV1::AfterVisibilityAudit, + RawSourceRebuildFailpointV1::AfterFoodResetAndReplay, + RawSourceRebuildFailpointV1::AfterFoodAudit, + RawSourceRebuildFailpointV1::AfterMarkerClose, + ] + .into_iter() + .enumerate() + { + let path = tempdir.path().join(format!("failpoint-{index}.sqlite")); + let store = RadrootsEventStore::open_file(&path).await.expect("open"); + seed_fixture_case( + &store, + FOOD_FIXTURE, + "authorized_address_deletion_retracts_projection", + "events", + ) + .await; + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_event_store_nip09_request", + ) + .fetch_one(store.pool()) + .await + .expect("NIP-09 request count"), + 1 + ); + let before = rebuild_authority_snapshot(&store).await; + let error = rebuild_from_raw_v1_on_pool_for_test( + store.pool(), + &FixedGeneration(0x41), + Some(failpoint), + ) + .await + .expect_err("injected rebuild must fail"); + assert!(matches!( + error, + RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::RebuildPostcondition, + .. + } + )); + assert_eq!(rebuild_authority_snapshot(&store).await, before); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_event_store_source_rebuild_marker", + ) + .fetch_one(store.pool()) + .await + .expect("marker count"), + 0 + ); + store.pool().close().await; + let reopened = RadrootsEventStore::open_file(&path) + .await + .expect("strict reopen after rollback"); + assert_eq!(rebuild_authority_snapshot(&reopened).await, before); + } +} + +#[test] +fn raw_source_rebuild_rollback_failure_preserves_primary_and_rollback_errors_v1() { + let primary = RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::RebuildPostcondition, + detail: "primary".to_owned(), + }; + let rollback = sqlx::Error::Protocol("rollback".to_owned()); + assert!(matches!( + preserve_raw_source_rebuild_primary_failure_for_test::<()>(primary, Err(rollback)), + Err(RadrootsEventStoreError::RawSourceRebuildTransactionRollbackFailed { + primary, + rollback: sqlx::Error::Protocol(_), + }) if matches!( + *primary, + RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::RebuildPostcondition, + .. + } + ) + )); +} + +#[test] +fn raw_source_rebuild_drift_kind_codes_and_display_are_stable_v1() { + for (kind, expected) in [ + ( + RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + "managed_schema_authority", + ), + ( + RadrootsEventStoreRawSourceRebuildDriftV1::ImmutableRawAuthority, + "immutable_raw_authority", + ), + ( + RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + "source_generation_lineage", + ), + ( + RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + "addressable_transition_authority", + ), + ( + RadrootsEventStoreRawSourceRebuildDriftV1::DerivedProductStateAuthority, + "derived_product_state_authority", + ), + ( + RadrootsEventStoreRawSourceRebuildDriftV1::RebuildPostcondition, + "rebuild_postcondition", + ), + ] { + assert_eq!(kind.code(), expected); + assert_eq!(kind.to_string(), expected); + } + + let error = RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::ImmutableRawAuthority, + detail: "diagnostic context".to_owned(), + }; + assert_eq!( + error.to_string(), + "event-store raw-source rebuild authority is inconsistent (immutable_raw_authority): diagnostic context" + ); +} + +#[tokio::test] +async fn raw_source_rebuild_wal_readers_observe_only_committed_generation_v1() { + let tempdir = tempfile::tempdir().expect("tempdir"); + let path = tempdir.path().join("raw-rebuild-wal.sqlite"); + let writer = RadrootsEventStore::open_file(&path).await.expect("writer"); + seed_food_fixture(&writer).await; + let reader = RadrootsEventStore::open_file(&path).await.expect("reader"); + assert_eq!(writer.pragma_journal_mode().await.expect("WAL"), "wal"); + let prior_generation = reader.source_generation().await.expect("prior generation"); + + let mut transaction = writer.begin_write_transaction().await.expect("writer tx"); + let report = + rebuild_from_raw_v1_in_transaction_for_test(&mut transaction, &FixedGeneration(0x51)) + .await + .expect("uncommitted rebuild"); + assert_eq!( + reader.source_generation().await.expect("reader snapshot"), + prior_generation + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_event_store_source_rebuild_marker", + ) + .fetch_one(reader.pool()) + .await + .expect("reader marker count"), + 0 + ); + transaction.commit().await.expect("commit rebuild"); + assert_eq!( + reader + .source_generation() + .await + .expect("committed generation"), + report.new_source_generation() + ); +} + +#[tokio::test] +async fn raw_source_rebuild_rejects_caller_inbound_foreign_keys_atomically_v1() { + for (suffix, on_delete) in [ + ("cascade", "CASCADE"), + ("set_null", "SET NULL"), + ("set_default", "SET DEFAULT"), + ("restrict", "RESTRICT"), + ("no_action", "NO ACTION"), + ] { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + let child_table = format!("caller_inbound_{suffix}"); + sqlx::query( + "CREATE TABLE caller_rebuild_side_effect(id INTEGER PRIMARY KEY AUTOINCREMENT, action TEXT NOT NULL)", + ) + .execute(store.pool()) + .await + .expect("caller side-effect table"); + let parent_table = if suffix == "cascade" { + "RADROOTS_EVENT_STORE_FOOD_AVAILABILITY_CURSOR" + } else { + "radroots_event_store_food_availability_cursor" + }; + let create_child = format!( + "CREATE TABLE {child_table}(id INTEGER PRIMARY KEY, parent_singleton INTEGER DEFAULT 1 REFERENCES {parent_table}(singleton) ON DELETE {on_delete}, note TEXT NOT NULL)" + ); + sqlx::query(sqlx::AssertSqlSafe(create_child)) + .execute(store.pool()) + .await + .expect("caller inbound-FK table"); + for (trigger_suffix, trigger_event) in [ + ("delete", "AFTER DELETE"), + ("update", "AFTER UPDATE OF parent_singleton"), + ] { + let create_trigger = format!( + "CREATE TRIGGER {child_table}_{trigger_suffix}_side_effect {trigger_event} ON {child_table} BEGIN INSERT INTO caller_rebuild_side_effect(action) VALUES ('{trigger_suffix}'); END" + ); + sqlx::query(sqlx::AssertSqlSafe(create_trigger)) + .execute(store.pool()) + .await + .expect("caller child side-effect trigger"); + } + let insert_child = format!( + "INSERT INTO {child_table}(id, parent_singleton, note) VALUES (1, 1, 'preserve')" + ); + sqlx::query(sqlx::AssertSqlSafe(insert_child)) + .execute(store.pool()) + .await + .expect("caller dependent row"); + + if suffix == "cascade" { + let mut connection = store.pool().acquire().await.expect("connection"); + sqlx::query("CREATE TEMP TABLE pragma_foreign_key_list(value TEXT)") + .execute(&mut *connection) + .await + .expect("temporary pragma decoy"); + } + + let authority_before = rebuild_authority_snapshot(&store).await; + let child_before = sqlx::query_scalar::<_, String>(sqlx::AssertSqlSafe(format!( + "SELECT printf('%d|%s|%s', id, quote(parent_singleton), note) FROM {child_table} ORDER BY id" + ))) + .fetch_all(store.pool()) + .await + .expect("caller child snapshot"); + let schema_before = query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s', type, name, tbl_name, sql) FROM main.sqlite_schema WHERE name LIKE 'caller_%' ORDER BY type, name", + ) + .await; + let side_effect_before = query_string_rows( + store.pool(), + "SELECT printf('%d|%s', id, action) FROM caller_rebuild_side_effect ORDER BY id", + ) + .await; + + let error = rebuild_from_raw_v1_on_pool_for_test(store.pool(), &PanickingGeneration, None) + .await + .expect_err("caller inbound FK must be rejected before entropy"); + match error { + RadrootsEventStoreError::RawSourceRebuildCallerInboundForeignKeyUnsupported { + dependency, + } => { + assert_eq!(dependency.child_table, child_table); + assert_eq!(dependency.foreign_key_id, 0); + assert_eq!(dependency.foreign_key_sequence, 0); + assert_eq!(dependency.child_column, "parent_singleton"); + assert_eq!( + dependency.parent_table, + "radroots_event_store_food_availability_cursor" + ); + assert_eq!(dependency.parent_column.as_deref(), Some("singleton")); + assert_eq!(dependency.on_update, "NO ACTION"); + assert_eq!(dependency.on_delete, on_delete); + assert_eq!(dependency.match_clause, "NONE"); + } + other => panic!("unexpected inbound-FK refusal: {other:?}"), + } + + assert_eq!(rebuild_authority_snapshot(&store).await, authority_before); + assert_eq!( + sqlx::query_scalar::<_, String>(sqlx::AssertSqlSafe(format!( + "SELECT printf('%d|%s|%s', id, quote(parent_singleton), note) FROM {child_table} ORDER BY id" + ))) + .fetch_all(store.pool()) + .await + .expect("caller child after refusal"), + child_before + ); + assert_eq!( + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s', type, name, tbl_name, sql) FROM main.sqlite_schema WHERE name LIKE 'caller_%' ORDER BY type, name", + ) + .await, + schema_before + ); + assert_eq!( + query_string_rows( + store.pool(), + "SELECT printf('%d|%s', id, action) FROM caller_rebuild_side_effect ORDER BY id", + ) + .await, + side_effect_before + ); + } + + for (suffix, parent_table, parent_column) in [ + ( + "virtual", + "radroots_event_store_food_availability_search_fts", + "rowid", + ), + ( + "config", + "radroots_event_store_food_availability_search_fts_config", + "k", + ), + ( + "content", + "radroots_event_store_food_availability_search_fts_content", + "id", + ), + ( + "data", + "radroots_event_store_food_availability_search_fts_data", + "id", + ), + ( + "docsize", + "radroots_event_store_food_availability_search_fts_docsize", + "id", + ), + ( + "idx", + "radroots_event_store_food_availability_search_fts_idx", + "segid", + ), + ("sqlite_sequence", "sqlite_sequence", "rowid"), + ] { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + let child_table = format!("caller_mutation_parent_{suffix}"); + let mut connection = store.pool().acquire().await.expect("connection"); + sqlx::query("PRAGMA foreign_keys = OFF") + .execute(&mut *connection) + .await + .expect("disable FK checks for mutation-parent fixture setup"); + sqlx::query( + "CREATE TABLE caller_mutation_parent_side_effect(id INTEGER PRIMARY KEY AUTOINCREMENT, action TEXT NOT NULL)", + ) + .execute(&mut *connection) + .await + .expect("caller mutation-parent side-effect table"); + let create_child = format!( + "CREATE TABLE {child_table}(id INTEGER PRIMARY KEY, parent_key, note TEXT NOT NULL, FOREIGN KEY(parent_key) REFERENCES {parent_table}({parent_column}) ON UPDATE SET NULL ON DELETE CASCADE)" + ); + sqlx::query(sqlx::AssertSqlSafe(create_child)) + .execute(&mut *connection) + .await + .expect("caller mutation-parent inbound-FK table"); + for (trigger_suffix, trigger_event) in [ + ("delete", "AFTER DELETE"), + ("update", "AFTER UPDATE OF parent_key"), + ] { + let create_trigger = format!( + "CREATE TRIGGER {child_table}_{trigger_suffix}_side_effect {trigger_event} ON {child_table} BEGIN INSERT INTO caller_mutation_parent_side_effect(action) VALUES ('{trigger_suffix}'); END" + ); + sqlx::query(sqlx::AssertSqlSafe(create_trigger)) + .execute(&mut *connection) + .await + .expect("caller mutation-parent child side-effect trigger"); + } + let insert_child = format!( + "INSERT INTO {child_table}(id, parent_key, note) SELECT 1, {parent_column}, 'preserve' FROM {parent_table} LIMIT 1" + ); + let inserted = sqlx::query(sqlx::AssertSqlSafe(insert_child)) + .execute(&mut *connection) + .await + .expect("caller mutation-parent dependent row"); + assert_eq!( + inserted.rows_affected(), + 1, + "empty rebuild mutation parent {parent_table}" + ); + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&mut *connection) + .await + .expect("restore FK checks"); + drop(connection); + + let authority_before = rebuild_authority_snapshot(&store).await; + let child_before = sqlx::query_scalar::<_, String>(sqlx::AssertSqlSafe(format!( + "SELECT printf('%d|%s|%s', id, quote(parent_key), note) FROM {child_table} ORDER BY id" + ))) + .fetch_all(store.pool()) + .await + .expect("caller mutation-parent child snapshot"); + let schema_before = query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s', type, name, tbl_name, sql) FROM main.sqlite_schema WHERE name LIKE 'caller_mutation_parent_%' ORDER BY type, name", + ) + .await; + let side_effect_before = query_string_rows( + store.pool(), + "SELECT printf('%d|%s', id, action) FROM caller_mutation_parent_side_effect ORDER BY id", + ) + .await; + + let error = rebuild_from_raw_v1_on_pool_for_test(store.pool(), &PanickingGeneration, None) + .await + .expect_err("caller mutation-parent inbound FK must be rejected before entropy"); + match error { + RadrootsEventStoreError::RawSourceRebuildCallerInboundForeignKeyUnsupported { + dependency, + } => { + assert_eq!(dependency.child_table, child_table); + assert_eq!(dependency.foreign_key_id, 0); + assert_eq!(dependency.foreign_key_sequence, 0); + assert_eq!(dependency.child_column, "parent_key"); + assert_eq!(dependency.parent_table, parent_table); + assert_eq!(dependency.parent_column.as_deref(), Some(parent_column)); + assert_eq!(dependency.on_update, "SET NULL"); + assert_eq!(dependency.on_delete, "CASCADE"); + assert_eq!(dependency.match_clause, "NONE"); + } + other => panic!("unexpected mutation-parent inbound-FK refusal: {other:?}"), + } + + assert_eq!(rebuild_authority_snapshot(&store).await, authority_before); + assert_eq!( + sqlx::query_scalar::<_, String>(sqlx::AssertSqlSafe(format!( + "SELECT printf('%d|%s|%s', id, quote(parent_key), note) FROM {child_table} ORDER BY id" + ))) + .fetch_all(store.pool()) + .await + .expect("caller mutation-parent child after refusal"), + child_before + ); + assert_eq!( + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s', type, name, tbl_name, sql) FROM main.sqlite_schema WHERE name LIKE 'caller_mutation_parent_%' ORDER BY type, name", + ) + .await, + schema_before + ); + assert_eq!( + query_string_rows( + store.pool(), + "SELECT printf('%d|%s', id, action) FROM caller_mutation_parent_side_effect ORDER BY id", + ) + .await, + side_effect_before + ); + } +} + +#[tokio::test] +async fn raw_source_rebuild_caller_schema_inventory_limits_are_typed_and_atomic_v1() { + let table_store = RadrootsEventStore::open_memory().await.expect("open"); + for table in ["caller_inventory_a", "caller_inventory_b"] { + let create = format!("CREATE TABLE {table}(id INTEGER PRIMARY KEY, value TEXT NOT NULL)"); + sqlx::query(sqlx::AssertSqlSafe(create)) + .execute(table_store.pool()) + .await + .expect("caller inventory table"); + let insert = format!("INSERT INTO {table}(id, value) VALUES (1, 'preserve')"); + sqlx::query(sqlx::AssertSqlSafe(insert)) + .execute(table_store.pool()) + .await + .expect("caller inventory row"); + } + let table_authority_before = rebuild_authority_snapshot(&table_store).await; + let table_rows_before = query_string_rows( + table_store.pool(), + "SELECT (SELECT value FROM caller_inventory_a WHERE id = 1) || '|' || (SELECT value FROM caller_inventory_b WHERE id = 1)", + ) + .await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_with_caller_schema_limits_for_test( + table_store.pool(), + &PanickingGeneration, + 1, + 4_096, + ) + .await, + Err( + RadrootsEventStoreError::RawSourceRebuildCallerTableCapacityExceeded { + observed_at_least: 2, + limit: 1, + } + ) + )); + assert_eq!( + rebuild_authority_snapshot(&table_store).await, + table_authority_before + ); + assert_eq!( + query_string_rows( + table_store.pool(), + "SELECT (SELECT value FROM caller_inventory_a WHERE id = 1) || '|' || (SELECT value FROM caller_inventory_b WHERE id = 1)", + ) + .await, + table_rows_before + ); + rebuild_from_raw_v1_on_pool_with_caller_schema_limits_for_test( + table_store.pool(), + &FixedGeneration(0x91), + 2, + 4_096, + ) + .await + .expect("exact caller-table capacity remains rebuildable"); + assert_eq!( + query_string_rows( + table_store.pool(), + "SELECT (SELECT value FROM caller_inventory_a WHERE id = 1) || '|' || (SELECT value FROM caller_inventory_b WHERE id = 1)", + ) + .await, + table_rows_before + ); + + let foreign_key_store = RadrootsEventStore::open_memory().await.expect("open"); + sqlx::query("CREATE TABLE caller_fk_parent(id INTEGER PRIMARY KEY)") + .execute(foreign_key_store.pool()) + .await + .expect("caller FK parent"); + sqlx::query( + "CREATE TABLE caller_fk_child(id INTEGER PRIMARY KEY, caller_parent_id INTEGER REFERENCES caller_fk_parent(id), managed_singleton INTEGER REFERENCES radroots_event_store_food_availability_cursor(singleton) ON DELETE CASCADE)", + ) + .execute(foreign_key_store.pool()) + .await + .expect("caller FK child"); + sqlx::query("INSERT INTO caller_fk_parent(id) VALUES (1)") + .execute(foreign_key_store.pool()) + .await + .expect("caller FK parent row"); + sqlx::query( + "INSERT INTO caller_fk_child(id, caller_parent_id, managed_singleton) VALUES (1, 1, 1)", + ) + .execute(foreign_key_store.pool()) + .await + .expect("caller FK child row"); + let foreign_key_authority_before = rebuild_authority_snapshot(&foreign_key_store).await; + let foreign_key_rows_before = query_string_rows( + foreign_key_store.pool(), + "SELECT printf('%d|%d|%d', id, caller_parent_id, managed_singleton) FROM caller_fk_child ORDER BY id", + ) + .await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_with_caller_schema_limits_for_test( + foreign_key_store.pool(), + &PanickingGeneration, + 2, + 1, + ) + .await, + Err( + RadrootsEventStoreError::RawSourceRebuildCallerForeignKeyCapacityExceeded { + observed_at_least: 2, + limit: 1, + } + ) + )); + assert_eq!( + rebuild_authority_snapshot(&foreign_key_store).await, + foreign_key_authority_before + ); + assert_eq!( + query_string_rows( + foreign_key_store.pool(), + "SELECT printf('%d|%d|%d', id, caller_parent_id, managed_singleton) FROM caller_fk_child ORDER BY id", + ) + .await, + foreign_key_rows_before + ); + assert!(matches!( + rebuild_from_raw_v1_on_pool_with_caller_schema_limits_for_test( + foreign_key_store.pool(), + &PanickingGeneration, + 2, + 2, + ) + .await, + Err(RadrootsEventStoreError::RawSourceRebuildCallerInboundForeignKeyUnsupported { + dependency, + }) if dependency.child_table == "caller_fk_child" + && dependency.parent_table == "radroots_event_store_food_availability_cursor" + )); + assert_eq!( + rebuild_authority_snapshot(&foreign_key_store).await, + foreign_key_authority_before + ); + assert_eq!( + query_string_rows( + foreign_key_store.pool(), + "SELECT printf('%d|%d|%d', id, caller_parent_id, managed_singleton) FROM caller_fk_child ORDER BY id", + ) + .await, + foreign_key_rows_before + ); +} + +#[tokio::test] +async fn raw_source_rebuild_scoped_integrity_preserves_caller_state_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + let (event_seq, event_id, pubkey, created_at): (i64, String, String, i64) = sqlx::query_as( + "SELECT seq, event_id, pubkey, created_at FROM event_envelopes ORDER BY seq LIMIT 1", + ) + .fetch_one(store.pool()) + .await + .expect("raw source row"); + sqlx::query( + "INSERT INTO event_transport_observation(event_id, transport_kind, endpoint_uri, endpoint_fingerprint, observation_type, first_observed_at_ms, last_observed_at_ms, observation_count, redacted_message) VALUES (?, 'nostr', 'wss://relay.example', 'caller-endpoint', 'received', 1, 2, 2, 'preserve')", + ) + .bind(&event_id) + .execute(store.pool()) + .await + .expect("legacy transport observation"); + sqlx::query( + "INSERT INTO listing_projection(listing_addr, listing_event_id, seller_pubkey, farm_pubkey, farm_d_tag, listing_d_tag, title, description, product_type, primary_bin_id, quantity_amount, quantity_unit, price_amount, price_currency, inventory_available, availability_status, delivery_method, locality_primary, locality_city, locality_region, locality_country, geohash5, listing_json, source_event_seq, created_at, updated_at_ms) VALUES ('caller-listing', ?, ?, ?, 'farm', 'listing', 'Carrots', 'Fresh carrots', 'vegetable', 'bin-1', '12', 'kg', '5.00', 'CAD', '12', 'available', 'pickup', 'Victoria, BC', 'Victoria', 'BC', 'CA', 'c28', '{}', ?, ?, 3)", + ) + .bind(&event_id) + .bind(&pubkey) + .bind(&pubkey) + .bind(event_seq) + .bind(created_at) + .execute(store.pool()) + .await + .expect("legacy listing projection"); + sqlx::query( + "INSERT INTO listing_search_fts(listing_addr, title, description, product_type, locality, seller_pubkey) VALUES ('caller-listing', 'Carrots', 'Fresh carrots', 'vegetable', 'Victoria, BC', ?)", + ) + .bind(&pubkey) + .execute(store.pool()) + .await + .expect("legacy listing search row"); + sqlx::query( + "INSERT INTO trade_mutation(mutation_id, trade_id, root_mutation_id, contract_id, mutation_kind, schema_version, candidate_id, proposal_mutation_id, target_claim_mutation_id, author_pubkey, counterparty_pubkey, buyer_pubkey, seller_pubkey, farm_id, authored_at_unix_s, canonical_payload_bytes, payload_sha256, first_event_seq, first_transport_event_id, inserted_at_ms) VALUES ('caller-mutation', 'caller-trade', NULL, 'radroots.trade.v1', 'proposal', 1, 'candidate-1', NULL, NULL, ?, ?, ?, ?, 'farm-1', ?, X'7B7D', ?, ?, ?, 4)", + ) + .bind(&pubkey) + .bind(&pubkey) + .bind(&pubkey) + .bind(&pubkey) + .bind(created_at) + .bind("a".repeat(64)) + .bind(event_seq) + .bind(&event_id) + .execute(store.pool()) + .await + .expect("legacy trade mutation"); + sqlx::query( + "INSERT INTO trade_transport_envelope(transport_event_id, mutation_id, trade_id, transport_kind, pubkey, created_at, event_seq, payload_sha256, observed_at_ms) VALUES (?, 'caller-mutation', 'caller-trade', 'nostr', ?, ?, ?, ?, 5)", + ) + .bind(&event_id) + .bind(&pubkey) + .bind(created_at) + .bind(event_seq) + .bind("a".repeat(64)) + .execute(store.pool()) + .await + .expect("legacy trade transport envelope"); + let mut connection = store.pool().acquire().await.expect("connection"); + sqlx::query("PRAGMA foreign_keys = OFF") + .execute(&mut *connection) + .await + .expect("disable caller FK checks"); + sqlx::query("CREATE TABLE caller_parent(id INTEGER PRIMARY KEY)") + .execute(&mut *connection) + .await + .expect("caller parent"); + sqlx::query( + "CREATE TABLE caller_child(id INTEGER PRIMARY KEY AUTOINCREMENT, parent_id INTEGER NOT NULL REFERENCES caller_parent(id), note TEXT NOT NULL)", + ) + .execute(&mut *connection) + .await + .expect("caller child"); + sqlx::query("CREATE INDEX caller_child_note_idx ON caller_child(note)") + .execute(&mut *connection) + .await + .expect("caller index"); + sqlx::query("INSERT INTO caller_child(parent_id, note) VALUES (999, 'preserve')") + .execute(&mut *connection) + .await + .expect("caller FK violation"); + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&mut *connection) + .await + .expect("restore FK checks"); + drop(connection); + let caller_before = query_string_rows( + store.pool(), + "SELECT printf('%d|%d|%s', id, parent_id, note) FROM caller_child ORDER BY id", + ) + .await; + let sequence_before = query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s', rowid, quote(name), quote(seq)) FROM sqlite_sequence WHERE name = 'caller_child'", + ) + .await; + let caller_index_before = query_string_rows( + store.pool(), + "SELECT sql FROM sqlite_schema WHERE type = 'index' AND name = 'caller_child_note_idx'", + ) + .await; + let legacy_before = vec![ + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%d|%d|%d|%s', event_id, transport_kind, endpoint_fingerprint, observation_type, first_observed_at_ms, last_observed_at_ms, observation_count, quote(redacted_message)) FROM event_transport_observation ORDER BY event_id, transport_kind, endpoint_fingerprint, observation_type", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s|%s|%d|%d', listing_addr, listing_event_id, seller_pubkey, title, locality_primary, listing_json, source_event_seq, updated_at_ms) FROM listing_projection ORDER BY listing_addr", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s|%s', listing_addr, title, description, product_type, locality, seller_pubkey) FROM listing_search_fts ORDER BY listing_addr", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%d|%s|%s|%d|%s', mutation_id, trade_id, contract_id, mutation_kind, schema_version, hex(canonical_payload_bytes), payload_sha256, first_event_seq, first_transport_event_id) FROM trade_mutation ORDER BY mutation_id", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s|%d|%d|%s|%d', transport_event_id, mutation_id, trade_id, transport_kind, pubkey, created_at, event_seq, payload_sha256, observed_at_ms) FROM trade_transport_envelope ORDER BY transport_event_id", + ) + .await, + ]; + + store + .rebuild_from_raw_v1() + .await + .expect("scoped rebuild ignores caller violation"); + assert_eq!( + query_string_rows( + store.pool(), + "SELECT printf('%d|%d|%s', id, parent_id, note) FROM caller_child ORDER BY id", + ) + .await, + caller_before + ); + assert_eq!( + query_string_rows( + store.pool(), + "SELECT printf('%d|%s|%s', rowid, quote(name), quote(seq)) FROM sqlite_sequence WHERE name = 'caller_child'", + ) + .await, + sequence_before + ); + assert_eq!( + query_string_rows( + store.pool(), + "SELECT sql FROM sqlite_schema WHERE type = 'index' AND name = 'caller_child_note_idx'", + ) + .await, + caller_index_before + ); + let legacy_after = vec![ + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%d|%d|%d|%s', event_id, transport_kind, endpoint_fingerprint, observation_type, first_observed_at_ms, last_observed_at_ms, observation_count, quote(redacted_message)) FROM event_transport_observation ORDER BY event_id, transport_kind, endpoint_fingerprint, observation_type", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s|%s|%d|%d', listing_addr, listing_event_id, seller_pubkey, title, locality_primary, listing_json, source_event_seq, updated_at_ms) FROM listing_projection ORDER BY listing_addr", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s|%s', listing_addr, title, description, product_type, locality, seller_pubkey) FROM listing_search_fts ORDER BY listing_addr", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%d|%s|%s|%d|%s', mutation_id, trade_id, contract_id, mutation_kind, schema_version, hex(canonical_payload_bytes), payload_sha256, first_event_seq, first_transport_event_id) FROM trade_mutation ORDER BY mutation_id", + ) + .await, + query_string_rows( + store.pool(), + "SELECT printf('%s|%s|%s|%s|%s|%d|%d|%s|%d', transport_event_id, mutation_id, trade_id, transport_kind, pubkey, created_at, event_seq, payload_sha256, observed_at_ms) FROM trade_transport_envelope ORDER BY transport_event_id", + ) + .await, + ]; + assert_eq!(legacy_after, legacy_before); + assert_eq!( + sqlx::query("PRAGMA foreign_key_check('caller_child')") + .fetch_all(store.pool()) + .await + .expect("caller FK audit") + .len(), + 1 + ); +} + +#[tokio::test] +async fn raw_source_rebuild_generation_exhaustion_precedes_entropy_and_mutation_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + for generation in 1..RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1 { + rebuild_from_raw_v1_on_pool_for_test( + store.pool(), + &FixedGeneration(u8::try_from(generation).expect("test generation")), + None, + ) + .await + .expect("fill retained generation history"); + } + let before = rebuild_authority_snapshot(&store).await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &PanickingGeneration, None).await, + Err(RadrootsEventStoreError::SourceGenerationHistoryLimitReached { current, limit }) + if current == limit && limit == RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1 + )); + assert_eq!(rebuild_authority_snapshot(&store).await, before); +} + +#[tokio::test] +async fn raw_source_rebuild_entropy_failure_is_atomic_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + let before = rebuild_authority_snapshot(&store).await; + + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FailingGeneration, None).await, + Err(RadrootsEventStoreError::SourceGenerationEntropyUnavailable) + )); + assert_eq!(rebuild_authority_snapshot(&store).await, before); +} + +#[tokio::test] +async fn raw_source_rebuild_empty_source_without_transitions_is_deterministic_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + let first = rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x61), None) + .await + .expect("first empty rebuild"); + let second = rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x62), None) + .await + .expect("second empty rebuild"); + assert_eq!(first.raw_high_water_seq(), 0); + assert_eq!(second.raw_high_water_seq(), 0); + assert_eq!(first.immutable_raw_digest(), second.immutable_raw_digest()); + assert_eq!( + first.active_product_state_digest(), + second.active_product_state_digest() + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_event_store_addressable_head_transition", + ) + .fetch_one(store.pool()) + .await + .expect("transition count"), + 0 + ); + let sequence = sqlx::query_as::<_, (i64, String, i64)>( + "SELECT rowid, name, seq FROM sqlite_sequence ORDER BY rowid LIMIT 1", + ) + .fetch_one(store.pool()) + .await + .expect("transition sequence"); + assert_eq!(sequence.1, TRANSITION_SEQUENCE_NAME); + assert_eq!(sequence.2, 0); +} + +#[tokio::test] +async fn raw_source_repair_preflights_reject_bounded_authority_drift_v1() { + let tempdir = tempfile::tempdir().expect("tempdir"); + let missing_canonical_path = tempdir.path().join("missing-canonical.sqlite"); + let missing_canonical_filename = missing_canonical_path.display().to_string(); + assert!(matches!( + super::canonical_raw_source_repair_main_path_v1(&missing_canonical_path), + Err(RadrootsEventStoreError::RawSourceRepairMainDatabaseCanonicalizationFailed { + filename, + source, + }) if filename == missing_canonical_filename + && source.kind() == std::io::ErrorKind::NotFound + )); + + let catalog_path = tempdir.path().join("repair-catalog-drift.sqlite"); + let catalog_store = RadrootsEventStore::open_file(&catalog_path) + .await + .expect("open catalog fixture"); + catalog_store.pool().close().await; + let mut catalog_connection = + SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&catalog_path)) + .await + .expect("catalog drift connection"); + sqlx::query("CREATE TABLE radroots_event_store_future_authority(value TEXT NOT NULL)") + .execute(&mut catalog_connection) + .await + .expect("add one reserved catalog object beyond managed authority"); + catalog_connection + .close() + .await + .expect("close catalog drift connection"); + assert!(matches!( + RadrootsEventStore::repair_file_from_raw_v1(&catalog_path).await, + Err(RadrootsEventStoreError::SchemaFingerprintMismatch { version: 4, .. }) + )); + let mut catalog_verifier = + SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&catalog_path)) + .await + .expect("catalog verifier"); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM main.sqlite_schema WHERE type = 'table' AND name = 'radroots_event_store_future_authority'", + ) + .fetch_one(&mut catalog_verifier) + .await + .expect("reserved catalog object count"), + 1 + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_event_store_source_generation", + ) + .fetch_one(&mut catalog_verifier) + .await + .expect("catalog rejection generation count"), + 1 + ); + catalog_verifier + .close() + .await + .expect("close catalog verifier"); + + let history_path = tempdir.path().join("repair-history-drift.sqlite"); + let history_store = RadrootsEventStore::open_file(&history_path) + .await + .expect("open history fixture"); + history_store.pool().close().await; + let mut history_connection = + SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&history_path)) + .await + .expect("history drift connection"); + sqlx::query( + "INSERT INTO radroots_event_store_schema_migrations(version, name, up_sha256, down_sha256, schema_sha256) VALUES (5, 'future_migration', ?, ?, ?)", + ) + .bind("0".repeat(64)) + .bind("1".repeat(64)) + .bind("2".repeat(64)) + .execute(&mut history_connection) + .await + .expect("add the bounded fifth history row"); + history_connection + .close() + .await + .expect("close history drift connection"); + assert!(matches!( + RadrootsEventStore::repair_file_from_raw_v1(&history_path).await, + Err(RadrootsEventStoreError::SchemaTooNew { + current: 4, + database: 5, + }) + )); + let mut history_verifier = + SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&history_path)) + .await + .expect("history verifier"); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_event_store_schema_migrations", + ) + .fetch_one(&mut history_verifier) + .await + .expect("history row count"), + 5 + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_event_store_source_generation", + ) + .fetch_one(&mut history_verifier) + .await + .expect("history rejection generation count"), + 1 + ); + history_verifier + .close() + .await + .expect("close history verifier"); + + let encoding_path = tempdir.path().join("repair-utf16.sqlite"); + let mut encoding_connection = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(&encoding_path) + .create_if_missing(true), + ) + .await + .expect("UTF-16 fixture connection"); + sqlx::query("PRAGMA main.encoding = 'UTF-16le'") + .execute(&mut encoding_connection) + .await + .expect("set UTF-16LE encoding"); + sqlx::query("CREATE TABLE encoding_anchor(value TEXT NOT NULL)") + .execute(&mut encoding_connection) + .await + .expect("materialize UTF-16LE database"); + sqlx::query("DROP TABLE encoding_anchor") + .execute(&mut encoding_connection) + .await + .expect("restore empty UTF-16LE catalog"); + encoding_connection + .close() + .await + .expect("close UTF-16 fixture"); + assert!(matches!( + RadrootsEventStore::repair_file_from_raw_v1(&encoding_path).await, + Err(RadrootsEventStoreError::SqliteMainDatabaseEncodingNotUtf8 { actual }) + if actual == "UTF-16le" + )); + let mut encoding_verifier = + SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&encoding_path)) + .await + .expect("UTF-16 verifier"); + assert_eq!( + sqlx::query_scalar::<_, String>("PRAGMA main.encoding") + .fetch_one(&mut encoding_verifier) + .await + .expect("UTF-16 encoding after rejection"), + "UTF-16le" + ); + assert_eq!( + sqlx::query_scalar::<_, String>("PRAGMA main.journal_mode") + .fetch_one(&mut encoding_verifier) + .await + .expect("UTF-16 journal mode after rejection"), + "delete" + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM main.sqlite_schema WHERE name = 'radroots_event_store_schema_migrations' OR name = 'event_envelopes' OR name LIKE 'radroots_event_store_%'", + ) + .fetch_one(&mut encoding_verifier) + .await + .expect("UTF-16 event-store catalog after rejection"), + 0 + ); + encoding_verifier + .close() + .await + .expect("close UTF-16 verifier"); +} + +#[tokio::test] +async fn raw_source_rebuild_cold_file_repair_v1() { + let tempdir = tempfile::tempdir().expect("tempdir"); + let missing = tempdir.path().join("missing.sqlite"); + assert!(matches!( + RadrootsEventStore::repair_file_from_raw_v1(&missing).await, + Err(RadrootsEventStoreError::RawSourceRepairMainDatabaseCanonicalizationFailed { + source, + .. + }) if source.kind() == std::io::ErrorKind::NotFound + )); + assert!( + !missing.exists(), + "cold repair must not create a missing file" + ); + + let unmanaged = tempdir.path().join("unmanaged.sqlite"); + let mut unmanaged_connection = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(&unmanaged) + .create_if_missing(true), + ) + .await + .expect("unmanaged connection"); + sqlx::query("CREATE TABLE caller_only(value TEXT NOT NULL)") + .execute(&mut unmanaged_connection) + .await + .expect("unmanaged caller table"); + assert_eq!( + sqlx::query_scalar::<_, String>("PRAGMA main.journal_mode = DELETE") + .fetch_one(&mut unmanaged_connection) + .await + .expect("set unmanaged journal mode"), + "delete" + ); + unmanaged_connection.close().await.expect("close unmanaged"); + assert!(matches!( + RadrootsEventStore::repair_file_from_raw_v1(&unmanaged).await, + Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + .. + }) + )); + let mut unmanaged_verifier = + SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&unmanaged)) + .await + .expect("unmanaged verifier"); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM sqlite_schema WHERE name = 'radroots_event_store_schema_migrations'", + ) + .fetch_one(&mut unmanaged_verifier) + .await + .expect("unmanaged ledger absence"), + 0 + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM sqlite_schema WHERE type = 'table' AND name = 'caller_only'", + ) + .fetch_one(&mut unmanaged_verifier) + .await + .expect("caller-owned table preservation"), + 1 + ); + assert_eq!( + sqlx::query_scalar::<_, String>("PRAGMA main.journal_mode") + .fetch_one(&mut unmanaged_verifier) + .await + .expect("unmanaged journal mode after rejected repair"), + "delete", + "rejected cold repair must not persistently configure WAL" + ); + unmanaged_verifier.close().await.expect("close verifier"); + + let v3_path = tempdir.path().join("managed-v3.sqlite"); + let v3_store = RadrootsEventStore::open_file(&v3_path) + .await + .expect("open v3 fixture"); + rollback_event_store_schema_offline_destructive_for_migration_test(v3_store.pool(), 3) + .await + .expect("rollback to v3"); + v3_store.pool().close().await; + assert!(matches!( + RadrootsEventStore::repair_file_from_raw_v1(&v3_path).await, + Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + .. + }) + )); + let mut v3_verifier = + SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&v3_path)) + .await + .expect("v3 verifier"); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT MAX(version) FROM radroots_event_store_schema_migrations", + ) + .fetch_one(&mut v3_verifier) + .await + .expect("v3 ledger"), + 3 + ); + v3_verifier.close().await.expect("close v3 verifier"); + + let path = tempdir.path().join("cold-raw-repair.sqlite"); + let store = RadrootsEventStore::open_file(&path).await.expect("open"); + seed_food_fixture(&store).await; + let pristine_product = logical_product_snapshot(&store).await; + set_trigger_guarded_drift( + &store, + "radroots_event_store_event_envelopes_derived_update_guard", + "UPDATE event_envelopes SET contract_status = 'invalid', contract_id = NULL, event_class = NULL, projection_eligible = 0", + ) + .await; + set_trigger_guarded_drift( + &store, + "radroots_event_store_food_availability_cursor_update_guard", + "UPDATE radroots_event_store_food_availability_cursor SET hook_manifest_sha256 = '0000000000000000000000000000000000000000000000000000000000000000'", + ) + .await; + store.pool().close().await; + let ordinary_open_error = match RadrootsEventStore::open_file(&path).await { + Ok(_) => panic!("ordinary open must reject drifted derived authority"), + Err(error) => error, + }; + assert!(matches!( + ordinary_open_error, + RadrootsEventStoreError::FoodAvailabilityProjectionDrift { reason } + if reason == "projection cursor identity is inconsistent" + )); + + let (repaired, first_report) = RadrootsEventStore::repair_file_from_raw_v1(&path) + .await + .expect("cold file repair"); + assert_eq!(first_report.source_capacity().raw_event_count(), 1); + assert_eq!(logical_product_snapshot(&repaired).await, pristine_product); + + set_trigger_guarded_drift( + &repaired, + "radroots_event_store_food_availability_cursor_update_guard", + "UPDATE radroots_event_store_food_availability_cursor SET hook_manifest_sha256 = '0000000000000000000000000000000000000000000000000000000000000000'", + ) + .await; + repaired.pool().close().await; + let (repaired_from_file, second_report) = RadrootsEventStore::repair_file_from_raw_v1(&path) + .await + .expect("second cold file repair"); + assert_eq!( + first_report.immutable_raw_digest(), + second_report.immutable_raw_digest() + ); + assert_eq!( + first_report.active_product_state_digest(), + second_report.active_product_state_digest() + ); + assert_eq!( + logical_product_snapshot(&repaired_from_file).await, + pristine_product + ); +} + +#[tokio::test] +async fn raw_source_repair_rejects_delete_mode_exact_v4_without_mutation_v1() { + let tempdir = tempfile::tempdir().expect("tempdir"); + let path = tempdir.path().join("repair-delete-mode.sqlite"); + let store = RadrootsEventStore::open_file(&path) + .await + .expect("open exact-v4 fixture"); + seed_food_fixture(&store).await; + store.pool().close().await; + + let mut connection = + SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&path)) + .await + .expect("DELETE-mode connection"); + assert_eq!( + sqlx::query_scalar::<_, String>("PRAGMA main.journal_mode = DELETE") + .fetch_one(&mut connection) + .await + .expect("set DELETE mode"), + "delete" + ); + connection.close().await.expect("close DELETE-mode fixture"); + let (before, before_journal_mode) = cold_file_authority_snapshot(&path).await; + assert_eq!(before_journal_mode, "delete"); + + assert!(matches!( + RadrootsEventStore::repair_file_from_raw_v1(&path).await, + Err(RadrootsEventStoreError::SqliteFileJournalModeNotWal { actual }) + if actual == "delete" + )); + + let (after, after_journal_mode) = cold_file_authority_snapshot(&path).await; + assert_eq!(after, before); + assert_eq!(after_journal_mode, "delete"); +} + +#[tokio::test] +async fn raw_source_repair_rejects_canonical_path_lock_domain_mismatch_v1() { + let tempdir = tempfile::tempdir().expect("tempdir"); + let primary_path = tempdir.path().join("repair-primary.sqlite"); + let candidate_path = tempdir.path().join("repair-candidate.sqlite"); + let primary_store = RadrootsEventStore::open_file(&primary_path) + .await + .expect("open primary fixture"); + seed_food_fixture(&primary_store).await; + primary_store.pool().close().await; + let candidate_store = RadrootsEventStore::open_file(&candidate_path) + .await + .expect("open candidate fixture"); + candidate_store.pool().close().await; + let primary_before = cold_file_authority_snapshot(&primary_path).await; + let candidate_before = cold_file_authority_snapshot(&candidate_path).await; + let canonical_candidate = + std::fs::canonicalize(&candidate_path).expect("canonical candidate path"); + + let mut primary = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(&primary_path) + .create_if_missing(false), + ) + .await + .expect("open primary connection"); + let transaction = primary + .begin_with("BEGIN IMMEDIATE") + .await + .expect("hold primary write domain"); + assert!(matches!( + super::validate_raw_source_repair_canonical_lock_domain_v1(&canonical_candidate).await, + Err(RadrootsEventStoreError::RawSourceRepairCanonicalPathLockDomainMismatch { + canonical_path, + }) if canonical_path == canonical_candidate.display().to_string() + )); + transaction + .rollback() + .await + .expect("rollback primary write domain"); + primary.close().await.expect("close primary connection"); + assert_eq!( + cold_file_authority_snapshot(&primary_path).await, + primary_before + ); + assert_eq!( + cold_file_authority_snapshot(&candidate_path).await, + candidate_before + ); +} + +#[tokio::test] +async fn raw_source_repair_post_preflight_failures_preserve_wal_and_state_v1() { + let tempdir = tempfile::tempdir().expect("tempdir"); + for (case, trigger, mutation) in [ + ( + "raw", + "radroots_event_store_event_envelopes_raw_update_guard", + "UPDATE event_envelopes SET content = 'Parsnip available this week.' WHERE content = 'Carrots available this week.'", + ), + ( + "source", + "radroots_event_store_source_capacity_update_guard", + "UPDATE radroots_event_store_source_capacity_v1 SET raw_event_count = raw_event_count + 1", + ), + ] { + let path = tempdir.path().join(format!("repair-{case}-drift.sqlite")); + let store = RadrootsEventStore::open_file(&path) + .await + .expect("open drift fixture"); + seed_food_fixture(&store).await; + set_trigger_guarded_drift(&store, trigger, mutation).await; + let before = rebuild_authority_snapshot(&store).await; + store.pool().close().await; + + let error = match RadrootsEventStore::repair_file_from_raw_v1(&path).await { + Ok(_) => panic!("post-preflight authority drift must fail"), + Err(error) => error, + }; + match case { + "raw" => assert!(matches!( + error, + RadrootsEventStoreError::RawEventReconciliationMismatch { + field: "content", + .. + } + )), + "source" => assert!(matches!( + error, + RadrootsEventStoreError::SourceCapacityStateDrift { reason } + if reason + == "capacity seal does not match active source state and generation history" + )), + _ => unreachable!(), + } + + let (after, journal_mode) = cold_file_authority_snapshot(&path).await; + assert_eq!(after, before, "{case} drift"); + assert_eq!(journal_mode, "wal", "{case} drift"); + } +} + +#[tokio::test] +async fn raw_source_rebuild_repairs_active_transition_high_water_metadata_drift_v1() { + assert_nonempty_transition_high_water_drift_is_repaired( + "UPDATE radroots_event_store_source_state SET last_transition_seq = 0 WHERE singleton = 1", + 0x81, + 0x82, + 0x83, + ) + .await; + assert_nonempty_transition_high_water_drift_is_repaired( + "UPDATE radroots_event_store_source_state SET last_transition_seq = (SELECT COALESCE(MAX(transition_seq), 0) + 7 FROM radroots_event_store_addressable_head_transition) WHERE singleton = 1", + 0x84, + 0x85, + 0x86, + ) + .await; +} + +#[tokio::test] +async fn raw_source_rebuild_repairs_empty_transition_high_water_metadata_drift_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + let pristine = rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x87), None) + .await + .expect("establish empty pristine rebuild"); + let pristine_product = logical_product_snapshot(&store).await; + assert_eq!(transition_high_water(&store).await, 0); + set_trigger_guarded_drift( + &store, + "radroots_event_store_source_state_authority_update_guard", + "UPDATE radroots_event_store_source_state SET last_transition_seq = 7 WHERE singleton = 1", + ) + .await; + + let repaired = rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x88), None) + .await + .expect("repair empty transition high-water drift"); + assert_eq!( + repaired.immutable_raw_digest(), + pristine.immutable_raw_digest() + ); + assert_eq!( + repaired.active_product_state_digest(), + pristine.active_product_state_digest() + ); + assert_eq!(logical_product_snapshot(&store).await, pristine_product); + let repaired_generation = repaired.new_source_generation(); + let repaired_floor: i64 = sqlx::query_scalar( + "SELECT transition_floor_seq FROM radroots_event_store_source_generation WHERE source_generation = ?", + ) + .bind(repaired_generation.as_bytes().as_slice()) + .fetch_one(store.pool()) + .await + .expect("empty repaired generation transition floor"); + assert_eq!(repaired_floor, 0); + assert_eq!(transition_high_water(&store).await, 0); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT last_transition_seq FROM radroots_event_store_source_state WHERE singleton = 1", + ) + .fetch_one(store.pool()) + .await + .expect("empty repaired source-state high-water"), + 0 + ); + + store + .migrate_to_current_schema() + .await + .expect("ordinary reopen validation after empty repair"); + let repeated = rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x89), None) + .await + .expect("repeat empty rebuild after repair"); + assert_eq!( + repeated.immutable_raw_digest(), + repaired.immutable_raw_digest() + ); + assert_eq!( + repeated.active_product_state_digest(), + repaired.active_product_state_digest() + ); +} + +#[tokio::test] +async fn raw_source_rebuild_refuses_transition_history_gap_atomically_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_fixture_case( + &store, + FOOD_FIXTURE, + "post_cutoff_replacement_restores_projection", + "events", + ) + .await; + let transition_count: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM radroots_event_store_addressable_head_transition") + .fetch_one(store.pool()) + .await + .expect("transition count before gap"); + assert!(transition_count >= 2); + set_trigger_guarded_drift( + &store, + "radroots_event_store_addressable_transition_delete_guard", + "DELETE FROM radroots_event_store_addressable_head_transition WHERE transition_seq = (SELECT MIN(transition_seq) FROM radroots_event_store_addressable_head_transition)", + ) + .await; + let before = rebuild_authority_snapshot(&store).await; + + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x8a), None).await, + Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + detail, + }) if detail.contains("lineage row") || detail.contains("gaps or foreign rows") + )); + assert_eq!(rebuild_authority_snapshot(&store).await, before); +} + +#[tokio::test] +async fn raw_source_rebuild_refuses_historical_generation_lineage_corruption_v1() { + let store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&store).await; + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x71), None) + .await + .expect("first rebuild"); + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x72), None) + .await + .expect("second rebuild"); + set_trigger_guarded_drift( + &store, + "radroots_event_store_source_generation_update_guard", + "UPDATE radroots_event_store_source_generation SET baseline_raw_event_count = 0 WHERE generation_ordinal = 2", + ) + .await; + let before = rebuild_authority_snapshot(&store).await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test(store.pool(), &FixedGeneration(0x73), None).await, + Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::SourceGenerationLineage, + detail, + }) if detail.contains("lineage row 2") + )); + assert_eq!(rebuild_authority_snapshot(&store).await, before); + + let transition_store = RadrootsEventStore::open_memory().await.expect("open"); + seed_food_fixture(&transition_store).await; + rebuild_from_raw_v1_on_pool_for_test(transition_store.pool(), &FixedGeneration(0x74), None) + .await + .expect("first rebuild"); + rebuild_from_raw_v1_on_pool_for_test(transition_store.pool(), &FixedGeneration(0x75), None) + .await + .expect("second rebuild"); + set_trigger_guarded_drift( + &transition_store, + "radroots_event_store_addressable_transition_update_guard", + "UPDATE radroots_event_store_addressable_head_transition SET source_generation = (SELECT source_generation FROM radroots_event_store_source_generation WHERE generation_ordinal = 2) WHERE transition_seq = 1", + ) + .await; + let transition_before = rebuild_authority_snapshot(&transition_store).await; + assert!(matches!( + rebuild_from_raw_v1_on_pool_for_test( + transition_store.pool(), + &FixedGeneration(0x76), + None, + ) + .await, + Err(RadrootsEventStoreError::RawSourceRebuildStateDrift { + kind: RadrootsEventStoreRawSourceRebuildDriftV1::AddressableTransitionAuthority, + detail, + }) if detail.contains("lineage row 1") + )); + assert_eq!( + rebuild_authority_snapshot(&transition_store).await, + transition_before + ); +} diff --git a/crates/event_store/tests/fixtures/raw_source_rebuild.v1.json b/crates/event_store/tests/fixtures/raw_source_rebuild.v1.json @@ -0,0 +1,462 @@ +{ + "schema_version": 1, + "contract_id": "radroots_event_store.raw_source_rebuild_v1", + "delegated_suite": { + "id": "radroots_event_store.raw_source_rebuild_v1.delegated_rust_test_suite.v1", + "lane": "nix run .#contract", + "package": "radroots_event_store", + "authorities": [ + { + "authority": "raw_source_rebuild_incremental_reopen_and_repeat_parity_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "projection_cursor_capacity_accepts_exact_and_rejects_one_over_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_invalidates_generic_cursors_without_enumerating_or_mutating_them_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_normalizes_only_transition_sqlite_sequence_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_rejects_unrelated_minimum_transition_sequence_rowid_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_reuses_target_alias_at_minimum_sequence_rowid_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_repairs_active_transition_high_water_metadata_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_repairs_empty_transition_high_water_metadata_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_repairs_derived_drift_and_refuses_raw_drift_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_refuses_transition_history_gap_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_refuses_historical_generation_lineage_corruption_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_rollback_failure_preserves_primary_and_rollback_errors_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_wal_readers_observe_only_committed_generation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_rejects_caller_inbound_foreign_keys_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_caller_schema_inventory_limits_are_typed_and_atomic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_scoped_integrity_preserves_caller_state_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_generation_exhaustion_precedes_entropy_and_mutation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_entropy_failure_is_atomic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_empty_source_without_transitions_is_deterministic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_rebuild_cold_file_repair_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_repair_preflights_reject_bounded_authority_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_repair_rejects_delete_mode_exact_v4_without_mutation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_repair_rejects_canonical_path_lock_domain_mismatch_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_source_repair_post_preflight_failures_preserve_wal_and_state_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" + }, + { + "authority": "raw_snapshot_visibility_oracle_covers_regular_replaceable_addressable_and_deletion_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" + }, + { + "authority": "raw_snapshot_visibility_oracle_matches_wide_event_and_address_requests_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" + }, + { + "authority": "raw_snapshot_visibility_oracle_matches_all_protocol_decision_branches_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" + }, + { + "authority": "raw_snapshot_visibility_oracle_is_order_and_repeat_invariant_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" + } + ] + }, + "cases": [ + { + "id": "empty_source_repeat_digest_parity", + "execution": "direct_executor", + "authority": "raw_source_rebuild_v1_result_vector", + "authority_path": "crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs", + "expected_outcome": "two committed rebuilds rotate generations while preserving zero capacity, raw high-water, immutable-raw digest, and normalized product digest", + "expected_immutable_raw_digest": "73e66ea95452e902176d701311e6e82b3cd7895ae77f3d73fd1cbb67bcf9d321", + "expected_active_product_state_digest": "bf20fc2ba0e7c64bb0958829e118d87a86efe17e2848bb098d3d9ab2a78c2245" + }, + { + "id": "signed_food_fixture_typed_digest_parity", + "execution": "direct_executor", + "authority": "raw_source_rebuild_v1_result_vector", + "authority_path": "crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs", + "expected_outcome": "one signed admitted FoodAvailability fixture freezes exact immutable-raw and generation-normalized product digests across text, i64, boolean, optional, and blob framing and preserves them across repeated rebuild", + "expected_immutable_raw_digest": "336a6a6cf1d84b0fcb185c4a7550cf5a8d8047c5a3f89df40e0d8f1ead543c68", + "expected_active_product_state_digest": "1ed8a22036091f0a492f3848027b313be4ef1202a9cdfa6c3ff35e12ab10f15c" + }, + { + "id": "incremental_reopen_repeat_product_parity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_incremental_reopen_and_repeat_parity_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "incremental, file reopen, first rebuild, and repeated rebuild expose identical generation-normalized current visibility, Food image, and logical FTS product witnesses", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "projection_cursor_capacity_exact_and_one_over", + "execution": "delegated_rust_test", + "authority": "projection_cursor_capacity_accepts_exact_and_rejects_one_over_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "4,096 unique generic cursors are accepted, the next unique insert and a 4,097-row migration/reconciliation inventory probe fail typed, and an existing identity at capacity remains updateable", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "generic_cursor_lazy_generation_invalidation", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_invalidates_generic_cursors_without_enumerating_or_mutating_them_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "generic cursor rows remain byte-identical and become invalid only through active-generation mismatch", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "target_first_transition_sequence_normalization", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_normalizes_only_transition_sqlite_sequence_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "missing, low, high, duplicate, and case-aliased target rows normalize once to a canonical target-first row at the retained transition maximum while every unrelated sqlite_sequence row triple remains unchanged", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "unrelated_minimum_transition_sequence_rowid_refusal", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_rejects_unrelated_minimum_transition_sequence_rowid_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "an unrelated sqlite_sequence row at the minimum SQLite rowid exhausts target-first placement and rejects atomically", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "minimum_target_alias_sequence_reuse", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_reuses_target_alias_at_minimum_sequence_rowid_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "a case-aliased target already at the minimum SQLite rowid is reused, canonicalized, and advanced to the replay high-water", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "active_transition_high_water_metadata_repair", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_repairs_active_transition_high_water_metadata_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "low and high active transition high-water metadata drift repairs to the exact retained transition maximum while preserving immutable-raw, normalized product, and logical product parity across repeat rebuild", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "empty_transition_high_water_metadata_repair", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_repairs_empty_transition_high_water_metadata_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "nonzero active transition high-water metadata on an empty source repairs to zero while preserving immutable-raw, normalized product, and logical product parity across reopen validation and repeat rebuild", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "derived_repair_and_raw_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_repairs_derived_drift_and_refuses_raw_drift_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "managed-v4 derived corruption is repaired, while immutable raw, capacity, governed catalog, or migration-ledger drift is refused before mutation", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "transition_history_gap_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_refuses_transition_history_gap_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "a retained transition-history gap is refused as addressable-transition authority drift before any rebuild-owned state mutates", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "historical_generation_lineage_corruption_refusal", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_refuses_historical_generation_lineage_corruption_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "historical generation baselines and generation-bound transition lineage are authenticated before mutation, and any mismatch is refused atomically", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_marker_open", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_marker_open restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_generation_rotation", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_generation_rotation restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_core_replay", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_core_replay restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_visibility_audit", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_visibility_audit restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_food_reset_replay", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_food_reset_replay restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_food_audit", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_food_audit restores the exact prior committed database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_after_marker_close", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "failure after_marker_close restores the exact prior committed database and leaves no marker residue", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "rollback_failure_preserves_both_errors", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_rollback_failure_preserves_primary_and_rollback_errors_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "the typed rollback error preserves both the primary rebuild failure and the SQL rollback failure", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "wal_reader_commit_visibility", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_wal_readers_observe_only_committed_generation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "concurrent WAL readers observe only the prior committed generation until rebuild commit", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "caller_inbound_foreign_key_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_rejects_caller_inbound_foreign_keys_atomically_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "caller-owned inbound foreign keys to directly or indirectly mutated parents are rejected before entropy or mutation; representative managed-parent cases cover CASCADE, SET NULL, SET DEFAULT, RESTRICT, and NO ACTION, while explicit parent-inventory cases cover the Food FTS5 virtual table, all five shadows, and sqlite_sequence, preserving caller rows, schema, triggers, side effects, and rebuild authority", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "caller_schema_inventory_capacity_exact_and_one_over", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_caller_schema_inventory_limits_are_typed_and_atomic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "bounded caller main-table and cumulative foreign-key-row inventories accept their exact limits and return typed atomic refusal one row over before entropy or mutation", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "scoped_integrity_preserves_caller_state", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_scoped_integrity_preserves_caller_state_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "unrelated caller rows, indices, foreign-key violations, and AUTOINCREMENT counters with no dependency on rebuild-owned tables remain outside rebuild authority and byte-identical", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "generation_exhaustion_preflight", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_generation_exhaustion_precedes_entropy_and_mutation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "the ninth retained generation fails before entropy, marker, sequence, raw, or derived mutation", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "generation_entropy_failure_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_entropy_failure_is_atomic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "source-generation entropy failure returns the typed error before marker, sequence, raw, or derived mutation", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "empty_source_without_transitions", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_empty_source_without_transitions_is_deterministic_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "an empty source and absent target transition sequence rebuild deterministically without creating unrelated sequence state", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_file_repair", + "execution": "delegated_rust_test", + "authority": "raw_source_rebuild_cold_file_repair_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "maintenance-only file repair restores exact managed-v4 derived corruption through a fresh governed connection while refusing nonexistent, unmanaged, and non-v4 databases without weakening ordinary constructors", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_bounded_preflight_authority_drift_refusal", + "execution": "delegated_rust_test", + "authority": "raw_source_repair_preflights_reject_bounded_authority_drift_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "bounded catalog, migration-history, temporary-schema, and encoding preflights refuse authority drift without creating rebuild state or changing persistent database authority", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_non_wal_file_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_repair_rejects_delete_mode_exact_v4_without_mutation_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "cold repair requires an existing WAL database and rejects an exact managed-v4 DELETE-mode file without changing journal mode or governed state", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_canonical_path_lock_domain_refusal_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_repair_rejects_canonical_path_lock_domain_mismatch_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "cold repair proves the caller path shares the validated SQLite writer-lock domain and rejects a mismatched canonical path without mutating either database", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "cold_post_preflight_failure_wal_state_atomicity", + "execution": "delegated_rust_test", + "authority": "raw_source_repair_post_preflight_failures_preserve_wal_and_state_v1", + "authority_path": "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + "expected_outcome": "raw reconciliation and source-capacity failures after cold preflight preserve the exact prior rebuild-owned state and WAL journal mode", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "pure_raw_snapshot_visibility_oracle", + "execution": "delegated_rust_test", + "authority": "raw_snapshot_visibility_oracle_covers_regular_replaceable_addressable_and_deletion_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "expected_outcome": "the independent pure oracle covers regular, replaceable, addressable, empty-identifier replaceable address targets, and kind-5 visibility without consulting derived SQL views", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "direct_indexed_visibility_oracle_wide_targets", + "execution": "delegated_rust_test", + "authority": "raw_snapshot_visibility_oracle_matches_wide_event_and_address_requests_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "expected_outcome": "one wide deletion request is reduced once into direct indexed event and address evidence whose per-target decisions exactly match the frozen NIP-09 evaluator without projection rescans", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "direct_indexed_visibility_oracle_protocol_matrix", + "execution": "delegated_rust_test", + "authority": "raw_snapshot_visibility_oracle_matches_all_protocol_decision_branches_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "expected_outcome": "the direct indexed reducer exactly matches all seven frozen NIP-09 outcomes, preserves stale and winning evidence, and applies authorized-evidence precedence over mismatches", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + }, + { + "id": "direct_indexed_visibility_oracle_order_repeat_invariance", + "execution": "delegated_rust_test", + "authority": "raw_snapshot_visibility_oracle_is_order_and_repeat_invariant_v1", + "authority_path": "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + "expected_outcome": "canonical exact-event and address evidence is invariant under reversed request order and repeated admitted requests, including cutoff ties", + "expected_immutable_raw_digest": null, + "expected_active_product_state_digest": null + } + ] +} diff --git a/crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs b/crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs @@ -0,0 +1,656 @@ +#![forbid(unsafe_code)] + +use radroots_event_store::{RadrootsEventIngest, RadrootsEventStore}; +use serde::Deserialize; +use serde_json::Value; +use std::collections::BTreeSet; + +const RESULT_VECTOR_EXECUTOR_ID: &str = + "radroots_event_store.raw_source_rebuild_v1.result_vector_executor.v1"; +const RESULT_VECTOR_BYTES: &[u8] = + include_bytes!("../../../contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json"); +const FOOD_FIXTURE_BYTES: &[u8] = include_bytes!("fixtures/food_availability_projection.v1.json"); + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RawSourceRebuildVector { + schema_version: u32, + contract_id: String, + delegated_suite: DelegatedSuite, + cases: Vec<VectorCase>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DelegatedSuite { + id: String, + lane: String, + package: String, + authorities: Vec<DelegatedAuthority>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DelegatedAuthority { + authority: String, + authority_path: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorCase { + id: String, + execution: String, + authority: String, + authority_path: String, + expected_outcome: String, + expected_immutable_raw_digest: Option<String>, + expected_active_product_state_digest: Option<String>, +} + +#[derive(Clone, Copy)] +struct ExpectedCase { + id: &'static str, + execution: &'static str, + authority: &'static str, + authority_path: &'static str, +} + +#[derive(Clone, Copy)] +struct ExpectedDelegatedAuthority { + authority: &'static str, + authority_path: &'static str, +} + +const DIRECT_EXECUTOR: &str = "direct_executor"; +const DELEGATED_RUST_TEST: &str = "delegated_rust_test"; +const EXECUTOR_TEST: &str = "raw_source_rebuild_v1_result_vector"; +const EXECUTOR_PATH: &str = "crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs"; +const REBUILD_TEST_PATH: &str = "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs"; +const ORACLE_TEST_PATH: &str = + "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs"; +const DELEGATED_SUITE_ID: &str = + "radroots_event_store.raw_source_rebuild_v1.delegated_rust_test_suite.v1"; +const DELEGATED_SUITE_LANE: &str = "nix run .#contract"; +const DELEGATED_SUITE_PACKAGE: &str = "radroots_event_store"; + +const EXPECTED_DELEGATED_AUTHORITIES: &[ExpectedDelegatedAuthority] = &[ + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_incremental_reopen_and_repeat_parity_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "projection_cursor_capacity_accepts_exact_and_rejects_one_over_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_invalidates_generic_cursors_without_enumerating_or_mutating_them_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_normalizes_only_transition_sqlite_sequence_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_rejects_unrelated_minimum_transition_sequence_rowid_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_reuses_target_alias_at_minimum_sequence_rowid_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_repairs_active_transition_high_water_metadata_drift_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_repairs_empty_transition_high_water_metadata_drift_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_repairs_derived_drift_and_refuses_raw_drift_atomically_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_refuses_transition_history_gap_atomically_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_refuses_historical_generation_lineage_corruption_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_rollback_failure_preserves_primary_and_rollback_errors_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_wal_readers_observe_only_committed_generation_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_rejects_caller_inbound_foreign_keys_atomically_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_caller_schema_inventory_limits_are_typed_and_atomic_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_scoped_integrity_preserves_caller_state_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_generation_exhaustion_precedes_entropy_and_mutation_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_entropy_failure_is_atomic_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_empty_source_without_transitions_is_deterministic_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_rebuild_cold_file_repair_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_repair_preflights_reject_bounded_authority_drift_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_repair_rejects_delete_mode_exact_v4_without_mutation_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_repair_rejects_canonical_path_lock_domain_mismatch_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_source_repair_post_preflight_failures_preserve_wal_and_state_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_snapshot_visibility_oracle_covers_regular_replaceable_addressable_and_deletion_v1", + authority_path: ORACLE_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_snapshot_visibility_oracle_matches_wide_event_and_address_requests_v1", + authority_path: ORACLE_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_snapshot_visibility_oracle_matches_all_protocol_decision_branches_v1", + authority_path: ORACLE_TEST_PATH, + }, + ExpectedDelegatedAuthority { + authority: "raw_snapshot_visibility_oracle_is_order_and_repeat_invariant_v1", + authority_path: ORACLE_TEST_PATH, + }, +]; + +const EXPECTED_CASES: &[ExpectedCase] = &[ + ExpectedCase { + id: "empty_source_repeat_digest_parity", + execution: DIRECT_EXECUTOR, + authority: EXECUTOR_TEST, + authority_path: EXECUTOR_PATH, + }, + ExpectedCase { + id: "signed_food_fixture_typed_digest_parity", + execution: DIRECT_EXECUTOR, + authority: EXECUTOR_TEST, + authority_path: EXECUTOR_PATH, + }, + ExpectedCase { + id: "incremental_reopen_repeat_product_parity", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_incremental_reopen_and_repeat_parity_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "projection_cursor_capacity_exact_and_one_over", + execution: DELEGATED_RUST_TEST, + authority: "projection_cursor_capacity_accepts_exact_and_rejects_one_over_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "generic_cursor_lazy_generation_invalidation", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_invalidates_generic_cursors_without_enumerating_or_mutating_them_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "target_first_transition_sequence_normalization", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_normalizes_only_transition_sqlite_sequence_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "unrelated_minimum_transition_sequence_rowid_refusal", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_rejects_unrelated_minimum_transition_sequence_rowid_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "minimum_target_alias_sequence_reuse", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_reuses_target_alias_at_minimum_sequence_rowid_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "active_transition_high_water_metadata_repair", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_repairs_active_transition_high_water_metadata_drift_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "empty_transition_high_water_metadata_repair", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_repairs_empty_transition_high_water_metadata_drift_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "derived_repair_and_raw_refusal_atomicity", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_repairs_derived_drift_and_refuses_raw_drift_atomically_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "transition_history_gap_refusal_atomicity", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_refuses_transition_history_gap_atomically_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "historical_generation_lineage_corruption_refusal", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_refuses_historical_generation_lineage_corruption_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "rollback_after_marker_open", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "rollback_after_generation_rotation", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "rollback_after_core_replay", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "rollback_after_visibility_audit", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "rollback_after_food_reset_replay", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "rollback_after_food_audit", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "rollback_after_marker_close", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_failpoints_roll_back_every_stage_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "rollback_failure_preserves_both_errors", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_rollback_failure_preserves_primary_and_rollback_errors_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "wal_reader_commit_visibility", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_wal_readers_observe_only_committed_generation_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "caller_inbound_foreign_key_refusal_atomicity", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_rejects_caller_inbound_foreign_keys_atomically_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "caller_schema_inventory_capacity_exact_and_one_over", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_caller_schema_inventory_limits_are_typed_and_atomic_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "scoped_integrity_preserves_caller_state", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_scoped_integrity_preserves_caller_state_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "generation_exhaustion_preflight", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_generation_exhaustion_precedes_entropy_and_mutation_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "generation_entropy_failure_atomicity", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_entropy_failure_is_atomic_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "empty_source_without_transitions", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_empty_source_without_transitions_is_deterministic_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "cold_file_repair", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_rebuild_cold_file_repair_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "cold_bounded_preflight_authority_drift_refusal", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_repair_preflights_reject_bounded_authority_drift_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "cold_non_wal_file_refusal_atomicity", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_repair_rejects_delete_mode_exact_v4_without_mutation_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "cold_canonical_path_lock_domain_refusal_atomicity", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_repair_rejects_canonical_path_lock_domain_mismatch_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "cold_post_preflight_failure_wal_state_atomicity", + execution: DELEGATED_RUST_TEST, + authority: "raw_source_repair_post_preflight_failures_preserve_wal_and_state_v1", + authority_path: REBUILD_TEST_PATH, + }, + ExpectedCase { + id: "pure_raw_snapshot_visibility_oracle", + execution: DELEGATED_RUST_TEST, + authority: "raw_snapshot_visibility_oracle_covers_regular_replaceable_addressable_and_deletion_v1", + authority_path: ORACLE_TEST_PATH, + }, + ExpectedCase { + id: "direct_indexed_visibility_oracle_wide_targets", + execution: DELEGATED_RUST_TEST, + authority: "raw_snapshot_visibility_oracle_matches_wide_event_and_address_requests_v1", + authority_path: ORACLE_TEST_PATH, + }, + ExpectedCase { + id: "direct_indexed_visibility_oracle_protocol_matrix", + execution: DELEGATED_RUST_TEST, + authority: "raw_snapshot_visibility_oracle_matches_all_protocol_decision_branches_v1", + authority_path: ORACLE_TEST_PATH, + }, + ExpectedCase { + id: "direct_indexed_visibility_oracle_order_repeat_invariance", + execution: DELEGATED_RUST_TEST, + authority: "raw_snapshot_visibility_oracle_is_order_and_repeat_invariant_v1", + authority_path: ORACLE_TEST_PATH, + }, +]; + +fn decode_digest(value: &str) -> [u8; 32] { + let mut bytes = [0_u8; 32]; + hex::decode_to_slice(value, &mut bytes).expect("decode governed lowercase SHA-256 digest"); + bytes +} + +fn signed_food_fixture_ingest() -> RadrootsEventIngest { + let fixture: Value = serde_json::from_slice(FOOD_FIXTURE_BYTES).expect("parse Food fixture"); + let observed = fixture["cases"] + .as_array() + .expect("Food fixture cases") + .iter() + .find(|case| case["id"].as_str() == Some("visible_food_availability_projects_and_searches")) + .and_then(|case| case["events"].as_array()) + .and_then(|events| events.first()) + .expect("signed Food fixture event"); + let raw_json = serde_json::to_string(&observed["event"]).expect("serialize Food fixture event"); + let observed_at_ms = observed["observed_at_ms"] + .as_i64() + .expect("Food fixture observation time"); + RadrootsEventIngest::from_raw_json(raw_json, observed_at_ms) + .expect("verify signed Food fixture event") +} + +#[tokio::test] +async fn raw_source_rebuild_v1_result_vector() { + assert_eq!( + RESULT_VECTOR_EXECUTOR_ID, + "radroots_event_store.raw_source_rebuild_v1.result_vector_executor.v1" + ); + let vector: RawSourceRebuildVector = + serde_json::from_slice(RESULT_VECTOR_BYTES).expect("parse raw-source rebuild vector"); + assert_eq!(vector.schema_version, 1); + assert_eq!( + vector.contract_id, + "radroots_event_store.raw_source_rebuild_v1" + ); + assert_eq!(vector.delegated_suite.id, DELEGATED_SUITE_ID); + assert_eq!(vector.delegated_suite.lane, DELEGATED_SUITE_LANE); + assert_eq!(vector.delegated_suite.package, DELEGATED_SUITE_PACKAGE); + assert_eq!(vector.cases.len(), EXPECTED_CASES.len()); + + let mut case_ids = BTreeSet::new(); + for (case, expected) in vector.cases.iter().zip(EXPECTED_CASES) { + assert!( + case_ids.insert(case.id.as_str()), + "duplicate case {}", + case.id + ); + assert_eq!(case.id, expected.id); + assert_eq!(case.execution, expected.execution); + assert_eq!(case.authority, expected.authority); + assert_eq!(case.authority_path, expected.authority_path); + assert!(!case.expected_outcome.is_empty()); + for digest in [ + case.expected_immutable_raw_digest.as_deref(), + case.expected_active_product_state_digest.as_deref(), + ] + .into_iter() + .flatten() + { + assert_eq!(digest.len(), 64); + assert!( + digest + .as_bytes() + .iter() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)) + ); + } + } + + assert_eq!( + vector.delegated_suite.authorities.len(), + EXPECTED_DELEGATED_AUTHORITIES.len() + ); + let mut delegated_suite_authorities = BTreeSet::new(); + for (actual, expected) in vector + .delegated_suite + .authorities + .iter() + .zip(EXPECTED_DELEGATED_AUTHORITIES) + { + assert_eq!(actual.authority, expected.authority); + assert_eq!(actual.authority_path, expected.authority_path); + assert!( + delegated_suite_authorities + .insert((actual.authority_path.as_str(), actual.authority.as_str(),)), + "duplicate delegated suite authority {}::{}", + actual.authority_path, + actual.authority + ); + } + let delegated_case_authorities = vector + .cases + .iter() + .filter(|case| case.execution == DELEGATED_RUST_TEST) + .map(|case| (case.authority_path.as_str(), case.authority.as_str())) + .collect::<BTreeSet<_>>(); + assert_eq!(delegated_suite_authorities, delegated_case_authorities); + + let direct_case = &vector.cases[0]; + assert_eq!(direct_case.id, "empty_source_repeat_digest_parity"); + let expected_immutable_raw_digest = decode_digest( + direct_case + .expected_immutable_raw_digest + .as_deref() + .expect("direct case immutable-raw digest"), + ); + let expected_active_product_state_digest = decode_digest( + direct_case + .expected_active_product_state_digest + .as_deref() + .expect("direct case active-product-state digest"), + ); + + let store = RadrootsEventStore::open_memory() + .await + .expect("open managed-v4 in-memory store"); + let initial_generation = store + .source_generation() + .await + .expect("initial source generation"); + let initial_capacity = store + .source_capacity_v1() + .await + .expect("initial source capacity"); + assert_eq!(initial_capacity.raw_event_count(), 0); + assert_eq!(initial_capacity.raw_tag_count(), 0); + assert_eq!(initial_capacity.raw_event_text_bytes(), 0); + assert_eq!(initial_capacity.raw_tag_text_bytes(), 0); + assert_eq!(initial_capacity.raw_high_water_seq(), 0); + + let first = store + .rebuild_from_raw_v1() + .await + .expect("first empty-source rebuild"); + assert_eq!(first.prior_source_generation(), initial_generation); + assert_ne!(first.new_source_generation(), initial_generation); + assert_eq!(first.source_capacity().raw_event_count(), 0); + assert_eq!(first.source_capacity().raw_tag_count(), 0); + assert_eq!(first.source_capacity().raw_event_text_bytes(), 0); + assert_eq!(first.source_capacity().raw_tag_text_bytes(), 0); + assert_eq!(first.raw_high_water_seq(), 0); + + let second = store + .rebuild_from_raw_v1() + .await + .expect("second empty-source rebuild"); + assert_eq!( + second.prior_source_generation(), + first.new_source_generation() + ); + assert_ne!( + second.new_source_generation(), + first.new_source_generation() + ); + assert_eq!(second.source_capacity().raw_event_count(), 0); + assert_eq!(second.source_capacity().raw_tag_count(), 0); + assert_eq!(second.source_capacity().raw_event_text_bytes(), 0); + assert_eq!(second.source_capacity().raw_tag_text_bytes(), 0); + assert_eq!(second.raw_high_water_seq(), 0); + assert_eq!(second.immutable_raw_digest(), first.immutable_raw_digest()); + assert_eq!( + second.active_product_state_digest(), + first.active_product_state_digest() + ); + assert_eq!( + first.immutable_raw_digest().as_bytes(), + &expected_immutable_raw_digest, + "actual empty immutable-raw digest: {}", + hex::encode(first.immutable_raw_digest().as_bytes()) + ); + assert_eq!( + first.active_product_state_digest().as_bytes(), + &expected_active_product_state_digest, + "actual empty active-product digest: {}", + hex::encode(first.active_product_state_digest().as_bytes()) + ); + + let food_case = &vector.cases[1]; + assert_eq!(food_case.id, "signed_food_fixture_typed_digest_parity"); + let expected_food_raw_digest = decode_digest( + food_case + .expected_immutable_raw_digest + .as_deref() + .expect("Food case immutable-raw digest"), + ); + let expected_food_product_digest = decode_digest( + food_case + .expected_active_product_state_digest + .as_deref() + .expect("Food case active-product-state digest"), + ); + let food_store = RadrootsEventStore::open_memory() + .await + .expect("open Food digest store"); + food_store + .ingest_event(signed_food_fixture_ingest()) + .await + .expect("ingest signed Food fixture"); + let food_first = food_store + .rebuild_from_raw_v1() + .await + .expect("first Food fixture rebuild"); + assert_eq!(food_first.source_capacity().raw_event_count(), 1); + assert!(food_first.source_capacity().raw_tag_count() > 0); + assert_eq!(food_first.raw_high_water_seq(), 1); + assert_eq!( + food_first.immutable_raw_digest().as_bytes(), + &expected_food_raw_digest, + "actual Food immutable-raw digest: {}", + hex::encode(food_first.immutable_raw_digest().as_bytes()) + ); + assert_eq!( + food_first.active_product_state_digest().as_bytes(), + &expected_food_product_digest, + "actual Food active-product digest: {}", + hex::encode(food_first.active_product_state_digest().as_bytes()) + ); + let food_second = food_store + .rebuild_from_raw_v1() + .await + .expect("second Food fixture rebuild"); + assert_ne!( + food_second.new_source_generation(), + food_first.new_source_generation() + ); + assert_eq!( + food_second.immutable_raw_digest(), + food_first.immutable_raw_digest() + ); + assert_eq!( + food_second.active_product_state_digest(), + food_first.active_product_state_digest() + ); +} diff --git a/tools/xtask/Cargo.toml b/tools/xtask/Cargo.toml @@ -7,6 +7,10 @@ description = "Workspace task runner for Radroots Rust crates" readme = "README" license.workspace = true publish = false +autolib = false +autotests = false +autoexamples = false +autobenches = false authors = ["Tyson Lupul <tyson@radroots.org>"] [dependencies] diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -6,6 +6,7 @@ mod comment_authority; mod deletion_authority; mod food_availability_projection; mod nip09_reconciliation; +mod raw_source_rebuild; mod registry_v7; mod source_maintenance; @@ -15,6 +16,9 @@ pub(crate) use food_availability_projection::{ pub(crate) use nip09_reconciliation::{ validate_nip09_reconciliation_manifest, write_nip09_reconciliation_manifest, }; +pub(crate) use raw_source_rebuild::{ + validate_raw_source_rebuild_manifest, write_raw_source_rebuild_manifest, +}; pub(crate) use registry_v7::{ validate_event_contract_registry_v7_inventory, write_event_contract_registry_v7_inventory, }; @@ -49,6 +53,7 @@ pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), S validate_nip09_reconciliation_manifest(workspace_root)?; validate_food_availability_projection_manifest(workspace_root)?; validate_source_maintenance_manifest(workspace_root)?; + validate_raw_source_rebuild_manifest(workspace_root)?; validate_knowledge_contract_manifest(workspace_root) } @@ -62,10 +67,13 @@ const FOOD_AVAILABILITY_PROJECTION_CONFORMANCE_VECTOR_RELATIVE: &str = "contracts/conformance/vectors/event_store/food_availability_projection.v1.json"; const SOURCE_MAINTENANCE_CONFORMANCE_VECTOR_RELATIVE: &str = "contracts/conformance/vectors/event_store/source_maintenance.v1.json"; -const SPECIALIZED_CONFORMANCE_VECTOR_RELATIVES: [&str; 3] = [ +const RAW_SOURCE_REBUILD_CONFORMANCE_VECTOR_RELATIVE: &str = + "contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json"; +const SPECIALIZED_CONFORMANCE_VECTOR_RELATIVES: [&str; 4] = [ NIP09_RECONCILIATION_CONFORMANCE_VECTOR_RELATIVE, FOOD_AVAILABILITY_PROJECTION_CONFORMANCE_VECTOR_RELATIVE, SOURCE_MAINTENANCE_CONFORMANCE_VECTOR_RELATIVE, + RAW_SOURCE_REBUILD_CONFORMANCE_VECTOR_RELATIVE, ]; const KNOWLEDGE_MANIFEST_RELATIVE: &str = "contracts/knowledge/knowledge_event_contract_manifest.v2.json"; @@ -86,7 +94,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); 22] = [ +const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 23] = [ ( "contracts/conformance/vectors/blossom/bud11_claims.v1.json", "crates/blossom/tests/fixtures/bud11_claims.v1.json", @@ -136,6 +144,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 22] = [ "crates/event_store/tests/fixtures/source_maintenance.v1.json", ), ( + RAW_SOURCE_REBUILD_CONFORMANCE_VECTOR_RELATIVE, + "crates/event_store/tests/fixtures/raw_source_rebuild.v1.json", + ), + ( "contracts/conformance/vectors/events/operational_listing_tags_full.v1.json", "crates/event_codec/tests/fixtures/operational_listing_tags_full.v1.json", ), diff --git a/tools/xtask/src/contract/food_availability_projection.rs b/tools/xtask/src/contract/food_availability_projection.rs @@ -3992,7 +3992,8 @@ mod tests { #[test] fn downstream_nip09_only_supersession_is_transitively_validated() { - const SOURCE_MAINTENANCE_SUPERSEDED_PATHS: &[&str] = &[ + const CURRENT_SUCCESSOR_SUPERSEDED_PATHS: &[&str] = &[ + "crates/event_store/Cargo.toml", "crates/event_store/src/error.rs", "crates/event_store/src/generated.rs", "crates/event_store/src/lib.rs", @@ -4001,6 +4002,7 @@ mod tests { "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/food_availability_projection_v1.rs", "crates/event_store/src/store/protocol_reconciliation_v1.rs", ]; @@ -4023,11 +4025,11 @@ mod tests { ); validate_food_availability_projection_predecessor_production_sources_under_lock( &root, - SOURCE_MAINTENANCE_SUPERSEDED_PATHS, + CURRENT_SUCCESSOR_SUPERSEDED_PATHS, ) .expect("Food and transitive NIP-09 successor source coverage"); - let mut duplicate = SOURCE_MAINTENANCE_SUPERSEDED_PATHS.to_vec(); + let mut duplicate = CURRENT_SUCCESSOR_SUPERSEDED_PATHS.to_vec(); duplicate.push("crates/event_store/src/store/protocol_reconciliation_v1.rs"); let error = validate_food_availability_projection_predecessor_production_sources_under_lock( @@ -4036,7 +4038,7 @@ mod tests { .expect_err("duplicate transitive supersession must fail"); assert!(error.contains("must be unique"), "{error}"); - let mut unknown = SOURCE_MAINTENANCE_SUPERSEDED_PATHS.to_vec(); + let mut unknown = CURRENT_SUCCESSOR_SUPERSEDED_PATHS.to_vec(); unknown.push("crates/event_store/src/store/not_predecessor_bound.rs"); let error = validate_food_availability_projection_predecessor_production_sources_under_lock( diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs @@ -184,8 +184,9 @@ const CORE_CARGO_MANIFEST_RELATIVE: &str = "crates/core/Cargo.toml"; const BLOSSOM_CARGO_MANIFEST_RELATIVE: &str = "crates/blossom/Cargo.toml"; const TRANSPORT_CARGO_MANIFEST_RELATIVE: &str = "crates/transport/Cargo.toml"; const CARGO_CONFIG_RELATIVE: &str = ".cargo/config.toml"; -const GOVERNED_WORKSPACE_DEPENDENCY_NAMES: [&str; 23] = [ +const GOVERNED_WORKSPACE_DEPENDENCY_NAMES: [&str; 24] = [ "dto_bindgen", + "futures", "getrandom", "hex", "jiff-tzdb", @@ -236,7 +237,7 @@ const GOVERNED_DEPENDENCY_TABLE_SHA256: [(&str, &str); 8] = [ ), ( "Cargo.toml#governed-workspace-dependencies", - "0aa0aeb7988745aad1101c820a340c8ac04a5833c2ec7f7c997b5d9dfac010a3", + "edb48180d3cc3d00fead18984159487bb07afedeb646249a84c1d64ec0529e24", ), ( "Cargo.toml#patch", @@ -395,6 +396,17 @@ const SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS: [&str; 8] = [ ]; const SUCCESSOR_08D_SOURCE_PATHS: [&str; 1] = ["crates/event_store/src/source_maintenance_v1.rs"]; const SUCCESSOR_08D_LIB_MODULES: [&str; 1] = ["source_maintenance_v1"]; +const RAW_SOURCE_REBUILD_SOURCE_RELATIVE: &str = + "crates/event_store/src/nip09/reconciliation_v1/raw_source_rebuild.rs"; +const RAW_SOURCE_REBUILD_TEST_SOURCE_RELATIVE: &str = + "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs"; +const SUCCESSOR_08D1_EXCLUSIVE_SOURCE_PATHS: [&str; 5] = [ + "crates/event_store/src/generated/raw_source_rebuild_manifest.rs", + "crates/event_store/src/model/raw_source_rebuild_v1.rs", + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + RAW_SOURCE_REBUILD_TEST_SOURCE_RELATIVE, +]; const EVENT_STORE_FIXED_PUBLIC_REEXPORTS: [&str; 40] = [ "error::RadrootsEventStoreError", "error::RadrootsEventStoreReconciliationResource", @@ -482,6 +494,14 @@ const SUCCESSOR_08D_PUBLIC_REEXPORTS: [&str; 7] = [ "error::RadrootsEventStoreSourceCapacityResourceV1", "source_maintenance_v1::RadrootsEventStoreSourceCapacityV1", ]; +const SUCCESSOR_08D1_PUBLIC_REEXPORTS: [&str; 6] = [ + "error::RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1", + "error::RadrootsEventStoreCallerInboundForeignKeyV1", + "error::RadrootsEventStoreRawSourceRebuildDriftV1", + "model::RadrootsEventStoreActiveProductStateDigestV1", + "model::RadrootsEventStoreImmutableRawDigestV1", + "model::RadrootsEventStoreRawSourceRebuildReportV1", +]; const POST_CORE_STORAGE_METHODS: [&str; 4] = [ "new", "quarantine_trade", @@ -2468,35 +2488,7 @@ pub(super) fn validate_nip09_predecessor_production_sources_under_lock( |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(), - ); - } - } + validate_predecessor_impl_resolution_authority(workspace_root, &manifest, superseded_paths)?; let post_core_paths = [ POST_CORE_CAPABILITIES_SOURCE_RELATIVE, @@ -2527,6 +2519,74 @@ pub(super) fn validate_nip09_predecessor_production_sources_under_lock( Ok(()) } +fn validate_predecessor_impl_resolution_authority( + workspace_root: &Path, + manifest: &Nip09ReconciliationManifest, + superseded_paths: &[&str], +) -> Result<(), String> { + let superseded = superseded_paths.iter().copied().collect::<BTreeSet<_>>(); + 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() { + return Ok(()); + } + + let excluded_paths = superseded_paths + .iter() + .copied() + .chain(SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS) + .chain(SUCCESSOR_08D_SOURCE_PATHS) + .chain(SUCCESSOR_08D1_EXCLUSIVE_SOURCE_PATHS) + .collect::<BTreeSet<_>>() + .into_iter() + .collect::<Vec<_>>(); + let predecessor_protected_members = manifest + .impl_resolution_witness + .impls + .iter() + .filter_map(|item| item.member.as_ref()) + .filter(|member| member.as_str() != "<macro>") + .cloned() + .collect::<BTreeSet<_>>(); + let current_impls = describe_impl_resolution_witness_excluding_paths( + workspace_root, + &excluded_paths, + &manifest.impl_resolution_witness.protected_self_types, + &predecessor_protected_members, + )? + .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 Ok(()); + } + + let current = current_impls.iter().collect::<BTreeSet<_>>(); + let expected = expected_impls.iter().collect::<BTreeSet<_>>(); + let missing = expected.difference(&current).copied().collect::<Vec<_>>(); + let unexpected = current.difference(&expected).copied().collect::<Vec<_>>(); + Err(format!( + "unchanged predecessor impl-resolution authority drifted from the immutable manifest: expected {} entries, found {}; missing {missing:?}; unexpected {unexpected:?}", + expected_impls.len(), + current_impls.len(), + )) +} + fn require_predecessor_frozen_source_match( expected: &FrozenSourceDescriptor, current: &FrozenSourceDescriptor, @@ -4734,6 +4794,15 @@ fn describe_source_route_witness( fn describe_impl_resolution_witness( workspace_root: &Path, ) -> Result<ImplResolutionWitnessDescriptor, String> { + describe_impl_resolution_witness_excluding_paths(workspace_root, &[], &[], &BTreeSet::new()) +} + +fn describe_impl_resolution_witness_excluding_paths( + workspace_root: &Path, + excluded_paths: &[&str], + frozen_protected_self_types: &[String], + frozen_protected_members: &BTreeSet<String>, +) -> Result<ImplResolutionWitnessDescriptor, String> { use syn::visit::Visit; fn impl_member_name(item: &syn::ImplItem) -> Option<String> { @@ -4938,6 +5007,7 @@ fn describe_impl_resolution_witness( } } + let excluded_paths = excluded_paths.iter().copied().collect::<BTreeSet<_>>(); let mut protected_paths = FROZEN_SOURCE_SPECS .iter() .map(|spec| spec.path) @@ -4955,6 +5025,7 @@ fn describe_impl_resolution_witness( POST_CORE_CAPABILITIES_SOURCE_RELATIVE, POST_CORE_DISPATCHER_SOURCE_RELATIVE, ]) + .filter(|relative| !excluded_paths.contains(relative)) .collect::<Vec<_>>(); protected_paths.sort_unstable(); protected_paths.dedup(); @@ -4967,6 +5038,9 @@ fn describe_impl_resolution_witness( let file = parse_canonical_production_rust(relative, &bytes)?; declaration_audit.visit_file(&file); } + declaration_audit + .names + .extend(frozen_protected_self_types.iter().cloned()); if declaration_audit.names.is_empty() { return Err("protected v1 impl-resolution type inventory must not be empty".to_owned()); } @@ -4979,6 +5053,9 @@ fn describe_impl_resolution_witness( }; for root in IMPL_RESOLUTION_SOURCE_ROOTS { for relative in governed_regular_file_inventory(workspace_root, root)? { + if excluded_paths.contains(relative.as_str()) { + continue; + } if !relative.ends_with(".rs") { return Err(format!( "{root} impl-resolution source inventory may contain only Rust files; found {relative}" @@ -5031,6 +5108,7 @@ fn describe_impl_resolution_witness( POST_CORE_EXTENSION_SOURCE_RELATIVE, POST_CORE_STORAGE_SOURCE_RELATIVE, ]) + .filter(|relative| !excluded_paths.contains(relative)) .collect::<Vec<_>>(); protected_member_paths.sort_unstable(); protected_member_paths.dedup(); @@ -5039,62 +5117,67 @@ fn describe_impl_resolution_witness( let file = parse_canonical_production_rust(relative, &bytes)?; protected_member_audit.visit_file(&file); } + protected_member_audit + .names + .extend(frozen_protected_members.iter().cloned()); - let store_bytes = read_regular_file(workspace_root, EVENT_STORE_STORE_SOURCE_RELATIVE)?; - let store_file = - parse_canonical_production_rust(EVENT_STORE_STORE_SOURCE_RELATIVE, &store_bytes)?; - let store_free_functions = store_file - .items - .iter() - .filter_map(|item| match item { - syn::Item::Fn(function) => Some((function.sig.ident.to_string(), function)), - _ => None, - }) - .collect::<BTreeMap<_, _>>(); - let mut local_resolution_queue = VecDeque::new(); - for spec in RUST_ITEM_WITNESS_ROOT_SPECS { - match spec.callable { - RustWitnessCallable::Associated { owner, name } => { - let function = exact_associated_function( - EVENT_STORE_STORE_SOURCE_RELATIVE, - &store_file, - owner, - name, - )?; - protected_member_audit.visit_impl_item_fn(function); - local_resolution_queue.extend( - WitnessedFunction::Associated(function) - .collect_call_routes() - .into_iter() - .filter_map(|route| route.strip_prefix("fn:").map(str::to_owned)) - .filter(|route| !route.contains("::")) - .filter(|route| store_free_functions.contains_key(route)), - ); - } - RustWitnessCallable::Free { name } => { - local_resolution_queue.push_back(name.to_owned()); + if !excluded_paths.contains(EVENT_STORE_STORE_SOURCE_RELATIVE) { + let store_bytes = read_regular_file(workspace_root, EVENT_STORE_STORE_SOURCE_RELATIVE)?; + let store_file = + parse_canonical_production_rust(EVENT_STORE_STORE_SOURCE_RELATIVE, &store_bytes)?; + let store_free_functions = store_file + .items + .iter() + .filter_map(|item| match item { + syn::Item::Fn(function) => Some((function.sig.ident.to_string(), function)), + _ => None, + }) + .collect::<BTreeMap<_, _>>(); + let mut local_resolution_queue = VecDeque::new(); + for spec in RUST_ITEM_WITNESS_ROOT_SPECS { + match spec.callable { + RustWitnessCallable::Associated { owner, name } => { + let function = exact_associated_function( + EVENT_STORE_STORE_SOURCE_RELATIVE, + &store_file, + owner, + name, + )?; + protected_member_audit.visit_impl_item_fn(function); + local_resolution_queue.extend( + WitnessedFunction::Associated(function) + .collect_call_routes() + .into_iter() + .filter_map(|route| route.strip_prefix("fn:").map(str::to_owned)) + .filter(|route| !route.contains("::")) + .filter(|route| store_free_functions.contains_key(route)), + ); + } + RustWitnessCallable::Free { name } => { + local_resolution_queue.push_back(name.to_owned()); + } } } - } - let mut visited_local_resolution_functions = BTreeSet::new(); - while let Some(name) = local_resolution_queue.pop_front() { - if !visited_local_resolution_functions.insert(name.clone()) { - continue; + let mut visited_local_resolution_functions = BTreeSet::new(); + while let Some(name) = local_resolution_queue.pop_front() { + if !visited_local_resolution_functions.insert(name.clone()) { + continue; + } + let function = store_free_functions.get(&name).ok_or_else(|| { + format!( + "{EVENT_STORE_STORE_SOURCE_RELATIVE} v1 resolution closure references missing local function `{name}`" + ) + })?; + protected_member_audit.visit_item_fn(function); + local_resolution_queue.extend( + WitnessedFunction::Free(function) + .collect_call_routes() + .into_iter() + .filter_map(|route| route.strip_prefix("fn:").map(str::to_owned)) + .filter(|route| !route.contains("::")) + .filter(|route| store_free_functions.contains_key(route)), + ); } - let function = store_free_functions.get(&name).ok_or_else(|| { - format!( - "{EVENT_STORE_STORE_SOURCE_RELATIVE} v1 resolution closure references missing local function `{name}`" - ) - })?; - protected_member_audit.visit_item_fn(function); - local_resolution_queue.extend( - WitnessedFunction::Free(function) - .collect_call_routes() - .into_iter() - .filter_map(|route| route.strip_prefix("fn:").map(str::to_owned)) - .filter(|route| !route.contains("::")) - .filter(|route| store_free_functions.contains_key(route)), - ); } for spec in ENTRY_POINT_SPECS { if let CallableSpec::Associated { name, .. } = spec.callable { @@ -5889,7 +5972,6 @@ fn validate_event_store_schema_import_authority( relative, file, &[ - "use crate::RadrootsEventStoreError;", r#"use crate::migrations::{ EVENT_STORE_LEDGER_CREATE_DDL, EVENT_STORE_LEDGER_DDL, EVENT_STORE_LEDGER_NAME, EVENT_STORE_MIGRATIONS, EventStoreMigration, EventStoreMigrationHook, @@ -5898,6 +5980,7 @@ fn validate_event_store_schema_import_authority( migration_for_version, sqlite_identifier_starts_with, validate_embedded_migration_registry, validate_migration_registry, };"#, + "use crate::{RadrootsEventStoreError, RadrootsEventStoreRawSourceRebuildDriftV1};", "use sha2::{Digest, Sha256};", "use sqlx::{Row, Sqlite, SqliteConnection, SqlitePool, Transaction};", "use std::collections::{BTreeMap, BTreeSet};", @@ -5994,6 +6077,7 @@ 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) + .chain([RAW_SOURCE_REBUILD_TEST_SOURCE_RELATIVE]) .map(str::to_owned) .collect::<BTreeSet<_>>(); let actual_module_sources = actual_module_sources.into_iter().collect::<BTreeSet<_>>(); @@ -6143,6 +6227,16 @@ fn validate_privileged_store_authority(workspace_root: &Path) -> Result<(), Stri ), ( EVENT_STORE_STORE_SOURCE_RELATIVE, + "free:prepare_raw_source_repair_connection_v1", + "validate_main_database_encoding", + ), + ( + EVENT_STORE_STORE_SOURCE_RELATIVE, + "free:validate_raw_source_repair_canonical_lock_domain_v1", + "validate_main_database_encoding", + ), + ( + EVENT_STORE_STORE_SOURCE_RELATIVE, "free:ingest_event_in_transaction", "crate::schema::validate_event_store_temp_schema", ), @@ -6247,6 +6341,12 @@ fn validate_event_store_privileged_terminal_authority(workspace_root: &Path) -> ] .map(str::to_owned) .to_vec(), + RAW_SOURCE_REBUILD_TEST_SOURCE_RELATIVE => [ + "include_bytes!(\"../../tests/fixtures/food_availability_projection.v1.json\")", + "include_bytes!(\"../../tests/fixtures/nip09_reconciliation.v1.json\")", + ] + .map(str::to_owned) + .to_vec(), _ => Vec::new(), }; validate_compiler_macro_inputs(&relative, &file, &expected_macro_inputs)?; @@ -6332,6 +6432,22 @@ fn validate_event_store_privileged_terminal_authority(workspace_root: &Path) -> EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE, "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1", ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "crate::source_maintenance_v1::preflight_source_generation_append_v1", + ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1", + ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "crate::source_maintenance_v1::validate_source_capacity_authority_full_v1", + ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "super::validate_active_hook_state_fast", + ), ] .into_iter() .map(|(relative, route)| (relative.to_owned(), route.to_owned())) @@ -6475,6 +6591,16 @@ fn validate_event_store_privileged_terminal_authority(workspace_root: &Path) -> "crate::schema::validate_event_store_temp_schema", ), ( + EVENT_STORE_STORE_SOURCE_RELATIVE, + "free:prepare_raw_source_repair_connection_v1", + "validate_main_database_encoding", + ), + ( + EVENT_STORE_STORE_SOURCE_RELATIVE, + "free:validate_raw_source_repair_canonical_lock_domain_v1", + "validate_main_database_encoding", + ), + ( POST_CORE_CAPABILITIES_SOURCE_RELATIVE, "associated:apply_v1", "PostCoreStorageV1::new", @@ -6624,6 +6750,36 @@ fn validate_event_store_privileged_terminal_authority(workspace_root: &Path) -> "free:read_protocol_post_extension_authority_seal", "validate_source_capacity_authority_fast_v1", ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "free:rebuild_from_raw_v1_in_transaction_inner", + "crate::source_maintenance_v1::bind_source_capacity_to_generation_v1", + ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "free:rebuild_from_raw_v1_in_transaction_inner", + "preflight_source_generation_append_v1", + ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "free:rebuild_from_raw_v1_in_transaction_inner", + "validate_active_hook_state_fast", + ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "free:rebuild_from_raw_v1_in_transaction_inner", + "validate_source_capacity_authority_fast_v1", + ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "free:rebuild_from_raw_v1_in_transaction_inner", + "validate_source_capacity_authority_full_v1", + ), + ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "free:validate_source_lineage_for_rebuild_v1", + "validate_source_capacity_authority_fast_v1", + ), ] .into_iter() .map(|(relative, function, route)| PrivilegedStoreCallSite { @@ -6679,6 +6835,16 @@ fn validate_event_store_module_source_graph( modules: Vec::new(), }; audit.visit_file(file); + if relative == "crates/event_store/src/nip09/reconciliation_v1.rs" { + let expected = ["modraw_source_rebuild;", "modvisibility_oracle_v1;"]; + if audit.modules == expected { + return Ok(()); + } + return Err(format!( + "{relative} raw-source rebuild module graph drifted: expected {expected:?}, found {:?}", + audit.modules + )); + } if !audit.modules.is_empty() { return Err(format!( "{relative} event-store production module source graph is closed outside governed facade roots; found {:?}", @@ -6741,17 +6907,29 @@ fn validate_event_store_trait_impl_authority( "core::fmt::Display", "RadrootsEventStoreSourceCapacityResourceV1", ), + ( + "core::fmt::Display", + "RadrootsEventStoreRawSourceRebuildDriftV1", + ), + ( + "core::fmt::Display", + "RadrootsEventStoreCallerInboundForeignKeyV1", + ), ("From<RadrootsTransportError>", "RadrootsEventStoreError"), ], - "crates/event_store/src/nip09/reconciliation_v1.rs" => &[ - ("SourceGenerationProvider", "OsSourceGenerationProvider"), - ("Iterator", "MergedRequestIndices<'_>"), - ], + "crates/event_store/src/nip09/reconciliation_v1.rs" => { + &[("SourceGenerationProvider", "OsSourceGenerationProvider")] + } "crates/event_store/src/model.rs" => &[ ("AsRef<str>", "RadrootsTransportObservationMessage"), ("core::ops::Deref", "RadrootsTransportObservationMessage"), ], RESULT_VECTOR_EXECUTOR_RELATIVE => &[("SourceGenerationProvider", "FixedGeneration")], + RAW_SOURCE_REBUILD_TEST_SOURCE_RELATIVE => &[ + ("SourceGenerationProvider", "FixedGeneration"), + ("SourceGenerationProvider", "PanickingGeneration"), + ("SourceGenerationProvider", "FailingGeneration"), + ], _ => &[], }; let actual_trait_impls = audit @@ -6765,7 +6943,10 @@ fn validate_event_store_trait_impl_authority( )); } let expected_inherent_self_types: &[&str] = match relative { - "crates/event_store/src/error.rs" => &["RadrootsEventStoreSourceCapacityResourceV1"], + "crates/event_store/src/error.rs" => &[ + "RadrootsEventStoreSourceCapacityResourceV1", + "RadrootsEventStoreRawSourceRebuildDriftV1", + ], EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE => &["EventStoreMigrationHook"], "crates/event_store/src/model.rs" => &[ "RadrootsTransportObservationMessage", @@ -6786,14 +6967,22 @@ fn validate_event_store_trait_impl_authority( "RadrootsRawHeadDecision", "RadrootsEventStoreSourceGeneration", ], + "crates/event_store/src/model/raw_source_rebuild_v1.rs" => &[ + "RadrootsEventStoreImmutableRawDigestV1", + "RadrootsEventStoreActiveProductStateDigestV1", + "RadrootsEventStoreRawSourceRebuildReportV1", + ], "crates/event_store/src/nip09/reconciliation_v1.rs" => &[ "ReconciliationCapacityLimits", "ReconciliationCapacity", "EventAdmission", - "RequestIndex<'a>", - "MergedRequestIndices<'a>", + "RequestIndex", "TransitionOrigin", ], + RAW_SOURCE_REBUILD_SOURCE_RELATIVE => &["RawSourceRebuildCallerSchemaLimitsV1"], + "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" => { + &["OracleRequestIndexV1<'a>"] + } EVENT_STORE_STORE_SOURCE_RELATIVE => &["RadrootsEventStore"], "crates/event_store/src/source_maintenance_v1.rs" => { &["RadrootsEventStoreSourceCapacityV1"] @@ -7346,6 +7535,18 @@ fn is_approved_privileged_terminal_import(relative: &str, route: &str) -> bool { ) | ( EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE, "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1" + ) | ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "crate::source_maintenance_v1::preflight_source_generation_append_v1" + ) | ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1" + ) | ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "crate::source_maintenance_v1::validate_source_capacity_authority_full_v1" + ) | ( + RAW_SOURCE_REBUILD_SOURCE_RELATIVE, + "super::validate_active_hook_state_fast" ) ) } @@ -7480,6 +7681,7 @@ fn validate_event_store_lib_resolution_authority( if !inherited_current && !SUCCESSOR_08C_PUBLIC_REEXPORTS.contains(&route.as_str()) && !SUCCESSOR_08D_PUBLIC_REEXPORTS.contains(&route.as_str()) + && !SUCCESSOR_08D1_PUBLIC_REEXPORTS.contains(&route.as_str()) { return Err(format!( "{relative} public export inventory is closed for this contract version; found unsupported reexport `{route}`" @@ -7493,6 +7695,7 @@ fn validate_event_store_lib_resolution_authority( .filter(|route| !SUCCESSOR_08D_RETIRED_PUBLIC_REEXPORTS.contains(route)) .chain(SUCCESSOR_08C_PUBLIC_REEXPORTS) .chain(SUCCESSOR_08D_PUBLIC_REEXPORTS) + .chain(SUCCESSOR_08D1_PUBLIC_REEXPORTS) .map(str::to_owned) .collect::<BTreeSet<_>>(); let actual_use_set = actual_uses.iter().cloned().collect::<BTreeSet<_>>(); @@ -7846,6 +8049,22 @@ impl<'ast> syn::visit::Visit<'ast> for PrivilegedStoreReferenceAudit<'_> { } fn visit_item_enum(&mut self, item: &'ast syn::ItemEnum) { + if self.relative == EVENT_STORE_STORE_SOURCE_RELATIVE + && item.ident == "PoolTempSchemaPolicy" + { + let expected = syn::parse_str::<syn::ItemEnum>( + r#"#[derive(Clone, Copy)] + enum PoolTempSchemaPolicy { + Standard, + RawSourceRepairV1, + }"#, + ) + .expect("parse governed pool TEMP-schema policy"); + if compact_tokens(item) != compact_tokens(&expected) { + self.fail("raw-source rebuild pool TEMP-schema policy drifted"); + } + return; + } if is_privileged_store_value_binding(&item.ident.to_string()) { self.fail(format!( "shadows privileged authority with enum `{}`", @@ -12495,7 +12714,6 @@ fn validate_sqlite_encoding_preflight_authority( file_backed: bool, ) -> Result<(), RadrootsEventStoreError> { let max_connections = pool.options().get_max_connections(); - let existing_options = pool.connect_options(); if !file_backed && max_connections != 1 { return Err(RadrootsEventStoreError::UnsafeInMemoryPoolConnectionCount { actual: max_connections, @@ -12517,19 +12735,6 @@ fn validate_sqlite_encoding_preflight_authority( } validate_main_database_encoding(connection).await?; crate::schema::validate_event_store_temp_schema(connection).await?; - } - - let mut connect_options = existing_options - .as_ref() - .clone() - .foreign_keys(true) - .busy_timeout(Duration::from_millis(5_000)); - if file_backed { - connect_options = connect_options.journal_mode(SqliteJournalMode::Wal); - } - pool.set_connect_options(connect_options); - - for connection in &mut connections { sqlx::query("PRAGMA foreign_keys = ON") .execute(&mut **connection) .await?; @@ -12540,12 +12745,24 @@ fn validate_sqlite_encoding_preflight_authority( configure_file_journal_mode(connection).await?; } } + let existing_options = pool.connect_options(); + let connect_options = existing_options + .as_ref() + .clone() + .foreign_keys(true) + .busy_timeout(Duration::from_millis(5_000)); + let connect_options = if file_backed { + connect_options.journal_mode(SqliteJournalMode::Wal) + } else { + connect_options + }; + pool.set_connect_options(connect_options); Ok(()) } "#; if compact_tokens(configure_pool) != compact_source_tokens(expected_configure_pool) { return Err(format!( - "{relative} `configure_pool` must validate every main database as UTF-8 after backing classification and before TEMP-schema, connection-option, PRAGMA, or journal mutation" + "{relative} `configure_pool` must validate every main database as UTF-8 after backing classification and before TEMP-schema, PRAGMA, journal, or connection-option mutation" )); } @@ -12756,7 +12973,7 @@ fn validate_source_maintenance_runtime_token_authority( ( "crates/event_store/src/nip09/reconciliation_v1.rs", "apply_reconciliation_hook", - "41a0bc1f4e529528f9bc13be28b4a31305156124282c1c7e955ed2e4a56e86d2", + "c73869559afe06b51c7df019f620509508bb574eaf51f2224493b3be28048682", ), ( EVENT_STORE_STORE_SOURCE_RELATIVE, @@ -14118,7 +14335,7 @@ fn describe_local_sqlite_source( }) } -fn governed_regular_file_inventory( +pub(super) fn governed_regular_file_inventory( workspace_root: &Path, relative_root: &str, ) -> Result<Vec<String>, String> { @@ -14326,6 +14543,23 @@ fn validate_support_source_graph_authority(relative: &str, file: &syn::File) -> } fn validate_governed_compiler_inputs(workspace_root: &Path) -> Result<(), String> { + validate_governed_compiler_inputs_with_event_store_successor(workspace_root, None) +} + +pub(super) fn validate_raw_source_rebuild_successor_compiler_inputs( + workspace_root: &Path, + event_store_compiler_tables_sha256: &str, +) -> Result<(), String> { + validate_governed_compiler_inputs_with_event_store_successor( + workspace_root, + Some(event_store_compiler_tables_sha256), + ) +} + +fn validate_governed_compiler_inputs_with_event_store_successor( + workspace_root: &Path, + event_store_successor_sha256: Option<&str>, +) -> Result<(), String> { let toolchain = parse_cargo_manifest(workspace_root, RUST_TOOLCHAIN_RELATIVE)?; let expected_toolchain: toml::Value = toml::from_str( r#" @@ -14595,7 +14829,14 @@ xtask = "run -q -p xtask --" let expected_identities = GOVERNED_DEPENDENCY_TABLE_SHA256 .iter() - .map(|(relative, sha256)| ((*relative).to_owned(), (*sha256).to_owned())) + .map(|(relative, sha256)| { + let sha256 = if *relative == EVENT_STORE_CARGO_MANIFEST_RELATIVE { + event_store_successor_sha256.unwrap_or(sha256) + } else { + sha256 + }; + ((*relative).to_owned(), sha256.to_owned()) + }) .collect::<Vec<_>>(); if actual_identities != expected_identities { return Err(format!( @@ -16972,6 +17213,20 @@ mod tests { use super::*; use std::fs; + const RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS: [&str; 11] = [ + "crates/event_store/Cargo.toml", + "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/food_availability_projection_v1.rs", + "crates/event_store/src/store/protocol_reconciliation_v1.rs", + ]; + fn repository_root() -> std::path::PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() @@ -16997,6 +17252,20 @@ mod tests { .get_mut("dependencies") .and_then(toml::Value::as_table_mut) .expect("event-store dependencies"); + let futures = dependencies + .remove("futures") + .expect("RawSourceRebuild futures compiler edge must be present in the live fixture"); + let expected_futures: toml::Value = + toml::from_str("dependency = { workspace = true, optional = true }") + .expect("parse expected futures dependency"); + assert_eq!( + futures, + expected_futures + .get("dependency") + .expect("expected futures dependency") + .clone(), + "RawSourceRebuild futures compiler edge must retain its exact semantic shape" + ); let blossom = dependencies .remove("radroots_blossom") .expect("successor Blossom compiler edge must be present in the live fixture"); @@ -17012,6 +17281,17 @@ mod tests { .clone(), "successor Blossom compiler edge must retain its exact semantic shape" ); + let sqlite_features = manifest + .get_mut("features") + .and_then(toml::Value::as_table_mut) + .and_then(|features| features.get_mut("sqlite")) + .and_then(toml::Value::as_array_mut) + .expect("event-store sqlite features"); + let futures_index = sqlite_features + .iter() + .position(|feature| feature.as_str() == Some("dep:futures")) + .expect("RawSourceRebuild futures feature edge must be present in the live fixture"); + sqlite_features.remove(futures_index); let tokio_features = manifest .get_mut("dev-dependencies") .and_then(toml::Value::as_table_mut) @@ -17114,6 +17394,7 @@ mod tests { paths.extend(SOURCE_ROUTE_WITNESS_SPECS.iter().map(|source| source.path)); paths.extend(SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS); paths.extend(SUCCESSOR_08D_SOURCE_PATHS); + paths.extend(SUCCESSOR_08D1_EXCLUSIVE_SOURCE_PATHS); paths.extend(super::super::source_maintenance::source_contract_fixture_source_paths()); paths.sort_unstable(); paths.dedup(); @@ -17806,8 +18087,8 @@ route!(r#hex); 1, ), lib_original.replacen( - "RadrootsEventStoreStatusSummary,\n RadrootsEventVisibility,", - "RadrootsEventVisibility,", + "RadrootsEventStoreSourceGeneration, RadrootsEventStoreStatusSummary, RadrootsEventVisibility,", + "RadrootsEventStoreSourceGeneration, RadrootsEventVisibility,", 1, ), ]; @@ -18410,18 +18691,53 @@ route!(r#hex); _ => None, }) .expect("configure_pool"); - let syn::Stmt::Expr(syn::Expr::ForLoop(preflight), _) = - &mut configure_pool.block.stmts[5] - else { - panic!("encoding preflight loop"); - }; + let preflight = configure_pool + .block + .stmts + .iter_mut() + .find_map(|statement| match statement { + syn::Stmt::Expr(syn::Expr::ForLoop(preflight), _) + if compact_tokens(&preflight.body) + .contains("validate_main_database_encoding(connection).await?") => + { + Some(preflight) + } + _ => None, + }) + .expect("encoding preflight loop"); + let encoding_index = preflight + .body + .stmts + .iter() + .position(|statement| { + compact_tokens(statement) + == "validate_main_database_encoding(connection).await?;" + }) + .expect("encoding preflight statement"); + let backing_index = preflight + .body + .stmts + .iter() + .position(|statement| { + compact_tokens(statement).starts_with("iffile_backed==database_is_memory") + }) + .expect("backing classification statement"); + let temp_index = preflight + .body + .stmts + .iter() + .position(|statement| { + compact_tokens(statement) + .starts_with("crate::schema::validate_event_store_temp_schema") + }) + .expect("TEMP-schema validation statement"); match mutation { "remove" => { - preflight.body.stmts.remove(3); + preflight.body.stmts.remove(encoding_index); } - "discard" => strip_outer_try(&mut preflight.body.stmts[3]), - "after_temp" => preflight.body.stmts.swap(3, 4), - "before_backing" => preflight.body.stmts.swap(2, 3), + "discard" => strip_outer_try(&mut preflight.body.stmts[encoding_index]), + "after_temp" => preflight.body.stmts.swap(encoding_index, temp_index), + "before_backing" => preflight.body.stmts.swap(backing_index, encoding_index), _ => unreachable!(), } assert!( @@ -20370,6 +20686,69 @@ pub(crate) fn migration_for_version"#, } #[test] + fn predecessor_impl_resolution_projection_is_fail_closed() { + let workspace = synthetic_workspace(); + let manifest = immutable_manifest(); + validate_predecessor_impl_resolution_authority( + workspace.path(), + &manifest, + &RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS, + ) + .expect("current successor must preserve predecessor impl authority"); + let unchanged_relative = "crates/event_codec/src/deletion/reconciliation_v1.rs"; + let unchanged_path = workspace.path().join(unchanged_relative); + let unchanged = fs::read_to_string(&unchanged_path).expect("unchanged predecessor source"); + + fs::write( + &unchanged_path, + format!( + "{unchanged}\ntrait UnexpectedPredecessorResolution {{}}\nimpl UnexpectedPredecessorResolution for RadrootsNip09SuppressionDecision {{}}\n" + ), + ) + .expect("add unexpected predecessor impl authority"); + let error = validate_predecessor_impl_resolution_authority( + workspace.path(), + &manifest, + &RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS, + ) + .expect_err("new predecessor-bound impl authority must fail closed"); + assert!(error.contains("unexpected ["), "{error}"); + assert!(error.contains("UnexpectedPredecessorResolution"), "{error}"); + + let changed = unchanged.replacen("self.address_reference.as_ref()", "None", 1); + assert_ne!(changed, unchanged, "expected impl fixture must mutate"); + fs::write(&unchanged_path, changed).expect("change expected predecessor impl authority"); + let error = validate_predecessor_impl_resolution_authority( + workspace.path(), + &manifest, + &RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS, + ) + .expect_err("changed predecessor-bound impl authority must fail closed"); + assert!(error.contains("missing ["), "{error}"); + assert!(error.contains("unexpected ["), "{error}"); + assert!(error.matches("address_reference").count() >= 2, "{error}"); + fs::write(&unchanged_path, unchanged).expect("restore unchanged predecessor source"); + + let successor_relative = + "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs"; + let successor_path = workspace.path().join(successor_relative); + let successor = fs::read_to_string(&successor_path).expect("successor-only source"); + fs::write( + successor_path, + format!( + "{successor}\ntrait SuccessorOnlyResolution {{}}\nimpl SuccessorOnlyResolution for RadrootsNip09SuppressionDecision {{}}\n" + ), + ) + .expect("change successor-only impl authority"); + validate_predecessor_impl_resolution_authority( + workspace.path(), + &manifest, + &RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS, + ) + .expect("successor-only authority must not rotate predecessor projection"); + } + + #[test] fn nip09_v1_manifest_is_independent_of_post_core_transport_evolution() { let workspace = synthetic_workspace(); let before = immutable_manifest(); diff --git a/tools/xtask/src/contract/raw_source_rebuild.rs b/tools/xtask/src/contract/raw_source_rebuild.rs @@ -0,0 +1,7433 @@ +use super::artifact_bundle::{ + GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction, +}; +use super::food_availability_projection::validate_food_availability_projection_predecessor_production_sources_under_lock; +use super::nip09_reconciliation::{ + governed_regular_file_inventory, validate_current_event_store_successor_authority, + validate_raw_source_rebuild_successor_compiler_inputs, +}; +use super::source_maintenance::validate_source_maintenance_manifest_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::fs; +use std::path::Path; +use syn::{Item, UseTree}; + +const SCHEMA_VERSION: u32 = 1; +const CONTRACT_ID: &str = "radroots_event_store.raw_source_rebuild_v1"; +const AUTHORITY_ID: &str = "raw_source_rebuild_v1"; +const PREDECESSOR_CONTRACT_ID: &str = "radroots_event_store.source_maintenance_v1"; +const PREDECESSOR_MANIFEST_RELATIVE: &str = + "crates/event_store/contracts/source_maintenance_v1.manifest.json"; +const PREDECESSOR_MANIFEST_BYTE_LENGTH: usize = 14_216; +const PREDECESSOR_MANIFEST_SHA256: &str = + "e8911e6e5710278969cbd15557a5b856b1575dfd11a655711403598370b41221"; +const EVENT_STORE_SCHEMA_VERSION: u32 = 4; +const EVENT_CONTRACT_REGISTRY_VERSION: u32 = 7; +const PROJECTION_CURSOR_COUNT_LIMIT: u32 = 4_096; +const PROJECTION_CURSOR_REJECTION_PROBE_LIMIT: u32 = PROJECTION_CURSOR_COUNT_LIMIT + 1; +const CALLER_MAIN_TABLE_COUNT_LIMIT: u32 = 4_096; +const CALLER_FOREIGN_KEY_ROW_COUNT_LIMIT: u32 = 4_096; +const CALLER_INBOUND_FOREIGN_KEY_POLICY: &str = + "reject_all_rebuild_mutated_parent_dependencies_before_entropy_v1"; +const CALLER_SCHEMA_PREFLIGHT_AST_SHA256: &str = + "81396b4c375ea40c7f928ec5e4599de5b0d64aab51b7e08e84b79ab9dca6ab64"; +const COLD_REPAIR_MODE: &str = "canonical_file_only_single_connection_lock_domain_probe_v1"; +const TRANSACTION_MODE: &str = "begin_immediate_v1"; +const DIGEST_ALGORITHM: &str = "sha256_domain_nul_typed_fields_v1"; +const DIGEST_DOMAIN_TERMINATOR: &str = "nul_byte"; +const RAW_DIGEST_DOMAIN_UTF8: &str = "radroots:event-store:immutable-raw-digest:v1"; +const PRODUCT_DIGEST_DOMAIN_UTF8: &str = "radroots:event-store:active-product-state-digest:v1"; +const VISIBILITY_ORACLE: &str = "pure_verified_raw_snapshot_direct_indexed_evidence_v1"; +const VISIBILITY_ORACLE_EXPECTED_VISIBILITY_AST_SHA256: &str = + "88155fb497668bc65d1adb107c8692483c44f8be1be7dea4663772aa6df23897"; +const VISIBILITY_ORACLE_DECISION_AST_SHA256: &str = + "be2034bc552829af7cb8f8f77ce7c0b97e2e23b94551994f6463877ef87c9404"; +const RECONCILIATION_REQUEST_INDEX_INSERT_AST_SHA256: &str = + "81d1e02d42dda1b34fd6ab30873765072d7030abf0bb42f7dc117b88eb206933"; +const RECONCILIATION_REQUEST_INDEX_DECISION_AST_SHA256: &str = + "2920383a13dc1f7f701039147cb3e5595797a5fe68217a7de6d26cb27715add1"; +const RECONCILIATION_AFFECTED_COORDINATES_AST_SHA256: &str = + "8b1aca89e5a20be8f5eb44e08e205e693ba6f2ea0cf4e3a5bd25910f5f4e25cf"; +const EVENT_STORE_SUCCESSOR_COMPILER_TABLES_SHA256: &str = + "10e6177bb51094e1775994e1cb3c7c72d01d71890ce45df6c3129ff3d7ee301c"; +const SCOPED_INTEGRITY_MODE: &str = "event_store_owned_tables_and_indices_v1"; +const SQLITE_SEQUENCE_SCOPE: &str = "target_first_after_single_shared_sequence_scan_v1"; +const HASH_ALGORITHM: &str = "sha256_bytes_v1"; +const WRITE_COMMAND: &str = "cargo xtask contract raw-source-rebuild-manifest --write"; + +const MANIFEST_RELATIVE: &str = "crates/event_store/contracts/raw_source_rebuild_v1.manifest.json"; +const MANIFEST_SCHEMA_RELATIVE: &str = + "crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json"; +const MANIFEST_SHA256_RELATIVE: &str = + "crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256"; +const GENERATED_DESCRIPTOR_RELATIVE: &str = + "crates/event_store/src/generated/raw_source_rebuild_manifest.rs"; +const RESULT_VECTOR_CANONICAL_RELATIVE: &str = + "contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json"; +const RESULT_VECTOR_MIRROR_RELATIVE: &str = + "crates/event_store/tests/fixtures/raw_source_rebuild.v1.json"; +const RESULT_VECTOR_EXECUTOR_RELATIVE: &str = + "crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs"; +const RESULT_VECTOR_EXECUTOR_ID: &str = + "radroots_event_store.raw_source_rebuild_v1.result_vector_executor.v1"; +const RESULT_VECTOR_EXECUTOR_TEST: &str = "raw_source_rebuild_v1_result_vector"; +const REBUILD_RUNTIME_SOURCE_RELATIVE: &str = + "crates/event_store/src/nip09/reconciliation_v1/raw_source_rebuild.rs"; +const REBUILD_FAILPOINT_TEST_SOURCE_RELATIVE: &str = + "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs"; +const REBUILD_FAILPOINT_TEST: &str = "raw_source_rebuild_failpoints_roll_back_every_stage_v1"; +const RESULT_VECTOR_DELEGATED_SUITE_ID: &str = + "radroots_event_store.raw_source_rebuild_v1.delegated_rust_test_suite.v1"; +const RESULT_VECTOR_DELEGATED_SUITE_LANE: &str = "nix run .#contract"; +const RESULT_VECTOR_DELEGATED_SUITE_PACKAGE: &str = "radroots_event_store"; +const RESULT_VECTOR_BYTE_LENGTH: usize = 26_833; +const RESULT_VECTOR_SHA256: &str = + "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15"; +const RESULT_VECTOR_DIRECT_CASE_IDS: &[&str] = &[ + "empty_source_repeat_digest_parity", + "signed_food_fixture_typed_digest_parity", +]; +const WORKSPACE_MANIFEST_RELATIVE: &str = "Cargo.toml"; +const FLAKE_SOURCE_RELATIVE: &str = "flake.nix"; +const FLAKE_LOCK_RELATIVE: &str = "flake.lock"; +const CONTRACT_APP_SOURCE_RELATIVE: &str = "build/nix/apps.nix"; +const CONTRACT_LANE_SOURCE_RELATIVE: &str = "build/nix/common.nix"; +const TOOLCHAIN_ROUTING_SOURCE_RELATIVE: &str = "build/nix/toolchains.nix"; +const RUST_TOOLCHAIN_RELATIVE: &str = "rust-toolchain.toml"; +const XTASK_MANIFEST_RELATIVE: &str = "tools/xtask/Cargo.toml"; +const XTASK_REQUIRED_DISABLED_AUTO_TARGET_FLAGS: &[&str] = + &["autolib", "autotests", "autoexamples", "autobenches"]; +const XTASK_FORBIDDEN_AUTO_TARGET_PATHS: &[&str] = &[ + "tools/xtask/build.rs", + "tools/xtask/src/lib.rs", + "tools/xtask/src/bin.rs", + "tools/xtask/src/bin", + "tools/xtask/tests", + "tools/xtask/examples", + "tools/xtask/benches", +]; +const CONTRACT_COMMAND_SOURCE_RELATIVE: &str = "tools/xtask/src/contract.rs"; +const XTASK_MAIN_SOURCE_RELATIVE: &str = "tools/xtask/src/main.rs"; +const RELEASE_RECORD_RELATIVE: &str = "contracts/releases/1.0.0-alpha.1.toml"; +const CHANGELOG_RELATIVE: &str = "CHANGELOG.md"; +const RELEASE_CHANGE_ID: &str = "event-store-raw-source-rebuild-authority"; +const RELEASE_CHANGE_SUMMARY: &str = "Add an authenticated managed-v4 raw-source rebuild and file-only cold-repair authority with stable typed drift categories, serialized generation rotation, independent immutable-raw visibility audit, typed generation-normalized product-state digests, bounded generic projection cursors, a bounded caller-schema dependency preflight over every directly or indirectly mutated parent including the full Food FTS5 table family and sqlite_sequence, target-first transition sequence normalization, separately scoped integrity checks, exact rollback failpoints, a canonical-path SQLite lock-domain probe, deterministic crate-owned connection policy, and an executable successor contract while freezing the SourceMaintenance predecessor and migration inventory."; +const RELEASE_CHANGE_IMPACTS: &[&str] = &[ + "add_exported_type", + "add_exported_function", + "add_exported_constant", + "add_exported_field", + "add_enum_variant", + "add_conformance_vector", + "change_exported_enum_variant", + "change_exported_algorithm_behavior", +]; +const CHANGELOG_RELEASE_MARKER: &str = + "<!-- release-change: event-store-raw-source-rebuild-authority -->"; + +const MIGRATION_RELATIVES: &[&str] = &[ + "crates/event_store/migrations/0001_event_store.down.sql", + "crates/event_store/migrations/0001_event_store.up.sql", + "crates/event_store/migrations/0002_nip09.down.sql", + "crates/event_store/migrations/0002_nip09.up.sql", + "crates/event_store/migrations/0003_food_availability_projection.down.sql", + "crates/event_store/migrations/0003_food_availability_projection.up.sql", + "crates/event_store/migrations/0004_source_maintenance.down.sql", + "crates/event_store/migrations/0004_source_maintenance.up.sql", +]; + +const REBUILD_STAGES: &[&str] = &[ + "after_marker_open", + "after_generation_rotation", + "after_core_replay", + "after_visibility_audit", + "after_food_reset_replay", + "after_food_audit", + "after_marker_close", +]; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct RebuildFailpointSpec { + id: &'static str, + variant: &'static str, + rollback_case_id: &'static str, +} + +const REBUILD_FAILPOINTS: &[RebuildFailpointSpec] = &[ + RebuildFailpointSpec { + id: "after_marker_open", + variant: "AfterMarkerOpen", + rollback_case_id: "rollback_after_marker_open", + }, + RebuildFailpointSpec { + id: "after_generation_rotation", + variant: "AfterGenerationRotation", + rollback_case_id: "rollback_after_generation_rotation", + }, + RebuildFailpointSpec { + id: "after_core_replay", + variant: "AfterCoreReplay", + rollback_case_id: "rollback_after_core_replay", + }, + RebuildFailpointSpec { + id: "after_visibility_audit", + variant: "AfterVisibilityAudit", + rollback_case_id: "rollback_after_visibility_audit", + }, + RebuildFailpointSpec { + id: "after_food_reset_replay", + variant: "AfterFoodResetAndReplay", + rollback_case_id: "rollback_after_food_reset_replay", + }, + RebuildFailpointSpec { + id: "after_food_audit", + variant: "AfterFoodAudit", + rollback_case_id: "rollback_after_food_audit", + }, + RebuildFailpointSpec { + id: "after_marker_close", + variant: "AfterMarkerClose", + rollback_case_id: "rollback_after_marker_close", + }, +]; + +const PRESERVED_AUTHORITIES: &[&str] = &[ + "legacy_listing", + "trade", + "transport_observation", + "generic_projection_cursor", + "unrelated_caller_state_without_dependencies_on_rebuild_owned_tables", +]; + +const PRODUCT_DIGEST_COMPONENTS: &[&str] = &[ + "logical_current_classifications", + "raw_heads", + "active_addressable_head_state", + "active_nip09_facts", + "current_visibility", + "food_availability_rows", + "food_availability_images", + "logical_food_fts_rows", + "stable_food_cursor_metadata", +]; + +const PRODUCT_DIGEST_EXCLUSIONS: &[&str] = &[ + "source_generation", + "absolute_transition_sequence", + "transition_history", + "rebuild_origin", + "rebuild_cause", + "operational_timestamps", + "generic_projection_cursors", + "caller_owned_state", +]; + +const SCOPED_INTEGRITY_TABLES: &[&str] = &[ + "event_envelopes", + "event_envelope_tags", + "event_envelope_head", + "radroots_event_store_source_generation", + "radroots_event_store_source_rebuild_commit_barrier", + "radroots_event_store_source_rebuild_marker", + "radroots_event_store_source_state", + "radroots_event_store_write_lock", + "radroots_event_store_source_capacity_v1", + "radroots_event_store_event_coordinate", + "radroots_event_store_nip09_request", + "radroots_event_store_nip09_event_target", + "radroots_event_store_nip09_address_target", + "radroots_event_store_addressable_head_state", + "radroots_event_store_addressable_head_transition", + "radroots_event_store_addressable_feed_integrity_v1", + "radroots_event_store_food_availability_cursor", + "radroots_event_store_food_availability_projection", + "radroots_event_store_food_availability_image", +]; + +const CALLER_INBOUND_FOREIGN_KEY_PARENT_TABLES: &[&str] = &[ + "event_envelopes", + "event_envelope_tags", + "event_envelope_head", + "radroots_event_store_source_generation", + "radroots_event_store_source_rebuild_commit_barrier", + "radroots_event_store_source_rebuild_marker", + "radroots_event_store_source_state", + "radroots_event_store_write_lock", + "radroots_event_store_source_capacity_v1", + "radroots_event_store_event_coordinate", + "radroots_event_store_nip09_request", + "radroots_event_store_nip09_event_target", + "radroots_event_store_nip09_address_target", + "radroots_event_store_addressable_head_state", + "radroots_event_store_addressable_head_transition", + "radroots_event_store_addressable_feed_integrity_v1", + "radroots_event_store_food_availability_cursor", + "radroots_event_store_food_availability_projection", + "radroots_event_store_food_availability_image", + "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", + "sqlite_sequence", +]; + +const RAW_DIGEST_QUERY_SPECS: &[DigestQuerySpec] = &[ + DigestQuerySpec { + section: "event_envelopes", + sql: "SELECT seq, event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, inserted_at_ms FROM event_envelopes ORDER BY seq", + fields: &[ + "seq", + "event_id", + "pubkey", + "created_at", + "kind", + "tags_json", + "content", + "sig", + "raw_json", + "inserted_at_ms", + ], + }, + DigestQuerySpec { + section: "event_envelope_tags", + sql: "SELECT event.seq, tag.event_id, tag.tag_index, tag.tag_name, tag.tag_value, tag.tag_json FROM event_envelope_tags AS tag JOIN event_envelopes AS event ON event.event_id = tag.event_id ORDER BY event.seq, tag.tag_index", + fields: &[ + "seq", + "event_id", + "tag_index", + "tag_name", + "tag_value", + "tag_json", + ], + }, +]; + +const PRODUCT_DIGEST_QUERY_SPECS: &[DigestQuerySpec] = &[ + DigestQuerySpec { + section: "envelope_classification", + sql: "SELECT event_id, verification_status, contract_status, contract_id, event_class, projection_eligible FROM event_envelopes ORDER BY event_id", + fields: &[ + "event_id", + "verification_status", + "contract_status", + "contract_id", + "event_class", + "projection_eligible", + ], + }, + DigestQuerySpec { + section: "tag_classification", + sql: "SELECT event_id, tag_index, contract_semantic, contract_value_type, relay_indexed FROM event_envelope_tags ORDER BY event_id, tag_index", + fields: &[ + "event_id", + "tag_index", + "contract_semantic", + "contract_value_type", + "relay_indexed", + ], + }, + DigestQuerySpec { + section: "raw_heads", + sql: "SELECT coordinate_type, kind, pubkey, d_tag, event_id, created_at FROM event_envelope_head ORDER BY coordinate_type, kind, pubkey, d_tag", + fields: &[ + "coordinate_type", + "kind", + "pubkey", + "d_tag", + "event_id", + "created_at", + ], + }, + DigestQuerySpec { + section: "event_coordinates", + sql: "SELECT event_id, coordinate_type, kind, pubkey, created_at, admission_status, admission_code, contract_id, raw_d_tag, nip09_matchable, nip09_d_tag FROM radroots_event_store_event_coordinate WHERE source_generation = ? ORDER BY event_id", + fields: &[ + "event_id", + "coordinate_type", + "kind", + "pubkey", + "created_at", + "admission_status", + "admission_code", + "contract_id", + "raw_d_tag", + "nip09_matchable", + "nip09_d_tag", + ], + }, + DigestQuerySpec { + section: "nip09_requests", + sql: "SELECT request_event_id, request_pubkey, request_created_at FROM radroots_event_store_nip09_request WHERE source_generation = ? ORDER BY request_event_id", + fields: &["request_event_id", "request_pubkey", "request_created_at"], + }, + DigestQuerySpec { + section: "nip09_event_targets", + sql: "SELECT request_event_id, target_event_id, source_tag_index, source_tag_value FROM radroots_event_store_nip09_event_target WHERE source_generation = ? ORDER BY request_event_id, target_event_id, source_tag_index", + fields: &[ + "request_event_id", + "target_event_id", + "source_tag_index", + "source_tag_value", + ], + }, + DigestQuerySpec { + section: "nip09_address_targets", + sql: "SELECT request_event_id, target_kind, target_pubkey, target_d_tag, inclusive_cutoff, source_tag_index, source_tag_value, source_kind_text, source_pubkey_text, source_d_tag FROM radroots_event_store_nip09_address_target WHERE source_generation = ? ORDER BY request_event_id, target_kind, target_pubkey, target_d_tag, source_tag_index", + fields: &[ + "request_event_id", + "target_kind", + "target_pubkey", + "target_d_tag", + "inclusive_cutoff", + "source_tag_index", + "source_tag_value", + "source_kind_text", + "source_pubkey_text", + "source_d_tag", + ], + }, + DigestQuerySpec { + section: "addressable_heads", + sql: "SELECT kind, pubkey, d_tag, raw_head_event_id, 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_state WHERE source_generation = ? ORDER BY kind, pubkey, d_tag", + fields: &[ + "kind", + "pubkey", + "d_tag", + "raw_head_event_id", + "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", + ], + }, + DigestQuerySpec { + section: "current_visibility", + sql: "SELECT event_id, admission_status, contract_id, event_class, raw_d_tag, is_raw_head, raw_head_event_id, 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 source_generation = ? ORDER BY event_id", + fields: &[ + "event_id", + "admission_status", + "contract_id", + "event_class", + "raw_d_tag", + "is_raw_head", + "raw_head_event_id", + "suppression_outcome", + "suppression_reason", + "event_reference_request_id", + "address_reference_request_id", + "address_reference_cutoff", + "current_visibility", + ], + }, + DigestQuerySpec { + section: "food_projection", + sql: "SELECT kind, pubkey, d_tag, event_id, created_at, contract_id, content, title, summary, published_at, location, price_amount, price_currency, price_unit, quantity_amount, quantity_unit, status, diagnostic_codes_json FROM radroots_event_store_food_availability_projection WHERE source_generation = ? ORDER BY pubkey, d_tag", + fields: &[ + "kind", + "pubkey", + "d_tag", + "event_id", + "created_at", + "contract_id", + "content", + "title", + "summary", + "published_at", + "location", + "price_amount", + "price_currency", + "price_unit", + "quantity_amount", + "quantity_unit", + "status", + "diagnostic_codes_json", + ], + }, + DigestQuerySpec { + section: "food_images", + sql: "SELECT pubkey, d_tag, image_index, raw_tag_json, url, width, height, blossom_sha256, qualifies, diagnostic_codes_json FROM radroots_event_store_food_availability_image WHERE source_generation = ? ORDER BY pubkey, d_tag, image_index", + fields: &[ + "pubkey", + "d_tag", + "image_index", + "raw_tag_json", + "url", + "width", + "height", + "blossom_sha256", + "qualifies", + "diagnostic_codes_json", + ], + }, + DigestQuerySpec { + section: "food_search", + sql: "SELECT event_id, pubkey, d_tag, title, summary, content, location FROM radroots_event_store_food_availability_search_fts ORDER BY event_id", + fields: &[ + "event_id", "pubkey", "d_tag", "title", "summary", "content", "location", + ], + }, + DigestQuerySpec { + section: "food_cursor", + sql: "SELECT feed_version, projection_version, scope_fingerprint, hook_manifest_sha256, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1", + fields: &[ + "feed_version", + "projection_version", + "scope_fingerprint", + "hook_manifest_sha256", + "projected_row_count", + ], + }, +]; + +const ADDED_PUBLIC_SYMBOLS: &[&str] = &[ + "RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1", + "RadrootsEventStoreCallerInboundForeignKeyV1", + "RadrootsEventStoreActiveProductStateDigestV1", + "RadrootsEventStoreImmutableRawDigestV1", + "RadrootsEventStoreRawSourceRebuildDriftV1", + "RadrootsEventStoreRawSourceRebuildReportV1", +]; + +const PUBLIC_METHODS: &[&str] = &[ + "RadrootsEventStore::rebuild_from_raw_v1", + "RadrootsEventStore::repair_file_from_raw_v1", + "RadrootsEventStoreRawSourceRebuildReportV1::prior_source_generation", + "RadrootsEventStoreRawSourceRebuildReportV1::new_source_generation", + "RadrootsEventStoreRawSourceRebuildReportV1::source_capacity", + "RadrootsEventStoreRawSourceRebuildReportV1::raw_high_water_seq", + "RadrootsEventStoreRawSourceRebuildReportV1::immutable_raw_digest", + "RadrootsEventStoreRawSourceRebuildReportV1::active_product_state_digest", + "RadrootsEventStoreImmutableRawDigestV1::as_bytes", + "RadrootsEventStoreActiveProductStateDigestV1::as_bytes", + "RadrootsEventStoreRawSourceRebuildDriftV1::code", +]; + +const RAW_SOURCE_REBUILD_DRIFT_KINDS: &[(&str, &str)] = &[ + ("ManagedSchemaAuthority", "managed_schema_authority"), + ("ImmutableRawAuthority", "immutable_raw_authority"), + ("SourceGenerationLineage", "source_generation_lineage"), + ( + "AddressableTransitionAuthority", + "addressable_transition_authority", + ), + ( + "DerivedProductStateAuthority", + "derived_product_state_authority", + ), + ("RebuildPostcondition", "rebuild_postcondition"), +]; + +const ERROR_VARIANTS: &[&str] = &[ + "ProjectionCursorCapacityExceeded", + "RawSourceRepairDatabaseIdentityMismatch", + "RawSourceRepairCanonicalPathLockDomainMismatch", + "RawSourceRepairMainDatabaseCanonicalizationFailed", + "RawSourceRebuildCallerForeignKeyCapacityExceeded", + "RawSourceRebuildCallerInboundForeignKeyUnsupported", + "RawSourceRebuildCallerTableCapacityExceeded", + "RawSourceRebuildStateDrift", + "RawSourceRebuildTransactionRollbackFailed", +]; + +const ENTRY_POINTS: &[(&str, &str)] = &[ + ( + "live_rebuild", + "radroots_event_store::RadrootsEventStore::rebuild_from_raw_v1", + ), + ( + "cold_file_repair", + "radroots_event_store::RadrootsEventStore::repair_file_from_raw_v1", + ), + ( + "projection_cursor_insert_preflight", + "radroots_event_store::nip09::reconciliation_v1::preflight_projection_cursor_insert_v1", + ), + ( + "serialized_rebuild_runtime", + "radroots_event_store::nip09::reconciliation_v1::raw_source_rebuild::rebuild_from_raw_v1_on_pool", + ), + ( + "independent_visibility_oracle", + "radroots_event_store::nip09::reconciliation_v1::visibility_oracle_v1::audit_current_visibility_from_raw_v1", + ), + ("result_vector_executor", RESULT_VECTOR_EXECUTOR_TEST), +]; + +#[derive(Clone, Copy)] +struct SourceSpec { + role: &'static str, + path: &'static str, +} + +#[derive(Clone, Copy)] +struct DigestQuerySpec { + section: &'static str, + sql: &'static str, + fields: &'static [&'static str], +} + +const DELEGATED_COMPILER_SOURCE_PINS: &[(&str, &str)] = &[ + ( + FLAKE_SOURCE_RELATIVE, + "0251b26040cf5338c12dc777a4deaadb8f63eb4e88bc05929dcec67db88ff2bf", + ), + ( + FLAKE_LOCK_RELATIVE, + "41b569739bfa0c488625326f4f0a874561601787951cdf7a3f171e60572fa20e", + ), + ( + CONTRACT_APP_SOURCE_RELATIVE, + "41a185ac87379e24c1ede09c0f1aac820653dffc09f99cd803b145b44bed982c", + ), + ( + CONTRACT_LANE_SOURCE_RELATIVE, + "b3340e1b4973e6a1e02899d164ca74842757f22b6b1a03f90461532fcd844df5", + ), + ( + TOOLCHAIN_ROUTING_SOURCE_RELATIVE, + "cd664be945e28bf6c25c7758182ff8d01e03248832dfc2c045c01b4f4aff960f", + ), + ( + RUST_TOOLCHAIN_RELATIVE, + "c33aa38292bab6513bf79ed2f69c1525b736dd738b15ca78af713b70b29265c9", + ), + ( + XTASK_MANIFEST_RELATIVE, + "7e858f4f33913f986c565be2a31c41615ea0585c9e19572363ef5cae36cafdc9", + ), +]; + +const REQUIRED_DELEGATED_COMPILER_SOURCES: &[(&str, &str)] = &[ + ("workspace_manifest_authority", WORKSPACE_MANIFEST_RELATIVE), + ("workspace_lockfile_authority", "Cargo.lock"), + ("nix_flake_app_export_authority", FLAKE_SOURCE_RELATIVE), + ("nix_input_lock_authority", FLAKE_LOCK_RELATIVE), + ( + "nix_contract_app_routing_authority", + CONTRACT_APP_SOURCE_RELATIVE, + ), + ( + "nix_contract_test_lane_authority", + CONTRACT_LANE_SOURCE_RELATIVE, + ), + ( + "nix_toolchain_routing_authority", + TOOLCHAIN_ROUTING_SOURCE_RELATIVE, + ), + ("rust_toolchain_authority", RUST_TOOLCHAIN_RELATIVE), + ("xtask_manifest_authority", XTASK_MANIFEST_RELATIVE), +]; + +const SOURCE_SPECS: &[SourceSpec] = &[ + SourceSpec { + role: "workspace_manifest_authority", + path: WORKSPACE_MANIFEST_RELATIVE, + }, + SourceSpec { + role: "workspace_lockfile_authority", + path: "Cargo.lock", + }, + SourceSpec { + role: "nix_flake_app_export_authority", + path: FLAKE_SOURCE_RELATIVE, + }, + SourceSpec { + role: "nix_input_lock_authority", + path: FLAKE_LOCK_RELATIVE, + }, + SourceSpec { + role: "nix_contract_app_routing_authority", + path: CONTRACT_APP_SOURCE_RELATIVE, + }, + SourceSpec { + role: "nix_contract_test_lane_authority", + path: CONTRACT_LANE_SOURCE_RELATIVE, + }, + SourceSpec { + role: "nix_toolchain_routing_authority", + path: TOOLCHAIN_ROUTING_SOURCE_RELATIVE, + }, + SourceSpec { + role: "rust_toolchain_authority", + path: RUST_TOOLCHAIN_RELATIVE, + }, + SourceSpec { + role: "xtask_manifest_authority", + path: XTASK_MANIFEST_RELATIVE, + }, + SourceSpec { + role: "event_store_dependency_feature_authority", + path: "crates/event_store/Cargo.toml", + }, + 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: "food_generated_descriptor_input", + path: "crates/event_store/src/generated/food_availability_projection_manifest.rs", + }, + SourceSpec { + role: "nip09_generated_descriptor_input", + path: "crates/event_store/src/generated/nip09_reconciliation_manifest.rs", + }, + SourceSpec { + role: "source_maintenance_generated_descriptor_input", + path: "crates/event_store/src/generated/source_maintenance_manifest.rs", + }, + SourceSpec { + role: "public_surface", + path: "crates/event_store/src/lib.rs", + }, + SourceSpec { + role: "migration_runtime_registry", + path: "crates/event_store/src/migrations.rs", + }, + SourceSpec { + role: "model_registration", + path: "crates/event_store/src/model.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_availability_projection_model", + path: "crates/event_store/src/model/food_availability_projection_v1.rs", + }, + SourceSpec { + role: "ingest_reconciliation_model", + path: "crates/event_store/src/model/ingest_reconciliation_v1.rs", + }, + SourceSpec { + role: "rebuild_report_and_digest_models", + path: "crates/event_store/src/model/raw_source_rebuild_v1.rs", + }, + SourceSpec { + role: "reconciliation_model", + path: "crates/event_store/src/model/reconciliation_v1.rs", + }, + SourceSpec { + role: "nip09_module_registration", + path: "crates/event_store/src/nip09.rs", + }, + SourceSpec { + role: "reconciliation_runtime_registration", + path: "crates/event_store/src/nip09/reconciliation_v1.rs", + }, + SourceSpec { + role: "serialized_raw_source_rebuild", + path: REBUILD_RUNTIME_SOURCE_RELATIVE, + }, + SourceSpec { + role: "nip09_result_vector_executor_input", + path: "crates/event_store/src/nip09/reconciliation_v1/result_vector_executor.rs", + }, + SourceSpec { + role: "independent_raw_visibility_oracle", + path: "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs", + }, + SourceSpec { + role: "managed_v4_validation_and_scoped_integrity", + path: "crates/event_store/src/schema.rs", + }, + SourceSpec { + role: "source_capacity_rebuild_authority", + path: "crates/event_store/src/source_maintenance_v1.rs", + }, + SourceSpec { + role: "public_rebuild_and_cold_repair_boundary", + path: "crates/event_store/src/store.rs", + }, + SourceSpec { + role: "addressable_transition_feed_storage", + path: "crates/event_store/src/store/addressable_transition_feed_v1.rs", + }, + SourceSpec { + role: "current_visibility_storage", + path: "crates/event_store/src/store/current_visibility_v1.rs", + }, + SourceSpec { + role: "raw_source_rebuild_focused_tests", + path: "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs", + }, + SourceSpec { + role: "signed_food_digest_fixture", + path: "crates/event_store/tests/fixtures/food_availability_projection.v1.json", + }, + SourceSpec { + role: "food_projection_reset_and_replay", + path: "crates/event_store/src/store/food_availability_projection_v1.rs", + }, + SourceSpec { + role: "post_core_extension_capabilities", + path: "crates/event_store/src/store/post_core_extension_capabilities.rs", + }, + SourceSpec { + role: "post_core_extension_dispatcher", + path: "crates/event_store/src/store/post_core_extension_dispatcher.rs", + }, + SourceSpec { + role: "post_core_extensions_v1", + path: "crates/event_store/src/store/post_core_extensions_v1.rs", + }, + SourceSpec { + role: "post_core_extensions_v2", + path: "crates/event_store/src/store/post_core_extensions_v2.rs", + }, + SourceSpec { + role: "post_core_storage_v1", + path: "crates/event_store/src/store/post_core_storage_v1.rs", + }, + SourceSpec { + role: "post_core_storage_v2", + path: "crates/event_store/src/store/post_core_storage_v2.rs", + }, + SourceSpec { + role: "protocol_reconciliation_storage", + path: "crates/event_store/src/store/protocol_reconciliation_v1.rs", + }, + SourceSpec { + role: "protocol_storage_boundary", + path: "crates/event_store/src/store/protocol_storage_v1.rs", + }, + SourceSpec { + role: "event_store_package_readme", + path: "crates/event_store/README", + }, + SourceSpec { + role: "signed_nip09_reconciliation_fixture", + path: "crates/event_store/tests/fixtures/nip09_reconciliation.v1.json", + }, + SourceSpec { + role: "transitive_food_predecessor_governance", + path: "tools/xtask/src/contract/food_availability_projection.rs", + }, + SourceSpec { + role: "immutable_predecessor_governance", + path: "tools/xtask/src/contract/source_maintenance.rs", + }, + SourceSpec { + role: "transitive_nip09_predecessor_governance", + path: "tools/xtask/src/contract/nip09_reconciliation.rs", + }, + SourceSpec { + role: "raw_source_rebuild_governance", + path: "tools/xtask/src/contract/raw_source_rebuild.rs", + }, + SourceSpec { + role: "contract_command_authority", + path: CONTRACT_COMMAND_SOURCE_RELATIVE, + }, + SourceSpec { + role: "xtask_dispatch_and_release_preflight", + path: XTASK_MAIN_SOURCE_RELATIVE, + }, + SourceSpec { + role: "release_breaking_change_authority", + path: RELEASE_RECORD_RELATIVE, + }, + SourceSpec { + role: "release_note_authority", + path: CHANGELOG_RELATIVE, + }, +]; + +const EXPECTED_SOURCE_MAINTENANCE_DRIFT_PATHS: &[&str] = &[ + "crates/event_store/src/error.rs", + "crates/event_store/src/generated.rs", + "crates/event_store/src/lib.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", + "tools/xtask/src/contract/food_availability_projection.rs", + "tools/xtask/src/contract/nip09_reconciliation.rs", + "tools/xtask/src/contract/source_maintenance.rs", + "tools/xtask/src/contract.rs", + "tools/xtask/src/main.rs", +]; + +const TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[ + "crates/event_store/Cargo.toml", + "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/food_availability_projection_v1.rs", + "crates/event_store/src/store/protocol_reconciliation_v1.rs", +]; + +const GENERATED_ARTIFACT_PATHS: &[&str] = &[ + MANIFEST_RELATIVE, + MANIFEST_SCHEMA_RELATIVE, + MANIFEST_SHA256_RELATIVE, + GENERATED_DESCRIPTOR_RELATIVE, + RESULT_VECTOR_MIRROR_RELATIVE, +]; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct RawSourceRebuildManifest { + schema_version: u32, + contract_id: String, + authority_id: String, + manifest_schema: FileDescriptor, + predecessor: PredecessorDescriptor, + migration_inventory: Vec<FileDescriptor>, + runtime: RuntimeDescriptor, + entry_points: Vec<EntryPointDescriptor>, + source_files: Vec<SourceFileDescriptor>, + public_api: PublicApiDescriptor, + 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 { + contract_id: String, + manifest: FileDescriptor, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct RuntimeDescriptor { + event_store_schema_version: u32, + event_contract_registry_version: u32, + transaction_mode: String, + projection_cursor_count_limit: u32, + projection_cursor_rejection_probe_limit: u32, + caller_main_table_count_limit: u32, + caller_foreign_key_row_count_limit: u32, + caller_inbound_foreign_key_policy: String, + caller_inbound_foreign_key_parent_tables: Vec<String>, + cold_repair_mode: String, + immutable_raw_digest: DigestDescriptor, + active_product_state_digest: ProductDigestDescriptor, + visibility_oracle: String, + scoped_integrity_mode: String, + scoped_integrity_tables: Vec<String>, + sqlite_sequence_scope: String, + stages: Vec<String>, + failpoints: Vec<String>, + preserved_authorities: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct DigestDescriptor { + algorithm: String, + domain_utf8: String, + domain_terminator: String, + framing: DigestFramingDescriptor, + output_bytes: u32, + source_queries: Vec<DigestQueryDescriptor>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct ProductDigestDescriptor { + algorithm: String, + domain_utf8: String, + domain_terminator: String, + framing: DigestFramingDescriptor, + output_bytes: u32, + components: Vec<String>, + exclusions: Vec<String>, + component_queries: Vec<DigestQueryDescriptor>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct DigestFramingDescriptor { + section: String, + row: String, + signed_i64: String, + boolean: String, + optional: String, + text: String, + blob: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct DigestQueryDescriptor { + section: String, + sql: String, + fields: Vec<DigestFieldDescriptor>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct DigestFieldDescriptor { + name: String, + framing: 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 PublicApiDescriptor { + added_symbols: Vec<String>, + methods: Vec<String>, + error_variants: Vec<String>, + drift_kinds: Vec<DriftKindDescriptor>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct DriftKindDescriptor { + variant: String, + code: 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(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct RawSourceRebuildVector { + schema_version: u32, + contract_id: String, + delegated_suite: DelegatedSuite, + cases: Vec<VectorCase>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct DelegatedSuite { + id: String, + lane: String, + package: String, + authorities: Vec<DelegatedAuthority>, +} + +#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(deny_unknown_fields)] +struct DelegatedAuthority { + authority: String, + authority_path: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct VectorCase { + id: String, + execution: String, + authority: String, + authority_path: String, + expected_outcome: String, + expected_immutable_raw_digest: Option<String>, + expected_active_product_state_digest: Option<String>, +} + +pub(crate) fn write_raw_source_rebuild_manifest(workspace_root: &Path) -> Result<(), String> { + with_artifact_bundle_transaction(workspace_root, |transaction| { + transaction.write(expected_artifacts(workspace_root)?)?; + validate_raw_source_rebuild_manifest_under_lock(workspace_root) + }) +} + +pub(crate) fn validate_raw_source_rebuild_manifest(workspace_root: &Path) -> Result<(), String> { + with_artifact_bundle_transaction(workspace_root, |_| { + validate_raw_source_rebuild_manifest_under_lock(workspace_root) + }) +} + +fn validate_raw_source_rebuild_manifest_under_lock(workspace_root: &Path) -> Result<(), String> { + validate_source_maintenance_manifest_under_lock(workspace_root)?; + for artifact in expected_artifacts(workspace_root)? { + let actual = read_regular_file(workspace_root, artifact.relative)?; + if actual != artifact.contents { + return Err(format!( + "generated raw-source rebuild artifact {} is stale; run `{WRITE_COMMAND}`", + artifact.relative + )); + } + } + + 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: RawSourceRebuildManifest = serde_json::from_value(manifest_value.clone()) + .map_err(|error| format!("parse typed {MANIFEST_RELATIVE}: {error}"))?; + validate_canonical_json(MANIFEST_RELATIVE, &manifest_bytes, &manifest)?; + validate_manifest_shape(&manifest)?; + + let schema_bytes = read_regular_file(workspace_root, MANIFEST_SCHEMA_RELATIVE)?; + let schema: Value = serde_json::from_slice(&schema_bytes) + .map_err(|error| format!("parse {MANIFEST_SCHEMA_RELATIVE}: {error}"))?; + validate_canonical_json(MANIFEST_SCHEMA_RELATIVE, &schema_bytes, &schema)?; + validate_json_schema(&schema, &manifest_value)?; + + let sidecar = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?; + validate_digest_sidecar(MANIFEST_SHA256_RELATIVE, &sidecar)?; + if sidecar != format!("{}\n", sha256_hex(&manifest_bytes)).as_bytes() { + return Err(format!( + "{MANIFEST_SHA256_RELATIVE} must match the checked-in manifest bytes" + )); + } + + let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?; + validate_result_vector_identity(&vector_bytes)?; + 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: RawSourceRebuildVector = 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(workspace_root, &vector)?; + validate_source_contract(workspace_root) +} + +fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> { + let schema_bytes = canonical_json_bytes(&manifest_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<RawSourceRebuildManifest, String> { + validate_source_maintenance_manifest_under_lock(workspace_root)?; + validate_source_contract(workspace_root)?; + + let predecessor_bytes = read_regular_file(workspace_root, PREDECESSOR_MANIFEST_RELATIVE)?; + validate_predecessor_identity(&predecessor_bytes)?; + validate_predecessor_source_supersession(workspace_root, &predecessor_bytes)?; + + let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?; + validate_result_vector_identity(&vector_bytes)?; + let vector: RawSourceRebuildVector = 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(workspace_root, &vector)?; + + let source_files = SOURCE_SPECS + .iter() + .map(|spec| { + let bytes = 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>>()?; + + let executor = descriptor_for_file(workspace_root, RESULT_VECTOR_EXECUTOR_RELATIVE)?; + Ok(RawSourceRebuildManifest { + schema_version: SCHEMA_VERSION, + contract_id: CONTRACT_ID.to_owned(), + authority_id: AUTHORITY_ID.to_owned(), + manifest_schema: descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, schema_bytes)?, + predecessor: PredecessorDescriptor { + contract_id: PREDECESSOR_CONTRACT_ID.to_owned(), + manifest: descriptor_for_bytes(PREDECESSOR_MANIFEST_RELATIVE, &predecessor_bytes)?, + }, + migration_inventory: MIGRATION_RELATIVES + .iter() + .map(|relative| descriptor_for_file(workspace_root, relative)) + .collect::<Result<Vec<_>, _>>()?, + runtime: expected_runtime(), + 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: PublicApiDescriptor { + added_symbols: owned(ADDED_PUBLIC_SYMBOLS), + methods: owned(PUBLIC_METHODS), + error_variants: owned(ERROR_VARIANTS), + drift_kinds: expected_drift_kinds(), + }, + 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 expected_runtime() -> RuntimeDescriptor { + RuntimeDescriptor { + event_store_schema_version: EVENT_STORE_SCHEMA_VERSION, + event_contract_registry_version: EVENT_CONTRACT_REGISTRY_VERSION, + transaction_mode: TRANSACTION_MODE.to_owned(), + projection_cursor_count_limit: PROJECTION_CURSOR_COUNT_LIMIT, + projection_cursor_rejection_probe_limit: PROJECTION_CURSOR_REJECTION_PROBE_LIMIT, + caller_main_table_count_limit: CALLER_MAIN_TABLE_COUNT_LIMIT, + caller_foreign_key_row_count_limit: CALLER_FOREIGN_KEY_ROW_COUNT_LIMIT, + caller_inbound_foreign_key_policy: CALLER_INBOUND_FOREIGN_KEY_POLICY.to_owned(), + caller_inbound_foreign_key_parent_tables: owned(CALLER_INBOUND_FOREIGN_KEY_PARENT_TABLES), + cold_repair_mode: COLD_REPAIR_MODE.to_owned(), + immutable_raw_digest: DigestDescriptor { + algorithm: DIGEST_ALGORITHM.to_owned(), + domain_utf8: RAW_DIGEST_DOMAIN_UTF8.to_owned(), + domain_terminator: DIGEST_DOMAIN_TERMINATOR.to_owned(), + framing: expected_digest_framing(), + output_bytes: 32, + source_queries: digest_query_descriptors(RAW_DIGEST_QUERY_SPECS), + }, + active_product_state_digest: ProductDigestDescriptor { + algorithm: DIGEST_ALGORITHM.to_owned(), + domain_utf8: PRODUCT_DIGEST_DOMAIN_UTF8.to_owned(), + domain_terminator: DIGEST_DOMAIN_TERMINATOR.to_owned(), + framing: expected_digest_framing(), + output_bytes: 32, + components: owned(PRODUCT_DIGEST_COMPONENTS), + exclusions: owned(PRODUCT_DIGEST_EXCLUSIONS), + component_queries: digest_query_descriptors(PRODUCT_DIGEST_QUERY_SPECS), + }, + visibility_oracle: VISIBILITY_ORACLE.to_owned(), + scoped_integrity_mode: SCOPED_INTEGRITY_MODE.to_owned(), + scoped_integrity_tables: owned(SCOPED_INTEGRITY_TABLES), + sqlite_sequence_scope: SQLITE_SEQUENCE_SCOPE.to_owned(), + stages: owned(REBUILD_STAGES), + failpoints: REBUILD_FAILPOINTS + .iter() + .map(|failpoint| failpoint.id.to_owned()) + .collect(), + preserved_authorities: owned(PRESERVED_AUTHORITIES), + } +} + +fn expected_drift_kinds() -> Vec<DriftKindDescriptor> { + RAW_SOURCE_REBUILD_DRIFT_KINDS + .iter() + .map(|(variant, code)| DriftKindDescriptor { + variant: (*variant).to_owned(), + code: (*code).to_owned(), + }) + .collect() +} + +fn expected_digest_framing() -> DigestFramingDescriptor { + DigestFramingDescriptor { + section: "S_then_N_then_u64be_length_then_utf8_name".to_owned(), + row: "R".to_owned(), + signed_i64: "I_then_i64be".to_owned(), + boolean: "B_then_u8_0_or_1".to_owned(), + optional: "O_then_presence_u8_then_nested_value_when_present".to_owned(), + text: "T_then_u64be_length_then_utf8_bytes".to_owned(), + blob: "X_then_u64be_length_then_bytes".to_owned(), + } +} + +fn digest_query_descriptors(specs: &[DigestQuerySpec]) -> Vec<DigestQueryDescriptor> { + specs + .iter() + .map(|spec| { + let framing = expected_digest_field_framing(spec.section); + assert_eq!( + spec.fields.len(), + framing.len(), + "governed digest field/framing inventory length" + ); + DigestQueryDescriptor { + section: spec.section.to_owned(), + sql: spec.sql.to_owned(), + fields: spec + .fields + .iter() + .zip(framing) + .map(|(name, framing)| DigestFieldDescriptor { + name: (*name).to_owned(), + framing: (*framing).to_owned(), + }) + .collect(), + } + }) + .collect() +} + +fn expected_digest_field_framing(section: &str) -> &'static [&'static str] { + match section { + "event_envelopes" => &[ + "i64", "text", "text", "i64", "i64", "text", "text", "text", "text", "i64", + ], + "event_envelope_tags" => &["i64", "text", "i64", "text", "optional_text", "text"], + "envelope_classification" => &[ + "text", + "text", + "text", + "optional_text", + "optional_text", + "boolean", + ], + "tag_classification" => &["text", "i64", "optional_text", "optional_text", "boolean"], + "raw_heads" => &["text", "i64", "text", "optional_text", "text", "i64"], + "event_coordinates" => &[ + "text", + "text", + "i64", + "text", + "i64", + "text", + "optional_text", + "optional_text", + "text", + "boolean", + "optional_text", + ], + "nip09_requests" => &["text", "text", "i64"], + "nip09_event_targets" => &["text", "text", "i64", "text"], + "nip09_address_targets" => &[ + "text", "i64", "text", "text", "i64", "i64", "text", "text", "text", "text", + ], + "addressable_heads" => &[ + "i64", + "text", + "text", + "text", + "i64", + "text", + "optional_text", + "optional_text", + "text", + "optional_text", + "optional_text", + "optional_text", + "optional_text", + "optional_i64", + ], + "current_visibility" => &[ + "text", + "text", + "optional_text", + "text", + "optional_text", + "boolean", + "optional_text", + "optional_text", + "optional_text", + "optional_text", + "optional_text", + "optional_i64", + "text", + ], + "food_projection" => &[ + "i64", + "text", + "text", + "text", + "i64", + "text", + "text", + "text", + "text", + "i64", + "text", + "text", + "text", + "text", + "optional_text", + "optional_text", + "text", + "text", + ], + "food_images" => &[ + "text", + "text", + "i64", + "text", + "optional_text", + "optional_i64", + "optional_i64", + "optional_text", + "boolean", + "text", + ], + "food_search" => &["text", "text", "text", "text", "text", "text", "text"], + "food_cursor" => &["i64", "i64", "blob", "text", "i64"], + other => panic!("unrecognized governed digest section `{other}`"), + } +} + +fn validate_manifest_shape(manifest: &RawSourceRebuildManifest) -> Result<(), String> { + let expected_entries = ENTRY_POINTS + .iter() + .map(|(role, rust_path)| EntryPointDescriptor { + role: (*role).to_owned(), + rust_path: (*rust_path).to_owned(), + }) + .collect::<Vec<_>>(); + let expected_public_api = PublicApiDescriptor { + added_symbols: owned(ADDED_PUBLIC_SYMBOLS), + methods: owned(PUBLIC_METHODS), + error_variants: owned(ERROR_VARIANTS), + drift_kinds: expected_drift_kinds(), + }; + if manifest.schema_version != SCHEMA_VERSION + || manifest.contract_id != CONTRACT_ID + || manifest.authority_id != AUTHORITY_ID + || manifest.manifest_schema.path != MANIFEST_SCHEMA_RELATIVE + || manifest.predecessor.contract_id != PREDECESSOR_CONTRACT_ID + || manifest.predecessor.manifest.path != PREDECESSOR_MANIFEST_RELATIVE + || manifest.predecessor.manifest.byte_length + != u64::try_from(PREDECESSOR_MANIFEST_BYTE_LENGTH) + .map_err(|_| "predecessor byte length does not fit u64".to_owned())? + || manifest.predecessor.manifest.sha256 != PREDECESSOR_MANIFEST_SHA256 + || manifest.runtime != expected_runtime() + || manifest.entry_points != expected_entries + || manifest.public_api != expected_public_api + { + return Err(format!( + "{MANIFEST_RELATIVE} has inconsistent raw-source rebuild identity or semantics" + )); + } + let expected_sources = SOURCE_SPECS + .iter() + .map(|spec| (spec.role, spec.path)) + .collect::<Vec<_>>(); + let actual_sources = manifest + .source_files + .iter() + .map(|source| (source.role.as_str(), source.path.as_str())) + .collect::<Vec<_>>(); + if actual_sources != expected_sources { + return Err(format!( + "{MANIFEST_RELATIVE} source-file inventory is not exact" + )); + } + let expected_migrations = MIGRATION_RELATIVES.to_vec(); + let actual_migrations = manifest + .migration_inventory + .iter() + .map(|descriptor| descriptor.path.as_str()) + .collect::<Vec<_>>(); + if actual_migrations != expected_migrations { + return Err(format!( + "{MANIFEST_RELATIVE} migration inventory must remain exactly versions 0001 through 0004" + )); + } + validate_unique( + "raw-source rebuild source roles", + manifest + .source_files + .iter() + .map(|source| source.role.as_str()), + )?; + validate_unique( + "raw-source rebuild source paths", + manifest + .source_files + .iter() + .map(|source| source.path.as_str()), + )?; + for source in &manifest.source_files { + if GENERATED_ARTIFACT_PATHS.contains(&source.path.as_str()) { + return Err(format!( + "{MANIFEST_RELATIVE} recursively hashes generated artifact `{}`", + source.path + )); + } + validate_file_descriptor( + source.path.as_str(), + source.byte_length, + &source.sha256, + &source.hash_algorithm, + )?; + } + for descriptor in manifest + .migration_inventory + .iter() + .chain([&manifest.manifest_schema, &manifest.predecessor.manifest]) + { + validate_file_descriptor( + descriptor.path.as_str(), + descriptor.byte_length, + &descriptor.sha256, + &descriptor.hash_algorithm, + )?; + } + if manifest.result_vector.canonical_path != RESULT_VECTOR_CANONICAL_RELATIVE + || manifest.result_vector.mirror_path != RESULT_VECTOR_MIRROR_RELATIVE + || manifest.result_vector.hash_algorithm != HASH_ALGORITHM + || manifest.result_vector.executor_id != RESULT_VECTOR_EXECUTOR_ID + || manifest.result_vector.executor_path != RESULT_VECTOR_EXECUTOR_RELATIVE + || manifest.result_vector.executor_test != RESULT_VECTOR_EXECUTOR_TEST + || manifest.result_vector.executor_hash_algorithm != HASH_ALGORITHM + || manifest.result_vector.byte_length == 0 + || manifest.result_vector.executor_byte_length == 0 + { + return Err(format!( + "{MANIFEST_RELATIVE} result-vector descriptor is invalid" + )); + } + validate_sha256("result vector", &manifest.result_vector.sha256)?; + validate_sha256( + "result-vector executor", + &manifest.result_vector.executor_sha256, + ) +} + +fn validate_source_contract(workspace_root: &Path) -> Result<(), String> { + validate_source_inventory()?; + validate_complete_event_store_source_closure(workspace_root)?; + validate_migration_inventory(workspace_root)?; + validate_current_event_store_successor_authority(workspace_root)?; + validate_raw_source_rebuild_successor_compiler_inputs( + workspace_root, + EVENT_STORE_SUCCESSOR_COMPILER_TABLES_SHA256, + )?; + validate_delegated_compiler_source_pins(workspace_root)?; + validate_xtask_manifest_authority(workspace_root)?; + validate_predecessor_source_supersession( + workspace_root, + &read_regular_file(workspace_root, PREDECESSOR_MANIFEST_RELATIVE)?, + )?; + validate_successor_compiler_input_authority(workspace_root)?; + validate_public_api_authority(workspace_root)?; + validate_error_authority(workspace_root)?; + validate_runtime_authority(workspace_root)?; + validate_release_authority(workspace_root)?; + validate_command_reachability(workspace_root) +} + +fn validate_source_inventory() -> Result<(), String> { + validate_source_inventory_specs(SOURCE_SPECS) +} + +fn validate_source_inventory_specs(source_specs: &[SourceSpec]) -> Result<(), String> { + validate_unique( + "raw-source rebuild source roles", + source_specs.iter().map(|spec| spec.role), + )?; + validate_unique( + "raw-source rebuild source paths", + source_specs.iter().map(|spec| spec.path), + )?; + for (role, path) in REQUIRED_DELEGATED_COMPILER_SOURCES { + let matches = source_specs + .iter() + .filter(|spec| spec.role == *role && spec.path == *path) + .count(); + if matches != 1 { + return Err(format!( + "raw-source rebuild source inventory must bind delegated compiler input `{role}` at `{path}` exactly once; found {matches}" + )); + } + } + validate_unique("raw-source rebuild stages", REBUILD_STAGES.iter().copied())?; + validate_unique( + "raw-source rebuild failpoint IDs", + REBUILD_FAILPOINTS.iter().map(|failpoint| failpoint.id), + )?; + validate_unique( + "raw-source rebuild failpoint variants", + REBUILD_FAILPOINTS.iter().map(|failpoint| failpoint.variant), + )?; + validate_unique( + "raw-source rebuild rollback vector case IDs", + REBUILD_FAILPOINTS + .iter() + .map(|failpoint| failpoint.rollback_case_id), + )?; + let sources = source_specs + .iter() + .map(|spec| spec.path) + .collect::<BTreeSet<_>>(); + for generated in GENERATED_ARTIFACT_PATHS { + if sources.contains(generated) { + return Err(format!( + "raw-source rebuild generated artifact `{generated}` must not participate in its own source hash graph" + )); + } + } + Ok(()) +} + +fn validate_complete_event_store_source_closure(workspace_root: &Path) -> Result<(), String> { + let actual = governed_regular_file_inventory(workspace_root, "crates/event_store/src")? + .into_iter() + .filter(|relative| relative.ends_with(".rs")) + .collect::<Vec<_>>(); + let mut expected = SOURCE_SPECS + .iter() + .map(|spec| spec.path) + .filter(|relative| { + relative.starts_with("crates/event_store/src/") && relative.ends_with(".rs") + }) + .map(str::to_owned) + .collect::<Vec<_>>(); + expected.push(GENERATED_DESCRIPTOR_RELATIVE.to_owned()); + expected.sort(); + + if actual != expected { + let actual_set = actual.iter().map(String::as_str).collect::<BTreeSet<_>>(); + let expected_set = expected.iter().map(String::as_str).collect::<BTreeSet<_>>(); + let missing = expected_set + .difference(&actual_set) + .copied() + .collect::<Vec<_>>(); + let unexpected = actual_set + .difference(&expected_set) + .copied() + .collect::<Vec<_>>(); + return Err(format!( + "event-store Rust source closure drifted: missing {missing:?}, unexpected {unexpected:?}" + )); + } + Ok(()) +} + +fn validate_delegated_compiler_source_pins(workspace_root: &Path) -> Result<(), String> { + for (relative, expected_sha256) in DELEGATED_COMPILER_SOURCE_PINS { + let actual_sha256 = sha256_hex(&read_regular_file(workspace_root, relative)?); + if actual_sha256 != *expected_sha256 { + return Err(format!( + "delegated compiler source `{relative}` drifted: expected {expected_sha256}, found {actual_sha256}" + )); + } + } + Ok(()) +} + +fn validate_xtask_manifest_authority(workspace_root: &Path) -> Result<(), String> { + let source = regular_utf8_source(workspace_root, XTASK_MANIFEST_RELATIVE)?; + let manifest: toml::Value = toml::from_str(&source) + .map_err(|error| format!("parse {XTASK_MANIFEST_RELATIVE}: {error}"))?; + let package = manifest + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{XTASK_MANIFEST_RELATIVE} must define one package table"))?; + for flag in XTASK_REQUIRED_DISABLED_AUTO_TARGET_FLAGS { + if package.get(*flag).and_then(toml::Value::as_bool) != Some(false) { + return Err(format!( + "{XTASK_MANIFEST_RELATIVE} package.{flag} must be exactly false so delegated compiler targets cannot be auto-discovered" + )); + } + } + if package.get("name").and_then(toml::Value::as_str) != Some("xtask") + || package.get("publish").and_then(toml::Value::as_bool) != Some(false) + || package.contains_key("build") + || package.contains_key("autobins") + || ["lib", "bin", "example", "test", "bench"] + .iter() + .any(|target| manifest.get(*target).is_some()) + { + return Err(format!( + "{XTASK_MANIFEST_RELATIVE} must remain the unpublished single default-binary xtask package with no build script, autobins override, or explicit additional Cargo targets" + )); + } + for relative in XTASK_FORBIDDEN_AUTO_TARGET_PATHS { + match fs::symlink_metadata(workspace_root.join(relative)) { + Ok(_) => { + return Err(format!( + "delegated xtask compiler authority forbids auto-target path `{relative}`" + )); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(format!( + "inspect delegated compiler auto-target path `{relative}`: {error}" + )); + } + } + } + Ok(()) +} + +fn validate_successor_compiler_input_authority(workspace_root: &Path) -> Result<(), String> { + let mut sources = governed_regular_file_inventory(workspace_root, "crates/event_store/src")? + .into_iter() + .filter(|relative| relative.ends_with(".rs")) + .collect::<Vec<_>>(); + sources.push(RESULT_VECTOR_EXECUTOR_RELATIVE.to_owned()); + for relative in sources { + let file = rust_file(workspace_root, &relative)?; + let expected_inputs = expected_successor_compiler_inputs(&relative); + validate_exact_successor_compiler_inputs(&relative, &file, expected_inputs)?; + } + Ok(()) +} + +fn expected_successor_compiler_inputs(relative: &str) -> &'static [&'static str] { + match relative { + "crates/event_store/src/migrations.rs" => &[ + "include_str!(\"../migrations/0001_event_store.up.sql\")", + "include_str!(\"../migrations/0001_event_store.down.sql\")", + "include_str!(\"../migrations/0002_nip09.up.sql\")", + "include_str!(\"../migrations/0002_nip09.down.sql\")", + "include_str!(\"../migrations/0003_food_availability_projection.up.sql\")", + "include_str!(\"../migrations/0003_food_availability_projection.down.sql\")", + "include_str!(\"../migrations/0004_source_maintenance.up.sql\")", + "include_str!(\"../migrations/0004_source_maintenance.down.sql\")", + "env!(\"CARGO_MANIFEST_DIR\")", + ], + "crates/event_store/src/nip09/reconciliation_v1.rs" => &[ + "include_str!(\"../../migrations/0001_event_store.up.sql\")", + "include_str!(\"../../migrations/0002_nip09.up.sql\")", + ], + "crates/event_store/src/nip09/reconciliation_v1/result_vector_executor.rs" => &[ + "include_bytes!(\"../../../tests/fixtures/nip09_reconciliation.v1.json\")", + "include_str!(\"../../../migrations/0001_event_store.up.sql\")", + "include_str!(\"../../../migrations/0002_nip09.up.sql\")", + ], + "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs" => { + &["include_bytes!(\"../../../tests/fixtures/food_availability_projection.v1.json\")"] + } + "crates/event_store/src/store/raw_source_rebuild_v1_tests.rs" => &[ + "include_bytes!(\"../../tests/fixtures/food_availability_projection.v1.json\")", + "include_bytes!(\"../../tests/fixtures/nip09_reconciliation.v1.json\")", + ], + RESULT_VECTOR_EXECUTOR_RELATIVE => &[ + "include_bytes!(\"../../../contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json\")", + "include_bytes!(\"fixtures/food_availability_projection.v1.json\")", + ], + _ => &[], + } +} + +fn validate_exact_successor_compiler_inputs( + relative: &str, + file: &syn::File, + expected_inputs: &[&str], +) -> Result<(), String> { + struct Audit { + inputs: Vec<String>, + path_attributes: Vec<String>, + } + + impl<'ast> syn::visit::Visit<'ast> for Audit { + fn visit_macro(&mut self, item: &'ast syn::Macro) { + if item.path.segments.last().is_some_and(|segment| { + matches!( + segment.ident.to_string().as_str(), + "include" | "include_bytes" | "include_str" | "env" | "option_env" + ) + }) { + self.inputs.push(compact_tokens(item)); + } + syn::visit::visit_macro(self, item); + } + + fn visit_attribute(&mut self, attribute: &'ast syn::Attribute) { + if attribute.path().is_ident("path") + || (attribute.path().is_ident("cfg_attr") + && token_stream_contains_ident(attribute.meta.to_token_stream(), "path")) + { + self.path_attributes.push(compact_tokens(attribute)); + } + syn::visit::visit_attribute(self, attribute); + } + } + + use syn::visit::Visit; + let mut audit = Audit { + inputs: Vec::new(), + path_attributes: Vec::new(), + }; + audit.visit_file(file); + let expected_inputs = expected_inputs + .iter() + .map(|input| (*input).to_owned()) + .collect::<Vec<_>>(); + if audit.inputs != expected_inputs || !audit.path_attributes.is_empty() { + return Err(format!( + "{relative} successor compiler-input authority drifted: expected {expected_inputs:?} and no path retargeting, found {:?} and {:?}", + audit.inputs, audit.path_attributes + )); + } + Ok(()) +} + +fn token_stream_contains_ident(tokens: proc_macro2::TokenStream, expected: &str) -> bool { + tokens.into_iter().any(|token| match token { + proc_macro2::TokenTree::Ident(ident) => ident == expected, + proc_macro2::TokenTree::Group(group) => { + token_stream_contains_ident(group.stream(), expected) + } + proc_macro2::TokenTree::Punct(_) | proc_macro2::TokenTree::Literal(_) => false, + }) +} + +fn validate_predecessor_identity(bytes: &[u8]) -> Result<(), String> { + if bytes.len() != PREDECESSOR_MANIFEST_BYTE_LENGTH + || sha256_hex(bytes) != PREDECESSOR_MANIFEST_SHA256 + { + return Err(format!( + "{PREDECESSOR_MANIFEST_RELATIVE} does not match the immutable SourceMaintenance predecessor identity" + )); + } + Ok(()) +} + +fn validate_predecessor_source_supersession( + workspace_root: &Path, + predecessor_bytes: &[u8], +) -> Result<(), String> { + validate_predecessor_identity(predecessor_bytes)?; + let predecessor: Value = serde_json::from_slice(predecessor_bytes) + .map_err(|error| format!("parse {PREDECESSOR_MANIFEST_RELATIVE}: {error}"))?; + let descriptors = predecessor + .get("source_files") + .and_then(Value::as_array) + .ok_or_else(|| format!("{PREDECESSOR_MANIFEST_RELATIVE} has no source_files array"))?; + let successor_paths = SOURCE_SPECS + .iter() + .map(|spec| spec.path) + .collect::<BTreeSet<_>>(); + let expected_drift = EXPECTED_SOURCE_MAINTENANCE_DRIFT_PATHS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + if expected_drift.len() != EXPECTED_SOURCE_MAINTENANCE_DRIFT_PATHS.len() { + return Err( + "raw-source rebuild expected predecessor drift paths must be unique".to_owned(), + ); + } + if let Some(path) = expected_drift + .iter() + .find(|path| !successor_paths.contains(**path)) + { + return Err(format!( + "raw-source rebuild successor does not current-byte-bind expected changed SourceMaintenance source `{path}`" + )); + } + let mut predecessor_paths = BTreeSet::new(); + let mut actual_drift = BTreeSet::new(); + for descriptor in descriptors { + let path = descriptor + .get("path") + .and_then(Value::as_str) + .ok_or_else(|| "predecessor source descriptor has no path".to_owned())?; + let predecessor_sha256 = descriptor + .get("sha256") + .and_then(Value::as_str) + .ok_or_else(|| format!("predecessor source descriptor `{path}` has no sha256"))?; + if !predecessor_paths.insert(path) { + return Err(format!( + "{PREDECESSOR_MANIFEST_RELATIVE} contains duplicate source descriptor `{path}`" + )); + } + let current = read_regular_file(workspace_root, path)?; + if sha256_hex(&current) != predecessor_sha256 { + actual_drift.insert(path); + } + } + if actual_drift != expected_drift { + return Err(format!( + "raw-source rebuild SourceMaintenance drift inventory differs: expected {expected_drift:?}, found {actual_drift:?}" + )); + } + + let transitive = TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + if transitive.len() != TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS.len() { + return Err( + "raw-source rebuild transitive predecessor supersession paths must be unique" + .to_owned(), + ); + } + if let Some(path) = transitive + .iter() + .find(|path| !successor_paths.contains(**path) && !predecessor_paths.contains(**path)) + { + return Err(format!( + "raw-source rebuild transitive supersession path `{path}` is not bound by the current successor or immutable SourceMaintenance predecessor" + )); + } + validate_food_availability_projection_predecessor_production_sources_under_lock( + workspace_root, + TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS, + )?; + Ok(()) +} + +fn validate_migration_inventory(workspace_root: &Path) -> Result<(), String> { + let migration_root = workspace_root.join("crates/event_store/migrations"); + let mut actual = fs::read_dir(&migration_root) + .map_err(|error| format!("read {}: {error}", migration_root.display()))? + .map(|entry| { + let entry = entry.map_err(|error| format!("read migration entry: {error}"))?; + if !entry + .file_type() + .map_err(|error| format!("inspect {}: {error}", entry.path().display()))? + .is_file() + { + return Err(format!( + "migration inventory entry {} must be a regular file", + entry.path().display() + )); + } + entry + .path() + .strip_prefix(workspace_root) + .map(|path| path.to_string_lossy().into_owned()) + .map_err(|error| format!("relativize migration path: {error}")) + }) + .collect::<Result<Vec<_>, String>>()?; + actual.sort(); + if actual != MIGRATION_RELATIVES { + return Err(format!( + "raw-source rebuild is runtime-only and requires the exact 0001-through-0004 migration inventory; found {actual:?}" + )); + } + Ok(()) +} + +fn validate_public_api_authority(workspace_root: &Path) -> Result<(), String> { + let model = rust_file( + workspace_root, + "crates/event_store/src/model/raw_source_rebuild_v1.rs", + )?; + let error = rust_file(workspace_root, "crates/event_store/src/error.rs")?; + let lib = rust_file(workspace_root, "crates/event_store/src/lib.rs")?; + let store = rust_file(workspace_root, "crates/event_store/src/store.rs")?; + + validate_digest_newtype(&model, "RadrootsEventStoreImmutableRawDigestV1")?; + validate_digest_newtype(&model, "RadrootsEventStoreActiveProductStateDigestV1")?; + validate_report_model(&model)?; + validate_caller_inbound_foreign_key_model(&error)?; + + let routes = collect_top_level_public_use_routes(&lib); + for symbol in ADDED_PUBLIC_SYMBOLS { + let expected_module = match *symbol { + "RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1" + | "RadrootsEventStoreCallerInboundForeignKeyV1" + | "RadrootsEventStoreRawSourceRebuildDriftV1" => "error", + _ => "model", + }; + let matches = routes + .iter() + .filter(|route| route.exported_name == *symbol) + .collect::<Vec<_>>(); + let [route] = matches.as_slice() else { + return Err(format!( + "crate root must export raw-source rebuild symbol `{symbol}` exactly once; found {}", + matches.len() + )); + }; + if route.attributes != ["#[cfg(feature=\"sqlite\")]"].map(str::to_owned) + || route.absolute + || route.renamed + || route.glob + || route.segments.as_slice() != [expected_module, *symbol] + { + return Err(format!( + "crate-root raw-source rebuild export `{symbol}` must be direct, non-renamed, and sqlite-gated from {expected_module}" + )); + } + } + + validate_public_method_signatures(&store)?; + validate_store_public_method_surface(&store) +} + +fn validate_public_method_signatures(store: &syn::File) -> Result<(), String> { + for (method, expected_signature) in [ + ( + "rebuild_from_raw_v1", + "pub async fn rebuild_from_raw_v1(&self,) -> Result<RadrootsEventStoreRawSourceRebuildReportV1, RadrootsEventStoreError>", + ), + ( + "repair_file_from_raw_v1", + "pub async fn repair_file_from_raw_v1(path: impl AsRef<Path>,) -> Result<(Self, RadrootsEventStoreRawSourceRebuildReportV1), RadrootsEventStoreError>", + ), + ] { + let function = exact_associated_method(store, "RadrootsEventStore", method)?; + let actual = compact_tokens(&function.sig); + let expected = compact_signature(expected_signature)?; + if actual != expected { + return Err(format!( + "RadrootsEventStore::{method} signature drifted: expected `{expected}`, found `{actual}`" + )); + } + } + Ok(()) +} + +fn validate_store_public_method_surface(file: &syn::File) -> Result<(), String> { + const EXPECTED_SHA256: &str = + "f96da738c7c24b9ebdc99f405035f7f9e0758d4e1d83f2a8de0b2877309eeb87"; + let signatures = file + .items + .iter() + .filter_map(|item| match item { + Item::Impl(item) + if item.trait_.is_none() + && compact_tokens(item.self_ty.as_ref()) == "RadrootsEventStore" => + { + Some(item) + } + _ => None, + }) + .flat_map(|item| &item.items) + .filter_map(|item| match item { + syn::ImplItem::Fn(function) if matches!(function.vis, syn::Visibility::Public(_)) => { + Some(compact_tokens(&function.sig)) + } + _ => None, + }) + .collect::<Vec<_>>(); + validate_unique( + "RadrootsEventStore public method signatures", + signatures.iter().map(String::as_str), + )?; + let actual_sha256 = sha256_hex(signatures.join("\n").as_bytes()); + if actual_sha256 != EXPECTED_SHA256 { + return Err(format!( + "RadrootsEventStore complete public method surface drifted: expected {EXPECTED_SHA256}, found {actual_sha256}" + )); + } + Ok(()) +} + +fn validate_digest_newtype(file: &syn::File, name: &str) -> Result<(), String> { + let item = exact_struct(file, name)?; + let mut item = item.clone(); + strip_doc_attributes(&mut item.attrs); + for field in &mut item.fields { + strip_doc_attributes(&mut field.attrs); + } + let expected = syn::parse_str::<syn::ItemStruct>(&format!( + "#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct {name}(pub(crate) [u8; 32]);" + )) + .map_err(|error| format!("parse authoritative digest model `{name}`: {error}"))?; + if compact_tokens(&item) != compact_tokens(&expected) { + return Err(format!( + "{name} must remain an opaque, fixed-width, Copy SHA-256 newtype" + )); + } + let inherent = exact_impl(file, name)?; + let methods = inherent + .items + .iter() + .filter_map(|item| match item { + syn::ImplItem::Fn(function) => Some((function.sig.ident.to_string(), function)), + _ => None, + }) + .collect::<BTreeMap<_, _>>(); + if methods.keys().cloned().collect::<Vec<_>>() != ["as_bytes", "from_bytes"] { + return Err(format!( + "{name} must expose only public as_bytes plus crate-private from_bytes" + )); + } + let from_bytes = methods["from_bytes"]; + let as_bytes = methods["as_bytes"]; + if compact_tokens(&from_bytes.sig) + != compact_signature("pub(crate) const fn from_bytes(bytes: [u8; 32]) -> Self")? + || compact_tokens(&as_bytes.sig) + != compact_signature("pub const fn as_bytes(&self) -> &[u8; 32]")? + { + return Err(format!("{name} constructor or accessor signature drifted")); + } + Ok(()) +} + +fn validate_report_model(file: &syn::File) -> Result<(), String> { + let item = exact_struct(file, "RadrootsEventStoreRawSourceRebuildReportV1")?; + let mut item = item.clone(); + strip_doc_attributes(&mut item.attrs); + for field in &mut item.fields { + strip_doc_attributes(&mut field.attrs); + } + let expected = syn::parse_str::<syn::ItemStruct>( + r#"#[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct RadrootsEventStoreRawSourceRebuildReportV1 { + pub(crate) prior_source_generation: RadrootsEventStoreSourceGeneration, + pub(crate) new_source_generation: RadrootsEventStoreSourceGeneration, + pub(crate) source_capacity: RadrootsEventStoreSourceCapacityV1, + pub(crate) immutable_raw_digest: RadrootsEventStoreImmutableRawDigestV1, + pub(crate) active_product_state_digest: RadrootsEventStoreActiveProductStateDigestV1, + }"#, + ) + .map_err(|error| format!("parse authoritative rebuild report: {error}"))?; + if compact_tokens(&item) != compact_tokens(&expected) { + return Err( + "RadrootsEventStoreRawSourceRebuildReportV1 field or visibility authority drifted" + .to_owned(), + ); + } + let inherent = exact_impl(file, "RadrootsEventStoreRawSourceRebuildReportV1")?; + let actual = inherent + .items + .iter() + .filter_map(|item| match item { + syn::ImplItem::Fn(function) => Some(function.sig.ident.to_string()), + _ => None, + }) + .collect::<Vec<_>>(); + let expected = [ + "prior_source_generation", + "new_source_generation", + "source_capacity", + "raw_high_water_seq", + "immutable_raw_digest", + "active_product_state_digest", + ]; + if actual != expected { + return Err(format!( + "raw-source rebuild report accessor inventory differs: expected {expected:?}, found {actual:?}" + )); + } + Ok(()) +} + +fn validate_caller_inbound_foreign_key_model(file: &syn::File) -> Result<(), String> { + let item = exact_struct(file, "RadrootsEventStoreCallerInboundForeignKeyV1")?; + let mut item = item.clone(); + strip_doc_attributes(&mut item.attrs); + for field in &mut item.fields { + strip_doc_attributes(&mut field.attrs); + } + let expected = syn::parse_str::<syn::ItemStruct>( + r#" + #[non_exhaustive] + #[derive(Debug, PartialEq, Eq)] + pub struct RadrootsEventStoreCallerInboundForeignKeyV1 { + pub child_table: String, + pub foreign_key_id: i64, + pub foreign_key_sequence: i64, + pub child_column: String, + pub parent_table: String, + pub parent_column: Option<String>, + pub on_update: String, + pub on_delete: String, + pub match_clause: String, + } + "#, + ) + .map_err(|error| format!("parse caller inbound foreign-key model: {error}"))?; + if compact_tokens(&item) != compact_tokens(&expected) { + return Err( + "RadrootsEventStoreCallerInboundForeignKeyV1 field, visibility, or derive authority drifted" + .to_owned(), + ); + } + + let display = file + .items + .iter() + .filter_map(|item| match item { + Item::Impl(item) + if compact_tokens(item.self_ty.as_ref()) + == "RadrootsEventStoreCallerInboundForeignKeyV1" + && item.trait_.as_ref().is_some_and(|(_, path, _)| { + compact_tokens(path) == "core::fmt::Display" + }) => + { + Some(item) + } + _ => None, + }) + .collect::<Vec<_>>(); + let [display] = display.as_slice() else { + return Err(format!( + "RadrootsEventStoreCallerInboundForeignKeyV1 must define one Display authority; found {}", + display.len() + )); + }; + let expected_display = syn::parse_str::<syn::ItemImpl>( + r#" + impl core::fmt::Display for RadrootsEventStoreCallerInboundForeignKeyV1 { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + formatter, + "{}:{} on `{}` (`{}` -> `{}`.", + self.foreign_key_id, + self.foreign_key_sequence, + self.child_table, + self.child_column, + self.parent_table, + )?; + match self.parent_column.as_deref() { + Some(parent_column) => write!(formatter, "`{parent_column}`")?, + None => formatter.write_str("<implicit primary key>")?, + } + write!( + formatter, + ", on update {}, on delete {}, match {})", + self.on_update, + self.on_delete, + self.match_clause, + ) + } + } + "#, + ) + .map_err(|error| format!("parse caller inbound foreign-key Display authority: {error}"))?; + if compact_tokens(*display) != compact_tokens(&expected_display) { + return Err( + "RadrootsEventStoreCallerInboundForeignKeyV1 Display authority drifted".to_owned(), + ); + } + Ok(()) +} + +fn validate_error_authority(workspace_root: &Path) -> Result<(), String> { + let file = rust_file(workspace_root, "crates/event_store/src/error.rs")?; + validate_raw_source_rebuild_drift_taxonomy(&file)?; + let limit = file + .items + .iter() + .filter_map(|item| match item { + Item::Const(item) + if item.ident == "RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1" => + { + Some(item) + } + _ => None, + }) + .collect::<Vec<_>>(); + let [limit] = limit.as_slice() else { + return Err(format!( + "projection-cursor capacity authority must define its public limit exactly once; found {}", + limit.len() + )); + }; + let mut limit = (*limit).clone(); + strip_doc_attributes(&mut limit.attrs); + let expected_limit = syn::parse_str::<syn::ItemConst>( + "pub const RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1: u32 = 4_096;", + ) + .map_err(|error| format!("parse projection-cursor limit authority: {error}"))?; + if compact_tokens(&limit) != compact_tokens(&expected_limit) { + return Err( + "RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1 must remain exactly 4,096" + .to_owned(), + ); + } + let errors = exact_enum(&file, "RadrootsEventStoreError")?; + const EXPECTED_ERROR_ENUM_SHA256: &str = + "dcb9416ca05bda35845f8708fe73132df7137b0c0e002e8ba6d709989bc31939"; + let actual_error_enum_sha256 = sha256_hex(compact_tokens(errors).as_bytes()); + if actual_error_enum_sha256 != EXPECTED_ERROR_ENUM_SHA256 { + return Err(format!( + "RadrootsEventStoreError complete variant surface drifted: expected {EXPECTED_ERROR_ENUM_SHA256}, found {actual_error_enum_sha256}" + )); + } + let variants = errors + .variants + .iter() + .map(|variant| (variant.ident.to_string(), variant)) + .collect::<BTreeMap<_, _>>(); + for name in ERROR_VARIANTS { + if !variants.contains_key(*name) { + return Err(format!( + "RadrootsEventStoreError must define raw-source rebuild variant `{name}`" + )); + } + } + let drift = variants["RawSourceRebuildStateDrift"]; + let rollback = variants["RawSourceRebuildTransactionRollbackFailed"]; + let cursor_capacity = variants["ProjectionCursorCapacityExceeded"]; + let repair_identity = variants["RawSourceRepairDatabaseIdentityMismatch"]; + let repair_lock_domain = variants["RawSourceRepairCanonicalPathLockDomainMismatch"]; + let repair_canonicalization = variants["RawSourceRepairMainDatabaseCanonicalizationFailed"]; + let caller_table_capacity = variants["RawSourceRebuildCallerTableCapacityExceeded"]; + let caller_foreign_key_capacity = variants["RawSourceRebuildCallerForeignKeyCapacityExceeded"]; + let caller_inbound_foreign_key = variants["RawSourceRebuildCallerInboundForeignKeyUnsupported"]; + let drift_fields = compact_tokens(&drift.fields); + let rollback_fields = compact_tokens(&rollback.fields); + if drift_fields != "{kind:RadrootsEventStoreRawSourceRebuildDriftV1,detail:String,}" { + return Err( + "RawSourceRebuildStateDrift must carry one stable drift kind and diagnostic detail" + .to_owned(), + ); + } + if rollback_fields != "{#[source]primary:Box<RadrootsEventStoreError>,rollback:sqlx::Error,}" { + return Err(format!( + "RawSourceRebuildTransactionRollbackFailed must preserve typed primary and SQL rollback errors; found `{rollback_fields}`" + )); + } + if compact_tokens(&cursor_capacity.fields) != "{current:u32,limit:u32}" { + return Err( + "ProjectionCursorCapacityExceeded must carry exact current and limit u32 fields" + .to_owned(), + ); + } + if compact_tokens(&repair_identity.fields) != "{expected:String,actual:String}" + || compact_tokens(&repair_lock_domain.fields) != "{canonical_path:String}" + || compact_tokens(&repair_canonicalization.fields) + != "{filename:String,#[source]source:std::io::Error,}" + { + return Err( + "raw-source file repair errors must retain their exact typed fields".to_owned(), + ); + } + if compact_tokens(&caller_table_capacity.fields) != "{observed_at_least:u64,limit:u64}" + || compact_tokens(&caller_foreign_key_capacity.fields) + != "{observed_at_least:u64,limit:u64}" + || compact_tokens(&caller_inbound_foreign_key.fields) + != "{dependency:Box<RadrootsEventStoreCallerInboundForeignKeyV1>,}" + { + return Err( + "raw-source rebuild caller-schema errors must retain their exact typed fields" + .to_owned(), + ); + } + let drift_attrs = drift.attrs.iter().map(compact_tokens).collect::<Vec<_>>(); + let rollback_attrs = rollback + .attrs + .iter() + .map(compact_tokens) + .collect::<Vec<_>>(); + if !drift_attrs.iter().any(|attribute| { + attribute.contains("event-storeraw-sourcerebuildauthorityisinconsistent({kind}):{detail}") + }) || !rollback_attrs.iter().any(|attribute| { + attribute + .contains("raw-sourcerebuildfailed:{primary};transactionrollbackalsofailed:{rollback}") + }) || !cursor_capacity + .attrs + .iter() + .map(compact_tokens) + .any(|attribute| { + attribute.contains( + "event-storegenericprojectioncursorcapacityexceeded:current{current},limit{limit}", + ) + }) + || !repair_identity.attrs.iter().map(compact_tokens).any(|attribute| { + attribute.contains( + "raw-sourcerepairSQLitemaindatabaseidentitymismatch:expected`{expected}`,found`{actual}`", + ) + }) + || !repair_lock_domain + .attrs + .iter() + .map(compact_tokens) + .any(|attribute| { + attribute.contains( + "raw-sourcerepaircanonicalpath`{canonical_path}`doesnotsharethevalidatedSQLitemainlockdomain", + ) + }) + || !repair_canonicalization + .attrs + .iter() + .map(compact_tokens) + .any(|attribute| { + attribute.contains( + "raw-sourcerepaircouldnotcanonicalizeSQLitemaindatabase`{filename}`:{source}", + ) + }) + || !caller_table_capacity + .attrs + .iter() + .map(compact_tokens) + .any(|attribute| { + attribute.contains( + "event-storeraw-sourcerebuildcallermain-tableinventoryexceedsboundedpreflightcapacity:observedatleast{observed_at_least},limit{limit}", + ) + }) + || !caller_foreign_key_capacity + .attrs + .iter() + .map(compact_tokens) + .any(|attribute| { + attribute.contains( + "event-storeraw-sourcerebuildcallerforeign-keyinventoryexceedsboundedpreflightcapacity:observedatleast{observed_at_least}rows,limit{limit}", + ) + }) + || !caller_inbound_foreign_key + .attrs + .iter() + .map(compact_tokens) + .any(|attribute| { + attribute.contains( + "event-storeraw-sourcerebuilddoesnotsupportcaller-ownedforeignkey{dependency}", + ) + }) + { + return Err("raw-source rebuild typed error display contract drifted".to_owned()); + } + Ok(()) +} + +fn validate_raw_source_rebuild_drift_taxonomy(file: &syn::File) -> Result<(), String> { + let item = exact_enum(file, "RadrootsEventStoreRawSourceRebuildDriftV1")?; + let mut item = item.clone(); + strip_doc_attributes(&mut item.attrs); + for variant in &mut item.variants { + strip_doc_attributes(&mut variant.attrs); + } + let expected = syn::parse_str::<syn::ItemEnum>( + r#" + #[non_exhaustive] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum RadrootsEventStoreRawSourceRebuildDriftV1 { + ManagedSchemaAuthority, + ImmutableRawAuthority, + SourceGenerationLineage, + AddressableTransitionAuthority, + DerivedProductStateAuthority, + RebuildPostcondition, + } + "#, + ) + .map_err(|error| format!("parse raw-source rebuild drift taxonomy: {error}"))?; + if compact_tokens(&item) != compact_tokens(&expected) { + return Err( + "RadrootsEventStoreRawSourceRebuildDriftV1 variant or derive authority drifted" + .to_owned(), + ); + } + + let code = exact_associated_method(file, "RadrootsEventStoreRawSourceRebuildDriftV1", "code")?; + let mut code = code.clone(); + strip_doc_attributes(&mut code.attrs); + let expected_code = syn::parse_str::<syn::ImplItemFn>( + r#" + pub const fn code(self) -> &'static str { + match self { + Self::ManagedSchemaAuthority => "managed_schema_authority", + Self::ImmutableRawAuthority => "immutable_raw_authority", + Self::SourceGenerationLineage => "source_generation_lineage", + Self::AddressableTransitionAuthority => "addressable_transition_authority", + Self::DerivedProductStateAuthority => "derived_product_state_authority", + Self::RebuildPostcondition => "rebuild_postcondition", + } + } + "#, + ) + .map_err(|error| format!("parse raw-source rebuild drift code authority: {error}"))?; + if compact_tokens(&code) != compact_tokens(&expected_code) { + return Err("RadrootsEventStoreRawSourceRebuildDriftV1::code mapping drifted".to_owned()); + } + + let display_impls = file + .items + .iter() + .filter_map(|item| match item { + Item::Impl(item) + if compact_tokens(item.self_ty.as_ref()) + == "RadrootsEventStoreRawSourceRebuildDriftV1" + && item.trait_.as_ref().is_some_and(|(_, path, _)| { + compact_tokens(path) == "core::fmt::Display" + }) => + { + Some(item) + } + _ => None, + }) + .collect::<Vec<_>>(); + let [display] = display_impls.as_slice() else { + return Err(format!( + "RadrootsEventStoreRawSourceRebuildDriftV1 must implement Display exactly once; found {}", + display_impls.len() + )); + }; + let expected_display = syn::parse_str::<syn::ItemImpl>( + r#" + impl core::fmt::Display for RadrootsEventStoreRawSourceRebuildDriftV1 { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(self.code()) + } + } + "#, + ) + .map_err(|error| format!("parse raw-source rebuild drift Display authority: {error}"))?; + if compact_tokens(*display) != compact_tokens(&expected_display) { + return Err("RadrootsEventStoreRawSourceRebuildDriftV1 Display mapping drifted".to_owned()); + } + Ok(()) +} + +fn validate_runtime_authority(workspace_root: &Path) -> Result<(), String> { + let rebuild_relative = REBUILD_RUNTIME_SOURCE_RELATIVE; + let rebuild = rust_source(workspace_root, rebuild_relative)?; + let rebuild_file = + syn::parse_file(&rebuild).map_err(|error| format!("parse {rebuild_relative}: {error}"))?; + for (name, domain) in [ + ("IMMUTABLE_RAW_DIGEST_DOMAIN_V1", RAW_DIGEST_DOMAIN_UTF8), + ( + "ACTIVE_PRODUCT_STATE_DIGEST_DOMAIN_V1", + PRODUCT_DIGEST_DOMAIN_UTF8, + ), + ] { + let mut expected = domain.as_bytes().to_vec(); + expected.push(0); + if exact_byte_string_const(&rebuild_file, name)? != expected { + return Err(format!( + "{rebuild_relative} `{name}` must be exact UTF-8 domain bytes followed by one NUL terminator" + )); + } + } + validate_failpoint_authority(&rebuild_file, rebuild_relative)?; + let failpoint_test_file = rust_file(workspace_root, REBUILD_FAILPOINT_TEST_SOURCE_RELATIVE)?; + validate_failpoint_test_array_authority( + &failpoint_test_file, + REBUILD_FAILPOINT_TEST_SOURCE_RELATIVE, + )?; + let reconciliation_relative = "crates/event_store/src/nip09/reconciliation_v1.rs"; + let reconciliation_file = rust_file(workspace_root, reconciliation_relative)?; + validate_rebuild_marker_token_authority( + &reconciliation_file, + reconciliation_relative, + &rebuild_file, + rebuild_relative, + )?; + validate_coordinator_authority(&rebuild_file, rebuild_relative)?; + validate_caller_schema_dependency_authority(&rebuild_file, rebuild_relative)?; + validate_transition_sequence_authority(&rebuild_file, rebuild_relative)?; + validate_scoped_integrity_authority(&rebuild_file, rebuild_relative)?; + validate_digest_query_authority(&rebuild_file, rebuild_relative)?; + if rebuild.contains("json_array(") + || rebuild.contains("Vec<String>") + || rebuild.contains("update_product_rows") + || rebuild.contains("update_active_product_rows") + { + return Err( + "active product digest must hash typed binary fields, not SQLite JSON row serialization" + .to_owned(), + ); + } + for helper in [ + "digest_section", + "digest_row_start", + "digest_i64", + "digest_text", + "digest_optional_text", + ] { + exact_free_function(&rebuild_file, helper).map_err(|error| { + format!("typed digest framing helper `{helper}` is missing: {error}") + })?; + } + + let oracle_relative = "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs"; + let oracle = rust_source(workspace_root, oracle_relative)?; + let oracle_file = + syn::parse_file(&oracle).map_err(|error| format!("parse {oracle_relative}: {error}"))?; + for marker in [ + "audit_current_visibility_from_raw_v1", + "ReconciledEvent", + "StoredEventClass::Regular", + "StoredEventClass::Replaceable", + "StoredEventClass::Addressable", + "kind_u32", + ] { + if !oracle.contains(marker) { + return Err(format!( + "{oracle_relative} is missing independent visibility-oracle witness `{marker}`" + )); + } + } + for forbidden in ["radroots_event_store_current_visibility_v1", "sqlx::query"] { + if oracle.contains(forbidden) { + return Err(format!( + "independent raw-snapshot visibility oracle must not query derived visibility authority `{forbidden}`" + )); + } + } + validate_visibility_oracle_index_authority(&oracle_file, oracle_relative)?; + + let store_relative = "crates/event_store/src/store.rs"; + let store = rust_source(workspace_root, store_relative)?; + let store_file = + syn::parse_file(&store).map_err(|error| format!("parse {store_relative}: {error}"))?; + validate_public_entry_point_authority(&store_file, store_relative)?; + validate_streaming_dependency_authority(workspace_root)?; + + let reconciliation_relative = "crates/event_store/src/nip09/reconciliation_v1.rs"; + let reconciliation = rust_source(workspace_root, reconciliation_relative)?; + let reconciliation_file = syn::parse_file(&reconciliation) + .map_err(|error| format!("parse {reconciliation_relative}: {error}"))?; + validate_reconciliation_direct_request_index_authority( + &reconciliation_file, + reconciliation_relative, + )?; + let validation = compact_tokens(exact_free_function( + &reconciliation_file, + "validate_projection_cursor_authority", + )?); + for marker in [ + "RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1", + "+1", + "SELECT1FROMprojection_cursorLIMIT?", + "SELECT1FROMradroots_event_store_projection_cursor_sourceLIMIT?", + "validate_projection_cursor_cardinality_v1(cursor_probe.len())?", + "validate_projection_cursor_cardinality_v1(identity_probe.len())?", + "LIMIT1", + ] { + if !validation.contains(marker) { + return Err(format!( + "bounded generic projection-cursor validation is missing `{marker}`" + )); + } + } + if validation.contains("COUNT(") { + return Err( + "generic projection-cursor validation must use cap-plus-one probes, not unbounded counts" + .to_owned(), + ); + } + let preflight = compact_tokens(exact_free_function( + &reconciliation_file, + "preflight_projection_cursor_insert_v1", + )?); + for marker in [ + "SELECT1FROMprojection_cursorLIMIT?", + "RADROOTS_EVENT_STORE_PROJECTION_CURSOR_COUNT_LIMIT_V1", + "ProjectionCursorCapacityExceeded", + ] { + if !preflight.contains(marker) { + return Err(format!( + "projection-cursor prospective insert preflight is missing `{marker}`" + )); + } + } + if rebuild.contains("validate_projection_cursor_authority") + || rebuild.contains("preflight_projection_cursor_insert_v1") + || oracle.contains("projection_cursor") + { + return Err( + "public raw-source rebuild must not enumerate caller-owned generic projection cursors" + .to_owned(), + ); + } + let schema_relative = "crates/event_store/src/schema.rs"; + let schema_source = rust_source(workspace_root, schema_relative)?; + let schema_file = syn::parse_file(&schema_source) + .map_err(|error| format!("parse {schema_relative}: {error}"))?; + let schema_version = schema_file + .items + .iter() + .filter_map(|item| match item { + Item::Const(item) if item.ident == "RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1" => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + let [schema_version] = schema_version.as_slice() else { + return Err(format!( + "{schema_relative} must define one dedicated raw-source rebuild schema version; found {}", + schema_version.len() + )); + }; + if compact_tokens(schema_version) != "constRAW_SOURCE_REBUILD_SCHEMA_VERSION_V1:u32=4;" { + return Err( + "raw-source rebuild maintenance authority must remain pinned to literal schema v4" + .to_owned(), + ); + } + let exact_v4 = compact_tokens(exact_free_function( + &schema_file, + "validate_exact_managed_v4_for_raw_source_rebuild_v1", + )?); + for marker in [ + "validate_embedded_migration_registry()?", + "validate_repair_temp_schema_bounded_v1(connection,EVENT_STORE_MIGRATIONS).await?", + "read_repair_catalog_bounded_v1(connection,EVENT_STORE_MIGRATIONS).await?", + "validate_ledger_catalog(&catalog)?", + "read_repair_history_bounded_v1(connection,RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1)", + "validate_history_against_registry(&history,EVENT_STORE_MIGRATIONS,RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1,)?", + "current!=RAW_SOURCE_REBUILD_SCHEMA_VERSION_V1", + "catalog_fingerprint(&governed_catalog(&catalog,EVENT_STORE_MIGRATIONS))", + "actual!=migration.schema_sha256", + ] { + if !exact_v4.contains(marker) { + return Err(format!( + "exact managed-v4 maintenance validator is missing `{marker}`" + )); + } + } + for forbidden in [ + "RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT", + "validate_event_store_temp_schema_with_registry", + "read_catalog(", + "read_history(", + "validate_active_hook_state", + "validate_food_availability_projection_hook", + ] { + if exact_v4.contains(forbidden) { + return Err(format!( + "exact managed-v4 maintenance validator must not depend on `{forbidden}`" + )); + } + } + validate_bounded_repair_schema_authority(&schema_file, schema_relative)?; + let store_tokens = compact_tokens(&store_file); + if store_tokens + .matches("preflight_projection_cursor_insert_v1") + .count() + < 3 + { + return Err( + "both supported generic projection-cursor insert paths must reach prospective capacity preflight" + .to_owned(), + ); + } + Ok(()) +} + +fn validate_rebuild_marker_token_authority( + reconciliation: &syn::File, + reconciliation_relative: &str, + rebuild: &syn::File, + rebuild_relative: &str, +) -> Result<(), String> { + let token = exact_struct(reconciliation, "SourceRebuildMarkerTokenV1")?; + let mut token = token.clone(); + strip_doc_attributes(&mut token.attrs); + for field in &mut token.fields { + strip_doc_attributes(&mut field.attrs); + } + let expected_token = syn::parse_str::<syn::ItemStruct>( + "struct SourceRebuildMarkerTokenV1 { generation: RadrootsEventStoreSourceGeneration, }", + ) + .map_err(|error| format!("parse rebuild marker token authority: {error}"))?; + if compact_tokens(&token) != compact_tokens(&expected_token) { + return Err(format!( + "{reconciliation_relative} rebuild marker token must remain private, single-field, and non-Clone/non-Copy" + )); + } + + let open = exact_free_function(reconciliation, "open_source_rebuild_marker")?; + let close = exact_free_function(reconciliation, "close_source_rebuild_marker")?; + if compact_tokens(&open.sig) + != compact_signature( + "async fn open_source_rebuild_marker(connection: &mut SqliteConnection, plan: &SourceRebuildPlan,) -> Result<SourceRebuildMarkerTokenV1, RadrootsEventStoreError>", + )? + || compact_tokens(&close.sig) + != compact_signature( + "async fn close_source_rebuild_marker(connection: &mut SqliteConnection, marker: SourceRebuildMarkerTokenV1,) -> Result<(), RadrootsEventStoreError>", + )? + { + return Err(format!( + "{reconciliation_relative} marker open/close signatures must create and consume the exact rebuild token" + )); + } + let open_body = compact_tokens(&open.block); + let close_body = compact_tokens(&close.block); + if open_body + .matches("SourceRebuildMarkerTokenV1{generation:plan.generation,}") + .count() + != 1 + || close_body.matches("marker.generation").count() != 1 + { + return Err(format!( + "{reconciliation_relative} marker token construction or generation-bound close authority drifted" + )); + } + + struct MarkerTokenConstructionCounter(usize); + impl<'ast> syn::visit::Visit<'ast> for MarkerTokenConstructionCounter { + fn visit_expr_struct(&mut self, expression: &'ast syn::ExprStruct) { + if expression + .path + .segments + .last() + .is_some_and(|segment| segment.ident == "SourceRebuildMarkerTokenV1") + { + self.0 += 1; + } + syn::visit::visit_expr_struct(self, expression); + } + } + use syn::visit::Visit; + let mut constructions = MarkerTokenConstructionCounter(0); + constructions.visit_file(reconciliation); + constructions.visit_file(rebuild); + if constructions.0 != 1 { + return Err(format!( + "governed rebuild sources must construct SourceRebuildMarkerTokenV1 exactly once inside marker open; found {}", + constructions.0 + )); + } + + for (relative, function) in [ + ( + reconciliation_relative, + exact_free_function(reconciliation, "apply_reconciliation_hook")?, + ), + ( + rebuild_relative, + exact_free_function(rebuild, "rebuild_from_raw_v1_in_transaction_inner")?, + ), + ] { + let body = compact_tokens(&function.block); + if body + .matches("letmarker=open_source_rebuild_marker(connection,&plan).await?;") + .count() + != 1 + || body + .matches("close_source_rebuild_marker(connection,marker).await?;") + .count() + != 1 + || body.contains("marker.clone()") + { + return Err(format!( + "{relative}::{} must acquire and consume one non-cloned rebuild marker token", + function.sig.ident + )); + } + } + Ok(()) +} + +fn validate_reconciliation_direct_request_index_authority( + file: &syn::File, + relative: &str, +) -> Result<(), String> { + let request_index = exact_impl(file, "RequestIndex")?; + let methods = request_index + .items + .iter() + .filter_map(|item| match item { + syn::ImplItem::Fn(function) => Some(function.sig.ident.to_string()), + _ => None, + }) + .collect::<Vec<_>>(); + if methods != ["new", "insert", "decision"] { + return Err(format!( + "{relative} RequestIndex method inventory must be exactly [new, insert, decision]; found {methods:?}" + )); + } + + let constructor = compact_tokens(exact_associated_method(file, "RequestIndex", "new")?); + for marker in ["forrequestinrequests", "index.insert(request)"] { + if !constructor.contains(marker) { + return Err(format!( + "{relative} RequestIndex::new is missing incremental construction authority `{marker}`" + )); + } + } + for forbidden in [".projection()", ".event_targets()", ".address_targets()"] { + if constructor.contains(forbidden) { + return Err(format!( + "{relative} RequestIndex::new must delegate each request once, not scan `{forbidden}`" + )); + } + } + + let insert_function = exact_associated_method(file, "RequestIndex", "insert")?; + let insert = compact_tokens(insert_function); + for (marker, count) in [ + (".projection()", 2), + (".event_targets()", 1), + (".address_targets()", 1), + ] { + if insert.matches(marker).count() != count { + return Err(format!( + "{relative} RequestIndex::insert must scan each admitted request target projection exactly once through `{marker}`" + )); + } + } + for marker in [ + "request_id<current.as_str()", + "request_event.created_at_u64()>current.created_at", + "request_event.created_at_u64()==current.created_at", + "request_id<current.request_id.as_str()", + "evidence.unauthorized=true", + ] { + if !insert.contains(marker) { + return Err(format!( + "{relative} RequestIndex::insert is missing canonical evidence reduction `{marker}`" + )); + } + } + let insert_sha256 = sha256_hex(insert.as_bytes()); + if insert_sha256 != RECONCILIATION_REQUEST_INDEX_INSERT_AST_SHA256 { + return Err(format!( + "{relative} RequestIndex::insert AST drifted: expected {RECONCILIATION_REQUEST_INDEX_INSERT_AST_SHA256}, found {insert_sha256}" + )); + } + + let decision_function = exact_associated_method(file, "RequestIndex", "decision")?; + let decision = compact_tokens(decision_function); + for marker in [ + "self.event_targets.get(event.id_str())", + "by_author.get(event.author_str())", + "self.address_targets.get(coordinate)", + "RadrootsNip09SuppressionReason::DeletionRequestImmune", + "RadrootsNip09SuppressionReason::NoAuthorizedReference", + "RadrootsNip09SuppressionReason::RequestAuthorMismatch", + "RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget", + "RadrootsNip09SuppressionReason::EventIdReference", + "RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff", + "RadrootsNip09SuppressionReason::EventIdAndAddressReference", + ] { + if !decision.contains(marker) { + return Err(format!( + "{relative} RequestIndex::decision is missing direct evidence authority `{marker}`" + )); + } + } + for forbidden in [ + "matching(", + "evaluate_nip09_suppression", + ".projection()", + ".event_targets()", + ".address_targets()", + ".iter()", + ".into_iter()", + ] { + if decision.contains(forbidden) { + return Err(format!( + "{relative} RequestIndex::decision must not iterate or rescan through `{forbidden}`" + )); + } + } + let decision_sha256 = sha256_hex(decision.as_bytes()); + if decision_sha256 != RECONCILIATION_REQUEST_INDEX_DECISION_AST_SHA256 { + return Err(format!( + "{relative} RequestIndex::decision AST drifted: expected {RECONCILIATION_REQUEST_INDEX_DECISION_AST_SHA256}, found {decision_sha256}" + )); + } + + let affected = compact_tokens(exact_free_function( + file, + "request_affected_addressable_coordinates", + )?); + for marker in [ + "for target in request.projection().event_targets()", + "event_by_id.get(target.event_id().as_str())", + "winners.get(coordinate)", + "for target in request.projection().address_targets()", + "winners.contains_key(&coordinate)", + ] { + let marker = marker.replace(' ', ""); + if !affected.contains(&marker) { + return Err(format!( + "{relative} affected-coordinate reducer is missing `{marker}`" + )); + } + } + let affected_sha256 = sha256_hex(affected.as_bytes()); + if affected_sha256 != RECONCILIATION_AFFECTED_COORDINATES_AST_SHA256 { + return Err(format!( + "{relative} affected-coordinate reducer AST drifted: expected {RECONCILIATION_AFFECTED_COORDINATES_AST_SHA256}, found {affected_sha256}" + )); + } + + let desired = compact_tokens(exact_free_function(file, "desired_addressable_states")?); + let history = compact_tokens(exact_free_function(file, "expected_transition_history")?); + let state = compact_tokens(exact_free_function(file, "addressable_state_for_event")?); + for (function, source, markers) in [ + ( + "desired_addressable_states", + desired.as_str(), + &["RequestIndex::new(requests)", "&request_index"][..], + ), + ( + "expected_transition_history", + history.as_str(), + &[ + "letmutrequest_index=RequestIndex::new(&requests)", + "request_affected_addressable_coordinates(", + "request_index.insert(&request)", + "&request_index", + ][..], + ), + ( + "addressable_state_for_event", + state.as_str(), + &["request_index.decision(event.verified_event.event())?"][..], + ), + ] { + for marker in markers { + if !source.contains(marker) { + return Err(format!( + "{relative}::{function} is missing direct indexed authority `{marker}`" + )); + } + } + for forbidden in [ + "matching(", + "request_references_event", + "evaluate_nip09_suppression", + ] { + if source.contains(forbidden) { + return Err(format!( + "{relative}::{function} contains projection-rescanning authority `{forbidden}`" + )); + } + } + } + if history.matches("RequestIndex::new(&requests)").count() != 1 { + return Err(format!( + "{relative}::expected_transition_history must build its request index exactly once" + )); + } + Ok(()) +} + +fn validate_visibility_oracle_index_authority( + file: &syn::File, + relative: &str, +) -> Result<(), String> { + let expected_visibility = compact_tokens(exact_free_function(file, "expected_visibility")?); + if expected_visibility + .matches("request_index.decision(envelope)") + .count() + != 1 + { + return Err(format!( + "{relative} expected visibility must make exactly one direct indexed suppression decision per admitted event" + )); + } + for forbidden in [ + "matching(", + "evaluate_nip09_suppression", + ".projection()", + ".event_targets()", + ".address_targets()", + ] { + if expected_visibility.contains(forbidden) { + return Err(format!( + "{relative} expected visibility must not use projection-rescanning authority `{forbidden}`" + )); + } + } + let expected_visibility_sha256 = sha256_hex(expected_visibility.as_bytes()); + if expected_visibility_sha256 != VISIBILITY_ORACLE_EXPECTED_VISIBILITY_AST_SHA256 { + return Err(format!( + "{relative} expected_visibility AST drifted: expected {VISIBILITY_ORACLE_EXPECTED_VISIBILITY_AST_SHA256}, found {expected_visibility_sha256}" + )); + } + + let request_index = exact_impl(file, "OracleRequestIndexV1<'a>")?; + let methods = request_index + .items + .iter() + .filter_map(|item| match item { + syn::ImplItem::Fn(function) => Some(function.sig.ident.to_string()), + _ => None, + }) + .collect::<Vec<_>>(); + if methods != ["new", "decision"] { + return Err(format!( + "{relative} OracleRequestIndexV1 method inventory must be exactly [new, decision]; found {methods:?}" + )); + } + let constructor = compact_tokens(exact_associated_method( + file, + "OracleRequestIndexV1<'a>", + "new", + )?); + for (marker, count) in [ + (".projection()", 2), + (".event_targets()", 1), + (".address_targets()", 1), + ] { + if constructor.matches(marker).count() != count { + return Err(format!( + "{relative} OracleRequestIndexV1::new must contain exactly {count} indexed construction use(s) of `{marker}`" + )); + } + } + + let decision_function = exact_associated_method(file, "OracleRequestIndexV1<'a>", "decision")?; + let decision = compact_tokens(decision_function); + for marker in [ + "self.event_targets.get(event.id_str())", + "by_author.get(event.author_str())", + "self.address_targets.get(coordinate)", + "self.requests[index].event()", + "RadrootsNip09SuppressionReason::DeletionRequestImmune", + "RadrootsNip09SuppressionReason::NoAuthorizedReference", + "RadrootsNip09SuppressionReason::RequestAuthorMismatch", + "RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget", + "RadrootsNip09SuppressionReason::EventIdReference", + "RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff", + "RadrootsNip09SuppressionReason::EventIdAndAddressReference", + "event_reference_request_id", + "address_reference_request_id", + "address_reference_cutoff", + ] { + if !decision.contains(marker) { + return Err(format!( + "{relative} direct indexed suppression decision is missing `{marker}`" + )); + } + } + for forbidden in [ + "matching(", + "evaluate_nip09_suppression", + ".projection()", + ".event_targets()", + ".address_targets()", + ".iter()", + ".into_iter()", + ] { + if decision.contains(forbidden) { + return Err(format!( + "{relative} direct indexed suppression decision must not iterate or rescan through `{forbidden}`" + )); + } + } + #[derive(Default)] + struct IterationAudit { + for_loops: usize, + while_loops: usize, + loops: usize, + } + impl<'ast> syn::visit::Visit<'ast> for IterationAudit { + fn visit_expr_for_loop(&mut self, expression: &'ast syn::ExprForLoop) { + self.for_loops += 1; + syn::visit::visit_expr_for_loop(self, expression); + } + + fn visit_expr_while(&mut self, expression: &'ast syn::ExprWhile) { + self.while_loops += 1; + syn::visit::visit_expr_while(self, expression); + } + + fn visit_expr_loop(&mut self, expression: &'ast syn::ExprLoop) { + self.loops += 1; + syn::visit::visit_expr_loop(self, expression); + } + } + use syn::visit::Visit; + let mut iteration_audit = IterationAudit::default(); + iteration_audit.visit_block(&decision_function.block); + if iteration_audit.for_loops != 0 + || iteration_audit.while_loops != 0 + || iteration_audit.loops != 0 + { + return Err(format!( + "{relative} direct indexed suppression decision must not contain loops" + )); + } + let decision_sha256 = sha256_hex(decision.as_bytes()); + if decision_sha256 != VISIBILITY_ORACLE_DECISION_AST_SHA256 { + return Err(format!( + "{relative} OracleRequestIndexV1::decision AST drifted: expected {VISIBILITY_ORACLE_DECISION_AST_SHA256}, found {decision_sha256}" + )); + } + Ok(()) +} + +fn validate_public_entry_point_authority(file: &syn::File, relative: &str) -> Result<(), String> { + validate_public_method_signatures(file)?; + let expected_methods = [ + ( + "rebuild_from_raw_v1", + r#"{ + crate::nip09::reconciliation_v1::rebuild_from_raw_v1_on_pool(&self.pool).await + }"#, + ), + ( + "repair_file_from_raw_v1", + r#"{ + let canonical_path = canonical_raw_source_repair_main_path_v1(path.as_ref())?; + let options = SqliteConnectOptions::new() + .filename(&canonical_path) + .create_if_missing(false); + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(options) + .await?; + pool.set_connect_options(raw_source_repair_connect_options_v1(&canonical_path)); + let mut connection = pool.acquire().await?; + prepare_raw_source_repair_connection_v1(&mut connection, &canonical_path).await?; + let transaction = connection.begin_with("BEGIN IMMEDIATE").await?; + if let Err(primary) = + validate_raw_source_repair_canonical_lock_domain_v1(&canonical_path).await + { + return preserve_raw_source_repair_probe_failure(primary, transaction.rollback().await); + } + let report = + crate::nip09::reconciliation_v1::rebuild_from_raw_v1_in_existing_transaction( + transaction, + ) + .await?; + drop(connection); + Ok((Self { pool }, report)) + }"#, + ), + ]; + for (method, expected_body) in expected_methods { + let actual = exact_associated_method(file, "RadrootsEventStore", method)?; + let expected = syn::parse_str::<syn::Block>(expected_body) + .map_err(|error| format!("parse governed {method} body: {error}"))?; + if compact_tokens(&actual.block) != compact_tokens(&expected) { + return Err(format!( + "{relative}::RadrootsEventStore::{method} must retain its exact governed rebuild/cold-repair call path; expected `{}`, found `{}`", + compact_tokens(&expected), + compact_tokens(&actual.block), + )); + } + } + + let full = compact_tokens(file); + for forbidden in [ + "repair_pool_from_raw_v1", + "RADROOTS_EVENT_STORE_RAW_SOURCE_REPAIR_POOL_CONNECTION_LIMIT_V1", + "RawSourceRepairPoolConnectionLimitExceeded", + "RawSourceRepairPoolDatabaseIdentityMismatch", + "RawSourceRepairRequiresFileBackedDatabase", + "PoolTempSchemaPolicy::RawSourceRepairV1", + ] { + if full.contains(forbidden) { + return Err(format!( + "{relative} file-only cold repair must not retain obsolete authority `{forbidden}`" + )); + } + } + + let expected_functions = [ + ( + "prepare_raw_source_repair_connection_v1", + "validated cold-repair connection preflight", + r#"async fn prepare_raw_source_repair_connection_v1( + connection: &mut SqliteConnection, + canonical_path: &Path, + ) -> Result<(), RadrootsEventStoreError> { + let main_filename = main_database_filename(connection).await?; + let actual = canonical_raw_source_repair_main_path_v1(Path::new(&main_filename))?; + if actual != canonical_path { + return Err( + RadrootsEventStoreError::RawSourceRepairDatabaseIdentityMismatch { + expected: canonical_path.display().to_string(), + actual: actual.display().to_string(), + }, + ); + } + validate_main_database_encoding(connection).await?; + crate::schema::validate_exact_managed_v4_for_raw_source_rebuild_v1(connection) + .await?; + validate_file_journal_mode_is_wal(connection).await?; + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&mut *connection) + .await?; + sqlx::query("PRAGMA busy_timeout = 5000") + .execute(&mut *connection) + .await?; + Ok(()) + }"#, + ), + ( + "raw_source_repair_connect_options_v1", + "sealed future cold-repair connection options", + r#"fn raw_source_repair_connect_options_v1(canonical_path: &Path) -> SqliteConnectOptions { + SqliteConnectOptions::new() + .filename(canonical_path) + .create_if_missing(false) + .journal_mode(SqliteJournalMode::Wal) + .foreign_keys(true) + .busy_timeout(Duration::from_millis(5_000)) + }"#, + ), + ( + "validate_raw_source_repair_canonical_lock_domain_v1", + "canonical-path SQLite writer-lock-domain probe", + r#"async fn validate_raw_source_repair_canonical_lock_domain_v1( + canonical_path: &Path, + ) -> Result<(), RadrootsEventStoreError> { + let mut candidate = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(canonical_path) + .create_if_missing(false) + .foreign_keys(true) + .busy_timeout(Duration::ZERO), + ) + .await?; + let candidate_filename = main_database_filename(&mut candidate).await?; + let candidate_path = + canonical_raw_source_repair_main_path_v1(Path::new(&candidate_filename))?; + if candidate_path != canonical_path { + return Err( + RadrootsEventStoreError::RawSourceRepairDatabaseIdentityMismatch { + expected: canonical_path.display().to_string(), + actual: candidate_path.display().to_string(), + }, + ); + } + validate_main_database_encoding(&mut candidate).await?; + crate::schema::validate_exact_managed_v4_for_raw_source_rebuild_v1(&mut candidate) + .await?; + validate_file_journal_mode_is_wal(&mut candidate).await?; + + let mut probe = candidate.begin().await?; + let write = sqlx::query( + "UPDATE main.radroots_event_store_write_lock SET lock_version = lock_version WHERE singleton = 1", + ) + .execute(&mut *probe) + .await; + let rollback = probe.rollback().await; + match write { + Ok(_) => preserve_raw_source_repair_probe_failure( + RadrootsEventStoreError::RawSourceRepairCanonicalPathLockDomainMismatch { + canonical_path: canonical_path.display().to_string(), + }, + rollback, + ), + Err(error) => { + if sqlite_error_is_busy_or_locked(&error) { + rollback?; + Ok(()) + } else { + preserve_raw_source_repair_probe_failure(error.into(), rollback) + } + } + } + }"#, + ), + ( + "preserve_raw_source_repair_probe_failure", + "cold-repair lock-probe rollback error preservation", + r#"fn preserve_raw_source_repair_probe_failure<T>( + primary: RadrootsEventStoreError, + rollback: Result<(), sqlx::Error>, + ) -> Result<T, RadrootsEventStoreError> { + match rollback { + Ok(()) => Err(primary), + Err(rollback) => Err( + RadrootsEventStoreError::RawSourceRebuildTransactionRollbackFailed { + primary: Box::new(primary), + rollback, + }, + ), + } + }"#, + ), + ( + "canonical_raw_source_repair_main_path_v1", + "existing canonical cold-repair file identity", + r#"fn canonical_raw_source_repair_main_path_v1( + path: &Path, + ) -> Result<PathBuf, RadrootsEventStoreError> { + let filename = path.display().to_string(); + std::fs::canonicalize(path).map_err(|source| { + RadrootsEventStoreError::RawSourceRepairMainDatabaseCanonicalizationFailed { + filename, + source, + } + }) + }"#, + ), + ( + "sqlite_error_is_busy_or_locked", + "SQLite writer-lock error classification", + r#"fn sqlite_error_is_busy_or_locked(error: &sqlx::Error) -> bool { + let sqlx::Error::Database(error) = error else { + return false; + }; + error + .code() + .and_then(|code| code.parse::<i32>().ok()) + .is_some_and(|code| code & 0xff == 5 || code & 0xff == 6) + }"#, + ), + ]; + for (function, authority, expected_source) in expected_functions { + let actual = exact_free_function(file, function)?; + let expected = syn::parse_str::<syn::ItemFn>(expected_source) + .map_err(|error| format!("parse governed {function}: {error}"))?; + if compact_tokens(actual) != compact_tokens(&expected) { + return Err(format!( + "{relative}::{function} must retain its exact {authority}; expected `{}`, found `{}`", + compact_tokens(&expected), + compact_tokens(actual), + )); + } + } + Ok(()) +} + +fn validate_streaming_dependency_authority(workspace_root: &Path) -> Result<(), String> { + let relative = "crates/event_store/Cargo.toml"; + 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}"))?; + let manifest: toml::Value = + toml::from_str(source).map_err(|error| format!("parse {relative}: {error}"))?; + let sqlite = manifest + .get("features") + .and_then(|features| features.get("sqlite")) + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("{relative} must define the sqlite feature array"))?; + if !sqlite + .iter() + .any(|feature| feature.as_str() == Some("dep:futures")) + { + return Err(format!( + "{relative} sqlite feature must enable the optional futures streaming dependency" + )); + } + let futures = manifest + .get("dependencies") + .and_then(|dependencies| dependencies.get("futures")) + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{relative} must define futures as a dependency table"))?; + if futures.get("workspace").and_then(toml::Value::as_bool) != Some(true) + || futures.get("optional").and_then(toml::Value::as_bool) != Some(true) + { + return Err(format!( + "{relative} futures dependency must remain workspace-governed and optional" + )); + } + Ok(()) +} + +fn validate_coordinator_authority(file: &syn::File, relative: &str) -> Result<(), String> { + let serialized = compact_tokens(exact_free_function( + file, + "rebuild_from_raw_v1_on_pool_inner", + )?); + require_ordered_markers( + relative, + "serialized rebuild transaction", + &serialized, + &[ + "begin_with(\"BEGINIMMEDIATE\").await?", + "rebuild_from_raw_v1_in_transaction_inner(", + "finish_raw_source_rebuild_transaction(transaction,result).await", + ], + )?; + + let existing_transaction = compact_tokens(exact_free_function( + file, + "rebuild_from_raw_v1_in_existing_transaction", + )?); + require_ordered_markers( + relative, + "validated existing rebuild transaction", + &existing_transaction, + &[ + "rebuild_from_raw_v1_in_transaction_inner(", + "&OsSourceGenerationProvider", + "finish_raw_source_rebuild_transaction(transaction,result).await", + ], + )?; + + let coordinator = compact_tokens(exact_free_function( + file, + "rebuild_from_raw_v1_in_transaction_inner", + )?); + require_ordered_markers( + relative, + "raw-source rebuild coordinator", + &coordinator, + &[ + "validate_exact_managed_v4_for_raw_source_rebuild_v1(connection).await?", + "preflight_caller_owned_schema_dependencies_v1(connection,caller_schema_limits).await?", + "validate_rebuild_marker_absent(connection).await?", + "validate_source_capacity_authority_full_v1(connection).await?", + "preflight_source_generation_append_v1(connection).await?", + "load_reconciliation_snapshot(", + "transition_high_water_v1(connection).await?", + "validate_source_lineage_for_rebuild_v1(connection,&snapshot.events,transition_floor_seq)", + "immutable_raw_digest_v1(connection).await?", + "generation_provider.fill_generation(&mutgeneration_bytes)?", + "open_source_rebuild_marker(connection,&plan).await?", + "RawSourceRebuildFailpointV1::AfterMarkerOpen", + "append_source_generation(connection,&plan).await?", + "rotate_source_state(connection,&plan).await?", + "RawSourceRebuildFailpointV1::AfterGenerationRotation", + "prepare_transition_sqlite_sequence_v1(connection,transition_floor_seq).await?", + "reconcile_raw_events(connection,&snapshot.events).await?", + "persist_event_coordinate_facts(connection,generation,&snapshot.events).await?", + "rebuild_raw_heads(connection,&snapshot.events).await?", + "persist_nip09_facts(connection,generation,&snapshot.events).await?", + "synchronize_addressable_heads(", + "update_source_authority(", + "transition_high_water_v1(connection).await?", + "validate_transition_sqlite_sequence_v1(connection,transition_sequence_rowid,replay_transition_high_water,).await?", + "validate_raw_source_rebuild_core_with_events_v1(connection,generation,&snapshot.events)", + "RawSourceRebuildFailpointV1::AfterCoreReplay", + "load_derived_visibility_rows_v1(connection,generation).await?", + "audit_current_visibility_from_raw_v1(&snapshot.events,derived_visibility).await?", + "RawSourceRebuildFailpointV1::AfterVisibilityAudit", + "bind_source_capacity_to_generation_v1(connection,generation)", + "reset_and_replay_food_availability_from_raw_v1(connection,generation).await?", + "RawSourceRebuildFailpointV1::AfterFoodResetAndReplay", + "validate_food_availability_projection_hook_v1(connection).await?", + "RawSourceRebuildFailpointV1::AfterFoodAudit", + "immutable_raw_digest_v1(connection).await?", + "final_raw_digest!=immutable_raw_digest", + "close_source_rebuild_marker(connection,marker).await?", + "RawSourceRebuildFailpointV1::AfterMarkerClose", + "validate_active_hook_state_fast(connection).await?", + "validate_source_capacity_authority_fast_v1(connection).await?", + "validate_food_availability_projection_hook_state_fast_v1(connection).await?", + "validate_scoped_integrity_v1(connection).await?", + "active_product_state_digest_v1(connection,generation).await?", + "RadrootsEventStoreRawSourceRebuildReportV1", + ], + )?; + if coordinator + .matches("preflight_caller_owned_schema_dependencies_v1(") + .count() + != 1 + { + return Err(format!( + "{relative} rebuild coordinator must run the caller-schema dependency preflight exactly once" + )); + } + if coordinator + .matches("inject_raw_source_rebuild_failpoint_v1(") + .count() + != REBUILD_FAILPOINTS.len() + { + return Err(format!( + "{relative} rebuild coordinator must inject exactly one failpoint for each governed stage" + )); + } + for variant in REBUILD_FAILPOINTS.iter().map(|failpoint| failpoint.variant) { + if coordinator + .matches(&format!("RawSourceRebuildFailpointV1::{variant}")) + .count() + != 1 + { + return Err(format!( + "{relative} rebuild coordinator must inject `{variant}` exactly once" + )); + } + } + if coordinator.contains("projection_cursor") { + return Err( + "raw-source rebuild coordinator must not enumerate or mutate generic projection cursors" + .to_owned(), + ); + } + + let finish = compact_tokens(exact_free_function( + file, + "finish_raw_source_rebuild_transaction", + )?); + for marker in [ + "transaction.commit().await?", + "transaction.rollback().await", + "preserve_raw_source_rebuild_primary_failure(primary,rollback)", + ] { + if !finish.contains(marker) { + return Err(format!( + "{relative} transaction finalizer is missing `{marker}`" + )); + } + } + Ok(()) +} + +fn validate_failpoint_authority(file: &syn::File, relative: &str) -> Result<(), String> { + let mut actual_enum = exact_enum(file, "RawSourceRebuildFailpointV1")?.clone(); + strip_doc_attributes(&mut actual_enum.attrs); + let variants = REBUILD_FAILPOINTS + .iter() + .map(|failpoint| format!("{},", failpoint.variant)) + .collect::<Vec<_>>() + .join("\n"); + let expected_enum = syn::parse_str::<syn::ItemEnum>(&format!( + r#" + #[cfg(test)] + #[allow(clippy::enum_variant_names)] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(crate) enum RawSourceRebuildFailpointV1 {{ + {variants} + }} + "#, + )) + .map_err(|error| format!("parse governed failpoint enum: {error}"))?; + if compact_tokens(&actual_enum) != compact_tokens(&expected_enum) { + return Err(format!( + "{relative} must retain the exact governed test-only rebuild failpoint enum" + )); + } + + let actual_impl = exact_impl(file, "RawSourceRebuildFailpointV1")?; + let match_arms = REBUILD_FAILPOINTS + .iter() + .map(|failpoint| format!("Self::{} => {:?},", failpoint.variant, failpoint.id)) + .collect::<Vec<_>>() + .join("\n"); + let expected_impl = syn::parse_str::<syn::ItemImpl>(&format!( + r#" + #[cfg(test)] + impl RawSourceRebuildFailpointV1 {{ + const fn as_str(self) -> &'static str {{ + match self {{ + {match_arms} + }} + }} + }} + "#, + )) + .map_err(|error| format!("parse governed failpoint mapping: {error}"))?; + if compact_tokens(actual_impl) != compact_tokens(&expected_impl) { + return Err(format!( + "{relative} must retain the exact failpoint-to-stage mapping" + )); + } + + let mut actual_injector = + exact_free_function(file, "inject_raw_source_rebuild_failpoint_v1")?.clone(); + strip_doc_attributes(&mut actual_injector.attrs); + let expected_injector = syn::parse_str::<syn::ItemFn>( + r#" + #[cfg(test)] + fn inject_raw_source_rebuild_failpoint_v1( + selected: Option<RawSourceRebuildFailpointV1>, + stage: RawSourceRebuildFailpointV1, + ) -> Result<(), RadrootsEventStoreError> { + if selected == Some(stage) { + return rebuild_drift( + RadrootsEventStoreRawSourceRebuildDriftV1::RebuildPostcondition, + format!("injected raw-source rebuild failure at {}", stage.as_str()), + ); + } + Ok(()) + } + "#, + ) + .map_err(|error| format!("parse governed failpoint injector: {error}"))?; + if compact_tokens(&actual_injector) != compact_tokens(&expected_injector) { + return Err(format!( + "{relative} must retain the exact rollback failpoint injector" + )); + } + validate_failpoint_injection_authority(file, relative) +} + +fn validate_failpoint_injection_authority(file: &syn::File, relative: &str) -> Result<(), String> { + #[derive(Default)] + struct InjectionAudit { + calls: Vec<String>, + propagated_calls: Vec<String>, + } + + impl<'ast> syn::visit::Visit<'ast> for InjectionAudit { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + if compact_tokens(call.func.as_ref()) == "inject_raw_source_rebuild_failpoint_v1" { + self.calls.push(compact_tokens(call)); + } + syn::visit::visit_expr_call(self, call); + } + + fn visit_expr_try(&mut self, expression: &'ast syn::ExprTry) { + if let syn::Expr::Call(call) = expression.expr.as_ref() + && compact_tokens(call.func.as_ref()) == "inject_raw_source_rebuild_failpoint_v1" + { + self.propagated_calls.push(compact_tokens(call)); + } + syn::visit::visit_expr_try(self, expression); + } + } + + let expected = REBUILD_FAILPOINTS + .iter() + .map(|failpoint| { + format!( + "inject_raw_source_rebuild_failpoint_v1(_failpoint,RawSourceRebuildFailpointV1::{},)", + failpoint.variant + ) + }) + .collect::<Vec<_>>(); + use syn::visit::Visit; + let coordinator = exact_free_function(file, "rebuild_from_raw_v1_in_transaction_inner")?; + let mut audit = InjectionAudit::default(); + audit.visit_block(&coordinator.block); + if audit.calls != expected || audit.propagated_calls != expected { + return Err(format!( + "{relative} rebuild coordinator must contain exactly one ordered, error-propagating injection call for every governed failpoint: expected {expected:?}, calls {:?}, propagated {:?}", + audit.calls, audit.propagated_calls, + )); + } + Ok(()) +} + +fn validate_failpoint_test_array_authority(file: &syn::File, relative: &str) -> Result<(), String> { + struct FailpointArrayAudit { + arrays: Vec<Vec<String>>, + } + + impl<'ast> syn::visit::Visit<'ast> for FailpointArrayAudit { + fn visit_expr_for_loop(&mut self, expression: &'ast syn::ExprForLoop) { + if compact_tokens(expression.pat.as_ref()) == "(index,failpoint)" + && let syn::Expr::MethodCall(enumerate) = expression.expr.as_ref() + && enumerate.method == "enumerate" + && enumerate.args.is_empty() + && let syn::Expr::MethodCall(into_iter) = enumerate.receiver.as_ref() + && into_iter.method == "into_iter" + && into_iter.args.is_empty() + && let syn::Expr::Array(array) = into_iter.receiver.as_ref() + { + self.arrays + .push(array.elems.iter().map(compact_tokens).collect()); + } + syn::visit::visit_expr_for_loop(self, expression); + } + } + + let expected = REBUILD_FAILPOINTS + .iter() + .map(|failpoint| format!("RawSourceRebuildFailpointV1::{}", failpoint.variant)) + .collect::<Vec<_>>(); + use syn::visit::Visit; + let test = exact_top_level_function(file, REBUILD_FAILPOINT_TEST)?; + let mut audit = FailpointArrayAudit { arrays: Vec::new() }; + audit.visit_block(&test.block); + let [actual] = audit.arrays.as_slice() else { + return Err(format!( + "{relative}::{REBUILD_FAILPOINT_TEST} must contain exactly one governed failpoint array loop" + )); + }; + if actual != &expected { + return Err(format!( + "{relative}::{REBUILD_FAILPOINT_TEST} must enumerate the exact governed failpoint array once and in order" + )); + } + Ok(()) +} + +fn validate_bounded_repair_schema_authority( + file: &syn::File, + relative: &str, +) -> Result<(), String> { + let authority = compact_tokens(exact_free_function( + file, + "repair_governed_catalog_authority_v1", + )?); + for marker in [ + "owned_object_names.iter().copied()", + "names.insert(EVENT_STORE_LEDGER_NAME)", + "canonical_row_count.checked_add(1)", + ] { + if !authority.contains(marker) { + return Err(format!( + "{relative} repair catalog bound authority is missing `{marker}`" + )); + } + } + + let catalog = compact_tokens(exact_free_function(file, "read_repair_catalog_bounded_v1")?); + for marker in [ + "repair_governed_catalog_authority_v1(registry)?", + "json_each(?)", + "FROMmain.sqlite_schema", + "lower(substr(name,1,7))!='sqlite_'", + "nameCOLLATENOCASEIN(SELECTnameFROMgoverned)", + "tbl_nameCOLLATENOCASEIN(SELECTnameFROMgoverned)", + "EVENT_STORE_RESERVED_PREFIX", + "LIMIT?", + ".bind(row_limit)", + ".fetch_all(&mut*connection)", + ] { + if !catalog.contains(marker) { + return Err(format!( + "{relative} bounded repair catalog reader is missing `{marker}`" + )); + } + } + + let temp = compact_tokens(exact_free_function( + file, + "validate_repair_temp_schema_bounded_v1", + )?); + for marker in [ + "repair_governed_catalog_authority_v1(registry)?", + "json_each(?)", + "FROMtemp.sqlite_schema", + "typeIN('trigger','view')", + "nameCOLLATENOCASEIN(SELECTnameFROMgoverned)", + "tbl_nameCOLLATENOCASEIN(SELECTnameFROMgoverned)", + "EVENT_STORE_RESERVED_PREFIX", + "LIMIT1", + ".fetch_optional(&mut*connection)", + "TemporarySchemaCollision", + ] { + if !temp.contains(marker) { + return Err(format!( + "{relative} bounded repair temp-collision probe is missing `{marker}`" + )); + } + } + + let history = compact_tokens(exact_free_function(file, "read_repair_history_bounded_v1")?); + for marker in [ + "i64::from(supported_current).checked_add(1)", + "FROMmain.radroots_event_store_schema_migrations", + "ORDERBYversion", + "LIMIT?", + ".bind(row_limit)", + ".fetch_all(&mut*connection)", + ] { + if !history.contains(marker) { + return Err(format!( + "{relative} bounded repair migration-history reader is missing `{marker}`" + )); + } + } + Ok(()) +} + +fn validate_transition_sequence_authority(file: &syn::File, relative: &str) -> Result<(), String> { + if exact_string_const(file, "TRANSITION_SEQUENCE_NAME")? + != "radroots_event_store_addressable_head_transition" + { + return Err(format!( + "{relative} transition sqlite_sequence target identity drifted" + )); + } + let prepare = compact_tokens(exact_free_function( + file, + "prepare_transition_sqlite_sequence_v1", + )?); + require_ordered_markers( + relative, + "target-first sqlite_sequence preparation", + &prepare, + &[ + "transition_max<0||transition_max==i64::MAX", + "SELECTrowid,name=?COLLATENOCASEFROMmain.sqlite_sequenceORDERBYrowidLIMIT1", + ".bind(TRANSITION_SEQUENCE_NAME)", + "None=>-1", + "Some((rowid,Some(1)))=>rowid", + "Some((i64::MIN,_))=>", + "Some((rowid,_))=>rowid-1", + "DELETEFROMmain.sqlite_sequenceWHEREnameCOLLATENOCASE=?", + "INSERTINTOmain.sqlite_sequence(rowid,name,seq)VALUES(?,?,?)", + ".bind(target_rowid)", + ".bind(TRANSITION_SEQUENCE_NAME)", + ".bind(transition_max)", + "validate_transition_sqlite_sequence_v1(connection,target_rowid,transition_max).await?", + "Ok(target_rowid)", + ], + )?; + if prepare + .matches("DELETEFROMmain.sqlite_sequenceWHEREnameCOLLATENOCASE=?") + .count() + != 1 + { + return Err(format!( + "{relative} target aliases must be removed by exactly one shared sqlite_sequence scan" + )); + } + + let validate = compact_tokens(exact_free_function( + file, + "validate_transition_sqlite_sequence_v1", + )?); + require_ordered_markers( + relative, + "target-first sqlite_sequence validation", + &validate, + &[ + "SELECTname,seqFROMmain.sqlite_sequenceWHERErowid=?", + ".bind(target_rowid)", + "SELECTrowidFROMmain.sqlite_sequenceORDERBYrowidLIMIT1", + "first_rowid!=Some(target_rowid)", + "AddressableTransitionAuthority", + ], + )?; + let full_source = compact_tokens(file); + for forbidden in [ + "normalize_transition_sqlite_sequence_v1", + "TRANSITION_SEQUENCE_RESERVED_ROWID_V1", + "unrelated_sqlite_sequence_snapshot_v1", + "validate_unrelated_sqlite_sequences_v1", + "quote(name)", + "nameISNULLORname!=?", + ] { + if full_source.contains(forbidden) { + return Err(format!( + "{relative} must not scan or promote unrelated sqlite_sequence rows through `{forbidden}`" + )); + } + } + Ok(()) +} + +fn validate_caller_schema_dependency_authority( + file: &syn::File, + relative: &str, +) -> Result<(), String> { + for name in [ + "RAW_SOURCE_REBUILD_CALLER_MAIN_TABLE_COUNT_LIMIT_V1", + "RAW_SOURCE_REBUILD_CALLER_FOREIGN_KEY_ROW_COUNT_LIMIT_V1", + ] { + let matches = file + .items + .iter() + .filter_map(|item| match item { + Item::Const(item) if item.ident == name => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + let [actual] = matches.as_slice() else { + return Err(format!( + "{relative} must define caller-schema limit `{name}` exactly once; found {}", + matches.len() + )); + }; + let mut actual = (*actual).clone(); + strip_doc_attributes(&mut actual.attrs); + let expected = syn::parse_str::<syn::ItemConst>(&format!("const {name}: u32 = 4_096;")) + .map_err(|error| format!("parse caller-schema limit `{name}`: {error}"))?; + if compact_tokens(&actual) != compact_tokens(&expected) { + return Err(format!( + "{relative} caller-schema limit `{name}` must remain exactly 4,096" + )); + } + } + + let limits = exact_struct(file, "RawSourceRebuildCallerSchemaLimitsV1")?; + let expected_limits = syn::parse_str::<syn::ItemStruct>( + r#" + #[derive(Clone, Copy)] + struct RawSourceRebuildCallerSchemaLimitsV1 { + main_tables: u32, + foreign_key_rows: u32, + } + "#, + ) + .map_err(|error| format!("parse caller-schema limits model: {error}"))?; + if compact_tokens(limits) != compact_tokens(&expected_limits) { + return Err(format!( + "{relative} caller-schema limits model field or derive authority drifted" + )); + } + let limits_impl = exact_impl(file, "RawSourceRebuildCallerSchemaLimitsV1")?; + let expected_limits_impl = syn::parse_str::<syn::ItemImpl>( + r#" + impl RawSourceRebuildCallerSchemaLimitsV1 { + const fn production() -> Self { + Self { + main_tables: RAW_SOURCE_REBUILD_CALLER_MAIN_TABLE_COUNT_LIMIT_V1, + foreign_key_rows: RAW_SOURCE_REBUILD_CALLER_FOREIGN_KEY_ROW_COUNT_LIMIT_V1, + } + } + } + "#, + ) + .map_err(|error| format!("parse caller-schema production limits authority: {error}"))?; + if compact_tokens(limits_impl) != compact_tokens(&expected_limits_impl) { + return Err(format!( + "{relative} caller-schema production limits must route through both governed constants" + )); + } + + for (function, expected_source) in [ + ( + "governed_schema_names_json_v1", + r#"fn governed_schema_names_json_v1() -> Result<String, RadrootsEventStoreError> { + let mut names = EVENT_STORE_MIGRATIONS + .iter() + .flat_map(|migration| migration.owned_object_names.iter().copied()) + .collect::<BTreeSet<_>>(); + names.insert(EVENT_STORE_LEDGER_NAME); + Ok(serde_json::to_string(&names)?) + }"#, + ), + ( + "caller_schema_count_v1", + r#"fn caller_schema_count_v1(count: i64) -> Result<u64, RadrootsEventStoreError> { + u64::try_from(count).map_err(|_| { + rebuild_state_error( + RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, + "caller-owned schema inventory returned a negative row count", + ) + }) + }"#, + ), + ] { + let actual = exact_free_function(file, function)?; + let expected = syn::parse_str::<syn::ItemFn>(expected_source) + .map_err(|error| format!("parse caller-schema helper `{function}`: {error}"))?; + if compact_tokens(actual) != compact_tokens(&expected) { + return Err(format!( + "{relative}::{function} caller-schema authority drifted" + )); + } + } + + let preflight_function = + exact_free_function(file, "preflight_caller_owned_schema_dependencies_v1")?; + let preflight = compact_tokens(preflight_function); + let query_literals = sqlx_query_family_literals(preflight_function); + if query_literals.len() != 3 { + return Err(format!( + "{relative} caller-schema preflight must contain exactly three literal sqlx queries; found {}", + query_literals.len() + )); + } + let query_authority = query_literals + .iter() + .map(|query| query.split_whitespace().collect::<String>()) + .collect::<Vec<_>>() + .join("\0"); + require_ordered_markers( + relative, + "bounded caller-schema dependency preflight", + &preflight, + &[ + "governed_schema_names_json_v1()?", + "serde_json::to_string(RAW_SOURCE_REBUILD_MUTATED_PARENT_TABLES_V1)?", + "i64::from(limits.main_tables)+1", + "RawSourceRebuildCallerTableCapacityExceeded", + "i64::from(limits.foreign_key_rows)+1", + "RawSourceRebuildCallerForeignKeyCapacityExceeded", + "RawSourceRebuildCallerInboundForeignKeyUnsupported", + "Box::new(RadrootsEventStoreCallerInboundForeignKeyV1", + ], + )?; + require_ordered_markers( + relative, + "caller-schema dependency SQL", + &query_authority, + &[ + "FROMmain.sqlite_schemaASchild", + "LIMIT?", + "main.pragma_foreign_key_list(child.name,'main')ASforeign_key", + "JOINrebuild_parentONforeign_key.\"table\"COLLATENOCASE=rebuild_parent.name", + "ORDERBYchild.nameCOLLATENOCASE,child.name,foreign_key.id,foreign_key.seq", + "LIMIT1", + ], + )?; + for (marker, expected_count) in [ + ("FROMmain.sqlite_schemaASchild", 3), + ( + "main.pragma_foreign_key_list(child.name,'main')ASforeign_key", + 2, + ), + ("LIMIT?", 2), + ("LIMIT1", 1), + ] { + if query_authority.matches(marker).count() != expected_count { + return Err(format!( + "{relative} caller-schema preflight must contain `{marker}` exactly {expected_count} time(s)" + )); + } + } + for forbidden in [ + "temp.sqlite_schema", + "temp.pragma_foreign_key_list", + "foreign_key.on_delete=", + "foreign_key.on_update=", + ] { + if query_authority.contains(forbidden) { + return Err(format!( + "{relative} caller-schema preflight must not narrow or redirect dependency discovery through `{forbidden}`" + )); + } + } + let actual_sha256 = sha256_hex(preflight.as_bytes()); + if actual_sha256 != CALLER_SCHEMA_PREFLIGHT_AST_SHA256 { + return Err(format!( + "{relative} caller-schema dependency preflight AST drifted: expected {CALLER_SCHEMA_PREFLIGHT_AST_SHA256}, found {actual_sha256}" + )); + } + Ok(()) +} + +fn validate_scoped_integrity_authority(file: &syn::File, relative: &str) -> Result<(), String> { + let actual_tables = exact_string_slice_const(file, "REBUILD_OWNED_TABLES_V1")?; + let expected_tables = owned(SCOPED_INTEGRITY_TABLES); + if actual_tables != expected_tables { + return Err(format!( + "{relative} scoped integrity table inventory differs: expected {expected_tables:?}, found {actual_tables:?}" + )); + } + let actual_mutated_parents = + exact_string_slice_const(file, "RAW_SOURCE_REBUILD_MUTATED_PARENT_TABLES_V1")?; + let expected_mutated_parents = owned(CALLER_INBOUND_FOREIGN_KEY_PARENT_TABLES); + if actual_mutated_parents != expected_mutated_parents { + return Err(format!( + "{relative} rebuild-mutated parent inventory differs: expected {expected_mutated_parents:?}, found {actual_mutated_parents:?}" + )); + } + let integrity = compact_tokens(exact_free_function(file, "validate_scoped_integrity_v1")?); + for marker in [ + "fortableinREBUILD_OWNED_TABLES_V1", + "PRAGMAmain.integrity_check('{table}')", + "sqlx::AssertSqlSafe(integrity_sql)", + "detail!=\"ok\"", + "PRAGMAmain.foreign_key_check('{table}')", + "sqlx::AssertSqlSafe(foreign_key_sql)", + ".fetch_optional(&mut*connection)", + "ForeignKeyViolation", + "radroots_event_store_food_availability_search_fts(radroots_event_store_food_availability_search_fts)VALUES('integrity-check')", + "Fts5IntegrityCheckFailed", + ] { + if !integrity.contains(marker) { + return Err(format!( + "{relative} scoped integrity authority is missing `{marker}`" + )); + } + } + if integrity.matches(".fetch_all(&mut*connection)").count() != 1 + || integrity + .matches(".fetch_optional(&mut*connection)") + .count() + != 1 + { + return Err(format!( + "{relative} scoped integrity must materialize only bounded integrity-check rows and fetch at most one foreign-key violation" + )); + } + for forbidden in [ + "PRAGMAintegrity_check\"", + "PRAGMAmain.integrity_check\"", + "PRAGMAforeign_key_check\"", + "PRAGMAmain.foreign_key_check\"", + "quick_check", + ] { + if integrity.contains(forbidden) { + return Err(format!( + "{relative} scoped integrity authority contains forbidden global scan `{forbidden}`" + )); + } + } + Ok(()) +} + +fn validate_digest_query_authority(file: &syn::File, relative: &str) -> Result<(), String> { + validate_one_digest_query_authority( + file, + relative, + "immutable_raw_digest_v1", + "IMMUTABLE_RAW_DIGEST_DOMAIN_V1", + RAW_DIGEST_QUERY_SPECS, + )?; + validate_one_digest_query_authority( + file, + relative, + "active_product_state_digest_v1", + "ACTIVE_PRODUCT_STATE_DIGEST_DOMAIN_V1", + PRODUCT_DIGEST_QUERY_SPECS, + )?; + validate_digest_framing_authority(file, relative) +} + +fn validate_one_digest_query_authority( + file: &syn::File, + relative: &str, + function_name: &str, + domain_name: &str, + specs: &[DigestQuerySpec], +) -> Result<(), String> { + let function = exact_free_function(file, function_name)?; + let compact = compact_tokens(function); + if !compact.contains(&format!("digest.update({domain_name})")) { + return Err(format!( + "{relative}::{function_name} does not begin from governed domain `{domain_name}`" + )); + } + let actual_queries = sqlx_query_literals(function); + let expected_queries = specs.iter().map(|spec| spec.sql).collect::<Vec<_>>(); + if actual_queries != expected_queries { + return Err(format!( + "{relative}::{function_name} digest query inventory differs from the governed source/component queries" + )); + } + let actual_sections = digest_section_literals(function); + let expected_sections = specs.iter().map(|spec| spec.section).collect::<Vec<_>>(); + if actual_sections != expected_sections { + return Err(format!( + "{relative}::{function_name} digest section order differs: expected {expected_sections:?}, found {actual_sections:?}" + )); + } + let expected_fields = specs + .iter() + .flat_map(|spec| { + spec.fields + .iter() + .copied() + .zip(expected_digest_field_framing(spec.section).iter().copied()) + .map(|(name, framing)| (name.to_owned(), framing.to_owned())) + }) + .collect::<Vec<_>>(); + let actual_fields = digest_field_witnesses(function)?; + if actual_fields != expected_fields { + return Err(format!( + "{relative}::{function_name} typed digest field witness order differs from its governed queries: expected {expected_fields:?}, found {actual_fields:?}" + )); + } + validate_digest_streaming_authority(function, relative, function_name, specs)?; + Ok(()) +} + +fn validate_digest_streaming_authority( + function: &syn::ItemFn, + relative: &str, + function_name: &str, + specs: &[DigestQuerySpec], +) -> Result<(), String> { + let compact = compact_tokens(function); + if compact.contains(".fetch_all(") { + return Err(format!( + "{relative}::{function_name} must stream digest rows and must not materialize them with fetch_all" + )); + } + + let statements = &function.block.stmts; + let mut witnessed_queries = Vec::new(); + for (index, statement) in statements.iter().enumerate() { + let syn::Stmt::Local(local) = statement else { + continue; + }; + let Some(initializer) = &local.init else { + continue; + }; + let query_literals = sqlx_query_literals_in_expr(&initializer.expr); + if query_literals.is_empty() { + continue; + } + let [query] = query_literals.as_slice() else { + return Err(format!( + "{relative}::{function_name} digest stream binding must contain exactly one SQL query" + )); + }; + let syn::Pat::Ident(binding) = &local.pat else { + return Err(format!( + "{relative}::{function_name} digest query `{query}` must bind one named mutable stream" + )); + }; + if binding.mutability.is_none() || binding.by_ref.is_some() || binding.subpat.is_some() { + return Err(format!( + "{relative}::{function_name} digest query `{query}` must bind one plain mutable stream" + )); + } + let stream = binding.ident.to_string(); + let syn::Expr::MethodCall(fetch) = initializer.expr.as_ref() else { + return Err(format!( + "{relative}::{function_name} digest query `{query}` must terminate in fetch" + )); + }; + if fetch.method != "fetch" + || fetch.args.len() != 1 + || compact_tokens(fetch.args.first().expect("one fetch argument")) != "&mut*connection" + { + return Err(format!( + "{relative}::{function_name} digest query `{query}` must stream via fetch(&mut *connection)" + )); + } + + let Some(syn::Stmt::Expr(syn::Expr::While(row_loop), _)) = statements.get(index + 1) else { + return Err(format!( + "{relative}::{function_name} digest stream `{stream}` must be consumed immediately by while let" + )); + }; + let syn::Expr::Let(condition) = row_loop.cond.as_ref() else { + return Err(format!( + "{relative}::{function_name} digest stream `{stream}` must use while let Some(row)" + )); + }; + if compact_tokens(&condition.pat) != "Some(row)" + || compact_tokens(&condition.expr) != format!("{stream}.try_next().await?") + { + return Err(format!( + "{relative}::{function_name} digest stream `{stream}` must terminate through try_next().await?" + )); + } + + let top_level_row_starts = row_loop + .body + .stmts + .iter() + .enumerate() + .filter_map(|(index, statement)| { + is_digest_row_start_statement(statement).then_some(index) + }) + .collect::<Vec<_>>(); + struct RowStartCounter(usize); + impl<'ast> syn::visit::Visit<'ast> for RowStartCounter { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + if compact_tokens(call) == "digest_row_start(&mutdigest)" { + self.0 += 1; + } + syn::visit::visit_expr_call(self, call); + } + } + use syn::visit::Visit; + let mut row_start_calls = RowStartCounter(0); + row_start_calls.visit_block(&row_loop.body); + if top_level_row_starts != [0] || row_start_calls.0 != 1 { + return Err(format!( + "{relative}::{function_name} digest stream `{stream}` must begin every row with exactly one top-level digest_row_start marker" + )); + } + + let Some(syn::Stmt::Expr(drop_expression, _)) = statements.get(index + 2) else { + return Err(format!( + "{relative}::{function_name} digest stream `{stream}` must be dropped before the next query" + )); + }; + if compact_tokens(drop_expression) != format!("drop({stream})") { + return Err(format!( + "{relative}::{function_name} digest stream `{stream}` must be explicitly dropped after consumption" + )); + } + witnessed_queries.push(query.clone()); + } + + let expected_queries = specs.iter().map(|spec| spec.sql).collect::<Vec<_>>(); + if witnessed_queries != expected_queries { + return Err(format!( + "{relative}::{function_name} must bind, consume, and drop exactly one streaming cursor for every governed digest query" + )); + } + Ok(()) +} + +fn is_digest_row_start_statement(statement: &syn::Stmt) -> bool { + let syn::Stmt::Expr(syn::Expr::Call(call), Some(_)) = statement else { + return false; + }; + compact_tokens(call) == "digest_row_start(&mutdigest)" +} + +fn validate_digest_framing_authority(file: &syn::File, relative: &str) -> Result<(), String> { + let expectations: &[(&str, &[&str])] = &[ + ( + "digest_section", + &["digest.update(b\"S\")", "digest_bytes(digest,b'N',name)"], + ), + ("digest_row_start", &["digest.update(b\"R\")"]), + ( + "digest_i64", + &[ + "digest.update(b\"I\")", + "digest.update(value.to_be_bytes())", + ], + ), + ( + "digest_bytes", + &[ + "u64::try_from(value.len())", + "digest.update([marker])", + "digest.update(length.to_be_bytes())", + "digest.update(value)", + ], + ), + ("digest_text", &["digest_bytes(digest,b'T',value)"]), + ( + "digest_optional_text", + &[ + "digest.update([b'O',1])", + "digest_text(digest,value.as_bytes())", + "digest.update([b'O',0])", + ], + ), + ( + "digest_optional_i64", + &[ + "digest.update([b'O',1])", + "digest_i64(digest,value)", + "digest.update([b'O',0])", + ], + ), + ( + "digest_bool", + &["digest.update([b'B',ifvalue==0{0}else{1}])"], + ), + ("digest_blob_field", &["digest_bytes(digest,b'X',&value)"]), + ]; + for (name, markers) in expectations { + let function = compact_tokens(exact_free_function(file, name)?); + require_ordered_markers(relative, name, &function, markers)?; + } + Ok(()) +} + +fn require_ordered_markers( + relative: &str, + authority: &str, + source: &str, + markers: &[&str], +) -> Result<(), String> { + let mut offset = 0_usize; + for marker in markers { + let Some(found) = source[offset..].find(marker) else { + return Err(format!( + "{relative} {authority} is missing ordered authority witness `{marker}`" + )); + }; + offset += found + marker.len(); + } + Ok(()) +} + +fn validate_command_reachability(workspace_root: &Path) -> Result<(), String> { + let contract = rust_source(workspace_root, CONTRACT_COMMAND_SOURCE_RELATIVE)?; + let main = rust_source(workspace_root, XTASK_MAIN_SOURCE_RELATIVE)?; + let main = syn::parse_file(&main) + .map_err(|error| format!("parse {XTASK_MAIN_SOURCE_RELATIVE}: {error}"))?; + let aggregate = compact_tokens(exact_top_level_function( + &syn::parse_file(&contract) + .map_err(|error| format!("parse {CONTRACT_COMMAND_SOURCE_RELATIVE}: {error}"))?, + "validate_artifact_contracts", + )?); + for ordered in [ + "validate_source_maintenance_manifest(workspace_root)?", + "validate_raw_source_rebuild_manifest(workspace_root)?", + "validate_knowledge_contract_manifest(workspace_root)", + ] { + if !aggregate.contains(ordered) { + return Err(format!( + "aggregate contract authority does not reach raw-source rebuild validation through `{ordered}`" + )); + } + } + let source_index = aggregate + .find("validate_source_maintenance_manifest(workspace_root)?") + .expect("checked above"); + let rebuild_index = aggregate + .find("validate_raw_source_rebuild_manifest(workspace_root)?") + .expect("checked above"); + let knowledge_index = aggregate + .find("validate_knowledge_contract_manifest(workspace_root)") + .expect("checked above"); + if !(source_index < rebuild_index && rebuild_index < knowledge_index) { + return Err( + "aggregate contract authority must validate the immutable predecessor before raw-source rebuild and knowledge contracts" + .to_owned(), + ); + } + + for (function_name, expected_call) in [ + ( + "validate_contract", + "contract::validate_artifact_contracts(&root)", + ), + ( + "release_preflight_at", + "contract::validate_artifact_contracts(root)", + ), + ] { + let function = exact_top_level_function(&main, function_name)?; + let calls = direct_call_tokens(function); + if calls.iter().filter(|call| *call == expected_call).count() != 1 { + return Err(format!( + "{XTASK_MAIN_SOURCE_RELATIVE}::{function_name} must directly reach `{expected_call}` exactly once" + )); + } + } + + let run_contract = exact_top_level_match(&main, "run_contract")?; + let raw_arm = exact_match_arm(run_contract, "Some(\"raw-source-rebuild-manifest\")")?; + let expected_raw_arm = syn::parse_str::<syn::Expr>( + r#"match &args[1..] { + [] => contract::validate_raw_source_rebuild_manifest(&workspace_root()), + [flag] if flag == "--write" => { + contract::write_raw_source_rebuild_manifest(&workspace_root()) + } + _ => Err( + "raw-source-rebuild-manifest accepts no arguments or exactly --write".to_string(), + ), + }"#, + ) + .map_err(|error| format!("parse governed raw-source rebuild command arm: {error}"))?; + if raw_arm.guard.is_some() + || compact_tokens(raw_arm.body.as_ref()) != compact_tokens(&expected_raw_arm) + { + return Err(format!( + "{XTASK_MAIN_SOURCE_RELATIVE} raw-source rebuild command arm drifted" + )); + } + + let run_release = exact_top_level_match(&main, "run_release")?; + let preflight_arm = exact_match_arm(run_release, "Some(\"preflight\")")?; + if preflight_arm.guard.is_some() + || compact_tokens(preflight_arm.body.as_ref()) != "release_preflight()" + { + return Err(format!( + "{XTASK_MAIN_SOURCE_RELATIVE} release preflight command arm drifted" + )); + } + Ok(()) +} + +fn direct_call_tokens(function: &syn::ItemFn) -> Vec<String> { + struct Audit(Vec<String>); + + impl<'ast> syn::visit::Visit<'ast> for Audit { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + self.0.push(compact_tokens(call)); + syn::visit::visit_expr_call(self, call); + } + } + + use syn::visit::Visit; + let mut audit = Audit(Vec::new()); + audit.visit_block(&function.block); + audit.0 +} + +fn exact_top_level_match<'a>( + file: &'a syn::File, + function_name: &str, +) -> Result<&'a syn::ExprMatch, String> { + let function = exact_top_level_function(file, function_name)?; + let [syn::Stmt::Expr(syn::Expr::Match(expression), None)] = function.block.stmts.as_slice() + else { + return Err(format!( + "{XTASK_MAIN_SOURCE_RELATIVE}::{function_name} must contain exactly one top-level match expression" + )); + }; + Ok(expression) +} + +fn exact_match_arm<'a>( + expression: &'a syn::ExprMatch, + pattern: &str, +) -> Result<&'a syn::Arm, String> { + let matching = expression + .arms + .iter() + .filter(|arm| compact_tokens(&arm.pat) == pattern) + .collect::<Vec<_>>(); + let [arm] = matching.as_slice() else { + return Err(format!( + "{XTASK_MAIN_SOURCE_RELATIVE} must contain exactly one command arm `{pattern}`; found {}", + matching.len() + )); + }; + Ok(arm) +} + +fn validate_release_authority(workspace_root: &Path) -> Result<(), String> { + let release_bytes = read_regular_file(workspace_root, RELEASE_RECORD_RELATIVE)?; + let release_source = std::str::from_utf8(&release_bytes) + .map_err(|error| format!("{RELEASE_RECORD_RELATIVE} must be UTF-8: {error}"))?; + let release: toml::Value = toml::from_str(release_source) + .map_err(|error| format!("parse {RELEASE_RECORD_RELATIVE}: {error}"))?; + let changes = release + .get("changes") + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("{RELEASE_RECORD_RELATIVE} must define changes"))?; + let matching = changes + .iter() + .filter(|change| change.get("id").and_then(toml::Value::as_str) == Some(RELEASE_CHANGE_ID)) + .collect::<Vec<_>>(); + let [change] = matching.as_slice() else { + return Err(format!( + "{RELEASE_RECORD_RELATIVE} must define exactly one `{RELEASE_CHANGE_ID}` change; found {}", + matching.len() + )); + }; + let impacts = change + .get("semver_impacts") + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("release change `{RELEASE_CHANGE_ID}` has no semver impacts"))? + .iter() + .map(|impact| { + impact.as_str().ok_or_else(|| { + format!("release change `{RELEASE_CHANGE_ID}` semver impacts must be strings") + }) + }) + .collect::<Result<Vec<_>, _>>()?; + if change.get("classification").and_then(toml::Value::as_str) != Some("breaking") + || impacts != RELEASE_CHANGE_IMPACTS + || change.get("summary").and_then(toml::Value::as_str) != Some(RELEASE_CHANGE_SUMMARY) + { + return Err(format!( + "release change `{RELEASE_CHANGE_ID}` must retain its exact breaking classification, semver impacts, and summary" + )); + } + + let changelog_bytes = read_regular_file(workspace_root, CHANGELOG_RELATIVE)?; + let changelog = std::str::from_utf8(&changelog_bytes) + .map_err(|error| format!("{CHANGELOG_RELATIVE} must be UTF-8: {error}"))?; + if changelog.matches(CHANGELOG_RELEASE_MARKER).count() != 1 { + return Err(format!( + "{CHANGELOG_RELATIVE} must contain exactly one `{CHANGELOG_RELEASE_MARKER}` marker" + )); + } + let current_start = changelog + .find("## [1.0.0-alpha.1]") + .ok_or_else(|| format!("{CHANGELOG_RELATIVE} has no current release section"))?; + let current = &changelog[current_start..]; + let current_end = current["## [1.0.0-alpha.1]".len()..] + .find("\n## [") + .map_or(current.len(), |offset| offset + "## [1.0.0-alpha.1]".len()); + let current = &current[..current_end]; + if !current.contains(&format!( + "{CHANGELOG_RELEASE_MARKER}\n- Event-store schema v4 now exposes a versioned raw-source rebuild operation" + )) { + return Err(format!( + "{CHANGELOG_RELATIVE} current release must bind the raw-source rebuild note to `{RELEASE_CHANGE_ID}`" + )); + } + Ok(()) +} + +fn validate_result_vector( + workspace_root: &Path, + vector: &RawSourceRebuildVector, +) -> Result<(), String> { + if vector.schema_version != SCHEMA_VERSION || vector.contract_id != CONTRACT_ID { + return Err(format!( + "{RESULT_VECTOR_CANONICAL_RELATIVE} has inconsistent identity" + )); + } + if vector.cases.is_empty() { + return Err(format!( + "{RESULT_VECTOR_CANONICAL_RELATIVE} must contain executable cases" + )); + } + validate_delegated_suite_inventory(vector)?; + validate_unique( + "raw-source rebuild vector case IDs", + vector.cases.iter().map(|case| case.id.as_str()), + )?; + validate_failpoint_result_vector_cases(vector)?; + let mut direct_count = 0_usize; + for case in &vector.cases { + if case.expected_outcome.trim().is_empty() { + return Err(format!("vector case `{}` has no expected outcome", case.id)); + } + match case.execution.as_str() { + "direct_executor" => { + direct_count += 1; + if case.authority != RESULT_VECTOR_EXECUTOR_TEST + || case.authority_path != RESULT_VECTOR_EXECUTOR_RELATIVE + || !RESULT_VECTOR_DIRECT_CASE_IDS.contains(&case.id.as_str()) + { + return Err(format!( + "direct vector case `{}` must bind the canonical executor", + case.id + )); + } + if case.expected_immutable_raw_digest.is_none() + || case.expected_active_product_state_digest.is_none() + { + return Err(format!( + "direct vector case `{}` must freeze exact immutable-raw and active-product digest bytes", + case.id + )); + } + } + "delegated_rust_test" => {} + other => { + return Err(format!( + "vector case `{}` has unsupported execution mode `{other}`", + case.id + )); + } + } + for digest in [ + case.expected_immutable_raw_digest.as_deref(), + case.expected_active_product_state_digest.as_deref(), + ] + .into_iter() + .flatten() + { + validate_vector_expected_digest(digest)?; + } + } + if direct_count != RESULT_VECTOR_DIRECT_CASE_IDS.len() { + return Err(format!( + "raw-source rebuild vector requires exactly {} direct executor cases; found {direct_count}", + RESULT_VECTOR_DIRECT_CASE_IDS.len() + )); + } + for authority in &vector.delegated_suite.authorities { + validate_executable_test( + workspace_root, + &authority.authority_path, + &authority.authority, + )?; + } + validate_executable_test( + workspace_root, + RESULT_VECTOR_EXECUTOR_RELATIVE, + RESULT_VECTOR_EXECUTOR_TEST, + )?; + validate_direct_executor_authority(workspace_root)?; + validate_delegated_suite_contract_lane(workspace_root) +} + +fn validate_failpoint_result_vector_cases(vector: &RawSourceRebuildVector) -> Result<(), String> { + let expected_ids = REBUILD_FAILPOINTS + .iter() + .map(|failpoint| failpoint.rollback_case_id) + .collect::<BTreeSet<_>>(); + let actual_cases = vector + .cases + .iter() + .filter(|case| { + case.authority == REBUILD_FAILPOINT_TEST || case.id.starts_with("rollback_after_") + }) + .collect::<Vec<_>>(); + let actual_ids = actual_cases + .iter() + .map(|case| case.id.as_str()) + .collect::<BTreeSet<_>>(); + if actual_ids != expected_ids { + return Err(format!( + "raw-source rebuild vector rollback failpoint case IDs drifted: expected {expected_ids:?}, found {actual_ids:?}" + )); + } + for failpoint in REBUILD_FAILPOINTS { + let matching = actual_cases + .iter() + .filter(|case| case.id == failpoint.rollback_case_id) + .copied() + .collect::<Vec<_>>(); + let [case] = matching.as_slice() else { + return Err(format!( + "raw-source rebuild vector must bind rollback case `{}` exactly once", + failpoint.rollback_case_id + )); + }; + if case.execution != "delegated_rust_test" + || case.authority != REBUILD_FAILPOINT_TEST + || case.authority_path != REBUILD_FAILPOINT_TEST_SOURCE_RELATIVE + { + return Err(format!( + "raw-source rebuild rollback case `{}` must bind the governed failpoint test authority", + failpoint.rollback_case_id + )); + } + } + Ok(()) +} + +fn validate_delegated_suite_inventory(vector: &RawSourceRebuildVector) -> Result<(), String> { + let suite = &vector.delegated_suite; + if suite.id != RESULT_VECTOR_DELEGATED_SUITE_ID + || suite.lane != RESULT_VECTOR_DELEGATED_SUITE_LANE + || suite.package != RESULT_VECTOR_DELEGATED_SUITE_PACKAGE + { + return Err(format!( + "{RESULT_VECTOR_CANONICAL_RELATIVE} delegated suite identity, lane, or package drifted" + )); + } + if suite.authorities.is_empty() { + return Err(format!( + "{RESULT_VECTOR_CANONICAL_RELATIVE} delegated suite must contain exact test authorities" + )); + } + let suite_authorities = suite + .authorities + .iter() + .map(|authority| { + ( + authority.authority_path.as_str(), + authority.authority.as_str(), + ) + }) + .collect::<Vec<_>>(); + let suite_authority_keys = suite_authorities + .iter() + .map(|(path, authority)| format!("{path}::{authority}")) + .collect::<Vec<_>>(); + validate_unique( + "raw-source rebuild delegated suite authorities", + suite_authority_keys.iter().map(String::as_str), + )?; + if suite_authorities + .iter() + .any(|(path, authority)| path.trim().is_empty() || authority.trim().is_empty()) + { + return Err( + "raw-source rebuild delegated suite authorities must have nonempty paths and names" + .to_owned(), + ); + } + + let suite_authorities = suite_authorities.into_iter().collect::<BTreeSet<_>>(); + let delegated_case_authorities = vector + .cases + .iter() + .filter(|case| case.execution == "delegated_rust_test") + .map(|case| (case.authority_path.as_str(), case.authority.as_str())) + .collect::<BTreeSet<_>>(); + if suite_authorities != delegated_case_authorities { + let missing = delegated_case_authorities + .difference(&suite_authorities) + .map(|(path, authority)| format!("{path}::{authority}")) + .collect::<Vec<_>>(); + let unrepresented = suite_authorities + .difference(&delegated_case_authorities) + .map(|(path, authority)| format!("{path}::{authority}")) + .collect::<Vec<_>>(); + return Err(format!( + "raw-source rebuild delegated suite must exactly cover delegated vector cases; missing {missing:?}; unrepresented {unrepresented:?}" + )); + } + Ok(()) +} + +fn validate_delegated_suite_contract_lane(workspace_root: &Path) -> Result<(), String> { + let flake = regular_utf8_source(workspace_root, FLAKE_SOURCE_RELATIVE)?; + let apps = regular_utf8_source(workspace_root, CONTRACT_APP_SOURCE_RELATIVE)?; + let common = regular_utf8_source(workspace_root, CONTRACT_LANE_SOURCE_RELATIVE)?; + let toolchains = regular_utf8_source(workspace_root, TOOLCHAIN_ROUTING_SOURCE_RELATIVE)?; + validate_delegated_suite_contract_lane_sources(&flake, &apps, &common, &toolchains)?; + validate_flake_lock_authority(workspace_root)?; + validate_delegated_suite_test_targets(workspace_root) +} + +fn validate_delegated_suite_test_targets(workspace_root: &Path) -> Result<(), String> { + let cargo_relative = "crates/event_store/Cargo.toml"; + let cargo = regular_utf8_source(workspace_root, cargo_relative)?; + let cargo: toml::Value = + toml::from_str(&cargo).map_err(|error| format!("parse {cargo_relative}: {error}"))?; + let package = cargo + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{cargo_relative} must define one package"))?; + if package.get("name").and_then(toml::Value::as_str) + != Some(RESULT_VECTOR_DELEGATED_SUITE_PACKAGE) + || package.get("autotests").and_then(toml::Value::as_bool) == Some(false) + || cargo + .get("lib") + .and_then(|lib| lib.get("test")) + .and_then(toml::Value::as_bool) + == Some(false) + { + return Err(format!( + "{cargo_relative} must leave the delegated library tests and direct integration executor enabled for `{RESULT_VECTOR_DELEGATED_SUITE_PACKAGE}`" + )); + } + + for (relative, module_name, expected_attributes) in [ + ( + "crates/event_store/src/store.rs", + "raw_source_rebuild_v1_tests", + &["#[cfg(test)]"][..], + ), + ( + "crates/event_store/src/nip09/reconciliation_v1.rs", + "visibility_oracle_v1", + &[][..], + ), + ] { + let file = rust_file(workspace_root, relative)?; + let modules = file + .items + .iter() + .filter_map(|item| match item { + Item::Mod(module) if module.ident == module_name => Some(module), + _ => None, + }) + .collect::<Vec<_>>(); + let [module] = modules.as_slice() else { + return Err(format!( + "{relative} must register delegated test module `{module_name}` exactly once; found {}", + modules.len() + )); + }; + let attributes = module.attrs.iter().map(compact_tokens).collect::<Vec<_>>(); + if !matches!(module.vis, syn::Visibility::Inherited) + || module.content.is_some() + || attributes != expected_attributes + { + return Err(format!( + "{relative} delegated test module `{module_name}` visibility, source routing, or attributes drifted" + )); + } + } + Ok(()) +} + +fn validate_delegated_suite_contract_lane_sources( + flake: &str, + apps: &str, + common: &str, + toolchains: &str, +) -> Result<(), String> { + let flake_toolchains = + nix_code_occurrences(flake, "toolchains = import ./build/nix/toolchains.nix {"); + let all_flake_toolchains = nix_code_occurrences(flake, "toolchains ="); + if flake_toolchains.len() != 1 || all_flake_toolchains != flake_toolchains { + return Err(format!( + "{FLAKE_SOURCE_RELATIVE} must bind exactly one toolchain authority from ./build/nix/toolchains.nix" + )); + } + let flake_common = nix_code_occurrences(flake, "common = import ./build/nix/common.nix {"); + let all_flake_common = nix_code_occurrences(flake, "common ="); + if flake_common.len() != 1 || all_flake_common != flake_common { + return Err(format!( + "{FLAKE_SOURCE_RELATIVE} must bind exactly one common authority from ./build/nix/common.nix" + )); + } + let flake_apps = nix_code_occurrences(flake, "apps = import ./build/nix/apps.nix {"); + let all_flake_apps = nix_code_occurrences(flake, "apps ="); + if flake_apps.len() != 1 || all_flake_apps != flake_apps { + return Err(format!( + "{FLAKE_SOURCE_RELATIVE} must export exactly one per-system apps authority from ./build/nix/apps.nix" + )); + } + let per_system = nix_code_occurrences(flake, "perSystem ="); + if per_system.len() != 1 + || per_system[0] >= flake_toolchains[0] + || flake_toolchains[0] >= flake_common[0] + || flake_common[0] >= flake_apps[0] + { + return Err(format!( + "{FLAKE_SOURCE_RELATIVE} must bind governed toolchains, common, and apps imports in order through perSystem" + )); + } + for (label, assignment_start, source, expected) in [ + ( + "toolchains", + flake_toolchains[0], + nix_balanced_slice(flake, flake_toolchains[0], b'{', b'}')?, + "{inheritpkgs;}", + ), + ( + "common", + flake_common[0], + nix_balanced_slice(flake, flake_common[0], b'{', b'}')?, + "{crane=inputs.crane;inheritlibpkgstoolchains;}", + ), + ( + "apps", + flake_apps[0], + nix_balanced_slice(flake, flake_apps[0], b'{', b'}')?, + "{inheritcommonconfiglibpkgstoolchains;}", + ), + ] { + let actual = source.split_whitespace().collect::<String>(); + if actual != expected { + return Err(format!( + "{FLAKE_SOURCE_RELATIVE} `{label}` import arguments drifted from the exact delegated contract-lane authority" + )); + } + validate_nix_attrset_assignment_terminator(flake, assignment_start).map_err(|error| { + format!( + "{FLAKE_SOURCE_RELATIVE} `{label}` import must end immediately after its governed arguments: {error}" + ) + })?; + } + + let stable_assignments = nix_code_occurrences(toolchains, "stable ="); + let stable_authority = nix_code_occurrences( + toolchains, + "stable = pkgs.rust-bin.fromRustupToolchainFile ../../rust-toolchain.toml;", + ); + if stable_assignments.len() != 1 || stable_authority != stable_assignments { + return Err(format!( + "{TOOLCHAIN_ROUTING_SOURCE_RELATIVE} must route the stable toolchain exactly through ../../{RUST_TOOLCHAIN_RELATIVE}" + )); + } + + let cargo_source_assignments = + nix_code_occurrences(common, "cargoSource = lib.fileset.toSource {"); + if cargo_source_assignments.len() != 1 { + return Err(format!( + "{CONTRACT_LANE_SOURCE_RELATIVE} must define exactly one cargoSource fileset" + )); + } + let cargo_source = nix_balanced_slice(common, cargo_source_assignments[0], b'{', b'}')?; + for required in [ + "../../Cargo.toml", + "../../Cargo.lock", + "../../flake.nix", + "../../flake.lock", + "../../build/nix/apps.nix", + "../../build/nix/common.nix", + "../../build/nix/toolchains.nix", + "../../rust-toolchain.toml", + "../../tools", + ] { + if nix_code_occurrences(cargo_source, required).len() != 1 { + return Err(format!( + "{CONTRACT_LANE_SOURCE_RELATIVE} cargoSource must include governed input `{required}` exactly once" + )); + } + } + + let contract_apps = nix_code_occurrences(apps, "contract = mkRepoApp {"); + if contract_apps.len() != 1 { + return Err(format!( + "{CONTRACT_APP_SOURCE_RELATIVE} must define exactly one executable contract app" + )); + } + let contract_app = nix_balanced_slice(apps, contract_apps[0], b'{', b'}')?; + let contract_app_authority = contract_app.split_whitespace().collect::<String>(); + let expected_contract_app_authority = concat!( + "{", + "name=\"contract\";", + "description=\"Runthecore-librarycontractlane\";", + "runtimeInputs=common.runtimeInputs.stable;", + "command=common.contractCommand;", + "}" + ); + if contract_app_authority != expected_contract_app_authority { + return Err(format!( + "{CONTRACT_APP_SOURCE_RELATIVE} contract app must bind the exact stable runtime, default path, environment, and command authority" + )); + } + + let crate_list_assignments = nix_code_occurrences(common, "coreContractCrates = ["); + if crate_list_assignments.len() != 1 { + return Err(format!( + "{CONTRACT_LANE_SOURCE_RELATIVE} must define one literal coreContractCrates list" + )); + } + let crate_list = nix_balanced_slice(common, crate_list_assignments[0], b'[', b']')?; + let crates = nix_literal_string_array(crate_list)?; + validate_unique( + "Nix core contract crates", + crates.iter().map(String::as_str), + )?; + if crates + .iter() + .filter(|package| package.as_str() == RESULT_VECTOR_DELEGATED_SUITE_PACKAGE) + .count() + != 1 + { + return Err(format!( + "{CONTRACT_LANE_SOURCE_RELATIVE} coreContractCrates must contain `{RESULT_VECTOR_DELEGATED_SUITE_PACKAGE}` exactly once" + )); + } + + let cargo_args = nix_assignment_through_semicolon(common, "coreContractCargoArgs")?; + let cargo_arg_lines = cargo_args + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .collect::<Vec<_>>(); + if cargo_arg_lines.len() != 3 + || cargo_arg_lines[0] != "coreContractCargoArgs =" + || cargo_arg_lines[1] + != "lib.concatStringsSep \" \" (map (crate: \"-p ${crate}\") coreContractCrates)" + || cargo_arg_lines[2] + != "+ \" --features radroots_event_codec/serde_json,radroots_event_codec/nostr,radroots_nostr/blossom,radroots_nostr/client,radroots_nostr/codec,radroots_nostr/events\";" + { + return Err(format!( + "{CONTRACT_LANE_SOURCE_RELATIVE} coreContractCargoArgs must map every literal core contract crate to an unfiltered `-p` package selection" + )); + } + + let contract_command = nix_indented_string_assignment(common, "contractCommand")?; + let contract_commands = contract_command + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .collect::<Vec<_>>(); + let expected_commands = [ + "cargo run -q -p xtask -- hygiene forbidden-identifiers", + "cargo check -q ${coreContractCargoArgs}", + "cargo test -q ${coreContractCargoArgs}", + "cargo run -q -p xtask -- contract validate", + ]; + if contract_commands != expected_commands { + return Err(format!( + "{CONTRACT_LANE_SOURCE_RELATIVE} contractCommand must run the exact unfiltered core package test lane before contract validation" + )); + } + Ok(()) +} + +fn validate_flake_lock_authority(workspace_root: &Path) -> Result<(), String> { + let source = regular_utf8_source(workspace_root, FLAKE_LOCK_RELATIVE)?; + let lock: Value = serde_json::from_str(&source) + .map_err(|error| format!("parse {FLAKE_LOCK_RELATIVE}: {error}"))?; + if lock.get("version").and_then(Value::as_u64) != Some(7) + || lock.get("root").and_then(Value::as_str) != Some("root") + { + return Err(format!( + "{FLAKE_LOCK_RELATIVE} must remain a version-7 lock with the root node named `root`" + )); + } + let nodes = lock + .get("nodes") + .and_then(Value::as_object) + .ok_or_else(|| format!("{FLAKE_LOCK_RELATIVE} must define a nodes object"))?; + let root_inputs = nodes + .get("root") + .and_then(|root| root.get("inputs")) + .and_then(Value::as_object) + .ok_or_else(|| format!("{FLAKE_LOCK_RELATIVE} root node must define direct inputs"))?; + let expected_inputs = [ + ("crane", "crane"), + ("flake-parts", "flake-parts"), + ("nixpkgs", "nixpkgs"), + ("rust-overlay", "rust-overlay"), + ("treefmt-nix", "treefmt-nix"), + ] + .into_iter() + .map(|(name, node)| (name.to_owned(), Value::String(node.to_owned()))) + .collect::<serde_json::Map<_, _>>(); + if root_inputs != &expected_inputs { + return Err(format!( + "{FLAKE_LOCK_RELATIVE} root inputs must exactly lock crane, flake-parts, nixpkgs, rust-overlay, and treefmt-nix" + )); + } + for node in expected_inputs.values().filter_map(Value::as_str) { + let locked = nodes + .get(node) + .and_then(|node| node.get("locked")) + .and_then(Value::as_object) + .ok_or_else(|| { + format!("{FLAKE_LOCK_RELATIVE} direct input node `{node}` must be locked") + })?; + for field in ["narHash", "rev"] { + if locked + .get(field) + .and_then(Value::as_str) + .is_none_or(str::is_empty) + { + return Err(format!( + "{FLAKE_LOCK_RELATIVE} direct input node `{node}` must bind nonempty `{field}`" + )); + } + } + } + Ok(()) +} + +fn regular_utf8_source(workspace_root: &Path, relative: &str) -> Result<String, String> { + let bytes = read_regular_file(workspace_root, relative)?; + String::from_utf8(bytes).map_err(|error| format!("{relative} must be UTF-8: {error}")) +} + +fn nix_code_mask(source: &str) -> Vec<bool> { + #[derive(Clone, Copy)] + enum State { + Code, + LineComment, + BlockComment, + DoubleString, + IndentedString, + } + + let bytes = source.as_bytes(); + let mut mask = vec![false; bytes.len()]; + let mut state = State::Code; + let mut index = 0_usize; + while index < bytes.len() { + match state { + State::Code => { + if bytes[index] == b'#' { + state = State::LineComment; + index += 1; + } else if bytes[index..].starts_with(b"/*") { + state = State::BlockComment; + index += 2; + } else if bytes[index] == b'"' { + mask[index] = true; + state = State::DoubleString; + index += 1; + } else if bytes[index..].starts_with(b"''") { + mask[index] = true; + state = State::IndentedString; + index += 2; + } else { + mask[index] = true; + index += 1; + } + } + State::LineComment => { + if bytes[index] == b'\n' { + state = State::Code; + } else { + index += 1; + } + } + State::BlockComment => { + if bytes[index..].starts_with(b"*/") { + state = State::Code; + index += 2; + } else { + index += 1; + } + } + State::DoubleString => { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + } else if bytes[index] == b'"' { + state = State::Code; + index += 1; + } else { + index += 1; + } + } + State::IndentedString => { + if bytes[index..].starts_with(b"''") { + match bytes.get(index + 2) { + Some(b'$' | b'\'' | b'\\') => index += 3, + _ => { + state = State::Code; + index += 2; + } + } + } else { + index += 1; + } + } + } + } + mask +} + +fn nix_code_occurrences(source: &str, needle: &str) -> Vec<usize> { + let mask = nix_code_mask(source); + source + .match_indices(needle) + .filter_map(|(index, _)| mask.get(index).copied().unwrap_or(false).then_some(index)) + .collect() +} + +fn nix_balanced_slice( + source: &str, + search_start: usize, + open: u8, + close: u8, +) -> Result<&str, String> { + let mask = nix_code_mask(source); + let bytes = source.as_bytes(); + let start = (search_start..bytes.len()) + .find(|index| mask[*index] && bytes[*index] == open) + .ok_or_else(|| { + format!( + "governed Nix authority has no `{}` opener", + char::from(open) + ) + })?; + let mut depth = 0_usize; + for index in start..bytes.len() { + if !mask[index] { + continue; + } + if bytes[index] == open { + depth += 1; + } else if bytes[index] == close { + depth = depth + .checked_sub(1) + .ok_or_else(|| "governed Nix authority has unbalanced delimiters".to_owned())?; + if depth == 0 { + return Ok(&source[start..=index]); + } + } + } + Err("governed Nix authority has an unterminated delimiter".to_owned()) +} + +fn validate_nix_attrset_assignment_terminator( + source: &str, + assignment_start: usize, +) -> Result<(), String> { + let attrset = nix_balanced_slice(source, assignment_start, b'{', b'}')?; + let attrset_start = attrset.as_ptr() as usize - source.as_ptr() as usize; + let after_attrset = &source[attrset_start + attrset.len()..]; + let first = after_attrset + .bytes() + .find(|byte| !byte.is_ascii_whitespace()); + if first != Some(b';') { + return Err("postfix merge or expression detected before assignment terminator".to_owned()); + } + Ok(()) +} + +fn nix_literal_string_array(source: &str) -> Result<Vec<String>, String> { + let mask = nix_code_mask(source); + let bytes = source.as_bytes(); + let mut values = Vec::new(); + let mut index = 0_usize; + while index < bytes.len() { + if !mask[index] { + index += 1; + continue; + } + match bytes[index] { + b'[' | b']' | b' ' | b'\t' | b'\r' | b'\n' => index += 1, + b'"' => { + let start = index + 1; + index = start; + while index < bytes.len() && bytes[index] != b'"' { + if bytes[index] == b'\\' || bytes[index..].starts_with(b"${") { + return Err( + "governed Nix package list must use plain literal strings".to_owned() + ); + } + index += 1; + } + if index == bytes.len() { + return Err("governed Nix package list has an unterminated string".to_owned()); + } + values.push(source[start..index].to_owned()); + index += 1; + } + _ => { + return Err( + "governed Nix package list must contain only literal strings".to_owned(), + ); + } + } + } + Ok(values) +} + +fn nix_assignment_through_semicolon<'a>(source: &'a str, name: &str) -> Result<&'a str, String> { + let needle = format!("{name} ="); + let starts = nix_code_occurrences(source, &needle); + let [start] = starts.as_slice() else { + return Err(format!( + "governed Nix source must define `{name}` exactly once; found {}", + starts.len() + )); + }; + let mask = nix_code_mask(source); + let end = (*start..source.len()) + .find(|index| mask[*index] && source.as_bytes()[*index] == b';') + .ok_or_else(|| format!("governed Nix assignment `{name}` has no terminator"))?; + Ok(&source[*start..=end]) +} + +fn nix_indented_string_assignment<'a>(source: &'a str, name: &str) -> Result<&'a str, String> { + let needle = format!("{name} = ''"); + let starts = nix_code_occurrences(source, &needle); + let [start] = starts.as_slice() else { + return Err(format!( + "governed Nix source must define indented string `{name}` exactly once; found {}", + starts.len() + )); + }; + let content_start = *start + needle.len(); + let bytes = source.as_bytes(); + let mut index = content_start; + while index < bytes.len() { + if bytes[index..].starts_with(b"''") { + match bytes.get(index + 2) { + Some(b'$' | b'\'' | b'\\') => index += 3, + _ => { + let content = &source[content_start..index]; + index += 2; + while bytes.get(index).is_some_and(u8::is_ascii_whitespace) { + index += 1; + } + if bytes.get(index) != Some(&b';') { + return Err(format!( + "governed Nix indented string `{name}` must end with one semicolon" + )); + } + return Ok(content); + } + } + } else { + index += 1; + } + } + Err(format!( + "governed Nix indented string `{name}` is unterminated" + )) +} + +fn validate_result_vector_identity(bytes: &[u8]) -> Result<(), String> { + if bytes.len() != RESULT_VECTOR_BYTE_LENGTH || sha256_hex(bytes) != RESULT_VECTOR_SHA256 { + return Err(format!( + "{RESULT_VECTOR_CANONICAL_RELATIVE} does not match the immutable executable case inventory" + )); + } + Ok(()) +} + +fn validate_direct_executor_authority(workspace_root: &Path) -> Result<(), String> { + let file = rust_file(workspace_root, RESULT_VECTOR_EXECUTOR_RELATIVE)?; + let full_source = compact_tokens(&file); + if !full_source.contains("include_bytes!(\"fixtures/food_availability_projection.v1.json\")") { + return Err( + "direct raw-source rebuild vector executor must byte-bind the signed Food fixture" + .to_owned(), + ); + } + let function = compact_tokens(exact_free_function(&file, RESULT_VECTOR_EXECUTOR_TEST)?); + for marker in [ + "decode_digest(", + "expected_immutable_raw_digest.as_deref().expect(", + "expected_active_product_state_digest.as_deref().expect(", + "first.immutable_raw_digest().as_bytes(),&expected_immutable_raw_digest", + "first.active_product_state_digest().as_bytes(),&expected_active_product_state_digest", + "signed_food_fixture_ingest()", + "food_first.immutable_raw_digest().as_bytes(),&expected_food_raw_digest", + "food_first.active_product_state_digest().as_bytes(),&expected_food_product_digest", + "food_second.immutable_raw_digest(),food_first.immutable_raw_digest()", + "food_second.active_product_state_digest(),food_first.active_product_state_digest()", + ] { + if !function.contains(marker) { + return Err(format!( + "direct raw-source rebuild vector executor is missing exact digest authority `{marker}`" + )); + } + } + for case_id in RESULT_VECTOR_DIRECT_CASE_IDS { + if !function.contains(case_id) { + return Err(format!( + "direct raw-source rebuild vector executor does not execute case `{case_id}`" + )); + } + } + Ok(()) +} + +fn validate_executable_test( + workspace_root: &Path, + relative: &str, + name: &str, +) -> Result<(), String> { + let file = rust_file(workspace_root, relative)?; + #[derive(Clone)] + struct ModuleContext { + name: String, + attributes: Vec<String>, + private: bool, + } + struct Match<'a> { + function: &'a syn::ItemFn, + modules: Vec<ModuleContext>, + } + fn collect<'a>( + items: &'a [Item], + name: &str, + modules: &mut Vec<ModuleContext>, + matches: &mut Vec<Match<'a>>, + ) { + for item in items { + match item { + Item::Fn(function) if function.sig.ident == name => matches.push(Match { + function, + modules: modules.clone(), + }), + Item::Mod(module) => { + if let Some((_, items)) = &module.content { + modules.push(ModuleContext { + name: module.ident.to_string(), + attributes: module.attrs.iter().map(compact_tokens).collect(), + private: matches!(module.vis, syn::Visibility::Inherited), + }); + collect(items, name, modules, matches); + modules.pop(); + } + } + _ => {} + } + } + } + let mut matches = Vec::new(); + collect(&file.items, name, &mut Vec::new(), &mut matches); + let [matched] = matches.as_slice() else { + return Err(format!( + "executable raw-source rebuild authority {relative}::{name} must exist exactly once; found {}", + matches.len() + )); + }; + let expected_test_module = (relative + == "crates/event_store/src/nip09/reconciliation_v1/visibility_oracle_v1.rs") + .then_some("tests"); + match (expected_test_module, matched.modules.as_slice()) { + (None, []) => {} + (Some(expected), [module]) + if module.name == expected + && module.private + && module.attributes == ["#[cfg(test)]"] => {} + _ => { + let actual = matched + .modules + .iter() + .map(|module| { + format!( + "{}:{:?}:private={}", + module.name, module.attributes, module.private + ) + }) + .collect::<Vec<_>>(); + return Err(format!( + "executable raw-source rebuild authority {relative}::{name} has unsupported module ancestry {actual:?}" + )); + } + } + let function = matched.function; + let attrs = function + .attrs + .iter() + .map(compact_tokens) + .collect::<Vec<_>>(); + if attrs.as_slice() != ["#[test]"] && attrs.as_slice() != ["#[tokio::test]"] { + return Err(format!( + "executable raw-source rebuild authority {relative}::{name} must have exactly one unconditional test attribute" + )); + } + struct ReturnCounter(usize); + impl<'ast> syn::visit::Visit<'ast> for ReturnCounter { + fn visit_expr_return(&mut self, expression: &'ast syn::ExprReturn) { + self.0 += 1; + syn::visit::visit_expr_return(self, expression); + } + } + use syn::visit::Visit; + let mut returns = ReturnCounter(0); + returns.visit_block(&function.block); + if returns.0 != 0 { + return Err(format!( + "executable raw-source rebuild authority {relative}::{name} must not contain early return control flow" + )); + } + Ok(()) +} + +fn generated_descriptor( + manifest: &RawSourceRebuildManifest, + manifest_bytes: &[u8], + manifest_sha256: &str, +) -> String { + let manifest_json = std::str::from_utf8(manifest_bytes).expect("canonical manifest UTF-8"); + format!( + "// @generated by `{WRITE_COMMAND}`; do not edit.\n\ +#![allow(dead_code)]\n\ +\n\ +pub(crate) const RAW_SOURCE_REBUILD_MANIFEST_JSON: &str = {manifest_json:?};\n\ +pub(crate) const RAW_SOURCE_REBUILD_MANIFEST_BYTE_LENGTH: usize = {};\n\ +pub(crate) const RAW_SOURCE_REBUILD_MANIFEST_SHA256: &str =\n \"{manifest_sha256}\";\n\ +pub(crate) const RAW_SOURCE_REBUILD_CONTRACT_ID: &str =\n \"{CONTRACT_ID}\";\n\ +pub(crate) const RAW_SOURCE_REBUILD_AUTHORITY_ID: &str = \"{AUTHORITY_ID}\";\n\ +pub(crate) const RAW_SOURCE_REBUILD_PREDECESSOR_MANIFEST_SHA256: &str =\n \"{PREDECESSOR_MANIFEST_SHA256}\";\n\ +pub(crate) const RAW_SOURCE_REBUILD_EVENT_STORE_SCHEMA_VERSION: u32 = {EVENT_STORE_SCHEMA_VERSION};\n\ +pub(crate) const RAW_SOURCE_REBUILD_EVENT_CONTRACT_REGISTRY_VERSION: u32 = {EVENT_CONTRACT_REGISTRY_VERSION};\n\ +pub(crate) const RAW_SOURCE_REBUILD_RESULT_VECTOR_SHA256: &str =\n \"{}\";\n\ +pub(crate) const RAW_SOURCE_REBUILD_RESULT_VECTOR_EXECUTOR_SHA256: &str =\n \"{}\";\n", + manifest_bytes.len(), + manifest.result_vector.sha256, + manifest.result_vector.executor_sha256, + ) +} + +fn manifest_schema() -> Value { + let path_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": {"type": "string", "pattern": path_pattern}, + "byte_length": {"type": "integer", "minimum": 1}, + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "hash_algorithm": {"const": HASH_ALGORITHM} + }, + "additionalProperties": false + }); + let string_array = json!({ + "type": "array", + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + }); + let digest_field = json!({ + "type": "object", + "required": ["name", "framing"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "framing": {"enum": ["i64", "boolean", "optional_i64", "text", "optional_text", "blob"]} + }, + "additionalProperties": false + }); + let digest_query = json!({ + "type": "object", + "required": ["section", "sql", "fields"], + "properties": { + "section": {"type": "string", "minLength": 1}, + "sql": {"type": "string", "minLength": 1}, + "fields": {"type": "array", "minItems": 1, "items": digest_field} + }, + "additionalProperties": false + }); + let digest_framing = json!({ + "type": "object", + "required": ["section", "row", "signed_i64", "boolean", "optional", "text", "blob"], + "properties": { + "section": {"const": "S_then_N_then_u64be_length_then_utf8_name"}, + "row": {"const": "R"}, + "signed_i64": {"const": "I_then_i64be"}, + "boolean": {"const": "B_then_u8_0_or_1"}, + "optional": {"const": "O_then_presence_u8_then_nested_value_when_present"}, + "text": {"const": "T_then_u64be_length_then_utf8_bytes"}, + "blob": {"const": "X_then_u64be_length_then_bytes"} + }, + "additionalProperties": false + }); + json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://radroots.org/contracts/event-store/raw-source-rebuild-v1-manifest.schema.json", + "title": "Radroots event-store raw-source rebuild v1 manifest", + "type": "object", + "required": [ + "schema_version", "contract_id", "authority_id", "manifest_schema", "predecessor", + "migration_inventory", "runtime", "entry_points", "source_files", "public_api", + "result_vector" + ], + "properties": { + "schema_version": {"const": SCHEMA_VERSION}, + "contract_id": {"const": CONTRACT_ID}, + "authority_id": {"const": AUTHORITY_ID}, + "manifest_schema": file.clone(), + "predecessor": { + "type": "object", + "required": ["contract_id", "manifest"], + "properties": { + "contract_id": {"const": PREDECESSOR_CONTRACT_ID}, + "manifest": file.clone() + }, + "additionalProperties": false + }, + "migration_inventory": { + "type": "array", "minItems": 8, "maxItems": 8, "items": file.clone() + }, + "runtime": { + "type": "object", + "required": [ + "event_store_schema_version", "event_contract_registry_version", + "transaction_mode", "projection_cursor_count_limit", + "projection_cursor_rejection_probe_limit", "caller_main_table_count_limit", + "caller_foreign_key_row_count_limit", "caller_inbound_foreign_key_policy", + "caller_inbound_foreign_key_parent_tables", + "cold_repair_mode", + "immutable_raw_digest", "active_product_state_digest", + "visibility_oracle", "scoped_integrity_mode", "scoped_integrity_tables", "sqlite_sequence_scope", "stages", "failpoints", + "preserved_authorities" + ], + "properties": { + "event_store_schema_version": {"const": EVENT_STORE_SCHEMA_VERSION}, + "event_contract_registry_version": {"const": EVENT_CONTRACT_REGISTRY_VERSION}, + "transaction_mode": {"const": TRANSACTION_MODE}, + "projection_cursor_count_limit": {"const": PROJECTION_CURSOR_COUNT_LIMIT}, + "projection_cursor_rejection_probe_limit": {"const": PROJECTION_CURSOR_REJECTION_PROBE_LIMIT}, + "caller_main_table_count_limit": {"const": CALLER_MAIN_TABLE_COUNT_LIMIT}, + "caller_foreign_key_row_count_limit": {"const": CALLER_FOREIGN_KEY_ROW_COUNT_LIMIT}, + "caller_inbound_foreign_key_policy": {"const": CALLER_INBOUND_FOREIGN_KEY_POLICY}, + "caller_inbound_foreign_key_parent_tables": string_array.clone(), + "cold_repair_mode": {"const": COLD_REPAIR_MODE}, + "immutable_raw_digest": { + "type": "object", + "required": ["algorithm", "domain_utf8", "domain_terminator", "framing", "output_bytes", "source_queries"], + "properties": { + "algorithm": {"const": DIGEST_ALGORITHM}, + "domain_utf8": {"const": RAW_DIGEST_DOMAIN_UTF8}, + "domain_terminator": {"const": DIGEST_DOMAIN_TERMINATOR}, + "framing": digest_framing.clone(), + "output_bytes": {"const": 32}, + "source_queries": {"type": "array", "minItems": 2, "maxItems": 2, "items": digest_query.clone()} + }, + "additionalProperties": false + }, + "active_product_state_digest": { + "type": "object", + "required": ["algorithm", "domain_utf8", "domain_terminator", "framing", "output_bytes", "components", "exclusions", "component_queries"], + "properties": { + "algorithm": {"const": DIGEST_ALGORITHM}, + "domain_utf8": {"const": PRODUCT_DIGEST_DOMAIN_UTF8}, + "domain_terminator": {"const": DIGEST_DOMAIN_TERMINATOR}, + "framing": digest_framing, + "output_bytes": {"const": 32}, + "components": string_array.clone(), + "exclusions": string_array.clone(), + "component_queries": {"type": "array", "minItems": 13, "maxItems": 13, "items": digest_query.clone()} + }, + "additionalProperties": false + }, + "visibility_oracle": {"const": VISIBILITY_ORACLE}, + "scoped_integrity_mode": {"const": SCOPED_INTEGRITY_MODE}, + "scoped_integrity_tables": string_array.clone(), + "sqlite_sequence_scope": {"const": SQLITE_SEQUENCE_SCOPE}, + "stages": string_array.clone(), + "failpoints": string_array.clone(), + "preserved_authorities": string_array.clone() + }, + "additionalProperties": false + }, + "entry_points": { + "type": "array", + "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", + "items": { + "type": "object", + "required": ["role", "path", "byte_length", "sha256", "hash_algorithm"], + "properties": { + "role": {"type": "string", "minLength": 1}, + "path": {"type": "string", "pattern": path_pattern}, + "byte_length": {"type": "integer", "minimum": 1}, + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "hash_algorithm": {"const": HASH_ALGORITHM} + }, + "additionalProperties": false + } + }, + "public_api": { + "type": "object", + "required": ["added_symbols", "methods", "error_variants", "drift_kinds"], + "properties": { + "added_symbols": string_array.clone(), + "methods": string_array.clone(), + "error_variants": string_array.clone(), + "drift_kinds": { + "type": "array", + "minItems": 6, + "maxItems": 6, + "items": { + "type": "object", + "required": ["variant", "code"], + "properties": { + "variant": {"type": "string", "minLength": 1}, + "code": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "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": {"type": "string", "pattern": path_pattern}, + "mirror_path": {"type": "string", "pattern": path_pattern}, + "byte_length": {"type": "integer", "minimum": 1}, + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "hash_algorithm": {"const": HASH_ALGORITHM}, + "executor_id": {"type": "string", "minLength": 1}, + "executor_path": {"type": "string", "pattern": path_pattern}, + "executor_test": {"type": "string", "minLength": 1}, + "executor_byte_length": {"type": "integer", "minimum": 1}, + "executor_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "executor_hash_algorithm": {"const": HASH_ALGORITHM} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }) +} + +#[derive(Clone, Debug)] +struct PublicUseRoute { + segments: Vec<String>, + exported_name: String, + renamed: bool, + glob: bool, + absolute: bool, + attributes: Vec<String>, +} + +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<_>>(); + flatten_public_use_tree( + &item_use.tree, + &mut Vec::new(), + item_use.leading_colon.is_some(), + &attributes, + &mut routes, + ); + } + routes +} + +fn flatten_public_use_tree( + tree: &UseTree, + prefix: &mut Vec<String>, + absolute: bool, + attributes: &[String], + routes: &mut Vec<PublicUseRoute>, +) { + match tree { + UseTree::Path(path) => { + prefix.push(path.ident.to_string()); + flatten_public_use_tree(&path.tree, prefix, absolute, attributes, routes); + prefix.pop(); + } + UseTree::Name(name) => { + let mut segments = prefix.clone(); + segments.push(name.ident.to_string()); + routes.push(PublicUseRoute { + exported_name: name.ident.to_string(), + segments, + renamed: false, + glob: false, + absolute, + attributes: attributes.to_vec(), + }); + } + UseTree::Rename(rename) => { + let mut segments = prefix.clone(); + segments.push(rename.ident.to_string()); + routes.push(PublicUseRoute { + exported_name: rename.rename.to_string(), + segments, + renamed: true, + glob: false, + absolute, + attributes: attributes.to_vec(), + }); + } + UseTree::Glob(_) => routes.push(PublicUseRoute { + exported_name: "*".to_owned(), + segments: prefix.clone(), + renamed: false, + glob: true, + absolute, + attributes: attributes.to_vec(), + }), + UseTree::Group(group) => { + for item in &group.items { + flatten_public_use_tree(item, prefix, absolute, attributes, routes); + } + } + } +} + +fn exact_struct<'a>(file: &'a syn::File, name: &str) -> Result<&'a syn::ItemStruct, String> { + let matches = file + .items + .iter() + .filter_map(|item| match item { + Item::Struct(item) if item.ident == name => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + let [item] = matches.as_slice() else { + return Err(format!( + "governed Rust source must define struct `{name}` exactly once; found {}", + matches.len() + )); + }; + Ok(item) +} + +fn exact_enum<'a>(file: &'a syn::File, name: &str) -> Result<&'a syn::ItemEnum, String> { + let matches = file + .items + .iter() + .filter_map(|item| match item { + Item::Enum(item) if item.ident == name => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + let [item] = matches.as_slice() else { + return Err(format!( + "governed Rust source must define enum `{name}` exactly once; found {}", + matches.len() + )); + }; + Ok(item) +} + +fn exact_byte_string_const(file: &syn::File, name: &str) -> Result<Vec<u8>, String> { + let matches = file + .items + .iter() + .filter_map(|item| match item { + Item::Const(item) if item.ident == name => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + let [item] = matches.as_slice() else { + return Err(format!( + "governed Rust source must define byte-string const `{name}` exactly once; found {}", + matches.len() + )); + }; + let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::ByteStr(value), + .. + }) = item.expr.as_ref() + else { + return Err(format!("`{name}` must be one literal byte string")); + }; + Ok(value.value()) +} + +fn exact_string_const(file: &syn::File, name: &str) -> Result<String, String> { + let matches = file + .items + .iter() + .filter_map(|item| match item { + Item::Const(item) if item.ident == name => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + let [item] = matches.as_slice() else { + return Err(format!( + "governed Rust source must define string const `{name}` exactly once; found {}", + matches.len() + )); + }; + let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(value), + .. + }) = item.expr.as_ref() + else { + return Err(format!("`{name}` must be one literal string")); + }; + Ok(value.value()) +} + +fn exact_string_slice_const(file: &syn::File, name: &str) -> Result<Vec<String>, String> { + let matches = file + .items + .iter() + .filter_map(|item| match item { + Item::Const(item) if item.ident == name => Some(item), + _ => None, + }) + .collect::<Vec<_>>(); + let [item] = matches.as_slice() else { + return Err(format!( + "governed Rust source must define string-slice const `{name}` exactly once; found {}", + matches.len() + )); + }; + let syn::Expr::Reference(reference) = item.expr.as_ref() else { + return Err(format!("`{name}` must be a reference to one literal array")); + }; + let syn::Expr::Array(array) = reference.expr.as_ref() else { + return Err(format!("`{name}` must be a reference to one literal array")); + }; + array + .elems + .iter() + .map(|element| match element { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(value), + .. + }) => Ok(value.value()), + _ => Err(format!("`{name}` must contain only literal strings")), + }) + .collect() +} + +fn sqlx_query_literals(function: &syn::ItemFn) -> Vec<String> { + struct Collector(Vec<String>); + impl<'ast> syn::visit::Visit<'ast> for Collector { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + if compact_tokens(call.func.as_ref()) == "sqlx::query" + && let Some(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(value), + .. + })) = call.args.first() + { + self.0.push(value.value()); + } + syn::visit::visit_expr_call(self, call); + } + } + use syn::visit::Visit; + let mut collector = Collector(Vec::new()); + collector.visit_block(&function.block); + collector.0 +} + +fn sqlx_query_family_literals(function: &syn::ItemFn) -> Vec<String> { + struct Collector(Vec<String>); + impl<'ast> syn::visit::Visit<'ast> for Collector { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + let function = compact_tokens(call.func.as_ref()); + if matches!(function.as_str(), "sqlx::query" | "sqlx::query_scalar") + && let Some(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(value), + .. + })) = call.args.first() + { + self.0.push(value.value()); + } + syn::visit::visit_expr_call(self, call); + } + } + use syn::visit::Visit; + let mut collector = Collector(Vec::new()); + collector.visit_block(&function.block); + collector.0 +} + +fn sqlx_query_literals_in_expr(expression: &syn::Expr) -> Vec<String> { + struct Collector(Vec<String>); + impl<'ast> syn::visit::Visit<'ast> for Collector { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + if compact_tokens(call.func.as_ref()) == "sqlx::query" + && let Some(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(value), + .. + })) = call.args.first() + { + self.0.push(value.value()); + } + syn::visit::visit_expr_call(self, call); + } + } + use syn::visit::Visit; + let mut collector = Collector(Vec::new()); + collector.visit_expr(expression); + collector.0 +} + +fn digest_section_literals(function: &syn::ItemFn) -> Vec<String> { + struct Collector(Vec<String>); + impl<'ast> syn::visit::Visit<'ast> for Collector { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + if compact_tokens(call.func.as_ref()) == "digest_section" + && let Some(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::ByteStr(value), + .. + })) = call.args.iter().nth(1) + { + self.0 + .push(String::from_utf8_lossy(&value.value()).into_owned()); + } + syn::visit::visit_expr_call(self, call); + } + } + use syn::visit::Visit; + let mut collector = Collector(Vec::new()); + collector.visit_block(&function.block); + collector.0 +} + +fn digest_field_witnesses(function: &syn::ItemFn) -> Result<Vec<(String, String)>, String> { + struct Collector { + fields: Vec<(String, String)>, + } + impl<'ast> syn::visit::Visit<'ast> for Collector { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + let function_name = compact_tokens(call.func.as_ref()); + if let Some(framing) = digest_field_call_framing(&function_name) { + if let Some(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(field), + .. + })) = call.args.last() + { + self.fields.push((field.value(), framing.to_owned())); + } + } else if let Some(framing) = direct_digest_call_framing(&function_name) + && let Some(value) = call.args.iter().nth(1) + { + let literals = expression_string_literals(value); + if let [field] = literals.as_slice() { + self.fields.push((field.clone(), framing.to_owned())); + } + } + syn::visit::visit_expr_call(self, call); + } + + fn visit_expr_for_loop(&mut self, expression: &'ast syn::ExprForLoop) { + let syn::Pat::Ident(binding) = expression.pat.as_ref() else { + syn::visit::visit_expr_for_loop(self, expression); + return; + }; + let syn::Expr::Array(array) = expression.expr.as_ref() else { + syn::visit::visit_expr_for_loop(self, expression); + return; + }; + let fields = array + .elems + .iter() + .filter_map(|element| match element { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(value), + .. + }) => Some(value.value()), + _ => None, + }) + .collect::<Vec<_>>(); + if fields.len() == array.elems.len() + && let Some(framing) = + loop_digest_field_framing(&expression.body, binding.ident.to_string().as_str()) + { + self.fields + .extend(fields.into_iter().map(|field| (field, framing.to_owned()))); + } + syn::visit::visit_expr_for_loop(self, expression); + } + } + use syn::visit::Visit; + let mut collector = Collector { fields: Vec::new() }; + collector.visit_block(&function.block); + Ok(collector.fields) +} + +fn digest_field_call_framing(function_name: &str) -> Option<&'static str> { + match function_name { + "digest_i64_field" => Some("i64"), + "digest_optional_i64_field" => Some("optional_i64"), + "digest_text_field" => Some("text"), + "digest_optional_text_field" => Some("optional_text"), + "digest_bool_field" => Some("boolean"), + "digest_blob_field" => Some("blob"), + _ => None, + } +} + +fn direct_digest_call_framing(function_name: &str) -> Option<&'static str> { + match function_name { + "digest_i64" => Some("i64"), + "digest_text" => Some("text"), + "digest_optional_text" => Some("optional_text"), + _ => None, + } +} + +fn expression_string_literals(expression: &syn::Expr) -> Vec<String> { + struct Collector(Vec<String>); + impl<'ast> syn::visit::Visit<'ast> for Collector { + fn visit_lit_str(&mut self, literal: &'ast syn::LitStr) { + self.0.push(literal.value()); + } + } + use syn::visit::Visit; + let mut collector = Collector(Vec::new()); + collector.visit_expr(expression); + collector.0 +} + +fn loop_digest_field_framing(block: &syn::Block, binding: &str) -> Option<&'static str> { + struct Collector<'a> { + binding: &'a str, + framings: Vec<&'static str>, + } + impl<'ast> syn::visit::Visit<'ast> for Collector<'_> { + fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) { + let function_name = compact_tokens(call.func.as_ref()); + if let Some(framing) = digest_field_call_framing(&function_name) + && call + .args + .last() + .is_some_and(|argument| compact_tokens(argument) == self.binding) + { + self.framings.push(framing); + } + syn::visit::visit_expr_call(self, call); + } + } + use syn::visit::Visit; + let mut collector = Collector { + binding, + framings: Vec::new(), + }; + collector.visit_block(block); + match collector.framings.as_slice() { + [framing] => Some(*framing), + _ => None, + } +} + +fn exact_impl<'a>(file: &'a syn::File, name: &str) -> Result<&'a syn::ItemImpl, String> { + let matches = file + .items + .iter() + .filter_map(|item| match item { + Item::Impl(item) + if item.trait_.is_none() && compact_tokens(item.self_ty.as_ref()) == name => + { + Some(item) + } + _ => None, + }) + .collect::<Vec<_>>(); + let [item] = matches.as_slice() else { + return Err(format!( + "governed Rust source must define one inherent impl for `{name}`; found {}", + matches.len() + )); + }; + Ok(item) +} + +fn exact_associated_method<'a>( + file: &'a syn::File, + owner: &str, + method: &str, +) -> Result<&'a syn::ImplItemFn, String> { + let mut matches = Vec::new(); + for item in &file.items { + let Item::Impl(item) = item else { + continue; + }; + if compact_tokens(item.self_ty.as_ref()) != owner { + continue; + } + for member in &item.items { + if let syn::ImplItem::Fn(function) = member + && function.sig.ident == method + { + matches.push(function); + } + } + } + let [function] = matches.as_slice() else { + return Err(format!( + "{owner} must define `{method}` exactly once; found {}", + matches.len() + )); + }; + Ok(function) +} + +struct FreeFunctionCollector<'name, 'ast> { + name: &'name str, + matches: Vec<&'ast syn::ItemFn>, +} + +impl<'ast> syn::visit::Visit<'ast> for FreeFunctionCollector<'_, 'ast> { + fn visit_item_fn(&mut self, function: &'ast syn::ItemFn) { + if function.sig.ident == self.name { + self.matches.push(function); + } + syn::visit::visit_item_fn(self, function); + } +} + +fn exact_free_function<'a>(file: &'a syn::File, name: &str) -> Result<&'a syn::ItemFn, String> { + use syn::visit::Visit; + let mut collector = FreeFunctionCollector { + name, + matches: Vec::new(), + }; + collector.visit_file(file); + let [function] = collector.matches.as_slice() else { + return Err(format!( + "governed Rust source must define free function `{name}` exactly once; found {}", + collector.matches.len() + )); + }; + Ok(function) +} + +fn exact_top_level_function<'a>( + file: &'a syn::File, + name: &str, +) -> Result<&'a syn::ItemFn, String> { + let matches = file + .items + .iter() + .filter_map(|item| match item { + Item::Fn(function) if function.sig.ident == name => Some(function), + _ => None, + }) + .collect::<Vec<_>>(); + let [function] = matches.as_slice() else { + return Err(format!( + "governed Rust source must define top-level function `{name}` exactly once; found {}", + matches.len() + )); + }; + Ok(function) +} + +fn compact_signature(source: &str) -> Result<String, String> { + let function = syn::parse_str::<syn::ImplItemFn>(&format!("{source} {{ unreachable!() }}")) + .map_err(|error| format!("parse authoritative signature `{source}`: {error}"))?; + Ok(compact_tokens(&function.sig)) +} + +fn strip_doc_attributes(attributes: &mut Vec<syn::Attribute>) { + attributes.retain(|attribute| !attribute.path().is_ident("doc")); +} + +fn descriptor_for_file(workspace_root: &Path, relative: &str) -> Result<FileDescriptor, String> { + descriptor_for_bytes(relative, &read_regular_file(workspace_root, relative)?) +} + +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 validate_file_descriptor( + path: &str, + byte_length: u64, + sha256: &str, + hash_algorithm: &str, +) -> Result<(), String> { + if byte_length == 0 || hash_algorithm != HASH_ALGORITHM { + return Err(format!("file descriptor `{path}` is invalid")); + } + validate_sha256(path, sha256) +} + +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 rust_file(workspace_root: &Path, relative: &str) -> Result<syn::File, String> { + let source = rust_source(workspace_root, relative)?; + syn::parse_file(&source).map_err(|error| format!("parse {relative}: {error}")) +} + +fn rust_source(workspace_root: &Path, relative: &str) -> Result<String, String> { + let bytes = read_regular_file(workspace_root, relative)?; + std::str::from_utf8(&bytes) + .map(str::to_owned) + .map_err(|error| format!("{relative} must be UTF-8 Rust: {error}")) +} + +fn compact_tokens(tokens: &impl ToTokens) -> String { + tokens.to_token_stream().to_string().replace(' ', "") +} + +fn owned(values: &[&str]) -> Vec<String> { + values.iter().map(|value| (*value).to_owned()).collect() +} + +fn validate_unique<'a>( + label: &str, + values: impl IntoIterator<Item = &'a str>, +) -> Result<(), String> { + let values = values.into_iter().collect::<Vec<_>>(); + let unique = values.iter().copied().collect::<BTreeSet<_>>(); + if unique.len() != values.len() { + return Err(format!("{label} must contain no duplicate values")); + } + Ok(()) +} + +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 canonical JSON: {error}"))?; + bytes.push(b'\n'); + Ok(bytes) +} + +fn validate_canonical_json<T: Serialize>( + relative: &str, + bytes: &[u8], + value: &T, +) -> Result<(), String> { + let expected = canonical_json_bytes(value)?; + if bytes != expected { + return Err(format!( + "{relative} must use canonical pretty JSON with one trailing newline" + )); + } + Ok(()) +} + +fn validate_json_schema(schema: &Value, manifest: &Value) -> Result<(), String> { + let validator = jsonschema::validator_for(schema) + .map_err(|error| format!("compile {MANIFEST_SCHEMA_RELATIVE}: {error}"))?; + let errors = validator + .iter_errors(manifest) + .map(|error| error.to_string()) + .collect::<Vec<_>>(); + if errors.is_empty() { + Ok(()) + } else { + Err(format!( + "{MANIFEST_RELATIVE} violates {MANIFEST_SCHEMA_RELATIVE}: {}", + errors.join("; ") + )) + } +} + +fn validate_digest_sidecar(relative: &str, bytes: &[u8]) -> Result<(), String> { + let value = + std::str::from_utf8(bytes).map_err(|error| format!("{relative} must be UTF-8: {error}"))?; + let Some(digest) = value.strip_suffix('\n') else { + return Err(format!("{relative} must end with one newline")); + }; + if digest.contains('\n') { + return Err(format!("{relative} must contain one digest line")); + } + validate_sha256(relative, digest) +} + +fn validate_sha256(label: &str, value: &str) -> Result<(), String> { + if value.len() != 64 + || !value + .as_bytes() + .iter() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)) + { + return Err(format!("{label} must be canonical lowercase SHA-256 hex")); + } + Ok(()) +} + +fn validate_vector_expected_digest(value: &str) -> Result<(), String> { + validate_sha256("vector expected digest", value)?; + if value.as_bytes().iter().all(|byte| *byte == b'0') { + return Err("vector expected digest must not be an all-zero bootstrap value".to_owned()); + } + Ok(()) +} + +fn sha256_hex(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn workspace_root() -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("xtask lives under tools in the workspace") + .to_path_buf() + } + + #[test] + fn source_inventory_is_unique_complete_and_excludes_generated_outputs() { + validate_source_inventory().expect("raw-source rebuild source inventory"); + let mut missing_compiler_input = SOURCE_SPECS.to_vec(); + missing_compiler_input.retain(|spec| spec.path != FLAKE_LOCK_RELATIVE); + let error = validate_source_inventory_specs(&missing_compiler_input) + .expect_err("delegated compiler input omission must fail closed"); + assert!(error.contains("nix_input_lock_authority"), "{error}"); + let root = workspace_root(); + validate_complete_event_store_source_closure(&root) + .expect("complete event-store Rust source closure"); + validate_successor_compiler_input_authority(&root) + .expect("complete event-store compiler-input authority"); + validate_delegated_compiler_source_pins(&root).expect("delegated compiler source pins"); + validate_xtask_manifest_authority(&root).expect("xtask compiler authority"); + } + + #[test] + fn delegated_compiler_sources_and_xtask_targets_fail_closed() { + let root = workspace_root(); + let pinned_workspace = tempfile::tempdir().expect("compiler pin workspace"); + for (relative, _) in DELEGATED_COMPILER_SOURCE_PINS { + let destination = pinned_workspace.path().join(relative); + fs::create_dir_all(destination.parent().expect("compiler pin parent")) + .expect("create compiler pin parent"); + fs::copy(root.join(relative), destination).expect("copy compiler pin source"); + } + validate_delegated_compiler_source_pins(pinned_workspace.path()) + .expect("current compiler source pins"); + fs::write(pinned_workspace.path().join(FLAKE_LOCK_RELATIVE), "{}\n") + .expect("mutate pinned flake lock"); + validate_delegated_compiler_source_pins(pinned_workspace.path()) + .expect_err("compiler source mutation must fail closed"); + + let manifest_workspace = tempfile::tempdir().expect("xtask manifest workspace"); + let manifest_path = manifest_workspace.path().join(XTASK_MANIFEST_RELATIVE); + fs::create_dir_all(manifest_path.parent().expect("xtask manifest parent")) + .expect("create xtask manifest parent"); + fs::copy(root.join(XTASK_MANIFEST_RELATIVE), &manifest_path).expect("copy xtask manifest"); + validate_xtask_manifest_authority(manifest_workspace.path()) + .expect("current xtask manifest authority"); + fs::write( + manifest_workspace.path().join("tools/xtask/build.rs"), + "fn main() {}\n", + ) + .expect("write injected build script"); + validate_xtask_manifest_authority(manifest_workspace.path()) + .expect_err("xtask build-script injection must fail closed"); + } + + #[test] + fn xtask_auto_target_flags_reject_omission_and_retargeting() { + let root = workspace_root(); + let manifest = + regular_utf8_source(&root, XTASK_MANIFEST_RELATIVE).expect("current xtask manifest"); + + for flag in XTASK_REQUIRED_DISABLED_AUTO_TARGET_FLAGS { + let assignment = format!("{flag} = false\n"); + let omitted = manifest.replacen(&assignment, "", 1); + assert_ne!(omitted, manifest, "{flag} omission fixture must mutate"); + let retargeted = manifest.replacen(&assignment, &format!("{flag} = true\n"), 1); + assert_ne!(retargeted, manifest, "{flag} retarget fixture must mutate"); + + for (mutation, label) in [(omitted, "omission"), (retargeted, "retarget")] { + let workspace = tempfile::tempdir().expect("xtask flag fixture workspace"); + let path = workspace.path().join(XTASK_MANIFEST_RELATIVE); + fs::create_dir_all(path.parent().expect("xtask manifest fixture parent")) + .expect("create xtask manifest fixture parent"); + fs::write(&path, mutation).expect("write xtask manifest mutation"); + let error = validate_xtask_manifest_authority(workspace.path()) + .expect_err("xtask auto-target flag mutation must fail closed"); + assert!( + error.contains(flag), + "{flag} {label} error must identify the exact flag: {error}" + ); + } + } + } + + #[test] + fn xtask_auto_target_source_paths_are_forbidden() { + let root = workspace_root(); + for (forbidden, injected) in [ + ("tools/xtask/build.rs", "tools/xtask/build.rs"), + ("tools/xtask/src/lib.rs", "tools/xtask/src/lib.rs"), + ("tools/xtask/src/bin.rs", "tools/xtask/src/bin.rs"), + ("tools/xtask/src/bin", "tools/xtask/src/bin/injected.rs"), + ("tools/xtask/tests", "tools/xtask/tests/injected.rs"), + ("tools/xtask/examples", "tools/xtask/examples/injected.rs"), + ("tools/xtask/benches", "tools/xtask/benches/injected.rs"), + ] { + let workspace = tempfile::tempdir().expect("xtask path fixture workspace"); + let manifest_path = workspace.path().join(XTASK_MANIFEST_RELATIVE); + fs::create_dir_all( + manifest_path + .parent() + .expect("xtask manifest fixture parent"), + ) + .expect("create xtask manifest fixture parent"); + fs::copy(root.join(XTASK_MANIFEST_RELATIVE), &manifest_path) + .expect("copy xtask manifest fixture"); + + let injected_path = workspace.path().join(injected); + fs::create_dir_all(injected_path.parent().expect("injected auto-target parent")) + .expect("create injected auto-target parent"); + fs::write(&injected_path, "fn main() {}\n").expect("write injected auto-target source"); + + let error = validate_xtask_manifest_authority(workspace.path()) + .expect_err("xtask auto-target source path must fail closed"); + assert!( + error.contains(forbidden), + "auto-target error must identify `{forbidden}`: {error}" + ); + } + } + + #[test] + fn rebuild_marker_token_is_non_cloneable_and_consumed_by_both_routes() { + let root = workspace_root(); + let reconciliation_relative = "crates/event_store/src/nip09/reconciliation_v1.rs"; + let reconciliation = + rust_source(&root, reconciliation_relative).expect("reconciliation marker authority"); + let rebuild = rust_source(&root, REBUILD_RUNTIME_SOURCE_RELATIVE) + .expect("raw rebuild marker authority"); + let reconciliation_file = + syn::parse_file(&reconciliation).expect("parse reconciliation marker authority"); + let rebuild_file = syn::parse_file(&rebuild).expect("parse raw rebuild marker authority"); + validate_rebuild_marker_token_authority( + &reconciliation_file, + reconciliation_relative, + &rebuild_file, + REBUILD_RUNTIME_SOURCE_RELATIVE, + ) + .expect("current rebuild marker token authority"); + + let cloneable = reconciliation.replacen( + "struct SourceRebuildMarkerTokenV1 {", + "#[derive(Clone)]\nstruct SourceRebuildMarkerTokenV1 {", + 1, + ); + assert_ne!(cloneable, reconciliation, "cloneable fixture must mutate"); + let cloneable = syn::parse_file(&cloneable).expect("parse cloneable marker fixture"); + validate_rebuild_marker_token_authority( + &cloneable, + reconciliation_relative, + &rebuild_file, + REBUILD_RUNTIME_SOURCE_RELATIVE, + ) + .expect_err("cloneable marker token must fail closed"); + + let non_consuming = rebuild.replacen( + "close_source_rebuild_marker(connection, marker).await?;", + "close_source_rebuild_marker(connection, marker.clone()).await?;", + 1, + ); + assert_ne!( + non_consuming, rebuild, + "non-consuming marker fixture must mutate" + ); + let non_consuming = + syn::parse_file(&non_consuming).expect("parse non-consuming marker fixture"); + validate_rebuild_marker_token_authority( + &reconciliation_file, + reconciliation_relative, + &non_consuming, + REBUILD_RUNTIME_SOURCE_RELATIVE, + ) + .expect_err("cloned marker consumption must fail closed"); + + let forged = rebuild.replacen( + " close_source_rebuild_marker(connection, marker).await?;", + " let marker = super::SourceRebuildMarkerTokenV1 { generation: plan.generation };\n close_source_rebuild_marker(connection, marker).await?;", + 1, + ); + assert_ne!(forged, rebuild, "forged marker fixture must mutate"); + let forged = syn::parse_file(&forged).expect("parse forged marker fixture"); + validate_rebuild_marker_token_authority( + &reconciliation_file, + reconciliation_relative, + &forged, + REBUILD_RUNTIME_SOURCE_RELATIVE, + ) + .expect_err("forged marker shadow must fail closed"); + } + + #[test] + fn compiler_input_authority_rejects_unapproved_inputs_and_path_retargeting() { + for source in [ + "#[path = \"escape.rs\"]\nmod escape;", + "#[cfg_attr(target_os = \"ios\", path = \"escape.rs\")]\nmod escape;", + ] { + let file = syn::parse_file(source).expect("path-retargeting probe parses"); + let error = validate_exact_successor_compiler_inputs("probe.rs", &file, &[]) + .expect_err("path retargeting must fail closed"); + assert!(error.contains("no path retargeting"), "{error}"); + } + + let file = syn::parse_file("const ESCAPE: &str = include_str!(\"escape.rs\");") + .expect("compiler-input probe parses"); + let error = validate_exact_successor_compiler_inputs("probe.rs", &file, &[]) + .expect_err("unapproved compiler input must fail closed"); + assert!(error.contains("include_str!"), "{error}"); + } + + #[test] + fn executable_authority_rejects_should_panic_and_extra_attributes() { + let workspace = tempfile::tempdir().expect("executable authority workspace"); + let relative = "probe.rs"; + fs::write( + workspace.path().join(relative), + "#[test]\n#[should_panic]\nfn governed_test() { panic!(\"bypass\"); }\n", + ) + .expect("write should-panic probe"); + let error = validate_executable_test(workspace.path(), relative, "governed_test") + .expect_err("should-panic authority must fail closed"); + assert!(error.contains("exactly one unconditional test attribute")); + } + + #[test] + fn command_reachability_rejects_string_literal_witnesses() { + let root = workspace_root(); + let workspace = tempfile::tempdir().expect("command authority workspace"); + for relative in [CONTRACT_COMMAND_SOURCE_RELATIVE, XTASK_MAIN_SOURCE_RELATIVE] { + let destination = workspace.path().join(relative); + fs::create_dir_all(destination.parent().expect("command source parent")) + .expect("create command source parent"); + fs::copy(root.join(relative), destination).expect("copy command source"); + } + let main_path = workspace.path().join(XTASK_MAIN_SOURCE_RELATIVE); + let main = fs::read_to_string(&main_path).expect("xtask main source"); + let bypass = main.replacen( + "[] => contract::validate_raw_source_rebuild_manifest(&workspace_root()),", + "[] => { let _ = \"contract::validate_raw_source_rebuild_manifest(&workspace_root())\"; Ok(()) },", + 1, + ); + assert_ne!(bypass, main, "command bypass fixture must mutate"); + fs::write(main_path, bypass).expect("write command bypass"); + let error = validate_command_reachability(workspace.path()) + .expect_err("string literal must not satisfy command reachability"); + assert!(error.contains("command arm drifted"), "{error}"); + } + + #[test] + fn delegated_suite_inventory_rejects_missing_and_unrepresented_authorities() { + let root = workspace_root(); + let bytes = read_regular_file(&root, RESULT_VECTOR_CANONICAL_RELATIVE) + .expect("raw-source rebuild vector"); + let vector: RawSourceRebuildVector = + serde_json::from_slice(&bytes).expect("typed raw-source rebuild vector"); + validate_delegated_suite_inventory(&vector).expect("current delegated suite inventory"); + + let mut missing = vector.clone(); + missing + .delegated_suite + .authorities + .pop() + .expect("nonempty suite"); + let error = validate_delegated_suite_inventory(&missing) + .expect_err("missing delegated authority must fail closed"); + assert!(error.contains("missing"), "{error}"); + + let mut unrepresented = vector.clone(); + unrepresented + .delegated_suite + .authorities + .push(DelegatedAuthority { + authority: "unrepresented_test_v1".to_owned(), + authority_path: REBUILD_FAILPOINT_TEST_SOURCE_RELATIVE.to_owned(), + }); + let error = validate_delegated_suite_inventory(&unrepresented) + .expect_err("unrepresented delegated authority must fail closed"); + assert!(error.contains("unrepresented"), "{error}"); + + let mut duplicate = vector.clone(); + duplicate + .delegated_suite + .authorities + .push(duplicate.delegated_suite.authorities[0].clone()); + let error = validate_delegated_suite_inventory(&duplicate) + .expect_err("duplicate delegated authority must fail closed"); + assert!(error.contains("duplicate"), "{error}"); + } + + #[test] + fn delegated_suite_contract_lane_rejects_filters_and_lexical_decoys() { + let root = workspace_root(); + let flake = regular_utf8_source(&root, FLAKE_SOURCE_RELATIVE).expect("flake authority"); + let apps = regular_utf8_source(&root, CONTRACT_APP_SOURCE_RELATIVE) + .expect("contract app authority"); + let common = regular_utf8_source(&root, CONTRACT_LANE_SOURCE_RELATIVE) + .expect("contract lane authority"); + let toolchains = regular_utf8_source(&root, TOOLCHAIN_ROUTING_SOURCE_RELATIVE) + .expect("toolchain routing authority"); + validate_delegated_suite_contract_lane_sources(&flake, &apps, &common, &toolchains) + .expect("current delegated suite contract lane"); + + let flake_bypass = flake + .replacen( + "apps = import ./build/nix/apps.nix {", + "apps = import ./build/nix/apps-bypass.nix {", + 1, + ) + .replacen( + "description = \"Radroots Core Libraries\";", + "description = \"apps = import ./build/nix/apps.nix {\";", + 1, + ); + assert_ne!(flake_bypass, flake, "flake bypass fixture must mutate"); + validate_delegated_suite_contract_lane_sources(&flake_bypass, &apps, &common, &toolchains) + .expect_err("string decoy must not satisfy flake app routing"); + + let mut common_import_bypass = flake + .replacen( + "common = import ./build/nix/common.nix {", + "common = import ./build/nix/common-bypass.nix {", + 1, + ) + .replacen( + "description = \"Radroots Core Libraries\";", + "description = \"common = import ./build/nix/common.nix {\";", + 1, + ); + common_import_bypass.push_str("\n# common = import ./build/nix/common.nix {\n"); + assert_ne!( + common_import_bypass, flake, + "common import bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &common_import_bypass, + &apps, + &common, + &toolchains, + ) + .expect_err("string and comment decoys must not satisfy flake common routing"); + + let common_argument_bypass = flake.replacen( + " inherit lib pkgs toolchains;", + " inherit lib pkgs;\n toolchains = import ./build/nix/toolchains-bypass.nix { inherit pkgs; };", + 1, + ); + assert_ne!( + common_argument_bypass, flake, + "common argument bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &common_argument_bypass, + &apps, + &common, + &toolchains, + ) + .expect_err("common toolchain argument substitution must fail closed"); + + let toolchain_import_bypass = flake + .replacen( + "toolchains = import ./build/nix/toolchains.nix {", + "toolchains = import ./build/nix/toolchains-bypass.nix {", + 1, + ) + .replacen( + "description = \"Radroots Core Libraries\";", + "description = \"toolchains = import ./build/nix/toolchains.nix {\";", + 1, + ); + assert_ne!( + toolchain_import_bypass, flake, + "toolchain import bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &toolchain_import_bypass, + &apps, + &common, + &toolchains, + ) + .expect_err("string decoy must not satisfy flake toolchain routing"); + + let toolchain_postfix_bypass = flake.replacen( + "toolchains = import ./build/nix/toolchains.nix { inherit pkgs; };", + "toolchains = import ./build/nix/toolchains.nix { inherit pkgs; } // { stable = null; };", + 1, + ); + assert_ne!( + toolchain_postfix_bypass, flake, + "toolchain postfix bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &toolchain_postfix_bypass, + &apps, + &common, + &toolchains, + ) + .expect_err("toolchain postfix override must fail closed"); + + let common_postfix_bypass = flake.replacen( + " inherit lib pkgs toolchains;\n };", + " inherit lib pkgs toolchains;\n } // { contractCommand = \"cargo test -q -p xtask\"; };", + 1, + ); + assert_ne!( + common_postfix_bypass, flake, + "common postfix bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &common_postfix_bypass, + &apps, + &common, + &toolchains, + ) + .expect_err("common postfix override must fail closed"); + + let apps_postfix_bypass = flake.replacen( + " ;\n };\n\n checks =", + " ;\n } // { contract = { type = \"app\"; program = \"/bin/false\"; }; };\n\n checks =", + 1, + ); + assert_ne!( + apps_postfix_bypass, flake, + "apps postfix bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &apps_postfix_bypass, + &apps, + &common, + &toolchains, + ) + .expect_err("apps postfix override must fail closed"); + + let apps_bypass = apps + .replacen( + "command = common.contractCommand;", + "command = \"cargo test -q -p xtask\";", + 1, + ) + .replacen( + "description = \"Run the core-library contract lane\";", + "description = \"command = common.contractCommand;\";", + 1, + ); + assert_ne!(apps_bypass, apps, "apps bypass fixture must mutate"); + validate_delegated_suite_contract_lane_sources(&flake, &apps_bypass, &common, &toolchains) + .expect_err("string decoy must not satisfy contract app command routing"); + + let contract_path_bypass = apps.replacen( + " command = common.contractCommand;\n };", + " command = common.contractCommand;\n pathPrefix = \"\";\n };", + 1, + ); + assert_ne!( + contract_path_bypass, apps, + "contract path bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &flake, + &contract_path_bypass, + &common, + &toolchains, + ) + .expect_err("contract path override must fail closed"); + + let package_bypass = common.replacen( + " \"radroots_event_store\"\n", + " # \"radroots_event_store\"\n", + 1, + ); + assert_ne!(package_bypass, common, "package bypass fixture must mutate"); + validate_delegated_suite_contract_lane_sources(&flake, &apps, &package_bypass, &toolchains) + .expect_err("commented package must not satisfy literal package inventory"); + + let cargo_source_bypass = common.replacen( + " ../../build/nix/toolchains.nix\n", + " # ../../build/nix/toolchains.nix\n", + 1, + ); + assert_ne!( + cargo_source_bypass, common, + "cargo source bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &flake, + &apps, + &cargo_source_bypass, + &toolchains, + ) + .expect_err("commented source path must not satisfy cargoSource closure"); + + let toolchain_bypass = toolchains.replacen( + "stable = pkgs.rust-bin.fromRustupToolchainFile ../../rust-toolchain.toml;", + "stable = pkgs.rust-bin.fromRustupToolchainFile ../../rust-toolchain-bypass.toml;", + 1, + ); + assert_ne!( + toolchain_bypass, toolchains, + "stable toolchain bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources(&flake, &apps, &common, &toolchain_bypass) + .expect_err("stable toolchain bypass must fail closed"); + + let command_bypass = common.replacen( + "cargo test -q ${coreContractCargoArgs}", + "cargo test -q -p xtask\n # cargo test -q ${coreContractCargoArgs}", + 1, + ); + assert_ne!( + command_bypass, common, + "contract command bypass fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources(&flake, &apps, &command_bypass, &toolchains) + .expect_err("shell-comment decoy must not satisfy unfiltered package tests"); + + let selector_bypass = common.replacen( + "radroots_nostr/events\";", + "radroots_nostr/events --lib\";", + 1, + ); + assert_ne!( + selector_bypass, common, + "cargo selector fixture must mutate" + ); + validate_delegated_suite_contract_lane_sources( + &flake, + &apps, + &selector_bypass, + &toolchains, + ) + .expect_err("Cargo target selector must not skip the integration executor"); + } + + #[test] + fn delegated_suite_flake_lock_rejects_unlocked_or_redirected_direct_inputs() { + let root = workspace_root(); + validate_flake_lock_authority(&root).expect("current flake lock authority"); + let workspace = tempfile::tempdir().expect("flake lock test workspace"); + let lock_path = workspace.path().join(FLAKE_LOCK_RELATIVE); + let lock = regular_utf8_source(&root, FLAKE_LOCK_RELATIVE).expect("flake lock source"); + + fs::write( + &lock_path, + lock.replacen("\"crane\": \"crane\"", "\"crane\": \"nixpkgs\"", 1), + ) + .expect("write redirected flake lock"); + validate_flake_lock_authority(workspace.path()) + .expect_err("redirected direct input must fail closed"); + + fs::write( + &lock_path, + lock.replacen("\"narHash\":", "\"untrustedNarHash\":", 1), + ) + .expect("write unlocked flake lock"); + validate_flake_lock_authority(workspace.path()) + .expect_err("missing direct-input narHash must fail closed"); + } + + #[test] + fn delegated_suite_test_targets_reject_disabled_or_detached_tests() { + let root = workspace_root(); + let workspace = tempfile::tempdir().expect("delegated test-target workspace"); + for relative in [ + "crates/event_store/Cargo.toml", + "crates/event_store/src/store.rs", + "crates/event_store/src/nip09/reconciliation_v1.rs", + ] { + let destination = workspace.path().join(relative); + fs::create_dir_all(destination.parent().expect("test-target source parent")) + .expect("create test-target source parent"); + fs::copy(root.join(relative), destination).expect("copy test-target source"); + } + validate_delegated_suite_test_targets(workspace.path()) + .expect("current delegated suite test targets"); + + let cargo_path = workspace.path().join("crates/event_store/Cargo.toml"); + let cargo = fs::read_to_string(&cargo_path).expect("event-store Cargo manifest"); + fs::write(&cargo_path, format!("{cargo}\n[lib]\ntest = false\n")) + .expect("disable library tests"); + validate_delegated_suite_test_targets(workspace.path()) + .expect_err("disabled delegated library tests must fail closed"); + fs::write(&cargo_path, cargo).expect("restore Cargo manifest"); + + let store_path = workspace.path().join("crates/event_store/src/store.rs"); + let store = fs::read_to_string(&store_path).expect("event-store source"); + let detached = store.replacen( + "#[cfg(test)]\nmod raw_source_rebuild_v1_tests;", + "#[cfg(any())]\nmod raw_source_rebuild_v1_tests;", + 1, + ); + assert_ne!(detached, store, "detached test module fixture must mutate"); + fs::write(store_path, detached).expect("detach delegated test module"); + validate_delegated_suite_test_targets(workspace.path()) + .expect_err("detached delegated test module must fail closed"); + } + + #[test] + fn drift_taxonomy_rejects_variant_and_code_retargeting() { + let root = workspace_root(); + let source = rust_source(&root, "crates/event_store/src/error.rs") + .expect("event-store error source"); + let baseline = syn::parse_file(&source).expect("event-store error AST"); + validate_raw_source_rebuild_drift_taxonomy(&baseline) + .expect("current raw-source rebuild drift taxonomy"); + + for mutation in [ + source.replacen( + " RebuildPostcondition,", + " RebuildPostconditionRetargeted,", + 1, + ), + source.replacen( + "\"source_generation_lineage\"", + "\"addressable_transition_authority\"", + 1, + ), + ] { + assert_ne!(mutation, source, "taxonomy fixture must mutate"); + let file = syn::parse_file(&mutation).expect("mutated taxonomy AST"); + validate_raw_source_rebuild_drift_taxonomy(&file) + .expect_err("taxonomy mutation must fail closed"); + } + } + + #[test] + fn failpoint_authority_rejects_mapping_retargeting_and_injection_drift() { + let root = workspace_root(); + let relative = REBUILD_RUNTIME_SOURCE_RELATIVE; + let source = rust_source(&root, relative).expect("raw-source rebuild runtime"); + let baseline = syn::parse_file(&source).expect("raw-source rebuild AST"); + validate_failpoint_authority(&baseline, relative).expect("current failpoint authority"); + validate_coordinator_authority(&baseline, relative).expect("current coordinator authority"); + + let retargeted = source.replacen( + "Self::AfterFoodAudit => \"after_food_audit\"", + "Self::AfterFoodAudit => \"after_food_reset_replay\"", + 1, + ); + assert_ne!(retargeted, source, "retargeted fixture must mutate"); + let retargeted = syn::parse_file(&retargeted).expect("retargeted failpoint AST"); + validate_failpoint_authority(&retargeted, relative) + .expect_err("retargeted failpoint stage must fail closed"); + + let extra = source.replacen( + "append_source_generation(connection, &plan).await?;", + "inject_raw_source_rebuild_failpoint_v1(\n _failpoint,\n RawSourceRebuildFailpointV1::AfterMarkerOpen,\n )?;\n append_source_generation(connection, &plan).await?;", + 1, + ); + assert_ne!(extra, source, "extra-injection fixture must mutate"); + let extra = syn::parse_file(&extra).expect("extra-injection AST"); + validate_failpoint_authority(&extra, relative) + .expect_err("extra rollback injection must fail closed"); + + let omitted = source.replacen( + " #[cfg(test)]\n inject_raw_source_rebuild_failpoint_v1(\n _failpoint,\n RawSourceRebuildFailpointV1::AfterFoodAudit,\n )?;\n", + "", + 1, + ); + assert_ne!(omitted, source, "omitted-injection fixture must mutate"); + let omitted = syn::parse_file(&omitted).expect("omitted-injection AST"); + validate_failpoint_authority(&omitted, relative) + .expect_err("omitted rollback injection must fail closed"); + } + + #[test] + fn failpoint_test_array_rejects_member_omission() { + let root = workspace_root(); + let source = rust_source(&root, REBUILD_FAILPOINT_TEST_SOURCE_RELATIVE) + .expect("raw-source rebuild failpoint tests"); + let baseline = syn::parse_file(&source).expect("raw-source rebuild failpoint test AST"); + validate_failpoint_test_array_authority(&baseline, REBUILD_FAILPOINT_TEST_SOURCE_RELATIVE) + .expect("current failpoint test array authority"); + + let omitted = source.replacen( + " RawSourceRebuildFailpointV1::AfterVisibilityAudit,\n", + "", + 1, + ); + assert_ne!(omitted, source, "test-array omission fixture must mutate"); + let omitted = syn::parse_file(&omitted).expect("omitted failpoint test array AST"); + validate_failpoint_test_array_authority(&omitted, REBUILD_FAILPOINT_TEST_SOURCE_RELATIVE) + .expect_err("omitted failpoint test array member must fail closed"); + } + + #[test] + fn digest_streaming_authority_rejects_duplicate_and_missing_row_markers() { + let root = workspace_root(); + let relative = REBUILD_RUNTIME_SOURCE_RELATIVE; + let source = rust_source(&root, relative).expect("raw-source rebuild runtime"); + for mutation in [ + source.replacen( + " digest_row_start(&mut digest);", + " digest_row_start(&mut digest);\n digest_row_start(&mut digest);", + 1, + ), + source.replacen(" digest_row_start(&mut digest);\n", "", 1), + ] { + assert_ne!(mutation, source, "row-marker fixture must mutate"); + let file = syn::parse_file(&mutation).expect("mutated digest runtime AST"); + let function = exact_free_function(&file, "immutable_raw_digest_v1") + .expect("immutable raw digest authority"); + let error = validate_digest_streaming_authority( + function, + relative, + "immutable_raw_digest_v1", + RAW_DIGEST_QUERY_SPECS, + ) + .expect_err("row-marker framing mutation must fail closed"); + assert!(error.contains("exactly one top-level"), "{error}"); + } + } + + #[test] + fn migration_inventory_remains_runtime_only_v4() { + validate_migration_inventory(&workspace_root()).expect("exact migration inventory"); + } + + #[test] + fn source_maintenance_predecessor_identity_is_frozen() { + let root = workspace_root(); + let bytes = read_regular_file(&root, PREDECESSOR_MANIFEST_RELATIVE) + .expect("SourceMaintenance predecessor manifest"); + validate_predecessor_identity(&bytes).expect("frozen predecessor identity"); + validate_predecessor_source_supersession(&root, &bytes) + .expect("changed predecessor sources are superseded"); + } + + #[test] + fn generated_bundle_render_is_deterministic() { + let root = workspace_root(); + let first = expected_artifacts(&root) + .expect("first render") + .into_iter() + .map(|artifact| (artifact.relative, artifact.contents)) + .collect::<Vec<_>>(); + let second = expected_artifacts(&root) + .expect("second render") + .into_iter() + .map(|artifact| (artifact.relative, artifact.contents)) + .collect::<Vec<_>>(); + assert_eq!(first, second); + } + + #[test] + fn direct_vector_digest_rejects_bootstrap_placeholder() { + let error = validate_vector_expected_digest(&"0".repeat(64)) + .expect_err("all-zero bootstrap digest must fail closed"); + assert!(error.contains("bootstrap"), "{error}"); + } + + #[test] + fn executable_vector_case_inventory_is_frozen() { + let root = workspace_root(); + let bytes = read_regular_file(&root, RESULT_VECTOR_CANONICAL_RELATIVE) + .expect("raw-source rebuild vector"); + validate_result_vector_identity(&bytes).expect("immutable vector identity"); + + let mut reduced = bytes; + reduced.pop(); + let error = validate_result_vector_identity(&reduced) + .expect_err("reduced vector inventory must fail closed"); + assert!( + error.contains("immutable executable case inventory"), + "{error}" + ); + } + + #[test] + fn rollback_vector_requires_exact_failpoint_case_ids() { + let root = workspace_root(); + let bytes = read_regular_file(&root, RESULT_VECTOR_CANONICAL_RELATIVE) + .expect("raw-source rebuild vector"); + let mut vector = serde_json::from_slice::<RawSourceRebuildVector>(&bytes) + .expect("raw-source rebuild vector schema"); + validate_failpoint_result_vector_cases(&vector) + .expect("current rollback failpoint vector cases"); + + let case = vector + .cases + .iter_mut() + .find(|case| case.id == "rollback_after_marker_open") + .expect("marker-open rollback case"); + case.id = "rollback_after_marker_open_retargeted".to_owned(); + validate_failpoint_result_vector_cases(&vector) + .expect_err("retargeted rollback failpoint case ID must fail closed"); + } + + #[test] + fn public_error_runtime_and_command_authorities_are_active() { + let root = workspace_root(); + validate_public_api_authority(&root).expect("public API authority"); + validate_error_authority(&root).expect("typed error authority"); + validate_runtime_authority(&root).expect("runtime authority"); + validate_command_reachability(&root).expect("command and release reachability"); + } + + #[test] + fn caller_schema_dependency_authority_rejects_limits_narrowing_and_bypass() { + let root = workspace_root(); + let relative = REBUILD_RUNTIME_SOURCE_RELATIVE; + let source = rust_source(&root, relative).expect("raw-source rebuild runtime"); + let baseline = syn::parse_file(&source).expect("raw-source rebuild AST"); + validate_caller_schema_dependency_authority(&baseline, relative) + .expect("current caller-schema dependency authority"); + validate_scoped_integrity_authority(&baseline, relative) + .expect("current mutated-parent inventory authority"); + validate_coordinator_authority(&baseline, relative) + .expect("current rebuild coordinator authority"); + + let missing_mutated_parent = source.replacen(" \"sqlite_sequence\",\n", "", 1); + assert_ne!( + missing_mutated_parent, source, + "mutated-parent omission fixture must mutate" + ); + let missing_mutated_parent = + syn::parse_file(&missing_mutated_parent).expect("parse mutated-parent omission AST"); + validate_scoped_integrity_authority(&missing_mutated_parent, relative) + .expect_err("mutated-parent omission must fail closed"); + + for (label, mutation) in [ + ( + "limit-retarget", + source.replacen( + "const RAW_SOURCE_REBUILD_CALLER_MAIN_TABLE_COUNT_LIMIT_V1: u32 = 4_096;", + "const RAW_SOURCE_REBUILD_CALLER_MAIN_TABLE_COUNT_LIMIT_V1: u32 = 4_097;", + 1, + ), + ), + ( + "unqualified-foreign-key-inventory", + source.replacen( + "JOIN main.pragma_foreign_key_list(child.name, 'main') AS foreign_key", + "JOIN pragma_foreign_key_list(child.name) AS foreign_key", + 1, + ), + ), + ( + "action-filter", + source.replacen( + r#"ON foreign_key.\"table\" COLLATE NOCASE = rebuild_parent.name"#, + r#"ON foreign_key.\"table\" COLLATE NOCASE = rebuild_parent.name + AND foreign_key.on_delete = 'CASCADE'"#, + 1, + ), + ), + ( + "temporary-schema-redirection", + source.replacen( + "FROM main.sqlite_schema AS child", + "FROM temp.sqlite_schema AS child", + 1, + ), + ), + ] { + assert_ne!(mutation, source, "{label} fixture must mutate"); + let mutation = syn::parse_file(&mutation) + .unwrap_or_else(|error| panic!("parse {label} caller-schema AST: {error}")); + assert!( + validate_caller_schema_dependency_authority(&mutation, relative).is_err(), + "{label} caller-schema bypass must fail closed" + ); + } + + let coordinator_bypass = source.replacen( + "preflight_caller_owned_schema_dependencies_v1(connection, caller_schema_limits).await?;", + "let _ = caller_schema_limits;", + 1, + ); + assert_ne!( + coordinator_bypass, source, + "coordinator bypass fixture must mutate" + ); + let coordinator_bypass = + syn::parse_file(&coordinator_bypass).expect("parse coordinator bypass AST"); + validate_coordinator_authority(&coordinator_bypass, relative) + .expect_err("caller-schema preflight bypass must fail closed"); + } + + #[test] + fn cold_repair_authority_rejects_caller_mode_and_route_bypasses() { + let root = workspace_root(); + let relative = "crates/event_store/src/store.rs"; + let source = rust_source(&root, relative).expect("event-store source"); + let baseline = syn::parse_file(&source).expect("event-store AST"); + validate_public_entry_point_authority(&baseline, relative) + .expect("current cold-repair authority"); + + let caller_mode = source.replacen( + "pub async fn repair_file_from_raw_v1(\n path: impl AsRef<Path>,\n )", + "pub async fn repair_file_from_raw_v1(\n path: impl AsRef<Path>,\n pool: SqlitePool,\n )", + 1, + ); + assert_ne!(caller_mode, source, "caller-mode fixture must mutate"); + let caller_mode = syn::parse_file(&caller_mode).expect("caller-mode AST"); + let error = validate_public_entry_point_authority(&caller_mode, relative) + .expect_err("caller-supplied backing mode must fail closed"); + assert!(error.contains("signature drifted"), "{error}"); + + for (label, mutation) in [ + ( + "multi-connection", + source.replacen( + ".max_connections(1)\n .connect_with(options)\n .await?;\n pool.set_connect_options(raw_source_repair_connect_options_v1(&canonical_path));", + ".max_connections(2)\n .connect_with(options)\n .await?;\n pool.set_connect_options(raw_source_repair_connect_options_v1(&canonical_path));", + 1, + ), + ), + ( + "lock-domain-bypass", + source.replacen( + "validate_raw_source_repair_canonical_lock_domain_v1(&canonical_path).await", + "Ok(())", + 1, + ), + ), + ( + "identity-bypass", + source.replacen( + " if actual != canonical_path {", + " if false && actual != canonical_path {", + 1, + ), + ), + ( + "unqualified-lock-probe", + source.replacen( + "UPDATE main.radroots_event_store_write_lock", + "UPDATE radroots_event_store_write_lock", + 1, + ), + ), + ( + "create-missing-file", + source.replacen(".create_if_missing(false)", ".create_if_missing(true)", 1), + ), + ] { + assert_ne!(mutation, source, "{label} fixture must mutate"); + let mutation = syn::parse_file(&mutation) + .unwrap_or_else(|error| panic!("parse {label} cold-repair bypass AST: {error}")); + assert!( + validate_public_entry_point_authority(&mutation, relative).is_err(), + "{label} cold-repair bypass must fail closed" + ); + } + } + + #[test] + fn schema_rejects_unknown_runtime_fields() { + let schema = manifest_schema(); + let root = workspace_root(); + let schema_bytes = canonical_json_bytes(&schema).expect("schema bytes"); + let mut manifest = serde_json::to_value( + describe_manifest(&root, &schema_bytes).expect("current manifest"), + ) + .expect("manifest value"); + manifest + .pointer_mut("/runtime") + .and_then(Value::as_object_mut) + .expect("runtime object") + .insert("unbounded_scan".to_owned(), Value::Bool(true)); + let error = validate_json_schema(&schema, &manifest) + .expect_err("unknown runtime field must fail closed"); + assert!(error.contains("violates"), "{error}"); + } +} diff --git a/tools/xtask/src/contract/source_maintenance.rs b/tools/xtask/src/contract/source_maintenance.rs @@ -1,16 +1,13 @@ +// Frozen predecessor mutation fixtures intentionally retain non-runtime helpers. #![allow(dead_code)] -use super::artifact_bundle::{ - GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction, -}; -use super::food_availability_projection::{ - validate_food_availability_projection_manifest_under_lock, - validate_food_availability_projection_predecessor_production_sources_under_lock, -}; +use super::artifact_bundle::{read_regular_file, with_artifact_bundle_transaction}; use super::nip09_reconciliation::validate_current_event_store_successor_authority; use quote::ToTokens; use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; +use serde_json::Value; +#[cfg(test)] +use serde_json::json; use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; @@ -37,8 +34,6 @@ const RAW_TAG_REJECTION_SCAN_BOUND: u64 = RAW_TAG_COUNT_LIMIT + 1; const RETAINED_GENERATION_REJECTION_SCAN_BOUND: u32 = RETAINED_SOURCE_GENERATION_LIMIT + 1; const SCHEMA_SHA256: &str = "d526d96ea02be12b4b0aed99e97cfdde17c4474ace67111506a7b900ee78b186"; const HASH_ALGORITHM: &str = "sha256_bytes_v1"; -const WRITE_COMMAND: &str = "cargo xtask contract source-maintenance-manifest --write"; - const PREDECESSOR_HOOK_ID: &str = "food_availability_projection_v1"; const PREDECESSOR_MANIFEST_RELATIVE: &str = "crates/event_store/contracts/food_availability_projection_v1.manifest.json"; @@ -73,10 +68,60 @@ const RESULT_VECTOR_EXECUTOR_RELATIVE: &str = const RESULT_VECTOR_EXECUTOR_ID: &str = "radroots_event_store.source_maintenance_v1.result_vector_executor.v1"; const RESULT_VECTOR_EXECUTOR_TEST: &str = "source_maintenance_v1_result_vector"; -const CONTRACT_COMMAND_SOURCE_RELATIVE: &str = "tools/xtask/src/contract.rs"; -const XTASK_MAIN_SOURCE_RELATIVE: &str = "tools/xtask/src/main.rs"; -const XTASK_MAIN_FULL_AST_SHA256: &str = - "b48c71c7f40f45c89bd7c83935d48eac3a1a367c8f73f62262e8ee14404616b4"; +#[derive(Clone, Copy)] +struct ImmutableArtifactSpec { + relative: &'static str, + byte_length: usize, + sha256: &'static str, +} + +const IMMUTABLE_PREDECESSOR_ARTIFACTS: [ImmutableArtifactSpec; 9] = [ + ImmutableArtifactSpec { + relative: MANIFEST_RELATIVE, + byte_length: 14_216, + sha256: "e8911e6e5710278969cbd15557a5b856b1575dfd11a655711403598370b41221", + }, + ImmutableArtifactSpec { + relative: MANIFEST_SCHEMA_RELATIVE, + byte_length: 12_315, + sha256: "ad4a6c8ae9488fc8033792bc6952af04687f312901c1847d8c668a62913bb642", + }, + ImmutableArtifactSpec { + relative: MANIFEST_SHA256_RELATIVE, + byte_length: 65, + sha256: "b6a6040932c092574f25caf0fa008a892ba9258f2848444edebbdbc3e441c633", + }, + ImmutableArtifactSpec { + relative: GENERATED_DESCRIPTOR_RELATIVE, + byte_length: 18_723, + sha256: "5f988f800425cf36d4327c828b30943c2f79c1fa577ce80730dc13383a1466b1", + }, + ImmutableArtifactSpec { + relative: RESULT_VECTOR_CANONICAL_RELATIVE, + byte_length: 16_253, + sha256: "997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f", + }, + ImmutableArtifactSpec { + relative: RESULT_VECTOR_MIRROR_RELATIVE, + byte_length: 16_253, + sha256: "997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f", + }, + ImmutableArtifactSpec { + relative: RESULT_VECTOR_EXECUTOR_RELATIVE, + byte_length: 23_510, + sha256: "a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3", + }, + ImmutableArtifactSpec { + relative: MIGRATION_UP_RELATIVE, + byte_length: 19_841, + sha256: "425dc799f392b87f265a6fb81f89c4a1c7a5db8391ab0380970708cb9c66704d", + }, + ImmutableArtifactSpec { + relative: MIGRATION_DOWN_RELATIVE, + byte_length: 5_172, + sha256: "fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860", + }, +]; const RAW_EVENT_COLUMNS: &[&str] = &[ "event_id", @@ -313,18 +358,6 @@ pub(super) fn source_contract_fixture_source_paths() -> Vec<&'static str> { SOURCE_SPECS.iter().map(|source| source.path).collect() } -const PREDECESSOR_SUPERSEDED_SOURCE_PATHS: &[&str] = &[ - "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/protocol_reconciliation_v1.rs", -]; - const GENERATED_ARTIFACT_PATHS: &[&str] = &[ MANIFEST_RELATIVE, MANIFEST_SCHEMA_RELATIVE, @@ -515,9 +548,7 @@ struct VectorCase { } pub(crate) fn write_source_maintenance_manifest(workspace_root: &Path) -> Result<(), String> { - with_artifact_bundle_transaction(workspace_root, |transaction| { - let artifacts = expected_artifacts(workspace_root)?; - transaction.write(artifacts)?; + with_artifact_bundle_transaction(workspace_root, |_| { validate_source_maintenance_manifest_under_lock(workspace_root) }) } @@ -531,14 +562,6 @@ pub(crate) fn validate_source_maintenance_manifest(workspace_root: &Path) -> Res pub(super) fn validate_source_maintenance_manifest_under_lock( workspace_root: &Path, ) -> Result<(), String> { - 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)); - } - } - 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}"))?; @@ -571,161 +594,28 @@ pub(super) fn validate_source_maintenance_manifest_under_lock( let vector: SourceMaintenanceVector = 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(workspace_root, &vector)?; - 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, - }, - ]) -} + validate_immutable_result_vector(&vector)?; -fn describe_manifest( - workspace_root: &Path, - schema_bytes: &[u8], -) -> Result<SourceMaintenanceManifest, String> { - validate_food_availability_projection_manifest_under_lock(workspace_root)?; - 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 + if manifest.migration.up.sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[7].sha256 + || manifest.migration.down.sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[8].sha256 + || manifest.result_vector.sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[4].sha256 + || manifest.result_vector.executor_sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[6].sha256 { return Err(format!( - "{PREDECESSOR_MANIFEST_RELATIVE} does not match the immutable predecessor identity" + "{MANIFEST_RELATIVE} does not describe the immutable SourceMaintenance predecessor identity" )); } - validate_predecessor_public_api(&predecessor_bytes)?; - - let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?; - let vector: SourceMaintenanceVector = 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(workspace_root, &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 migration_up = descriptor_for_file(workspace_root, MIGRATION_UP_RELATIVE)?; - let migration_down = descriptor_for_file(workspace_root, MIGRATION_DOWN_RELATIVE)?; - validate_migration_identity(&migration_up, &migration_down)?; - - 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(SourceMaintenanceManifest { - 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: migration_up, - down: migration_down, - schema_sha256: SCHEMA_SHA256.to_owned(), - catalog, - }, - source_maintenance: SourceMaintenanceDescriptor { - version: CAPACITY_VERSION, - event_contract_registry_version: EVENT_CONTRACT_REGISTRY_VERSION, - capacity_authority_id: CAPACITY_AUTHORITY_ID.to_owned(), - accounting: AccountingDescriptor { - algorithm: ACCOUNTING_ALGORITHM.to_owned(), - raw_event_columns: owned(RAW_EVENT_COLUMNS), - raw_tag_columns: owned(RAW_TAG_COLUMNS), - nullable_raw_tag_columns: owned(NULLABLE_RAW_TAG_COLUMNS), - }, - limits: expected_limits(), - reopen_validation: ReopenValidationDescriptor { - mode: REOPEN_VALIDATION_MODE.to_owned(), - raw_event_rejection_scan_bound: RAW_EVENT_REJECTION_SCAN_BOUND, - raw_tag_rejection_scan_bound: RAW_TAG_REJECTION_SCAN_BOUND, - generation_history_validation: GENERATION_HISTORY_VALIDATION.to_owned(), - retained_generation_rejection_scan_bound: RETAINED_GENERATION_REJECTION_SCAN_BOUND, - }, - rebuild_seal: RebuildSealDescriptor { - nip09_hook_id: NIP09_HOOK_ID.to_owned(), - nip09_manifest_sha256: NIP09_MANIFEST_SHA256.to_owned(), - food_hook_id: PREDECESSOR_HOOK_ID.to_owned(), - food_manifest_sha256: PREDECESSOR_MANIFEST_SHA256.to_owned(), - food_scope_fingerprint_sha256: FOOD_SCOPE_FINGERPRINT_SHA256.to_owned(), - active_generation_authority: ACTIVE_GENERATION_AUTHORITY.to_owned(), - marker_close_authority: MARKER_CLOSE_AUTHORITY.to_owned(), - }, - }, - 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: expected_public_api(), - 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(), - }, - }) + for artifact in IMMUTABLE_PREDECESSOR_ARTIFACTS { + let actual = read_regular_file(workspace_root, artifact.relative)?; + if actual.len() != artifact.byte_length || sha256_hex(&actual) != artifact.sha256 { + return Err(format!( + "immutable SourceMaintenance predecessor artifact {} does not match its authenticated byte identity", + artifact.relative + )); + } + } + Ok(()) } fn expected_limits() -> LimitDescriptor { @@ -759,62 +649,6 @@ fn owned(values: &[&str]) -> Vec<String> { values.iter().map(|value| (*value).to_owned()).collect() } -fn validate_predecessor_production_source_coverage(workspace_root: &Path) -> Result<(), String> { - let source_paths = SOURCE_SPECS - .iter() - .map(|source| source.path) - .collect::<Vec<_>>(); - let unique_source_paths = source_paths.iter().copied().collect::<BTreeSet<_>>(); - if unique_source_paths.len() != source_paths.len() { - return Err("SourceMaintenance SOURCE_SPECS paths must be unique".to_owned()); - } - for path in PREDECESSOR_SUPERSEDED_SOURCE_PATHS { - let count = source_paths - .iter() - .filter(|candidate| **candidate == *path) - .count(); - if count != 1 { - return Err(format!( - "SourceMaintenance successor must current-byte-bind superseded predecessor path `{path}` exactly once; found {count}" - )); - } - } - let superseded = PREDECESSOR_SUPERSEDED_SOURCE_PATHS - .iter() - .copied() - .collect::<BTreeSet<_>>(); - if superseded.len() != PREDECESSOR_SUPERSEDED_SOURCE_PATHS.len() { - return Err("SourceMaintenance predecessor supersession paths must be unique".to_owned()); - } - validate_food_availability_projection_predecessor_production_sources_under_lock( - workspace_root, - PREDECESSOR_SUPERSEDED_SOURCE_PATHS, - ) -} - -fn validate_predecessor_public_api(predecessor_bytes: &[u8]) -> Result<(), String> { - let predecessor: Value = serde_json::from_slice(predecessor_bytes) - .map_err(|error| format!("parse {PREDECESSOR_MANIFEST_RELATIVE}: {error}"))?; - let actual = predecessor - .pointer("/public_api") - .and_then(Value::as_array) - .ok_or_else(|| format!("{PREDECESSOR_MANIFEST_RELATIVE} has no public_api array"))? - .iter() - .map(|value| { - value.as_str().map(str::to_owned).ok_or_else(|| { - format!("{PREDECESSOR_MANIFEST_RELATIVE} public_api values must be strings") - }) - }) - .collect::<Result<Vec<_>, String>>()?; - if actual != owned(INHERITED_PUBLIC_API) { - return Err( - "SourceMaintenance inherited public API must exactly equal the immutable FoodAvailability public API" - .to_owned(), - ); - } - Ok(()) -} - fn descriptor_for_file(workspace_root: &Path, relative: &str) -> Result<FileDescriptor, String> { descriptor_for_bytes(relative, &read_regular_file(workspace_root, relative)?) } @@ -959,84 +793,9 @@ pub(super) fn validate_source_contract(workspace_root: &Path) -> Result<(), Stri validate_schema_capacity_authority(workspace_root)?; validate_generation_rebuild_authority(workspace_root)?; validate_sql_capacity_authority(workspace_root)?; - validate_contract_command_reachability_authority(workspace_root)?; validate_current_event_store_successor_authority(workspace_root) } -fn validate_contract_command_reachability_authority(workspace_root: &Path) -> Result<(), String> { - let contract = rust_source(workspace_root, CONTRACT_COMMAND_SOURCE_RELATIVE)?; - let main = rust_source(workspace_root, XTASK_MAIN_SOURCE_RELATIVE)?; - validate_contract_command_reachability_sources(&contract, &main) -} - -fn validate_contract_command_reachability_sources( - contract_source: &str, - main_source: &str, -) -> Result<(), String> { - let contract = syn::parse_file(contract_source) - .map_err(|error| format!("parse {CONTRACT_COMMAND_SOURCE_RELATIVE}: {error}"))?; - let main = syn::parse_file(main_source) - .map_err(|error| format!("parse {XTASK_MAIN_SOURCE_RELATIVE}: {error}"))?; - let main_ast_sha256 = sha256_hex(compact_tokens(&main).as_bytes()); - if main_ast_sha256 != XTASK_MAIN_FULL_AST_SHA256 { - return Err(format!( - "{XTASK_MAIN_SOURCE_RELATIVE} full dispatch AST authority drifted: expected {XTASK_MAIN_FULL_AST_SHA256}, found {main_ast_sha256}" - )); - } - for (relative, file, name, expected) in [ - ( - CONTRACT_COMMAND_SOURCE_RELATIVE, - &contract, - "validate_artifact_contracts", - r#"pub(crate) fn validate_artifact_contracts( - workspace_root: &Path - ) -> Result<(), String> { - validate_event_contract_registry_v7_inventory(workspace_root)?; - validate_nip09_reconciliation_manifest(workspace_root)?; - validate_food_availability_projection_manifest(workspace_root)?; - validate_source_maintenance_manifest(workspace_root)?; - validate_knowledge_contract_manifest(workspace_root) - }"#, - ), - ( - XTASK_MAIN_SOURCE_RELATIVE, - &main, - "validate_contract", - r#"fn validate_contract() -> Result<(), String> { - radroots_protocol_contract_v1::validate_protocol_contract_v1() - .map_err(|error| error.to_string())?; - let root = workspace_root(); - dto_roots::check(&root)?; - contract::load_contract_bundle(&root) - .and_then(|bundle| contract::validate_contract_bundle(&bundle)) - .and_then(|_| contract::validate_canonical_event_boundary(&root)) - .and_then(|_| contract::validate_artifact_contracts(&root)) - }"#, - ), - ( - XTASK_MAIN_SOURCE_RELATIVE, - &main, - "release_preflight_at", - r#"fn release_preflight_at(root: &Path) -> Result<(), String> { - dto_roots::check(root)?; - contract::validate_artifact_contracts(root)?; - contract::validate_release_preflight(root) - }"#, - ), - ] { - let actual = compact_tokens(exact_top_level_function(file, name)?); - let expected_file = syn::parse_file(expected) - .map_err(|error| format!("parse authoritative `{name}` function: {error}"))?; - let expected = compact_tokens(exact_top_level_function(&expected_file, name)?); - if actual != expected { - return Err(format!( - "{relative} `{name}` SourceMaintenance validation call-path authority drifted: expected `{expected}`, found `{actual}`" - )); - } - } - Ok(()) -} - fn validate_source_inventory() -> Result<(), String> { validate_unique( "SourceMaintenance source roles", @@ -2089,27 +1848,6 @@ fn exact_free_function_tokens(file: &syn::File, name: &str) -> Result<String, St Ok(compact_tokens(exact_free_function(file, name)?)) } -fn exact_top_level_function<'a>( - file: &'a syn::File, - name: &str, -) -> Result<&'a syn::ItemFn, String> { - let matches = file - .items - .iter() - .filter_map(|item| match item { - Item::Fn(function) if function.sig.ident == name => Some(function), - _ => None, - }) - .collect::<Vec<_>>(); - let [function] = matches.as_slice() else { - return Err(format!( - "governed Rust source must define top-level function `{name}` exactly once; found {}", - matches.len() - )); - }; - Ok(function) -} - fn rust_source(workspace_root: &Path, relative: &str) -> Result<String, String> { let bytes = read_regular_file(workspace_root, relative)?; std::str::from_utf8(&bytes) @@ -2296,78 +2034,7 @@ fn validate_manifest_shape(manifest: &SourceMaintenanceManifest) -> Result<(), S Ok(()) } -fn generated_descriptor( - manifest: &SourceMaintenanceManifest, - manifest_bytes: &[u8], - manifest_sha256: &str, -) -> String { - let manifest_json = std::str::from_utf8(manifest_bytes).expect("canonical manifest is UTF-8"); - let manifest_literal = format!("{manifest_json:?}"); - format!( - "// @generated by `cargo xtask contract source-maintenance-manifest --write`; do not edit.\n\ -pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = {manifest_literal};\n\ -pub(crate) const SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH: usize = {};\n\ -pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SHA256: &str =\n \"{manifest_sha256}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION: u32 = {SCHEMA_VERSION};\n\ -pub(crate) const SOURCE_MAINTENANCE_CONTRACT_ID: &str =\n \"{CONTRACT_ID}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_HOOK_ID: &str = \"{HOOK_ID}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_MIGRATION_VERSION: u32 = {MIGRATION_VERSION};\n\ -pub(crate) const SOURCE_MAINTENANCE_MIGRATION_NAME: &str = \"{MIGRATION_NAME}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_MIGRATION_UP_BYTE_LENGTH: usize = {};\n\ -pub(crate) const SOURCE_MAINTENANCE_MIGRATION_UP_SHA256: &str =\n \"{}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_MIGRATION_DOWN_BYTE_LENGTH: usize = {};\n\ -pub(crate) const SOURCE_MAINTENANCE_MIGRATION_DOWN_SHA256: &str =\n \"{}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_SCHEMA_SHA256: &str =\n \"{SCHEMA_SHA256}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION: u32 = {EVENT_CONTRACT_REGISTRY_VERSION};\n\ -pub(crate) const SOURCE_MAINTENANCE_CAPACITY_VERSION: u32 = {CAPACITY_VERSION};\n\ -pub(crate) const SOURCE_MAINTENANCE_CAPACITY_AUTHORITY_ID: &str =\n \"{CAPACITY_AUTHORITY_ID}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_ACCOUNTING_ALGORITHM: &str = \"{ACCOUNTING_ALGORITHM}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_COLUMNS: &[&str] = &{};\n\ -pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_COLUMNS: &[&str] =\n &{};\n\ -pub(crate) const SOURCE_MAINTENANCE_NULLABLE_RAW_TAG_COLUMNS: &[&str] = &{};\n\ -pub(crate) const SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES: &[&str] = &{};\n\ -pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_COUNT_LIMIT: u64 = {RAW_EVENT_COUNT_LIMIT};\n\ -pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_COUNT_LIMIT: u64 = {RAW_TAG_COUNT_LIMIT};\n\ -pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_TEXT_BYTES_LIMIT: u64 = {RAW_EVENT_TEXT_BYTES_LIMIT};\n\ -pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_TEXT_BYTES_LIMIT: u64 = {RAW_TAG_TEXT_BYTES_LIMIT};\n\ -pub(crate) const SOURCE_MAINTENANCE_RETAINED_SOURCE_GENERATION_LIMIT: u32 = {RETAINED_SOURCE_GENERATION_LIMIT};\n\ -pub(crate) const SOURCE_MAINTENANCE_PREDECESSOR_HOOK_ID: &str = \"{PREDECESSOR_HOOK_ID}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_PREDECESSOR_MANIFEST_SHA256: &str =\n \"{PREDECESSOR_MANIFEST_SHA256}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_SHA256: &str =\n \"{}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_ID: &str =\n \"{RESULT_VECTOR_EXECUTOR_ID}\";\n\ -pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_SHA256: &str =\n \"{}\";\n", - manifest_bytes.len(), - usize::try_from(manifest.migration.up.byte_length).expect("up length fits usize"), - manifest.migration.up.sha256, - usize::try_from(manifest.migration.down.byte_length).expect("down length fits usize"), - manifest.migration.down.sha256, - rust_multiline_string_slice(RAW_EVENT_COLUMNS), - rust_string_slice(RAW_TAG_COLUMNS), - rust_string_slice(NULLABLE_RAW_TAG_COLUMNS), - rust_multiline_string_slice(EXPECTED_REPLACED_CATALOG_OBJECTS), - manifest.result_vector.sha256, - manifest.result_vector.executor_sha256, - ) -} - -fn rust_string_slice(values: &[&str]) -> String { - let values = values - .iter() - .map(|value| format!("{value:?}")) - .collect::<Vec<_>>() - .join(", "); - format!("[{values}]") -} - -fn rust_multiline_string_slice(values: &[&str]) -> String { - let values = values - .iter() - .map(|value| format!(" {value:?},")) - .collect::<Vec<_>>() - .join("\n"); - format!("[\n{values}\n]") -} - +#[cfg(test)] fn manifest_schema() -> Value { let path_pattern = "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$"; let file = json!({ @@ -2677,10 +2344,6 @@ fn sha256_hex(bytes: &[u8]) -> String { hex::encode(Sha256::digest(bytes)) } -fn stale_error(relative: &str) -> String { - format!("{relative} is stale; run `{WRITE_COMMAND}`") -} - #[derive(Clone, Copy)] struct ExpectedVectorCase { id: &'static str, @@ -3079,10 +2742,7 @@ const EXPECTED_VECTOR_CASES: &[ExpectedVectorCase] = &[ }, ]; -fn validate_result_vector( - workspace_root: &Path, - vector: &SourceMaintenanceVector, -) -> Result<(), String> { +fn validate_immutable_result_vector(vector: &SourceMaintenanceVector) -> Result<(), String> { if vector.schema_version != SCHEMA_VERSION || vector.contract_id != CONTRACT_ID || vector.capacity_version != CAPACITY_VERSION @@ -3140,632 +2800,13 @@ fn validate_result_vector( } } } - validate_delegated_test_authorities(workspace_root, vector) -} - -#[derive(Clone, Copy)] -struct DelegatedAuthoritySpec { - path: &'static str, - test: &'static str, - ordered_markers: &'static [&'static str], -} - -const EXECUTABLE_AUTHORITY_AST_SHA256: &str = - "19c405fc91468997aa53f08ebbaed82c526bf4810b2175ad9034d95dc95b8597"; -const BOUND_AUTHORITY_SOURCE_AST_SHA256: &str = - "ba44c729ebba14e658ec7b48f7ba395fd4e8fb3d597d306df5cfa7010a4f9bac"; - -#[derive(Clone, Debug, Serialize)] -struct ExecutableAuthorityIdentity { - path: String, - test: String, - tokens: String, -} - -#[derive(Clone, Debug, Serialize)] -struct BoundAuthoritySourceIdentity { - path: String, - tokens: String, -} - -const DELEGATED_AUTHORITIES: &[DelegatedAuthoritySpec] = &[ - DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "exact_capacity_boundary_allows_duplicate_observation_and_ephemeral_noop", - ordered_markers: &[ - "RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1", - "validate_source_capacity_authority_fast_v1", - "duplicate.persistence.is_duplicate()", - "SourceCapacityExceeded", - "RadrootsEventPersistence::NotPersisted", - "assert_eq!(ephemeral_observation_count,0)", - "transaction.rollback().await", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "borrowed_ingest_savepoint_rolls_back_post_core_authority_forge", - ordered_markers: &[ - "priorcallerwork", - "capacity_after_prior", - "expect_err(\"post-corerawauthoritymutationmustfail\")", - "MigrationHookStateDrift", - "capacity_after_rollback,capacity_after_prior", - "callermaycommitpriorworkafterfailedingest", - "raw_event(prior_event.id_str())", - "raw_event(trigger_event.id_str())", - "raw_event(forged_event.id_str())", - "trade_mutation_count,0", - "transition_count,0", - "capacity_after_prior", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/source_maintenance_v1.rs", - test: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over", - ordered_markers: &[ - "RadrootsEventStoreSourceCapacityResourceV1::RawEvents", - "RadrootsEventStoreSourceCapacityResourceV1::RawTags", - "RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes", - "RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes", - "capacity_with(resource,limit-1)", - "delta_with(resource,1)", - "capacity_with(resource,limit)", - "SourceCapacityExceeded", - "requested:1", - "capacity_with(resource,limit+1)", - "requested:0", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "v3_to_v4_under_limit_backfills_exact_capacity_and_preserves_source", - ordered_markers: &[ - "&EVENT_STORE_MIGRATIONS[..3]", - "event_envelopes", - "active_generation", - "RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT", - "Managed{version:4}", - "radroots_event_store_source_capacity_v1", - "assert_eq!(capacity,(generation_before,1,0,event_bytes,0,1,8))", - "preserved", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "v3_to_v4_rejects_prior_transition_drift_atomically", - ordered_markers: &[ - "&EVENT_STORE_MIGRATIONS[..3]", - "predecessor_trigger_sql", - "corruption.commit().await", - "expect_err(\"v4upgrademustnotrepaircorruptmanaged-v3hookstate\")", - "MigrationHookStateDrift{hook_id:\"nip09_reconciliation_v1\"", - "ledger_after,ledger_before", - "v4_objects,0", - "v4_ledger_rows,0", - "schema_object_sql(&pool,name).await,*sql", - "Managed{version:3}", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "source_capacity_is_rechecked_for_every_rebuild_bound_migration", - ordered_markers: &[ - "raw_events:0", - "UnledgeredBaseline", - "&EVENT_STORE_MIGRATIONS[..3]", - "expect_err(\"v4capacityexcessmustfail\")", - "SourceCapacityExceeded", - "Managed{version:3}", - "radroots_event_store_source_capacity_v1", - "assert_eq!(v4_object_count,0)", - "assert_eq!(v4_ledger_count,0)", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "v4_rejects_persisted_legacy_ephemeral_rows_atomically", - ordered_markers: &[ - "&EVENT_STORE_MIGRATIONS[..3]", - "kind,tags_json,content", - "20000", - "begin_with(\"BEGINIMMEDIATE\")", - "expect_err(\"persistedephemeralsourcemustrejectv4\")", - "PersistedEphemeralRawEvent", - "Managed{version:3}", - "radroots_event_store_source_capacity_v1", - "assert_eq!(v4_object_count,0)", - "assert_eq!(v4_ledger_count,0)", - "assert_eq!(raw_count,1)", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/source_maintenance_v1.rs", - test: "reopen_full_measure_detects_every_persisted_capacity_dimension", - ordered_markers: &[ - "raw_event_count=1", - "raw_tag_count=1", - "raw_event_bytes=1", - "raw_tag_bytes=1", - "RadrootsEventStore::open_file(&path).await", - "SourceCapacityStateDrift", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/source_maintenance_v1.rs", - test: "reopen_stops_at_the_first_raw_event_one_over_before_ephemeral_probe", - ordered_markers: &[ - "value<25001", - "CASEWHENvalue=25001THEN20000ELSE1END", - "RadrootsEventStore::open_file(&path).await", - "SourceCapacityExceeded", - "current:25_000", - "requested:1", - "limit:25_000", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "ninth_current_v4_rebuild_is_typed_and_preflight_atomic", - ordered_markers: &[ - "forordinalin2_u8..=8", - "assert_eq!(capacity_before.retained_generation_count(),8)", - "PanickingGeneration", - "expect_err(\"ninthrebuildmustfailbeforeentropyormutation\")", - "SourceGenerationHistoryLimitReached{current:8,limit:8,}", - "assert_eq!(source_authority_snapshot(&store).await,source_before)", - "assert_eq!(raw_authority_digest(&store).await,raw_before)", - "assert_eq!(normalized_nip09_snapshot(&store).await,nip09_before)", - "assert_eq!(food_after,food_before)", - "assert_eq!(derived_after,(derived_before.0,derived_before.1,derived_before.2,0))", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/source_maintenance_v1.rs", - test: "generation_sql_backstop_allows_exact_append_and_is_conflict_safe_one_over", - ordered_markers: &[ - "retained_generation_count=retained_generation_limit-1", - "exactgenerationboundarymustappend", - "assert_eq!(retained_count,8)", - "sourcegenerationalreadyexists", - "uniquegenerationappendmusthittheSQLcapacitybackstop", - "sqlx::Error::Database", - "retainedsourcegenerationlimitreached", - "transaction.rollback().await", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "current_v4_rebuild_rotates_capacity_and_food_authority_end_to_end", - ordered_markers: &[ - "apply_reconciliation_hook", - "after.retained_generation_count()", - "before.retained_generation_count()+1", - "assert_eq!(marker_count,0)", - "audit_food_availability_projection_v1", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/source_maintenance_v1.rs", - test: "marker_close_sql_backstop_rejects_each_required_seal_drift", - ordered_markers: &[ - "rebuildmarkercannotclosebeforecapacity,NIP-09,andFoodAvailabilitysealsagree", - "fordriftin[\"capacity\",\"nip09\",\"food\",\"fts\"]", - "radroots_event_store_source_capacity_update_guard", - "raw_event_bytes=raw_event_bytes+1", - "radroots_event_store_addressable_feed_integrity_v1", - "last_transition_seq=last_transition_seq+1", - "radroots_event_store_food_availability_cursor_update_guard", - "projected_row_count=projected_row_count+1", - "radroots_event_store_food_availability_search_fts", - "DELETEFROMradroots_event_store_source_rebuild_marker", - "sqlx::Error::Database", - "database.message()==MARKER_CLOSE_ERROR", - "transaction.rollback().await", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "v4_marker_open_allows_repairing_prior_transition_high_water_drift", - ordered_markers: &[ - "last_transition_seq=7", - "validate_schema_fingerprint", - "wrong_prior", - "wrong_floor", - "expect(\"derivedtransitiondriftisrepairableunderv4\")", - "repaired.0.as_slice(),target_generation.as_slice()", - "repaired.1,repaired.2", - "repaired.1,0", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "v4_food_reset_requires_marker_rotation_and_preserves_target_rows", - ordered_markers: &[ - "expect_err(\"marker-freeFoodresetmustfail\")", - "expect_err(\"markeralonemustnotauthorizeFoodreset\")", - "expect(\"rotatesourcestate\")", - "expect(\"post-rotationhistoricalFoodreset\")", - "expect_err(\"activetarget-generationFoodrowsmustremainguarded\")", - "remaining,(1,1)", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "v4_down_restores_exact_predecessor_trigger_sql_and_fingerprint", - ordered_markers: &[ - "&EVENT_STORE_MIGRATIONS[..3]", - "predecessor_sql", - "assert_ne!(schema_object_sql(&pool,name).await,predecessor_sql[*name])", - "rollback_event_store_schema_with_registry", - "assert_eq!(schema_object_sql(&pool,name).await,predecessor_sql[*name])", - "Managed{version:3}", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "open_file_rejects_utf16_main_database_before_schema_or_journal_mutation", - ordered_markers: &[ - "initialize_utf16le_database(&path).await", - "RadrootsEventStore::open_file(&path).await", - "SqliteMainDatabaseEncodingNotUtf8{actual}", - "actual==\"UTF-16le\"", - "assert_utf16le_database_was_not_mutated(&path).await", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "open_pool_rejects_utf16_main_database_before_schema_or_journal_mutation", - ordered_markers: &[ - "initialize_utf16le_database(&path).await", - "max_connections(2)", - "RadrootsEventStore::open_pool(pool,true).await", - "SqliteMainDatabaseEncodingNotUtf8{actual}", - "actual==\"UTF-16le\"", - "assert_utf16le_database_was_not_mutated(&path).await", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "utf8_file_reopen_preserves_non_ascii_and_nul_capacity_accounting", - ordered_markers: &[ - "letexpected_tag_count=", - "raw_source_text_bytes(&event)", - "expect(\"non-ASCIIandNULingest\")", - "before_reopen.raw_event_count(),1", - "before_reopen.raw_tag_count(),expected_tag_count", - "before_reopen.raw_event_text_bytes(),expected_event_bytes", - "before_reopen.raw_tag_text_bytes(),expected_tag_bytes", - "store.pool().close().await", - "RadrootsEventStore::open_file(&path).await", - "source_capacity_v1()", - "before_reopen", - "raw_event(&event_id)", - "tags_for_event(&event_id)", - "assert_eq!(tags.len(),2)", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "rollback_rejects_below_floor_ahead_unmanaged_and_generation_destructive_targets", - ordered_markers: &[ - "lethistory_before:Vec<(Vec<u8>,i64)>=", - "rollback_event_store_schema_offline(&managed,1).await", - "RollbackWouldDiscardSourceGenerationHistory{current:RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,target:1,floor:2,}", - "inspect_event_store_schema_status(&managed).await", - "Managed{version:RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,}", - "source-generationhistoryafterrejectedrollback", - "history_before", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/schema.rs", - test: "rollback_cannot_bypass_generation_history_guard_through_version_three", - ordered_markers: &[ - "rollback_event_store_schema_offline(&managed,3).await", - "lethistory_before:Vec<(Vec<u8>,i64)>=", - "rollback_event_store_schema_offline(&managed,1).await", - "RollbackWouldDiscardSourceGenerationHistory{current:3,target:1,floor:2,}", - "inspect_event_store_schema_status(&managed).await", - "Managed{version:3}", - "v3source-generationhistoryafterrejectedbypass", - "history_before", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "independent_file_pools_serialize_the_last_raw_event_byte_capacity_slot", - ordered_markers: &[ - "RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1", - "before_race.raw_event_text_bytes(),filler_target", - "Barrier::new(3)", - "tokio::spawn", - "tokio::spawn", - "tokio::join!", - "(Ok(accepted),Err(rejected))|(Err(rejected),Ok(accepted))", - "accepted.persistence.is_inserted()", - "SourceCapacityExceeded{resource:crate::RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes", - "requested==contender_bytes", - "cleanfullreopenafterlast-slotrace", - "after_race.raw_event_count(),filler_count+1", - "after_race.raw_event_text_bytes(),crate::RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1", - "assert_eq!(retained_contenders,1)", - ], - }, -]; - -const MANDATORY_BOUND_AUTHORITIES: &[DelegatedAuthoritySpec] = &[ - DelegatedAuthoritySpec { - path: "crates/event_store/src/nip09/reconciliation_v1.rs", - test: "bounded_capacity_page_len_caps_gross_source_probe_at_one_over", - ordered_markers: &[ - "forlimitin[25_000_u64,250_000_u64]", - "bounded_capacity_page_len(current,limit)", - "(1..=RECONCILIATION_SNAPSHOT_BATCH_LEN).contains(&fetched_len)", - "assert_eq!(fetched,limit+1)", - "bounded_capacity_page_len(24_576,25_000),(425,425)", - "bounded_capacity_page_len(249_856,250_000),(145,145)", - "bounded_capacity_page_len(25_000,25_000),(1,1)", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/source_maintenance_v1.rs", - test: "generation_append_limit_returns_the_typed_current_and_limit", - ordered_markers: &[ - "validate_source_generation_append_available_v1(7,8)", - "validate_source_generation_append_available_v1(8,8)", - "SourceGenerationHistoryLimitReached{current:8,limit:8,}", - ], - }, - DelegatedAuthoritySpec { - path: "crates/event_store/src/source_maintenance_v1.rs", - test: "retained_generation_nip09_logical_rows_have_an_audited_upper_bound", - ordered_markers: &[ - "RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1", - "RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1", - "letper_generation=", - "4*events+2*tags+2", - "RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1", - "4_800_016", - "EVENT_STORE_MIGRATIONS", - ], - }, -]; - -const MANDATORY_BOUND_HELPER_AUTHORITIES: &[DelegatedAuthoritySpec] = &[DelegatedAuthoritySpec { - path: "crates/event_store/src/store.rs", - test: "assert_utf16le_database_was_not_mutated", - ordered_markers: &[ - "PRAGMAmain.encoding", - "PRAGMAmain.journal_mode", - "SELECTCOUNT(*)FROMmain.sqlite_schemaWHEREname='radroots_event_store_schema_migrations'ORname='event_envelopes'ORnameLIKE'radroots_event_store_%'", - "assert_eq!(encoding,\"UTF-16le\")", - "assert_eq!(journal_mode,\"delete\")", - "assert_eq!(event_store_objects,0)", - ], -}]; - -fn validate_delegated_test_authorities( - workspace_root: &Path, - vector: &SourceMaintenanceVector, -) -> Result<(), String> { - let delegated = vector - .cases - .iter() - .filter(|case| case.execution != DIRECT_EXECUTOR) - .map(|case| (case.authority_path.as_str(), case.authority.as_str())) - .collect::<BTreeSet<_>>(); - let expected = DELEGATED_AUTHORITIES - .iter() - .map(|spec| (spec.path, spec.test)) - .collect::<BTreeSet<_>>(); - if delegated != expected { - return Err(format!( - "SourceMaintenance delegated-test inventory differs: expected {expected:?}, found {delegated:?}" - )); - } - let mut executable_identities = Vec::new(); - for spec in DELEGATED_AUTHORITIES - .iter() - .chain(MANDATORY_BOUND_AUTHORITIES) - { - executable_identities.push(ExecutableAuthorityIdentity { - path: spec.path.to_owned(), - test: spec.test.to_owned(), - tokens: validate_delegated_authority(workspace_root, *spec)?, - }); - } - for spec in MANDATORY_BOUND_HELPER_AUTHORITIES { - executable_identities.push(ExecutableAuthorityIdentity { - path: spec.path.to_owned(), - test: spec.test.to_owned(), - tokens: validate_bound_function_authority(workspace_root, *spec)?, - }); - } - - let executor_source = rust_source(workspace_root, RESULT_VECTOR_EXECUTOR_RELATIVE)?; - let executor = syn::parse_file(&executor_source) - .map_err(|error| format!("parse {RESULT_VECTOR_EXECUTOR_RELATIVE}: {error}"))?; - let executor = exact_free_function(&executor, RESULT_VECTOR_EXECUTOR_TEST)?; - validate_executable_test_authority( - RESULT_VECTOR_EXECUTOR_RELATIVE, - RESULT_VECTOR_EXECUTOR_TEST, - executor, - )?; - let executor = compact_tokens(executor); - for case in vector - .cases - .iter() - .filter(|case| case.execution == DIRECT_EXECUTOR) - { - require_marker( - "SourceMaintenance direct result-vector executor", - &executor, - case.id.as_str(), - )?; - } - require_marker( - "SourceMaintenance direct result-vector executor", - &executor, - "assert_direct_cases_executed", - )?; - executable_identities.push(ExecutableAuthorityIdentity { - path: RESULT_VECTOR_EXECUTOR_RELATIVE.to_owned(), - test: RESULT_VECTOR_EXECUTOR_TEST.to_owned(), - tokens: executor, - }); - validate_executable_authority_identities(&executable_identities)?; - let source_identities = bound_authority_source_identities(workspace_root)?; - validate_bound_authority_source_identities(&source_identities)?; - Ok(()) -} - -fn validate_delegated_authority( - workspace_root: &Path, - spec: DelegatedAuthoritySpec, -) -> Result<String, String> { - let source = rust_source(workspace_root, spec.path)?; - let syntax = syn::parse_file(&source) - .map_err(|error| format!("parse delegated authority {}: {error}", spec.path))?; - let function = exact_free_function(&syntax, spec.test)?; - validate_executable_test_authority(spec.path, spec.test, function)?; - let function = compact_tokens(function); - require_ordered_markers( - &format!("delegated authority {}::{}", spec.path, spec.test), - &function, - spec.ordered_markers, - )?; - Ok(function) -} - -fn validate_executable_authority_identities( - identities: &[ExecutableAuthorityIdentity], -) -> Result<(), String> { - let bytes = canonical_json_bytes(&identities)?; - let actual = sha256_hex(&bytes); - if actual != EXECUTABLE_AUTHORITY_AST_SHA256 { - return Err(format!( - "SourceMaintenance executable direct/delegated authority AST identity drifted: expected {EXECUTABLE_AUTHORITY_AST_SHA256}, found {actual}" - )); - } - Ok(()) -} - -fn bound_authority_source_identities( - workspace_root: &Path, -) -> Result<Vec<BoundAuthoritySourceIdentity>, String> { - let paths = DELEGATED_AUTHORITIES - .iter() - .chain(MANDATORY_BOUND_AUTHORITIES) - .chain(MANDATORY_BOUND_HELPER_AUTHORITIES) - .map(|spec| spec.path) - .chain([RESULT_VECTOR_EXECUTOR_RELATIVE]) - .collect::<BTreeSet<_>>(); - paths - .into_iter() - .map(|path| { - let source = rust_source(workspace_root, path)?; - let file = syn::parse_file(&source) - .map_err(|error| format!("parse bound authority source {path}: {error}"))?; - Ok(BoundAuthoritySourceIdentity { - path: path.to_owned(), - tokens: compact_tokens(&file), - }) - }) - .collect() -} - -fn validate_bound_authority_source_identities( - identities: &[BoundAuthoritySourceIdentity], -) -> Result<(), String> { - let bytes = canonical_json_bytes(&identities)?; - let actual = sha256_hex(&bytes); - if actual != BOUND_AUTHORITY_SOURCE_AST_SHA256 { - return Err(format!( - "SourceMaintenance bound executor/test-module/helper source AST identity drifted: expected {BOUND_AUTHORITY_SOURCE_AST_SHA256}, found {actual}" - )); - } - Ok(()) -} - -#[derive(Default)] -struct EarlyReturnAudit { - count: usize, -} - -impl<'ast> syn::visit::Visit<'ast> for EarlyReturnAudit { - fn visit_expr_return(&mut self, expression: &'ast syn::ExprReturn) { - self.count += 1; - syn::visit::visit_expr_return(self, expression); - } -} - -fn validate_executable_test_authority( - relative: &str, - name: &str, - function: &syn::ItemFn, -) -> Result<(), String> { - let expected_attribute = if function.sig.asyncness.is_some() { - "#[tokio::test]" - } else { - "#[test]" - }; - let attributes = function - .attrs - .iter() - .map(compact_tokens) - .collect::<Vec<_>>(); - if attributes != [expected_attribute] { - return Err(format!( - "executable test authority {relative}::{name} must have exactly `{expected_attribute}` and no disabling or conditional attributes; found {attributes:?}" - )); - } - if !matches!(function.vis, syn::Visibility::Inherited) - || function.sig.constness.is_some() - || function.sig.unsafety.is_some() - || function.sig.abi.is_some() - || !function.sig.inputs.is_empty() - || !function.sig.generics.params.is_empty() - || function.sig.generics.where_clause.is_some() - || !matches!(function.sig.output, syn::ReturnType::Default) - || function.sig.variadic.is_some() - { - return Err(format!( - "executable test authority {relative}::{name} must remain a private zero-argument test with no generic, ABI, unsafe, variadic, or return-type escape" - )); - } - - use syn::visit::Visit; - let mut returns = EarlyReturnAudit::default(); - returns.visit_block(&function.block); - if returns.count != 0 { - return Err(format!( - "executable test authority {relative}::{name} must not contain early return control flow; found {} return expression(s)", - returns.count - )); - } Ok(()) } -fn validate_bound_function_authority( - workspace_root: &Path, - spec: DelegatedAuthoritySpec, -) -> Result<String, String> { - let source = rust_source(workspace_root, spec.path)?; - let syntax = syn::parse_file(&source) - .map_err(|error| format!("parse bound authority {}: {error}", spec.path))?; - let function = exact_free_function_tokens(&syntax, spec.test)?; - require_ordered_markers( - &format!("bound authority {}::{}", spec.path, spec.test), - &function, - spec.ordered_markers, - )?; - Ok(function) -} - #[cfg(test)] mod tests { use super::*; + use std::fs; use std::path::PathBuf; fn workspace_root() -> PathBuf { @@ -3788,11 +2829,8 @@ mod tests { } #[test] - fn source_inventory_excludes_outputs_and_exactly_supersedes_predecessors() { - let root = workspace_root(); + fn source_inventory_excludes_generated_outputs() { validate_source_inventory().expect("source inventory"); - validate_predecessor_production_source_coverage(&root) - .expect("exact predecessor supersession"); let source_paths = SOURCE_SPECS .iter() @@ -3801,14 +2839,6 @@ mod tests { for generated in GENERATED_ARTIFACT_PATHS { assert!(!source_paths.contains(generated)); } - assert_eq!( - PREDECESSOR_SUPERSEDED_SOURCE_PATHS - .iter() - .copied() - .collect::<BTreeSet<_>>() - .len(), - PREDECESSOR_SUPERSEDED_SOURCE_PATHS.len() - ); } #[test] @@ -3866,308 +2896,45 @@ mod tests { } #[test] - fn generated_bundle_render_is_rerunnable_without_byte_drift() { + fn immutable_bundle_write_path_is_validation_only() { let root = workspace_root(); - let before = expected_artifacts(&root) - .expect("first in-memory generated bundle render") - .into_iter() - .map(|artifact| (artifact.relative, artifact.contents)) - .collect::<Vec<_>>(); - let after = expected_artifacts(&root) - .expect("second in-memory generated bundle render") - .into_iter() - .map(|artifact| (artifact.relative, artifact.contents)) - .collect::<Vec<_>>(); - assert_eq!(before, after); - } - - fn current_executable_authority_identities(root: &Path) -> Vec<ExecutableAuthorityIdentity> { - let mut identities = DELEGATED_AUTHORITIES + let before = IMMUTABLE_PREDECESSOR_ARTIFACTS .iter() - .chain(MANDATORY_BOUND_AUTHORITIES) - .map(|spec| ExecutableAuthorityIdentity { - path: spec.path.to_owned(), - test: spec.test.to_owned(), - tokens: validate_delegated_authority(root, *spec) - .expect("current delegated executable authority"), + .map(|artifact| { + ( + artifact.relative, + fs::read(root.join(artifact.relative)).expect("immutable artifact"), + ) }) .collect::<Vec<_>>(); - identities.extend(MANDATORY_BOUND_HELPER_AUTHORITIES.iter().map(|spec| { - ExecutableAuthorityIdentity { - path: spec.path.to_owned(), - test: spec.test.to_owned(), - tokens: validate_bound_function_authority(root, *spec) - .expect("current bound helper authority"), - } - })); - let source = rust_source(root, RESULT_VECTOR_EXECUTOR_RELATIVE) - .expect("direct result-vector executor source"); - let file = syn::parse_file(&source).expect("direct result-vector executor AST"); - let function = exact_free_function(&file, RESULT_VECTOR_EXECUTOR_TEST) - .expect("direct result-vector executor function"); - validate_executable_test_authority( - RESULT_VECTOR_EXECUTOR_RELATIVE, - RESULT_VECTOR_EXECUTOR_TEST, - function, - ) - .expect("current direct executable authority"); - identities.push(ExecutableAuthorityIdentity { - path: RESULT_VECTOR_EXECUTOR_RELATIVE.to_owned(), - test: RESULT_VECTOR_EXECUTOR_TEST.to_owned(), - tokens: compact_tokens(function), - }); - identities - } - - fn assert_bound_source_mutation_rejected( - baseline: &[BoundAuthoritySourceIdentity], - path: &str, - source: &str, - label: &str, - ) { - let mut identities = baseline.to_vec(); - let identity = identities - .iter_mut() - .find(|identity| identity.path == path) - .unwrap_or_else(|| panic!("missing bound source identity for {path}")); - let file = syn::parse_file(source) - .unwrap_or_else(|error| panic!("parse {label} mutation for {path}: {error}")); - let tokens = compact_tokens(&file); - assert_ne!(tokens, identity.tokens, "{label} fixture must mutate"); - identity.tokens = tokens; - let error = validate_bound_authority_source_identities(&identities) - .expect_err("bound authority source-context drift must fail closed"); - assert!( - error.contains("bound executor/test-module/helper source AST identity drifted"), - "unexpected {label} error: {error}" - ); - } - - #[test] - fn bound_executor_and_test_module_sources_are_exact_ast_authority() { - let root = workspace_root(); - let baseline = - bound_authority_source_identities(&root).expect("current bound source identities"); - validate_bound_authority_source_identities(&baseline) - .expect("current bound source aggregate identity"); - - let executor = rust_source(&root, RESULT_VECTOR_EXECUTOR_RELATIVE) - .expect("direct result-vector executor source"); - let crate_disabled = executor.replacen( - "#![forbid(unsafe_code)]", - "#![forbid(unsafe_code)]\n#![cfg(any())]", - 1, - ); - assert_bound_source_mutation_rejected( - &baseline, - RESULT_VECTOR_EXECUTOR_RELATIVE, - &crate_disabled, - "crate-disabled direct executor", - ); - - let delegated_path = "crates/event_store/src/source_maintenance_v1.rs"; - let delegated = - rust_source(&root, delegated_path).expect("delegated authority module source"); - let module_disabled = delegated.replacen( - "#[cfg(test)]\nmod tests {", - "#[cfg(all(test, any()))]\nmod tests {", - 1, - ); - assert_bound_source_mutation_rejected( - &baseline, - delegated_path, - &module_disabled, - "disabled delegated test module", - ); - - let macro_shadowed = executor.replacen( - "#![forbid(unsafe_code)]", - "#![forbid(unsafe_code)]\nmacro_rules! assert_eq { ($($tokens:tt)*) => {}; }", - 1, - ); - assert_bound_source_mutation_rejected( - &baseline, - RESULT_VECTOR_EXECUTOR_RELATIVE, - &macro_shadowed, - "shadowing direct-executor assertion macro", - ); - } - - #[test] - fn executable_authority_rejects_disabled_missing_and_unreachable_tests() { - let root = workspace_root(); - let source = rust_source(&root, RESULT_VECTOR_EXECUTOR_RELATIVE) - .expect("direct result-vector executor source"); - for (label, mutation) in [ - ( - "ignored direct executor", - source.replacen("#[tokio::test]", "#[ignore]\n#[tokio::test]", 1), - ), - ( - "missing direct executor attribute", - source.replacen("#[tokio::test]\n", "", 1), - ), - ( - "early-returning direct executor", - source.replacen( - "async fn source_maintenance_v1_result_vector() {", - "async fn source_maintenance_v1_result_vector() {\n return;", - 1, - ), - ), - ] { - assert_ne!(mutation, source, "{label} fixture must mutate"); - let file = syn::parse_file(&mutation).expect("mutated direct executor AST"); - let function = exact_free_function(&file, RESULT_VECTOR_EXECUTOR_TEST) - .expect("mutated direct executor function"); - let error = validate_executable_test_authority( - RESULT_VECTOR_EXECUTOR_RELATIVE, - RESULT_VECTOR_EXECUTOR_TEST, - function, - ) - .expect_err("disabled or early-returning direct executor must fail closed"); - assert!( - error.contains("executable test authority"), - "unexpected {label} error: {error}" + write_source_maintenance_manifest(&root) + .expect("valid frozen predecessor remains accepted"); + for (relative, bytes) in &before { + assert_eq!( + fs::read(root.join(relative)).expect("immutable artifact after validation"), + *bytes, + "validation-only write path mutated {relative}" ); } - let mut identities = current_executable_authority_identities(&root); - validate_executable_authority_identities(&identities) - .expect("current aggregate executable identity"); - let direct = identities - .last_mut() - .expect("direct executable identity is terminal"); - let file = syn::parse_file(&source).expect("direct executor AST"); - let function = exact_free_function(&file, RESULT_VECTOR_EXECUTOR_TEST) - .expect("direct executor function"); - let mut function = function.clone(); - let body = function.block.clone(); - *function.block = syn::parse_quote!({ if false #body; }); - direct.tokens = compact_tokens(&function); - let error = validate_executable_authority_identities(&identities) - .expect_err("non-returning unreachable test body must fail exact AST authority"); - assert!(error.contains("executable direct/delegated authority AST identity drifted")); - - let mut identities = current_executable_authority_identities(&root); - let helper_spec = MANDATORY_BOUND_HELPER_AUTHORITIES[0]; - let helper_source = rust_source(&root, helper_spec.path).expect("bound helper source"); - let helper_file = syn::parse_file(&helper_source).expect("bound helper AST"); - let helper = exact_free_function(&helper_file, helper_spec.test).expect("bound helper"); - let mut bypass = helper.clone(); - let body = bypass.block.clone(); - *bypass.block = syn::parse_quote!({ if false #body; }); - let identity = identities - .iter_mut() - .find(|identity| identity.path == helper_spec.path && identity.test == helper_spec.test) - .expect("bound helper identity"); - identity.tokens = compact_tokens(&bypass); - validate_executable_authority_identities(&identities) - .expect_err("unreachable bound helper body must fail exact AST authority"); - } - - #[test] - fn command_and_release_validation_reachability_is_exact() { - let root = workspace_root(); - let contract = - rust_source(&root, CONTRACT_COMMAND_SOURCE_RELATIVE).expect("contract command source"); - let main = rust_source(&root, XTASK_MAIN_SOURCE_RELATIVE).expect("xtask main source"); - validate_contract_command_reachability_sources(&contract, &main) - .expect("current aggregate and release validation reachability"); - - let mutations = [ - ( - "aggregate removal", - contract.replacen( - " validate_source_maintenance_manifest(workspace_root)?;\n", - "", - 1, - ), - main.clone(), - ), - ( - "aggregate discarded result", - contract.replacen( - " validate_source_maintenance_manifest(workspace_root)?;", - " let _ = validate_source_maintenance_manifest(workspace_root);", - 1, - ), - main.clone(), - ), - ( - "aggregate reordering", - contract.replacen( - " validate_food_availability_projection_manifest(workspace_root)?;\n validate_source_maintenance_manifest(workspace_root)?;", - " validate_source_maintenance_manifest(workspace_root)?;\n validate_food_availability_projection_manifest(workspace_root)?;", - 1, - ), - main.clone(), - ), - ( - "contract validation removal", - contract.clone(), - main.replacen( - " .and_then(|_| contract::validate_artifact_contracts(&root))", - " .map(|_| ())", - 1, - ), - ), - ( - "contract validate dispatch bypass", - contract.clone(), - main.replacen( - " Some(\"validate\") => validate_contract(),", - " Some(\"validate\") => Ok(()),", - 1, - ), - ), - ( - "release preflight dispatch bypass", - contract.clone(), - main.replacen( - " Some(\"preflight\") => release_preflight(),", - " Some(\"preflight\") => Ok(()),", - 1, - ), - ), - ( - "release validation removal", - contract.clone(), - main.replacen(" contract::validate_artifact_contracts(root)?;\n", "", 1), - ), - ( - "release validation discarded result", - contract.clone(), - main.replacen( - " contract::validate_artifact_contracts(root)?;", - " let _ = contract::validate_artifact_contracts(root);", - 1, - ), - ), - ( - "release validation reordering", - contract.clone(), - main.replacen( - " dto_roots::check(root)?;\n contract::validate_artifact_contracts(root)?;", - " contract::validate_artifact_contracts(root)?;\n dto_roots::check(root)?;", - 1, - ), - ), - ]; - for (label, contract_mutation, main_mutation) in mutations { - assert!( - contract_mutation != contract || main_mutation != main, - "{label} fixture must mutate" - ); - let error = - validate_contract_command_reachability_sources(&contract_mutation, &main_mutation) - .expect_err("validation reachability drift must fail closed"); - assert!( - error.contains("validation call-path authority drifted") - || error.contains("full dispatch AST authority drifted"), - "unexpected {label} error: {error}" - ); + let tampered = tempfile::tempdir().expect("tampered workspace"); + for (relative, bytes) in &before { + let path = tampered.path().join(relative); + fs::create_dir_all(path.parent().expect("artifact parent")) + .expect("create artifact parent"); + fs::write(path, bytes).expect("copy immutable artifact"); } + let manifest = tampered.path().join(MANIFEST_RELATIVE); + let mut tampered_manifest = fs::read(&manifest).expect("tampered manifest source"); + tampered_manifest.push(b'\n'); + fs::write(&manifest, &tampered_manifest).expect("tamper manifest"); + write_source_maintenance_manifest(tampered.path()) + .expect_err("tampered frozen predecessor must be rejected"); + assert_eq!( + fs::read(manifest).expect("tampered manifest after validation"), + tampered_manifest, + "rejected validation-only write path must not rewrite a tampered bundle" + ); } #[test] diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -21,6 +21,7 @@ fn usage() { eprintln!(" cargo xtask contract nip09-reconciliation-manifest [--write]"); eprintln!(" cargo xtask contract food-availability-projection-manifest [--write]"); eprintln!(" cargo xtask contract source-maintenance-manifest [--write]"); + eprintln!(" cargo xtask contract raw-source-rebuild-manifest [--write]"); eprintln!(" cargo xtask contract knowledge-manifest [--write]"); eprintln!(" cargo xtask dto-roots --check|--write"); eprintln!(" cargo xtask release preflight"); @@ -128,6 +129,15 @@ fn run_contract(args: &[String]) -> Result<(), String> { "source-maintenance-manifest accepts no arguments or exactly --write".to_string(), ), }, + Some("raw-source-rebuild-manifest") => match &args[1..] { + [] => contract::validate_raw_source_rebuild_manifest(&workspace_root()), + [flag] if flag == "--write" => { + contract::write_raw_source_rebuild_manifest(&workspace_root()) + } + _ => Err( + "raw-source-rebuild-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()) @@ -248,6 +258,12 @@ mod tests { ]) .expect_err("invalid SourceMaintenance manifest mode"); assert!(invalid_source_maintenance.contains("exactly --write")); + let invalid_raw_source_rebuild = run_contract(&[ + "raw-source-rebuild-manifest".to_string(), + "--invalid".to_string(), + ]) + .expect_err("invalid raw-source rebuild manifest mode"); + assert!(invalid_raw_source_rebuild.contains("exactly --write")); let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command"); assert!(unknown_root.contains("unknown command")); @@ -347,6 +363,8 @@ mod tests { .expect("contract FoodAvailability projection manifest"); run_contract(&["source-maintenance-manifest".to_string()]) .expect("contract SourceMaintenance manifest"); + run_contract(&["raw-source-rebuild-manifest".to_string()]) + .expect("contract raw-source rebuild manifest"); run_contract(&["knowledge-manifest".to_string()]).expect("contract knowledge manifest"); } }