commit ab24efd2d4887ca0f06118642776f43e3edfe626
parent 5e91441795b5a838631af610a42d0ae367045bea
Author: triesap <tyson@radroots.org>
Date: Wed, 22 Jul 2026 02:54:46 +0000
outbox: add versioned migration authority
- freeze the existing outbox migration and authenticate its exact catalog
- adopt only exact unledgered baselines under serialized writer authority
- replace unrestricted destruction with versioned terminal rollback
- bind runtime behavior to generated contracts and executable conformance
Diffstat:
25 files changed, 4877 insertions(+), 72 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -9,6 +9,20 @@ publish policy both pass for the same source revision.
### Changed
+<!-- release-change: outbox-versioned-migration-authority -->
+- Outbox schema initialization now uses an ordered, checksummed migration
+ registry and an exact authenticated catalog fingerprint. Existing
+ unledgered databases are adopted only when all five baseline tables and
+ eight indexes match the frozen `0001_outbox` identity; partial, changed,
+ counterfeit, gapped, newer, or otherwise unknown `outbox_*` state fails
+ before governed schema or history mutation while unrelated caller tables
+ remain untouched. Every open serializes under SQLite writer authority and
+ validates UTF-8 encoding, foreign keys, file WAL mode, bounded catalog and
+ history reads, integrity, and the tamper-evident ledger. Raw migration SQL
+ exports and unrestricted `migrate_down` were replaced by schema status,
+ migrate-to-current, and a terminal explicit-target rollback with a public
+ version floor. Complete destruction is available only through a separately
+ named migration-test helper.
- Event-store schema initialization now uses a transactional, checksummed
migration authority with exact legacy-baseline adoption, shared-database
catalog scoping, tamper-evident fail-closed managed history, exact catalog
diff --git a/build/nix/common.nix b/build/nix/common.nix
@@ -112,6 +112,7 @@ let
"radroots_nostr"
"radroots_nostr_connect"
"radroots_nostr_signer"
+ "radroots_outbox"
];
coreContractCargoArgs =
lib.concatStringsSep " " (map (crate: "-p ${crate}") coreContractCrates)
diff --git a/contracts/conformance/vectors/outbox/migration_authority.v1.json b/contracts/conformance/vectors/outbox/migration_authority.v1.json
@@ -0,0 +1,105 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_outbox.migration_authority.v1",
+ "executor": {
+ "id": "radroots_outbox.migration_authority_v1.result_vector_executor.v1",
+ "path": "crates/outbox/tests/migration_authority_v1_result_vector.rs",
+ "test": "migration_authority_v1_result_vector"
+ },
+ "delegated_suite": {
+ "lane": "nix run .#contract",
+ "package": "radroots_outbox",
+ "authorities": [
+ {
+ "authority": "migration_source_discovery_is_exact_and_fail_closed",
+ "authority_path": "crates/outbox/src/migrations.rs"
+ },
+ {
+ "authority": "ledger_name_checksum_and_catalog_mutations_fail_closed",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "newer_history_and_governed_catalog_overflow_are_bounded_and_rejected",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "history_validation_rejects_gaps_and_unknown_versions",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "temporary_authority_collisions_are_rejected_before_migration",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "concurrent_file_initializers_serialize_to_one_exact_history",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "terminal_target_rollback_preserves_v1_rows_and_closes_every_clone",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "reopen_rejects_outbox_foreign_key_corruption",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "open_rejects_utf16_before_journal_or_schema_mutation",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "file_pool_rejects_successful_non_wal_journal_result",
+ "authority_path": "crates/outbox/src/store.rs"
+ }
+ ]
+ },
+ "cases": [
+ {
+ "id": "fresh_initialization",
+ "execution": "direct_executor",
+ "expected_outcome": "managed_v1",
+ "expected_error": null
+ },
+ {
+ "id": "exact_unledgered_adoption",
+ "execution": "direct_executor",
+ "expected_outcome": "managed_v1_without_replay",
+ "expected_error": null
+ },
+ {
+ "id": "partial_unledgered_rejected",
+ "execution": "direct_executor",
+ "expected_outcome": "rejected_before_mutation",
+ "expected_error": "UnmanagedSchema"
+ },
+ {
+ "id": "ledger_checksum_tamper_rejected",
+ "execution": "direct_executor",
+ "expected_outcome": "rejected_before_mutation",
+ "expected_error": "MigrationHistoryChecksumDrift"
+ },
+ {
+ "id": "newer_history_rejected",
+ "execution": "direct_executor",
+ "expected_outcome": "rejected_before_mutation",
+ "expected_error": "SchemaTooNew"
+ },
+ {
+ "id": "rollback_below_floor_rejected",
+ "execution": "direct_executor",
+ "expected_outcome": "rejected_without_schema_change",
+ "expected_error": "RollbackBelowVersionFloor"
+ },
+ {
+ "id": "caller_state_preserved",
+ "execution": "direct_executor",
+ "expected_outcome": "managed_v1_with_caller_state_preserved",
+ "expected_error": null
+ },
+ {
+ "id": "current_reopen_no_history_write",
+ "execution": "direct_executor",
+ "expected_outcome": "managed_v1_without_history_write",
+ "expected_error": null
+ }
+ ]
+}
diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml
@@ -310,6 +310,21 @@ semver_impacts = ["add_enum_variant", "change_exported_algorithm_behavior"]
summary = "Reject every NIP-16 ephemeral event from all generic durable-outbox entry points, keep transient events inside their owning live transport exchanges, and validate and configure every externally supplied SQLite pool connection before migration or writes."
[[changes]]
+id = "outbox-versioned-migration-authority"
+classification = "breaking"
+semver_impacts = [
+ "add_exported_type",
+ "add_exported_function",
+ "add_exported_constant",
+ "add_enum_variant",
+ "add_conformance_vector",
+ "remove_exported_constant",
+ "remove_exported_function",
+ "change_exported_algorithm_behavior",
+]
+summary = "Replace raw outbox migration SQL and unrestricted destruction with a contiguous checksummed registry, exact unledgered-baseline adoption, a tamper-evident ledger and catalog fingerprint, bounded fail-closed open validation, serialized migration, explicit target rollback with a version floor, test-only destruction, and an executable conformance contract while freezing the 0001 migration bytes."
+
+[[changes]]
id = "shared-read-only-owner-boundaries"
classification = "feature"
semver_impacts = ["add_exported_function"]
diff --git a/crates/event_store/contracts/raw_source_rebuild_v1.manifest.json b/crates/event_store/contracts/raw_source_rebuild_v1.manifest.json
@@ -877,8 +877,8 @@
{
"role": "nix_contract_test_lane_authority",
"path": "build/nix/common.nix",
- "byte_length": 11127,
- "sha256": "b3340e1b4973e6a1e02899d164ca74842757f22b6b1a03f90461532fcd844df5",
+ "byte_length": 11149,
+ "sha256": "738003cb1703baaf73a97c010c84731a42230782661c79c6a152fbb9e6fa9f6e",
"hash_algorithm": "sha256_bytes_v1"
},
{
@@ -1193,35 +1193,35 @@
"role": "raw_source_rebuild_governance",
"path": "tools/xtask/src/contract/raw_source_rebuild.rs",
"byte_length": 294540,
- "sha256": "543c21131346381a833f9e063fd535efd0d0e192419aefa1f60e9b0f68475866",
+ "sha256": "3f0df95aa892eda6139f1251b3522e26bfc4a617af18386fc76c98c696a6d3f7",
"hash_algorithm": "sha256_bytes_v1"
},
{
"role": "contract_command_authority",
"path": "tools/xtask/src/contract.rs",
- "byte_length": 479703,
- "sha256": "72fbd457b0cfdff1e30f07bf2452a0c71c23cef93bdaefffc114defa616d6342",
+ "byte_length": 480209,
+ "sha256": "b3a7c9486c2c2cc904d3877be7e7e6a48ef1e00445f07b9884b6778dbc55e416",
"hash_algorithm": "sha256_bytes_v1"
},
{
"role": "xtask_dispatch_and_release_preflight",
"path": "tools/xtask/src/main.rs",
- "byte_length": 15018,
- "sha256": "9aab8db1186b776ba8dcac90cc46c29d96928b610850b084be0e3a25d3e4cb0f",
+ "byte_length": 16291,
+ "sha256": "326c64082e406e278b097ae88131534b7aad283e3135aa6f1302f967d021ed3f",
"hash_algorithm": "sha256_bytes_v1"
},
{
"role": "release_breaking_change_authority",
"path": "contracts/releases/1.0.0-alpha.1.toml",
- "byte_length": 19840,
- "sha256": "946d90dacc9db522825898dbb5d9d424b020a22fe53b80ec15eb67c5f1d8cd2d",
+ "byte_length": 20581,
+ "sha256": "c7765df4fc7e217fbf6b30d5b0dd2902dbe276f14b2cb2dd34c9fb89c04749c8",
"hash_algorithm": "sha256_bytes_v1"
},
{
"role": "release_note_authority",
"path": "CHANGELOG.md",
- "byte_length": 28939,
- "sha256": "61b9d2a9050e4123bc5324aebf6e54393b9b1efdc2145a4a2cf6c009a4345b23",
+ "byte_length": 29929,
+ "sha256": "e6b645684a8ee0f48e4212ec99eb38142b2aeff02d35edbcbf79efb9030ec855",
"hash_algorithm": "sha256_bytes_v1"
}
],
diff --git a/crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256 b/crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256
@@ -1 +1 @@
-b8737a9c5836517114e7df6c2194c46e3c200093e12c4e6297165d2b9dae56a1
+b44b379b91f586e94ca2c3c581f07cef0a20d2279fbd2c687916390928fa79ba
diff --git a/crates/event_store/src/generated/raw_source_rebuild_manifest.rs b/crates/event_store/src/generated/raw_source_rebuild_manifest.rs
@@ -1,10 +1,10 @@
// @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_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\": 11149,\n \"sha256\": \"738003cb1703baaf73a97c010c84731a42230782661c79c6a152fbb9e6fa9f6e\",\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\": \"3f0df95aa892eda6139f1251b3522e26bfc4a617af18386fc76c98c696a6d3f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 480209,\n \"sha256\": \"b3a7c9486c2c2cc904d3877be7e7e6a48ef1e00445f07b9884b6778dbc55e416\",\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\": 16291,\n \"sha256\": \"326c64082e406e278b097ae88131534b7aad283e3135aa6f1302f967d021ed3f\",\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\": 20581,\n \"sha256\": \"c7765df4fc7e217fbf6b30d5b0dd2902dbe276f14b2cb2dd34c9fb89c04749c8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"release_note_authority\",\n \"path\": \"CHANGELOG.md\",\n \"byte_length\": 29929,\n \"sha256\": \"e6b645684a8ee0f48e4212ec99eb38142b2aeff02d35edbcbf79efb9030ec855\",\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";
+ "b44b379b91f586e94ca2c3c581f07cef0a20d2279fbd2c687916390928fa79ba";
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";
diff --git a/crates/outbox/Cargo.toml b/crates/outbox/Cargo.toml
@@ -14,7 +14,7 @@ readme = "README"
[features]
default = ["sqlite", "runtime-tokio"]
sqlite = ["dep:sqlx", "sqlx/sqlite-bundled"]
-runtime-tokio = ["sqlx/runtime-tokio"]
+runtime-tokio = ["dep:tokio", "sqlx/runtime-tokio"]
[dependencies]
radroots_event = { workspace = true, default-features = false, features = [
@@ -32,6 +32,7 @@ serde_json = { workspace = true, features = ["std"] }
sha2 = { workspace = true }
sqlx = { workspace = true, optional = true, features = ["derive"] }
thiserror = { workspace = true }
+tokio = { workspace = true, optional = true, features = ["time"] }
[dev-dependencies]
radroots_nostr = { workspace = true, default-features = false, features = [
@@ -39,7 +40,7 @@ radroots_nostr = { workspace = true, default-features = false, features = [
"events",
] }
tempfile = { workspace = true }
-tokio = { workspace = true, features = ["macros", "rt"] }
+tokio = { workspace = true, features = ["macros", "rt", "sync"] }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
diff --git a/crates/outbox/README b/crates/outbox/README
@@ -13,3 +13,25 @@ unambiguous for every queued event.
multi-connection in-memory pools in every supported URL form, and configures
every file-pool connection with foreign-key enforcement and the required busy
timeout before migrations or writes.
+
+Schema lifecycle is governed by an ordered, checksummed migration registry.
+The original `0001_outbox` up and down files are immutable contract inputs. An
+existing unledgered database is adopted only when its complete managed catalog
+matches the authenticated five-table, eight-index baseline fingerprint. The
+managed ledger pins migration names, source digests, and catalog fingerprints;
+unknown or drifted `outbox_*` objects, counterfeit ledgers, history gaps, and
+newer schema versions fail before governed schema or history mutation. Catalog
+and history reads are bounded, while unrelated caller tables in a shared SQLite
+database are outside the outbox fingerprint and remain untouched. Registry
+entries add and remove disjoint governed object sets; altering or replacing an
+existing governed object requires an explicit migration-contract revision.
+
+Every open validates UTF-8 before journal or schema mutation, enables and
+verifies foreign keys, verifies WAL for file databases, takes a serialized
+writer transaction for migration/adoption, and runs SQLite integrity plus
+outbox-scoped foreign-key checks before exposing the store. Use
+`schema_status` for inspection and `migrate_to_current_schema` for an explicit
+recheck. `rollback_to_schema_version_and_close` consumes the store, closes all
+pool clones, and requires an explicit target at or above the public schema
+floor. Full schema destruction is intentionally confined to the separately
+named migration-test helper.
diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.json b/crates/outbox/contracts/migration_authority_v1.manifest.json
@@ -0,0 +1,298 @@
+{
+ "authority_id": "versioned_outbox_migration_authority_v1",
+ "contract_id": "radroots_outbox.migration_authority.v1",
+ "manifest_schema": {
+ "byte_length": 8336,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/contracts/migration_authority_v1.manifest.schema.json",
+ "sha256": "2af281adfdc9b6d5dc16486db5a56e6e737dbfd9645adb486b28051e79c02f59"
+ },
+ "migrations": [
+ {
+ "catalog": {
+ "indexes": [
+ "outbox_delivery_attempt_target_idx",
+ "outbox_delivery_plan_event_idx",
+ "outbox_delivery_target_ready_idx",
+ "outbox_event_event_id_idx",
+ "outbox_event_ready_idx",
+ "outbox_operation_idempotency_idx",
+ "outbox_operation_status_idx",
+ "outbox_operation_trade_mutation_idx"
+ ],
+ "objects": [
+ "outbox_delivery_attempt",
+ "outbox_delivery_attempt_target_idx",
+ "outbox_delivery_plan",
+ "outbox_delivery_plan_event_idx",
+ "outbox_delivery_target",
+ "outbox_delivery_target_ready_idx",
+ "outbox_event",
+ "outbox_event_event_id_idx",
+ "outbox_event_ready_idx",
+ "outbox_operation_idempotency_idx",
+ "outbox_operation_status_idx",
+ "outbox_operation_trade_mutation_idx",
+ "outbox_operations"
+ ],
+ "tables": [
+ "outbox_delivery_attempt",
+ "outbox_delivery_plan",
+ "outbox_delivery_target",
+ "outbox_event",
+ "outbox_operations"
+ ]
+ },
+ "down": {
+ "byte_length": 159,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/migrations/0001_outbox.down.sql",
+ "sha256": "5d56f978f9172dc5ecbc5043a6c286c75926974d8a2a9e44fffa7c134829af61"
+ },
+ "name": "outbox",
+ "schema_sha256": "e7eeba00de78ec6d990c620e7c056018166e8a00bb703e472ef6f67a00870293",
+ "up": {
+ "byte_length": 5470,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/migrations/0001_outbox.up.sql",
+ "sha256": "a7ee775d32c2b9f845961425362e1b1e558ce0d025f7d22dd58f118ba4dab4fa"
+ },
+ "version": 1
+ }
+ ],
+ "public_api": {
+ "added_symbols": [
+ "RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT",
+ "RADROOTS_OUTBOX_SCHEMA_VERSION_MIN",
+ "RadrootsOutboxSchemaStatus",
+ "inspect_outbox_schema_status"
+ ],
+ "error_enum_non_exhaustive": true,
+ "error_variants": [
+ "EmbeddedMigrationChecksumMismatch",
+ "EmbeddedMigrationLengthMismatch",
+ "ForeignKeyViolation",
+ "GovernedCatalogCapacityExceeded",
+ "IntegrityCheckFailed",
+ "MigrationCatalogDeltaMismatch",
+ "MigrationHistoryChecksumDrift",
+ "MigrationHistoryGap",
+ "MigrationHistoryNameDrift",
+ "MigrationLedgerDrift",
+ "MigrationRegistryDefect",
+ "MigrationTransactionRollbackFailed",
+ "RollbackAhead",
+ "RollbackBelowVersionFloor",
+ "RollbackUnmanaged",
+ "SchemaFingerprintMismatch",
+ "SchemaTooNew",
+ "SqliteForeignKeysNotEnabled",
+ "SqliteMainDatabaseEncodingNotUtf8",
+ "SqliteMainDatabaseUnavailable",
+ "TemporarySchemaCollision",
+ "UnknownMigration",
+ "UnmanagedSchema"
+ ],
+ "methods": [
+ "RadrootsOutbox::migrate_to_current_schema",
+ "RadrootsOutbox::rollback_to_schema_version_and_close",
+ "RadrootsOutbox::schema_status"
+ ],
+ "removed_methods": [
+ "RadrootsOutbox::migrate_down"
+ ],
+ "removed_symbols": [
+ "OUTBOX_MIGRATION_DOWN",
+ "OUTBOX_MIGRATION_UP"
+ ]
+ },
+ "release": {
+ "change_id": "outbox-versioned-migration-authority",
+ "changelog": "CHANGELOG.md",
+ "release_record": "contracts/releases/1.0.0-alpha.1.toml"
+ },
+ "result_vector": {
+ "canonical": {
+ "byte_length": 3483,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "contracts/conformance/vectors/outbox/migration_authority.v1.json",
+ "sha256": "90b82ac034784871627f1b662682203ee3d621b652b866a8b4cee3ba9937444e"
+ },
+ "executor": {
+ "byte_length": 9491,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/tests/migration_authority_v1_result_vector.rs",
+ "sha256": "d86570baf8dfb9779eb238e851d4272e7404463de86bd7303cee799ca7eed1f3"
+ },
+ "executor_id": "radroots_outbox.migration_authority_v1.result_vector_executor.v1",
+ "executor_test": "migration_authority_v1_result_vector",
+ "mirror_path": "crates/outbox/tests/fixtures/migration_authority.v1.json"
+ },
+ "runtime": {
+ "adoption_policy": "exact_unledgered_0001_catalog_only_v1",
+ "catalog_fingerprint_algorithm": "sha256_type_nul_name_nul_table_name_nul_sql_nul_sorted_v1",
+ "catalog_rejection_probe_limit": 15,
+ "catalog_row_limit": 14,
+ "current_version": 1,
+ "history_rejection_probe_limit": 2,
+ "history_row_limit": 1,
+ "ledger_ddl_sha256": "a23a4f0325ec5338dd9240573bac42a164c2376bcdce3fb306b554da574f5973",
+ "ledger_name": "radroots_outbox_schema_migrations",
+ "migration_transaction": "begin_immediate_v1",
+ "minimum_version": 1,
+ "open_validations": [
+ "main_database_backing_identity",
+ "utf8_encoding_before_journal_or_schema_mutation",
+ "foreign_keys_enabled",
+ "file_wal_result",
+ "temporary_schema_authority",
+ "bounded_managed_catalog",
+ "tamper_evident_history",
+ "catalog_fingerprint",
+ "integrity_check_one",
+ "outbox_scoped_foreign_key_check"
+ ],
+ "reserved_prefix": "outbox_",
+ "rollback_floor": 1,
+ "rollback_transaction": "begin_exclusive_terminal_close_v1",
+ "test_destruction": "cfg_test_destroy_outbox_schema_for_migration_test"
+ },
+ "schema_version": 1,
+ "source_files": [
+ {
+ "file": {
+ "byte_length": 1408,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/Cargo.toml",
+ "sha256": "3846cb81a0638d2b3e2875073ba5b8262f63d46961dd5868ec4e287061f16d30"
+ },
+ "role": "outbox_package_manifest"
+ },
+ {
+ "file": {
+ "byte_length": 1188,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/src/lib.rs",
+ "sha256": "4e4ed499158216aed8654ef9fc239a6680636c0711826d657fb43d85e08a633b"
+ },
+ "role": "outbox_public_surface"
+ },
+ {
+ "file": {
+ "byte_length": 8631,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/src/error.rs",
+ "sha256": "43eb19df2e07b46fa969b8072df7cf901e562fd723714e44b2eedd356879024a"
+ },
+ "role": "outbox_error_surface"
+ },
+ {
+ "file": {
+ "byte_length": 67,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/src/generated.rs",
+ "sha256": "5a1f4d3257a07c88aef1c1b7b330ff78ada9cba8bcaa48405b4a86f37a7769fb"
+ },
+ "role": "generated_descriptor_registration"
+ },
+ {
+ "file": {
+ "byte_length": 25558,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/src/migrations.rs",
+ "sha256": "7fa22dfa26ffd8c5b6bc575ef6f193ffca8e883c019866f04a8125992d7e0ced"
+ },
+ "role": "outbox_migration_registry"
+ },
+ {
+ "file": {
+ "byte_length": 59368,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/src/schema.rs",
+ "sha256": "e6e467ca19e8b09bade67728d7025cb4be0fa87aa3bb88d5682c3182af6d3051"
+ },
+ "role": "outbox_schema_runtime"
+ },
+ {
+ "file": {
+ "byte_length": 332102,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/src/store.rs",
+ "sha256": "b271545c0bb5ae7121b4d7e2b9d01d07556729059dd1ce3e126f969940bc850f"
+ },
+ "role": "outbox_store_runtime"
+ },
+ {
+ "file": {
+ "byte_length": 2185,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/README",
+ "sha256": "1b55a3ff04c2c44b22dffee494e50711ce90089a56c51364d59ac9eb42beea7f"
+ },
+ "role": "outbox_package_readme"
+ },
+ {
+ "file": {
+ "byte_length": 9491,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "crates/outbox/tests/migration_authority_v1_result_vector.rs",
+ "sha256": "d86570baf8dfb9779eb238e851d4272e7404463de86bd7303cee799ca7eed1f3"
+ },
+ "role": "vector_executor"
+ },
+ {
+ "file": {
+ "byte_length": 41824,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "tools/xtask/src/contract/outbox_migration.rs",
+ "sha256": "62e1b2bc02c1a7c4c741c04020e21eb878918570d3ca9b6cb7008700c5f1a883"
+ },
+ "role": "contract_governance"
+ },
+ {
+ "file": {
+ "byte_length": 480209,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "tools/xtask/src/contract.rs",
+ "sha256": "b3a7c9486c2c2cc904d3877be7e7e6a48ef1e00445f07b9884b6778dbc55e416"
+ },
+ "role": "contract_dispatch"
+ },
+ {
+ "file": {
+ "byte_length": 16291,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "tools/xtask/src/main.rs",
+ "sha256": "326c64082e406e278b097ae88131534b7aad283e3135aa6f1302f967d021ed3f"
+ },
+ "role": "xtask_dispatch"
+ },
+ {
+ "file": {
+ "byte_length": 11149,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "build/nix/common.nix",
+ "sha256": "738003cb1703baaf73a97c010c84731a42230782661c79c6a152fbb9e6fa9f6e"
+ },
+ "role": "nix_contract_lane"
+ },
+ {
+ "file": {
+ "byte_length": 20581,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "contracts/releases/1.0.0-alpha.1.toml",
+ "sha256": "c7765df4fc7e217fbf6b30d5b0dd2902dbe276f14b2cb2dd34c9fb89c04749c8"
+ },
+ "role": "release_record"
+ },
+ {
+ "file": {
+ "byte_length": 29929,
+ "hash_algorithm": "sha256_bytes_v1",
+ "path": "CHANGELOG.md",
+ "sha256": "e6b645684a8ee0f48e4212ec99eb38142b2aeff02d35edbcbf79efb9030ec855"
+ },
+ "role": "release_notes"
+ }
+ ]
+}
diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.schema.json b/crates/outbox/contracts/migration_authority_v1.manifest.schema.json
@@ -0,0 +1,352 @@
+{
+ "$defs": {
+ "file": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "path": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "sha256": {
+ "$ref": "#/$defs/sha256"
+ }
+ },
+ "required": [
+ "path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm"
+ ],
+ "type": "object"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "$id": "https://radroots.org/contracts/outbox/migration_authority_v1.manifest.schema.json",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "properties": {
+ "authority_id": {
+ "const": "versioned_outbox_migration_authority_v1"
+ },
+ "contract_id": {
+ "const": "radroots_outbox.migration_authority.v1"
+ },
+ "manifest_schema": {
+ "$ref": "#/$defs/file"
+ },
+ "migrations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "catalog": {
+ "additionalProperties": false,
+ "properties": {
+ "indexes": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array",
+ "uniqueItems": true
+ },
+ "objects": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 13,
+ "minItems": 13,
+ "type": "array",
+ "uniqueItems": true
+ },
+ "tables": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 5,
+ "minItems": 5,
+ "type": "array",
+ "uniqueItems": true
+ }
+ },
+ "required": [
+ "objects",
+ "tables",
+ "indexes"
+ ],
+ "type": "object"
+ },
+ "down": {
+ "$ref": "#/$defs/file"
+ },
+ "name": {
+ "const": "outbox"
+ },
+ "schema_sha256": {
+ "$ref": "#/$defs/sha256"
+ },
+ "up": {
+ "$ref": "#/$defs/file"
+ },
+ "version": {
+ "const": 1
+ }
+ },
+ "required": [
+ "version",
+ "name",
+ "up",
+ "down",
+ "schema_sha256",
+ "catalog"
+ ],
+ "type": "object"
+ },
+ "maxItems": 1,
+ "minItems": 1,
+ "type": "array"
+ },
+ "public_api": {
+ "additionalProperties": false,
+ "properties": {
+ "added_symbols": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "maxItems": 4,
+ "minItems": 4,
+ "type": "array",
+ "uniqueItems": true
+ },
+ "error_enum_non_exhaustive": {
+ "const": true
+ },
+ "error_variants": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "maxItems": 23,
+ "minItems": 23,
+ "type": "array",
+ "uniqueItems": true
+ },
+ "methods": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 3,
+ "type": "array",
+ "uniqueItems": true
+ },
+ "removed_methods": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "maxItems": 1,
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ },
+ "removed_symbols": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "maxItems": 2,
+ "minItems": 2,
+ "type": "array",
+ "uniqueItems": true
+ }
+ },
+ "required": [
+ "added_symbols",
+ "methods",
+ "removed_symbols",
+ "removed_methods",
+ "error_variants",
+ "error_enum_non_exhaustive"
+ ],
+ "type": "object"
+ },
+ "release": {
+ "additionalProperties": false,
+ "properties": {
+ "change_id": {
+ "const": "outbox-versioned-migration-authority"
+ },
+ "changelog": {
+ "const": "CHANGELOG.md"
+ },
+ "release_record": {
+ "const": "contracts/releases/1.0.0-alpha.1.toml"
+ }
+ },
+ "required": [
+ "change_id",
+ "release_record",
+ "changelog"
+ ],
+ "type": "object"
+ },
+ "result_vector": {
+ "additionalProperties": false,
+ "properties": {
+ "canonical": {
+ "$ref": "#/$defs/file"
+ },
+ "executor": {
+ "$ref": "#/$defs/file"
+ },
+ "executor_id": {
+ "const": "radroots_outbox.migration_authority_v1.result_vector_executor.v1"
+ },
+ "executor_test": {
+ "const": "migration_authority_v1_result_vector"
+ },
+ "mirror_path": {
+ "const": "crates/outbox/tests/fixtures/migration_authority.v1.json"
+ }
+ },
+ "required": [
+ "canonical",
+ "mirror_path",
+ "executor",
+ "executor_id",
+ "executor_test"
+ ],
+ "type": "object"
+ },
+ "runtime": {
+ "additionalProperties": false,
+ "properties": {
+ "adoption_policy": {
+ "const": "exact_unledgered_0001_catalog_only_v1"
+ },
+ "catalog_fingerprint_algorithm": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "catalog_rejection_probe_limit": {
+ "const": 15
+ },
+ "catalog_row_limit": {
+ "const": 14
+ },
+ "current_version": {
+ "const": 1
+ },
+ "history_rejection_probe_limit": {
+ "const": 2
+ },
+ "history_row_limit": {
+ "const": 1
+ },
+ "ledger_ddl_sha256": {
+ "$ref": "#/$defs/sha256"
+ },
+ "ledger_name": {
+ "const": "radroots_outbox_schema_migrations"
+ },
+ "migration_transaction": {
+ "const": "begin_immediate_v1"
+ },
+ "minimum_version": {
+ "const": 1
+ },
+ "open_validations": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "minItems": 10,
+ "type": "array",
+ "uniqueItems": true
+ },
+ "reserved_prefix": {
+ "const": "outbox_"
+ },
+ "rollback_floor": {
+ "const": 1
+ },
+ "rollback_transaction": {
+ "const": "begin_exclusive_terminal_close_v1"
+ },
+ "test_destruction": {
+ "const": "cfg_test_destroy_outbox_schema_for_migration_test"
+ }
+ },
+ "required": [
+ "minimum_version",
+ "current_version",
+ "reserved_prefix",
+ "ledger_name",
+ "ledger_ddl_sha256",
+ "catalog_fingerprint_algorithm",
+ "migration_transaction",
+ "rollback_transaction",
+ "catalog_row_limit",
+ "catalog_rejection_probe_limit",
+ "history_row_limit",
+ "history_rejection_probe_limit",
+ "open_validations",
+ "adoption_policy",
+ "rollback_floor",
+ "test_destruction"
+ ],
+ "type": "object"
+ },
+ "schema_version": {
+ "const": 1
+ },
+ "source_files": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "file": {
+ "$ref": "#/$defs/file"
+ },
+ "role": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "required": [
+ "role",
+ "file"
+ ],
+ "type": "object"
+ },
+ "maxItems": 15,
+ "minItems": 15,
+ "type": "array"
+ }
+ },
+ "required": [
+ "schema_version",
+ "contract_id",
+ "authority_id",
+ "manifest_schema",
+ "runtime",
+ "migrations",
+ "public_api",
+ "source_files",
+ "result_vector",
+ "release"
+ ],
+ "type": "object"
+}
diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.sha256 b/crates/outbox/contracts/migration_authority_v1.manifest.sha256
@@ -0,0 +1 @@
+1fb495009fd258380a02c0f5714f7280f832220aec251a7a29a5478ce64b7730
diff --git a/crates/outbox/src/error.rs b/crates/outbox/src/error.rs
@@ -4,6 +4,7 @@ use radroots_transport::RadrootsTransportError;
use thiserror::Error;
#[derive(Debug, Error)]
+#[non_exhaustive]
pub enum RadrootsOutboxError {
#[error("SQLx error: {0}")]
Sqlx(#[from] sqlx::Error),
@@ -46,6 +47,129 @@ pub enum RadrootsOutboxError {
#[error("SQLite outbox file connection did not enter WAL journal mode; reported `{actual}`")]
SqliteFileJournalModeNotWal { actual: String },
+ #[error("SQLite outbox connection has no main database")]
+ SqliteMainDatabaseUnavailable,
+
+ #[error("SQLite outbox main database must use UTF-8 encoding; reported `{actual}`")]
+ SqliteMainDatabaseEncodingNotUtf8 { actual: String },
+
+ #[error("SQLite outbox connection must enforce foreign keys; reported {actual}")]
+ SqliteForeignKeysNotEnabled { actual: i64 },
+
+ #[error(
+ "temporary schema object `{name}` ({object_type}, table `{table_name}`) collides with outbox authority"
+ )]
+ TemporarySchemaCollision {
+ object_type: String,
+ name: String,
+ table_name: String,
+ },
+
+ #[error("outbox migration registry defect: {reason}")]
+ MigrationRegistryDefect { reason: String },
+
+ #[error(
+ "embedded outbox migration {version} {direction} length mismatch: expected {expected}, found {actual}"
+ )]
+ EmbeddedMigrationLengthMismatch {
+ version: u32,
+ direction: &'static str,
+ expected: usize,
+ actual: usize,
+ },
+
+ #[error(
+ "embedded outbox migration {version} {direction} checksum mismatch: expected {expected}, found {actual}"
+ )]
+ EmbeddedMigrationChecksumMismatch {
+ version: u32,
+ direction: &'static str,
+ expected: &'static str,
+ actual: String,
+ },
+
+ #[error("outbox migration {version} {direction} catalog delta mismatch: {reason}")]
+ MigrationCatalogDeltaMismatch {
+ version: u32,
+ direction: &'static str,
+ reason: String,
+ },
+
+ #[error("outbox governed catalog exceeds the supported {max} rows")]
+ GovernedCatalogCapacityExceeded { max: usize },
+
+ #[error("unmanaged outbox schema has fingerprint {actual_schema_sha256}")]
+ UnmanagedSchema { actual_schema_sha256: String },
+
+ #[error("outbox migration ledger catalog is invalid: {reason}")]
+ MigrationLedgerDrift { reason: String },
+
+ #[error("outbox migration history gap: expected version {expected}, found {actual:?}")]
+ MigrationHistoryGap { expected: u32, actual: Option<u32> },
+
+ #[error("outbox migration history references unknown version {version}")]
+ UnknownMigration { version: u32 },
+
+ #[error("outbox schema version {database} is newer than supported version {current}")]
+ SchemaTooNew { current: u32, database: i64 },
+
+ #[error("outbox migration {version} name drift: expected `{expected}`, found `{actual}`")]
+ MigrationHistoryNameDrift {
+ version: u32,
+ expected: &'static str,
+ actual: String,
+ },
+
+ #[error(
+ "outbox migration {version} {field} checksum drift: expected {expected}, found {actual}"
+ )]
+ MigrationHistoryChecksumDrift {
+ version: u32,
+ field: &'static str,
+ expected: &'static str,
+ actual: String,
+ },
+
+ #[error(
+ "outbox schema fingerprint mismatch at version {version}: expected {expected}, found {actual}"
+ )]
+ SchemaFingerprintMismatch {
+ version: u32,
+ expected: &'static str,
+ actual: String,
+ },
+
+ #[error("outbox rollback target {target} is below the supported version floor {floor}")]
+ RollbackBelowVersionFloor { floor: u32, target: u32 },
+
+ #[error("outbox rollback target {target} is ahead of managed version {current}")]
+ RollbackAhead { current: u32, target: u32 },
+
+ #[error("outbox rollback requires a managed schema")]
+ RollbackUnmanaged,
+
+ #[error(
+ "outbox schema operation failed: {primary}; transaction rollback also failed: {rollback}"
+ )]
+ MigrationTransactionRollbackFailed {
+ #[source]
+ primary: Box<RadrootsOutboxError>,
+ rollback: sqlx::Error,
+ },
+
+ #[error("outbox SQLite integrity check failed: {detail}")]
+ IntegrityCheckFailed { detail: String },
+
+ #[error(
+ "outbox foreign-key violation in `{table}` row {rowid:?} against `{parent}` constraint {foreign_key_id}"
+ )]
+ ForeignKeyViolation {
+ table: String,
+ rowid: Option<i64>,
+ parent: String,
+ foreign_key_id: i64,
+ },
+
#[error(
"trade mutation outbox metadata does not match the canonical mutation content: {field}"
)]
diff --git a/crates/outbox/src/generated.rs b/crates/outbox/src/generated.rs
@@ -0,0 +1,3 @@
+#![forbid(unsafe_code)]
+
+pub(crate) mod outbox_migration_manifest;
diff --git a/crates/outbox/src/generated/outbox_migration_manifest.rs b/crates/outbox/src/generated/outbox_migration_manifest.rs
@@ -0,0 +1,15 @@
+// @generated by `cargo xtask contract outbox-migration-manifest --write`; do not edit.
+
+pub(crate) const OUTBOX_MIGRATION_CONTRACT_ID: &str = "radroots_outbox.migration_authority.v1";
+pub(crate) const OUTBOX_MIGRATION_SCHEMA_VERSION: u32 = 1;
+pub(crate) const OUTBOX_MIGRATION_CURRENT_VERSION: u32 = 1;
+pub(crate) const OUTBOX_MIGRATION_0001_UP_BYTE_LENGTH: usize = 5470;
+pub(crate) const OUTBOX_MIGRATION_0001_DOWN_BYTE_LENGTH: usize = 159;
+pub(crate) const OUTBOX_MIGRATION_0001_UP_SHA256: &str =
+ "a7ee775d32c2b9f845961425362e1b1e558ce0d025f7d22dd58f118ba4dab4fa";
+pub(crate) const OUTBOX_MIGRATION_0001_DOWN_SHA256: &str =
+ "5d56f978f9172dc5ecbc5043a6c286c75926974d8a2a9e44fffa7c134829af61";
+pub(crate) const OUTBOX_MIGRATION_0001_SCHEMA_SHA256: &str =
+ "e7eeba00de78ec6d990c620e7c056018166e8a00bb703e472ef6f67a00870293";
+pub(crate) const OUTBOX_MIGRATION_MANIFEST_SHA256: &str =
+ "1fb495009fd258380a02c0f5714f7280f832220aec251a7a29a5478ce64b7730";
diff --git a/crates/outbox/src/lib.rs b/crates/outbox/src/lib.rs
@@ -2,12 +2,14 @@
#![forbid(unsafe_code)]
mod error;
+mod generated;
mod migrations;
mod model;
+mod schema;
mod store;
pub use error::RadrootsOutboxError;
-pub use migrations::{OUTBOX_MIGRATION_DOWN, OUTBOX_MIGRATION_UP};
+pub use migrations::{RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT, RADROOTS_OUTBOX_SCHEMA_VERSION_MIN};
pub use model::{
RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryAttemptRecord,
RadrootsOutboxDeliveryPlanInput, RadrootsOutboxDeliveryPlanRecord,
@@ -20,4 +22,5 @@ pub use model::{
RadrootsOutboxSignedTradeMutationInput, RadrootsOutboxStatusSummary,
RadrootsOutboxTradeMutationInput,
};
+pub use schema::{RadrootsOutboxSchemaStatus, inspect_outbox_schema_status};
pub use store::RadrootsOutbox;
diff --git a/crates/outbox/src/migrations.rs b/crates/outbox/src/migrations.rs
@@ -1,4 +1,665 @@
#![forbid(unsafe_code)]
-pub const OUTBOX_MIGRATION_UP: &str = include_str!("../migrations/0001_outbox.up.sql");
-pub const OUTBOX_MIGRATION_DOWN: &str = include_str!("../migrations/0001_outbox.down.sql");
+use crate::RadrootsOutboxError;
+use crate::generated::outbox_migration_manifest as manifest;
+use sha2::{Digest, Sha256};
+use std::collections::BTreeSet;
+
+pub(crate) const OUTBOX_LEDGER_NAME: &str = "radroots_outbox_schema_migrations";
+pub(crate) const OUTBOX_RESERVED_PREFIX: &str = "outbox_";
+
+/// Oldest managed schema version that the runtime can preserve or target.
+pub const RADROOTS_OUTBOX_SCHEMA_VERSION_MIN: u32 = 1;
+/// Latest managed schema version understood by this runtime.
+pub const RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT: u32 = 1;
+
+pub(crate) const OUTBOX_LEDGER_DDL: &str = "CREATE TABLE radroots_outbox_schema_migrations (
+ version INTEGER PRIMARY KEY NOT NULL CHECK (version > 0),
+ name TEXT NOT NULL UNIQUE CHECK (length(name) > 0),
+ up_sha256 TEXT NOT NULL CHECK (length(up_sha256) = 64 AND up_sha256 NOT GLOB '*[^0-9a-f]*'),
+ down_sha256 TEXT NOT NULL CHECK (length(down_sha256) = 64 AND down_sha256 NOT GLOB '*[^0-9a-f]*'),
+ schema_sha256 TEXT NOT NULL CHECK (length(schema_sha256) = 64 AND schema_sha256 NOT GLOB '*[^0-9a-f]*')
+) STRICT, WITHOUT ROWID";
+
+pub(crate) const OUTBOX_LEDGER_CREATE_DDL: &str =
+ "CREATE TABLE main.radroots_outbox_schema_migrations (
+ version INTEGER PRIMARY KEY NOT NULL CHECK (version > 0),
+ name TEXT NOT NULL UNIQUE CHECK (length(name) > 0),
+ up_sha256 TEXT NOT NULL CHECK (length(up_sha256) = 64 AND up_sha256 NOT GLOB '*[^0-9a-f]*'),
+ down_sha256 TEXT NOT NULL CHECK (length(down_sha256) = 64 AND down_sha256 NOT GLOB '*[^0-9a-f]*'),
+ schema_sha256 TEXT NOT NULL CHECK (length(schema_sha256) = 64 AND schema_sha256 NOT GLOB '*[^0-9a-f]*')
+) STRICT, WITHOUT ROWID";
+
+pub(crate) const OUTBOX_BASELINE_OBJECT_NAMES: &[&str] = &[
+ "outbox_delivery_attempt",
+ "outbox_delivery_attempt_target_idx",
+ "outbox_delivery_plan",
+ "outbox_delivery_plan_event_idx",
+ "outbox_delivery_target",
+ "outbox_delivery_target_ready_idx",
+ "outbox_event",
+ "outbox_event_event_id_idx",
+ "outbox_event_ready_idx",
+ "outbox_operation_idempotency_idx",
+ "outbox_operation_status_idx",
+ "outbox_operation_trade_mutation_idx",
+ "outbox_operations",
+];
+
+pub(crate) const OUTBOX_BASELINE_TABLE_NAMES: &[&str] = &[
+ "outbox_delivery_attempt",
+ "outbox_delivery_plan",
+ "outbox_delivery_target",
+ "outbox_event",
+ "outbox_operations",
+];
+
+#[derive(Clone, Copy)]
+pub(crate) struct OutboxMigration {
+ pub(crate) version: u32,
+ pub(crate) name: &'static str,
+ pub(crate) up_sql: &'static str,
+ pub(crate) down_sql: &'static str,
+ pub(crate) up_len: usize,
+ pub(crate) down_len: usize,
+ pub(crate) up_sha256: &'static str,
+ pub(crate) down_sha256: &'static str,
+ pub(crate) schema_sha256: &'static str,
+ pub(crate) owned_object_names: &'static [&'static str],
+ pub(crate) owned_table_names: &'static [&'static str],
+}
+
+pub(crate) const OUTBOX_MIGRATIONS: &[OutboxMigration] = &[OutboxMigration {
+ version: 1,
+ name: "outbox",
+ up_sql: include_str!("../migrations/0001_outbox.up.sql"),
+ down_sql: include_str!("../migrations/0001_outbox.down.sql"),
+ up_len: manifest::OUTBOX_MIGRATION_0001_UP_BYTE_LENGTH,
+ down_len: manifest::OUTBOX_MIGRATION_0001_DOWN_BYTE_LENGTH,
+ up_sha256: manifest::OUTBOX_MIGRATION_0001_UP_SHA256,
+ down_sha256: manifest::OUTBOX_MIGRATION_0001_DOWN_SHA256,
+ schema_sha256: manifest::OUTBOX_MIGRATION_0001_SCHEMA_SHA256,
+ owned_object_names: OUTBOX_BASELINE_OBJECT_NAMES,
+ owned_table_names: OUTBOX_BASELINE_TABLE_NAMES,
+}];
+
+pub(crate) fn migration_for_version(
+ registry: &[OutboxMigration],
+ version: u32,
+) -> Option<&OutboxMigration> {
+ registry
+ .iter()
+ .find(|migration| migration.version == version)
+}
+
+pub(crate) fn is_outbox_owned_table_name(registry: &[OutboxMigration], name: &str) -> bool {
+ sqlite_identifier_starts_with(name, OUTBOX_RESERVED_PREFIX)
+ || registry
+ .iter()
+ .flat_map(|migration| migration.owned_table_names)
+ .any(|owned| name.eq_ignore_ascii_case(owned))
+}
+
+pub(crate) fn is_outbox_governed_schema_name(registry: &[OutboxMigration], name: &str) -> bool {
+ name.eq_ignore_ascii_case(OUTBOX_LEDGER_NAME)
+ || sqlite_identifier_starts_with(name, OUTBOX_RESERVED_PREFIX)
+ || registry
+ .iter()
+ .flat_map(|migration| migration.owned_object_names)
+ .any(|owned| name.eq_ignore_ascii_case(owned))
+}
+
+pub(crate) fn sqlite_identifier_starts_with(name: &str, prefix: &str) -> bool {
+ name.get(..prefix.len())
+ .is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
+}
+
+pub(crate) fn validate_embedded_migration_registry() -> Result<(), RadrootsOutboxError> {
+ validate_generated_descriptor_identity(
+ manifest::OUTBOX_MIGRATION_CONTRACT_ID,
+ manifest::OUTBOX_MIGRATION_SCHEMA_VERSION,
+ manifest::OUTBOX_MIGRATION_CURRENT_VERSION,
+ )?;
+ validate_sha256_literal(0, "manifest", manifest::OUTBOX_MIGRATION_MANIFEST_SHA256)?;
+ validate_migration_registry(
+ OUTBOX_MIGRATIONS,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_MIN,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ )
+}
+
+fn validate_generated_descriptor_identity(
+ contract_id: &str,
+ schema_version: u32,
+ current_version: u32,
+) -> Result<(), RadrootsOutboxError> {
+ if contract_id != "radroots_outbox.migration_authority.v1"
+ || schema_version != 1
+ || current_version != RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT
+ {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: "generated outbox migration descriptor identity is invalid".to_owned(),
+ });
+ }
+ Ok(())
+}
+
+pub(crate) fn validate_migration_registry(
+ registry: &[OutboxMigration],
+ minimum: u32,
+ current: u32,
+) -> Result<(), RadrootsOutboxError> {
+ validate_ledger_ddl_identity(OUTBOX_LEDGER_DDL, OUTBOX_LEDGER_CREATE_DDL)?;
+ if minimum == 0 || current < minimum || registry.is_empty() {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version range {minimum}..={current} requires a non-empty positive registry"
+ ),
+ });
+ }
+ let mut expected_version = minimum;
+ let mut object_names = BTreeSet::new();
+ let mut table_names = BTreeSet::new();
+ for (index, migration) in registry.iter().enumerate() {
+ if migration.version != expected_version {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!(
+ "expected migration version {expected_version}, found {}",
+ migration.version
+ ),
+ });
+ }
+ if migration.name.is_empty()
+ || registry[..index]
+ .iter()
+ .any(|prior| prior.name == migration.name)
+ {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {} has an invalid or duplicate name",
+ migration.version
+ ),
+ });
+ }
+ if migration.owned_object_names.is_empty() || migration.owned_table_names.is_empty() {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {} must own schema objects and tables",
+ migration.version
+ ),
+ });
+ }
+ for name in migration.owned_object_names {
+ validate_owned_schema_name(migration.version, "object", name)?;
+ if !object_names.insert(*name) {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!("owned schema object `{name}` is declared more than once"),
+ });
+ }
+ }
+ for name in migration.owned_table_names {
+ validate_owned_schema_name(migration.version, "table", name)?;
+ if !migration.owned_object_names.contains(name) || !table_names.insert(*name) {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!(
+ "owned table `{name}` is missing from the object inventory or is duplicated"
+ ),
+ });
+ }
+ }
+ validate_embedded_migration_input(
+ migration.version,
+ "up",
+ migration.up_sql,
+ migration.up_len,
+ migration.up_sha256,
+ )?;
+ validate_embedded_migration_input(
+ migration.version,
+ "down",
+ migration.down_sql,
+ migration.down_len,
+ migration.down_sha256,
+ )?;
+ validate_sha256_literal(migration.version, "schema", migration.schema_sha256)?;
+ expected_version = expected_version.checked_add(1).ok_or_else(|| {
+ RadrootsOutboxError::MigrationRegistryDefect {
+ reason: "migration version overflow".to_owned(),
+ }
+ })?;
+ }
+ if expected_version - 1 != current {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!(
+ "migration registry ends at {}, expected {current}",
+ expected_version - 1
+ ),
+ });
+ }
+ Ok(())
+}
+
+fn validate_ledger_ddl_identity(
+ catalog_ddl: &str,
+ create_ddl: &str,
+) -> Result<(), RadrootsOutboxError> {
+ let catalog_ddl = catalog_ddl.strip_prefix("CREATE TABLE ");
+ let create_ddl = create_ddl.strip_prefix("CREATE TABLE main.");
+ if catalog_ddl.is_none() || create_ddl.is_none() || create_ddl != catalog_ddl {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: "main-qualified ledger creation DDL does not match canonical catalog DDL"
+ .to_owned(),
+ });
+ }
+ Ok(())
+}
+
+fn validate_owned_schema_name(
+ version: u32,
+ object_kind: &'static str,
+ name: &str,
+) -> Result<(), RadrootsOutboxError> {
+ if name.is_empty()
+ || name == OUTBOX_LEDGER_NAME
+ || !sqlite_identifier_starts_with(name, OUTBOX_RESERVED_PREFIX)
+ || !name
+ .bytes()
+ .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
+ {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {version} has invalid owned {object_kind} name `{name}`"
+ ),
+ });
+ }
+ Ok(())
+}
+
+fn validate_embedded_migration_input(
+ version: u32,
+ direction: &'static str,
+ sql: &str,
+ expected_len: usize,
+ expected_sha256: &'static str,
+) -> Result<(), RadrootsOutboxError> {
+ if sql.len() != expected_len {
+ return Err(RadrootsOutboxError::EmbeddedMigrationLengthMismatch {
+ version,
+ direction,
+ expected: expected_len,
+ actual: sql.len(),
+ });
+ }
+ validate_sha256_literal(version, direction, expected_sha256)?;
+ let actual = sha256_hex(sql.as_bytes());
+ if actual != expected_sha256 {
+ return Err(RadrootsOutboxError::EmbeddedMigrationChecksumMismatch {
+ version,
+ direction,
+ expected: expected_sha256,
+ actual,
+ });
+ }
+ Ok(())
+}
+
+fn validate_sha256_literal(
+ version: u32,
+ field: &'static str,
+ value: &str,
+) -> Result<(), RadrootsOutboxError> {
+ if value.len() != 64
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!("migration version {version} has an invalid {field} SHA-256 literal"),
+ });
+ }
+ Ok(())
+}
+
+pub(crate) fn sha256_hex(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
+}
+
+#[cfg(test)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+mod tests {
+ use super::*;
+ use std::fs;
+ use std::path::{Path, PathBuf};
+
+ #[derive(Debug, PartialEq, Eq)]
+ struct DiscoveredMigration {
+ version: u32,
+ name: String,
+ up: PathBuf,
+ down: PathBuf,
+ }
+
+ fn discover(root: &Path) -> Result<Vec<DiscoveredMigration>, String> {
+ let directory = root.join("migrations");
+ let mut entries = fs::read_dir(&directory)
+ .map_err(|error| format!("read {}: {error}", directory.display()))?
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(|error| format!("read migration entry: {error}"))?;
+ entries.sort_by_key(|entry| entry.file_name());
+ let mut pairs =
+ std::collections::BTreeMap::<(u32, String), (Option<PathBuf>, Option<PathBuf>)>::new();
+ for entry in entries {
+ let file_type = entry.file_type().map_err(|error| error.to_string())?;
+ if !file_type.is_file() || file_type.is_symlink() {
+ return Err(format!(
+ "migration input must be a regular file: {}",
+ entry.path().display()
+ ));
+ }
+ let name = entry
+ .file_name()
+ .into_string()
+ .map_err(|_| "migration filename is not UTF-8".to_owned())?;
+ let (stem, direction) = if let Some(stem) = name.strip_suffix(".up.sql") {
+ (stem, "up")
+ } else if let Some(stem) = name.strip_suffix(".down.sql") {
+ (stem, "down")
+ } else {
+ return Err(format!("unknown migration file `{name}`"));
+ };
+ let (version, migration_name) = stem
+ .split_once('_')
+ .ok_or_else(|| format!("invalid migration filename `{name}`"))?;
+ if version.len() != 4
+ || !version.bytes().all(|byte| byte.is_ascii_digit())
+ || migration_name.is_empty()
+ || !migration_name
+ .bytes()
+ .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
+ {
+ return Err(format!("invalid migration filename `{name}`"));
+ }
+ let version = version.parse::<u32>().map_err(|error| error.to_string())?;
+ let pair = pairs
+ .entry((version, migration_name.to_owned()))
+ .or_default();
+ let slot = if direction == "up" {
+ &mut pair.0
+ } else {
+ &mut pair.1
+ };
+ if slot.replace(entry.path()).is_some() {
+ return Err(format!(
+ "duplicate {direction} migration for version {version}"
+ ));
+ }
+ }
+ pairs
+ .into_iter()
+ .map(|((version, name), (up, down))| {
+ Ok(DiscoveredMigration {
+ version,
+ name,
+ up: up.ok_or_else(|| format!("migration {version} is missing up SQL"))?,
+ down: down.ok_or_else(|| format!("migration {version} is missing down SQL"))?,
+ })
+ })
+ .collect()
+ }
+
+ #[test]
+ fn migration_source_discovery_is_exact_and_fail_closed() {
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
+ let discovered = discover(root).expect("canonical migration discovery");
+ assert_eq!(discovered.len(), OUTBOX_MIGRATIONS.len());
+ assert_eq!(discovered[0].version, 1);
+ assert_eq!(discovered[0].name, "outbox");
+ assert_eq!(
+ fs::read(&discovered[0].up).expect("up bytes"),
+ OUTBOX_MIGRATIONS[0].up_sql.as_bytes()
+ );
+ assert_eq!(
+ fs::read(&discovered[0].down).expect("down bytes"),
+ OUTBOX_MIGRATIONS[0].down_sql.as_bytes()
+ );
+
+ let temp = tempfile::tempdir().expect("tempdir");
+ fs::create_dir(temp.path().join("migrations")).expect("migrations");
+ for (name, bytes) in [
+ ("0001_outbox.up.sql", OUTBOX_MIGRATIONS[0].up_sql.as_bytes()),
+ (
+ "0001_outbox.down.sql",
+ OUTBOX_MIGRATIONS[0].down_sql.as_bytes(),
+ ),
+ ] {
+ fs::write(temp.path().join("migrations").join(name), bytes).expect("fixture");
+ }
+ fs::write(
+ temp.path().join("migrations/0002_unknown.txt"),
+ b"SELECT 1;",
+ )
+ .expect("unknown");
+ assert!(
+ discover(temp.path())
+ .expect_err("unknown file")
+ .contains("unknown migration file")
+ );
+ fs::remove_file(temp.path().join("migrations/0002_unknown.txt")).expect("remove");
+ fs::remove_file(temp.path().join("migrations/0001_outbox.down.sql")).expect("remove down");
+ assert!(
+ discover(temp.path())
+ .expect_err("missing pair")
+ .contains("missing down SQL")
+ );
+ }
+
+ #[test]
+ fn embedded_registry_and_frozen_baseline_are_exact() {
+ validate_embedded_migration_registry().expect("registry");
+ assert_eq!(OUTBOX_MIGRATIONS[0].up_len, 5_470);
+ assert_eq!(OUTBOX_MIGRATIONS[0].down_len, 159);
+ assert_eq!(
+ OUTBOX_MIGRATIONS[0].up_sha256,
+ "a7ee775d32c2b9f845961425362e1b1e558ce0d025f7d22dd58f118ba4dab4fa"
+ );
+ assert_eq!(
+ OUTBOX_MIGRATIONS[0].down_sha256,
+ "5d56f978f9172dc5ecbc5043a6c286c75926974d8a2a9e44fffa7c134829af61"
+ );
+ assert_eq!(OUTBOX_BASELINE_OBJECT_NAMES.len(), 13);
+ assert_eq!(OUTBOX_BASELINE_TABLE_NAMES.len(), 5);
+ }
+
+ fn assert_registry_defect(result: Result<(), RadrootsOutboxError>) -> String {
+ match result.expect_err("registry defect") {
+ RadrootsOutboxError::MigrationRegistryDefect { reason } => reason,
+ other => panic!("unexpected error: {other}"),
+ }
+ }
+
+ fn future_migration() -> OutboxMigration {
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.version = 2;
+ migration.name = "future";
+ migration.owned_object_names = &["outbox_future"];
+ migration.owned_table_names = &["outbox_future"];
+ migration
+ }
+
+ #[test]
+ fn namespace_predicates_cover_reserved_ledger_registry_and_unrelated_names() {
+ let mut legacy = OUTBOX_MIGRATIONS[0];
+ legacy.owned_object_names = &["legacy_object"];
+ legacy.owned_table_names = &["legacy_table"];
+ let registry = [legacy];
+
+ assert!(migration_for_version(OUTBOX_MIGRATIONS, 1).is_some());
+ assert!(migration_for_version(OUTBOX_MIGRATIONS, 2).is_none());
+ assert!(sqlite_identifier_starts_with("OUTBOX_EVENT", "outbox_"));
+ assert!(!sqlite_identifier_starts_with("short", "outbox_"));
+ assert!(is_outbox_owned_table_name(®istry, "outbox_new"));
+ assert!(is_outbox_owned_table_name(®istry, "LEGACY_TABLE"));
+ assert!(!is_outbox_owned_table_name(®istry, "caller_table"));
+ assert!(is_outbox_governed_schema_name(
+ ®istry,
+ "RADROOTS_OUTBOX_SCHEMA_MIGRATIONS"
+ ));
+ assert!(is_outbox_governed_schema_name(®istry, "outbox_new"));
+ assert!(is_outbox_governed_schema_name(®istry, "LEGACY_OBJECT"));
+ assert!(!is_outbox_governed_schema_name(®istry, "caller_object"));
+ }
+
+ #[test]
+ fn descriptor_and_ledger_identifiers_fail_closed() {
+ validate_generated_descriptor_identity(
+ "radroots_outbox.migration_authority.v1",
+ 1,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ )
+ .expect("descriptor");
+ for (contract_id, schema_version, current_version) in [
+ ("counterfeit", 1, RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT),
+ (
+ "radroots_outbox.migration_authority.v1",
+ 2,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ ),
+ ("radroots_outbox.migration_authority.v1", 1, 2),
+ ] {
+ assert_registry_defect(validate_generated_descriptor_identity(
+ contract_id,
+ schema_version,
+ current_version,
+ ));
+ }
+
+ validate_ledger_ddl_identity(OUTBOX_LEDGER_DDL, OUTBOX_LEDGER_CREATE_DDL)
+ .expect("ledger DDL");
+ for (catalog, create) in [
+ (OUTBOX_LEDGER_DDL, "CREATE TABLE main.counterfeit"),
+ ("counterfeit", OUTBOX_LEDGER_CREATE_DDL),
+ (OUTBOX_LEDGER_DDL, "counterfeit"),
+ ("counterfeit", "counterfeit"),
+ ] {
+ assert_registry_defect(validate_ledger_ddl_identity(catalog, create));
+ }
+ }
+
+ #[test]
+ fn registry_shape_validation_rejects_every_structural_defect() {
+ assert_registry_defect(validate_migration_registry(OUTBOX_MIGRATIONS, 0, 1));
+ assert_registry_defect(validate_migration_registry(OUTBOX_MIGRATIONS, 2, 1));
+ assert_registry_defect(validate_migration_registry(&[], 1, 1));
+
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.version = 2;
+ assert_registry_defect(validate_migration_registry(&[migration], 1, 1));
+
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.name = "";
+ assert_registry_defect(validate_migration_registry(&[migration], 1, 1));
+
+ let mut duplicate_name = future_migration();
+ duplicate_name.name = OUTBOX_MIGRATIONS[0].name;
+ assert_registry_defect(validate_migration_registry(
+ &[OUTBOX_MIGRATIONS[0], duplicate_name],
+ 1,
+ 2,
+ ));
+
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.owned_object_names = &[];
+ assert_registry_defect(validate_migration_registry(&[migration], 1, 1));
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.owned_table_names = &[];
+ assert_registry_defect(validate_migration_registry(&[migration], 1, 1));
+
+ for invalid_name in ["", OUTBOX_LEDGER_NAME, "caller_object", "outbox_UPPER"] {
+ assert_registry_defect(validate_owned_schema_name(1, "object", invalid_name));
+ }
+ validate_owned_schema_name(1, "object", "outbox_123").expect("numeric identifier");
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.owned_object_names = &["outbox_valid"];
+ migration.owned_table_names = &["outbox_UPPER"];
+ assert_registry_defect(validate_migration_registry(&[migration], 1, 1));
+
+ let mut duplicate_object = future_migration();
+ duplicate_object.owned_object_names = &["outbox_event"];
+ duplicate_object.owned_table_names = &["outbox_event"];
+ assert_registry_defect(validate_migration_registry(
+ &[OUTBOX_MIGRATIONS[0], duplicate_object],
+ 1,
+ 2,
+ ));
+
+ let mut missing_table = future_migration();
+ missing_table.owned_object_names = &["outbox_future_index"];
+ assert_registry_defect(validate_migration_registry(
+ &[OUTBOX_MIGRATIONS[0], missing_table],
+ 1,
+ 2,
+ ));
+
+ let mut duplicate_table = future_migration();
+ duplicate_table.owned_table_names = &["outbox_future", "outbox_future"];
+ assert_registry_defect(validate_migration_registry(
+ &[OUTBOX_MIGRATIONS[0], duplicate_table],
+ 1,
+ 2,
+ ));
+
+ assert_registry_defect(validate_migration_registry(OUTBOX_MIGRATIONS, 1, 2));
+ validate_migration_registry(&[OUTBOX_MIGRATIONS[0], future_migration()], 1, 2)
+ .expect("contiguous synthetic registry");
+
+ let mut overflow = OUTBOX_MIGRATIONS[0];
+ overflow.version = u32::MAX;
+ validate_migration_registry(&[overflow], u32::MAX, u32::MAX).expect_err("version overflow");
+ }
+
+ #[test]
+ fn registry_checksum_validation_rejects_lengths_literals_and_bytes() {
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.up_len += 1;
+ assert!(matches!(
+ validate_migration_registry(&[migration], 1, 1),
+ Err(RadrootsOutboxError::EmbeddedMigrationLengthMismatch {
+ direction: "up",
+ ..
+ })
+ ));
+
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.down_len += 1;
+ assert!(matches!(
+ validate_migration_registry(&[migration], 1, 1),
+ Err(RadrootsOutboxError::EmbeddedMigrationLengthMismatch {
+ direction: "down",
+ ..
+ })
+ ));
+
+ for invalid_sha in [
+ "short",
+ "gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
+ ] {
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.up_sha256 = invalid_sha;
+ assert_registry_defect(validate_migration_registry(&[migration], 1, 1));
+ }
+
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.up_sha256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
+ assert!(matches!(
+ validate_migration_registry(&[migration], 1, 1),
+ Err(RadrootsOutboxError::EmbeddedMigrationChecksumMismatch {
+ direction: "up",
+ ..
+ })
+ ));
+
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.schema_sha256 = "short";
+ assert_registry_defect(validate_migration_registry(&[migration], 1, 1));
+ }
+}
diff --git a/crates/outbox/src/schema.rs b/crates/outbox/src/schema.rs
@@ -0,0 +1,1601 @@
+#![forbid(unsafe_code)]
+
+use crate::RadrootsOutboxError;
+use crate::migrations::{
+ OUTBOX_LEDGER_CREATE_DDL, OUTBOX_LEDGER_DDL, OUTBOX_LEDGER_NAME, OUTBOX_MIGRATIONS,
+ OUTBOX_RESERVED_PREFIX, OutboxMigration, RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_MIN, is_outbox_governed_schema_name, is_outbox_owned_table_name,
+ migration_for_version, validate_embedded_migration_registry, validate_migration_registry,
+};
+use sha2::{Digest, Sha256};
+use sqlx::{Row, Sqlite, SqliteConnection, SqlitePool, Transaction};
+use std::collections::{BTreeMap, BTreeSet};
+
+#[cfg(test)]
+const EMPTY_SCHEMA_SHA256: &str =
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+/// Authenticated lifecycle state of the governed outbox schema.
+pub enum RadrootsOutboxSchemaStatus {
+ /// No governed outbox schema objects exist.
+ Uninitialized,
+ /// The exact frozen baseline exists without its migration ledger.
+ UnledgeredBaseline,
+ /// The schema and ledger match a supported managed version.
+ Managed { version: u32 },
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct CatalogRow {
+ object_type: String,
+ name: String,
+ table_name: String,
+ sql: Option<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct AppliedMigration {
+ version: i64,
+ name: String,
+ up_sha256: String,
+ down_sha256: String,
+ schema_sha256: String,
+}
+
+/// Inspects and authenticates schema state without adopting or migrating it.
+pub async fn inspect_outbox_schema_status(
+ pool: &SqlitePool,
+) -> Result<RadrootsOutboxSchemaStatus, RadrootsOutboxError> {
+ validate_embedded_migration_registry()?;
+ let mut transaction = pool.begin().await?;
+ let result = inspect_schema_on_connection(
+ &mut transaction,
+ OUTBOX_MIGRATIONS,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ )
+ .await;
+ finish_schema_transaction(transaction, result).await
+}
+
+pub(crate) async fn migrate_outbox_schema(pool: &SqlitePool) -> Result<(), RadrootsOutboxError> {
+ validate_embedded_migration_registry()?;
+ migrate_outbox_schema_with_registry(
+ pool,
+ OUTBOX_MIGRATIONS,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_MIN,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ )
+ .await
+}
+
+async fn migrate_outbox_schema_with_registry(
+ pool: &SqlitePool,
+ registry: &[OutboxMigration],
+ minimum: u32,
+ supported_current: u32,
+) -> Result<(), RadrootsOutboxError> {
+ validate_migration_registry(registry, minimum, supported_current)?;
+ let mut transaction = pool.begin_with("BEGIN IMMEDIATE").await?;
+ let result = migrate_schema_on_connection(&mut transaction, registry, supported_current).await;
+ finish_schema_transaction(transaction, result).await
+}
+
+pub(crate) async fn rollback_outbox_schema_offline(
+ pool: &SqlitePool,
+ target: u32,
+) -> Result<(), RadrootsOutboxError> {
+ if target < RADROOTS_OUTBOX_SCHEMA_VERSION_MIN {
+ return Err(RadrootsOutboxError::RollbackBelowVersionFloor {
+ floor: RADROOTS_OUTBOX_SCHEMA_VERSION_MIN,
+ target,
+ });
+ }
+ validate_embedded_migration_registry()?;
+ let mut transaction = pool.begin_with("BEGIN EXCLUSIVE").await?;
+ let result = rollback_schema_on_connection(
+ &mut transaction,
+ OUTBOX_MIGRATIONS,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ target,
+ )
+ .await;
+ finish_schema_transaction(transaction, result).await
+}
+
+#[cfg(test)]
+pub(crate) async fn destroy_outbox_schema_for_migration_test(
+ pool: &SqlitePool,
+) -> Result<(), RadrootsOutboxError> {
+ validate_embedded_migration_registry()?;
+ let mut transaction = pool.begin_with("BEGIN EXCLUSIVE").await?;
+ let result = destroy_schema_on_connection(&mut transaction).await;
+ finish_schema_transaction(transaction, result).await
+}
+
+async fn finish_schema_transaction<T>(
+ transaction: Transaction<'static, Sqlite>,
+ result: Result<T, RadrootsOutboxError>,
+) -> Result<T, RadrootsOutboxError> {
+ match result {
+ Ok(value) => {
+ transaction.commit().await?;
+ Ok(value)
+ }
+ Err(primary) => match transaction.rollback().await {
+ Ok(()) => Err(primary),
+ Err(rollback) => Err(RadrootsOutboxError::MigrationTransactionRollbackFailed {
+ primary: Box::new(primary),
+ rollback,
+ }),
+ },
+ }
+}
+
+async fn migrate_schema_on_connection(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+ supported_current: u32,
+) -> Result<(), RadrootsOutboxError> {
+ let status = inspect_schema_on_connection(connection, registry, supported_current).await?;
+ let current_version = match status {
+ RadrootsOutboxSchemaStatus::Uninitialized => {
+ apply_migration_up(connection, registry, ®istry[0]).await?;
+ create_ledger(connection).await?;
+ insert_ledger_row(connection, ®istry[0]).await?;
+ registry[0].version
+ }
+ RadrootsOutboxSchemaStatus::UnledgeredBaseline => {
+ create_ledger(connection).await?;
+ insert_ledger_row(connection, ®istry[0]).await?;
+ registry[0].version
+ }
+ RadrootsOutboxSchemaStatus::Managed { version } if version == supported_current => version,
+ RadrootsOutboxSchemaStatus::Managed { version } => version,
+ };
+
+ for migration in registry
+ .iter()
+ .filter(|migration| migration.version > current_version)
+ {
+ apply_migration_up(connection, registry, migration).await?;
+ insert_ledger_row(connection, migration).await?;
+ }
+
+ validate_database_integrity(connection, registry).await?;
+ match inspect_schema_on_connection(connection, registry, supported_current).await? {
+ RadrootsOutboxSchemaStatus::Managed { version } if version == supported_current => Ok(()),
+ status => Err(RadrootsOutboxError::MigrationRegistryDefect {
+ reason: format!("migration completed in unexpected state {status:?}"),
+ }),
+ }
+}
+
+async fn rollback_schema_on_connection(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+ supported_current: u32,
+ target: u32,
+) -> Result<(), RadrootsOutboxError> {
+ let RadrootsOutboxSchemaStatus::Managed {
+ version: current_version,
+ } = inspect_schema_on_connection(connection, registry, supported_current).await?
+ else {
+ return Err(RadrootsOutboxError::RollbackUnmanaged);
+ };
+ if target > current_version {
+ return Err(RadrootsOutboxError::RollbackAhead {
+ current: current_version,
+ target,
+ });
+ }
+
+ for version in ((target + 1)..=current_version).rev() {
+ let migration = migration_for_version(registry, version)
+ .ok_or(RadrootsOutboxError::UnknownMigration { version })?;
+ apply_migration_down(connection, registry, migration).await?;
+ let prior = migration_for_version(registry, version - 1).ok_or(
+ RadrootsOutboxError::MigrationHistoryGap {
+ expected: version - 1,
+ actual: None,
+ },
+ )?;
+ validate_schema_fingerprint(connection, registry, prior).await?;
+ let deleted =
+ sqlx::query("DELETE FROM main.radroots_outbox_schema_migrations WHERE version = ?")
+ .bind(i64::from(version))
+ .execute(&mut *connection)
+ .await?;
+ if deleted.rows_affected() != 1 {
+ return Err(RadrootsOutboxError::MigrationLedgerDrift {
+ reason: format!(
+ "rollback expected one ledger row for version {version}, deleted {}",
+ deleted.rows_affected()
+ ),
+ });
+ }
+ }
+
+ validate_database_integrity(connection, registry).await?;
+ match inspect_schema_on_connection(connection, registry, supported_current).await? {
+ RadrootsOutboxSchemaStatus::Managed { version } if version == target => Ok(()),
+ status => Err(RadrootsOutboxError::MigrationLedgerDrift {
+ reason: format!("rollback completed in unexpected state {status:?}"),
+ }),
+ }
+}
+
+#[cfg(test)]
+async fn destroy_schema_on_connection(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsOutboxError> {
+ match inspect_schema_on_connection(
+ connection,
+ OUTBOX_MIGRATIONS,
+ RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ )
+ .await?
+ {
+ RadrootsOutboxSchemaStatus::Managed { version } => {
+ for version in (RADROOTS_OUTBOX_SCHEMA_VERSION_MIN..=version).rev() {
+ let migration = migration_for_version(OUTBOX_MIGRATIONS, version)
+ .ok_or(RadrootsOutboxError::UnknownMigration { version })?;
+ apply_migration_down(connection, OUTBOX_MIGRATIONS, migration).await?;
+ let deleted = sqlx::query(
+ "DELETE FROM main.radroots_outbox_schema_migrations WHERE version = ?",
+ )
+ .bind(i64::from(version))
+ .execute(&mut *connection)
+ .await?;
+ if deleted.rows_affected() != 1 {
+ return Err(RadrootsOutboxError::MigrationLedgerDrift {
+ reason: format!(
+ "test destruction expected one ledger row for version {version}, deleted {}",
+ deleted.rows_affected()
+ ),
+ });
+ }
+ }
+ validate_empty_governed_catalog(connection, OUTBOX_MIGRATIONS).await?;
+ sqlx::query("DROP TABLE main.radroots_outbox_schema_migrations")
+ .execute(&mut *connection)
+ .await?;
+ }
+ RadrootsOutboxSchemaStatus::UnledgeredBaseline => {
+ apply_migration_down(connection, OUTBOX_MIGRATIONS, &OUTBOX_MIGRATIONS[0]).await?;
+ validate_empty_governed_catalog(connection, OUTBOX_MIGRATIONS).await?;
+ }
+ RadrootsOutboxSchemaStatus::Uninitialized => {}
+ }
+ validate_database_integrity(connection, OUTBOX_MIGRATIONS).await
+}
+
+async fn apply_migration_up(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+ migration: &OutboxMigration,
+) -> Result<(), RadrootsOutboxError> {
+ let before = read_catalog_bounded(connection, registry).await?;
+ sqlx::raw_sql(migration.up_sql)
+ .execute(&mut *connection)
+ .await?;
+ let after = read_catalog_bounded(connection, registry).await?;
+ validate_catalog_delta(&before, &after, migration, "up")?;
+ validate_schema_fingerprint(connection, registry, migration).await
+}
+
+async fn apply_migration_down(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+ migration: &OutboxMigration,
+) -> Result<(), RadrootsOutboxError> {
+ let before = read_catalog_bounded(connection, registry).await?;
+ sqlx::raw_sql(migration.down_sql)
+ .execute(&mut *connection)
+ .await?;
+ let after = read_catalog_bounded(connection, registry).await?;
+ validate_catalog_delta(&before, &after, migration, "down")
+}
+
+fn validate_catalog_delta(
+ before: &[CatalogRow],
+ after: &[CatalogRow],
+ migration: &OutboxMigration,
+ direction: &'static str,
+) -> Result<(), RadrootsOutboxError> {
+ let before = before
+ .iter()
+ .map(|row| (row.name.as_str(), row))
+ .collect::<BTreeMap<_, _>>();
+ let after = after
+ .iter()
+ .map(|row| (row.name.as_str(), row))
+ .collect::<BTreeMap<_, _>>();
+ let added = after
+ .keys()
+ .filter(|name| !before.contains_key(**name))
+ .copied()
+ .collect::<BTreeSet<_>>();
+ let removed = before
+ .keys()
+ .filter(|name| !after.contains_key(**name))
+ .copied()
+ .collect::<BTreeSet<_>>();
+ let changed = before
+ .iter()
+ .filter_map(|(name, row)| {
+ after
+ .get(name)
+ .filter(|after_row| *after_row != row)
+ .map(|_| *name)
+ })
+ .collect::<BTreeSet<_>>();
+ let expected = migration
+ .owned_object_names
+ .iter()
+ .copied()
+ .collect::<BTreeSet<_>>();
+ let valid = match direction {
+ "up" => added == expected && removed.is_empty() && changed.is_empty(),
+ "down" => removed == expected && added.is_empty() && changed.is_empty(),
+ _ => false,
+ };
+ if !valid {
+ return Err(RadrootsOutboxError::MigrationCatalogDeltaMismatch {
+ version: migration.version,
+ direction,
+ reason: format!(
+ "expected {expected:?}; added {added:?}, removed {removed:?}, changed {changed:?}"
+ ),
+ });
+ }
+ Ok(())
+}
+
+async fn create_ledger(connection: &mut SqliteConnection) -> Result<(), RadrootsOutboxError> {
+ sqlx::query(OUTBOX_LEDGER_CREATE_DDL)
+ .execute(&mut *connection)
+ .await?;
+ validate_ledger_catalog(&read_catalog_bounded(connection, OUTBOX_MIGRATIONS).await?)?;
+ Ok(())
+}
+
+async fn insert_ledger_row(
+ connection: &mut SqliteConnection,
+ migration: &OutboxMigration,
+) -> Result<(), RadrootsOutboxError> {
+ sqlx::query(
+ "INSERT INTO main.radroots_outbox_schema_migrations(version, name, up_sha256, down_sha256, schema_sha256) VALUES (?, ?, ?, ?, ?)",
+ )
+ .bind(i64::from(migration.version))
+ .bind(migration.name)
+ .bind(migration.up_sha256)
+ .bind(migration.down_sha256)
+ .bind(migration.schema_sha256)
+ .execute(&mut *connection)
+ .await?;
+ Ok(())
+}
+
+async fn inspect_schema_on_connection(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+ supported_current: u32,
+) -> Result<RadrootsOutboxSchemaStatus, RadrootsOutboxError> {
+ validate_outbox_temp_schema_with_registry(connection, registry).await?;
+ let catalog = read_catalog_bounded(connection, registry).await?;
+ let has_ledger = validate_ledger_catalog(&catalog)?;
+ let governed = governed_catalog(&catalog, registry);
+ let actual_schema_sha256 = catalog_fingerprint(&governed);
+
+ if !has_ledger {
+ if governed.is_empty() {
+ return Ok(RadrootsOutboxSchemaStatus::Uninitialized);
+ }
+ let baseline = ®istry[0];
+ if governed.len() == baseline.owned_object_names.len()
+ && actual_schema_sha256 == baseline.schema_sha256
+ {
+ return Ok(RadrootsOutboxSchemaStatus::UnledgeredBaseline);
+ }
+ return Err(RadrootsOutboxError::UnmanagedSchema {
+ actual_schema_sha256,
+ });
+ }
+
+ let history = read_history_bounded(connection, supported_current).await?;
+ let current = validate_history_against_registry(&history, registry, supported_current)?;
+ let expected = migration_for_version(registry, current)
+ .ok_or(RadrootsOutboxError::UnknownMigration { version: current })?;
+ if actual_schema_sha256 != expected.schema_sha256 {
+ return Err(RadrootsOutboxError::SchemaFingerprintMismatch {
+ version: current,
+ expected: expected.schema_sha256,
+ actual: actual_schema_sha256,
+ });
+ }
+ Ok(RadrootsOutboxSchemaStatus::Managed { version: current })
+}
+
+pub(crate) async fn validate_outbox_temp_schema(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsOutboxError> {
+ validate_outbox_temp_schema_with_registry(connection, OUTBOX_MIGRATIONS).await
+}
+
+async fn validate_outbox_temp_schema_with_registry(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+) -> Result<(), RadrootsOutboxError> {
+ let collision = sqlx::query(
+ "SELECT type, name, tbl_name FROM temp.sqlite_schema
+ WHERE type IN ('trigger', 'view')
+ OR lower(substr(name, 1, length(?))) = lower(?)
+ OR lower(substr(tbl_name, 1, length(?))) = lower(?)
+ OR name = ? COLLATE NOCASE
+ OR tbl_name = ? COLLATE NOCASE
+ ORDER BY type, name, tbl_name LIMIT 1",
+ )
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_LEDGER_NAME)
+ .bind(OUTBOX_LEDGER_NAME)
+ .fetch_optional(&mut *connection)
+ .await?;
+ if let Some(row) = collision {
+ let object_type: String = row.try_get("type")?;
+ let name: String = row.try_get("name")?;
+ let table_name: String = row.try_get("tbl_name")?;
+ if matches!(object_type.as_str(), "trigger" | "view")
+ || is_outbox_governed_schema_name(registry, &name)
+ || is_outbox_governed_schema_name(registry, &table_name)
+ {
+ return Err(RadrootsOutboxError::TemporarySchemaCollision {
+ object_type,
+ name,
+ table_name,
+ });
+ }
+ }
+ Ok(())
+}
+
+fn catalog_row_limit(registry: &[OutboxMigration]) -> Result<i64, RadrootsOutboxError> {
+ let max = registry
+ .iter()
+ .flat_map(|migration| migration.owned_object_names.iter().copied())
+ .collect::<BTreeSet<_>>()
+ .len()
+ .checked_add(1)
+ .ok_or_else(|| RadrootsOutboxError::MigrationRegistryDefect {
+ reason: "governed catalog object limit overflow".to_owned(),
+ })?;
+ i64::try_from(max.checked_add(1).ok_or_else(|| {
+ RadrootsOutboxError::MigrationRegistryDefect {
+ reason: "governed catalog collision limit overflow".to_owned(),
+ }
+ })?)
+ .map_err(|_| RadrootsOutboxError::MigrationRegistryDefect {
+ reason: "governed catalog object limit is outside SQLite range".to_owned(),
+ })
+}
+
+async fn read_catalog_bounded(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+) -> Result<Vec<CatalogRow>, RadrootsOutboxError> {
+ let row_limit = catalog_row_limit(registry)?;
+ let rows = sqlx::query(
+ "SELECT type, name, tbl_name, sql FROM main.sqlite_schema
+ WHERE lower(substr(name, 1, 7)) != 'sqlite_'
+ AND (lower(substr(name, 1, length(?))) = lower(?)
+ OR lower(substr(tbl_name, 1, length(?))) = lower(?)
+ OR name = ? COLLATE NOCASE
+ OR tbl_name = ? COLLATE NOCASE)
+ ORDER BY type, name, tbl_name LIMIT ?",
+ )
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_LEDGER_NAME)
+ .bind(OUTBOX_LEDGER_NAME)
+ .bind(row_limit)
+ .fetch_all(&mut *connection)
+ .await?;
+ if i64::try_from(rows.len()).ok() == Some(row_limit) {
+ return Err(RadrootsOutboxError::GovernedCatalogCapacityExceeded {
+ max: usize::try_from(row_limit - 1).unwrap_or(usize::MAX),
+ });
+ }
+ 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()
+}
+
+fn validate_ledger_catalog(catalog: &[CatalogRow]) -> Result<bool, RadrootsOutboxError> {
+ let rows = catalog
+ .iter()
+ .filter(|row| {
+ row.name.eq_ignore_ascii_case(OUTBOX_LEDGER_NAME)
+ || row.table_name.eq_ignore_ascii_case(OUTBOX_LEDGER_NAME)
+ })
+ .collect::<Vec<_>>();
+ if rows.is_empty() {
+ return Ok(false);
+ }
+ if rows.len() != 1 {
+ return Err(RadrootsOutboxError::MigrationLedgerDrift {
+ reason: format!(
+ "expected exactly one non-internal ledger catalog object, found {}",
+ rows.len()
+ ),
+ });
+ }
+ let row = rows[0];
+ if row.object_type != "table"
+ || row.name != OUTBOX_LEDGER_NAME
+ || row.table_name != OUTBOX_LEDGER_NAME
+ || row.sql.as_deref() != Some(OUTBOX_LEDGER_DDL)
+ {
+ return Err(RadrootsOutboxError::MigrationLedgerDrift {
+ reason: "ledger table definition does not match canonical catalog SQL".to_owned(),
+ });
+ }
+ Ok(true)
+}
+
+fn governed_catalog(catalog: &[CatalogRow], registry: &[OutboxMigration]) -> Vec<CatalogRow> {
+ catalog
+ .iter()
+ .filter(|row| !row.name.eq_ignore_ascii_case(OUTBOX_LEDGER_NAME))
+ .filter(|row| {
+ is_outbox_governed_schema_name(registry, &row.name)
+ || is_outbox_governed_schema_name(registry, &row.table_name)
+ })
+ .cloned()
+ .collect()
+}
+
+fn catalog_fingerprint(catalog: &[CatalogRow]) -> String {
+ let mut rows = catalog.to_vec();
+ rows.sort_by(|left, right| {
+ (
+ left.object_type.as_bytes(),
+ left.name.as_bytes(),
+ left.table_name.as_bytes(),
+ left.sql.as_deref().unwrap_or("").as_bytes(),
+ )
+ .cmp(&(
+ right.object_type.as_bytes(),
+ right.name.as_bytes(),
+ right.table_name.as_bytes(),
+ right.sql.as_deref().unwrap_or("").as_bytes(),
+ ))
+ });
+ let mut digest = Sha256::new();
+ for row in rows {
+ for field in [
+ row.object_type.as_str(),
+ row.name.as_str(),
+ row.table_name.as_str(),
+ row.sql.as_deref().unwrap_or(""),
+ ] {
+ digest.update(field.as_bytes());
+ digest.update([0]);
+ }
+ }
+ hex::encode(digest.finalize())
+}
+
+async fn read_history_bounded(
+ connection: &mut SqliteConnection,
+ supported_current: u32,
+) -> Result<Vec<AppliedMigration>, RadrootsOutboxError> {
+ let row_limit = i64::from(supported_current).checked_add(1).ok_or_else(|| {
+ RadrootsOutboxError::MigrationRegistryDefect {
+ reason: "migration history row limit overflow".to_owned(),
+ }
+ })?;
+ let rows = sqlx::query(
+ "SELECT version, name, up_sha256, down_sha256, schema_sha256
+ FROM main.radroots_outbox_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: &[OutboxMigration],
+ supported_current: u32,
+) -> Result<u32, RadrootsOutboxError> {
+ if history.is_empty() {
+ return Err(RadrootsOutboxError::MigrationLedgerDrift {
+ reason: "ledger exists without migration history".to_owned(),
+ });
+ }
+ let database_version = history
+ .iter()
+ .map(|row| row.version)
+ .max()
+ .unwrap_or_default();
+ if database_version > i64::from(supported_current) {
+ return Err(RadrootsOutboxError::SchemaTooNew {
+ current: supported_current,
+ database: database_version,
+ });
+ }
+ let mut expected_version = registry[0].version;
+ for row in history {
+ let version =
+ u32::try_from(row.version).map_err(|_| RadrootsOutboxError::MigrationLedgerDrift {
+ reason: format!(
+ "ledger version {} is outside the positive range",
+ row.version
+ ),
+ })?;
+ if version != expected_version {
+ return Err(RadrootsOutboxError::MigrationHistoryGap {
+ expected: expected_version,
+ actual: Some(version),
+ });
+ }
+ let migration = migration_for_version(registry, version)
+ .ok_or(RadrootsOutboxError::UnknownMigration { version })?;
+ if row.name != migration.name {
+ return Err(RadrootsOutboxError::MigrationHistoryNameDrift {
+ version,
+ expected: migration.name,
+ actual: row.name.clone(),
+ });
+ }
+ validate_history_checksum(version, "up_sha256", &row.up_sha256, migration.up_sha256)?;
+ validate_history_checksum(
+ version,
+ "down_sha256",
+ &row.down_sha256,
+ migration.down_sha256,
+ )?;
+ validate_history_checksum(
+ version,
+ "schema_sha256",
+ &row.schema_sha256,
+ migration.schema_sha256,
+ )?;
+ expected_version = expected_version.checked_add(1).ok_or_else(|| {
+ RadrootsOutboxError::MigrationLedgerDrift {
+ reason: "migration history version overflow".to_owned(),
+ }
+ })?;
+ }
+ Ok(expected_version - 1)
+}
+
+fn validate_history_checksum(
+ version: u32,
+ field: &'static str,
+ actual: &str,
+ expected: &'static str,
+) -> Result<(), RadrootsOutboxError> {
+ if actual != expected {
+ return Err(RadrootsOutboxError::MigrationHistoryChecksumDrift {
+ version,
+ field,
+ expected,
+ actual: actual.to_owned(),
+ });
+ }
+ Ok(())
+}
+
+async fn validate_schema_fingerprint(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+ migration: &OutboxMigration,
+) -> Result<(), RadrootsOutboxError> {
+ let catalog = read_catalog_bounded(connection, registry).await?;
+ let actual = catalog_fingerprint(&governed_catalog(&catalog, registry));
+ if actual != migration.schema_sha256 {
+ return Err(RadrootsOutboxError::SchemaFingerprintMismatch {
+ version: migration.version,
+ expected: migration.schema_sha256,
+ actual,
+ });
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+async fn validate_empty_governed_catalog(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+) -> Result<(), RadrootsOutboxError> {
+ let catalog = read_catalog_bounded(connection, registry).await?;
+ let actual = catalog_fingerprint(&governed_catalog(&catalog, registry));
+ if actual != EMPTY_SCHEMA_SHA256 {
+ return Err(RadrootsOutboxError::SchemaFingerprintMismatch {
+ version: 0,
+ expected: EMPTY_SCHEMA_SHA256,
+ actual,
+ });
+ }
+ Ok(())
+}
+
+async fn validate_database_integrity(
+ connection: &mut SqliteConnection,
+ registry: &[OutboxMigration],
+) -> Result<(), RadrootsOutboxError> {
+ let detail: String = sqlx::query_scalar("PRAGMA integrity_check(1)")
+ .fetch_one(&mut *connection)
+ .await?;
+ if detail != "ok" {
+ return Err(RadrootsOutboxError::IntegrityCheckFailed { detail });
+ }
+
+ let violation = sqlx::query(
+ "SELECT \"table\", rowid, parent, fkid FROM pragma_foreign_key_check
+ WHERE lower(substr(\"table\", 1, length(?))) = lower(?) LIMIT 1",
+ )
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .fetch_optional(&mut *connection)
+ .await?;
+ if let Some(row) = violation {
+ let table: String = row.try_get("table")?;
+ if is_outbox_owned_table_name(registry, &table) {
+ return Err(RadrootsOutboxError::ForeignKeyViolation {
+ table,
+ rowid: row.try_get("rowid")?,
+ parent: row.try_get("parent")?,
+ foreign_key_id: row.try_get("fkid")?,
+ });
+ }
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+mod tests {
+ use super::*;
+ use crate::RadrootsOutbox;
+ use sqlx::Connection;
+ use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
+ use std::str::FromStr;
+ use std::sync::Arc;
+ use tokio::sync::Barrier;
+
+ async fn memory_pool() -> SqlitePool {
+ SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(SqliteConnectOptions::from_str("sqlite::memory:").expect("options"))
+ .await
+ .expect("pool")
+ }
+
+ #[tokio::test]
+ async fn fresh_migration_reaches_the_authenticated_schema_fingerprint() {
+ let pool = memory_pool().await;
+ migrate_outbox_schema(&pool).await.expect("fresh migration");
+ assert_eq!(
+ inspect_outbox_schema_status(&pool)
+ .await
+ .expect("managed schema"),
+ RadrootsOutboxSchemaStatus::Managed { version: 1 }
+ );
+ }
+
+ async fn apply_unledgered_baseline(pool: &SqlitePool) {
+ sqlx::raw_sql(OUTBOX_MIGRATIONS[0].up_sql)
+ .execute(pool)
+ .await
+ .expect("unledgered baseline");
+ }
+
+ async fn ledger_count(pool: &SqlitePool) -> i64 {
+ sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE type = 'table' AND name = ?",
+ )
+ .bind(OUTBOX_LEDGER_NAME)
+ .fetch_one(pool)
+ .await
+ .expect("ledger count")
+ }
+
+ async fn synthetic_v2_registry(pool: &SqlitePool) -> [OutboxMigration; 2] {
+ const UP_SQL: &str = "CREATE TABLE outbox_future (value TEXT NOT NULL) STRICT;\n";
+ const DOWN_SQL: &str = "DROP TABLE outbox_future;\n";
+
+ migrate_outbox_schema(pool).await.expect("version 1");
+ sqlx::raw_sql(UP_SQL)
+ .execute(pool)
+ .await
+ .expect("synthetic version 2 schema");
+ let catalog = sqlx::query(
+ "SELECT type, name, tbl_name, sql FROM main.sqlite_schema
+ WHERE lower(substr(name, 1, 7)) != 'sqlite_'
+ AND (lower(substr(name, 1, length(?))) = lower(?)
+ OR lower(substr(tbl_name, 1, length(?))) = lower(?))",
+ )
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .bind(OUTBOX_RESERVED_PREFIX)
+ .fetch_all(pool)
+ .await
+ .expect("synthetic version 2 catalog")
+ .into_iter()
+ .map(|row| CatalogRow {
+ object_type: row.try_get("type").expect("catalog type"),
+ name: row.try_get("name").expect("catalog name"),
+ table_name: row.try_get("tbl_name").expect("catalog table name"),
+ sql: row.try_get("sql").expect("catalog SQL"),
+ })
+ .collect::<Vec<_>>();
+ let schema_sha256 = Box::leak(catalog_fingerprint(&catalog).into_boxed_str());
+ sqlx::raw_sql(DOWN_SQL)
+ .execute(pool)
+ .await
+ .expect("remove synthetic version 2 schema");
+
+ let up_sha256 =
+ Box::leak(crate::migrations::sha256_hex(UP_SQL.as_bytes()).into_boxed_str());
+ let down_sha256 =
+ Box::leak(crate::migrations::sha256_hex(DOWN_SQL.as_bytes()).into_boxed_str());
+ [
+ OUTBOX_MIGRATIONS[0],
+ OutboxMigration {
+ version: 2,
+ name: "future",
+ up_sql: UP_SQL,
+ down_sql: DOWN_SQL,
+ up_len: UP_SQL.len(),
+ down_len: DOWN_SQL.len(),
+ up_sha256,
+ down_sha256,
+ schema_sha256,
+ owned_object_names: &["outbox_future"],
+ owned_table_names: &["outbox_future"],
+ },
+ ]
+ }
+
+ async fn inspect_with_registry(
+ pool: &SqlitePool,
+ registry: &[OutboxMigration],
+ supported_current: u32,
+ ) -> Result<RadrootsOutboxSchemaStatus, RadrootsOutboxError> {
+ let mut transaction = pool.begin().await?;
+ let result =
+ inspect_schema_on_connection(&mut transaction, registry, supported_current).await;
+ finish_schema_transaction(transaction, result).await
+ }
+
+ #[tokio::test]
+ async fn additive_future_migration_advances_and_rolls_back_to_an_explicit_target() {
+ let pool = memory_pool().await;
+ sqlx::raw_sql(
+ "CREATE TABLE caller_state (value TEXT NOT NULL);
+ INSERT INTO caller_state(value) VALUES ('preserved');",
+ )
+ .execute(&pool)
+ .await
+ .expect("caller state");
+ let registry = synthetic_v2_registry(&pool).await;
+
+ migrate_outbox_schema_with_registry(&pool, ®istry, 1, 2)
+ .await
+ .expect("advance to version 2");
+ assert_eq!(
+ inspect_with_registry(&pool, ®istry, 2)
+ .await
+ .expect("managed version 2"),
+ RadrootsOutboxSchemaStatus::Managed { version: 2 }
+ );
+ let future_objects: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE name = 'outbox_future'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("future object count");
+ assert_eq!(future_objects, 1);
+
+ let mut transaction = pool
+ .begin_with("BEGIN EXCLUSIVE")
+ .await
+ .expect("rollback transaction");
+ let result = rollback_schema_on_connection(&mut transaction, ®istry, 2, 1).await;
+ finish_schema_transaction(transaction, result)
+ .await
+ .expect("rollback to version 1");
+ assert_eq!(
+ inspect_with_registry(&pool, ®istry, 2)
+ .await
+ .expect("managed version 1"),
+ RadrootsOutboxSchemaStatus::Managed { version: 1 }
+ );
+ let future_objects: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE name = 'outbox_future'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("rolled-back object count");
+ assert_eq!(future_objects, 0);
+ let caller_value: String = sqlx::query_scalar("SELECT value FROM caller_state")
+ .fetch_one(&pool)
+ .await
+ .expect("preserved caller state");
+ assert_eq!(caller_value, "preserved");
+
+ let mut transaction = pool
+ .begin_with("BEGIN EXCLUSIVE")
+ .await
+ .expect("ahead transaction");
+ let result = rollback_schema_on_connection(&mut transaction, ®istry, 2, 2).await;
+ assert!(matches!(
+ result,
+ Err(RadrootsOutboxError::RollbackAhead { .. })
+ ));
+ transaction
+ .rollback()
+ .await
+ .expect("rollback ahead fixture");
+
+ let unmanaged = memory_pool().await;
+ let mut transaction = unmanaged
+ .begin_with("BEGIN EXCLUSIVE")
+ .await
+ .expect("unmanaged transaction");
+ let result = rollback_schema_on_connection(&mut transaction, ®istry, 2, 1).await;
+ assert!(matches!(
+ result,
+ Err(RadrootsOutboxError::RollbackUnmanaged)
+ ));
+ transaction
+ .rollback()
+ .await
+ .expect("rollback unmanaged fixture");
+ }
+
+ #[tokio::test]
+ async fn post_up_authority_failure_rolls_back_catalog_ledger_and_caller_state() {
+ let pool = memory_pool().await;
+ sqlx::raw_sql(
+ "CREATE TABLE caller_state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
+ INSERT INTO caller_state(key, value) VALUES ('victoria', 'preserved');",
+ )
+ .execute(&pool)
+ .await
+ .expect("caller state");
+ let mut registry = synthetic_v2_registry(&pool).await;
+
+ let mut connection = pool.acquire().await.expect("catalog connection");
+ let before_catalog = read_catalog_bounded(&mut connection, ®istry)
+ .await
+ .expect("before catalog");
+ let before_fingerprint = catalog_fingerprint(&governed_catalog(&before_catalog, ®istry));
+ let before_history = read_history_bounded(&mut connection, 2)
+ .await
+ .expect("before history");
+ drop(connection);
+
+ registry[1].owned_object_names = &["outbox_expected"];
+ registry[1].owned_table_names = &["outbox_expected"];
+ let error = migrate_outbox_schema_with_registry(&pool, ®istry, 1, 2)
+ .await
+ .expect_err("post-UP catalog delta must fail");
+ assert!(matches!(
+ error,
+ RadrootsOutboxError::MigrationCatalogDeltaMismatch {
+ version: 2,
+ direction: "up",
+ ..
+ }
+ ));
+
+ let mut connection = pool.acquire().await.expect("verification connection");
+ let after_catalog = read_catalog_bounded(&mut connection, ®istry)
+ .await
+ .expect("after catalog");
+ let after_fingerprint = catalog_fingerprint(&governed_catalog(&after_catalog, ®istry));
+ let after_history = read_history_bounded(&mut connection, 2)
+ .await
+ .expect("after history");
+ drop(connection);
+ assert_eq!(after_fingerprint, before_fingerprint);
+ assert_eq!(after_history, before_history);
+ assert_eq!(
+ inspect_outbox_schema_status(&pool)
+ .await
+ .expect("restored managed schema"),
+ RadrootsOutboxSchemaStatus::Managed { version: 1 }
+ );
+ let future_objects: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE name = 'outbox_future'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("rolled-back future object count");
+ assert_eq!(future_objects, 0);
+ let caller_value: String =
+ sqlx::query_scalar("SELECT value FROM caller_state WHERE key = 'victoria'")
+ .fetch_one(&pool)
+ .await
+ .expect("preserved caller row");
+ assert_eq!(caller_value, "preserved");
+ }
+
+ #[tokio::test]
+ async fn exact_unledgered_baseline_is_adopted_without_replaying_or_losing_caller_state() {
+ let pool = memory_pool().await;
+ sqlx::query("CREATE TABLE caller_state (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
+ .execute(&pool)
+ .await
+ .expect("caller table");
+ sqlx::query("INSERT INTO caller_state(key, value) VALUES ('victoria', 'preserved')")
+ .execute(&pool)
+ .await
+ .expect("caller row");
+ apply_unledgered_baseline(&pool).await;
+ sqlx::query(
+ "INSERT INTO outbox_operations(operation_kind, expected_pubkey, semantic_scope, trade_id, mutation_id, canonical_payload_sha256, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms)
+ VALUES ('post', 'author', 'generic_event', NULL, NULL, NULL, NULL, ?, 'queued', 1, 1)",
+ )
+ .bind("a".repeat(64))
+ .execute(&pool)
+ .await
+ .expect("legacy row");
+
+ assert_eq!(
+ inspect_outbox_schema_status(&pool)
+ .await
+ .expect("unledgered status"),
+ RadrootsOutboxSchemaStatus::UnledgeredBaseline
+ );
+ migrate_outbox_schema(&pool).await.expect("adoption");
+ assert_eq!(ledger_count(&pool).await, 1);
+ let caller: String =
+ sqlx::query_scalar("SELECT value FROM caller_state WHERE key = 'victoria'")
+ .fetch_one(&pool)
+ .await
+ .expect("caller row preserved");
+ assert_eq!(caller, "preserved");
+ let operations: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM outbox_operations")
+ .fetch_one(&pool)
+ .await
+ .expect("legacy row count");
+ assert_eq!(operations, 1);
+ }
+
+ #[tokio::test]
+ async fn test_only_destruction_handles_unledgered_and_uninitialized_schemas() {
+ let pool = memory_pool().await;
+ apply_unledgered_baseline(&pool).await;
+ destroy_outbox_schema_for_migration_test(&pool)
+ .await
+ .expect("destroy unledgered baseline");
+ assert_eq!(
+ inspect_outbox_schema_status(&pool)
+ .await
+ .expect("uninitialized after destruction"),
+ RadrootsOutboxSchemaStatus::Uninitialized
+ );
+ destroy_outbox_schema_for_migration_test(&pool)
+ .await
+ .expect("destroy uninitialized schema");
+ }
+
+ #[tokio::test]
+ async fn fresh_initialization_preserves_unrelated_caller_schema_and_rows() {
+ let pool = memory_pool().await;
+ sqlx::raw_sql(
+ "CREATE TABLE caller_table (value TEXT NOT NULL);
+ INSERT INTO caller_table(value) VALUES ('keep');
+ CREATE INDEX caller_table_value_idx ON caller_table(value);",
+ )
+ .execute(&pool)
+ .await
+ .expect("caller schema");
+ migrate_outbox_schema(&pool).await.expect("migration");
+ let value: String = sqlx::query_scalar("SELECT value FROM caller_table")
+ .fetch_one(&pool)
+ .await
+ .expect("caller row");
+ assert_eq!(value, "keep");
+ let index_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE name = 'caller_table_value_idx'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("caller index");
+ assert_eq!(index_count, 1);
+ }
+
+ #[tokio::test]
+ async fn partial_changed_and_unknown_unledgered_outbox_catalogs_fail_before_adoption() {
+ for mutation in [
+ "DROP INDEX outbox_event_event_id_idx",
+ "DROP INDEX outbox_event_event_id_idx; CREATE INDEX outbox_event_event_id_idx ON outbox_event(expected_pubkey)",
+ "CREATE TABLE outbox_counterfeit (value TEXT NOT NULL)",
+ ] {
+ let pool = memory_pool().await;
+ apply_unledgered_baseline(&pool).await;
+ sqlx::raw_sql(mutation)
+ .execute(&pool)
+ .await
+ .expect("catalog mutation");
+ assert!(matches!(
+ migrate_outbox_schema(&pool).await,
+ Err(RadrootsOutboxError::UnmanagedSchema { .. })
+ ));
+ assert_eq!(ledger_count(&pool).await, 0);
+ }
+ }
+
+ #[tokio::test]
+ async fn counterfeit_ledger_shape_fails_before_any_outbox_schema_mutation() {
+ let pool = memory_pool().await;
+ sqlx::query("CREATE TABLE radroots_outbox_schema_migrations (version INTEGER)")
+ .execute(&pool)
+ .await
+ .expect("counterfeit ledger");
+ assert!(matches!(
+ migrate_outbox_schema(&pool).await,
+ Err(RadrootsOutboxError::MigrationLedgerDrift { .. })
+ ));
+ let outbox_objects: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE lower(substr(name, 1, 7)) = 'outbox_'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("outbox object count");
+ assert_eq!(outbox_objects, 0);
+ }
+
+ #[tokio::test]
+ async fn ledger_name_checksum_and_catalog_mutations_fail_closed() {
+ for statement in [
+ "UPDATE radroots_outbox_schema_migrations SET name = 'counterfeit' WHERE version = 1",
+ "UPDATE radroots_outbox_schema_migrations SET up_sha256 = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' WHERE version = 1",
+ "UPDATE radroots_outbox_schema_migrations SET schema_sha256 = 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' WHERE version = 1",
+ "DROP INDEX outbox_event_event_id_idx; CREATE INDEX outbox_event_event_id_idx ON outbox_event(expected_pubkey)",
+ ] {
+ let pool = memory_pool().await;
+ migrate_outbox_schema(&pool).await.expect("migration");
+ sqlx::raw_sql(statement)
+ .execute(&pool)
+ .await
+ .expect("managed mutation");
+ assert!(migrate_outbox_schema(&pool).await.is_err(), "{statement}");
+ let rows: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM radroots_outbox_schema_migrations")
+ .fetch_one(&pool)
+ .await
+ .expect("ledger rows");
+ assert_eq!(rows, 1);
+ }
+ }
+
+ #[tokio::test]
+ async fn newer_history_and_governed_catalog_overflow_are_bounded_and_rejected() {
+ let newer = memory_pool().await;
+ migrate_outbox_schema(&newer).await.expect("migration");
+ sqlx::query(
+ "INSERT INTO radroots_outbox_schema_migrations(version, name, up_sha256, down_sha256, schema_sha256)
+ VALUES (2, 'future', ?, ?, ?)",
+ )
+ .bind("a".repeat(64))
+ .bind("b".repeat(64))
+ .bind("c".repeat(64))
+ .execute(&newer)
+ .await
+ .expect("future history");
+ assert!(matches!(
+ inspect_outbox_schema_status(&newer).await,
+ Err(RadrootsOutboxError::SchemaTooNew {
+ current: 1,
+ database: 2
+ })
+ ));
+
+ let overflow = memory_pool().await;
+ migrate_outbox_schema(&overflow).await.expect("migration");
+ sqlx::query("CREATE TABLE outbox_unknown (value TEXT)")
+ .execute(&overflow)
+ .await
+ .expect("unknown governed object");
+ assert!(matches!(
+ inspect_outbox_schema_status(&overflow).await,
+ Err(RadrootsOutboxError::GovernedCatalogCapacityExceeded { max: 14 })
+ ));
+ }
+
+ #[test]
+ fn history_validation_rejects_gaps_and_unknown_versions() {
+ let row = |version, migration: &OutboxMigration| AppliedMigration {
+ version,
+ name: migration.name.to_owned(),
+ up_sha256: migration.up_sha256.to_owned(),
+ down_sha256: migration.down_sha256.to_owned(),
+ schema_sha256: migration.schema_sha256.to_owned(),
+ };
+ assert!(matches!(
+ validate_history_against_registry(
+ &[row(2, &OUTBOX_MIGRATIONS[0])],
+ OUTBOX_MIGRATIONS,
+ 3
+ ),
+ Err(RadrootsOutboxError::MigrationHistoryGap {
+ expected: 1,
+ actual: Some(2)
+ })
+ ));
+ assert!(matches!(
+ validate_history_against_registry(
+ &[row(1, &OUTBOX_MIGRATIONS[0]), row(2, &OUTBOX_MIGRATIONS[0])],
+ OUTBOX_MIGRATIONS,
+ 3,
+ ),
+ Err(RadrootsOutboxError::UnknownMigration { version: 2 })
+ ));
+
+ assert!(matches!(
+ validate_history_against_registry(&[], OUTBOX_MIGRATIONS, 1),
+ Err(RadrootsOutboxError::MigrationLedgerDrift { .. })
+ ));
+ assert!(matches!(
+ validate_history_against_registry(
+ &[row(-1, &OUTBOX_MIGRATIONS[0])],
+ OUTBOX_MIGRATIONS,
+ 1
+ ),
+ Err(RadrootsOutboxError::MigrationLedgerDrift { .. })
+ ));
+ assert_eq!(
+ validate_history_against_registry(
+ &[row(1, &OUTBOX_MIGRATIONS[0])],
+ OUTBOX_MIGRATIONS,
+ 1
+ )
+ .expect("canonical history"),
+ 1
+ );
+
+ let mut name_drift = row(1, &OUTBOX_MIGRATIONS[0]);
+ name_drift.name = "counterfeit".to_owned();
+ assert!(matches!(
+ validate_history_against_registry(&[name_drift], OUTBOX_MIGRATIONS, 1),
+ Err(RadrootsOutboxError::MigrationHistoryNameDrift { .. })
+ ));
+ for field in ["up_sha256", "down_sha256", "schema_sha256"] {
+ let mut checksum_drift = row(1, &OUTBOX_MIGRATIONS[0]);
+ match field {
+ "up_sha256" => checksum_drift.up_sha256 = "a".repeat(64),
+ "down_sha256" => checksum_drift.down_sha256 = "a".repeat(64),
+ "schema_sha256" => checksum_drift.schema_sha256 = "a".repeat(64),
+ _ => unreachable!(),
+ }
+ assert!(matches!(
+ validate_history_against_registry(&[checksum_drift], OUTBOX_MIGRATIONS, 1),
+ Err(RadrootsOutboxError::MigrationHistoryChecksumDrift {
+ field: actual_field,
+ ..
+ }) if actual_field == field
+ ));
+ }
+ }
+
+ fn catalog_row(
+ object_type: &str,
+ name: &str,
+ table_name: &str,
+ sql: Option<&str>,
+ ) -> CatalogRow {
+ CatalogRow {
+ object_type: object_type.to_owned(),
+ name: name.to_owned(),
+ table_name: table_name.to_owned(),
+ sql: sql.map(str::to_owned),
+ }
+ }
+
+ #[test]
+ fn catalog_delta_and_ledger_validators_fail_closed() {
+ let mut migration = OUTBOX_MIGRATIONS[0];
+ migration.version = 2;
+ migration.owned_object_names = &["outbox_future"];
+ migration.owned_table_names = &["outbox_future"];
+ let future = catalog_row(
+ "table",
+ "outbox_future",
+ "outbox_future",
+ Some("CREATE TABLE outbox_future (value TEXT)"),
+ );
+ validate_catalog_delta(&[], std::slice::from_ref(&future), &migration, "up")
+ .expect("additive delta");
+ validate_catalog_delta(std::slice::from_ref(&future), &[], &migration, "down")
+ .expect("rollback delta");
+ assert!(matches!(
+ validate_catalog_delta(&[], std::slice::from_ref(&future), &migration, "sideways"),
+ Err(RadrootsOutboxError::MigrationCatalogDeltaMismatch { .. })
+ ));
+ let changed = catalog_row(
+ "table",
+ "outbox_future",
+ "outbox_future",
+ Some("CREATE TABLE outbox_future (changed TEXT)"),
+ );
+ assert!(matches!(
+ validate_catalog_delta(
+ std::slice::from_ref(&future),
+ std::slice::from_ref(&changed),
+ &migration,
+ "up"
+ ),
+ Err(RadrootsOutboxError::MigrationCatalogDeltaMismatch { .. })
+ ));
+
+ assert!(!validate_ledger_catalog(&[]).expect("absent ledger"));
+ let canonical = catalog_row(
+ "table",
+ OUTBOX_LEDGER_NAME,
+ OUTBOX_LEDGER_NAME,
+ Some(OUTBOX_LEDGER_DDL),
+ );
+ assert!(validate_ledger_catalog(std::slice::from_ref(&canonical)).expect("ledger"));
+ assert!(matches!(
+ validate_ledger_catalog(&[canonical.clone(), canonical.clone()]),
+ Err(RadrootsOutboxError::MigrationLedgerDrift { .. })
+ ));
+ for counterfeit in [
+ catalog_row(
+ "view",
+ OUTBOX_LEDGER_NAME,
+ OUTBOX_LEDGER_NAME,
+ Some(OUTBOX_LEDGER_DDL),
+ ),
+ catalog_row(
+ "table",
+ "counterfeit",
+ OUTBOX_LEDGER_NAME,
+ Some(OUTBOX_LEDGER_DDL),
+ ),
+ catalog_row(
+ "table",
+ OUTBOX_LEDGER_NAME,
+ "counterfeit",
+ Some(OUTBOX_LEDGER_DDL),
+ ),
+ catalog_row(
+ "table",
+ OUTBOX_LEDGER_NAME,
+ OUTBOX_LEDGER_NAME,
+ Some("counterfeit"),
+ ),
+ ] {
+ assert!(matches!(
+ validate_ledger_catalog(&[counterfeit]),
+ Err(RadrootsOutboxError::MigrationLedgerDrift { .. })
+ ));
+ }
+ }
+
+ #[tokio::test]
+ async fn temporary_authority_collisions_are_rejected_before_migration() {
+ for sql in [
+ "CREATE TEMP TABLE outbox_event (value TEXT)",
+ "CREATE TEMP TABLE radroots_outbox_schema_migrations (value TEXT)",
+ "CREATE TEMP VIEW caller_view AS SELECT 1 AS value",
+ ] {
+ let pool = memory_pool().await;
+ sqlx::raw_sql(sql)
+ .execute(&pool)
+ .await
+ .expect("temp fixture");
+ assert!(matches!(
+ migrate_outbox_schema(&pool).await,
+ Err(RadrootsOutboxError::TemporarySchemaCollision { .. })
+ ));
+ assert_eq!(ledger_count(&pool).await, 0);
+ }
+ }
+
+ #[tokio::test]
+ async fn current_schema_reopen_is_idempotent_and_does_not_rewrite_history() {
+ let pool = memory_pool().await;
+ migrate_outbox_schema(&pool).await.expect("first migration");
+ let before_changes: i64 = sqlx::query_scalar("SELECT total_changes()")
+ .fetch_one(&pool)
+ .await
+ .expect("before total changes");
+ migrate_outbox_schema(&pool)
+ .await
+ .expect("current migration");
+ let after_changes: i64 = sqlx::query_scalar("SELECT total_changes()")
+ .fetch_one(&pool)
+ .await
+ .expect("after total changes");
+ assert_eq!(before_changes, after_changes);
+ assert_eq!(ledger_count(&pool).await, 1);
+ }
+
+ #[tokio::test]
+ async fn concurrent_file_initializers_serialize_to_one_exact_history() {
+ let directory = tempfile::tempdir().expect("tempdir");
+ let path = directory.path().join("concurrent-outbox.sqlite");
+ let participants = 6;
+ let barrier = Arc::new(Barrier::new(participants));
+ let mut tasks = Vec::new();
+ for _ in 0..participants {
+ let barrier = Arc::clone(&barrier);
+ let path = path.clone();
+ tasks.push(tokio::spawn(async move {
+ barrier.wait().await;
+ let store = RadrootsOutbox::open_file(path).await?;
+ store.schema_status().await
+ }));
+ }
+ for task in tasks {
+ assert_eq!(
+ task.await.expect("initializer task").expect("initializer"),
+ RadrootsOutboxSchemaStatus::Managed { version: 1 }
+ );
+ }
+ let store = RadrootsOutbox::open_file(&path)
+ .await
+ .expect("verification open");
+ let history: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM radroots_outbox_schema_migrations")
+ .fetch_one(store.pool())
+ .await
+ .expect("history count");
+ assert_eq!(history, 1);
+ }
+
+ #[tokio::test]
+ async fn terminal_target_rollback_preserves_v1_rows_and_closes_every_clone() {
+ let directory = tempfile::tempdir().expect("tempdir");
+ let path = directory.path().join("rollback-outbox.sqlite");
+ let store = RadrootsOutbox::open_file(&path).await.expect("open");
+ sqlx::query(
+ "INSERT INTO outbox_operations(operation_kind, expected_pubkey, semantic_scope, trade_id, mutation_id, canonical_payload_sha256, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms)
+ VALUES ('post', 'author', 'generic_event', NULL, NULL, NULL, NULL, ?, 'queued', 1, 1)",
+ )
+ .bind("a".repeat(64))
+ .execute(store.pool())
+ .await
+ .expect("queued row");
+ let clone = store.clone();
+ store
+ .rollback_to_schema_version_and_close(1)
+ .await
+ .expect("rollback to current floor");
+ assert!(clone.pool().is_closed());
+
+ let reopened = RadrootsOutbox::open_file(&path).await.expect("reopen");
+ let rows: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM outbox_operations")
+ .fetch_one(reopened.pool())
+ .await
+ .expect("preserved queued rows");
+ assert_eq!(rows, 1);
+ assert!(matches!(
+ rollback_outbox_schema_offline(reopened.pool(), 2).await,
+ Err(RadrootsOutboxError::RollbackAhead {
+ current: 1,
+ target: 2
+ })
+ ));
+ }
+
+ #[tokio::test]
+ async fn reopen_rejects_outbox_foreign_key_corruption() {
+ let directory = tempfile::tempdir().expect("tempdir");
+ let path = directory.path().join("foreign-key-outbox.sqlite");
+ let store = RadrootsOutbox::open_file(&path).await.expect("open");
+ store.pool().close().await;
+
+ let mut connection = SqliteConnection::connect_with(
+ &SqliteConnectOptions::new()
+ .filename(&path)
+ .foreign_keys(false),
+ )
+ .await
+ .expect("corruption connection");
+ sqlx::query(
+ "INSERT INTO outbox_event(operation_id, event_id, expected_pubkey, draft_json, signed_event_json, raw_event_json, state, attempt_count, claim_token, claim_owner, claim_expires_at_ms, active_delivery_plan_id, next_attempt_after_ms, last_error, event_store_ingested, event_store_inserted, event_store_ingested_at_ms, created_at_ms, updated_at_ms)
+ VALUES (999, 'event', 'author', '{}', NULL, NULL, 'draft_queued', 0, NULL, NULL, NULL, NULL, 0, NULL, 0, 0, NULL, 0, 0)",
+ )
+ .execute(&mut connection)
+ .await
+ .expect("invalid child row");
+ connection
+ .close()
+ .await
+ .expect("close corruption connection");
+
+ assert!(matches!(
+ RadrootsOutbox::open_file(&path).await,
+ Err(RadrootsOutboxError::ForeignKeyViolation { table, .. })
+ if table == "outbox_event"
+ ));
+ }
+
+ #[tokio::test]
+ async fn open_rejects_utf16_before_journal_or_schema_mutation() {
+ let directory = tempfile::tempdir().expect("tempdir");
+ let path = directory.path().join("utf16-outbox.sqlite");
+ let mut connection = SqliteConnection::connect_with(
+ &SqliteConnectOptions::new()
+ .filename(&path)
+ .create_if_missing(true),
+ )
+ .await
+ .expect("UTF-16 fixture connection");
+ sqlx::query("PRAGMA main.encoding = 'UTF-16le'")
+ .execute(&mut connection)
+ .await
+ .expect("set UTF-16");
+ sqlx::query("CREATE TABLE encoding_anchor (value TEXT NOT NULL)")
+ .execute(&mut connection)
+ .await
+ .expect("materialize UTF-16");
+ sqlx::query("DROP TABLE encoding_anchor")
+ .execute(&mut connection)
+ .await
+ .expect("empty UTF-16 catalog");
+ connection.close().await.expect("close fixture");
+
+ assert!(matches!(
+ RadrootsOutbox::open_file(&path).await,
+ Err(RadrootsOutboxError::SqliteMainDatabaseEncodingNotUtf8 { actual })
+ if actual == "UTF-16le"
+ ));
+ let mut verifier =
+ SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(&path))
+ .await
+ .expect("verifier");
+ let encoding: String = sqlx::query_scalar("PRAGMA main.encoding")
+ .fetch_one(&mut verifier)
+ .await
+ .expect("encoding");
+ let journal: String = sqlx::query_scalar("PRAGMA main.journal_mode")
+ .fetch_one(&mut verifier)
+ .await
+ .expect("journal");
+ let objects: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE lower(substr(name, 1, 7)) = 'outbox_' OR name = ?",
+ )
+ .bind(OUTBOX_LEDGER_NAME)
+ .fetch_one(&mut verifier)
+ .await
+ .expect("outbox objects");
+ assert_eq!(encoding, "UTF-16le");
+ assert_eq!(journal, "delete");
+ assert_eq!(objects, 0);
+ }
+}
diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs
@@ -1,7 +1,6 @@
#![forbid(unsafe_code)]
use crate::RadrootsOutboxError;
-use crate::migrations::{OUTBOX_MIGRATION_DOWN, OUTBOX_MIGRATION_UP};
use crate::model::{
RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryAttemptRecord,
RadrootsOutboxDeliveryPlanInput, RadrootsOutboxDeliveryPlanRecord,
@@ -14,6 +13,12 @@ use crate::model::{
RadrootsOutboxSignedTradeMutationInput, RadrootsOutboxStatusSummary,
RadrootsOutboxTradeMutationInput,
};
+#[cfg(test)]
+use crate::schema::destroy_outbox_schema_for_migration_test;
+use crate::schema::{
+ RadrootsOutboxSchemaStatus, inspect_outbox_schema_status, migrate_outbox_schema,
+ rollback_outbox_schema_offline,
+};
use radroots_event::RadrootsEventKindClass;
use radroots_event::draft::{
RadrootsEventDraft, RadrootsSignedEvent, validate_signed_nostr_event_matches_draft,
@@ -34,10 +39,10 @@ use radroots_transport::{
};
use serde::Serialize;
use sha2::{Digest, Sha256};
-use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteQueryResult};
#[cfg(test)]
-use sqlx::{Connection, SqliteConnection};
-use sqlx::{Row, SqlitePool};
+use sqlx::Connection;
+use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteQueryResult};
+use sqlx::{Row, SqliteConnection, SqlitePool};
use std::collections::BTreeSet;
use std::path::Path;
use std::str::FromStr;
@@ -56,7 +61,7 @@ impl RadrootsOutbox {
.connect_with(options)
.await?;
configure_pool(&pool, false).await?;
- apply_up(&pool).await?;
+ migrate_outbox_schema(&pool).await?;
Ok(Self { pool })
}
@@ -69,7 +74,7 @@ impl RadrootsOutbox {
.connect_with(options)
.await?;
configure_pool(&pool, true).await?;
- apply_up(&pool).await?;
+ migrate_outbox_schema(&pool).await?;
Ok(Self { pool })
}
@@ -78,16 +83,43 @@ impl RadrootsOutbox {
file_backed: bool,
) -> Result<Self, RadrootsOutboxError> {
configure_pool(&pool, file_backed).await?;
- apply_up(&pool).await?;
+ migrate_outbox_schema(&pool).await?;
Ok(Self { pool })
}
+ /// Returns the fully trusted database-authority escape hatch.
+ ///
+ /// Arbitrary SQL can invalidate the managed outbox schema. Prefer the
+ /// typed outbox methods for ordinary writes.
pub fn pool(&self) -> &SqlitePool {
&self.pool
}
- pub async fn migrate_down(&self) -> Result<(), RadrootsOutboxError> {
- apply_down(&self.pool).await
+ /// Inspects and authenticates the current managed schema state.
+ pub async fn schema_status(&self) -> Result<RadrootsOutboxSchemaStatus, RadrootsOutboxError> {
+ inspect_outbox_schema_status(&self.pool).await
+ }
+
+ /// Serializes schema adoption or migration to the current supported version.
+ pub async fn migrate_to_current_schema(&self) -> Result<(), RadrootsOutboxError> {
+ migrate_outbox_schema(&self.pool).await
+ }
+
+ /// Closes every clone after attempting an exclusive, target-version rollback.
+ ///
+ /// Independent pools for the same file must be quiesced by the caller.
+ pub async fn rollback_to_schema_version_and_close(
+ self,
+ target: u32,
+ ) -> Result<(), RadrootsOutboxError> {
+ let result = rollback_outbox_schema_offline(&self.pool, target).await;
+ self.pool.close().await;
+ result
+ }
+
+ #[cfg(test)]
+ async fn destroy_schema_for_migration_test(&self) -> Result<(), RadrootsOutboxError> {
+ destroy_outbox_schema_for_migration_test(&self.pool).await
}
pub async fn pragma_foreign_keys(&self) -> Result<i64, RadrootsOutboxError> {
@@ -1805,55 +1837,124 @@ fn publish_lifecycle_from_plan_evaluation<'a>(
async fn configure_pool(pool: &SqlitePool, file_backed: bool) -> Result<(), RadrootsOutboxError> {
let max_connections = pool.options().get_max_connections();
- let existing_options = pool.connect_options();
- let main_filename: String =
- sqlx::query_scalar("SELECT file FROM pragma_database_list WHERE name = 'main'")
- .fetch_one(pool)
- .await?;
- let database_is_memory = main_filename.is_empty();
- if file_backed == database_is_memory {
- return Err(RadrootsOutboxError::SqlitePoolBackingMismatch {
- file_backed,
- filename: main_filename,
- });
- }
if !file_backed && max_connections != 1 {
return Err(RadrootsOutboxError::UnsafeInMemoryPoolConnectionCount {
actual: max_connections,
});
}
- 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);
-
let mut connections = Vec::with_capacity(max_connections as usize);
for _ in 0..max_connections {
connections.push(pool.acquire().await?);
}
for connection in &mut connections {
+ let main_filename = main_database_filename(connection).await?;
+ let database_is_memory = main_filename.is_empty();
+ if file_backed == database_is_memory {
+ return Err(RadrootsOutboxError::SqlitePoolBackingMismatch {
+ file_backed,
+ filename: main_filename,
+ });
+ }
+ validate_main_database_encoding(connection).await?;
+ crate::schema::validate_outbox_temp_schema(connection).await?;
sqlx::query("PRAGMA foreign_keys = ON")
.execute(&mut **connection)
.await?;
+ let foreign_keys: i64 = sqlx::query_scalar("PRAGMA foreign_keys")
+ .fetch_one(&mut **connection)
+ .await?;
+ if foreign_keys != 1 {
+ return Err(RadrootsOutboxError::SqliteForeignKeysNotEnabled {
+ actual: foreign_keys,
+ });
+ }
sqlx::query("PRAGMA busy_timeout = 5000")
.execute(&mut **connection)
.await?;
if file_backed {
- let actual = sqlx::query_scalar::<_, String>("PRAGMA main.journal_mode = WAL")
- .fetch_one(&mut **connection)
- .await?;
- if actual != "wal" {
+ 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 validate_main_database_encoding(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsOutboxError> {
+ let actual: String = sqlx::query_scalar("PRAGMA main.encoding")
+ .fetch_one(&mut *connection)
+ .await?;
+ if actual == "UTF-8" {
+ return Ok(());
+ }
+ Err(RadrootsOutboxError::SqliteMainDatabaseEncodingNotUtf8 { actual })
+}
+
+async fn configure_file_journal_mode(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsOutboxError> {
+ const RETRY_ATTEMPTS: usize = 100;
+ const RETRY_DELAY: Duration = Duration::from_millis(50);
+
+ for attempt in 0..RETRY_ATTEMPTS {
+ match sqlx::query_scalar::<_, String>("PRAGMA main.journal_mode = WAL")
+ .fetch_one(&mut *connection)
+ .await
+ {
+ Ok(actual) if actual == "wal" => return Ok(()),
+ Ok(actual) => {
return Err(RadrootsOutboxError::SqliteFileJournalModeNotWal { actual });
}
+ Err(error) if attempt + 1 < RETRY_ATTEMPTS && is_sqlite_lock_contention(&error) => {
+ tokio::time::sleep(RETRY_DELAY).await;
+ }
+ Err(error) => return Err(error.into()),
}
}
- Ok(())
+ unreachable!("bounded journal-mode retry exits from every terminal branch")
+}
+
+fn is_sqlite_lock_contention(error: &sqlx::Error) -> bool {
+ matches!(
+ error,
+ sqlx::Error::Database(database)
+ if database
+ .code()
+ .as_deref()
+ .is_some_and(sqlite_code_is_lock_contention)
+ )
+}
+
+fn sqlite_code_is_lock_contention(code: &str) -> bool {
+ code.parse::<u32>()
+ .is_ok_and(|code| matches!(code & 0xff, 5 | 6))
+}
+
+async fn main_database_filename(
+ connection: &mut SqliteConnection,
+) -> Result<String, RadrootsOutboxError> {
+ let rows = sqlx::query("PRAGMA database_list")
+ .fetch_all(&mut *connection)
+ .await?;
+ for row in rows {
+ if row.try_get::<String, _>("name")? == "main" {
+ return Ok(row.try_get("file")?);
+ }
+ }
+ Err(RadrootsOutboxError::SqliteMainDatabaseUnavailable)
}
#[cfg(test)]
@@ -1880,18 +1981,6 @@ async fn file_pool_with_immutable_delete_journal(path: &Path) -> SqlitePool {
}
#[cfg_attr(coverage_nightly, coverage(off))]
-async fn apply_up(pool: &SqlitePool) -> Result<(), RadrootsOutboxError> {
- sqlx::raw_sql(OUTBOX_MIGRATION_UP).execute(pool).await?;
- Ok(())
-}
-
-#[cfg_attr(coverage_nightly, coverage(off))]
-async fn apply_down(pool: &SqlitePool) -> Result<(), RadrootsOutboxError> {
- sqlx::raw_sql(OUTBOX_MIGRATION_DOWN).execute(pool).await?;
- Ok(())
-}
-
-#[cfg_attr(coverage_nightly, coverage(off))]
async fn query_i64(pool: &SqlitePool, sql: &'static str) -> Result<i64, RadrootsOutboxError> {
let row = sqlx::query(sql).fetch_one(pool).await?;
Ok(row.try_get(0)?)
@@ -4179,7 +4268,7 @@ mod tests {
}
#[tokio::test]
- async fn migration_applies_delivery_plan_schema_and_migrates_down() {
+ async fn migration_applies_delivery_plan_schema_and_uses_explicit_test_destruction() {
let outbox = RadrootsOutbox::open_memory().await.expect("open");
assert_eq!(outbox.pragma_foreign_keys().await.expect("foreign keys"), 1);
@@ -4236,7 +4325,23 @@ mod tests {
"outcome_kind"
);
- outbox.migrate_down().await.expect("migrate down");
+ assert_eq!(
+ outbox.schema_status().await.expect("managed schema"),
+ RadrootsOutboxSchemaStatus::Managed {
+ version: crate::RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ }
+ );
+ assert!(matches!(
+ rollback_outbox_schema_offline(outbox.pool(), 0).await,
+ Err(RadrootsOutboxError::RollbackBelowVersionFloor {
+ floor: 1,
+ target: 0
+ })
+ ));
+ outbox
+ .destroy_schema_for_migration_test()
+ .await
+ .expect("test destruction");
let row = sqlx::query(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'outbox_event'",
)
@@ -4244,6 +4349,12 @@ mod tests {
.await
.expect("table query");
assert!(row.is_none());
+ assert_eq!(
+ inspect_outbox_schema_status(outbox.pool())
+ .await
+ .expect("uninitialized schema"),
+ RadrootsOutboxSchemaStatus::Uninitialized
+ );
}
#[tokio::test]
@@ -4260,6 +4371,60 @@ mod tests {
}
#[tokio::test]
+ async fn file_wal_configuration_retries_exclusive_lock_contention() {
+ let directory = tempfile::tempdir().expect("tempdir");
+ let path = directory.path().join("wal-contention.sqlite");
+ let options = SqliteConnectOptions::new()
+ .filename(&path)
+ .create_if_missing(true);
+ let mut blocker = SqliteConnection::connect_with(&options)
+ .await
+ .expect("blocker connection");
+ sqlx::query("CREATE TABLE lock_anchor (value TEXT NOT NULL)")
+ .execute(&mut blocker)
+ .await
+ .expect("materialize database");
+ let mut contender = SqliteConnection::connect_with(&options)
+ .await
+ .expect("contender connection");
+ sqlx::query("PRAGMA busy_timeout = 0")
+ .execute(&mut contender)
+ .await
+ .expect("disable SQLite busy wait");
+ sqlx::query("BEGIN EXCLUSIVE")
+ .execute(&mut blocker)
+ .await
+ .expect("exclusive blocker");
+
+ let release = tokio::spawn(async move {
+ tokio::time::sleep(Duration::from_millis(10)).await;
+ sqlx::query("COMMIT")
+ .execute(&mut blocker)
+ .await
+ .expect("release exclusive blocker");
+ });
+ configure_file_journal_mode(&mut contender)
+ .await
+ .expect("retry WAL transition");
+ release.await.expect("blocker task");
+ let actual: String = sqlx::query_scalar("PRAGMA main.journal_mode")
+ .fetch_one(&mut contender)
+ .await
+ .expect("journal mode");
+ assert_eq!(actual, "wal");
+ }
+
+ #[test]
+ fn sqlite_lock_contention_recognizes_primary_and_extended_codes() {
+ for code in ["5", "6", "261", "262", "517", "773"] {
+ assert!(sqlite_code_is_lock_contention(code), "{code}");
+ }
+ for code in ["", "not-a-code", "0", "1", "7", "260"] {
+ assert!(!sqlite_code_is_lock_contention(code), "{code}");
+ }
+ }
+
+ #[tokio::test]
async fn constructors_preflight_and_transactional_enqueue_cover_public_storage_surfaces() {
let directory = tempfile::tempdir().expect("tempdir");
let file_outbox = RadrootsOutbox::open_file(directory.path().join("outbox.sqlite"))
diff --git a/crates/outbox/tests/fixtures/migration_authority.v1.json b/crates/outbox/tests/fixtures/migration_authority.v1.json
@@ -0,0 +1,105 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_outbox.migration_authority.v1",
+ "executor": {
+ "id": "radroots_outbox.migration_authority_v1.result_vector_executor.v1",
+ "path": "crates/outbox/tests/migration_authority_v1_result_vector.rs",
+ "test": "migration_authority_v1_result_vector"
+ },
+ "delegated_suite": {
+ "lane": "nix run .#contract",
+ "package": "radroots_outbox",
+ "authorities": [
+ {
+ "authority": "migration_source_discovery_is_exact_and_fail_closed",
+ "authority_path": "crates/outbox/src/migrations.rs"
+ },
+ {
+ "authority": "ledger_name_checksum_and_catalog_mutations_fail_closed",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "newer_history_and_governed_catalog_overflow_are_bounded_and_rejected",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "history_validation_rejects_gaps_and_unknown_versions",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "temporary_authority_collisions_are_rejected_before_migration",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "concurrent_file_initializers_serialize_to_one_exact_history",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "terminal_target_rollback_preserves_v1_rows_and_closes_every_clone",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "reopen_rejects_outbox_foreign_key_corruption",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "open_rejects_utf16_before_journal_or_schema_mutation",
+ "authority_path": "crates/outbox/src/schema.rs"
+ },
+ {
+ "authority": "file_pool_rejects_successful_non_wal_journal_result",
+ "authority_path": "crates/outbox/src/store.rs"
+ }
+ ]
+ },
+ "cases": [
+ {
+ "id": "fresh_initialization",
+ "execution": "direct_executor",
+ "expected_outcome": "managed_v1",
+ "expected_error": null
+ },
+ {
+ "id": "exact_unledgered_adoption",
+ "execution": "direct_executor",
+ "expected_outcome": "managed_v1_without_replay",
+ "expected_error": null
+ },
+ {
+ "id": "partial_unledgered_rejected",
+ "execution": "direct_executor",
+ "expected_outcome": "rejected_before_mutation",
+ "expected_error": "UnmanagedSchema"
+ },
+ {
+ "id": "ledger_checksum_tamper_rejected",
+ "execution": "direct_executor",
+ "expected_outcome": "rejected_before_mutation",
+ "expected_error": "MigrationHistoryChecksumDrift"
+ },
+ {
+ "id": "newer_history_rejected",
+ "execution": "direct_executor",
+ "expected_outcome": "rejected_before_mutation",
+ "expected_error": "SchemaTooNew"
+ },
+ {
+ "id": "rollback_below_floor_rejected",
+ "execution": "direct_executor",
+ "expected_outcome": "rejected_without_schema_change",
+ "expected_error": "RollbackBelowVersionFloor"
+ },
+ {
+ "id": "caller_state_preserved",
+ "execution": "direct_executor",
+ "expected_outcome": "managed_v1_with_caller_state_preserved",
+ "expected_error": null
+ },
+ {
+ "id": "current_reopen_no_history_write",
+ "execution": "direct_executor",
+ "expected_outcome": "managed_v1_without_history_write",
+ "expected_error": null
+ }
+ ]
+}
diff --git a/crates/outbox/tests/migration_authority_v1_result_vector.rs b/crates/outbox/tests/migration_authority_v1_result_vector.rs
@@ -0,0 +1,252 @@
+#![forbid(unsafe_code)]
+
+use radroots_outbox::{
+ RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT, RadrootsOutbox, RadrootsOutboxError,
+ RadrootsOutboxSchemaStatus,
+};
+use serde::Deserialize;
+use sqlx::SqlitePool;
+use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
+use std::collections::BTreeSet;
+use std::str::FromStr;
+
+const VECTOR_BYTES: &[u8] = include_bytes!("fixtures/migration_authority.v1.json");
+const BASELINE_UP: &str = include_str!("../migrations/0001_outbox.up.sql");
+const SCHEMA_SOURCE: &str = include_str!("../src/schema.rs");
+const MIGRATIONS_SOURCE: &str = include_str!("../src/migrations.rs");
+const STORE_SOURCE: &str = include_str!("../src/store.rs");
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Vector {
+ schema_version: u32,
+ contract_id: String,
+ executor: Executor,
+ delegated_suite: DelegatedSuite,
+ cases: Vec<Case>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Executor {
+ id: String,
+ path: String,
+ test: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct DelegatedSuite {
+ lane: String,
+ package: String,
+ authorities: Vec<Authority>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Authority {
+ authority: String,
+ authority_path: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Case {
+ id: String,
+ execution: String,
+ expected_outcome: String,
+ expected_error: Option<String>,
+}
+
+async fn memory_pool() -> SqlitePool {
+ SqlitePoolOptions::new()
+ .max_connections(1)
+ .connect_with(SqliteConnectOptions::from_str("sqlite::memory:").expect("options"))
+ .await
+ .expect("pool")
+}
+
+async fn execute_case(case: &Case) {
+ assert_eq!(case.execution, "direct_executor");
+ match case.id.as_str() {
+ "fresh_initialization" => {
+ let store = RadrootsOutbox::open_memory().await.expect("fresh store");
+ assert_eq!(
+ store.schema_status().await.expect("status"),
+ RadrootsOutboxSchemaStatus::Managed {
+ version: RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT,
+ }
+ );
+ assert_eq!(case.expected_outcome, "managed_v1");
+ }
+ "exact_unledgered_adoption" => {
+ let pool = memory_pool().await;
+ sqlx::raw_sql(BASELINE_UP)
+ .execute(&pool)
+ .await
+ .expect("baseline");
+ let changes: i64 = sqlx::query_scalar("SELECT total_changes()")
+ .fetch_one(&pool)
+ .await
+ .expect("changes");
+ let store = RadrootsOutbox::open_pool(pool, false)
+ .await
+ .expect("adoption");
+ let after: i64 = sqlx::query_scalar("SELECT total_changes()")
+ .fetch_one(store.pool())
+ .await
+ .expect("after changes");
+ assert_eq!(after - changes, 1, "only the ledger row is inserted");
+ assert_eq!(case.expected_outcome, "managed_v1_without_replay");
+ }
+ "partial_unledgered_rejected" => {
+ let pool = memory_pool().await;
+ sqlx::raw_sql(BASELINE_UP)
+ .execute(&pool)
+ .await
+ .expect("baseline");
+ sqlx::query("DROP INDEX outbox_event_event_id_idx")
+ .execute(&pool)
+ .await
+ .expect("partial schema");
+ assert!(matches!(
+ RadrootsOutbox::open_pool(pool, false).await,
+ Err(RadrootsOutboxError::UnmanagedSchema { .. })
+ ));
+ assert_eq!(case.expected_outcome, "rejected_before_mutation");
+ assert_eq!(case.expected_error.as_deref(), Some("UnmanagedSchema"));
+ }
+ "ledger_checksum_tamper_rejected" => {
+ let store = RadrootsOutbox::open_memory().await.expect("store");
+ sqlx::query(
+ "UPDATE radroots_outbox_schema_migrations SET up_sha256 = ? WHERE version = 1",
+ )
+ .bind("b".repeat(64))
+ .execute(store.pool())
+ .await
+ .expect("tamper");
+ assert!(matches!(
+ store.schema_status().await,
+ Err(RadrootsOutboxError::MigrationHistoryChecksumDrift { .. })
+ ));
+ assert_eq!(case.expected_outcome, "rejected_before_mutation");
+ assert_eq!(
+ case.expected_error.as_deref(),
+ Some("MigrationHistoryChecksumDrift")
+ );
+ }
+ "newer_history_rejected" => {
+ let store = RadrootsOutbox::open_memory().await.expect("store");
+ sqlx::query(
+ "INSERT INTO radroots_outbox_schema_migrations(version, name, up_sha256, down_sha256, schema_sha256) VALUES (2, 'future', ?, ?, ?)",
+ )
+ .bind("a".repeat(64))
+ .bind("b".repeat(64))
+ .bind("c".repeat(64))
+ .execute(store.pool())
+ .await
+ .expect("future row");
+ assert!(matches!(
+ store.schema_status().await,
+ Err(RadrootsOutboxError::SchemaTooNew { database: 2, .. })
+ ));
+ assert_eq!(case.expected_outcome, "rejected_before_mutation");
+ assert_eq!(case.expected_error.as_deref(), Some("SchemaTooNew"));
+ }
+ "rollback_below_floor_rejected" => {
+ let store = RadrootsOutbox::open_memory().await.expect("store");
+ assert!(matches!(
+ store.rollback_to_schema_version_and_close(0).await,
+ Err(RadrootsOutboxError::RollbackBelowVersionFloor {
+ floor: 1,
+ target: 0
+ })
+ ));
+ assert_eq!(case.expected_outcome, "rejected_without_schema_change");
+ assert_eq!(
+ case.expected_error.as_deref(),
+ Some("RollbackBelowVersionFloor")
+ );
+ }
+ "caller_state_preserved" => {
+ let pool = memory_pool().await;
+ sqlx::raw_sql(
+ "CREATE TABLE caller_state(value TEXT NOT NULL); INSERT INTO caller_state VALUES ('preserved');",
+ )
+ .execute(&pool)
+ .await
+ .expect("caller state");
+ let store = RadrootsOutbox::open_pool(pool, false)
+ .await
+ .expect("fresh migration");
+ let value: String = sqlx::query_scalar("SELECT value FROM caller_state")
+ .fetch_one(store.pool())
+ .await
+ .expect("caller value");
+ assert_eq!(value, "preserved");
+ assert_eq!(
+ case.expected_outcome,
+ "managed_v1_with_caller_state_preserved"
+ );
+ }
+ "current_reopen_no_history_write" => {
+ let store = RadrootsOutbox::open_memory().await.expect("store");
+ let before: i64 = sqlx::query_scalar("SELECT total_changes()")
+ .fetch_one(store.pool())
+ .await
+ .expect("before");
+ store
+ .migrate_to_current_schema()
+ .await
+ .expect("current reopen");
+ let after: i64 = sqlx::query_scalar("SELECT total_changes()")
+ .fetch_one(store.pool())
+ .await
+ .expect("after");
+ assert_eq!(before, after);
+ assert_eq!(case.expected_outcome, "managed_v1_without_history_write");
+ }
+ other => panic!("unknown direct vector case `{other}`"),
+ }
+ assert_eq!(case.expected_error.is_some(), case.id.contains("rejected"));
+}
+
+#[tokio::test]
+async fn migration_authority_v1_result_vector() {
+ let canonical =
+ include_bytes!("../../../contracts/conformance/vectors/outbox/migration_authority.v1.json");
+ assert_eq!(VECTOR_BYTES, canonical, "packaged vector mirror drift");
+ let vector: Vector = serde_json::from_slice(VECTOR_BYTES).expect("vector JSON");
+ assert_eq!(vector.schema_version, 1);
+ assert_eq!(vector.contract_id, "radroots_outbox.migration_authority.v1");
+ assert_eq!(
+ vector.executor.id,
+ "radroots_outbox.migration_authority_v1.result_vector_executor.v1"
+ );
+ assert_eq!(
+ vector.executor.path,
+ "crates/outbox/tests/migration_authority_v1_result_vector.rs"
+ );
+ assert_eq!(vector.executor.test, "migration_authority_v1_result_vector");
+ assert_eq!(vector.delegated_suite.lane, "nix run .#contract");
+ assert_eq!(vector.delegated_suite.package, "radroots_outbox");
+
+ let mut authorities = BTreeSet::new();
+ for authority in vector.delegated_suite.authorities {
+ assert!(authorities.insert(authority.authority.clone()));
+ let source = match authority.authority_path.as_str() {
+ "crates/outbox/src/schema.rs" => SCHEMA_SOURCE,
+ "crates/outbox/src/migrations.rs" => MIGRATIONS_SOURCE,
+ "crates/outbox/src/store.rs" => STORE_SOURCE,
+ other => panic!("unknown delegated authority path `{other}`"),
+ };
+ assert!(source.contains(authority.authority.as_str()));
+ }
+
+ let mut case_ids = BTreeSet::new();
+ for case in &vector.cases {
+ assert!(case_ids.insert(case.id.clone()), "duplicate case id");
+ execute_case(case).await;
+ }
+ assert_eq!(case_ids.len(), 8);
+}
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 outbox_migration;
mod raw_source_rebuild;
mod registry_v7;
mod source_maintenance;
@@ -16,6 +17,9 @@ pub(crate) use food_availability_projection::{
pub(crate) use nip09_reconciliation::{
validate_nip09_reconciliation_manifest, write_nip09_reconciliation_manifest,
};
+pub(crate) use outbox_migration::{
+ validate_outbox_migration_manifest, write_outbox_migration_manifest,
+};
pub(crate) use raw_source_rebuild::{
validate_raw_source_rebuild_manifest, write_raw_source_rebuild_manifest,
};
@@ -54,6 +58,7 @@ pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), S
validate_food_availability_projection_manifest(workspace_root)?;
validate_source_maintenance_manifest(workspace_root)?;
validate_raw_source_rebuild_manifest(workspace_root)?;
+ validate_outbox_migration_manifest(workspace_root)?;
validate_knowledge_contract_manifest(workspace_root)
}
@@ -69,11 +74,14 @@ const SOURCE_MAINTENANCE_CONFORMANCE_VECTOR_RELATIVE: &str =
"contracts/conformance/vectors/event_store/source_maintenance.v1.json";
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] = [
+const OUTBOX_MIGRATION_CONFORMANCE_VECTOR_RELATIVE: &str =
+ "contracts/conformance/vectors/outbox/migration_authority.v1.json";
+const SPECIALIZED_CONFORMANCE_VECTOR_RELATIVES: [&str; 5] = [
NIP09_RECONCILIATION_CONFORMANCE_VECTOR_RELATIVE,
FOOD_AVAILABILITY_PROJECTION_CONFORMANCE_VECTOR_RELATIVE,
SOURCE_MAINTENANCE_CONFORMANCE_VECTOR_RELATIVE,
RAW_SOURCE_REBUILD_CONFORMANCE_VECTOR_RELATIVE,
+ OUTBOX_MIGRATION_CONFORMANCE_VECTOR_RELATIVE,
];
const KNOWLEDGE_MANIFEST_RELATIVE: &str =
"contracts/knowledge/knowledge_event_contract_manifest.v2.json";
@@ -94,7 +102,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); 23] = [
+const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 24] = [
(
"contracts/conformance/vectors/blossom/bud11_claims.v1.json",
"crates/blossom/tests/fixtures/bud11_claims.v1.json",
@@ -148,6 +156,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 23] = [
"crates/event_store/tests/fixtures/raw_source_rebuild.v1.json",
),
(
+ OUTBOX_MIGRATION_CONFORMANCE_VECTOR_RELATIVE,
+ "crates/outbox/tests/fixtures/migration_authority.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/outbox_migration.rs b/tools/xtask/src/contract/outbox_migration.rs
@@ -0,0 +1,1026 @@
+use super::artifact_bundle::{
+ GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction,
+};
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
+use sha2::{Digest, Sha256};
+use std::collections::BTreeSet;
+use std::fs;
+use std::path::{Path, PathBuf};
+use syn::{Expr, Item};
+
+const CONTRACT_ID: &str = "radroots_outbox.migration_authority.v1";
+const AUTHORITY_ID: &str = "versioned_outbox_migration_authority_v1";
+const SCHEMA_VERSION: u32 = 1;
+const MINIMUM_VERSION: u32 = 1;
+const CURRENT_VERSION: u32 = 1;
+const LEDGER_NAME: &str = "radroots_outbox_schema_migrations";
+const RESERVED_PREFIX: &str = "outbox_";
+const CATALOG_ROW_LIMIT: u32 = 14;
+const CATALOG_REJECTION_PROBE_LIMIT: u32 = 15;
+const HISTORY_ROW_LIMIT: u32 = 1;
+const HISTORY_REJECTION_PROBE_LIMIT: u32 = 2;
+const HASH_ALGORITHM: &str = "sha256_bytes_v1";
+const CATALOG_FINGERPRINT_ALGORITHM: &str =
+ "sha256_type_nul_name_nul_table_name_nul_sql_nul_sorted_v1";
+const WRITE_COMMAND: &str = "cargo xtask contract outbox-migration-manifest --write";
+
+const UP_RELATIVE: &str = "crates/outbox/migrations/0001_outbox.up.sql";
+const DOWN_RELATIVE: &str = "crates/outbox/migrations/0001_outbox.down.sql";
+const UP_BYTE_LENGTH: usize = 5_470;
+const DOWN_BYTE_LENGTH: usize = 159;
+const UP_SHA256: &str = "a7ee775d32c2b9f845961425362e1b1e558ce0d025f7d22dd58f118ba4dab4fa";
+const DOWN_SHA256: &str = "5d56f978f9172dc5ecbc5043a6c286c75926974d8a2a9e44fffa7c134829af61";
+const SCHEMA_SHA256: &str = "e7eeba00de78ec6d990c620e7c056018166e8a00bb703e472ef6f67a00870293";
+
+const MANIFEST_RELATIVE: &str = "crates/outbox/contracts/migration_authority_v1.manifest.json";
+const MANIFEST_SCHEMA_RELATIVE: &str =
+ "crates/outbox/contracts/migration_authority_v1.manifest.schema.json";
+const MANIFEST_SHA256_RELATIVE: &str =
+ "crates/outbox/contracts/migration_authority_v1.manifest.sha256";
+const GENERATED_DESCRIPTOR_RELATIVE: &str =
+ "crates/outbox/src/generated/outbox_migration_manifest.rs";
+const VECTOR_RELATIVE: &str = "contracts/conformance/vectors/outbox/migration_authority.v1.json";
+const VECTOR_MIRROR_RELATIVE: &str = "crates/outbox/tests/fixtures/migration_authority.v1.json";
+const VECTOR_EXECUTOR_RELATIVE: &str =
+ "crates/outbox/tests/migration_authority_v1_result_vector.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 = "outbox-versioned-migration-authority";
+const CHANGELOG_MARKER: &str = "<!-- release-change: outbox-versioned-migration-authority -->";
+
+const OBJECTS: &[&str] = &[
+ "outbox_delivery_attempt",
+ "outbox_delivery_attempt_target_idx",
+ "outbox_delivery_plan",
+ "outbox_delivery_plan_event_idx",
+ "outbox_delivery_target",
+ "outbox_delivery_target_ready_idx",
+ "outbox_event",
+ "outbox_event_event_id_idx",
+ "outbox_event_ready_idx",
+ "outbox_operation_idempotency_idx",
+ "outbox_operation_status_idx",
+ "outbox_operation_trade_mutation_idx",
+ "outbox_operations",
+];
+const TABLES: &[&str] = &[
+ "outbox_delivery_attempt",
+ "outbox_delivery_plan",
+ "outbox_delivery_target",
+ "outbox_event",
+ "outbox_operations",
+];
+const INDEXES: &[&str] = &[
+ "outbox_delivery_attempt_target_idx",
+ "outbox_delivery_plan_event_idx",
+ "outbox_delivery_target_ready_idx",
+ "outbox_event_event_id_idx",
+ "outbox_event_ready_idx",
+ "outbox_operation_idempotency_idx",
+ "outbox_operation_status_idx",
+ "outbox_operation_trade_mutation_idx",
+];
+
+const PUBLIC_SYMBOLS: &[&str] = &[
+ "RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT",
+ "RADROOTS_OUTBOX_SCHEMA_VERSION_MIN",
+ "RadrootsOutboxSchemaStatus",
+ "inspect_outbox_schema_status",
+];
+const PUBLIC_METHODS: &[&str] = &[
+ "RadrootsOutbox::migrate_to_current_schema",
+ "RadrootsOutbox::rollback_to_schema_version_and_close",
+ "RadrootsOutbox::schema_status",
+];
+const REMOVED_SYMBOLS: &[&str] = &["OUTBOX_MIGRATION_DOWN", "OUTBOX_MIGRATION_UP"];
+const REMOVED_METHODS: &[&str] = &["RadrootsOutbox::migrate_down"];
+
+const ERROR_VARIANTS: &[&str] = &[
+ "EmbeddedMigrationChecksumMismatch",
+ "EmbeddedMigrationLengthMismatch",
+ "ForeignKeyViolation",
+ "GovernedCatalogCapacityExceeded",
+ "IntegrityCheckFailed",
+ "MigrationCatalogDeltaMismatch",
+ "MigrationHistoryChecksumDrift",
+ "MigrationHistoryGap",
+ "MigrationHistoryNameDrift",
+ "MigrationLedgerDrift",
+ "MigrationRegistryDefect",
+ "MigrationTransactionRollbackFailed",
+ "RollbackAhead",
+ "RollbackBelowVersionFloor",
+ "RollbackUnmanaged",
+ "SchemaFingerprintMismatch",
+ "SchemaTooNew",
+ "SqliteForeignKeysNotEnabled",
+ "SqliteMainDatabaseEncodingNotUtf8",
+ "SqliteMainDatabaseUnavailable",
+ "TemporarySchemaCollision",
+ "UnknownMigration",
+ "UnmanagedSchema",
+];
+
+const SOURCE_SPECS: &[(&str, &str)] = &[
+ ("outbox_package_manifest", "crates/outbox/Cargo.toml"),
+ ("outbox_public_surface", "crates/outbox/src/lib.rs"),
+ ("outbox_error_surface", "crates/outbox/src/error.rs"),
+ (
+ "generated_descriptor_registration",
+ "crates/outbox/src/generated.rs",
+ ),
+ (
+ "outbox_migration_registry",
+ "crates/outbox/src/migrations.rs",
+ ),
+ ("outbox_schema_runtime", "crates/outbox/src/schema.rs"),
+ ("outbox_store_runtime", "crates/outbox/src/store.rs"),
+ ("outbox_package_readme", "crates/outbox/README"),
+ ("vector_executor", VECTOR_EXECUTOR_RELATIVE),
+ (
+ "contract_governance",
+ "tools/xtask/src/contract/outbox_migration.rs",
+ ),
+ ("contract_dispatch", "tools/xtask/src/contract.rs"),
+ ("xtask_dispatch", "tools/xtask/src/main.rs"),
+ ("nix_contract_lane", "build/nix/common.nix"),
+ ("release_record", RELEASE_RECORD_RELATIVE),
+ ("release_notes", CHANGELOG_RELATIVE),
+];
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct Vector {
+ schema_version: u32,
+ contract_id: String,
+ executor: VectorExecutor,
+ delegated_suite: DelegatedSuite,
+ cases: Vec<VectorCase>,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct VectorExecutor {
+ id: String,
+ path: String,
+ test: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct DelegatedSuite {
+ lane: String,
+ package: String,
+ authorities: Vec<DelegatedAuthority>,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct DelegatedAuthority {
+ authority: String,
+ authority_path: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+struct VectorCase {
+ id: String,
+ execution: String,
+ expected_outcome: String,
+ expected_error: Option<String>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+struct DiscoveredMigration {
+ version: u32,
+ name: String,
+ up_relative: String,
+ down_relative: String,
+}
+
+pub(crate) fn write_outbox_migration_manifest(workspace_root: &Path) -> Result<(), String> {
+ with_artifact_bundle_transaction(workspace_root, |transaction| {
+ transaction.write(expected_artifacts(workspace_root)?)?;
+ validate_under_lock(workspace_root)
+ })
+}
+
+pub(crate) fn validate_outbox_migration_manifest(workspace_root: &Path) -> Result<(), String> {
+ with_artifact_bundle_transaction(workspace_root, |_| validate_under_lock(workspace_root))
+}
+
+fn validate_under_lock(workspace_root: &Path) -> Result<(), String> {
+ for artifact in expected_artifacts(workspace_root)? {
+ let actual = read_regular_file(workspace_root, artifact.relative)?;
+ if actual != artifact.contents {
+ return Err(format!(
+ "generated outbox migration artifact {} is stale; run `{WRITE_COMMAND}`",
+ artifact.relative
+ ));
+ }
+ }
+ let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
+ let manifest: Value = serde_json::from_slice(&manifest_bytes)
+ .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
+ 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}"))?;
+ 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() {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} violates its schema: {}",
+ errors.join("; ")
+ ));
+ }
+ let sidecar = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?;
+ if sidecar != format!("{}\n", sha256_hex(&manifest_bytes)).as_bytes() {
+ return Err(format!(
+ "{MANIFEST_SHA256_RELATIVE} must authenticate the exact manifest bytes"
+ ));
+ }
+ validate_source_authority(workspace_root)?;
+ validate_vector(workspace_root)?;
+ validate_release_authority(workspace_root)
+}
+
+fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> {
+ validate_source_authority(workspace_root)?;
+ validate_vector(workspace_root)?;
+ validate_release_authority(workspace_root)?;
+ let schema_bytes = canonical_json_bytes(&manifest_schema())?;
+ let manifest = expected_manifest(workspace_root, &schema_bytes)?;
+ let manifest_bytes = canonical_json_bytes(&manifest)?;
+ let manifest_sha256 = sha256_hex(&manifest_bytes);
+ let descriptor = generated_descriptor(&manifest_sha256);
+ let vector = read_regular_file(workspace_root, VECTOR_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: VECTOR_MIRROR_RELATIVE,
+ contents: vector,
+ },
+ ])
+}
+
+fn expected_manifest(workspace_root: &Path, schema_bytes: &[u8]) -> Result<Value, String> {
+ let vector_bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?;
+ let executor_bytes = read_regular_file(workspace_root, VECTOR_EXECUTOR_RELATIVE)?;
+ let ledger_ddl = extract_string_const(
+ &parse_rust(workspace_root, "crates/outbox/src/migrations.rs")?,
+ "OUTBOX_LEDGER_DDL",
+ )?;
+ Ok(json!({
+ "schema_version": SCHEMA_VERSION,
+ "contract_id": CONTRACT_ID,
+ "authority_id": AUTHORITY_ID,
+ "manifest_schema": descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, schema_bytes)?,
+ "runtime": {
+ "minimum_version": MINIMUM_VERSION,
+ "current_version": CURRENT_VERSION,
+ "reserved_prefix": RESERVED_PREFIX,
+ "ledger_name": LEDGER_NAME,
+ "ledger_ddl_sha256": sha256_hex(ledger_ddl.as_bytes()),
+ "catalog_fingerprint_algorithm": CATALOG_FINGERPRINT_ALGORITHM,
+ "migration_transaction": "begin_immediate_v1",
+ "rollback_transaction": "begin_exclusive_terminal_close_v1",
+ "catalog_row_limit": CATALOG_ROW_LIMIT,
+ "catalog_rejection_probe_limit": CATALOG_REJECTION_PROBE_LIMIT,
+ "history_row_limit": HISTORY_ROW_LIMIT,
+ "history_rejection_probe_limit": HISTORY_REJECTION_PROBE_LIMIT,
+ "open_validations": [
+ "main_database_backing_identity",
+ "utf8_encoding_before_journal_or_schema_mutation",
+ "foreign_keys_enabled",
+ "file_wal_result",
+ "temporary_schema_authority",
+ "bounded_managed_catalog",
+ "tamper_evident_history",
+ "catalog_fingerprint",
+ "integrity_check_one",
+ "outbox_scoped_foreign_key_check"
+ ],
+ "adoption_policy": "exact_unledgered_0001_catalog_only_v1",
+ "rollback_floor": 1,
+ "test_destruction": "cfg_test_destroy_outbox_schema_for_migration_test"
+ },
+ "migrations": [{
+ "version": 1,
+ "name": "outbox",
+ "up": descriptor_for_file(workspace_root, UP_RELATIVE)?,
+ "down": descriptor_for_file(workspace_root, DOWN_RELATIVE)?,
+ "schema_sha256": SCHEMA_SHA256,
+ "catalog": {
+ "objects": OBJECTS,
+ "tables": TABLES,
+ "indexes": INDEXES
+ }
+ }],
+ "public_api": {
+ "added_symbols": PUBLIC_SYMBOLS,
+ "methods": PUBLIC_METHODS,
+ "removed_symbols": REMOVED_SYMBOLS,
+ "removed_methods": REMOVED_METHODS,
+ "error_variants": ERROR_VARIANTS,
+ "error_enum_non_exhaustive": true
+ },
+ "source_files": SOURCE_SPECS.iter().map(|(role, path)| {
+ Ok(json!({
+ "role": role,
+ "file": descriptor_for_file(workspace_root, path)?
+ }))
+ }).collect::<Result<Vec<Value>, String>>()?,
+ "result_vector": {
+ "canonical": descriptor_for_bytes(VECTOR_RELATIVE, &vector_bytes)?,
+ "mirror_path": VECTOR_MIRROR_RELATIVE,
+ "executor": descriptor_for_bytes(VECTOR_EXECUTOR_RELATIVE, &executor_bytes)?,
+ "executor_id": "radroots_outbox.migration_authority_v1.result_vector_executor.v1",
+ "executor_test": "migration_authority_v1_result_vector"
+ },
+ "release": {
+ "change_id": RELEASE_CHANGE_ID,
+ "release_record": RELEASE_RECORD_RELATIVE,
+ "changelog": CHANGELOG_RELATIVE
+ }
+ }))
+}
+
+fn manifest_schema() -> Value {
+ json!({
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://radroots.org/contracts/outbox/migration_authority_v1.manifest.schema.json",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema_version", "contract_id", "authority_id", "manifest_schema", "runtime",
+ "migrations", "public_api", "source_files", "result_vector", "release"
+ ],
+ "properties": {
+ "schema_version": { "const": 1 },
+ "contract_id": { "const": CONTRACT_ID },
+ "authority_id": { "const": AUTHORITY_ID },
+ "manifest_schema": { "$ref": "#/$defs/file" },
+ "runtime": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "minimum_version", "current_version", "reserved_prefix", "ledger_name",
+ "ledger_ddl_sha256", "catalog_fingerprint_algorithm", "migration_transaction",
+ "rollback_transaction", "catalog_row_limit", "catalog_rejection_probe_limit",
+ "history_row_limit", "history_rejection_probe_limit", "open_validations",
+ "adoption_policy", "rollback_floor", "test_destruction"
+ ],
+ "properties": {
+ "minimum_version": { "const": 1 },
+ "current_version": { "const": 1 },
+ "reserved_prefix": { "const": "outbox_" },
+ "ledger_name": { "const": LEDGER_NAME },
+ "ledger_ddl_sha256": { "$ref": "#/$defs/sha256" },
+ "catalog_fingerprint_algorithm": { "type": "string", "minLength": 1 },
+ "migration_transaction": { "const": "begin_immediate_v1" },
+ "rollback_transaction": { "const": "begin_exclusive_terminal_close_v1" },
+ "catalog_row_limit": { "const": 14 },
+ "catalog_rejection_probe_limit": { "const": 15 },
+ "history_row_limit": { "const": 1 },
+ "history_rejection_probe_limit": { "const": 2 },
+ "open_validations": { "type": "array", "minItems": 10, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
+ "adoption_policy": { "const": "exact_unledgered_0001_catalog_only_v1" },
+ "rollback_floor": { "const": 1 },
+ "test_destruction": { "const": "cfg_test_destroy_outbox_schema_for_migration_test" }
+ }
+ },
+ "migrations": {
+ "type": "array", "minItems": 1, "maxItems": 1,
+ "items": {
+ "type": "object", "additionalProperties": false,
+ "required": ["version", "name", "up", "down", "schema_sha256", "catalog"],
+ "properties": {
+ "version": { "const": 1 }, "name": { "const": "outbox" },
+ "up": { "$ref": "#/$defs/file" }, "down": { "$ref": "#/$defs/file" },
+ "schema_sha256": { "$ref": "#/$defs/sha256" },
+ "catalog": {
+ "type": "object", "additionalProperties": false,
+ "required": ["objects", "tables", "indexes"],
+ "properties": {
+ "objects": { "type": "array", "minItems": 13, "maxItems": 13, "uniqueItems": true, "items": { "type": "string" } },
+ "tables": { "type": "array", "minItems": 5, "maxItems": 5, "uniqueItems": true, "items": { "type": "string" } },
+ "indexes": { "type": "array", "minItems": 8, "maxItems": 8, "uniqueItems": true, "items": { "type": "string" } }
+ }
+ }
+ }
+ }
+ },
+ "public_api": {
+ "type": "object", "additionalProperties": false,
+ "required": ["added_symbols", "methods", "removed_symbols", "removed_methods", "error_variants", "error_enum_non_exhaustive"],
+ "properties": {
+ "added_symbols": { "type": "array", "minItems": 4, "maxItems": 4, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
+ "methods": { "type": "array", "minItems": 3, "maxItems": 3, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
+ "removed_symbols": { "type": "array", "minItems": 2, "maxItems": 2, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
+ "removed_methods": { "type": "array", "minItems": 1, "maxItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
+ "error_variants": { "type": "array", "minItems": 23, "maxItems": 23, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
+ "error_enum_non_exhaustive": { "const": true }
+ }
+ },
+ "source_files": {
+ "type": "array", "minItems": 15, "maxItems": 15,
+ "items": {
+ "type": "object", "additionalProperties": false,
+ "required": ["role", "file"],
+ "properties": {
+ "role": { "type": "string", "minLength": 1 },
+ "file": { "$ref": "#/$defs/file" }
+ }
+ }
+ },
+ "result_vector": {
+ "type": "object", "additionalProperties": false,
+ "required": ["canonical", "mirror_path", "executor", "executor_id", "executor_test"],
+ "properties": {
+ "canonical": { "$ref": "#/$defs/file" },
+ "mirror_path": { "const": VECTOR_MIRROR_RELATIVE },
+ "executor": { "$ref": "#/$defs/file" },
+ "executor_id": { "const": "radroots_outbox.migration_authority_v1.result_vector_executor.v1" },
+ "executor_test": { "const": "migration_authority_v1_result_vector" }
+ }
+ },
+ "release": {
+ "type": "object", "additionalProperties": false,
+ "required": ["change_id", "release_record", "changelog"],
+ "properties": {
+ "change_id": { "const": RELEASE_CHANGE_ID },
+ "release_record": { "const": RELEASE_RECORD_RELATIVE },
+ "changelog": { "const": CHANGELOG_RELATIVE }
+ }
+ }
+ },
+ "$defs": {
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
+ "file": {
+ "type": "object", "additionalProperties": false,
+ "required": ["path", "byte_length", "sha256", "hash_algorithm"],
+ "properties": {
+ "path": { "type": "string", "minLength": 1 },
+ "byte_length": { "type": "integer", "minimum": 1 },
+ "sha256": { "$ref": "#/$defs/sha256" },
+ "hash_algorithm": { "const": HASH_ALGORITHM }
+ }
+ }
+ }
+ })
+}
+
+fn generated_descriptor(manifest_sha256: &str) -> String {
+ format!(
+ "// @generated by `cargo xtask contract outbox-migration-manifest --write`; do not edit.\n\n\
+pub(crate) const OUTBOX_MIGRATION_CONTRACT_ID: &str = \"{CONTRACT_ID}\";\n\
+pub(crate) const OUTBOX_MIGRATION_SCHEMA_VERSION: u32 = {SCHEMA_VERSION};\n\
+pub(crate) const OUTBOX_MIGRATION_CURRENT_VERSION: u32 = {CURRENT_VERSION};\n\
+pub(crate) const OUTBOX_MIGRATION_0001_UP_BYTE_LENGTH: usize = {UP_BYTE_LENGTH};\n\
+pub(crate) const OUTBOX_MIGRATION_0001_DOWN_BYTE_LENGTH: usize = {DOWN_BYTE_LENGTH};\n\
+pub(crate) const OUTBOX_MIGRATION_0001_UP_SHA256: &str =\n \"{UP_SHA256}\";\n\
+pub(crate) const OUTBOX_MIGRATION_0001_DOWN_SHA256: &str =\n \"{DOWN_SHA256}\";\n\
+pub(crate) const OUTBOX_MIGRATION_0001_SCHEMA_SHA256: &str =\n \"{SCHEMA_SHA256}\";\n\
+pub(crate) const OUTBOX_MIGRATION_MANIFEST_SHA256: &str =\n \"{manifest_sha256}\";\n"
+ )
+}
+
+fn validate_source_authority(workspace_root: &Path) -> Result<(), String> {
+ let source_roles = SOURCE_SPECS
+ .iter()
+ .map(|(role, _)| *role)
+ .collect::<BTreeSet<_>>();
+ let source_paths = SOURCE_SPECS
+ .iter()
+ .map(|(_, path)| *path)
+ .collect::<BTreeSet<_>>();
+ if source_roles.len() != SOURCE_SPECS.len() || source_paths.len() != SOURCE_SPECS.len() {
+ return Err("outbox migration source roles and paths must be unique".to_owned());
+ }
+ let objects = OBJECTS.iter().copied().collect::<BTreeSet<_>>();
+ let tables = TABLES.iter().copied().collect::<BTreeSet<_>>();
+ let indexes = INDEXES.iter().copied().collect::<BTreeSet<_>>();
+ if objects.len() != 13
+ || tables.len() != 5
+ || indexes.len() != 8
+ || !tables.is_disjoint(&indexes)
+ || tables.union(&indexes).copied().collect::<BTreeSet<_>>() != objects
+ {
+ return Err(
+ "outbox catalog contract must contain exactly five tables and eight indexes".to_owned(),
+ );
+ }
+ let discovered = discover_migrations(workspace_root)?;
+ if discovered
+ != [DiscoveredMigration {
+ version: 1,
+ name: "outbox".to_owned(),
+ up_relative: UP_RELATIVE.to_owned(),
+ down_relative: DOWN_RELATIVE.to_owned(),
+ }]
+ {
+ return Err(
+ "outbox migration discovery must contain exactly the 0001 outbox pair".to_owned(),
+ );
+ }
+ validate_frozen_file(workspace_root, UP_RELATIVE, UP_BYTE_LENGTH, UP_SHA256)?;
+ validate_frozen_file(workspace_root, DOWN_RELATIVE, DOWN_BYTE_LENGTH, DOWN_SHA256)?;
+
+ let migrations = parse_rust(workspace_root, "crates/outbox/src/migrations.rs")?;
+ if extract_string_array_const(&migrations, "OUTBOX_BASELINE_OBJECT_NAMES")? != OBJECTS
+ || extract_string_array_const(&migrations, "OUTBOX_BASELINE_TABLE_NAMES")? != TABLES
+ {
+ return Err("outbox migration source catalog inventory is not exact".to_owned());
+ }
+ let migration_source = read_utf8(workspace_root, "crates/outbox/src/migrations.rs")?;
+ for marker in [
+ "include_str!(\"../migrations/0001_outbox.up.sql\")",
+ "include_str!(\"../migrations/0001_outbox.down.sql\")",
+ "validate_embedded_migration_input",
+ "validate_migration_registry",
+ "RADROOTS_OUTBOX_SCHEMA_VERSION_CURRENT",
+ ] {
+ if !migration_source.contains(marker) {
+ return Err(format!(
+ "outbox migration registry is missing `{marker}` authority"
+ ));
+ }
+ }
+ let schema = read_utf8(workspace_root, "crates/outbox/src/schema.rs")?;
+ for marker in [
+ "begin_with(\"BEGIN IMMEDIATE\")",
+ "begin_with(\"BEGIN EXCLUSIVE\")",
+ "main.sqlite_schema",
+ "PRAGMA integrity_check(1)",
+ "pragma_foreign_key_check",
+ "LIMIT ?",
+ "UnledgeredBaseline",
+ "destroy_outbox_schema_for_migration_test",
+ ] {
+ if !schema.contains(marker) {
+ return Err(format!(
+ "outbox schema runtime is missing `{marker}` authority"
+ ));
+ }
+ }
+ let store = read_utf8(workspace_root, "crates/outbox/src/store.rs")?;
+ for marker in [
+ "rollback_to_schema_version_and_close",
+ "migrate_to_current_schema",
+ "validate_main_database_encoding",
+ "PRAGMA foreign_keys",
+ "PRAGMA main.journal_mode = WAL",
+ "PRAGMA database_list",
+ ] {
+ if !store.contains(marker) {
+ return Err(format!(
+ "outbox store runtime is missing `{marker}` authority"
+ ));
+ }
+ }
+ if store.contains("pub async fn migrate_down") {
+ return Err("unrestricted public outbox migrate_down remains reachable".to_owned());
+ }
+ let store_ast = parse_rust(workspace_root, "crates/outbox/src/store.rs")?;
+ let public_async_methods = store_ast
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Impl(item) if item.trait_.is_none() => Some(item),
+ _ => None,
+ })
+ .filter(|item| match item.self_ty.as_ref() {
+ syn::Type::Path(path) => path
+ .path
+ .segments
+ .last()
+ .is_some_and(|segment| segment.ident == "RadrootsOutbox"),
+ _ => false,
+ })
+ .flat_map(|item| item.items.iter())
+ .filter_map(|item| match item {
+ syn::ImplItem::Fn(method)
+ if matches!(method.vis, syn::Visibility::Public(_))
+ && method.sig.asyncness.is_some() =>
+ {
+ Some(method.sig.ident.to_string())
+ }
+ _ => None,
+ })
+ .collect::<BTreeSet<_>>();
+ for qualified in PUBLIC_METHODS {
+ let method = qualified
+ .rsplit_once("::")
+ .map(|(_, method)| method)
+ .ok_or_else(|| format!("invalid public outbox method identity `{qualified}`"))?;
+ if !public_async_methods.contains(method) {
+ return Err(format!(
+ "outbox public method `{qualified}` is not reachable"
+ ));
+ }
+ }
+ for qualified in REMOVED_METHODS {
+ let method = qualified
+ .rsplit_once("::")
+ .map(|(_, method)| method)
+ .ok_or_else(|| format!("invalid removed outbox method identity `{qualified}`"))?;
+ if public_async_methods.contains(method) {
+ return Err(format!(
+ "removed outbox method `{qualified}` remains reachable"
+ ));
+ }
+ }
+
+ let errors = parse_rust(workspace_root, "crates/outbox/src/error.rs")?;
+ let error_enum = errors
+ .items
+ .iter()
+ .find_map(|item| match item {
+ Item::Enum(item) if item.ident == "RadrootsOutboxError" => Some(item),
+ _ => None,
+ })
+ .ok_or_else(|| "RadrootsOutboxError enum is missing".to_owned())?;
+ if !matches!(error_enum.vis, syn::Visibility::Public(_))
+ || !error_enum
+ .attrs
+ .iter()
+ .any(|attribute| attribute.path().is_ident("non_exhaustive"))
+ {
+ return Err("outbox error authority must be non-exhaustive".to_owned());
+ }
+ let error_variants = error_enum
+ .variants
+ .iter()
+ .map(|variant| variant.ident.to_string())
+ .collect::<BTreeSet<_>>();
+ for variant in ERROR_VARIANTS {
+ if !error_variants.contains(*variant) {
+ return Err(format!(
+ "outbox error authority is missing typed variant `{variant}`"
+ ));
+ }
+ }
+ let public = read_utf8(workspace_root, "crates/outbox/src/lib.rs")?;
+ for symbol in PUBLIC_SYMBOLS {
+ if !public.contains(symbol) {
+ return Err(format!("outbox public surface is missing `{symbol}`"));
+ }
+ }
+ for removed in REMOVED_SYMBOLS {
+ if public.contains(removed) {
+ return Err(format!(
+ "removed raw outbox migration symbol `{removed}` remains public"
+ ));
+ }
+ }
+ let generated = read_utf8(workspace_root, "crates/outbox/src/generated.rs")?;
+ if !generated.contains("mod outbox_migration_manifest") {
+ return Err("outbox generated descriptor is not registered".to_owned());
+ }
+ Ok(())
+}
+
+fn validate_vector(workspace_root: &Path) -> Result<(), String> {
+ let bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?;
+ let vector: Vector = serde_json::from_slice(&bytes)
+ .map_err(|error| format!("parse {VECTOR_RELATIVE}: {error}"))?;
+ if bytes != canonical_json_bytes(&vector)? {
+ return Err(format!("{VECTOR_RELATIVE} must be canonical pretty JSON"));
+ }
+ if vector.schema_version != 1
+ || vector.contract_id != CONTRACT_ID
+ || vector.executor.id != "radroots_outbox.migration_authority_v1.result_vector_executor.v1"
+ || vector.executor.path != VECTOR_EXECUTOR_RELATIVE
+ || vector.executor.test != "migration_authority_v1_result_vector"
+ || vector.delegated_suite.lane != "nix run .#contract"
+ || vector.delegated_suite.package != "radroots_outbox"
+ {
+ return Err(format!(
+ "{VECTOR_RELATIVE} has invalid executor or contract identity"
+ ));
+ }
+ let direct = [
+ "fresh_initialization",
+ "exact_unledgered_adoption",
+ "partial_unledgered_rejected",
+ "ledger_checksum_tamper_rejected",
+ "newer_history_rejected",
+ "rollback_below_floor_rejected",
+ "caller_state_preserved",
+ "current_reopen_no_history_write",
+ ];
+ if vector.cases.len() != direct.len()
+ || vector
+ .cases
+ .iter()
+ .map(|case| case.id.as_str())
+ .collect::<Vec<_>>()
+ != direct
+ || vector
+ .cases
+ .iter()
+ .any(|case| case.execution != "direct_executor")
+ {
+ return Err(format!(
+ "{VECTOR_RELATIVE} direct case inventory is not exact"
+ ));
+ }
+ let mut authorities = BTreeSet::new();
+ for authority in &vector.delegated_suite.authorities {
+ if !authorities.insert(authority.authority.as_str()) {
+ return Err(format!(
+ "{VECTOR_RELATIVE} contains duplicate delegated authority"
+ ));
+ }
+ let source = read_utf8(workspace_root, &authority.authority_path)?;
+ if !source.contains(&authority.authority) {
+ return Err(format!(
+ "delegated authority `{}` is absent from {}",
+ authority.authority, authority.authority_path
+ ));
+ }
+ }
+ if authorities.len() != 10 {
+ return Err(format!(
+ "{VECTOR_RELATIVE} must bind ten delegated authorities"
+ ));
+ }
+ let executor = read_utf8(workspace_root, VECTOR_EXECUTOR_RELATIVE)?;
+ for case in direct {
+ if !executor.contains(case) {
+ return Err(format!("vector executor does not execute `{case}`"));
+ }
+ }
+ Ok(())
+}
+
+fn validate_release_authority(workspace_root: &Path) -> Result<(), String> {
+ let release = read_utf8(workspace_root, RELEASE_RECORD_RELATIVE)?;
+ if !release.contains(&format!("id = \"{RELEASE_CHANGE_ID}\"")) {
+ return Err(format!(
+ "{RELEASE_RECORD_RELATIVE} is missing `{RELEASE_CHANGE_ID}`"
+ ));
+ }
+ let changelog = read_utf8(workspace_root, CHANGELOG_RELATIVE)?;
+ if !changelog.contains(CHANGELOG_MARKER) {
+ return Err(format!(
+ "{CHANGELOG_RELATIVE} is missing `{CHANGELOG_MARKER}`"
+ ));
+ }
+ Ok(())
+}
+
+fn discover_migrations(workspace_root: &Path) -> Result<Vec<DiscoveredMigration>, String> {
+ discover_migrations_in(
+ &workspace_root.join("crates/outbox/migrations"),
+ workspace_root,
+ )
+}
+
+fn discover_migrations_in(
+ directory: &Path,
+ relative_root: &Path,
+) -> Result<Vec<DiscoveredMigration>, String> {
+ let mut entries = fs::read_dir(directory)
+ .map_err(|error| format!("read {}: {error}", directory.display()))?
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(|error| format!("read migration directory entry: {error}"))?;
+ entries.sort_by_key(|entry| entry.file_name());
+ let mut pairs =
+ std::collections::BTreeMap::<(u32, String), (Option<PathBuf>, Option<PathBuf>)>::new();
+ for entry in entries {
+ let metadata = fs::symlink_metadata(entry.path())
+ .map_err(|error| format!("inspect {}: {error}", entry.path().display()))?;
+ if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
+ return Err(format!(
+ "migration input must be a regular file: {}",
+ entry.path().display()
+ ));
+ }
+ let filename = entry
+ .file_name()
+ .into_string()
+ .map_err(|_| "migration filename must be UTF-8".to_owned())?;
+ let (stem, direction) = if let Some(stem) = filename.strip_suffix(".up.sql") {
+ (stem, "up")
+ } else if let Some(stem) = filename.strip_suffix(".down.sql") {
+ (stem, "down")
+ } else {
+ return Err(format!("unknown migration file `{filename}`"));
+ };
+ let (version, name) = stem
+ .split_once('_')
+ .ok_or_else(|| format!("invalid migration filename `{filename}`"))?;
+ if version.len() != 4
+ || !version.bytes().all(|byte| byte.is_ascii_digit())
+ || name.is_empty()
+ || !name
+ .bytes()
+ .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
+ {
+ return Err(format!("invalid migration filename `{filename}`"));
+ }
+ let version = version.parse::<u32>().map_err(|error| error.to_string())?;
+ let pair = pairs.entry((version, name.to_owned())).or_default();
+ let slot = if direction == "up" {
+ &mut pair.0
+ } else {
+ &mut pair.1
+ };
+ if slot.replace(entry.path()).is_some() {
+ return Err(format!(
+ "duplicate {direction} migration for version {version}"
+ ));
+ }
+ }
+ pairs
+ .into_iter()
+ .map(|((version, name), (up, down))| {
+ let relative = |path: PathBuf| {
+ path.strip_prefix(relative_root)
+ .map(|path| path.to_string_lossy().replace('\\', "/"))
+ .map_err(|_| format!("migration is outside {}", relative_root.display()))
+ };
+ Ok(DiscoveredMigration {
+ version,
+ name,
+ up_relative: relative(
+ up.ok_or_else(|| format!("migration {version} is missing up SQL"))?,
+ )?,
+ down_relative: relative(
+ down.ok_or_else(|| format!("migration {version} is missing down SQL"))?,
+ )?,
+ })
+ })
+ .collect()
+}
+
+fn validate_frozen_file(
+ workspace_root: &Path,
+ relative: &str,
+ expected_length: usize,
+ expected_sha256: &str,
+) -> Result<(), String> {
+ let bytes = read_regular_file(workspace_root, relative)?;
+ if bytes.len() != expected_length || sha256_hex(&bytes) != expected_sha256 {
+ return Err(format!(
+ "frozen migration `{relative}` byte identity drifted"
+ ));
+ }
+ Ok(())
+}
+
+fn parse_rust(workspace_root: &Path, relative: &str) -> Result<syn::File, String> {
+ let source = read_utf8(workspace_root, relative)?;
+ syn::parse_file(&source).map_err(|error| format!("parse {relative}: {error}"))
+}
+
+fn extract_string_const(source: &syn::File, name: &str) -> Result<String, String> {
+ source
+ .items
+ .iter()
+ .find_map(|item| {
+ let Item::Const(item) = item else { return None };
+ (item.ident == name).then_some(item.expr.as_ref())
+ })
+ .and_then(|expr| match expr {
+ Expr::Lit(literal) => match &literal.lit {
+ syn::Lit::Str(value) => Some(value.value()),
+ _ => None,
+ },
+ _ => None,
+ })
+ .ok_or_else(|| format!("missing string const `{name}`"))
+}
+
+fn extract_string_array_const(source: &syn::File, name: &str) -> Result<Vec<String>, String> {
+ let expression = source
+ .items
+ .iter()
+ .find_map(|item| {
+ let Item::Const(item) = item else { return None };
+ (item.ident == name).then_some(item.expr.as_ref())
+ })
+ .ok_or_else(|| format!("missing array const `{name}`"))?;
+ let Expr::Reference(reference) = expression else {
+ return Err(format!("array const `{name}` must be a reference"));
+ };
+ let Expr::Array(array) = reference.expr.as_ref() else {
+ return Err(format!("array const `{name}` must reference an array"));
+ };
+ array
+ .elems
+ .iter()
+ .map(|element| {
+ let Expr::Lit(literal) = element else {
+ return Err(format!("array const `{name}` must contain string literals"));
+ };
+ let syn::Lit::Str(value) = &literal.lit else {
+ return Err(format!("array const `{name}` must contain string literals"));
+ };
+ Ok(value.value())
+ })
+ .collect()
+}
+
+fn descriptor_for_file(workspace_root: &Path, relative: &str) -> Result<Value, String> {
+ descriptor_for_bytes(relative, &read_regular_file(workspace_root, relative)?)
+}
+
+fn descriptor_for_bytes(relative: &str, bytes: &[u8]) -> Result<Value, String> {
+ let byte_length =
+ u64::try_from(bytes.len()).map_err(|_| format!("{relative} byte length is outside u64"))?;
+ Ok(json!({
+ "path": relative,
+ "byte_length": byte_length,
+ "sha256": sha256_hex(bytes),
+ "hash_algorithm": HASH_ALGORITHM
+ }))
+}
+
+fn read_utf8(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 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 sha256_hex(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn migration_discovery_rejects_unknown_missing_and_non_regular_inputs() {
+ let root = tempfile::tempdir().expect("tempdir");
+ let directory = root.path().join("migrations");
+ fs::create_dir(&directory).expect("directory");
+ fs::write(directory.join("0001_outbox.up.sql"), b"up").expect("up");
+ fs::write(directory.join("0001_outbox.down.sql"), b"down").expect("down");
+ assert_eq!(
+ discover_migrations_in(&directory, root.path())
+ .expect("discovery")
+ .len(),
+ 1
+ );
+ fs::write(directory.join("README"), b"unknown").expect("unknown");
+ assert!(
+ discover_migrations_in(&directory, root.path())
+ .expect_err("unknown")
+ .contains("unknown migration file")
+ );
+ fs::remove_file(directory.join("README")).expect("remove unknown");
+ fs::remove_file(directory.join("0001_outbox.down.sql")).expect("remove down");
+ assert!(
+ discover_migrations_in(&directory, root.path())
+ .expect_err("missing down")
+ .contains("missing down SQL")
+ );
+ fs::create_dir(directory.join("0002_future.up.sql")).expect("non-regular");
+ assert!(
+ discover_migrations_in(&directory, root.path())
+ .expect_err("non-regular")
+ .contains("regular file")
+ );
+ }
+
+ #[test]
+ fn canonical_workspace_authority_and_generated_artifacts_are_current() {
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .and_then(Path::parent)
+ .expect("workspace root");
+ validate_source_authority(root).expect("source authority");
+ validate_vector(root).expect("vector");
+ validate_release_authority(root).expect("release authority");
+ validate_outbox_migration_manifest(root).expect("generated artifacts");
+ }
+}
diff --git a/tools/xtask/src/contract/raw_source_rebuild.rs b/tools/xtask/src/contract/raw_source_rebuild.rs
@@ -593,7 +593,7 @@ const DELEGATED_COMPILER_SOURCE_PINS: &[(&str, &str)] = &[
),
(
CONTRACT_LANE_SOURCE_RELATIVE,
- "b3340e1b4973e6a1e02899d164ca74842757f22b6b1a03f90461532fcd844df5",
+ "738003cb1703baaf73a97c010c84731a42230782661c79c6a152fbb9e6fa9f6e",
),
(
TOOLCHAIN_ROUTING_SOURCE_RELATIVE,
diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs
@@ -22,6 +22,7 @@ fn usage() {
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 outbox-migration-manifest [--write]");
eprintln!(" cargo xtask contract knowledge-manifest [--write]");
eprintln!(" cargo xtask dto-roots --check|--write");
eprintln!(" cargo xtask release preflight");
@@ -138,6 +139,15 @@ fn run_contract(args: &[String]) -> Result<(), String> {
"raw-source-rebuild-manifest accepts no arguments or exactly --write".to_string(),
),
},
+ Some("outbox-migration-manifest") => match &args[1..] {
+ [] => contract::validate_outbox_migration_manifest(&workspace_root()),
+ [flag] if flag == "--write" => {
+ contract::write_outbox_migration_manifest(&workspace_root())
+ }
+ _ => {
+ Err("outbox-migration-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())
@@ -264,6 +274,12 @@ mod tests {
])
.expect_err("invalid raw-source rebuild manifest mode");
assert!(invalid_raw_source_rebuild.contains("exactly --write"));
+ let invalid_outbox_migration = run_contract(&[
+ "outbox-migration-manifest".to_string(),
+ "--invalid".to_string(),
+ ])
+ .expect_err("invalid outbox migration manifest mode");
+ assert!(invalid_outbox_migration.contains("exactly --write"));
let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command");
assert!(unknown_root.contains("unknown command"));
@@ -286,6 +302,17 @@ mod tests {
}
#[test]
+ fn release_preflight_reaches_contract_authorities_after_dto_roots() {
+ let _guard = lock_workspace();
+ if let Err(error) = release_preflight_at(&workspace_root()) {
+ assert!(
+ !error.trim().is_empty(),
+ "release preflight blockers must be actionable"
+ );
+ }
+ }
+
+ #[test]
fn lock_workspace_recovers_from_poisoned_mutex() {
let handle = std::thread::spawn(|| {
let _guard = workspace_lock().lock().expect("lock workspace");
@@ -365,6 +392,8 @@ mod tests {
.expect("contract SourceMaintenance manifest");
run_contract(&["raw-source-rebuild-manifest".to_string()])
.expect("contract raw-source rebuild manifest");
+ run_contract(&["outbox-migration-manifest".to_string()])
+ .expect("contract outbox migration manifest");
run_contract(&["knowledge-manifest".to_string()]).expect("contract knowledge manifest");
}
}