commit 08d7134408cd95c9d3fbda1aa60ae4f46682f5e4
parent 577efcb4fe88f7aaff75bdd5a16a09debbcfc034
Author: triesap <tyson@radroots.org>
Date: Tue, 21 Jul 2026 09:31:52 +0000
event-store: enforce retained source maintenance authority
- add prospective raw-source capacity and finite generation enforcement
- reject encoding drift and destructive source-history rollback
- seal migration, reopen, ingest, and rebuild authority atomically
- preserve immutable NIP-09 and FoodAvailability predecessors
Diffstat:
26 files changed, 14850 insertions(+), 716 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -114,6 +114,29 @@ publish policy both pass for the same source revision.
authenticates schema `0003`, registry-v7 admission, exact kind scope `30402`,
executable transition/projection vectors, and the frozen NIP-09 predecessor.
Stored Blossom image digests use the public typed SHA-256 value.
+- Event-store schema v4 adds a persisted raw-source capacity seal for event
+ rows, tag rows, and their governed UTF-8 text bytes. Unique durable ingest
+ now refuses prospective capacity excess before mutation, database reopen
+ performs a bounded full recount, and independent file pools serialize an
+ exact final capacity slot. Every supplied main database must report UTF-8
+ before schema or journal mutation. Retained source history stops at eight
+ generations before requesting fresh-store replacement and resync, and
+ production rollback cannot cross the migration that introduced that
+ append-only history. The
+ authenticated SourceMaintenance successor binds the immutable schema-v3
+ predecessor, migration and runtime sources, breaking capacity-error API
+ replacements, and an executable result vector. Schema v4 is intentionally
+ non-additive: it replaces exactly the Food projection delete guard, Food image
+ delete guard, and source rebuild-marker insert guard, requires that exact
+ symmetric catalog delta, and restores the exact v3 trigger SQL on rollback.
+ A drifted v3 predecessor is rejected atomically rather than repaired during
+ upgrade; repair authorization is reserved for a future rebuild after exact
+ managed-v4 catalog, ledger, and migration history plus immutable raw/source
+ lineage and capacity validation. Derived hook state is the repair target,
+ not a repair precondition. The former
+ `RadrootsEventStoreReconciliationResource` type and
+ `ReconciliationCapacityExceeded` error variant are replaced by the
+ versioned source-capacity resource and typed capacity/history errors.
- Bare-envelope replica ingestion is quarantined behind the explicit,
non-default `legacy-ingest` feature. Default replica APIs expose emit and sync
surfaces only; a future product ingest boundary must consume a store-produced
diff --git a/contracts/conformance/vectors/event_store/source_maintenance.v1.json b/contracts/conformance/vectors/event_store/source_maintenance.v1.json
@@ -0,0 +1,408 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_event_store.source_maintenance_v1",
+ "capacity_version": 1,
+ "limits": {
+ "raw_events": 25000,
+ "raw_tags": 250000,
+ "raw_event_text_bytes": 67108864,
+ "raw_tag_text_bytes": 33554432,
+ "retained_source_generations": 8
+ },
+ "accounting": {
+ "algorithm": "sqlite_cast_blob_octet_sum_v1",
+ "raw_event_columns": [
+ "event_id",
+ "pubkey",
+ "tags_json",
+ "content",
+ "sig",
+ "raw_json"
+ ],
+ "raw_tag_columns": [
+ "event_id",
+ "tag_name",
+ "tag_value",
+ "tag_json"
+ ],
+ "nullable_raw_tag_columns": [
+ "tag_value"
+ ]
+ },
+ "cases": [
+ {
+ "id": "fresh_store_zero_authority",
+ "execution": "direct_executor",
+ "authority": "source_maintenance_v1_result_vector",
+ "authority_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "durable_unique_append_updates_all_dimensions",
+ "execution": "direct_executor",
+ "authority": "source_maintenance_v1_result_vector",
+ "authority_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "duplicate_at_exact_boundary_is_idempotent",
+ "execution": "delegated_rust_test",
+ "authority": "exact_capacity_boundary_allows_duplicate_observation_and_ephemeral_noop",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": "raw_events",
+ "boundary": "exact",
+ "expected_outcome": "accepted_without_capacity_delta",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "ephemeral_consumes_no_capacity",
+ "execution": "direct_executor",
+ "authority": "source_maintenance_v1_result_vector",
+ "authority_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted_without_capacity_delta",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_event_count_exact",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_events",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_event_count_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_events",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "raw_tag_count_exact",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_tags",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_tag_count_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_tags",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "raw_event_text_bytes_exact",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_event_text_bytes",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_event_text_bytes_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_event_text_bytes",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "raw_tag_text_bytes_exact",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_tag_text_bytes",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_tag_text_bytes_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_tag_text_bytes",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "outer_transaction_rollback_restores_capacity",
+ "execution": "direct_executor",
+ "authority": "source_maintenance_v1_result_vector",
+ "authority_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rolled_back_without_capacity_delta",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "failed_nested_ingest_rolls_back_savepoint_only",
+ "execution": "delegated_rust_test",
+ "authority": "borrowed_ingest_savepoint_rolls_back_post_core_authority_forge",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "failed_ingest_rolled_back_and_prior_caller_work_preserved",
+ "error_domain": "typed",
+ "expected_error": "MigrationHookStateDrift"
+ },
+ {
+ "id": "v3_to_v4_under_limit_succeeds",
+ "execution": "delegated_rust_test",
+ "authority": "v3_to_v4_under_limit_backfills_exact_capacity_and_preserves_source",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "under_limit",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "v3_to_v4_prior_transition_drift_is_atomic",
+ "execution": "delegated_rust_test",
+ "authority": "v3_to_v4_rejects_prior_transition_drift_atomically",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "corrupt_managed_v3",
+ "expected_outcome": "rejected_before_v4_schema_ledger_or_predecessor_trigger_mutation",
+ "error_domain": "typed",
+ "expected_error": "MigrationHookStateDrift"
+ },
+ {
+ "id": "v3_to_v4_one_over_is_atomic",
+ "execution": "delegated_rust_test",
+ "authority": "source_capacity_is_rechecked_for_every_rebuild_bound_migration",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": "raw_events",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "v3_to_v4_persisted_ephemeral_is_atomic",
+ "execution": "delegated_rust_test",
+ "authority": "v4_rejects_persisted_legacy_ephemeral_rows_atomically",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "PersistedEphemeralRawEvent"
+ },
+ {
+ "id": "reopen_rejects_incoherent_capacity_authority",
+ "execution": "delegated_rust_test",
+ "authority": "reopen_full_measure_detects_every_persisted_capacity_dimension",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_on_reopen",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityStateDrift"
+ },
+ {
+ "id": "reopen_stops_at_first_raw_event_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "reopen_stops_at_the_first_raw_event_one_over_before_ephemeral_probe",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_events",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_at_scan_bound",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "retained_generation_rebuild_exact",
+ "execution": "delegated_rust_test",
+ "authority": "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": "retained_source_generations",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "ninth_rebuild_is_typed_and_atomic",
+ "execution": "delegated_rust_test",
+ "authority": "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": "retained_source_generations",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_entropy_or_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceGenerationHistoryLimitReached"
+ },
+ {
+ "id": "retained_generation_sql_backstop_one_over",
+ "execution": "delegated_sql_test",
+ "authority": "generation_sql_backstop_allows_exact_append_and_is_conflict_safe_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "retained_source_generations",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_by_sql_backstop",
+ "error_domain": "sqlite_database",
+ "expected_error": "event-store retained source generation limit reached; replace and resync into a fresh store"
+ },
+ {
+ "id": "rebuild_marker_accepts_consistent_seals",
+ "execution": "delegated_rust_test",
+ "authority": "current_v4_rebuild_rotates_capacity_and_food_authority_end_to_end",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "rebuild_marker_rejects_incoherent_seals",
+ "execution": "delegated_sql_test",
+ "authority": "marker_close_sql_backstop_rejects_each_required_seal_drift",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_by_sql_backstop",
+ "error_domain": "sqlite_database",
+ "expected_error": "event-store rebuild marker cannot close before capacity, NIP-09, and FoodAvailability seals agree"
+ },
+ {
+ "id": "v4_marker_repair_binds_exact_prior_and_floor",
+ "execution": "delegated_rust_test",
+ "authority": "v4_marker_open_allows_repairing_prior_transition_high_water_drift",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "managed_v4_rebuild",
+ "expected_outcome": "accepts_derived_high_water_repair_and_rejects_wrong_prior_or_floor",
+ "error_domain": "sqlite_database",
+ "expected_error": "exact raw and prior source authority"
+ },
+ {
+ "id": "v4_food_reset_requires_target_rotation",
+ "execution": "delegated_rust_test",
+ "authority": "v4_food_reset_requires_marker_rotation_and_preserves_target_rows",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "managed_v4_rebuild",
+ "expected_outcome": "historical_rows_deleted_only_after_rotation_and_target_rows_preserved",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "v4_down_restores_predecessor_triggers",
+ "execution": "delegated_rust_test",
+ "authority": "v4_down_restores_exact_predecessor_trigger_sql_and_fingerprint",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "v4_to_v3",
+ "expected_outcome": "restored_exact_predecessor_trigger_sql_and_v3_fingerprint",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "utf16_open_file_rejected_before_mutation",
+ "execution": "delegated_rust_test",
+ "authority": "open_file_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_before_schema_or_journal_mutation",
+ "error_domain": "typed",
+ "expected_error": "SqliteMainDatabaseEncodingNotUtf8"
+ },
+ {
+ "id": "utf16_open_pool_rejected_before_mutation",
+ "execution": "delegated_rust_test",
+ "authority": "open_pool_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_before_schema_or_journal_mutation",
+ "error_domain": "typed",
+ "expected_error": "SqliteMainDatabaseEncodingNotUtf8"
+ },
+ {
+ "id": "utf8_non_ascii_nul_reopen_accounting",
+ "execution": "delegated_rust_test",
+ "authority": "utf8_file_reopen_preserves_non_ascii_and_nul_capacity_accounting",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted_with_exact_capacity_after_reopen",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "generation_destructive_rollback_rejected",
+ "execution": "delegated_rust_test",
+ "authority": "rollback_rejects_below_floor_ahead_unmanaged_and_generation_destructive_targets",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": "retained_source_generations",
+ "boundary": null,
+ "expected_outcome": "rejected_before_mutation_with_status_and_history_preserved",
+ "error_domain": "typed",
+ "expected_error": "RollbackWouldDiscardSourceGenerationHistory"
+ },
+ {
+ "id": "generation_destructive_two_step_rollback_rejected",
+ "execution": "delegated_rust_test",
+ "authority": "rollback_cannot_bypass_generation_history_guard_through_version_three",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": "retained_source_generations",
+ "boundary": null,
+ "expected_outcome": "rejected_before_mutation_after_history_preserving_intermediate_rollback",
+ "error_domain": "typed",
+ "expected_error": "RollbackWouldDiscardSourceGenerationHistory"
+ },
+ {
+ "id": "independent_pool_last_byte_slot_race",
+ "execution": "delegated_rust_test",
+ "authority": "independent_file_pools_serialize_the_last_raw_event_byte_capacity_slot",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": "raw_event_text_bytes",
+ "boundary": "exact",
+ "expected_outcome": "exactly_one_accepted_one_typed_rejection_and_clean_reopen",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ }
+ ]
+}
diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml
@@ -404,6 +404,22 @@ semver_impacts = [
summary = "Advance the event store to schema version 3 with central current visibility, a generation-bound addressable transition feed, an atomic registry-v7 FoodAvailability projection and bounded search authority, typed Blossom digests, and an executable successor contract that preserves the frozen NIP-09 predecessor."
[[changes]]
+id = "event-store-source-maintenance-authority"
+classification = "breaking"
+semver_impacts = [
+ "add_exported_type",
+ "add_exported_function",
+ "add_exported_constant",
+ "add_enum_variant",
+ "add_conformance_vector",
+ "remove_exported_type",
+ "change_exported_enum_variant",
+ "change_exported_constant_value",
+ "change_exported_algorithm_behavior",
+]
+summary = "Advance the event store to schema version 4 with prospective retained-source capacity enforcement across independent file pools, UTF-8 preflight before schema or journal mutation, bounded reopen recounts, rollback-protected finite generation history, coherent NIP-09 and FoodAvailability rebuild seals, typed capacity and recovery failures, and an authenticated executable SourceMaintenance successor contract that replaces exactly radroots_event_store_food_availability_image_delete_guard, radroots_event_store_food_availability_projection_delete_guard, and radroots_event_store_source_rebuild_marker_insert_guard; rejects drifted v3 upgrades atomically; restores the exact predecessor trigger SQL on rollback; and reserves future derived-state repair for an exact managed-v4 catalog, ledger, migration history, immutable raw/source lineage, and capacity without requiring derived hook health as a precondition."
+
+[[changes]]
id = "transport-event-outcomes-and-replica-quarantine"
classification = "breaking"
semver_impacts = [
diff --git a/crates/event_store/README b/crates/event_store/README
@@ -49,8 +49,21 @@ tags, observations, or heads. Their ingest receipt carries
`RadrootsEventPersistence::NotPersisted`; repeated delivery remains live-only
and is never reported as a durable duplicate.
-Schema version 3 composes NIP-09 reconciliation with a generation-bound current
-visibility view, generic addressable transition feed, and focused kind-`30402`
+Every unique durable event is charged prospectively against the retained
+raw-source event, tag, event-text-byte, and tag-text-byte limits before its raw
+row is inserted. Admitted, unsupported, and contract-invalid durable events are
+charged identically because all remain part of rebuild authority. Event-id
+existence is checked first, so a duplicate at an exact capacity boundary still
+follows the observation path without consuming capacity. The source-capacity
+seal advances in the same savepoint as raw and derived authority; any ingest
+failure rolls all four dimensions back. `source_capacity_v1` exposes the fast
+persisted seal. Migration and database reopen perform the exhaustive raw-row
+recount.
+
+Schema version 4 adds persisted source-capacity and retained-generation
+authority to the version-3 composition of NIP-09 reconciliation with a
+generation-bound current visibility view, generic addressable transition feed,
+and focused kind-`30402`
FoodAvailability projection. Version 2 stores generation-partitioned event
coordinate facts, admitted deletion-request facts, normalized event and address
targets, canonical addressable-head state, and ordered transition history.
@@ -76,10 +89,18 @@ boundary. A caller that executes arbitrary DML, reproduces an internal
maintenance protocol, disables connection invariants, or changes the schema
can subvert derived authority and is outside the supported mutation model.
Typed event-store methods are the supported integrity-enforced write surface.
+Replacing a database shared with unrelated caller-owned tables also removes
+that unrelated state. Capacity recovery therefore requires a new disposable
+event-cache database, not replacement of a shared application database.
`open_pool` inspects the opened main database rather than trusting URL text,
validates the declared backing mode, rejects multi-connection in-memory pools,
-and configures every file-pool connection with foreign-key enforcement and the
+and requires `PRAGMA main.encoding` to report exactly `UTF-8` on every supplied
+connection before any schema or journal mutation. This makes Rust prospective
+UTF-8 byte accounting identical to the SQLite `CAST(... AS BLOB)` authority;
+pre-created UTF-16 databases fail with
+`SqliteMainDatabaseEncodingNotUtf8` and are not remediated in place. The store
+then configures every file-pool connection with foreign-key enforcement and the
required busy timeout before migrations or writes. Backing inspection uses
SQLite's non-shadowable `PRAGMA database_list`. Every supplied connection is
also checked for temporary schema objects whose name or target table collides
@@ -122,10 +143,17 @@ than quadratic per-ingest scans.
## Schema authority
Every constructor converges the database through a versioned migration
-authority whose current version is `3`. The frozen `0001_event_store`,
-`0002_nip09`, and additive `0003_food_availability_projection` SQL inputs are
-pinned by byte length and SHA-256 in the embedded registry. Fresh databases
-install them transactionally; an existing
+authority whose current version is `4`. The frozen `0001_event_store`,
+`0002_nip09`, and `0003_food_availability_projection` predecessors remain
+byte-identical and byte-pinned. The non-additive `0004_source_maintenance` is
+also pinned by byte length and SHA-256 in the embedded registry. It
+authenticates exactly three predecessor-trigger replacements:
+`radroots_event_store_food_availability_image_delete_guard`,
+`radroots_event_store_food_availability_projection_delete_guard`, and
+`radroots_event_store_source_rebuild_marker_insert_guard`. Its declared catalog
+delta requires exactly those three definitions to change while its new owned
+objects are added or removed; the down migration restores the exact version-3
+trigger SQL. Fresh databases install the migrations transactionally; an existing
unledgered database is adopted only when its exact 46-object SQLite catalog
matches the frozen version-1 baseline fingerprint. Partial schemas, altered
tables, attached indexes or triggers, counterfeit ledgers, history gaps,
@@ -166,7 +194,13 @@ version, addressable-feed version, registry version, hook-manifest digest, raw
counts, raw high-water sequence, and transition floor are stored with each
generation and validated on open. A supported current-schema full rebuild must
also reset and replay the version-3 Food authority before that marker closes;
-this checkpoint does not yet expose such a maintenance operation.
+this checkpoint does not yet expose such a maintenance operation. Repair of
+derived transition high-water or Food projection state is authorized only
+inside that future rebuild after the exact managed-v4 catalog, ledger, and
+migration history plus immutable raw/source lineage and capacity validate.
+Derived hook state is the repair target, not a repair precondition. A drifted
+managed-v3 database is rejected atomically before v4 changes begin; schema
+upgrade is not a repair path for corrupt v3 authority.
Every pending rebuild-bound migration, including `0002_nip09` and
`0003_food_availability_projection`, preflights and then rechecks under the
@@ -177,17 +211,37 @@ FTS authority are derived. Reconciliation loads both raw authorities in checked
512-row pages. Candidate heads evaluate only the exact event- and
address-target request indices, merging shared matches once in canonical
request order without cloning request payloads. Exceeding a dimension returns
-`ReconciliationCapacityExceeded` before schema changes; local cache owners can
-reduce or rebuild the cache and retry.
+`SourceCapacityExceeded` before schema changes or durable append. Raw rows are
+never pruned in place. Recovery is to select a bounded source set and resync it
+into a new disposable cache.
+
+Source generations are append-only and never pruned. At most eight generations
+may be retained, bounding how many partitions of generation-scoped NIP-09
+logical authority can accumulate. For the governed 25,000-event and
+250,000-tag source limits, the audited generation-partitioned NIP-09 bound is
+`4E + 2T + 2 = 600,002` logical rows per generation: generation, coordinate,
+request, combined event/address target, head-state, transition, and feed-
+integrity rows. Eight retained generations therefore admit at most 4,800,016
+such logical rows. The current Food projection and FTS authority are reset
+rather than retained for every generation. This is only a logical-row bound;
+it is not a total SQLite row-count, byte-size, index-entry, or FTS-segment
+bound. Once the eighth generation is retained, rebuild returns
+`SourceGenerationHistoryLimitReached` with the current count and limit. The
+supported recovery is replacement and resync into a fresh disposable cache,
+subject to the shared-database warning above.
`inspect_event_store_schema_status` classifies a pool as `Uninitialized`,
`UnledgeredBaseline`, or `Managed` without configuring or migrating it.
`schema_status` exposes the same read-only inspection on an opened store.
`rollback_to_schema_version_and_close` validates every descending step under
-`BEGIN EXCLUSIVE`, cannot cross the public version floor, consumes the store,
-and closes its shared pool on every outcome. Independent SQLite pools for the
-same file must be quiesced before this offline maintenance operation.
-Destructive removal is intentionally unavailable in the public API.
+`BEGIN EXCLUSIVE`, cannot cross the public version floor, and cannot cross the
+applied NIP-09 source-generation migration because doing so would discard its
+append-only history. `RollbackWouldDiscardSourceGenerationHistory` reports the
+current, requested, and protected floor versions before any down migration is
+applied. The operation consumes the store and closes its shared pool on every
+outcome. Independent SQLite pools for the same file must be quiesced before
+this offline maintenance operation. Destructive removal is intentionally
+unavailable in the public API.
## Status inspection
diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.json b/crates/event_store/contracts/source_maintenance_v1.manifest.json
@@ -0,0 +1,357 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_event_store.source_maintenance_v1",
+ "hook_id": "source_maintenance_v1",
+ "manifest_schema": {
+ "path": "crates/event_store/contracts/source_maintenance_v1.manifest.schema.json",
+ "byte_length": 12315,
+ "sha256": "ad4a6c8ae9488fc8033792bc6952af04687f312901c1847d8c668a62913bb642",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "predecessor": {
+ "hook_id": "food_availability_projection_v1",
+ "manifest": {
+ "path": "crates/event_store/contracts/food_availability_projection_v1.manifest.json",
+ "byte_length": 17455,
+ "sha256": "33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23",
+ "hash_algorithm": "sha256_bytes_v1"
+ }
+ },
+ "migration": {
+ "version": 4,
+ "name": "source_maintenance",
+ "up": {
+ "path": "crates/event_store/migrations/0004_source_maintenance.up.sql",
+ "byte_length": 19841,
+ "sha256": "425dc799f392b87f265a6fb81f89c4a1c7a5db8391ab0380970708cb9c66704d",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "down": {
+ "path": "crates/event_store/migrations/0004_source_maintenance.down.sql",
+ "byte_length": 5172,
+ "sha256": "fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ "schema_sha256": "d526d96ea02be12b4b0aed99e97cfdde17c4474ace67111506a7b900ee78b186",
+ "catalog": {
+ "objects": [
+ "radroots_event_store_source_capacity_delete_guard",
+ "radroots_event_store_source_capacity_insert_guard",
+ "radroots_event_store_source_capacity_marker_close_guard",
+ "radroots_event_store_source_capacity_update_guard",
+ "radroots_event_store_source_capacity_v1",
+ "radroots_event_store_source_generation_capacity_advance",
+ "radroots_event_store_source_generation_capacity_guard"
+ ],
+ "replaced_objects": [
+ "radroots_event_store_food_availability_image_delete_guard",
+ "radroots_event_store_food_availability_projection_delete_guard",
+ "radroots_event_store_source_rebuild_marker_insert_guard"
+ ],
+ "tables": [
+ "radroots_event_store_source_capacity_v1"
+ ],
+ "fts5_tables": []
+ }
+ },
+ "source_maintenance": {
+ "version": 1,
+ "event_contract_registry_version": 7,
+ "capacity_authority_id": "radroots_event_store_source_capacity_v1",
+ "accounting": {
+ "algorithm": "sqlite_cast_blob_octet_sum_v1",
+ "raw_event_columns": [
+ "event_id",
+ "pubkey",
+ "tags_json",
+ "content",
+ "sig",
+ "raw_json"
+ ],
+ "raw_tag_columns": [
+ "event_id",
+ "tag_name",
+ "tag_value",
+ "tag_json"
+ ],
+ "nullable_raw_tag_columns": [
+ "tag_value"
+ ]
+ },
+ "limits": {
+ "raw_events": 25000,
+ "raw_tags": 250000,
+ "raw_event_text_bytes": 67108864,
+ "raw_tag_text_bytes": 33554432,
+ "retained_source_generations": 8
+ },
+ "reopen_validation": {
+ "mode": "bounded_full_raw_recount_v1",
+ "raw_event_rejection_scan_bound": 25001,
+ "raw_tag_rejection_scan_bound": 250001,
+ "generation_history_validation": "bounded_count_plus_active_ordinal_v1",
+ "retained_generation_rejection_scan_bound": 9
+ },
+ "rebuild_seal": {
+ "nip09_hook_id": "nip09_reconciliation_v1",
+ "nip09_manifest_sha256": "74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77",
+ "food_hook_id": "food_availability_projection_v1",
+ "food_manifest_sha256": "33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23",
+ "food_scope_fingerprint_sha256": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0",
+ "active_generation_authority": "radroots_event_store_source_state",
+ "marker_close_authority": "radroots_event_store_source_capacity_marker_close_guard"
+ }
+ },
+ "entry_points": [
+ {
+ "role": "migration_registry",
+ "rust_path": "radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]"
+ },
+ {
+ "role": "migration_apply_hook",
+ "rust_path": "radroots_event_store::schema::apply_migration_hook"
+ },
+ {
+ "role": "migration_validation_hook",
+ "rust_path": "radroots_event_store::schema::validate_migration_hook_state"
+ },
+ {
+ "role": "capacity_query",
+ "rust_path": "radroots_event_store::RadrootsEventStore::source_capacity_v1"
+ },
+ {
+ "role": "raw_append_preflight",
+ "rust_path": "radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1"
+ },
+ {
+ "role": "raw_append_advance",
+ "rust_path": "radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1"
+ },
+ {
+ "role": "generation_append_preflight",
+ "rust_path": "radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1"
+ },
+ {
+ "role": "generation_rebuild_bind",
+ "rust_path": "radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1"
+ },
+ {
+ "role": "sqlite_encoding_preflight",
+ "rust_path": "radroots_event_store::store::validate_main_database_encoding"
+ },
+ {
+ "role": "source_generation_history_rollback_guard",
+ "rust_path": "radroots_event_store::schema::validate_rollback_preserves_source_generation_history"
+ },
+ {
+ "role": "result_vector_executor",
+ "rust_path": "source_maintenance_v1_result_vector"
+ }
+ ],
+ "source_files": [
+ {
+ "role": "event_store_error_and_limits",
+ "path": "crates/event_store/src/error.rs",
+ "byte_length": 19421,
+ "sha256": "4772e041cb20a4963afb2f3159804c777e2f2be61bfdb6ee267e7a7c04258972",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "generated_descriptor_registration",
+ "path": "crates/event_store/src/generated.rs",
+ "byte_length": 144,
+ "sha256": "6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "public_surface",
+ "path": "crates/event_store/src/lib.rs",
+ "byte_length": 3844,
+ "sha256": "3cd9653bcb752fb3c4442d4904b98a0a6208011a9a238125b7b7073d7f4e312b",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "migration_registry",
+ "path": "crates/event_store/src/migrations.rs",
+ "byte_length": 73585,
+ "sha256": "a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "predecessor_model_public_surface",
+ "path": "crates/event_store/src/model.rs",
+ "byte_length": 33617,
+ "sha256": "79296b8f263aa06d17005795e4515f769f064ea6fd971eeb1296e1151debaf20",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "source_generation_rebuild_authority",
+ "path": "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "byte_length": 184407,
+ "sha256": "c455d40fc736e3db264f567c7809af7bd897d89be8a33dfb21667a6ef6b8d6c6",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "schema_migration_and_reopen_authority",
+ "path": "crates/event_store/src/schema.rs",
+ "byte_length": 146146,
+ "sha256": "93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "public_store_and_transaction_authority",
+ "path": "crates/event_store/src/store.rs",
+ "byte_length": 394574,
+ "sha256": "db57dc3e35e64c7194683142fe55edba853671a829269449dd2273056dfc3a0e",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "raw_ingest_capacity_authority",
+ "path": "crates/event_store/src/store/protocol_reconciliation_v1.rs",
+ "byte_length": 30140,
+ "sha256": "210112eeaa6975a3b4fbb97d5c52588f8c6d8d07975e531d39737fd11235de51",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "source_maintenance_runtime",
+ "path": "crates/event_store/src/source_maintenance_v1.rs",
+ "byte_length": 51756,
+ "sha256": "f8d5b62f0613104aa86658d5bf1baade92c7df83f00ef0cddadd734b9797afca",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "artifact_transaction_authority",
+ "path": "tools/xtask/src/contract/artifact_bundle.rs",
+ "byte_length": 38279,
+ "sha256": "f326ea57b56d40135f95b6b1e15961f66eed363337180a6d12a5ea903e1a9a29",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "predecessor_successor_governance",
+ "path": "tools/xtask/src/contract/food_availability_projection.rs",
+ "byte_length": 194875,
+ "sha256": "63cc3e6985741e2002ae59f56500bf8d6e0a8246f97a0b8b7f2d2372ef0642e0",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "transitive_predecessor_membership_governance",
+ "path": "tools/xtask/src/contract/nip09_reconciliation.rs",
+ "byte_length": 834200,
+ "sha256": "155374b306f3b30f6095dbfc203150c928b7418e646764011b0401996a636f84",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "source_maintenance_governance",
+ "path": "tools/xtask/src/contract/source_maintenance.rs",
+ "byte_length": 176693,
+ "sha256": "88ea58150c49faaad7dde6c9cc89a92ad6b693b112fd55041ac17984bc5c6700",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "contract_command_authority",
+ "path": "tools/xtask/src/contract.rs",
+ "byte_length": 479173,
+ "sha256": "15b3e754ed6794c8f4c48d7bd316b05ff90578137adb0275e1febdf2891707b4",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "xtask_dispatch_and_release_preflight",
+ "path": "tools/xtask/src/main.rs",
+ "byte_length": 14077,
+ "sha256": "d815e65241e47143a51dd87d95e014d975be1d9b94075f3920e4812f41188353",
+ "hash_algorithm": "sha256_bytes_v1"
+ }
+ ],
+ "public_api": {
+ "inherited_predecessor_symbols": [
+ "RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1",
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1",
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1",
+ "RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1",
+ "RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1",
+ "RadrootsAddressableTransitionCauseV1",
+ "RadrootsAddressableTransitionCoordinateV1",
+ "RadrootsAddressableTransitionCursorV1",
+ "RadrootsAddressableTransitionEventReferenceV1",
+ "RadrootsAddressableTransitionOriginV1",
+ "RadrootsAddressableTransitionPageV1",
+ "RadrootsAddressableTransitionRawHeadDecisionV1",
+ "RadrootsAddressableTransitionScopeFingerprintV1",
+ "RadrootsAddressableTransitionScopeV1",
+ "RadrootsAddressableTransitionV1",
+ "RadrootsAddressableTransitionVisibilityV1",
+ "RadrootsCurrentEventVisibilityV1",
+ "RadrootsCurrentVisibilityDecisionV1",
+ "RadrootsFoodAvailabilitySearchQueryV1",
+ "RadrootsFoodAvailabilityStatusFilterV1",
+ "RadrootsNip09SuppressionEvidenceV1",
+ "RadrootsNip09SuppressionOutcome",
+ "RadrootsNip09SuppressionReason",
+ "RadrootsStoreProducedCanonicalEventV1",
+ "RadrootsStoredFoodAvailabilityImageV1",
+ "RadrootsStoredFoodAvailabilityV1"
+ ],
+ "added_symbols": [
+ "RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1",
+ "RadrootsEventStoreSourceCapacityResourceV1",
+ "RadrootsEventStoreSourceCapacityV1"
+ ],
+ "methods": [
+ "RadrootsEventStore::source_capacity_v1",
+ "RadrootsEventStoreSourceCapacityResourceV1::as_str",
+ "RadrootsEventStoreSourceCapacityV1::source_generation",
+ "RadrootsEventStoreSourceCapacityV1::raw_event_count",
+ "RadrootsEventStoreSourceCapacityV1::raw_tag_count",
+ "RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes",
+ "RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes",
+ "RadrootsEventStoreSourceCapacityV1::raw_high_water_seq",
+ "RadrootsEventStoreSourceCapacityV1::retained_generation_count",
+ "RadrootsEventStoreSourceCapacityV1::retained_generation_limit"
+ ],
+ "error_variants": [
+ "SourceCapacityExceeded",
+ "SourceGenerationHistoryLimitReached",
+ "PersistedEphemeralRawEvent",
+ "SourceCapacityStateDrift",
+ "SqliteMainDatabaseEncodingNotUtf8",
+ "RollbackWouldDiscardSourceGenerationHistory"
+ ],
+ "removed_symbols": [
+ "RadrootsEventStoreReconciliationResource",
+ "RadrootsEventStoreError::ReconciliationCapacityExceeded"
+ ],
+ "breaking_replacements": [
+ {
+ "removed": "RadrootsEventStoreReconciliationResource",
+ "replacement": "RadrootsEventStoreSourceCapacityResourceV1"
+ },
+ {
+ "removed": "RadrootsEventStoreError::ReconciliationCapacityExceeded",
+ "replacement": "RadrootsEventStoreError::SourceCapacityExceeded"
+ }
+ ]
+ },
+ "result_vector": {
+ "canonical_path": "contracts/conformance/vectors/event_store/source_maintenance.v1.json",
+ "mirror_path": "crates/event_store/tests/fixtures/source_maintenance.v1.json",
+ "byte_length": 16253,
+ "sha256": "997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f",
+ "hash_algorithm": "sha256_bytes_v1",
+ "executor_id": "radroots_event_store.source_maintenance_v1.result_vector_executor.v1",
+ "executor_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "executor_test": "source_maintenance_v1_result_vector",
+ "executor_byte_length": 23510,
+ "executor_sha256": "a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3",
+ "executor_hash_algorithm": "sha256_bytes_v1"
+ }
+}
diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.schema.json b/crates/event_store/contracts/source_maintenance_v1.manifest.schema.json
@@ -0,0 +1,493 @@
+{
+ "$defs": {
+ "accounting": {
+ "additionalProperties": false,
+ "properties": {
+ "algorithm": {
+ "const": "sqlite_cast_blob_octet_sum_v1"
+ },
+ "nullable_raw_tag_columns": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "raw_event_columns": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "raw_tag_columns": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "algorithm",
+ "raw_event_columns",
+ "raw_tag_columns",
+ "nullable_raw_tag_columns"
+ ],
+ "type": "object"
+ },
+ "file": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "path": {
+ "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$",
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm"
+ ],
+ "type": "object"
+ },
+ "limits": {
+ "additionalProperties": false,
+ "properties": {
+ "raw_event_text_bytes": {
+ "const": 67108864
+ },
+ "raw_events": {
+ "const": 25000
+ },
+ "raw_tag_text_bytes": {
+ "const": 33554432
+ },
+ "raw_tags": {
+ "const": 250000
+ },
+ "retained_source_generations": {
+ "const": 8
+ }
+ },
+ "required": [
+ "raw_events",
+ "raw_tags",
+ "raw_event_text_bytes",
+ "raw_tag_text_bytes",
+ "retained_source_generations"
+ ],
+ "type": "object"
+ },
+ "source_file": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "path": {
+ "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$",
+ "type": "string"
+ },
+ "role": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "role",
+ "path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm"
+ ],
+ "type": "object"
+ }
+ },
+ "$id": "https://radroots.org/contracts/event-store/source-maintenance-v1-manifest.schema.json",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "properties": {
+ "contract_id": {
+ "const": "radroots_event_store.source_maintenance_v1"
+ },
+ "entry_points": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "role": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "rust_path": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "required": [
+ "role",
+ "rust_path"
+ ],
+ "type": "object"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "hook_id": {
+ "const": "source_maintenance_v1"
+ },
+ "manifest_schema": {
+ "$ref": "#/$defs/file"
+ },
+ "migration": {
+ "additionalProperties": false,
+ "properties": {
+ "catalog": {
+ "additionalProperties": false,
+ "properties": {
+ "fts5_tables": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "objects": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "replaced_objects": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "tables": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "objects",
+ "replaced_objects",
+ "tables",
+ "fts5_tables"
+ ],
+ "type": "object"
+ },
+ "down": {
+ "$ref": "#/$defs/file"
+ },
+ "name": {
+ "const": "source_maintenance"
+ },
+ "schema_sha256": {
+ "const": "d526d96ea02be12b4b0aed99e97cfdde17c4474ace67111506a7b900ee78b186"
+ },
+ "up": {
+ "$ref": "#/$defs/file"
+ },
+ "version": {
+ "const": 4
+ }
+ },
+ "required": [
+ "version",
+ "name",
+ "up",
+ "down",
+ "schema_sha256",
+ "catalog"
+ ],
+ "type": "object"
+ },
+ "predecessor": {
+ "additionalProperties": false,
+ "properties": {
+ "hook_id": {
+ "const": "food_availability_projection_v1"
+ },
+ "manifest": {
+ "$ref": "#/$defs/file"
+ }
+ },
+ "required": [
+ "hook_id",
+ "manifest"
+ ],
+ "type": "object"
+ },
+ "public_api": {
+ "additionalProperties": false,
+ "properties": {
+ "added_symbols": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "breaking_replacements": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "removed": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "replacement": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "required": [
+ "removed",
+ "replacement"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "error_variants": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "inherited_predecessor_symbols": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "methods": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "removed_symbols": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "inherited_predecessor_symbols",
+ "added_symbols",
+ "methods",
+ "error_variants",
+ "removed_symbols",
+ "breaking_replacements"
+ ],
+ "type": "object"
+ },
+ "result_vector": {
+ "additionalProperties": false,
+ "properties": {
+ "byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "canonical_path": {
+ "const": "contracts/conformance/vectors/event_store/source_maintenance.v1.json"
+ },
+ "executor_byte_length": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "executor_hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "executor_id": {
+ "const": "radroots_event_store.source_maintenance_v1.result_vector_executor.v1"
+ },
+ "executor_path": {
+ "const": "crates/event_store/tests/source_maintenance_v1_result_vector.rs"
+ },
+ "executor_sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ },
+ "executor_test": {
+ "const": "source_maintenance_v1_result_vector"
+ },
+ "hash_algorithm": {
+ "const": "sha256_bytes_v1"
+ },
+ "mirror_path": {
+ "const": "crates/event_store/tests/fixtures/source_maintenance.v1.json"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "canonical_path",
+ "mirror_path",
+ "byte_length",
+ "sha256",
+ "hash_algorithm",
+ "executor_id",
+ "executor_path",
+ "executor_test",
+ "executor_byte_length",
+ "executor_sha256",
+ "executor_hash_algorithm"
+ ],
+ "type": "object"
+ },
+ "schema_version": {
+ "const": 1
+ },
+ "source_files": {
+ "items": {
+ "$ref": "#/$defs/source_file"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "source_maintenance": {
+ "additionalProperties": false,
+ "properties": {
+ "accounting": {
+ "$ref": "#/$defs/accounting"
+ },
+ "capacity_authority_id": {
+ "const": "radroots_event_store_source_capacity_v1"
+ },
+ "event_contract_registry_version": {
+ "const": 7
+ },
+ "limits": {
+ "$ref": "#/$defs/limits"
+ },
+ "rebuild_seal": {
+ "additionalProperties": false,
+ "properties": {
+ "active_generation_authority": {
+ "const": "radroots_event_store_source_state"
+ },
+ "food_hook_id": {
+ "const": "food_availability_projection_v1"
+ },
+ "food_manifest_sha256": {
+ "const": "33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23"
+ },
+ "food_scope_fingerprint_sha256": {
+ "const": "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0"
+ },
+ "marker_close_authority": {
+ "const": "radroots_event_store_source_capacity_marker_close_guard"
+ },
+ "nip09_hook_id": {
+ "const": "nip09_reconciliation_v1"
+ },
+ "nip09_manifest_sha256": {
+ "const": "74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77"
+ }
+ },
+ "required": [
+ "nip09_hook_id",
+ "nip09_manifest_sha256",
+ "food_hook_id",
+ "food_manifest_sha256",
+ "food_scope_fingerprint_sha256",
+ "active_generation_authority",
+ "marker_close_authority"
+ ],
+ "type": "object"
+ },
+ "reopen_validation": {
+ "additionalProperties": false,
+ "properties": {
+ "generation_history_validation": {
+ "const": "bounded_count_plus_active_ordinal_v1"
+ },
+ "mode": {
+ "const": "bounded_full_raw_recount_v1"
+ },
+ "raw_event_rejection_scan_bound": {
+ "const": 25001
+ },
+ "raw_tag_rejection_scan_bound": {
+ "const": 250001
+ },
+ "retained_generation_rejection_scan_bound": {
+ "const": 9
+ }
+ },
+ "required": [
+ "mode",
+ "raw_event_rejection_scan_bound",
+ "raw_tag_rejection_scan_bound",
+ "generation_history_validation",
+ "retained_generation_rejection_scan_bound"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "const": 1
+ }
+ },
+ "required": [
+ "version",
+ "event_contract_registry_version",
+ "capacity_authority_id",
+ "accounting",
+ "limits",
+ "reopen_validation",
+ "rebuild_seal"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "schema_version",
+ "contract_id",
+ "hook_id",
+ "manifest_schema",
+ "predecessor",
+ "migration",
+ "source_maintenance",
+ "entry_points",
+ "source_files",
+ "public_api",
+ "result_vector"
+ ],
+ "title": "Radroots event-store SourceMaintenance v1 manifest",
+ "type": "object"
+}
diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 b/crates/event_store/contracts/source_maintenance_v1.manifest.sha256
@@ -0,0 +1 @@
+e8911e6e5710278969cbd15557a5b856b1575dfd11a655711403598370b41221
diff --git a/crates/event_store/migrations/0004_source_maintenance.down.sql b/crates/event_store/migrations/0004_source_maintenance.down.sql
@@ -0,0 +1,138 @@
+DROP TRIGGER radroots_event_store_source_capacity_marker_close_guard;
+DROP TRIGGER radroots_event_store_source_generation_capacity_advance;
+DROP TRIGGER radroots_event_store_source_generation_capacity_guard;
+DROP TRIGGER radroots_event_store_source_capacity_delete_guard;
+DROP TRIGGER radroots_event_store_source_capacity_update_guard;
+DROP TRIGGER radroots_event_store_source_capacity_insert_guard;
+DROP TABLE radroots_event_store_source_capacity_v1;
+
+DROP TRIGGER radroots_event_store_food_availability_image_delete_guard;
+
+CREATE TRIGGER radroots_event_store_food_availability_image_delete_guard
+BEFORE DELETE ON radroots_event_store_food_availability_image
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_source_state AS source ON source.singleton = 1
+ WHERE cursor.singleton = 1
+ AND cursor.source_generation != source.active_generation
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_addressable_head_transition AS transition
+ ON transition.source_generation = cursor.source_generation
+ AND transition.transition_seq > cursor.last_transition_seq
+ AND transition.kind = 30402
+ AND transition.pubkey = OLD.pubkey
+ AND transition.d_tag = OLD.d_tag
+ AND transition.retracted_event_id IS NOT NULL
+ WHERE cursor.singleton = 1
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability image delete is not backed by a pending retraction');
+END;
+
+DROP TRIGGER radroots_event_store_food_availability_projection_delete_guard;
+
+CREATE TRIGGER radroots_event_store_food_availability_projection_delete_guard
+BEFORE DELETE ON radroots_event_store_food_availability_projection
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_source_state AS source ON source.singleton = 1
+ WHERE cursor.singleton = 1
+ AND cursor.source_generation != source.active_generation
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_addressable_head_transition AS transition
+ ON transition.source_generation = cursor.source_generation
+ AND transition.transition_seq > cursor.last_transition_seq
+ AND transition.kind = OLD.kind
+ AND transition.pubkey = OLD.pubkey
+ AND transition.d_tag = OLD.d_tag
+ AND transition.retracted_event_id = OLD.event_id
+ WHERE cursor.singleton = 1
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability projection delete is not backed by a pending retraction');
+END;
+
+DROP TRIGGER radroots_event_store_source_rebuild_marker_insert_guard;
+
+CREATE TRIGGER radroots_event_store_source_rebuild_marker_insert_guard
+BEFORE INSERT ON radroots_event_store_source_rebuild_marker
+WHEN EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_rebuild_marker
+)
+OR EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ WHERE source_generation = NEW.target_generation
+)
+OR NEW.target_generation_ordinal != (
+ SELECT COALESCE(MAX(generation_ordinal), 0) + 1
+ FROM radroots_event_store_source_generation
+)
+OR NEW.transition_floor_seq != (
+ SELECT COALESCE(MAX(transition_seq), 0)
+ FROM radroots_event_store_addressable_head_transition
+)
+OR NEW.baseline_raw_event_count != (
+ SELECT COUNT(*)
+ FROM event_envelopes
+)
+OR NEW.baseline_raw_tag_count != (
+ SELECT COUNT(*)
+ FROM event_envelope_tags
+)
+OR NEW.baseline_raw_high_water_seq != (
+ SELECT COALESCE(MAX(seq), 0)
+ FROM event_envelopes
+)
+OR NOT (
+ (
+ NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_state
+ )
+ AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ )
+ AND NEW.target_generation_ordinal = 1
+ AND NEW.transition_floor_seq = 0
+ AND NEW.prior_active_generation IS NULL
+ AND NEW.prior_raw_event_count IS NULL
+ AND NEW.prior_raw_tag_count IS NULL
+ AND NEW.prior_raw_high_water_seq IS NULL
+ AND NEW.prior_last_transition_seq IS NULL
+ )
+ OR EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_state AS state
+ JOIN radroots_event_store_source_generation AS generation
+ ON generation.source_generation = state.active_generation
+ WHERE state.singleton = 1
+ AND generation.generation_ordinal = (
+ SELECT MAX(candidate.generation_ordinal)
+ FROM radroots_event_store_source_generation AS candidate
+ )
+ AND NEW.target_generation_ordinal = generation.generation_ordinal + 1
+ AND NEW.prior_active_generation = state.active_generation
+ AND NEW.prior_raw_event_count = state.raw_event_count
+ AND NEW.prior_raw_tag_count = state.raw_tag_count
+ AND NEW.prior_raw_high_water_seq = state.raw_high_water_seq
+ AND NEW.prior_last_transition_seq = state.last_transition_seq
+ AND NEW.transition_floor_seq = state.last_transition_seq
+ AND NEW.baseline_raw_event_count = state.raw_event_count
+ AND NEW.baseline_raw_tag_count = state.raw_tag_count
+ AND NEW.baseline_raw_high_water_seq = state.raw_high_water_seq
+ )
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store rebuild marker does not bind exact raw and prior source authority');
+END;
diff --git a/crates/event_store/migrations/0004_source_maintenance.up.sql b/crates/event_store/migrations/0004_source_maintenance.up.sql
@@ -0,0 +1,583 @@
+DROP TRIGGER radroots_event_store_source_rebuild_marker_insert_guard;
+
+CREATE TRIGGER radroots_event_store_source_rebuild_marker_insert_guard
+BEFORE INSERT ON radroots_event_store_source_rebuild_marker
+WHEN EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_rebuild_marker
+)
+OR EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ WHERE source_generation = NEW.target_generation
+)
+OR NEW.target_generation_ordinal != (
+ SELECT COALESCE(MAX(generation_ordinal), 0) + 1
+ FROM radroots_event_store_source_generation
+)
+OR NEW.transition_floor_seq != (
+ SELECT COALESCE(MAX(transition_seq), 0)
+ FROM radroots_event_store_addressable_head_transition
+)
+OR NEW.baseline_raw_event_count != (
+ SELECT COUNT(*)
+ FROM event_envelopes
+)
+OR NEW.baseline_raw_tag_count != (
+ SELECT COUNT(*)
+ FROM event_envelope_tags
+)
+OR NEW.baseline_raw_high_water_seq != (
+ SELECT COALESCE(MAX(seq), 0)
+ FROM event_envelopes
+)
+OR NOT (
+ (
+ NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_state
+ )
+ AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ )
+ AND NEW.target_generation_ordinal = 1
+ AND NEW.transition_floor_seq = 0
+ AND NEW.prior_active_generation IS NULL
+ AND NEW.prior_raw_event_count IS NULL
+ AND NEW.prior_raw_tag_count IS NULL
+ AND NEW.prior_raw_high_water_seq IS NULL
+ AND NEW.prior_last_transition_seq IS NULL
+ )
+ OR EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_state AS state
+ JOIN radroots_event_store_source_generation AS generation
+ ON generation.source_generation = state.active_generation
+ WHERE state.singleton = 1
+ AND generation.generation_ordinal = (
+ SELECT MAX(candidate.generation_ordinal)
+ FROM radroots_event_store_source_generation AS candidate
+ )
+ AND NEW.target_generation_ordinal = generation.generation_ordinal + 1
+ AND NEW.prior_active_generation = state.active_generation
+ AND NEW.prior_raw_event_count = state.raw_event_count
+ AND NEW.prior_raw_tag_count = state.raw_tag_count
+ AND NEW.prior_raw_high_water_seq = state.raw_high_water_seq
+ AND NEW.prior_last_transition_seq = state.last_transition_seq
+ AND NEW.baseline_raw_event_count = state.raw_event_count
+ AND NEW.baseline_raw_tag_count = state.raw_tag_count
+ AND NEW.baseline_raw_high_water_seq = state.raw_high_water_seq
+ )
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store rebuild marker does not bind exact raw and prior source authority');
+END;
+
+DROP TRIGGER radroots_event_store_food_availability_projection_delete_guard;
+
+CREATE TRIGGER radroots_event_store_food_availability_projection_delete_guard
+BEFORE DELETE ON radroots_event_store_food_availability_projection
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_source_state AS source ON source.singleton = 1
+ WHERE cursor.singleton = 1
+ AND cursor.source_generation != source.active_generation
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_addressable_head_transition AS transition
+ ON transition.source_generation = cursor.source_generation
+ AND transition.transition_seq > cursor.last_transition_seq
+ AND transition.kind = OLD.kind
+ AND transition.pubkey = OLD.pubkey
+ AND transition.d_tag = OLD.d_tag
+ AND transition.retracted_event_id = OLD.event_id
+ WHERE cursor.singleton = 1
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_rebuild_marker AS marker
+ JOIN radroots_event_store_source_state AS source
+ ON source.singleton = marker.singleton
+ AND source.active_generation = marker.target_generation
+ WHERE marker.singleton = 1
+ AND OLD.source_generation != source.active_generation
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability projection delete is not backed by a pending retraction or active source rebuild');
+END;
+
+DROP TRIGGER radroots_event_store_food_availability_image_delete_guard;
+
+CREATE TRIGGER radroots_event_store_food_availability_image_delete_guard
+BEFORE DELETE ON radroots_event_store_food_availability_image
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_source_state AS source ON source.singleton = 1
+ WHERE cursor.singleton = 1
+ AND cursor.source_generation != source.active_generation
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_cursor AS cursor
+ JOIN radroots_event_store_addressable_head_transition AS transition
+ ON transition.source_generation = cursor.source_generation
+ AND transition.transition_seq > cursor.last_transition_seq
+ AND transition.kind = 30402
+ AND transition.pubkey = OLD.pubkey
+ AND transition.d_tag = OLD.d_tag
+ AND transition.retracted_event_id IS NOT NULL
+ WHERE cursor.singleton = 1
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_rebuild_marker AS marker
+ JOIN radroots_event_store_source_state AS source
+ ON source.singleton = marker.singleton
+ AND source.active_generation = marker.target_generation
+ WHERE marker.singleton = 1
+ AND OLD.source_generation != source.active_generation
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store FoodAvailability image delete is not backed by a pending retraction or active source rebuild');
+END;
+
+CREATE TABLE radroots_event_store_source_capacity_v1 (
+ singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
+ source_generation BLOB NOT NULL UNIQUE CHECK (
+ length(source_generation) = 32
+ ) REFERENCES radroots_event_store_source_generation(source_generation)
+ ON DELETE RESTRICT,
+ raw_event_count INTEGER NOT NULL CHECK (
+ raw_event_count >= 0 AND raw_event_count <= 25000
+ ),
+ raw_tag_count INTEGER NOT NULL CHECK (
+ raw_tag_count >= 0 AND raw_tag_count <= 250000
+ ),
+ raw_event_bytes INTEGER NOT NULL CHECK (
+ raw_event_bytes >= 0 AND raw_event_bytes <= 67108864
+ ),
+ raw_tag_bytes INTEGER NOT NULL CHECK (
+ raw_tag_bytes >= 0 AND raw_tag_bytes <= 33554432
+ ),
+ raw_high_water_seq INTEGER NOT NULL CHECK (raw_high_water_seq >= 0),
+ retained_generation_count INTEGER NOT NULL CHECK (
+ retained_generation_count >= 1 AND retained_generation_count <= 8
+ ),
+ retained_generation_limit INTEGER NOT NULL CHECK (
+ retained_generation_limit = 8
+ )
+) STRICT, WITHOUT ROWID;
+
+CREATE TRIGGER radroots_event_store_source_capacity_insert_guard
+BEFORE INSERT ON radroots_event_store_source_capacity_v1
+WHEN EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_capacity_v1
+)
+OR NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_state AS state
+ WHERE state.singleton = 1
+ AND NEW.singleton = state.singleton
+ AND NEW.source_generation = state.active_generation
+ AND NEW.raw_event_count = state.raw_event_count
+ AND NEW.raw_tag_count = state.raw_tag_count
+ AND NEW.raw_high_water_seq = state.raw_high_water_seq
+ AND NEW.raw_event_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM event_envelopes
+ LIMIT 25001
+ )
+ )
+ AND NEW.raw_tag_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM event_envelope_tags
+ LIMIT 250001
+ )
+ )
+ AND NEW.raw_event_bytes = (
+ SELECT COALESCE(SUM(raw_bytes), 0)
+ FROM (
+ SELECT
+ length(CAST(event_id AS BLOB))
+ + length(CAST(pubkey AS BLOB))
+ + length(CAST(tags_json AS BLOB))
+ + length(CAST(content AS BLOB))
+ + length(CAST(sig AS BLOB))
+ + length(CAST(raw_json AS BLOB)) AS raw_bytes
+ FROM event_envelopes
+ LIMIT 25001
+ )
+ )
+ AND NEW.raw_tag_bytes = (
+ SELECT COALESCE(SUM(raw_bytes), 0)
+ FROM (
+ SELECT
+ length(CAST(event_id AS BLOB))
+ + length(CAST(tag_name AS BLOB))
+ + COALESCE(length(CAST(tag_value AS BLOB)), 0)
+ + length(CAST(tag_json AS BLOB)) AS raw_bytes
+ FROM event_envelope_tags
+ LIMIT 250001
+ )
+ )
+ AND NEW.retained_generation_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ LIMIT 9
+ )
+ )
+ AND NEW.retained_generation_limit = 8
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store source capacity initialization must seal exact raw and generation authority');
+END;
+
+CREATE TRIGGER radroots_event_store_source_capacity_update_guard
+BEFORE UPDATE ON radroots_event_store_source_capacity_v1
+WHEN NOT (
+ (
+ NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_rebuild_marker
+ WHERE singleton = 1
+ )
+ AND NEW.singleton IS OLD.singleton
+ AND NEW.source_generation IS OLD.source_generation
+ AND NEW.retained_generation_count = OLD.retained_generation_count
+ AND NEW.retained_generation_limit = OLD.retained_generation_limit
+ AND NEW.raw_event_count = OLD.raw_event_count + 1
+ AND NEW.raw_tag_count = OLD.raw_tag_count + (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM event_envelope_tags AS tag
+ JOIN event_envelopes AS event ON event.event_id = tag.event_id
+ WHERE event.seq > OLD.raw_high_water_seq
+ LIMIT 250001
+ )
+ )
+ AND NEW.raw_event_bytes = OLD.raw_event_bytes + (
+ SELECT COALESCE(SUM(raw_bytes), 0)
+ FROM (
+ SELECT
+ length(CAST(event_id AS BLOB))
+ + length(CAST(pubkey AS BLOB))
+ + length(CAST(tags_json AS BLOB))
+ + length(CAST(content AS BLOB))
+ + length(CAST(sig AS BLOB))
+ + length(CAST(raw_json AS BLOB)) AS raw_bytes
+ FROM event_envelopes
+ WHERE seq > OLD.raw_high_water_seq
+ LIMIT 2
+ )
+ )
+ AND NEW.raw_tag_bytes = OLD.raw_tag_bytes + (
+ SELECT COALESCE(SUM(raw_bytes), 0)
+ FROM (
+ SELECT
+ length(CAST(tag.event_id AS BLOB))
+ + length(CAST(tag.tag_name AS BLOB))
+ + COALESCE(length(CAST(tag.tag_value AS BLOB)), 0)
+ + length(CAST(tag.tag_json AS BLOB)) AS raw_bytes
+ FROM event_envelope_tags AS tag
+ JOIN event_envelopes AS event ON event.event_id = tag.event_id
+ WHERE event.seq > OLD.raw_high_water_seq
+ LIMIT 250001
+ )
+ )
+ AND NEW.raw_high_water_seq = (
+ SELECT COALESCE(MAX(seq), 0)
+ FROM event_envelopes
+ )
+ AND 1 = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM event_envelopes
+ WHERE seq > OLD.raw_high_water_seq
+ LIMIT 2
+ )
+ )
+ AND EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_state AS state
+ WHERE state.singleton = 1
+ AND state.active_generation = NEW.source_generation
+ AND state.raw_event_count = NEW.raw_event_count
+ AND state.raw_tag_count = NEW.raw_tag_count
+ AND state.raw_high_water_seq = NEW.raw_high_water_seq
+ )
+ )
+ OR (
+ NEW.singleton IS OLD.singleton
+ AND NEW.source_generation IS OLD.source_generation
+ AND NEW.raw_event_count = OLD.raw_event_count
+ AND NEW.raw_tag_count = OLD.raw_tag_count
+ AND NEW.raw_event_bytes = OLD.raw_event_bytes
+ AND NEW.raw_tag_bytes = OLD.raw_tag_bytes
+ AND NEW.raw_high_water_seq = OLD.raw_high_water_seq
+ AND NEW.retained_generation_count = OLD.retained_generation_count + 1
+ AND NEW.retained_generation_limit = OLD.retained_generation_limit
+ AND NEW.retained_generation_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ LIMIT 9
+ )
+ )
+ AND EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_rebuild_marker AS marker
+ JOIN radroots_event_store_source_generation AS generation
+ ON generation.source_generation = marker.target_generation
+ WHERE marker.singleton = 1
+ AND generation.generation_ordinal = NEW.retained_generation_count
+ AND generation.generation_ordinal = (
+ SELECT MAX(candidate.generation_ordinal)
+ FROM radroots_event_store_source_generation AS candidate
+ )
+ )
+ )
+ OR (
+ NEW.singleton IS OLD.singleton
+ AND NEW.source_generation IS NOT OLD.source_generation
+ AND NEW.raw_event_count = OLD.raw_event_count
+ AND NEW.raw_tag_count = OLD.raw_tag_count
+ AND NEW.raw_event_bytes = OLD.raw_event_bytes
+ AND NEW.raw_tag_bytes = OLD.raw_tag_bytes
+ AND NEW.raw_high_water_seq = OLD.raw_high_water_seq
+ AND NEW.retained_generation_count = OLD.retained_generation_count
+ AND NEW.retained_generation_limit = OLD.retained_generation_limit
+ AND EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_rebuild_marker AS marker
+ JOIN radroots_event_store_source_state AS state
+ ON state.singleton = marker.singleton
+ AND state.active_generation = marker.target_generation
+ WHERE marker.singleton = 1
+ AND NEW.source_generation = marker.target_generation
+ AND NEW.source_generation = state.active_generation
+ AND NEW.raw_event_count = state.raw_event_count
+ AND NEW.raw_tag_count = state.raw_tag_count
+ AND NEW.raw_high_water_seq = state.raw_high_water_seq
+ AND NEW.raw_event_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM event_envelopes
+ LIMIT 25001
+ )
+ )
+ AND NEW.raw_tag_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM event_envelope_tags
+ LIMIT 250001
+ )
+ )
+ AND NEW.raw_event_bytes = (
+ SELECT COALESCE(SUM(raw_bytes), 0)
+ FROM (
+ SELECT
+ length(CAST(event_id AS BLOB))
+ + length(CAST(pubkey AS BLOB))
+ + length(CAST(tags_json AS BLOB))
+ + length(CAST(content AS BLOB))
+ + length(CAST(sig AS BLOB))
+ + length(CAST(raw_json AS BLOB)) AS raw_bytes
+ FROM event_envelopes
+ LIMIT 25001
+ )
+ )
+ AND NEW.raw_tag_bytes = (
+ SELECT COALESCE(SUM(raw_bytes), 0)
+ FROM (
+ SELECT
+ length(CAST(event_id AS BLOB))
+ + length(CAST(tag_name AS BLOB))
+ + COALESCE(length(CAST(tag_value AS BLOB)), 0)
+ + length(CAST(tag_json AS BLOB)) AS raw_bytes
+ FROM event_envelope_tags
+ LIMIT 250001
+ )
+ )
+ AND NEW.retained_generation_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ LIMIT 9
+ )
+ )
+ )
+ )
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store source capacity update is outside its append or rebuild phase');
+END;
+
+CREATE TRIGGER radroots_event_store_source_capacity_delete_guard
+BEFORE DELETE ON radroots_event_store_source_capacity_v1
+BEGIN
+ SELECT RAISE(ABORT, 'event-store source capacity authority is immutable');
+END;
+
+CREATE TRIGGER radroots_event_store_source_generation_capacity_guard
+BEFORE INSERT ON radroots_event_store_source_generation
+WHEN EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_capacity_v1
+ WHERE singleton = 1
+ AND retained_generation_count >= retained_generation_limit
+)
+AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ WHERE source_generation = NEW.source_generation
+ OR generation_ordinal = NEW.generation_ordinal
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store retained source generation limit reached; replace and resync into a fresh store');
+END;
+
+CREATE TRIGGER radroots_event_store_source_generation_capacity_advance
+AFTER INSERT ON radroots_event_store_source_generation
+WHEN EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_capacity_v1
+ WHERE singleton = 1
+)
+BEGIN
+ UPDATE radroots_event_store_source_capacity_v1
+ SET retained_generation_count = retained_generation_count + 1
+ WHERE singleton = 1;
+ SELECT CASE
+ WHEN changes() != 1
+ THEN RAISE(ABORT, 'event-store retained source generation authority did not advance')
+ END;
+END;
+
+CREATE TRIGGER radroots_event_store_source_capacity_marker_close_guard
+BEFORE DELETE ON radroots_event_store_source_rebuild_marker
+WHEN NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_source_capacity_v1 AS capacity
+ JOIN radroots_event_store_source_state AS state
+ ON state.singleton = capacity.singleton
+ AND state.active_generation = capacity.source_generation
+ JOIN radroots_event_store_source_generation AS generation
+ ON generation.source_generation = state.active_generation
+ JOIN radroots_event_store_addressable_feed_integrity_v1 AS integrity
+ ON integrity.source_generation = state.active_generation
+ JOIN radroots_event_store_food_availability_cursor AS cursor
+ ON cursor.singleton = state.singleton
+ AND cursor.source_generation = state.active_generation
+ WHERE capacity.singleton = 1
+ AND OLD.singleton = capacity.singleton
+ AND OLD.target_generation = capacity.source_generation
+ AND capacity.raw_event_count = state.raw_event_count
+ AND capacity.raw_tag_count = state.raw_tag_count
+ AND capacity.raw_high_water_seq = state.raw_high_water_seq
+ AND capacity.raw_event_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM event_envelopes
+ LIMIT 25001
+ )
+ )
+ AND capacity.raw_tag_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM event_envelope_tags
+ LIMIT 250001
+ )
+ )
+ AND capacity.raw_event_bytes = (
+ SELECT COALESCE(SUM(raw_bytes), 0)
+ FROM (
+ SELECT
+ length(CAST(event_id AS BLOB))
+ + length(CAST(pubkey AS BLOB))
+ + length(CAST(tags_json AS BLOB))
+ + length(CAST(content AS BLOB))
+ + length(CAST(sig AS BLOB))
+ + length(CAST(raw_json AS BLOB)) AS raw_bytes
+ FROM event_envelopes
+ LIMIT 25001
+ )
+ )
+ AND capacity.raw_tag_bytes = (
+ SELECT COALESCE(SUM(raw_bytes), 0)
+ FROM (
+ SELECT
+ length(CAST(event_id AS BLOB))
+ + length(CAST(tag_name AS BLOB))
+ + COALESCE(length(CAST(tag_value AS BLOB)), 0)
+ + length(CAST(tag_json AS BLOB)) AS raw_bytes
+ FROM event_envelope_tags
+ LIMIT 250001
+ )
+ )
+ AND capacity.retained_generation_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM radroots_event_store_source_generation
+ LIMIT 9
+ )
+ )
+ AND capacity.retained_generation_limit = 8
+ AND generation.generation_ordinal = capacity.retained_generation_count
+ AND integrity.transition_floor_seq = generation.transition_floor_seq
+ AND integrity.last_transition_seq = state.last_transition_seq
+ AND integrity.transition_count =
+ state.last_transition_seq - generation.transition_floor_seq
+ AND cursor.feed_version = 1
+ AND cursor.projection_version = 1
+ AND hex(cursor.scope_fingerprint) =
+ '8B63C5DDC48A2CC7DB69295238B96D5F814DBA50427C80B4D0079F061E6D3DE0'
+ AND cursor.hook_manifest_sha256 =
+ '33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23'
+ AND cursor.last_transition_seq = state.last_transition_seq
+ AND cursor.projected_row_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM radroots_event_store_food_availability_projection
+ WHERE source_generation = state.active_generation
+ LIMIT 25001
+ )
+ )
+ AND cursor.projected_row_count = (
+ SELECT COUNT(*)
+ FROM (
+ SELECT 1
+ FROM radroots_event_store_food_availability_search_fts
+ LIMIT 25001
+ )
+ )
+ AND NOT EXISTS (
+ SELECT 1
+ FROM radroots_event_store_food_availability_projection
+ WHERE source_generation != state.active_generation
+ )
+)
+BEGIN
+ SELECT RAISE(ABORT, 'event-store rebuild marker cannot close before capacity, NIP-09, and FoodAvailability seals agree');
+END;
diff --git a/crates/event_store/src/error.rs b/crates/event_store/src/error.rs
@@ -4,10 +4,21 @@ use radroots_event::wire::RadrootsEventWireError;
use radroots_event_codec::verification::RadrootsNip01VerificationError;
use radroots_transport::RadrootsTransportError;
-/// Resource dimension that bounded NIP-09 reconciliation exceeded.
+/// Maximum retained raw event rows in one event store.
+pub const RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1: u64 = 25_000;
+/// Maximum retained raw tag rows in one event store.
+pub const RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1: u64 = 250_000;
+/// Maximum governed UTF-8 bytes in retained raw event text columns.
+pub const RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1: u64 = 64 * 1024 * 1024;
+/// Maximum governed UTF-8 bytes in retained raw tag text columns.
+pub const RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1: u64 = 32 * 1024 * 1024;
+/// Maximum append-only source generations retained before fresh-store resync.
+pub const RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1: u32 = 8;
+
+/// Governed retained raw-source resource dimension.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
-pub enum RadrootsEventStoreReconciliationResource {
+pub enum RadrootsEventStoreSourceCapacityResourceV1 {
/// Number of retained raw-source event rows.
RawEvents,
/// Number of retained raw-source tag rows.
@@ -18,7 +29,7 @@ pub enum RadrootsEventStoreReconciliationResource {
RawTagBytes,
}
-impl RadrootsEventStoreReconciliationResource {
+impl RadrootsEventStoreSourceCapacityResourceV1 {
/// Stable diagnostic label used by the typed capacity error.
pub const fn as_str(self) -> &'static str {
match self {
@@ -30,7 +41,7 @@ impl RadrootsEventStoreReconciliationResource {
}
}
-impl core::fmt::Display for RadrootsEventStoreReconciliationResource {
+impl core::fmt::Display for RadrootsEventStoreSourceCapacityResourceV1 {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(self.as_str())
}
@@ -114,6 +125,8 @@ pub enum RadrootsEventStoreError {
SqlitePoolBackingMismatch { file_backed: bool, filename: String },
#[error("event-store SQLite connection has no main database")]
SqliteMainDatabaseUnavailable,
+ #[error("event-store SQLite main database must use UTF-8 encoding; reported `{actual}`")]
+ SqliteMainDatabaseEncodingNotUtf8 { actual: String },
#[error(
"event-store SQLite file connection did not enter WAL journal mode; reported `{actual}`"
)]
@@ -189,6 +202,14 @@ pub enum RadrootsEventStoreError {
RollbackBelowVersionFloor { floor: u32, target: u32 },
#[error("event-store rollback target {target} is ahead of managed version {current}")]
RollbackAhead { current: u32, target: u32 },
+ #[error(
+ "event-store rollback from version {current} to {target} would discard retained source-generation history; minimum retained-history schema version is {floor}"
+ )]
+ RollbackWouldDiscardSourceGenerationHistory {
+ current: u32,
+ target: u32,
+ floor: u32,
+ },
#[error("event-store rollback requires a managed schema")]
RollbackUnmanaged,
#[error(
@@ -210,15 +231,26 @@ pub enum RadrootsEventStoreError {
#[error("event-store source generation entropy is unavailable")]
SourceGenerationEntropyUnavailable,
#[error(
- "event-store NIP-09 reconciliation {resource} capacity exceeded: observed {actual}, limit {limit}"
+ "event-store retained source {resource} capacity exceeded: current {current}, requested additional {requested}, limit {limit}; durable append refused, retain a bounded source set in a new disposable cache"
)]
- /// Refuses a one-time local-store migration before unbounded retention or
- /// partial writes; callers can recover by pruning or rebuilding the cache.
- ReconciliationCapacityExceeded {
- resource: RadrootsEventStoreReconciliationResource,
- actual: u64,
+ /// Refuses migration or prospective durable ingest before the retained raw
+ /// source can become unrebuildable. Immutable raw rows are never pruned.
+ SourceCapacityExceeded {
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
+ current: u64,
+ requested: u64,
limit: u64,
},
+ #[error(
+ "event-store retained source generation limit reached: current {current}, limit {limit}; replace and resync into a fresh store"
+ )]
+ SourceGenerationHistoryLimitReached { current: u32, limit: u32 },
+ #[error(
+ "event-store retained source contains ephemeral event `{event_id}` of kind {kind}; ephemeral events must be discarded"
+ )]
+ PersistedEphemeralRawEvent { event_id: String, kind: i64 },
+ #[error("event-store retained source capacity authority is inconsistent: {reason}")]
+ SourceCapacityStateDrift { reason: String },
#[error("event-store migration hook `{hook_id}` state is invalid: {reason}")]
MigrationHookStateDrift {
hook_id: &'static str,
diff --git a/crates/event_store/src/generated.rs b/crates/event_store/src/generated.rs
@@ -1,2 +1,3 @@
pub(crate) mod food_availability_projection_manifest;
pub(crate) mod nip09_reconciliation_manifest;
+pub(crate) mod source_maintenance_manifest;
diff --git a/crates/event_store/src/generated/source_maintenance_manifest.rs b/crates/event_store/src/generated/source_maintenance_manifest.rs
@@ -0,0 +1,54 @@
+// @generated by `cargo xtask contract source-maintenance-manifest --write`; do not edit.
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"ad4a6c8ae9488fc8033792bc6952af04687f312901c1847d8c668a62913bb642\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\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 \"down\": {\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 \"schema_sha256\": \"d526d96ea02be12b4b0aed99e97cfdde17c4474ace67111506a7b900ee78b186\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19421,\n \"sha256\": \"4772e041cb20a4963afb2f3159804c777e2f2be61bfdb6ee267e7a7c04258972\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3844,\n \"sha256\": \"3cd9653bcb752fb3c4442d4904b98a0a6208011a9a238125b7b7073d7f4e312b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_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\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 33617,\n \"sha256\": \"79296b8f263aa06d17005795e4515f769f064ea6fd971eeb1296e1151debaf20\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184407,\n \"sha256\": \"c455d40fc736e3db264f567c7809af7bd897d89be8a33dfb21667a6ef6b8d6c6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 394574,\n \"sha256\": \"db57dc3e35e64c7194683142fe55edba853671a829269449dd2273056dfc3a0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\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\": \"source_maintenance_runtime\",\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\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 38279,\n \"sha256\": \"f326ea57b56d40135f95b6b1e15961f66eed363337180a6d12a5ea903e1a9a29\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 194875,\n \"sha256\": \"63cc3e6985741e2002ae59f56500bf8d6e0a8246f97a0b8b7f2d2372ef0642e0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 834200,\n \"sha256\": \"155374b306f3b30f6095dbfc203150c928b7418e646764011b0401996a636f84\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 176693,\n \"sha256\": \"88ea58150c49faaad7dde6c9cc89a92ad6b693b112fd55041ac17984bc5c6700\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 479173,\n \"sha256\": \"15b3e754ed6794c8f4c48d7bd316b05ff90578137adb0275e1febdf2891707b4\",\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\": 14077,\n \"sha256\": \"d815e65241e47143a51dd87d95e014d975be1d9b94075f3920e4812f41188353\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n";
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH: usize = 14216;
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SHA256: &str =
+ "e8911e6e5710278969cbd15557a5b856b1575dfd11a655711403598370b41221";
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION: u32 = 1;
+pub(crate) const SOURCE_MAINTENANCE_CONTRACT_ID: &str =
+ "radroots_event_store.source_maintenance_v1";
+pub(crate) const SOURCE_MAINTENANCE_HOOK_ID: &str = "source_maintenance_v1";
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_VERSION: u32 = 4;
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_NAME: &str = "source_maintenance";
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_UP_BYTE_LENGTH: usize = 19841;
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_UP_SHA256: &str =
+ "425dc799f392b87f265a6fb81f89c4a1c7a5db8391ab0380970708cb9c66704d";
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_DOWN_BYTE_LENGTH: usize = 5172;
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_DOWN_SHA256: &str =
+ "fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860";
+pub(crate) const SOURCE_MAINTENANCE_SCHEMA_SHA256: &str =
+ "d526d96ea02be12b4b0aed99e97cfdde17c4474ace67111506a7b900ee78b186";
+pub(crate) const SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION: u32 = 7;
+pub(crate) const SOURCE_MAINTENANCE_CAPACITY_VERSION: u32 = 1;
+pub(crate) const SOURCE_MAINTENANCE_CAPACITY_AUTHORITY_ID: &str =
+ "radroots_event_store_source_capacity_v1";
+pub(crate) const SOURCE_MAINTENANCE_ACCOUNTING_ALGORITHM: &str = "sqlite_cast_blob_octet_sum_v1";
+pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_COLUMNS: &[&str] = &[
+ "event_id",
+ "pubkey",
+ "tags_json",
+ "content",
+ "sig",
+ "raw_json",
+];
+pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_COLUMNS: &[&str] =
+ &["event_id", "tag_name", "tag_value", "tag_json"];
+pub(crate) const SOURCE_MAINTENANCE_NULLABLE_RAW_TAG_COLUMNS: &[&str] = &["tag_value"];
+pub(crate) const SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES: &[&str] = &[
+ "radroots_event_store_food_availability_image_delete_guard",
+ "radroots_event_store_food_availability_projection_delete_guard",
+ "radroots_event_store_source_rebuild_marker_insert_guard",
+];
+pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_COUNT_LIMIT: u64 = 25000;
+pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_COUNT_LIMIT: u64 = 250000;
+pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_TEXT_BYTES_LIMIT: u64 = 67108864;
+pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_TEXT_BYTES_LIMIT: u64 = 33554432;
+pub(crate) const SOURCE_MAINTENANCE_RETAINED_SOURCE_GENERATION_LIMIT: u32 = 8;
+pub(crate) const SOURCE_MAINTENANCE_PREDECESSOR_HOOK_ID: &str = "food_availability_projection_v1";
+pub(crate) const SOURCE_MAINTENANCE_PREDECESSOR_MANIFEST_SHA256: &str =
+ "33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23";
+pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_SHA256: &str =
+ "997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f";
+pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_ID: &str =
+ "radroots_event_store.source_maintenance_v1.result_vector_executor.v1";
+pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_SHA256: &str =
+ "a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3";
diff --git a/crates/event_store/src/lib.rs b/crates/event_store/src/lib.rs
@@ -14,10 +14,18 @@ mod nip09;
#[cfg(feature = "sqlite")]
mod schema;
#[cfg(feature = "sqlite")]
+mod source_maintenance_v1;
+#[cfg(feature = "sqlite")]
mod store;
#[cfg(feature = "sqlite")]
-pub use error::{RadrootsEventStoreError, RadrootsEventStoreReconciliationResource};
+pub use error::{
+ RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1,
+ RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1,
+ RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1,
+ RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1, RadrootsEventStoreError,
+ RadrootsEventStoreSourceCapacityResourceV1,
+};
#[cfg(feature = "sqlite")]
pub use migrations::{
RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT, RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
@@ -61,6 +69,8 @@ pub use model::{
#[cfg(feature = "sqlite")]
pub use schema::{RadrootsEventStoreSchemaStatus, inspect_event_store_schema_status};
#[cfg(feature = "sqlite")]
+pub use source_maintenance_v1::RadrootsEventStoreSourceCapacityV1;
+#[cfg(feature = "sqlite")]
pub use store::{
RADROOTS_EVENT_STORE_CONTRACT_QUERY_LIMIT_MAX, RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX,
RadrootsEventStore, RadrootsTransportObservationRow, inspect_event_store_status,
diff --git a/crates/event_store/src/migrations.rs b/crates/event_store/src/migrations.rs
@@ -1,6 +1,7 @@
use crate::RadrootsEventStoreError;
use crate::generated::food_availability_projection_manifest as food_manifest;
use crate::generated::nip09_reconciliation_manifest as nip09_manifest;
+use crate::generated::source_maintenance_manifest;
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
@@ -8,7 +9,7 @@ pub(crate) const EVENT_STORE_LEDGER_NAME: &str = "radroots_event_store_schema_mi
pub(crate) const EVENT_STORE_RESERVED_PREFIX: &str = "radroots_event_store_";
pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN: u32 = 1;
-pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 3;
+pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 4;
pub(crate) const EVENT_STORE_LEDGER_DDL: &str = "CREATE TABLE radroots_event_store_schema_migrations (
version INTEGER PRIMARY KEY NOT NULL CHECK (version > 0),
@@ -105,6 +106,7 @@ pub(crate) enum EventStoreMigrationHook {
None,
Nip09ReconciliationV1,
FoodAvailabilityProjectionV1,
+ SourceMaintenanceV1,
}
impl EventStoreMigrationHook {
@@ -115,6 +117,7 @@ impl EventStoreMigrationHook {
Self::FoodAvailabilityProjectionV1 => {
food_manifest::FOOD_AVAILABILITY_PROJECTION_HOOK_ID
}
+ Self::SourceMaintenanceV1 => source_maintenance_manifest::SOURCE_MAINTENANCE_HOOK_ID,
}
}
@@ -127,10 +130,32 @@ impl EventStoreMigrationHook {
Self::FoodAvailabilityProjectionV1 => {
Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256)
}
+ Self::SourceMaintenanceV1 => {
+ Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256)
+ }
}
}
}
+pub(crate) const EVENT_STORE_SOURCE_MAINTENANCE_OBJECT_NAMES: &[&str] = &[
+ "radroots_event_store_source_capacity_delete_guard",
+ "radroots_event_store_source_capacity_insert_guard",
+ "radroots_event_store_source_capacity_marker_close_guard",
+ "radroots_event_store_source_capacity_update_guard",
+ "radroots_event_store_source_capacity_v1",
+ "radroots_event_store_source_generation_capacity_advance",
+ "radroots_event_store_source_generation_capacity_guard",
+];
+
+pub(crate) const EVENT_STORE_SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES: &[&str] = &[
+ "radroots_event_store_food_availability_image_delete_guard",
+ "radroots_event_store_food_availability_projection_delete_guard",
+ "radroots_event_store_source_rebuild_marker_insert_guard",
+];
+
+pub(crate) const EVENT_STORE_SOURCE_MAINTENANCE_TABLE_NAMES: &[&str] =
+ &["radroots_event_store_source_capacity_v1"];
+
pub(crate) const EVENT_STORE_FOOD_AVAILABILITY_OBJECT_NAMES: &[&str] = &[
"radroots_event_store_addressable_feed_generation_insert",
"radroots_event_store_addressable_feed_integrity_v1",
@@ -292,6 +317,7 @@ pub(crate) struct EventStoreMigration {
pub(crate) down_sha256: &'static str,
pub(crate) schema_sha256: &'static str,
pub(crate) owned_object_names: &'static [&'static str],
+ pub(crate) replaced_object_names: &'static [&'static str],
pub(crate) owned_table_names: &'static [&'static str],
pub(crate) fts5_table_names: &'static [&'static str],
pub(crate) hook: EventStoreMigrationHook,
@@ -311,6 +337,7 @@ pub(crate) const EVENT_STORE_MIGRATIONS: &[EventStoreMigration] = &[
down_sha256: "fa84d587f657f601947eaeb9cd239c962a48f6fcdce723588476e8d22f3c1f53",
schema_sha256: "5b1f92779640f1a2dbd75e37a96996bda6c8be58883190f69eb3eced22a48f03",
owned_object_names: EVENT_STORE_BASELINE_OBJECT_NAMES,
+ replaced_object_names: &[],
owned_table_names: EVENT_STORE_BASELINE_TABLE_NAMES,
fts5_table_names: EVENT_STORE_BASELINE_FTS5_TABLE_NAMES,
hook: EventStoreMigrationHook::None,
@@ -328,6 +355,7 @@ pub(crate) const EVENT_STORE_MIGRATIONS: &[EventStoreMigration] = &[
down_sha256: nip09_manifest::NIP09_RECONCILIATION_MIGRATION_DOWN_SHA256,
schema_sha256: nip09_manifest::NIP09_RECONCILIATION_SCHEMA_SHA256,
owned_object_names: EVENT_STORE_NIP09_OBJECT_NAMES,
+ replaced_object_names: &[],
owned_table_names: EVENT_STORE_NIP09_TABLE_NAMES,
fts5_table_names: &[],
hook: EventStoreMigrationHook::Nip09ReconciliationV1,
@@ -347,6 +375,7 @@ pub(crate) const EVENT_STORE_MIGRATIONS: &[EventStoreMigration] = &[
down_sha256: food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_DOWN_SHA256,
schema_sha256: food_manifest::FOOD_AVAILABILITY_PROJECTION_SCHEMA_SHA256,
owned_object_names: EVENT_STORE_FOOD_AVAILABILITY_OBJECT_NAMES,
+ replaced_object_names: &[],
owned_table_names: EVENT_STORE_FOOD_AVAILABILITY_TABLE_NAMES,
fts5_table_names: EVENT_STORE_FOOD_AVAILABILITY_FTS5_TABLE_NAMES,
hook: EventStoreMigrationHook::FoodAvailabilityProjectionV1,
@@ -355,6 +384,26 @@ pub(crate) const EVENT_STORE_MIGRATIONS: &[EventStoreMigration] = &[
food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION,
),
},
+ EventStoreMigration {
+ version: 4,
+ name: "source_maintenance",
+ up_sql: include_str!("../migrations/0004_source_maintenance.up.sql"),
+ down_sql: include_str!("../migrations/0004_source_maintenance.down.sql"),
+ up_len: source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_BYTE_LENGTH,
+ down_len: source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_BYTE_LENGTH,
+ up_sha256: source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_SHA256,
+ down_sha256: source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_SHA256,
+ schema_sha256: source_maintenance_manifest::SOURCE_MAINTENANCE_SCHEMA_SHA256,
+ owned_object_names: EVENT_STORE_SOURCE_MAINTENANCE_OBJECT_NAMES,
+ replaced_object_names: EVENT_STORE_SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES,
+ owned_table_names: EVENT_STORE_SOURCE_MAINTENANCE_TABLE_NAMES,
+ fts5_table_names: &[],
+ hook: EventStoreMigrationHook::SourceMaintenanceV1,
+ hook_manifest_sha256: Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256),
+ event_contract_registry_version: Some(
+ source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION,
+ ),
+ },
];
pub(crate) fn migration_for_version(
@@ -427,6 +476,12 @@ pub(crate) fn validate_migration_registry(
{
validate_generated_food_availability_projection_manifest_descriptor()?;
}
+ if registry
+ .iter()
+ .any(|migration| migration.hook == EventStoreMigrationHook::SourceMaintenanceV1)
+ {
+ validate_generated_source_maintenance_manifest_descriptor()?;
+ }
if minimum == 0 || current < minimum || registry.is_empty() {
return Err(RadrootsEventStoreError::MigrationRegistryDefect {
reason: format!(
@@ -438,7 +493,43 @@ pub(crate) fn validate_migration_registry(
let mut expected_version = minimum;
let mut owned_object_names = BTreeSet::new();
let mut owned_table_names = BTreeSet::new();
+ let mut migration_hook_ids = BTreeSet::new();
for (index, migration) in registry.iter().enumerate() {
+ let canonical_hook_migration = match migration.hook {
+ EventStoreMigrationHook::None => None,
+ EventStoreMigrationHook::Nip09ReconciliationV1 => Some((
+ nip09_manifest::NIP09_RECONCILIATION_MIGRATION_VERSION,
+ nip09_manifest::NIP09_RECONCILIATION_MIGRATION_NAME,
+ )),
+ EventStoreMigrationHook::FoodAvailabilityProjectionV1 => Some((
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_VERSION,
+ food_manifest::FOOD_AVAILABILITY_PROJECTION_MIGRATION_NAME,
+ )),
+ EventStoreMigrationHook::SourceMaintenanceV1 => Some((
+ source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_VERSION,
+ source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_NAME,
+ )),
+ };
+ if let Some((canonical_version, canonical_name)) = canonical_hook_migration {
+ if !migration_hook_ids.insert(migration.hook.id()) {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "migration hook `{}` is declared more than once",
+ migration.hook.id()
+ ),
+ });
+ }
+ if migration.version != canonical_version || migration.name != canonical_name {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "migration hook `{}` is bound to canonical migration {canonical_version} `{canonical_name}`, not migration {} `{}`",
+ migration.hook.id(),
+ migration.version,
+ migration.name
+ ),
+ });
+ }
+ }
if migration.version != expected_version {
return Err(RadrootsEventStoreError::MigrationRegistryDefect {
reason: format!(
@@ -486,6 +577,78 @@ pub(crate) fn validate_migration_registry(
});
}
}
+ if index == 0 && !migration.replaced_object_names.is_empty() {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "the baseline migration cannot replace predecessor schema objects"
+ .to_owned(),
+ });
+ }
+ if !migration.replaced_object_names.is_empty()
+ && (migration.hook == EventStoreMigrationHook::None
+ || migration.hook_manifest_sha256.is_none()
+ || migration.event_contract_registry_version.is_none())
+ {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {} replaces predecessor schema objects without an authenticated successor hook",
+ migration.version
+ ),
+ });
+ }
+ let mut migration_replacement_names = BTreeSet::new();
+ for object_name in migration.replaced_object_names {
+ validate_owned_schema_name(migration.version, "replacement object", object_name)?;
+ if !object_name.starts_with(EVENT_STORE_RESERVED_PREFIX) {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {} replacement object `{object_name}` is outside the reserved `{EVENT_STORE_RESERVED_PREFIX}` namespace",
+ migration.version
+ ),
+ });
+ }
+ if !migration_replacement_names.insert(*object_name) {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {} replacement object `{object_name}` is declared more than once",
+ migration.version
+ ),
+ });
+ }
+ if migration.owned_object_names.contains(object_name)
+ || migration.owned_table_names.contains(object_name)
+ {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {} replacement object `{object_name}` is also newly owned by that migration",
+ migration.version
+ ),
+ });
+ }
+ let prior_owners = registry[..index]
+ .iter()
+ .filter(|prior| prior.owned_object_names.contains(object_name))
+ .collect::<Vec<_>>();
+ if prior_owners.len() != 1 {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {} replacement object `{object_name}` must be owned by exactly one prior migration; found {} owners",
+ migration.version,
+ prior_owners.len()
+ ),
+ });
+ }
+ let prior_owner = prior_owners[0];
+ if prior_owner.owned_table_names.contains(object_name)
+ || prior_owner.fts5_table_names.contains(object_name)
+ {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!(
+ "migration version {} replacement object `{object_name}` is a predecessor table; only non-table schema objects may be replaced",
+ migration.version
+ ),
+ });
+ }
+ }
for table_name in migration.owned_table_names {
validate_owned_schema_name(migration.version, "table", table_name)?;
if !migration.owned_object_names.contains(table_name) {
@@ -556,6 +719,13 @@ pub(crate) fn validate_migration_registry(
EventStoreMigrationHook::FoodAvailabilityProjectionV1,
Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256),
Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION),
+ )
+ | (
+ EventStoreMigrationHook::SourceMaintenanceV1,
+ Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256),
+ Some(
+ source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION,
+ ),
) => {}
(hook, manifest, registry_version) => {
return Err(RadrootsEventStoreError::MigrationRegistryDefect {
@@ -910,6 +1080,224 @@ fn validate_generated_food_availability_projection_manifest_descriptor()
Ok(())
}
+fn validate_generated_source_maintenance_manifest_descriptor() -> Result<(), RadrootsEventStoreError>
+{
+ use source_maintenance_manifest as source_manifest;
+
+ let bytes = source_manifest::SOURCE_MAINTENANCE_MANIFEST_JSON.as_bytes();
+ if bytes.len() != source_manifest::SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated source-maintenance manifest byte length is inconsistent".to_owned(),
+ });
+ }
+ validate_sha256_literal(
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_VERSION,
+ "hook manifest",
+ source_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256,
+ )?;
+ if sha256_hex(bytes) != source_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256 {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated source-maintenance manifest digest is inconsistent".to_owned(),
+ });
+ }
+ let manifest: serde_json::Value = serde_json::from_slice(bytes).map_err(|error| {
+ RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: format!("generated source-maintenance manifest JSON is invalid: {error}"),
+ }
+ })?;
+ let expected_numbers = [
+ (
+ "/schema_version",
+ u64::from(source_manifest::SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION),
+ ),
+ (
+ "/migration/version",
+ u64::from(source_manifest::SOURCE_MAINTENANCE_MIGRATION_VERSION),
+ ),
+ (
+ "/migration/up/byte_length",
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_BYTE_LENGTH as u64,
+ ),
+ (
+ "/migration/down/byte_length",
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_BYTE_LENGTH as u64,
+ ),
+ (
+ "/source_maintenance/version",
+ u64::from(source_manifest::SOURCE_MAINTENANCE_CAPACITY_VERSION),
+ ),
+ (
+ "/source_maintenance/event_contract_registry_version",
+ u64::from(source_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION),
+ ),
+ (
+ "/source_maintenance/limits/raw_events",
+ source_manifest::SOURCE_MAINTENANCE_RAW_EVENT_COUNT_LIMIT,
+ ),
+ (
+ "/source_maintenance/limits/raw_tags",
+ source_manifest::SOURCE_MAINTENANCE_RAW_TAG_COUNT_LIMIT,
+ ),
+ (
+ "/source_maintenance/limits/raw_event_text_bytes",
+ source_manifest::SOURCE_MAINTENANCE_RAW_EVENT_TEXT_BYTES_LIMIT,
+ ),
+ (
+ "/source_maintenance/limits/raw_tag_text_bytes",
+ source_manifest::SOURCE_MAINTENANCE_RAW_TAG_TEXT_BYTES_LIMIT,
+ ),
+ (
+ "/source_maintenance/limits/retained_source_generations",
+ u64::from(source_manifest::SOURCE_MAINTENANCE_RETAINED_SOURCE_GENERATION_LIMIT),
+ ),
+ ];
+ let expected_strings = [
+ (
+ "/contract_id",
+ source_manifest::SOURCE_MAINTENANCE_CONTRACT_ID,
+ ),
+ ("/hook_id", source_manifest::SOURCE_MAINTENANCE_HOOK_ID),
+ (
+ "/predecessor/hook_id",
+ source_manifest::SOURCE_MAINTENANCE_PREDECESSOR_HOOK_ID,
+ ),
+ (
+ "/predecessor/manifest/sha256",
+ source_manifest::SOURCE_MAINTENANCE_PREDECESSOR_MANIFEST_SHA256,
+ ),
+ (
+ "/migration/name",
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_NAME,
+ ),
+ (
+ "/migration/up/sha256",
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_SHA256,
+ ),
+ (
+ "/migration/down/sha256",
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_SHA256,
+ ),
+ (
+ "/migration/schema_sha256",
+ source_manifest::SOURCE_MAINTENANCE_SCHEMA_SHA256,
+ ),
+ (
+ "/source_maintenance/capacity_authority_id",
+ source_manifest::SOURCE_MAINTENANCE_CAPACITY_AUTHORITY_ID,
+ ),
+ (
+ "/source_maintenance/accounting/algorithm",
+ source_manifest::SOURCE_MAINTENANCE_ACCOUNTING_ALGORITHM,
+ ),
+ (
+ "/result_vector/sha256",
+ source_manifest::SOURCE_MAINTENANCE_RESULT_VECTOR_SHA256,
+ ),
+ (
+ "/result_vector/executor_id",
+ source_manifest::SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_ID,
+ ),
+ (
+ "/result_vector/executor_sha256",
+ source_manifest::SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_SHA256,
+ ),
+ ];
+ let numbers_match = expected_numbers.iter().all(|(pointer, expected)| {
+ manifest.pointer(pointer).and_then(|value| value.as_u64()) == Some(*expected)
+ });
+ let strings_match = expected_strings.iter().all(|(pointer, expected)| {
+ manifest.pointer(pointer).and_then(|value| value.as_str()) == Some(*expected)
+ });
+ let string_array_matches = |pointer: &str, expected: &[&str]| {
+ manifest
+ .pointer(pointer)
+ .and_then(|value| value.as_array())
+ .is_some_and(|values| {
+ values.len() == expected.len()
+ && values
+ .iter()
+ .zip(expected)
+ .all(|(value, expected)| value.as_str() == Some(*expected))
+ })
+ };
+ if source_manifest::SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION != 1
+ || source_manifest::SOURCE_MAINTENANCE_CONTRACT_ID
+ != "radroots_event_store.source_maintenance_v1"
+ || source_manifest::SOURCE_MAINTENANCE_HOOK_ID != "source_maintenance_v1"
+ || source_manifest::SOURCE_MAINTENANCE_MIGRATION_VERSION != 4
+ || source_manifest::SOURCE_MAINTENANCE_MIGRATION_NAME != "source_maintenance"
+ || source_manifest::SOURCE_MAINTENANCE_CAPACITY_VERSION != 1
+ || source_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION
+ != food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION
+ || source_manifest::SOURCE_MAINTENANCE_PREDECESSOR_HOOK_ID
+ != food_manifest::FOOD_AVAILABILITY_PROJECTION_HOOK_ID
+ || source_manifest::SOURCE_MAINTENANCE_PREDECESSOR_MANIFEST_SHA256
+ != food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256
+ || source_manifest::SOURCE_MAINTENANCE_RAW_EVENT_COUNT_LIMIT
+ != crate::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1
+ || source_manifest::SOURCE_MAINTENANCE_RAW_TAG_COUNT_LIMIT
+ != crate::RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1
+ || source_manifest::SOURCE_MAINTENANCE_RAW_EVENT_TEXT_BYTES_LIMIT
+ != crate::RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1
+ || source_manifest::SOURCE_MAINTENANCE_RAW_TAG_TEXT_BYTES_LIMIT
+ != crate::RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1
+ || source_manifest::SOURCE_MAINTENANCE_RETAINED_SOURCE_GENERATION_LIMIT
+ != crate::RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1
+ || !numbers_match
+ || !strings_match
+ || !string_array_matches(
+ "/migration/catalog/replaced_objects",
+ source_manifest::SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES,
+ )
+ || !string_array_matches(
+ "/source_maintenance/accounting/raw_event_columns",
+ source_manifest::SOURCE_MAINTENANCE_RAW_EVENT_COLUMNS,
+ )
+ || !string_array_matches(
+ "/source_maintenance/accounting/raw_tag_columns",
+ source_manifest::SOURCE_MAINTENANCE_RAW_TAG_COLUMNS,
+ )
+ || !string_array_matches(
+ "/source_maintenance/accounting/nullable_raw_tag_columns",
+ source_manifest::SOURCE_MAINTENANCE_NULLABLE_RAW_TAG_COLUMNS,
+ )
+ {
+ return Err(RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated source-maintenance manifest metadata is inconsistent".to_owned(),
+ });
+ }
+ for (field, digest) in [
+ (
+ "migration up",
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_SHA256,
+ ),
+ (
+ "migration down",
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_SHA256,
+ ),
+ ("schema", source_manifest::SOURCE_MAINTENANCE_SCHEMA_SHA256),
+ (
+ "predecessor manifest",
+ source_manifest::SOURCE_MAINTENANCE_PREDECESSOR_MANIFEST_SHA256,
+ ),
+ (
+ "result vector",
+ source_manifest::SOURCE_MAINTENANCE_RESULT_VECTOR_SHA256,
+ ),
+ (
+ "result-vector executor",
+ source_manifest::SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_SHA256,
+ ),
+ ] {
+ validate_sha256_literal(
+ source_manifest::SOURCE_MAINTENANCE_MIGRATION_VERSION,
+ field,
+ digest,
+ )?;
+ }
+ Ok(())
+}
+
fn validate_owned_schema_name(
version: u32,
object_kind: &'static str,
@@ -1146,7 +1534,7 @@ mod migration_framework {
#[test]
fn embedded_registry_is_contiguous_and_byte_pinned() {
validate_embedded_migration_registry().expect("valid registry");
- assert_eq!(EVENT_STORE_MIGRATIONS.len(), 3);
+ assert_eq!(EVENT_STORE_MIGRATIONS.len(), 4);
assert_eq!(EVENT_STORE_MIGRATIONS[0].version, 1);
assert_eq!(EVENT_STORE_MIGRATIONS[0].name, "event_store");
assert_eq!(EVENT_STORE_MIGRATIONS[0].up_len, FROZEN_V1_UP_LEN);
@@ -1184,6 +1572,16 @@ mod migration_framework {
EVENT_STORE_MIGRATIONS[2].event_contract_registry_version,
Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION)
);
+ assert_eq!(EVENT_STORE_MIGRATIONS[3].version, 4);
+ assert_eq!(EVENT_STORE_MIGRATIONS[3].name, "source_maintenance");
+ assert_eq!(
+ EVENT_STORE_MIGRATIONS[3].hook,
+ EventStoreMigrationHook::SourceMaintenanceV1
+ );
+ assert_eq!(
+ EVENT_STORE_MIGRATIONS[3].event_contract_registry_version,
+ Some(source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION,)
+ );
}
#[test]
diff --git a/crates/event_store/src/nip09/reconciliation_v1.rs b/crates/event_store/src/nip09/reconciliation_v1.rs
@@ -11,7 +11,12 @@ use crate::model::reconciliation_v1::{
RadrootsEventAdmissionStatus, RadrootsEventIngest, RadrootsEventStoreSourceGeneration,
RadrootsRawHeadDecision, StoredEventClass, tag_semantic_name, tag_value_type_name,
};
-use crate::{RadrootsEventStoreError, RadrootsEventStoreReconciliationResource};
+use crate::{
+ RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1,
+ RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1,
+ RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1,
+ RadrootsEventStoreError, RadrootsEventStoreSourceCapacityResourceV1,
+};
use radroots_event::contract::registry_v7::RadrootsEventContract;
use radroots_event::envelope::{RadrootsEventEnvelope, RadrootsEventKindClass};
use radroots_event::event_head::v1::{
@@ -40,11 +45,7 @@ mod result_vector_executor;
const RECONCILIATION_SNAPSHOT_BATCH_SIZE: i64 = 512;
const RECONCILIATION_SNAPSHOT_BATCH_LEN: usize = 512;
-const RECONCILIATION_RAW_EVENT_LIMIT: u64 = 25_000;
-const RECONCILIATION_RAW_TAG_LIMIT: u64 = 250_000;
-const RECONCILIATION_RAW_EVENT_BYTE_LIMIT: u64 = 64 * 1024 * 1024;
-const RECONCILIATION_RAW_TAG_BYTE_LIMIT: u64 = 32 * 1024 * 1024;
-
+const RECONCILIATION_SNAPSHOT_BATCH_COUNT: u64 = 512;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ReconciliationCapacityLimits {
pub(crate) raw_events: u64,
@@ -56,68 +57,70 @@ pub(crate) struct ReconciliationCapacityLimits {
impl ReconciliationCapacityLimits {
pub(crate) const fn production() -> Self {
Self {
- raw_events: RECONCILIATION_RAW_EVENT_LIMIT,
- raw_tags: RECONCILIATION_RAW_TAG_LIMIT,
- raw_event_bytes: RECONCILIATION_RAW_EVENT_BYTE_LIMIT,
- raw_tag_bytes: RECONCILIATION_RAW_TAG_BYTE_LIMIT,
+ raw_events: RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1,
+ raw_tags: RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1,
+ raw_event_bytes: RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1,
+ raw_tag_bytes: RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1,
}
}
- const fn limit(self, resource: RadrootsEventStoreReconciliationResource) -> u64 {
+ pub(crate) const fn limit(self, resource: RadrootsEventStoreSourceCapacityResourceV1) -> u64 {
match resource {
- RadrootsEventStoreReconciliationResource::RawEvents => self.raw_events,
- RadrootsEventStoreReconciliationResource::RawTags => self.raw_tags,
- RadrootsEventStoreReconciliationResource::RawEventBytes => self.raw_event_bytes,
- RadrootsEventStoreReconciliationResource::RawTagBytes => self.raw_tag_bytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents => self.raw_events,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags => self.raw_tags,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes => self.raw_event_bytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes => self.raw_tag_bytes,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-struct ReconciliationCapacity {
- raw_events: u64,
- raw_tags: u64,
- raw_event_bytes: u64,
- raw_tag_bytes: u64,
+pub(crate) struct ReconciliationCapacity {
+ pub(crate) raw_events: u64,
+ pub(crate) raw_tags: u64,
+ pub(crate) raw_event_bytes: u64,
+ pub(crate) raw_tag_bytes: u64,
}
impl ReconciliationCapacity {
- fn value(self, resource: RadrootsEventStoreReconciliationResource) -> u64 {
+ pub(crate) fn value(self, resource: RadrootsEventStoreSourceCapacityResourceV1) -> u64 {
match resource {
- RadrootsEventStoreReconciliationResource::RawEvents => self.raw_events,
- RadrootsEventStoreReconciliationResource::RawTags => self.raw_tags,
- RadrootsEventStoreReconciliationResource::RawEventBytes => self.raw_event_bytes,
- RadrootsEventStoreReconciliationResource::RawTagBytes => self.raw_tag_bytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents => self.raw_events,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags => self.raw_tags,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes => self.raw_event_bytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes => self.raw_tag_bytes,
}
}
- fn value_mut(&mut self, resource: RadrootsEventStoreReconciliationResource) -> &mut u64 {
+ fn value_mut(&mut self, resource: RadrootsEventStoreSourceCapacityResourceV1) -> &mut u64 {
match resource {
- RadrootsEventStoreReconciliationResource::RawEvents => &mut self.raw_events,
- RadrootsEventStoreReconciliationResource::RawTags => &mut self.raw_tags,
- RadrootsEventStoreReconciliationResource::RawEventBytes => &mut self.raw_event_bytes,
- RadrootsEventStoreReconciliationResource::RawTagBytes => &mut self.raw_tag_bytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents => &mut self.raw_events,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags => &mut self.raw_tags,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes => &mut self.raw_event_bytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes => &mut self.raw_tag_bytes,
}
}
fn checked_add(
&mut self,
limits: ReconciliationCapacityLimits,
- resource: RadrootsEventStoreReconciliationResource,
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
amount: u64,
) -> Result<(), RadrootsEventStoreError> {
let limit = limits.limit(resource);
let actual = self.value(resource).checked_add(amount).ok_or(
- RadrootsEventStoreError::ReconciliationCapacityExceeded {
+ RadrootsEventStoreError::SourceCapacityExceeded {
resource,
- actual: u64::MAX,
+ current: self.value(resource),
+ requested: amount,
limit,
},
)?;
if actual > limit {
- return Err(RadrootsEventStoreError::ReconciliationCapacityExceeded {
+ return Err(RadrootsEventStoreError::SourceCapacityExceeded {
resource,
- actual,
+ current: self.value(resource),
+ requested: amount,
limit,
});
}
@@ -127,17 +130,18 @@ impl ReconciliationCapacity {
fn validate(self, limits: ReconciliationCapacityLimits) -> Result<(), RadrootsEventStoreError> {
for resource in [
- RadrootsEventStoreReconciliationResource::RawEvents,
- RadrootsEventStoreReconciliationResource::RawTags,
- RadrootsEventStoreReconciliationResource::RawEventBytes,
- RadrootsEventStoreReconciliationResource::RawTagBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
] {
let actual = self.value(resource);
let limit = limits.limit(resource);
if actual > limit {
- return Err(RadrootsEventStoreError::ReconciliationCapacityExceeded {
+ return Err(RadrootsEventStoreError::SourceCapacityExceeded {
resource,
- actual,
+ current: actual,
+ requested: 0,
limit,
});
}
@@ -489,44 +493,13 @@ pub(crate) async fn validate_reconciliation_capacity(
connection: &mut SqliteConnection,
limits: ReconciliationCapacityLimits,
) -> Result<(), RadrootsEventStoreError> {
- measure_reconciliation_capacity(connection)
+ measure_reconciliation_capacity_bounded(connection, limits)
.await?
.validate(limits)
}
-async fn measure_reconciliation_capacity(
- connection: &mut SqliteConnection,
-) -> Result<ReconciliationCapacity, RadrootsEventStoreError> {
- // SQLite's maximum database size is well below i64::MAX bytes. Rust still
- // performs checked sign conversion, and the loader independently performs
- // checked per-row accumulation before retaining a bounded snapshot.
- let row = sqlx::query(
- "SELECT (SELECT COUNT(*) FROM event_envelopes) AS raw_events, (SELECT COUNT(*) FROM event_envelope_tags) AS raw_tags, (SELECT COALESCE(SUM(length(CAST(event_id AS BLOB)) + length(CAST(pubkey AS BLOB)) + length(CAST(tags_json AS BLOB)) + length(CAST(content AS BLOB)) + length(CAST(sig AS BLOB)) + length(CAST(raw_json AS BLOB))), 0) FROM event_envelopes) AS raw_event_bytes, (SELECT COALESCE(SUM(length(CAST(event_id AS BLOB)) + length(CAST(tag_name AS BLOB)) + COALESCE(length(CAST(tag_value AS BLOB)), 0) + length(CAST(tag_json AS BLOB))), 0) FROM event_envelope_tags) AS raw_tag_bytes",
- )
- .fetch_one(&mut *connection)
- .await?;
- Ok(ReconciliationCapacity {
- raw_events: reconciliation_capacity_value(
- RadrootsEventStoreReconciliationResource::RawEvents,
- row.try_get("raw_events")?,
- )?,
- raw_tags: reconciliation_capacity_value(
- RadrootsEventStoreReconciliationResource::RawTags,
- row.try_get("raw_tags")?,
- )?,
- raw_event_bytes: reconciliation_capacity_value(
- RadrootsEventStoreReconciliationResource::RawEventBytes,
- row.try_get("raw_event_bytes")?,
- )?,
- raw_tag_bytes: reconciliation_capacity_value(
- RadrootsEventStoreReconciliationResource::RawTagBytes,
- row.try_get("raw_tag_bytes")?,
- )?,
- })
-}
-
fn reconciliation_capacity_value(
- resource: RadrootsEventStoreReconciliationResource,
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
value: i64,
) -> Result<u64, RadrootsEventStoreError> {
u64::try_from(value).map_err(|_| RadrootsEventStoreError::MigrationHookStateDrift {
@@ -540,6 +513,7 @@ pub(crate) async fn apply_reconciliation_hook(
generation_provider: &dyn SourceGenerationProvider,
limits: ReconciliationCapacityLimits,
) -> Result<(), RadrootsEventStoreError> {
+ crate::source_maintenance_v1::preflight_source_generation_append_v1(connection).await?;
validate_rebuild_marker_absent(connection).await?;
validate_projection_cursor_authority(connection).await?;
let snapshot = load_reconciliation_snapshot(connection, limits).await?;
@@ -646,6 +620,17 @@ pub(crate) async fn apply_reconciliation_hook(
)
.await?;
validate_rebuild_hook_state_with_events(connection, plan.generation, &events).await?;
+ if crate::source_maintenance_v1::bind_source_capacity_to_generation_v1(
+ connection,
+ plan.generation,
+ )
+ .await?
+ {
+ crate::store::food_availability_projection_v1::apply_food_availability_projection_hook_v1(
+ connection,
+ )
+ .await?;
+ }
close_source_rebuild_marker(connection, plan.generation).await?;
validate_sqlite_integrity_after_rebuild(connection).await?;
validate_active_hook_state_fast(connection).await
@@ -1369,7 +1354,7 @@ async fn load_reconciliation_snapshot(
connection: &mut SqliteConnection,
limits: ReconciliationCapacityLimits,
) -> Result<ReconciliationSnapshot, RadrootsEventStoreError> {
- let measured_capacity = measure_snapshot_capacity_bounded(connection, limits).await?;
+ let measured_capacity = measure_reconciliation_capacity_bounded(connection, limits).await?;
let mut loaded_capacity = ReconciliationCapacity::default();
let mut tags_by_event = BTreeMap::<String, Vec<StoredRawTag>>::new();
let mut next_tag_rowid = i64::MIN;
@@ -1391,14 +1376,14 @@ async fn load_reconciliation_snapshot(
let tag_json: String = row.try_get("tag_json")?;
loaded_capacity.checked_add(
limits,
- RadrootsEventStoreReconciliationResource::RawTags,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
1,
)?;
loaded_capacity.checked_add(
limits,
- RadrootsEventStoreReconciliationResource::RawTagBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
text_payload_bytes(
- RadrootsEventStoreReconciliationResource::RawTagBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
limits,
[
event_id.len(),
@@ -1453,14 +1438,14 @@ async fn load_reconciliation_snapshot(
let raw_json: String = row.try_get("raw_json")?;
loaded_capacity.checked_add(
limits,
- RadrootsEventStoreReconciliationResource::RawEvents,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
1,
)?;
loaded_capacity.checked_add(
limits,
- RadrootsEventStoreReconciliationResource::RawEventBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
text_payload_bytes(
- RadrootsEventStoreReconciliationResource::RawEventBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
limits,
[
event_id.len(),
@@ -1570,18 +1555,20 @@ fn compare_raw_tags(
Ok(())
}
-async fn measure_snapshot_capacity_bounded(
+pub(crate) async fn measure_reconciliation_capacity_bounded(
connection: &mut SqliteConnection,
limits: ReconciliationCapacityLimits,
) -> Result<ReconciliationCapacity, RadrootsEventStoreError> {
let mut capacity = ReconciliationCapacity::default();
let mut next_event_seq = i64::MIN;
loop {
+ let (page_size, page_len) =
+ bounded_capacity_page_len(capacity.raw_events, limits.raw_events);
let rows = sqlx::query(
"SELECT seq, length(CAST(event_id AS BLOB)) + length(CAST(pubkey AS BLOB)) + length(CAST(tags_json AS BLOB)) + length(CAST(content AS BLOB)) + length(CAST(sig AS BLOB)) + length(CAST(raw_json AS BLOB)) AS raw_bytes FROM event_envelopes WHERE seq >= ? ORDER BY seq LIMIT ?",
)
.bind(next_event_seq)
- .bind(RECONCILIATION_SNAPSHOT_BATCH_SIZE)
+ .bind(page_size)
.fetch_all(&mut *connection)
.await?;
let row_count = rows.len();
@@ -1589,14 +1576,14 @@ async fn measure_snapshot_capacity_bounded(
let seq: i64 = row.try_get("seq")?;
capacity.checked_add(
limits,
- RadrootsEventStoreReconciliationResource::RawEvents,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
1,
)?;
capacity.checked_add(
limits,
- RadrootsEventStoreReconciliationResource::RawEventBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
reconciliation_capacity_value(
- RadrootsEventStoreReconciliationResource::RawEventBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
row.try_get("raw_bytes")?,
)?,
)?;
@@ -1609,28 +1596,33 @@ async fn measure_snapshot_capacity_bounded(
}
})?;
}
- if row_count < RECONCILIATION_SNAPSHOT_BATCH_LEN {
+ if row_count < page_len {
break;
}
}
let mut next_tag_rowid = i64::MIN;
loop {
+ let (page_size, page_len) = bounded_capacity_page_len(capacity.raw_tags, limits.raw_tags);
let rows = sqlx::query(
"SELECT rowid AS tag_rowid, length(CAST(event_id AS BLOB)) + length(CAST(tag_name AS BLOB)) + COALESCE(length(CAST(tag_value AS BLOB)), 0) + length(CAST(tag_json AS BLOB)) AS raw_bytes FROM event_envelope_tags WHERE rowid >= ? ORDER BY rowid LIMIT ?",
)
.bind(next_tag_rowid)
- .bind(RECONCILIATION_SNAPSHOT_BATCH_SIZE)
+ .bind(page_size)
.fetch_all(&mut *connection)
.await?;
let row_count = rows.len();
for row in rows {
- capacity.checked_add(limits, RadrootsEventStoreReconciliationResource::RawTags, 1)?;
capacity.checked_add(
limits,
- RadrootsEventStoreReconciliationResource::RawTagBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
+ 1,
+ )?;
+ capacity.checked_add(
+ limits,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
reconciliation_capacity_value(
- RadrootsEventStoreReconciliationResource::RawTagBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
row.try_get("raw_bytes")?,
)?,
)?;
@@ -1642,13 +1634,24 @@ async fn measure_snapshot_capacity_bounded(
}
})?;
}
- if row_count < RECONCILIATION_SNAPSHOT_BATCH_LEN {
+ if row_count < page_len {
break;
}
}
Ok(capacity)
}
+fn bounded_capacity_page_len(current: u64, limit: u64) -> (i64, usize) {
+ let page_count = limit
+ .saturating_sub(current)
+ .saturating_add(1)
+ .min(RECONCILIATION_SNAPSHOT_BATCH_COUNT);
+ (
+ i64::try_from(page_count).unwrap_or(RECONCILIATION_SNAPSHOT_BATCH_SIZE),
+ usize::try_from(page_count).unwrap_or(RECONCILIATION_SNAPSHOT_BATCH_LEN),
+ )
+}
+
async fn update_derived_tags(
connection: &mut SqliteConnection,
event: &ReconciledEvent,
@@ -3542,24 +3545,25 @@ fn i64_from_usize(field: &'static str, value: usize) -> Result<i64, RadrootsEven
}
fn text_payload_bytes<const N: usize>(
- resource: RadrootsEventStoreReconciliationResource,
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
limits: ReconciliationCapacityLimits,
lengths: [usize; N],
) -> Result<u64, RadrootsEventStoreError> {
let limit = limits.limit(resource);
lengths.into_iter().try_fold(0_u64, |total, length| {
- let length = u64::try_from(length).map_err(|_| {
- RadrootsEventStoreError::ReconciliationCapacityExceeded {
+ let length =
+ u64::try_from(length).map_err(|_| RadrootsEventStoreError::SourceCapacityExceeded {
resource,
- actual: u64::MAX,
+ current: u64::MAX,
+ requested: 0,
limit,
- }
- })?;
+ })?;
total
.checked_add(length)
- .ok_or(RadrootsEventStoreError::ReconciliationCapacityExceeded {
+ .ok_or(RadrootsEventStoreError::SourceCapacityExceeded {
resource,
- actual: u64::MAX,
+ current: u64::MAX,
+ requested: 0,
limit,
})
})
@@ -3610,6 +3614,30 @@ mod tests {
}
}
+ #[test]
+ fn bounded_capacity_page_len_caps_gross_source_probe_at_one_over() {
+ for limit in [25_000_u64, 250_000_u64] {
+ let mut current = 0_u64;
+ loop {
+ let (sqlite_limit, fetched_len) = bounded_capacity_page_len(current, limit);
+ assert_eq!(
+ usize::try_from(sqlite_limit).expect("positive bounded page limit"),
+ fetched_len
+ );
+ assert!((1..=RECONCILIATION_SNAPSHOT_BATCH_LEN).contains(&fetched_len));
+ let fetched = current + u64::try_from(fetched_len).expect("bounded page length");
+ if fetched > limit {
+ assert_eq!(fetched, limit + 1);
+ break;
+ }
+ current = fetched;
+ }
+ }
+ assert_eq!(bounded_capacity_page_len(24_576, 25_000), (425, 425));
+ assert_eq!(bounded_capacity_page_len(249_856, 250_000), (145, 145));
+ assert_eq!(bounded_capacity_page_len(25_000, 25_000), (1, 1));
+ }
+
#[tokio::test]
async fn source_rebuild_rotates_three_generations_with_deterministic_parity() {
let pool = open_v1_test_pool().await;
diff --git a/crates/event_store/src/schema.rs b/crates/event_store/src/schema.rs
@@ -15,6 +15,10 @@ use crate::nip09::reconciliation_v1::{
OsSourceGenerationProvider, ReconciliationCapacityLimits, SourceGenerationProvider,
apply_reconciliation_hook, validate_active_hook_state_fast, validate_reconciliation_capacity,
};
+use crate::source_maintenance_v1::{
+ apply_source_maintenance_hook_v1, validate_no_persisted_ephemeral_raw_rows_v1,
+ validate_source_capacity_authority_full_v1,
+};
use crate::store::food_availability_projection_v1::{
apply_food_availability_projection_hook_v1,
validate_food_availability_projection_hook_state_fast_v1,
@@ -49,6 +53,13 @@ struct AppliedMigration {
schema_sha256: String,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum SourceGenerationHistoryRollbackPolicy {
+ Preserve,
+ #[cfg(test)]
+ AllowDestructiveForMigrationTest,
+}
+
pub async fn inspect_event_store_schema_status(
pool: &SqlitePool,
) -> Result<RadrootsEventStoreSchemaStatus, RadrootsEventStoreError> {
@@ -160,6 +171,9 @@ async fn migrate_event_store_schema_with_registry_and_generation_provider(
let mut connection = pool.acquire().await?;
validate_event_store_temp_schema_with_registry(&mut connection, registry).await?;
validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;
+ if has_pending_source_maintenance_hook(&status, registry) {
+ validate_no_persisted_ephemeral_raw_rows_v1(&mut connection).await?;
+ }
}
let mut transaction = pool.begin_with("BEGIN IMMEDIATE").await?;
@@ -189,10 +203,26 @@ fn has_pending_source_capacity_hook(
migration.hook,
EventStoreMigrationHook::Nip09ReconciliationV1
| EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ | EventStoreMigrationHook::SourceMaintenanceV1
)
})
}
+fn has_pending_source_maintenance_hook(
+ status: &RadrootsEventStoreSchemaStatus,
+ registry: &[EventStoreMigration],
+) -> bool {
+ let current_version = match status {
+ RadrootsEventStoreSchemaStatus::Uninitialized => return false,
+ RadrootsEventStoreSchemaStatus::UnledgeredBaseline => registry[0].version,
+ RadrootsEventStoreSchemaStatus::Managed { version } => *version,
+ };
+ registry.iter().any(|migration| {
+ migration.version > current_version
+ && migration.hook == EventStoreMigrationHook::SourceMaintenanceV1
+ })
+}
+
pub(crate) async fn rollback_event_store_schema_offline(
pool: &SqlitePool,
target: u32,
@@ -207,6 +237,22 @@ pub(crate) async fn rollback_event_store_schema_offline(
.await
}
+#[cfg(test)]
+pub(crate) async fn rollback_event_store_schema_offline_destructive_for_migration_test(
+ pool: &SqlitePool,
+ target: u32,
+) -> Result<(), RadrootsEventStoreError> {
+ rollback_event_store_schema_with_registry_inner(
+ pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ target,
+ SourceGenerationHistoryRollbackPolicy::AllowDestructiveForMigrationTest,
+ )
+ .await
+}
+
async fn rollback_event_store_schema_with_registry(
pool: &SqlitePool,
registry: &[EventStoreMigration],
@@ -214,6 +260,25 @@ async fn rollback_event_store_schema_with_registry(
supported_current: u32,
target: u32,
) -> Result<(), RadrootsEventStoreError> {
+ rollback_event_store_schema_with_registry_inner(
+ pool,
+ registry,
+ minimum,
+ supported_current,
+ target,
+ SourceGenerationHistoryRollbackPolicy::Preserve,
+ )
+ .await
+}
+
+async fn rollback_event_store_schema_with_registry_inner(
+ pool: &SqlitePool,
+ registry: &[EventStoreMigration],
+ minimum: u32,
+ supported_current: u32,
+ target: u32,
+ source_generation_history_policy: SourceGenerationHistoryRollbackPolicy,
+) -> Result<(), RadrootsEventStoreError> {
if target < minimum {
return Err(RadrootsEventStoreError::RollbackBelowVersionFloor {
floor: minimum,
@@ -222,8 +287,14 @@ async fn rollback_event_store_schema_with_registry(
}
validate_migration_registry(registry, minimum, supported_current)?;
let mut transaction = pool.begin_with("BEGIN EXCLUSIVE").await?;
- let result =
- rollback_schema_on_connection(&mut transaction, registry, supported_current, target).await;
+ let result = rollback_schema_on_connection(
+ &mut transaction,
+ registry,
+ supported_current,
+ target,
+ source_generation_history_policy,
+ )
+ .await;
finish_schema_transaction(transaction, result).await
}
@@ -302,8 +373,12 @@ async fn migrate_schema_on_connection(
migration.hook,
EventStoreMigrationHook::Nip09ReconciliationV1
| EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ | EventStoreMigrationHook::SourceMaintenanceV1
) {
validate_reconciliation_capacity(connection, reconciliation_limits).await?;
+ if migration.hook == EventStoreMigrationHook::SourceMaintenanceV1 {
+ validate_no_persisted_ephemeral_raw_rows_v1(connection).await?;
+ }
}
apply_migration_up(connection, registry, migration).await?;
apply_migration_hook(
@@ -333,6 +408,7 @@ async fn rollback_schema_on_connection(
registry: &[EventStoreMigration],
supported_current: u32,
target: u32,
+ source_generation_history_policy: SourceGenerationHistoryRollbackPolicy,
) -> Result<(), RadrootsEventStoreError> {
let RadrootsEventStoreSchemaStatus::Managed {
version: current_version,
@@ -346,6 +422,9 @@ async fn rollback_schema_on_connection(
target,
});
}
+ if source_generation_history_policy == SourceGenerationHistoryRollbackPolicy::Preserve {
+ validate_rollback_preserves_source_generation_history(registry, current_version, target)?;
+ }
for version in ((target + 1)..=current_version).rev() {
let migration = migration_for_version(registry, version)
@@ -384,6 +463,31 @@ async fn rollback_schema_on_connection(
}
}
+fn validate_rollback_preserves_source_generation_history(
+ registry: &[EventStoreMigration],
+ current: u32,
+ target: u32,
+) -> Result<(), RadrootsEventStoreError> {
+ let Some(floor) = registry
+ .iter()
+ .find(|migration| migration.hook == EventStoreMigrationHook::Nip09ReconciliationV1)
+ .map(|migration| migration.version)
+ else {
+ return Ok(());
+ };
+ if current < floor || target >= floor {
+ return Ok(());
+ }
+
+ Err(
+ RadrootsEventStoreError::RollbackWouldDiscardSourceGenerationHistory {
+ current,
+ target,
+ floor,
+ },
+ )
+}
+
#[cfg(test)]
async fn destroy_schema_on_connection(
connection: &mut SqliteConnection,
@@ -493,10 +597,15 @@ fn validate_catalog_delta(
.iter()
.copied()
.collect::<BTreeSet<_>>();
+ let expected_changed = migration
+ .replaced_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(),
+ "up" => added == expected && removed.is_empty() && changed == expected_changed,
+ "down" => removed == expected && added.is_empty() && changed == expected_changed,
_ => false,
};
if !valid {
@@ -504,7 +613,7 @@ fn validate_catalog_delta(
version: migration.version,
direction,
reason: format!(
- "expected {} objects {expected:?}; added {added:?}, removed {removed:?}, changed {changed:?}",
+ "expected {} objects {expected:?} and changed replacement objects {expected_changed:?}; added {added:?}, removed {removed:?}, changed {changed:?}",
if direction == "up" {
"added"
} else {
@@ -629,6 +738,9 @@ async fn apply_migration_hook(
EventStoreMigrationHook::FoodAvailabilityProjectionV1 => {
apply_food_availability_projection_hook_v1(connection).await
}
+ EventStoreMigrationHook::SourceMaintenanceV1 => {
+ apply_source_maintenance_hook_v1(connection).await
+ }
}
}
@@ -644,6 +756,9 @@ async fn validate_migration_hook_state(
EventStoreMigrationHook::FoodAvailabilityProjectionV1 => {
validate_food_availability_projection_hook_state_fast_v1(connection).await
}
+ EventStoreMigrationHook::SourceMaintenanceV1 => {
+ validate_source_capacity_authority_full_v1(connection).await
+ }
}
}
@@ -958,7 +1073,7 @@ mod migration_framework {
use crate::migrations::sha256_hex;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::str::FromStr;
- use std::time::{Duration, Instant};
+ use std::time::Duration;
const SYNTHETIC_V2_UP: &str = "CREATE TABLE radroots_event_store_v2_parent (
id INTEGER PRIMARY KEY NOT NULL
@@ -1010,6 +1125,7 @@ DROP TABLE radroots_event_store_v2_parent;";
down_sha256: leaked_sha256(SYNTHETIC_V2_DOWN),
schema_sha256,
owned_object_names: SYNTHETIC_V2_OBJECT_NAMES,
+ replaced_object_names: &[],
owned_table_names: SYNTHETIC_V2_TABLE_NAMES,
fts5_table_names: NO_FTS5_TABLES,
hook: crate::migrations::EventStoreMigrationHook::None,
@@ -1050,6 +1166,30 @@ DROP TABLE radroots_event_store_v2_parent;";
.expect("baseline schema");
}
+ async fn schema_object_sql(pool: &SqlitePool, name: &str) -> String {
+ sqlx::query_scalar("SELECT sql FROM main.sqlite_schema WHERE name = ?")
+ .bind(name)
+ .fetch_one(pool)
+ .await
+ .expect("schema object SQL")
+ }
+
+ async fn insert_test_rebuild_marker(
+ connection: &mut SqliteConnection,
+ target_generation: &[u8; 32],
+ transition_floor_seq: i64,
+ prior_last_transition_seq: i64,
+ ) -> Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> {
+ sqlx::query(
+ "INSERT INTO radroots_event_store_source_rebuild_marker(singleton, barrier_key, target_generation, target_generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq, prior_active_generation, prior_raw_event_count, prior_raw_tag_count, prior_raw_high_water_seq, prior_last_transition_seq) SELECT 1, 1, ?, generation.generation_ordinal + 1, generation.reconciliation_version, generation.addressable_feed_version, generation.event_contract_registry_version, generation.hook_id, generation.hook_manifest_sha256, ?, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, state.active_generation, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, ? FROM radroots_event_store_source_state AS state JOIN radroots_event_store_source_generation AS generation ON generation.source_generation = state.active_generation WHERE state.singleton = 1",
+ )
+ .bind(target_generation.as_slice())
+ .bind(transition_floor_seq)
+ .bind(prior_last_transition_seq)
+ .execute(connection)
+ .await
+ }
+
#[tokio::test]
async fn later_hookless_migrations_do_not_disable_prior_hook_validation() {
let store = RadrootsEventStore::open_memory().await.expect("v2 store");
@@ -1121,9 +1261,10 @@ DROP TABLE radroots_event_store_v2_parent;";
assert!(
matches!(
error,
- RadrootsEventStoreError::ReconciliationCapacityExceeded {
- resource: crate::RadrootsEventStoreReconciliationResource::RawEvents,
- actual: 1,
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: crate::RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ current: 0,
+ requested: 1,
limit: 0,
}
),
@@ -1186,9 +1327,10 @@ DROP TABLE radroots_event_store_v2_parent;";
assert!(
matches!(
error,
- RadrootsEventStoreError::ReconciliationCapacityExceeded {
- resource: crate::RadrootsEventStoreReconciliationResource::RawEvents,
- actual: 1,
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: crate::RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ current: 0,
+ requested: 1,
limit: 0,
}
),
@@ -1214,6 +1356,239 @@ DROP TABLE radroots_event_store_v2_parent;";
.await
.expect("v3 ledger count");
assert_eq!(v3_ledger_count, 0);
+
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(
+ &pool,
+ &EVENT_STORE_MIGRATIONS[..3],
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ 3,
+ )
+ .await
+ .expect("install v3 schema");
+ sqlx::query(
+ "INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, 1, 1, '[]', '', ?, '{}', 'verified', 'unsupported', NULL, 'regular', 0, 1, 1)",
+ )
+ .bind("1".repeat(64))
+ .bind("2".repeat(64))
+ .bind("3".repeat(128))
+ .execute(&pool)
+ .await
+ .expect("post-v3 raw event");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET raw_event_count = 1, raw_high_water_seq = (SELECT MAX(seq) FROM event_envelopes) WHERE singleton = 1",
+ )
+ .execute(&pool)
+ .await
+ .expect("advance post-v3 source authority");
+
+ let mut transaction = pool
+ .begin_with("BEGIN IMMEDIATE")
+ .await
+ .expect("v4 migration transaction");
+ let result = migrate_schema_on_connection(
+ &mut transaction,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ &OsSourceGenerationProvider,
+ limits,
+ )
+ .await;
+ let error = finish_schema_transaction(transaction, result)
+ .await
+ .expect_err("v4 capacity excess must fail");
+ assert!(
+ matches!(
+ error,
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: crate::RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ current: 0,
+ requested: 1,
+ limit: 0,
+ }
+ ),
+ "unexpected v4 capacity failure: {error:?}"
+ );
+ assert_eq!(
+ inspect_event_store_schema_status(&pool)
+ .await
+ .expect("v3 status after rejected v4 migration"),
+ RadrootsEventStoreSchemaStatus::Managed { version: 3 }
+ );
+ let v4_object_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM sqlite_schema WHERE name = 'radroots_event_store_source_capacity_v1'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v4 object count");
+ assert_eq!(v4_object_count, 0);
+ let v4_ledger_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_schema_migrations WHERE version = 4",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v4 ledger count");
+ assert_eq!(v4_ledger_count, 0);
+ }
+
+ #[tokio::test]
+ async fn v3_to_v4_under_limit_backfills_exact_capacity_and_preserves_source() {
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(
+ &pool,
+ &EVENT_STORE_MIGRATIONS[..3],
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ 3,
+ )
+ .await
+ .expect("install v3 schema");
+ sqlx::query("CREATE TABLE caller_state(id INTEGER PRIMARY KEY, value TEXT NOT NULL)")
+ .execute(&pool)
+ .await
+ .expect("create unrelated caller table");
+ sqlx::query("INSERT INTO caller_state(id, value) VALUES (1, 'preserve')")
+ .execute(&pool)
+ .await
+ .expect("seed unrelated caller row");
+ let event_id = "7".repeat(64);
+ sqlx::query(
+ "INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, 1, 1, '[]', 'under-limit', ?, '{}', 'verified', 'unsupported', NULL, 'regular', 0, 1, 1)",
+ )
+ .bind(event_id.as_str())
+ .bind("8".repeat(64))
+ .bind("9".repeat(128))
+ .execute(&pool)
+ .await
+ .expect("post-v3 raw event");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET raw_event_count = 1, raw_high_water_seq = (SELECT MAX(seq) FROM event_envelopes) WHERE singleton = 1",
+ )
+ .execute(&pool)
+ .await
+ .expect("advance post-v3 source authority");
+ let generation_before: Vec<u8> = sqlx::query_scalar(
+ "SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v3 source generation");
+ let event_bytes: i64 = sqlx::query_scalar(
+ "SELECT length(CAST(event_id AS BLOB)) + length(CAST(pubkey AS BLOB)) + length(CAST(tags_json AS BLOB)) + length(CAST(content AS BLOB)) + length(CAST(sig AS BLOB)) + length(CAST(raw_json AS BLOB)) FROM event_envelopes WHERE event_id = ?",
+ )
+ .bind(event_id.as_str())
+ .fetch_one(&pool)
+ .await
+ .expect("raw event byte authority");
+
+ migrate_event_store_schema_with_registry(
+ &pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ )
+ .await
+ .expect("migrate under-limit v3 source to v4");
+
+ assert_eq!(
+ inspect_event_store_schema_status(&pool)
+ .await
+ .expect("v4 status"),
+ RadrootsEventStoreSchemaStatus::Managed { version: 4 }
+ );
+ let capacity: (Vec<u8>, i64, i64, i64, i64, i64, i64) = sqlx::query_as(
+ "SELECT source_generation, raw_event_count, raw_tag_count, raw_event_bytes, raw_tag_bytes, retained_generation_count, retained_generation_limit FROM radroots_event_store_source_capacity_v1 WHERE singleton = 1",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v4 capacity authority");
+ assert_eq!(capacity, (generation_before, 1, 0, event_bytes, 0, 1, 8));
+ let preserved: (i64, String) = sqlx::query_as(
+ "SELECT (SELECT COUNT(*) FROM event_envelopes WHERE event_id = ?), (SELECT value FROM caller_state WHERE id = 1)",
+ )
+ .bind(event_id)
+ .fetch_one(&pool)
+ .await
+ .expect("preserved source and caller state");
+ assert_eq!(preserved, (1, "preserve".to_owned()));
+ }
+
+ #[tokio::test]
+ async fn v4_rejects_persisted_legacy_ephemeral_rows_atomically() {
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(
+ &pool,
+ &EVENT_STORE_MIGRATIONS[..3],
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ 3,
+ )
+ .await
+ .expect("install v3 schema");
+ let event_id = "4".repeat(64);
+ sqlx::query(
+ "INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, 1, 20000, '[]', '', ?, '{}', 'verified', 'unsupported', NULL, 'ephemeral', 0, 1, 1)",
+ )
+ .bind(event_id.as_str())
+ .bind("5".repeat(64))
+ .bind("6".repeat(128))
+ .execute(&pool)
+ .await
+ .expect("legacy persisted ephemeral row");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET raw_event_count = 1, raw_high_water_seq = (SELECT MAX(seq) FROM event_envelopes) WHERE singleton = 1",
+ )
+ .execute(&pool)
+ .await
+ .expect("advance legacy source authority");
+
+ let mut transaction = pool
+ .begin_with("BEGIN IMMEDIATE")
+ .await
+ .expect("v4 migration transaction");
+ let result = migrate_schema_on_connection(
+ &mut transaction,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ &OsSourceGenerationProvider,
+ ReconciliationCapacityLimits::production(),
+ )
+ .await;
+ let error = finish_schema_transaction(transaction, result)
+ .await
+ .expect_err("persisted ephemeral source must reject v4");
+ assert!(matches!(
+ error,
+ RadrootsEventStoreError::PersistedEphemeralRawEvent {
+ ref event_id,
+ kind: 20_000,
+ } if event_id == &"4".repeat(64)
+ ));
+ assert_eq!(
+ inspect_event_store_schema_status(&pool)
+ .await
+ .expect("v3 remains valid after rejected v4 migration"),
+ RadrootsEventStoreSchemaStatus::Managed { version: 3 }
+ );
+ let v4_object_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM sqlite_schema WHERE name = 'radroots_event_store_source_capacity_v1'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v4 object count");
+ assert_eq!(v4_object_count, 0);
+ let v4_ledger_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_schema_migrations WHERE version = 4",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v4 ledger count");
+ assert_eq!(v4_ledger_count, 0);
+ let raw_count: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM event_envelopes WHERE event_id = ?")
+ .bind(event_id)
+ .fetch_one(&pool)
+ .await
+ .expect("legacy raw row remains after rollback");
+ assert_eq!(raw_count, 1);
}
#[tokio::test]
@@ -1487,6 +1862,7 @@ DROP TABLE event_envelopes;";
down_sha256: ZERO_SHA256,
schema_sha256: ZERO_SHA256,
owned_object_names: DELTA_OBJECT_NAMES,
+ replaced_object_names: &[],
owned_table_names: DELTA_TABLE_NAMES,
fts5_table_names: NO_FTS5_TABLES,
hook: EventStoreMigrationHook::None,
@@ -2054,7 +2430,7 @@ DROP TABLE event_envelopes;";
));
let unknown = AppliedMigration {
- version: 4,
+ version: 5,
name: "future".to_owned(),
up_sha256: "0".repeat(64),
down_sha256: "1".repeat(64),
@@ -2066,12 +2442,13 @@ DROP TABLE event_envelopes;";
row(&EVENT_STORE_MIGRATIONS[0]),
row(&EVENT_STORE_MIGRATIONS[1]),
row(&EVENT_STORE_MIGRATIONS[2]),
+ row(&EVENT_STORE_MIGRATIONS[3]),
unknown
],
EVENT_STORE_MIGRATIONS,
- 4
+ 5
),
- Err(RadrootsEventStoreError::UnknownMigration { version: 4 })
+ Err(RadrootsEventStoreError::UnknownMigration { version: 5 })
));
}
@@ -2098,7 +2475,7 @@ DROP TABLE event_envelopes;";
}
#[tokio::test]
- async fn rollback_rejects_below_floor_ahead_and_unmanaged_targets() {
+ async fn rollback_rejects_below_floor_ahead_unmanaged_and_generation_destructive_targets() {
let unmanaged = memory_pool().await;
assert!(matches!(
rollback_event_store_schema_offline(&unmanaged, 1).await,
@@ -2124,26 +2501,106 @@ DROP TABLE event_envelopes;";
target
}) if target == ahead
));
+ let history_before: Vec<(Vec<u8>, i64)> = sqlx::query_as(
+ "SELECT source_generation, generation_ordinal FROM radroots_event_store_source_generation ORDER BY generation_ordinal",
+ )
+ .fetch_all(&managed)
+ .await
+ .expect("source-generation history before rejected rollback");
+ assert!(matches!(
+ rollback_event_store_schema_offline(&managed, 1).await,
+ Err(
+ RadrootsEventStoreError::RollbackWouldDiscardSourceGenerationHistory {
+ current: RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ target: 1,
+ floor: 2,
+ }
+ )
+ ));
+ assert_eq!(
+ inspect_event_store_schema_status(&managed)
+ .await
+ .expect("current status after rejected rollback"),
+ RadrootsEventStoreSchemaStatus::Managed {
+ version: RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ }
+ );
+ assert_eq!(
+ sqlx::query_as::<_, (Vec<u8>, i64)>(
+ "SELECT source_generation, generation_ordinal FROM radroots_event_store_source_generation ORDER BY generation_ordinal",
+ )
+ .fetch_all(&managed)
+ .await
+ .expect("source-generation history after rejected rollback"),
+ history_before
+ );
+
+ rollback_event_store_schema_offline_destructive_for_migration_test(&managed, 1)
+ .await
+ .expect("test-only destructive rollback to v1");
rollback_event_store_schema_offline(&managed, 1)
.await
- .expect("idempotent rollback");
+ .expect("v1 to v1 idempotent rollback");
}
#[tokio::test]
- async fn synthetic_v2_rolls_back_to_v1() {
- let registry = synthetic_v2_registry().await;
- let pool = memory_pool().await;
- migrate_event_store_schema_with_registry(&pool, ®istry, 1, 2)
+ async fn rollback_cannot_bypass_generation_history_guard_through_version_three() {
+ let managed = memory_pool().await;
+ migrate_event_store_schema(&managed)
.await
- .expect("migrate to v2");
-
- rollback_event_store_schema_with_registry(&pool, ®istry, 1, 2, 1)
+ .expect("migration");
+ rollback_event_store_schema_offline(&managed, 3)
.await
- .expect("rollback to v1");
- assert_eq!(
- inspect_event_store_schema_status_with_registry(&pool, ®istry, 2)
- .await
- .expect("v1 status"),
+ .expect("rollback to history-preserving v3");
+ let history_before: Vec<(Vec<u8>, i64)> = sqlx::query_as(
+ "SELECT source_generation, generation_ordinal FROM radroots_event_store_source_generation ORDER BY generation_ordinal",
+ )
+ .fetch_all(&managed)
+ .await
+ .expect("v3 source-generation history");
+
+ assert!(matches!(
+ rollback_event_store_schema_offline(&managed, 1).await,
+ Err(
+ RadrootsEventStoreError::RollbackWouldDiscardSourceGenerationHistory {
+ current: 3,
+ target: 1,
+ floor: 2,
+ }
+ )
+ ));
+ assert_eq!(
+ inspect_event_store_schema_status(&managed)
+ .await
+ .expect("v3 status after rejected bypass"),
+ RadrootsEventStoreSchemaStatus::Managed { version: 3 }
+ );
+ assert_eq!(
+ sqlx::query_as::<_, (Vec<u8>, i64)>(
+ "SELECT source_generation, generation_ordinal FROM radroots_event_store_source_generation ORDER BY generation_ordinal",
+ )
+ .fetch_all(&managed)
+ .await
+ .expect("v3 source-generation history after rejected bypass"),
+ history_before
+ );
+ }
+
+ #[tokio::test]
+ async fn synthetic_v2_rolls_back_to_v1() {
+ let registry = synthetic_v2_registry().await;
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(&pool, ®istry, 1, 2)
+ .await
+ .expect("migrate to v2");
+
+ rollback_event_store_schema_with_registry(&pool, ®istry, 1, 2, 1)
+ .await
+ .expect("rollback to v1");
+ assert_eq!(
+ inspect_event_store_schema_status_with_registry(&pool, ®istry, 2)
+ .await
+ .expect("v1 status"),
RadrootsEventStoreSchemaStatus::Managed { version: 1 }
);
let v2_objects: i64 = sqlx::query_scalar(
@@ -2237,6 +2694,7 @@ INSERT INTO radroots_event_store_missing_v2(id) VALUES (1);";
down_sha256: leaked_sha256(DOWN),
schema_sha256: ZERO_SHA256,
owned_object_names: OBJECTS,
+ replaced_object_names: &[],
owned_table_names: OBJECTS,
fts5_table_names: NO_FTS5_TABLES,
hook: crate::migrations::EventStoreMigrationHook::None,
@@ -2394,6 +2852,659 @@ INSERT INTO caller_child(id, parent_id) VALUES (1, 999);",
));
}
+ #[test]
+ fn registry_rejects_invalid_predecessor_replacement_declarations() {
+ const BASELINE_REPLACEMENT: &[&str] = &["event_envelope_kind_created_idx"];
+ const DUPLICATE_REPLACEMENTS: &[&str] = &[
+ "radroots_event_store_source_rebuild_marker_insert_guard",
+ "radroots_event_store_source_rebuild_marker_insert_guard",
+ ];
+ const MISSING_REPLACEMENT: &[&str] = &["radroots_event_store_missing_guard"];
+ const CURRENT_OWNED_REPLACEMENT: &[&str] =
+ &["radroots_event_store_source_capacity_insert_guard"];
+ const TABLE_REPLACEMENT: &[&str] = &["radroots_event_store_source_state"];
+
+ let mut baseline = EVENT_STORE_MIGRATIONS[0];
+ baseline.replaced_object_names = BASELINE_REPLACEMENT;
+ assert!(matches!(
+ validate_migration_registry(&[baseline], 1, 1),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason.contains("baseline migration cannot replace")
+ ));
+
+ let mut hookless = synthetic_v2_descriptor(ZERO_SHA256);
+ hookless.replaced_object_names = BASELINE_REPLACEMENT;
+ assert!(matches!(
+ validate_migration_registry(&[EVENT_STORE_MIGRATIONS[0], hookless], 1, 2),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason.contains("without an authenticated successor hook")
+ ));
+
+ for (replacements, expected_reason) in [
+ (DUPLICATE_REPLACEMENTS, "declared more than once"),
+ (MISSING_REPLACEMENT, "exactly one prior migration"),
+ (CURRENT_OWNED_REPLACEMENT, "also newly owned"),
+ (TABLE_REPLACEMENT, "only non-table schema objects"),
+ ] {
+ let mut v4 = EVENT_STORE_MIGRATIONS[3];
+ v4.replaced_object_names = replacements;
+ let registry = [
+ EVENT_STORE_MIGRATIONS[0],
+ EVENT_STORE_MIGRATIONS[1],
+ EVENT_STORE_MIGRATIONS[2],
+ v4,
+ ];
+ assert!(matches!(
+ validate_migration_registry(®istry, 1, 4),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason.contains(expected_reason)
+ ));
+ }
+ }
+
+ #[test]
+ fn registry_binds_authenticated_hooks_to_one_canonical_migration() {
+ let mut duplicate = EVENT_STORE_MIGRATIONS[1];
+ duplicate.version = 3;
+ duplicate.name = "duplicate_nip09";
+ assert!(matches!(
+ validate_migration_registry(
+ &[EVENT_STORE_MIGRATIONS[0], EVENT_STORE_MIGRATIONS[1], duplicate],
+ 1,
+ 3,
+ ),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason.contains("migration hook `nip09_reconciliation_v1` is declared more than once")
+ ));
+
+ let mut misbound = EVENT_STORE_MIGRATIONS[3];
+ misbound.version = 2;
+ misbound.name = "future_replacement";
+ assert!(matches!(
+ validate_migration_registry(&[EVENT_STORE_MIGRATIONS[0], misbound], 1, 2),
+ Err(RadrootsEventStoreError::MigrationRegistryDefect { reason })
+ if reason.contains("bound to canonical migration 4 `source_maintenance`")
+ ));
+ }
+
+ #[tokio::test]
+ async fn migration_catalog_delta_requires_exact_symmetric_replacements() {
+ const ADDED_OBJECTS: &[&str] = &["radroots_event_store_replacement_probe"];
+ const REPLACED_OBJECTS: &[&str] = &["event_envelope_kind_created_idx"];
+ const EXTRA_REPLACEMENTS: &[&str] = &[
+ "event_envelope_kind_created_idx",
+ "event_envelope_projection_idx",
+ ];
+ let pool = memory_pool().await;
+ install_unledgered_baseline(&pool).await;
+ let original_index_sql = schema_object_sql(&pool, REPLACED_OBJECTS[0]).await;
+ let mut connection = pool.acquire().await.expect("connection");
+ let before = read_catalog(&mut connection).await.expect("before catalog");
+ sqlx::raw_sql(
+ "DROP INDEX event_envelope_kind_created_idx;
+ CREATE INDEX event_envelope_kind_created_idx
+ ON event_envelopes(kind, event_id);
+ CREATE TABLE radroots_event_store_replacement_probe (
+ id INTEGER PRIMARY KEY NOT NULL
+ ) STRICT;",
+ )
+ .execute(&mut *connection)
+ .await
+ .expect("replacement up delta");
+ let changed = read_catalog(&mut connection)
+ .await
+ .expect("changed catalog");
+ let mut migration = synthetic_v2_descriptor(ZERO_SHA256);
+ migration.owned_object_names = ADDED_OBJECTS;
+ migration.owned_table_names = ADDED_OBJECTS;
+ migration.replaced_object_names = REPLACED_OBJECTS;
+ validate_catalog_delta(&before, &changed, &migration, "up")
+ .expect("exact up replacement delta");
+
+ let mut undeclared = migration;
+ undeclared.replaced_object_names = &[];
+ assert!(matches!(
+ validate_catalog_delta(&before, &changed, &undeclared, "up"),
+ Err(RadrootsEventStoreError::MigrationCatalogDeltaMismatch { .. })
+ ));
+ let mut missing = migration;
+ missing.replaced_object_names = EXTRA_REPLACEMENTS;
+ assert!(matches!(
+ validate_catalog_delta(&before, &changed, &missing, "up"),
+ Err(RadrootsEventStoreError::MigrationCatalogDeltaMismatch { .. })
+ ));
+ let mut add_remove_masquerade = changed.clone();
+ add_remove_masquerade.retain(|row| row.name != REPLACED_OBJECTS[0]);
+ assert!(matches!(
+ validate_catalog_delta(&before, &add_remove_masquerade, &migration, "up"),
+ Err(RadrootsEventStoreError::MigrationCatalogDeltaMismatch { .. })
+ ));
+
+ sqlx::raw_sql("DROP TABLE radroots_event_store_replacement_probe;")
+ .execute(&mut *connection)
+ .await
+ .expect("remove added object");
+ sqlx::raw_sql("DROP INDEX event_envelope_kind_created_idx;")
+ .execute(&mut *connection)
+ .await
+ .expect("remove replacement index");
+ sqlx::query(sqlx::AssertSqlSafe(original_index_sql.clone()))
+ .execute(&mut *connection)
+ .await
+ .expect("restore predecessor index");
+ let restored = read_catalog(&mut connection)
+ .await
+ .expect("restored catalog");
+ validate_catalog_delta(&changed, &restored, &migration, "down")
+ .expect("exact down replacement delta");
+ assert_eq!(
+ restored
+ .iter()
+ .find(|row| row.name == REPLACED_OBJECTS[0])
+ .and_then(|row| row.sql.as_deref()),
+ Some(original_index_sql.as_str())
+ );
+ }
+
+ #[tokio::test]
+ async fn v4_marker_open_allows_repairing_prior_transition_high_water_drift() {
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(
+ &pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ )
+ .await
+ .expect("install v4 schema");
+
+ let authority_guard = schema_object_sql(
+ &pool,
+ "radroots_event_store_source_state_authority_update_guard",
+ )
+ .await;
+ sqlx::query("DROP TRIGGER radroots_event_store_source_state_authority_update_guard")
+ .execute(&pool)
+ .await
+ .expect("temporarily remove state authority guard");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET last_transition_seq = 7 WHERE singleton = 1",
+ )
+ .execute(&pool)
+ .await
+ .expect("drift derived transition high-water");
+ sqlx::query(sqlx::AssertSqlSafe(authority_guard))
+ .execute(&pool)
+ .await
+ .expect("restore state authority guard");
+ let mut connection = pool.acquire().await.expect("fingerprint connection");
+ validate_schema_fingerprint(
+ &mut connection,
+ EVENT_STORE_MIGRATIONS,
+ &EVENT_STORE_MIGRATIONS[3],
+ )
+ .await
+ .expect("exact v4 catalog after drift fixture");
+ drop(connection);
+
+ let target_generation = [0x91; 32];
+ let mut transaction = pool
+ .begin_with("BEGIN IMMEDIATE")
+ .await
+ .expect("marker transaction");
+ let wrong_prior = insert_test_rebuild_marker(&mut transaction, &target_generation, 0, 6)
+ .await
+ .expect_err("marker must still bind the exact prior transition high-water");
+ assert!(wrong_prior.as_database_error().is_some_and(|error| {
+ error
+ .message()
+ .contains("exact raw and prior source authority")
+ }));
+ let wrong_floor = insert_test_rebuild_marker(&mut transaction, &target_generation, 1, 7)
+ .await
+ .expect_err("marker must bind the actual retained transition maximum");
+ assert!(wrong_floor.as_database_error().is_some_and(|error| {
+ error
+ .message()
+ .contains("exact raw and prior source authority")
+ }));
+ let inserted = insert_test_rebuild_marker(&mut transaction, &target_generation, 0, 7)
+ .await
+ .expect("derived transition drift is repairable under v4");
+ assert_eq!(inserted.rows_affected(), 1);
+ let marker_count: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM radroots_event_store_source_rebuild_marker")
+ .fetch_one(&mut *transaction)
+ .await
+ .expect("marker count");
+ assert_eq!(marker_count, 1);
+
+ let appended = sqlx::query(
+ "INSERT INTO radroots_event_store_source_generation(source_generation, generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq) SELECT target_generation, target_generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("append repair generation");
+ assert_eq!(appended.rows_affected(), 1);
+ let rotated = sqlx::query(
+ "UPDATE radroots_event_store_source_state SET active_generation = ?, raw_event_count = 0, raw_tag_count = 0, raw_high_water_seq = 0, last_transition_seq = 0 WHERE singleton = 1 AND active_generation = (SELECT prior_active_generation FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1) AND raw_event_count = (SELECT prior_raw_event_count FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1) AND raw_tag_count = (SELECT prior_raw_tag_count FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1) AND raw_high_water_seq = (SELECT prior_raw_high_water_seq FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1) AND last_transition_seq = (SELECT prior_last_transition_seq FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1)",
+ )
+ .bind(target_generation.as_slice())
+ .execute(&mut *transaction)
+ .await
+ .expect("rotate source state through exact marker CAS");
+ assert_eq!(rotated.rows_affected(), 1);
+ let repaired: (Vec<u8>, i64, i64) = sqlx::query_as(
+ "SELECT state.active_generation, state.last_transition_seq, COALESCE(MAX(transition.transition_seq), 0) FROM radroots_event_store_source_state AS state LEFT JOIN radroots_event_store_addressable_head_transition AS transition ON transition.source_generation = state.active_generation WHERE state.singleton = 1 GROUP BY state.active_generation, state.last_transition_seq",
+ )
+ .fetch_one(&mut *transaction)
+ .await
+ .expect("repaired transition authority");
+ assert_eq!(repaired.0.as_slice(), target_generation.as_slice());
+ assert_eq!(repaired.1, repaired.2);
+ assert_eq!(repaired.1, 0);
+ transaction
+ .rollback()
+ .await
+ .expect("rollback marker fixture");
+ }
+
+ #[tokio::test]
+ async fn v3_to_v4_rejects_prior_transition_drift_atomically() {
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(
+ &pool,
+ &EVENT_STORE_MIGRATIONS[..3],
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ 3,
+ )
+ .await
+ .expect("install managed v3 schema");
+ let healthy_state: (Vec<u8>, i64, i64, i64, i64) = sqlx::query_as(
+ "SELECT active_generation, raw_event_count, raw_tag_count, raw_high_water_seq, last_transition_seq FROM radroots_event_store_source_state WHERE singleton = 1",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("healthy v3 source state");
+ let authority_guard = schema_object_sql(
+ &pool,
+ "radroots_event_store_source_state_authority_update_guard",
+ )
+ .await;
+ let mut predecessor_trigger_sql = BTreeMap::new();
+ for name in crate::migrations::EVENT_STORE_SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES {
+ predecessor_trigger_sql.insert(*name, schema_object_sql(&pool, name).await);
+ }
+ let ledger_before: Vec<(i64, String, String, String, String)> = sqlx::query_as(
+ "SELECT version, name, up_sha256, down_sha256, schema_sha256 FROM radroots_event_store_schema_migrations ORDER BY version",
+ )
+ .fetch_all(&pool)
+ .await
+ .expect("v3 ledger before drift");
+
+ let mut corruption = pool
+ .begin_with("BEGIN IMMEDIATE")
+ .await
+ .expect("v3 corruption fixture transaction");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_state_authority_update_guard")
+ .execute(&mut *corruption)
+ .await
+ .expect("temporarily remove state authority guard");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET last_transition_seq = 7 WHERE singleton = 1",
+ )
+ .execute(&mut *corruption)
+ .await
+ .expect("drift managed v3 transition high-water");
+ sqlx::query(sqlx::AssertSqlSafe(authority_guard.clone()))
+ .execute(&mut *corruption)
+ .await
+ .expect("restore exact v3 state authority guard");
+ corruption
+ .commit()
+ .await
+ .expect("commit corrupt managed-v3 fixture");
+ let corrupt_state: (Vec<u8>, i64, i64, i64, i64) = sqlx::query_as(
+ "SELECT active_generation, raw_event_count, raw_tag_count, raw_high_water_seq, last_transition_seq FROM radroots_event_store_source_state WHERE singleton = 1",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("committed corrupt v3 source state");
+ assert_eq!(corrupt_state.4, 7);
+ assert_ne!(corrupt_state, healthy_state);
+
+ let error = migrate_event_store_schema_with_registry(
+ &pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ )
+ .await
+ .expect_err("v4 upgrade must not repair corrupt managed-v3 hook state");
+ assert!(
+ matches!(
+ error,
+ RadrootsEventStoreError::MigrationHookStateDrift {
+ hook_id: "nip09_reconciliation_v1",
+ ..
+ }
+ ),
+ "unexpected managed-v3 drift failure: {error:?}"
+ );
+
+ assert_eq!(
+ sqlx::query_as::<_, (Vec<u8>, i64, i64, i64, i64)>(
+ "SELECT active_generation, raw_event_count, raw_tag_count, raw_high_water_seq, last_transition_seq FROM radroots_event_store_source_state WHERE singleton = 1",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("corrupt state after rejected upgrade"),
+ corrupt_state
+ );
+ let ledger_after: Vec<(i64, String, String, String, String)> = sqlx::query_as(
+ "SELECT version, name, up_sha256, down_sha256, schema_sha256 FROM radroots_event_store_schema_migrations ORDER BY version",
+ )
+ .fetch_all(&pool)
+ .await
+ .expect("ledger after rejected upgrade");
+ assert_eq!(ledger_after, ledger_before);
+ assert_eq!(
+ ledger_after.iter().map(|row| row.0).collect::<Vec<_>>(),
+ vec![1, 2, 3]
+ );
+ let v4_objects: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE name = 'radroots_event_store_source_capacity_v1'",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v4 object count after failed upgrade");
+ assert_eq!(v4_objects, 0);
+ let v4_ledger_rows: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM radroots_event_store_schema_migrations WHERE version = 4",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("v4 ledger row count after failed upgrade");
+ assert_eq!(v4_ledger_rows, 0);
+ for (name, sql) in &predecessor_trigger_sql {
+ assert_eq!(schema_object_sql(&pool, name).await, *sql);
+ }
+
+ let mut repair = pool
+ .begin_with("BEGIN IMMEDIATE")
+ .await
+ .expect("v3 fixture repair transaction");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_state_authority_update_guard")
+ .execute(&mut *repair)
+ .await
+ .expect("temporarily remove state authority guard for repair");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET last_transition_seq = ? WHERE singleton = 1",
+ )
+ .bind(healthy_state.4)
+ .execute(&mut *repair)
+ .await
+ .expect("repair v3 transition high-water fixture");
+ sqlx::query(sqlx::AssertSqlSafe(authority_guard))
+ .execute(&mut *repair)
+ .await
+ .expect("restore exact v3 state authority guard after repair");
+ repair.commit().await.expect("commit v3 fixture repair");
+ assert_eq!(
+ inspect_event_store_schema_status_with_registry(
+ &pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ )
+ .await
+ .expect("managed v3 status after explicit fixture repair"),
+ RadrootsEventStoreSchemaStatus::Managed { version: 3 }
+ );
+ }
+
+ #[tokio::test]
+ async fn v4_food_reset_requires_marker_rotation_and_preserves_target_rows() {
+ const PROJECTION_INSERT_GUARD: &str =
+ "radroots_event_store_food_availability_projection_insert_guard";
+ const IMAGE_INSERT_GUARD: &str =
+ "radroots_event_store_food_availability_image_insert_guard";
+ const CURSOR_DELETE_GUARD: &str =
+ "radroots_event_store_food_availability_cursor_delete_guard";
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(
+ &pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ )
+ .await
+ .expect("install v4 schema");
+ let active_generation: Vec<u8> = sqlx::query_scalar(
+ "SELECT active_generation FROM radroots_event_store_source_state WHERE singleton = 1",
+ )
+ .fetch_one(&pool)
+ .await
+ .expect("active generation");
+ let target_generation = [0x92; 32];
+ assert_ne!(active_generation.as_slice(), target_generation.as_slice());
+
+ let mut connection = pool.acquire().await.expect("fixture connection");
+ sqlx::query("PRAGMA foreign_keys = OFF")
+ .execute(&mut *connection)
+ .await
+ .expect("disable fixture foreign keys");
+ let mut guard_definitions = Vec::new();
+ for guard in [
+ PROJECTION_INSERT_GUARD,
+ IMAGE_INSERT_GUARD,
+ CURSOR_DELETE_GUARD,
+ ] {
+ let definition: String =
+ sqlx::query_scalar("SELECT sql FROM main.sqlite_schema WHERE name = ?")
+ .bind(guard)
+ .fetch_one(&mut *connection)
+ .await
+ .expect("fixture guard definition");
+ let drop_statement = format!("DROP TRIGGER {guard}");
+ sqlx::query(sqlx::AssertSqlSafe(drop_statement))
+ .execute(&mut *connection)
+ .await
+ .expect("drop fixture guard");
+ guard_definitions.push(definition);
+ }
+ sqlx::query("DELETE FROM radroots_event_store_food_availability_cursor")
+ .execute(&mut *connection)
+ .await
+ .expect("remove Food cursor fixture");
+ let author = "a".repeat(64);
+ for (generation, event_id, event_seq, d_tag) in [
+ (
+ active_generation.as_slice(),
+ "b".repeat(64),
+ 1_i64,
+ "historical",
+ ),
+ (
+ target_generation.as_slice(),
+ "c".repeat(64),
+ 2_i64,
+ "target",
+ ),
+ ] {
+ sqlx::query(
+ "INSERT INTO radroots_event_store_food_availability_projection(source_generation, kind, pubkey, d_tag, event_id, event_seq, created_at, contract_id, content, title, summary, published_at, location, price_amount, price_currency, price_unit, quantity_amount, quantity_unit, status, diagnostic_codes_json, source_transition_seq) VALUES (?, 30402, ?, ?, ?, ?, 10, 'radroots.food.availability.v1', 'fixture', 'Fixture', 'Fixture summary', 10, 'Victoria, BC', '3', 'CAD', 'lb', NULL, NULL, 'active', '[]', 1)",
+ )
+ .bind(generation)
+ .bind(author.as_str())
+ .bind(d_tag)
+ .bind(event_id)
+ .bind(event_seq)
+ .execute(&mut *connection)
+ .await
+ .expect("insert Food projection fixture");
+ sqlx::query(
+ "INSERT INTO radroots_event_store_food_availability_image(source_generation, pubkey, d_tag, image_index, raw_tag_json, url, width, height, blossom_sha256, qualifies, diagnostic_codes_json) VALUES (?, ?, ?, 0, '[\"image\",\"https://media.example/fixture.webp\"]', NULL, NULL, NULL, NULL, 0, '[]')",
+ )
+ .bind(generation)
+ .bind(author.as_str())
+ .bind(d_tag)
+ .execute(&mut *connection)
+ .await
+ .expect("insert Food image fixture");
+ }
+ for definition in guard_definitions {
+ sqlx::query(sqlx::AssertSqlSafe(definition))
+ .execute(&mut *connection)
+ .await
+ .expect("restore fixture guard");
+ }
+ sqlx::query("PRAGMA foreign_keys = ON")
+ .execute(&mut *connection)
+ .await
+ .expect("restore fixture foreign keys");
+ drop(connection);
+
+ for table in [
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_projection",
+ ] {
+ let statement = format!("DELETE FROM {table} WHERE source_generation = ?");
+ let error = sqlx::query(sqlx::AssertSqlSafe(statement))
+ .bind(active_generation.as_slice())
+ .execute(&pool)
+ .await
+ .expect_err("marker-free Food reset must fail");
+ assert!(error.as_database_error().is_some());
+ }
+
+ let mut transaction = pool
+ .begin_with("BEGIN IMMEDIATE")
+ .await
+ .expect("Food reset transaction");
+ insert_test_rebuild_marker(&mut transaction, &target_generation, 0, 0)
+ .await
+ .expect("open rebuild marker");
+ let pre_rotation = sqlx::query(
+ "DELETE FROM radroots_event_store_food_availability_image WHERE source_generation = ?",
+ )
+ .bind(active_generation.as_slice())
+ .execute(&mut *transaction)
+ .await
+ .expect_err("marker alone must not authorize Food reset");
+ assert!(pre_rotation.as_database_error().is_some());
+
+ let appended = sqlx::query(
+ "INSERT INTO radroots_event_store_source_generation(source_generation, generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq) SELECT target_generation, target_generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("append target generation");
+ assert_eq!(appended.rows_affected(), 1);
+ let rotated = sqlx::query(
+ "UPDATE radroots_event_store_source_state SET active_generation = ?, raw_event_count = 0, raw_tag_count = 0, raw_high_water_seq = 0, last_transition_seq = 0 WHERE singleton = 1",
+ )
+ .bind(target_generation.as_slice())
+ .execute(&mut *transaction)
+ .await
+ .expect("rotate source state");
+ assert_eq!(rotated.rows_affected(), 1);
+
+ for table in [
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_projection",
+ ] {
+ let statement = format!("DELETE FROM {table} WHERE source_generation = ?");
+ let deleted = sqlx::query(sqlx::AssertSqlSafe(statement))
+ .bind(active_generation.as_slice())
+ .execute(&mut *transaction)
+ .await
+ .expect("post-rotation historical Food reset");
+ assert_eq!(deleted.rows_affected(), 1);
+ }
+ for table in [
+ "radroots_event_store_food_availability_image",
+ "radroots_event_store_food_availability_projection",
+ ] {
+ let statement = format!("DELETE FROM {table} WHERE source_generation = ?");
+ let error = sqlx::query(sqlx::AssertSqlSafe(statement))
+ .bind(target_generation.as_slice())
+ .execute(&mut *transaction)
+ .await
+ .expect_err("active target-generation Food rows must remain guarded");
+ assert!(error.as_database_error().is_some());
+ }
+ let remaining: (i64, i64) = sqlx::query_as(
+ "SELECT (SELECT COUNT(*) FROM radroots_event_store_food_availability_projection WHERE source_generation = ?), (SELECT COUNT(*) FROM radroots_event_store_food_availability_image WHERE source_generation = ?)",
+ )
+ .bind(target_generation.as_slice())
+ .bind(target_generation.as_slice())
+ .fetch_one(&mut *transaction)
+ .await
+ .expect("target Food rows");
+ assert_eq!(remaining, (1, 1));
+ transaction
+ .rollback()
+ .await
+ .expect("rollback Food reset fixture");
+ }
+
+ #[tokio::test]
+ async fn v4_down_restores_exact_predecessor_trigger_sql_and_fingerprint() {
+ const REPLACED: &[&str] = &[
+ "radroots_event_store_food_availability_image_delete_guard",
+ "radroots_event_store_food_availability_projection_delete_guard",
+ "radroots_event_store_source_rebuild_marker_insert_guard",
+ ];
+ let pool = memory_pool().await;
+ migrate_event_store_schema_with_registry(
+ &pool,
+ &EVENT_STORE_MIGRATIONS[..3],
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ 3,
+ )
+ .await
+ .expect("install v3 schema");
+ let mut predecessor_sql = BTreeMap::new();
+ for name in REPLACED {
+ predecessor_sql.insert(*name, schema_object_sql(&pool, name).await);
+ }
+
+ migrate_event_store_schema_with_registry(
+ &pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ )
+ .await
+ .expect("upgrade to v4");
+ for name in REPLACED {
+ assert_ne!(schema_object_sql(&pool, name).await, predecessor_sql[*name]);
+ }
+
+ rollback_event_store_schema_with_registry(
+ &pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ 3,
+ )
+ .await
+ .expect("rollback v4 to v3");
+ for name in REPLACED {
+ assert_eq!(schema_object_sql(&pool, name).await, predecessor_sql[*name]);
+ }
+ assert_eq!(
+ inspect_event_store_schema_status_with_registry(
+ &pool,
+ EVENT_STORE_MIGRATIONS,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,
+ )
+ .await
+ .expect("restored v3 status"),
+ RadrootsEventStoreSchemaStatus::Managed { version: 3 }
+ );
+ }
+
#[tokio::test]
async fn synthetic_v2_owned_objects_are_fingerprinted_and_foreign_keys_are_checked() {
let registry = synthetic_v2_registry().await;
@@ -2480,6 +3591,7 @@ CREATE TABLE forgotten_v2_table (
down_sha256: leaked_sha256(DOWN),
schema_sha256: ZERO_SHA256,
owned_object_names: OBJECTS,
+ replaced_object_names: &[],
owned_table_names: OBJECTS,
fts5_table_names: NO_FTS5_TABLES,
hook: crate::migrations::EventStoreMigrationHook::None,
@@ -2520,7 +3632,7 @@ CREATE TABLE forgotten_v2_table (
SqliteConnectOptions::new()
.filename(&path)
.create_if_missing(true)
- .busy_timeout(Duration::from_millis(100))
+ .busy_timeout(Duration::ZERO)
};
let first = SqlitePoolOptions::new()
.max_connections(1)
@@ -2540,14 +3652,9 @@ CREATE TABLE forgotten_v2_table (
.begin_with("BEGIN IMMEDIATE")
.await
.expect("writer transaction");
- let started = Instant::now();
migrate_event_store_schema(&second)
.await
.expect("read-only fast path");
- assert!(
- started.elapsed() < Duration::from_secs(1),
- "current-schema migration attempted to wait for a writer lock"
- );
writer.rollback().await.expect("release writer");
}
diff --git a/crates/event_store/src/source_maintenance_v1.rs b/crates/event_store/src/source_maintenance_v1.rs
@@ -0,0 +1,1197 @@
+#![forbid(unsafe_code)]
+
+use crate::model::{RadrootsEventIngest, RadrootsEventStoreSourceGeneration};
+use crate::nip09::reconciliation_v1::{
+ ReconciliationCapacity, ReconciliationCapacityLimits, measure_reconciliation_capacity_bounded,
+};
+use crate::{
+ RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1, RadrootsEventStoreError,
+ RadrootsEventStoreSourceCapacityResourceV1,
+};
+use sqlx::{Row, SqliteConnection};
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) struct RawSourceCapacityDeltaV1 {
+ raw_events: u64,
+ raw_tags: u64,
+ raw_event_bytes: u64,
+ raw_tag_bytes: u64,
+}
+
+/// Persisted retained raw-source capacity sealed to one database snapshot.
+///
+/// This is the constant-cost authority used by ordinary reads and writes. It
+/// does not rescan raw rows; migrations and database reopen perform the full
+/// raw-source recount that authenticates this seal.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct RadrootsEventStoreSourceCapacityV1 {
+ source_generation: RadrootsEventStoreSourceGeneration,
+ capacity: ReconciliationCapacity,
+ raw_high_water_seq: i64,
+ retained_generation_count: u32,
+ retained_generation_limit: u32,
+}
+
+impl RadrootsEventStoreSourceCapacityV1 {
+ /// Returns the active source generation sealed by this snapshot.
+ pub const fn source_generation(&self) -> RadrootsEventStoreSourceGeneration {
+ self.source_generation
+ }
+
+ /// Returns the retained raw event-row count.
+ pub const fn raw_event_count(&self) -> u64 {
+ self.capacity.raw_events
+ }
+
+ /// Returns the retained raw tag-row count.
+ pub const fn raw_tag_count(&self) -> u64 {
+ self.capacity.raw_tags
+ }
+
+ /// Returns governed UTF-8 bytes across retained raw event text fields.
+ pub const fn raw_event_text_bytes(&self) -> u64 {
+ self.capacity.raw_event_bytes
+ }
+
+ /// Returns governed UTF-8 bytes across retained raw tag text fields.
+ pub const fn raw_tag_text_bytes(&self) -> u64 {
+ self.capacity.raw_tag_bytes
+ }
+
+ /// Returns the greatest retained raw event sequence.
+ pub const fn raw_high_water_seq(&self) -> i64 {
+ self.raw_high_water_seq
+ }
+
+ /// Returns the append-only source generations currently retained.
+ pub const fn retained_generation_count(&self) -> u32 {
+ self.retained_generation_count
+ }
+
+ /// Returns the maximum append-only source generations this store retains.
+ pub const fn retained_generation_limit(&self) -> u32 {
+ self.retained_generation_limit
+ }
+}
+
+pub(crate) fn raw_source_capacity_delta_v1(
+ ingest: &RadrootsEventIngest,
+ tags_json: &str,
+) -> Result<RawSourceCapacityDeltaV1, RadrootsEventStoreError> {
+ let event = ingest.event();
+ let raw_event_bytes = raw_event_row_bytes_v1(
+ event.id_str(),
+ event.author_str(),
+ tags_json,
+ event.content(),
+ event.sig_str(),
+ ingest.raw_json(),
+ )?;
+ let mut raw_tags = 0_u64;
+ let mut raw_tag_bytes = 0_u64;
+ for tag in event.tag_slices() {
+ let values = tag.as_slice();
+ let tag_name = values.first().map(String::as_str).unwrap_or("");
+ let tag_value = values.get(1).map(String::as_str);
+ let tag_json = serde_json::to_string(values)?;
+ raw_tags = checked_capacity_add(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
+ raw_tags,
+ 1,
+ )?;
+ raw_tag_bytes = checked_capacity_add(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
+ raw_tag_bytes,
+ raw_tag_row_bytes_v1(event.id_str(), tag_name, tag_value, tag_json.as_str())?,
+ )?;
+ }
+ Ok(RawSourceCapacityDeltaV1 {
+ raw_events: 1,
+ raw_tags,
+ raw_event_bytes,
+ raw_tag_bytes,
+ })
+}
+
+pub(crate) async fn preflight_unique_raw_source_append_v1(
+ connection: &mut SqliteConnection,
+ delta: RawSourceCapacityDeltaV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let current = validate_source_capacity_authority_fast_v1(connection).await?;
+ validate_prospective_capacity(current.capacity, delta)
+}
+
+pub(crate) async fn advance_source_capacity_after_insert_v1(
+ connection: &mut SqliteConnection,
+ delta: RawSourceCapacityDeltaV1,
+ inserted_seq: i64,
+) -> Result<(), RadrootsEventStoreError> {
+ let current = read_source_capacity_v1(connection).await?;
+ validate_prospective_capacity(current.capacity, delta)?;
+ let next = ReconciliationCapacity {
+ raw_events: checked_capacity_add(
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ current.capacity.raw_events,
+ delta.raw_events,
+ )?,
+ raw_tags: checked_capacity_add(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
+ current.capacity.raw_tags,
+ delta.raw_tags,
+ )?,
+ raw_event_bytes: checked_capacity_add(
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
+ current.capacity.raw_event_bytes,
+ delta.raw_event_bytes,
+ )?,
+ raw_tag_bytes: checked_capacity_add(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
+ current.capacity.raw_tag_bytes,
+ delta.raw_tag_bytes,
+ )?,
+ };
+ let updated = sqlx::query(
+ "UPDATE radroots_event_store_source_capacity_v1 SET raw_event_count = ?, raw_tag_count = ?, raw_event_bytes = ?, raw_tag_bytes = ?, raw_high_water_seq = ? WHERE singleton = 1 AND source_generation = ? AND raw_event_count = ? AND raw_tag_count = ? AND raw_event_bytes = ? AND raw_tag_bytes = ? AND raw_high_water_seq = ? AND retained_generation_count = ? AND retained_generation_limit = ?",
+ )
+ .bind(sqlite_capacity_value(next.raw_events, "raw_event_count")?)
+ .bind(sqlite_capacity_value(next.raw_tags, "raw_tag_count")?)
+ .bind(sqlite_capacity_value(next.raw_event_bytes, "raw_event_bytes")?)
+ .bind(sqlite_capacity_value(next.raw_tag_bytes, "raw_tag_bytes")?)
+ .bind(inserted_seq)
+ .bind(current.source_generation.as_bytes().as_slice())
+ .bind(sqlite_capacity_value(
+ current.capacity.raw_events,
+ "raw_event_count",
+ )?)
+ .bind(sqlite_capacity_value(
+ current.capacity.raw_tags,
+ "raw_tag_count",
+ )?)
+ .bind(sqlite_capacity_value(
+ current.capacity.raw_event_bytes,
+ "raw_event_bytes",
+ )?)
+ .bind(sqlite_capacity_value(
+ current.capacity.raw_tag_bytes,
+ "raw_tag_bytes",
+ )?)
+ .bind(current.raw_high_water_seq)
+ .bind(i64::from(current.retained_generation_count))
+ .bind(i64::from(current.retained_generation_limit))
+ .execute(&mut *connection)
+ .await?;
+ if updated.rows_affected() != 1 {
+ return source_capacity_drift(format!(
+ "append authority compare-and-swap affected {} rows",
+ updated.rows_affected()
+ ));
+ }
+ validate_source_capacity_authority_fast_v1(connection)
+ .await
+ .map(|_| ())
+}
+
+pub(crate) async fn apply_source_maintenance_hook_v1(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ let capacity = measure_reconciliation_capacity_bounded(
+ connection,
+ ReconciliationCapacityLimits::production(),
+ )
+ .await?;
+ validate_measured_capacity(capacity)?;
+ validate_no_persisted_ephemeral_raw_rows_v1(connection).await?;
+ let row = sqlx::query(
+ "SELECT state.active_generation, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, (SELECT COUNT(*) FROM (SELECT 1 FROM radroots_event_store_source_generation LIMIT 9)) AS retained_generation_count FROM radroots_event_store_source_state AS state WHERE state.singleton = 1",
+ )
+ .fetch_one(&mut *connection)
+ .await?;
+ let source_generation = source_generation_bytes(row.try_get("active_generation")?)?;
+ let raw_event_count: i64 = row.try_get("raw_event_count")?;
+ let raw_tag_count: i64 = row.try_get("raw_tag_count")?;
+ let raw_high_water_seq: i64 = row.try_get("raw_high_water_seq")?;
+ let retained_generation_count = generation_count(row.try_get("retained_generation_count")?)?;
+ if retained_generation_count > RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1 {
+ return Err(
+ RadrootsEventStoreError::SourceGenerationHistoryLimitReached {
+ current: retained_generation_count,
+ limit: RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1,
+ },
+ );
+ }
+ if raw_event_count != sqlite_capacity_value(capacity.raw_events, "raw_event_count")?
+ || raw_tag_count != sqlite_capacity_value(capacity.raw_tags, "raw_tag_count")?
+ {
+ return source_capacity_drift(
+ "measured raw row counts disagree with active source state".to_owned(),
+ );
+ }
+ let inserted = sqlx::query(
+ "INSERT INTO radroots_event_store_source_capacity_v1(singleton, source_generation, raw_event_count, raw_tag_count, raw_event_bytes, raw_tag_bytes, raw_high_water_seq, retained_generation_count, retained_generation_limit) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)",
+ )
+ .bind(source_generation.as_slice())
+ .bind(raw_event_count)
+ .bind(raw_tag_count)
+ .bind(sqlite_capacity_value(
+ capacity.raw_event_bytes,
+ "raw_event_bytes",
+ )?)
+ .bind(sqlite_capacity_value(
+ capacity.raw_tag_bytes,
+ "raw_tag_bytes",
+ )?)
+ .bind(raw_high_water_seq)
+ .bind(i64::from(retained_generation_count))
+ .bind(i64::from(
+ RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1,
+ ))
+ .execute(&mut *connection)
+ .await?;
+ if inserted.rows_affected() != 1 {
+ return source_capacity_drift(format!(
+ "source capacity initialization affected {} rows",
+ inserted.rows_affected()
+ ));
+ }
+ validate_source_capacity_authority_full_v1(connection).await
+}
+
+pub(crate) async fn validate_source_capacity_authority_fast_v1(
+ connection: &mut SqliteConnection,
+) -> Result<RadrootsEventStoreSourceCapacityV1, RadrootsEventStoreError> {
+ let capacity = read_source_capacity_v1(connection).await?;
+ validate_measured_capacity(capacity.capacity)?;
+ if capacity.retained_generation_limit
+ != RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1
+ {
+ return source_capacity_drift(format!(
+ "retained generation limit is {}, expected {}",
+ capacity.retained_generation_limit,
+ RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1
+ ));
+ }
+ let row = sqlx::query(
+ "SELECT state.active_generation, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, generation.generation_ordinal, (SELECT COUNT(*) FROM (SELECT 1 FROM radroots_event_store_source_generation LIMIT 9)) AS retained_generation_count FROM radroots_event_store_source_state AS state JOIN radroots_event_store_source_generation AS generation ON generation.source_generation = state.active_generation WHERE state.singleton = 1",
+ )
+ .fetch_one(&mut *connection)
+ .await?;
+ let active_generation = RadrootsEventStoreSourceGeneration::from_bytes(
+ source_generation_bytes(row.try_get("active_generation")?)?,
+ );
+ let raw_event_count = sqlite_nonnegative_capacity(
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ row.try_get("raw_event_count")?,
+ )?;
+ let raw_tag_count = sqlite_nonnegative_capacity(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
+ row.try_get("raw_tag_count")?,
+ )?;
+ let raw_high_water_seq: i64 = row.try_get("raw_high_water_seq")?;
+ let generation_ordinal = generation_count(row.try_get("generation_ordinal")?)?;
+ let retained_generation_count = generation_count(row.try_get("retained_generation_count")?)?;
+ if active_generation != capacity.source_generation
+ || raw_event_count != capacity.capacity.raw_events
+ || raw_tag_count != capacity.capacity.raw_tags
+ || raw_high_water_seq != capacity.raw_high_water_seq
+ || generation_ordinal != retained_generation_count
+ || retained_generation_count != capacity.retained_generation_count
+ || retained_generation_count > capacity.retained_generation_limit
+ {
+ return source_capacity_drift(
+ "capacity seal does not match active source state and generation history".to_owned(),
+ );
+ }
+ Ok(capacity)
+}
+
+pub(crate) async fn validate_source_capacity_authority_full_v1(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ let persisted = validate_source_capacity_authority_fast_v1(connection).await?;
+ let measured = measure_reconciliation_capacity_bounded(
+ connection,
+ ReconciliationCapacityLimits::production(),
+ )
+ .await?;
+ validate_measured_capacity(measured)?;
+ validate_no_persisted_ephemeral_raw_rows_v1(connection).await?;
+ if measured != persisted.capacity {
+ return source_capacity_drift(format!(
+ "persisted capacity {:?} differs from measured raw authority {measured:?}",
+ persisted.capacity
+ ));
+ }
+ Ok(())
+}
+
+pub(crate) async fn validate_no_persisted_ephemeral_raw_rows_v1(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ let row = sqlx::query(
+ "SELECT event_id, kind FROM event_envelopes WHERE kind BETWEEN 20000 AND 29999 ORDER BY seq LIMIT 1",
+ )
+ .fetch_optional(&mut *connection)
+ .await?;
+ if let Some(row) = row {
+ return Err(RadrootsEventStoreError::PersistedEphemeralRawEvent {
+ event_id: row.try_get("event_id")?,
+ kind: row.try_get("kind")?,
+ });
+ }
+ Ok(())
+}
+
+pub(crate) async fn preflight_source_generation_append_v1(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ let capacity_authority_exists: i64 = sqlx::query_scalar(
+ "SELECT EXISTS (SELECT 1 FROM main.sqlite_schema WHERE type = 'table' AND name = 'radroots_event_store_source_capacity_v1')",
+ )
+ .fetch_one(&mut *connection)
+ .await?;
+ if capacity_authority_exists == 0 {
+ return Ok(());
+ }
+ let capacity = validate_source_capacity_authority_fast_v1(connection).await?;
+ validate_source_generation_append_available_v1(
+ capacity.retained_generation_count,
+ capacity.retained_generation_limit,
+ )
+}
+
+pub(crate) async fn bind_source_capacity_to_generation_v1(
+ connection: &mut SqliteConnection,
+ target_generation: RadrootsEventStoreSourceGeneration,
+) -> Result<bool, RadrootsEventStoreError> {
+ let capacity_authority_exists: i64 = sqlx::query_scalar(
+ "SELECT EXISTS (SELECT 1 FROM main.sqlite_schema WHERE type = 'table' AND name = 'radroots_event_store_source_capacity_v1')",
+ )
+ .fetch_one(&mut *connection)
+ .await?;
+ if capacity_authority_exists == 0 {
+ return Ok(false);
+ }
+ let current = read_source_capacity_v1(connection).await?;
+ if current.source_generation == target_generation {
+ return source_capacity_drift(
+ "source rebuild target already owns the persisted capacity seal".to_owned(),
+ );
+ }
+ let updated = sqlx::query(
+ "UPDATE radroots_event_store_source_capacity_v1 SET source_generation = ? WHERE singleton = 1 AND source_generation = ? AND raw_event_count = ? AND raw_tag_count = ? AND raw_event_bytes = ? AND raw_tag_bytes = ? AND raw_high_water_seq = ? AND retained_generation_count = ? AND retained_generation_limit = ?",
+ )
+ .bind(target_generation.as_bytes().as_slice())
+ .bind(current.source_generation.as_bytes().as_slice())
+ .bind(sqlite_capacity_value(
+ current.capacity.raw_events,
+ "raw_event_count",
+ )?)
+ .bind(sqlite_capacity_value(
+ current.capacity.raw_tags,
+ "raw_tag_count",
+ )?)
+ .bind(sqlite_capacity_value(
+ current.capacity.raw_event_bytes,
+ "raw_event_bytes",
+ )?)
+ .bind(sqlite_capacity_value(
+ current.capacity.raw_tag_bytes,
+ "raw_tag_bytes",
+ )?)
+ .bind(current.raw_high_water_seq)
+ .bind(i64::from(current.retained_generation_count))
+ .bind(i64::from(current.retained_generation_limit))
+ .execute(&mut *connection)
+ .await?;
+ if updated.rows_affected() != 1 {
+ return source_capacity_drift(format!(
+ "source rebuild capacity bind affected {} rows",
+ updated.rows_affected()
+ ));
+ }
+ let rebound = validate_source_capacity_authority_fast_v1(connection).await?;
+ if rebound.source_generation != target_generation {
+ return source_capacity_drift(
+ "source rebuild capacity bind did not select its target generation".to_owned(),
+ );
+ }
+ Ok(true)
+}
+
+fn validate_source_generation_append_available_v1(
+ current: u32,
+ limit: u32,
+) -> Result<(), RadrootsEventStoreError> {
+ if current >= limit {
+ return Err(
+ RadrootsEventStoreError::SourceGenerationHistoryLimitReached { current, limit },
+ );
+ }
+ Ok(())
+}
+
+async fn read_source_capacity_v1(
+ connection: &mut SqliteConnection,
+) -> Result<RadrootsEventStoreSourceCapacityV1, RadrootsEventStoreError> {
+ let rows = sqlx::query(
+ "SELECT source_generation, raw_event_count, raw_tag_count, raw_event_bytes, raw_tag_bytes, raw_high_water_seq, retained_generation_count, retained_generation_limit FROM radroots_event_store_source_capacity_v1 WHERE singleton = 1",
+ )
+ .fetch_all(&mut *connection)
+ .await?;
+ if rows.len() != 1 {
+ return source_capacity_drift(format!(
+ "expected one source capacity row, found {}",
+ rows.len()
+ ));
+ }
+ let row = &rows[0];
+ Ok(RadrootsEventStoreSourceCapacityV1 {
+ source_generation: RadrootsEventStoreSourceGeneration::from_bytes(source_generation_bytes(
+ row.try_get("source_generation")?,
+ )?),
+ capacity: ReconciliationCapacity {
+ raw_events: sqlite_nonnegative_capacity(
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ row.try_get("raw_event_count")?,
+ )?,
+ raw_tags: sqlite_nonnegative_capacity(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
+ row.try_get("raw_tag_count")?,
+ )?,
+ raw_event_bytes: sqlite_nonnegative_capacity(
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
+ row.try_get("raw_event_bytes")?,
+ )?,
+ raw_tag_bytes: sqlite_nonnegative_capacity(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
+ row.try_get("raw_tag_bytes")?,
+ )?,
+ },
+ raw_high_water_seq: row.try_get("raw_high_water_seq")?,
+ retained_generation_count: generation_count(row.try_get("retained_generation_count")?)?,
+ retained_generation_limit: generation_count(row.try_get("retained_generation_limit")?)?,
+ })
+}
+
+fn validate_prospective_capacity(
+ current: ReconciliationCapacity,
+ delta: RawSourceCapacityDeltaV1,
+) -> Result<(), RadrootsEventStoreError> {
+ let limits = ReconciliationCapacityLimits::production();
+ for (resource, requested) in [
+ (
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ delta.raw_events,
+ ),
+ (
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
+ delta.raw_tags,
+ ),
+ (
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
+ delta.raw_event_bytes,
+ ),
+ (
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
+ delta.raw_tag_bytes,
+ ),
+ ] {
+ let current_value = current.value(resource);
+ let limit = limits.limit(resource);
+ if current_value
+ .checked_add(requested)
+ .is_none_or(|next| next > limit)
+ {
+ return Err(RadrootsEventStoreError::SourceCapacityExceeded {
+ resource,
+ current: current_value,
+ requested,
+ limit,
+ });
+ }
+ }
+ Ok(())
+}
+
+fn validate_measured_capacity(
+ capacity: ReconciliationCapacity,
+) -> Result<(), RadrootsEventStoreError> {
+ validate_prospective_capacity(
+ capacity,
+ RawSourceCapacityDeltaV1 {
+ raw_events: 0,
+ raw_tags: 0,
+ raw_event_bytes: 0,
+ raw_tag_bytes: 0,
+ },
+ )
+}
+
+fn raw_event_row_bytes_v1(
+ event_id: &str,
+ pubkey: &str,
+ tags_json: &str,
+ content: &str,
+ sig: &str,
+ raw_json: &str,
+) -> Result<u64, RadrootsEventStoreError> {
+ checked_text_byte_sum(
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
+ [event_id, pubkey, tags_json, content, sig, raw_json],
+ )
+}
+
+fn raw_tag_row_bytes_v1(
+ event_id: &str,
+ tag_name: &str,
+ tag_value: Option<&str>,
+ tag_json: &str,
+) -> Result<u64, RadrootsEventStoreError> {
+ let required = checked_text_byte_sum(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
+ [event_id, tag_name, tag_json],
+ )?;
+ checked_capacity_add(
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
+ required,
+ u64::try_from(tag_value.map_or(0, str::len)).map_err(|_| {
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
+ current: required,
+ requested: u64::MAX,
+ limit: ReconciliationCapacityLimits::production()
+ .limit(RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes),
+ }
+ })?,
+ )
+}
+
+fn checked_text_byte_sum<const N: usize>(
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
+ values: [&str; N],
+) -> Result<u64, RadrootsEventStoreError> {
+ values.iter().try_fold(0_u64, |current, value| {
+ let requested = u64::try_from(value.len()).map_err(|_| {
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource,
+ current,
+ requested: u64::MAX,
+ limit: ReconciliationCapacityLimits::production().limit(resource),
+ }
+ })?;
+ checked_capacity_add(resource, current, requested)
+ })
+}
+
+fn checked_capacity_add(
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
+ current: u64,
+ requested: u64,
+) -> Result<u64, RadrootsEventStoreError> {
+ current
+ .checked_add(requested)
+ .ok_or_else(|| RadrootsEventStoreError::SourceCapacityExceeded {
+ resource,
+ current,
+ requested,
+ limit: ReconciliationCapacityLimits::production().limit(resource),
+ })
+}
+
+fn sqlite_nonnegative_capacity(
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
+ value: i64,
+) -> Result<u64, RadrootsEventStoreError> {
+ u64::try_from(value).map_err(|_| RadrootsEventStoreError::SourceCapacityStateDrift {
+ reason: format!("persisted {resource} is negative or outside the unsigned range: {value}"),
+ })
+}
+
+fn sqlite_capacity_value(value: u64, field: &'static str) -> Result<i64, RadrootsEventStoreError> {
+ i64::try_from(value).map_err(|_| RadrootsEventStoreError::SourceCapacityStateDrift {
+ reason: format!("{field} exceeds the SQLite integer range: {value}"),
+ })
+}
+
+fn generation_count(value: i64) -> Result<u32, RadrootsEventStoreError> {
+ u32::try_from(value)
+ .ok()
+ .filter(|value| *value > 0)
+ .ok_or_else(|| RadrootsEventStoreError::SourceCapacityStateDrift {
+ reason: format!("retained generation count is outside the positive u32 range: {value}"),
+ })
+}
+
+fn source_generation_bytes(value: Vec<u8>) -> Result<[u8; 32], RadrootsEventStoreError> {
+ value.try_into().map_err(
+ |value: Vec<u8>| RadrootsEventStoreError::SourceCapacityStateDrift {
+ reason: format!(
+ "source capacity generation has {} bytes instead of 32",
+ value.len()
+ ),
+ },
+ )
+}
+
+fn source_capacity_drift<T>(reason: String) -> Result<T, RadrootsEventStoreError> {
+ Err(RadrootsEventStoreError::SourceCapacityStateDrift { reason })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::RadrootsEventStore;
+ use sqlx::Connection;
+
+ fn capacity_with(
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
+ value: u64,
+ ) -> ReconciliationCapacity {
+ let mut capacity = ReconciliationCapacity::default();
+ match resource {
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents => {
+ capacity.raw_events = value;
+ }
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags => {
+ capacity.raw_tags = value;
+ }
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes => {
+ capacity.raw_event_bytes = value;
+ }
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes => {
+ capacity.raw_tag_bytes = value;
+ }
+ }
+ capacity
+ }
+
+ fn delta_with(
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
+ value: u64,
+ ) -> RawSourceCapacityDeltaV1 {
+ let mut delta = RawSourceCapacityDeltaV1 {
+ raw_events: 0,
+ raw_tags: 0,
+ raw_event_bytes: 0,
+ raw_tag_bytes: 0,
+ };
+ match resource {
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents => delta.raw_events = value,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags => delta.raw_tags = value,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes => {
+ delta.raw_event_bytes = value;
+ }
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes => {
+ delta.raw_tag_bytes = value;
+ }
+ }
+ delta
+ }
+
+ #[tokio::test]
+ async fn rust_utf8_accounting_matches_sqlite_blob_lengths() {
+ let mut connection = SqliteConnection::connect("sqlite::memory:")
+ .await
+ .expect("memory SQLite");
+ let event_fields = [
+ "event\0id",
+ "publíckey",
+ "[[\"t\",\"野菜\u{0000}\"]]",
+ "café 🥕\0",
+ "signature",
+ "{\"content\":\"café 🥕\\u0000\"}",
+ ];
+ let rust_event = raw_event_row_bytes_v1(
+ event_fields[0],
+ event_fields[1],
+ event_fields[2],
+ event_fields[3],
+ event_fields[4],
+ event_fields[5],
+ )
+ .expect("Rust event byte count");
+ let sqlite_event: i64 = sqlx::query_scalar(
+ "SELECT length(CAST(? AS BLOB)) + length(CAST(? AS BLOB)) + length(CAST(? AS BLOB)) + length(CAST(? AS BLOB)) + length(CAST(? AS BLOB)) + length(CAST(? AS BLOB))",
+ )
+ .bind(event_fields[0])
+ .bind(event_fields[1])
+ .bind(event_fields[2])
+ .bind(event_fields[3])
+ .bind(event_fields[4])
+ .bind(event_fields[5])
+ .fetch_one(&mut connection)
+ .await
+ .expect("SQLite event byte count");
+ assert_eq!(rust_event, u64::try_from(sqlite_event).expect("positive"));
+
+ let rust_tag = raw_tag_row_bytes_v1(
+ event_fields[0],
+ "t\0",
+ Some("野菜🥕"),
+ "[\"t\\u0000\",\"野菜🥕\"]",
+ )
+ .expect("Rust tag byte count");
+ let sqlite_tag: i64 = sqlx::query_scalar(
+ "SELECT length(CAST(? AS BLOB)) + length(CAST(? AS BLOB)) + COALESCE(length(CAST(? AS BLOB)), 0) + length(CAST(? AS BLOB))",
+ )
+ .bind(event_fields[0])
+ .bind("t\0")
+ .bind("野菜🥕")
+ .bind("[\"t\\u0000\",\"野菜🥕\"]")
+ .fetch_one(&mut connection)
+ .await
+ .expect("SQLite tag byte count");
+ assert_eq!(rust_tag, u64::try_from(sqlite_tag).expect("positive"));
+ }
+
+ #[test]
+ fn every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over() {
+ let limits = ReconciliationCapacityLimits::production();
+ for resource in [
+ RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTags,
+ RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
+ RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
+ ] {
+ let limit = limits.limit(resource);
+ validate_prospective_capacity(
+ capacity_with(resource, limit - 1),
+ delta_with(resource, 1),
+ )
+ .expect("exact prospective capacity boundary");
+ let error = validate_prospective_capacity(
+ capacity_with(resource, limit),
+ delta_with(resource, 1),
+ )
+ .expect_err("one-over prospective capacity boundary");
+ assert!(matches!(
+ error,
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: actual_resource,
+ current,
+ requested: 1,
+ limit: actual_limit,
+ } if actual_resource == resource && current == limit && actual_limit == limit
+ ));
+
+ validate_measured_capacity(capacity_with(resource, limit))
+ .expect("exact measured capacity boundary");
+ let error = validate_measured_capacity(capacity_with(resource, limit + 1))
+ .expect_err("one-over measured capacity boundary");
+ assert!(matches!(
+ error,
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: actual_resource,
+ current,
+ requested: 0,
+ limit: actual_limit,
+ } if actual_resource == resource
+ && current == limit + 1
+ && actual_limit == limit
+ ));
+ }
+ }
+
+ #[test]
+ fn retained_generation_nip09_logical_rows_have_an_audited_upper_bound() {
+ let events = crate::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1;
+ let tags = crate::RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1;
+
+ // Per generation: one generation row, at most E coordinates, E
+ // deletion requests, T combined event/address targets, E head states,
+ // E + T transitions, and one feed-integrity row.
+ let generation = 1_u64;
+ let coordinates = events;
+ let requests = events;
+ let combined_targets = tags;
+ let head_states = events;
+ let transitions = events + tags;
+ let feed_integrity = 1_u64;
+ let per_generation = generation
+ + coordinates
+ + requests
+ + combined_targets
+ + head_states
+ + transitions
+ + feed_integrity;
+ assert_eq!(per_generation, 4 * events + 2 * tags + 2);
+ assert_eq!(per_generation, 600_002);
+ assert_eq!(
+ per_generation * u64::from(RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1),
+ 4_800_016
+ );
+
+ for table in [
+ "radroots_event_store_source_generation",
+ "radroots_event_store_event_coordinate",
+ "radroots_event_store_nip09_request",
+ "radroots_event_store_nip09_event_target",
+ "radroots_event_store_nip09_address_target",
+ "radroots_event_store_addressable_head_state",
+ "radroots_event_store_addressable_head_transition",
+ "radroots_event_store_addressable_feed_integrity_v1",
+ ] {
+ assert!(
+ crate::migrations::EVENT_STORE_MIGRATIONS
+ .iter()
+ .any(|migration| migration.owned_table_names.contains(&table)),
+ "logical-bound table is absent from the governed migration catalog: {table}"
+ );
+ }
+ }
+
+ #[test]
+ fn generation_append_limit_returns_the_typed_current_and_limit() {
+ validate_source_generation_append_available_v1(7, 8)
+ .expect("one retained generation slot remains");
+ assert!(matches!(
+ validate_source_generation_append_available_v1(8, 8),
+ Err(
+ RadrootsEventStoreError::SourceGenerationHistoryLimitReached {
+ current: 8,
+ limit: 8,
+ }
+ )
+ ));
+ }
+
+ #[tokio::test]
+ async fn generation_sql_backstop_allows_exact_append_and_is_conflict_safe_one_over() {
+ let store = RadrootsEventStore::open_memory().await.expect("open store");
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("maintenance fixture transaction");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_update_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove capacity update guard in rolled-back fixture");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_capacity_v1 SET retained_generation_count = retained_generation_limit - 1 WHERE singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("place fixture one below retained generation limit");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_generation_append_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("isolate the v4 generation-capacity backstop");
+
+ let exact = sqlx::query(
+ "INSERT INTO radroots_event_store_source_generation(source_generation, generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq) SELECT ?, generation_ordinal + 1, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq FROM radroots_event_store_source_generation ORDER BY generation_ordinal DESC LIMIT 1",
+ )
+ .bind(vec![0xa4_u8; 32])
+ .execute(&mut *transaction)
+ .await
+ .expect("exact generation boundary must append");
+ assert_eq!(exact.rows_affected(), 1);
+ let retained_count: i64 = sqlx::query_scalar(
+ "SELECT retained_generation_count FROM radroots_event_store_source_capacity_v1 WHERE singleton = 1",
+ )
+ .fetch_one(&mut *transaction)
+ .await
+ .expect("advanced retained generation count");
+ assert_eq!(retained_count, 8);
+
+ let duplicate_error = sqlx::query(
+ "INSERT INTO radroots_event_store_source_generation(source_generation, generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq) SELECT source_generation, generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq FROM radroots_event_store_source_generation ORDER BY generation_ordinal DESC LIMIT 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect_err("duplicate generation remains a v2 conflict at the limit");
+ assert!(matches!(
+ duplicate_error,
+ sqlx::Error::Database(ref database)
+ if database.message().contains("source generation already exists")
+ && !database.message().contains("generation limit reached")
+ ));
+
+ let error = sqlx::query(
+ "INSERT INTO radroots_event_store_source_generation(source_generation, generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq) SELECT ?, generation_ordinal + 1, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq FROM radroots_event_store_source_generation ORDER BY generation_ordinal DESC LIMIT 1",
+ )
+ .bind(vec![0xa5_u8; 32])
+ .execute(&mut *transaction)
+ .await
+ .expect_err("unique generation append must hit the SQL capacity backstop");
+ assert!(matches!(
+ error,
+ sqlx::Error::Database(ref database)
+ if database.message().contains("retained source generation limit reached")
+ ));
+ transaction
+ .rollback()
+ .await
+ .expect("roll back SQL backstop fixture");
+ }
+
+ #[tokio::test]
+ async fn marker_close_sql_backstop_rejects_each_required_seal_drift() {
+ const MARKER_CLOSE_ERROR: &str = "event-store rebuild marker cannot close before capacity, NIP-09, and FoodAvailability seals agree";
+
+ for drift in ["capacity", "nip09", "food", "fts"] {
+ let store = RadrootsEventStore::open_memory().await.expect("open store");
+ let capacity_before = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity before marker fixture");
+ let derived_seals_before: (i64, i64, i64) = sqlx::query_as(
+ "SELECT integrity.last_transition_seq, integrity.transition_count, cursor.projected_row_count FROM radroots_event_store_addressable_feed_integrity_v1 AS integrity JOIN radroots_event_store_source_state AS source ON source.active_generation = integrity.source_generation JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.source_generation = source.active_generation WHERE source.singleton = 1 AND cursor.singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("derived seals before marker fixture");
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("marker fixture transaction");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_rebuild_marker_insert_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove marker insert guard in rolled-back fixture");
+ sqlx::query(
+ "INSERT INTO radroots_event_store_source_rebuild_marker(singleton, barrier_key, target_generation, target_generation_ordinal, reconciliation_version, addressable_feed_version, event_contract_registry_version, hook_id, hook_manifest_sha256, transition_floor_seq, baseline_raw_event_count, baseline_raw_tag_count, baseline_raw_high_water_seq, prior_active_generation, prior_raw_event_count, prior_raw_tag_count, prior_raw_high_water_seq, prior_last_transition_seq) SELECT 1, 1, generation.source_generation, generation.generation_ordinal, generation.reconciliation_version, generation.addressable_feed_version, generation.event_contract_registry_version, generation.hook_id, generation.hook_manifest_sha256, generation.transition_floor_seq, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, state.active_generation, state.raw_event_count, state.raw_tag_count, state.raw_high_water_seq, state.last_transition_seq FROM radroots_event_store_source_generation AS generation JOIN radroots_event_store_source_state AS state ON state.active_generation = generation.source_generation WHERE state.singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("install completed synthetic marker");
+ match drift {
+ "capacity" => {
+ sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_update_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove capacity guard in rolled-back fixture");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_capacity_v1 SET raw_event_bytes = raw_event_bytes + 1 WHERE singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("corrupt capacity byte seal");
+ }
+ "nip09" => {
+ sqlx::query(
+ "UPDATE radroots_event_store_addressable_feed_integrity_v1 SET last_transition_seq = last_transition_seq + 1, transition_count = transition_count + 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("corrupt NIP-09 feed-integrity seal");
+ }
+ "food" => {
+ sqlx::query(
+ "DROP TRIGGER radroots_event_store_food_availability_cursor_update_guard",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("remove Food cursor guard in rolled-back fixture");
+ sqlx::query(
+ "UPDATE radroots_event_store_food_availability_cursor SET projected_row_count = projected_row_count + 1 WHERE singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("corrupt Food cursor/projection seal");
+ }
+ "fts" => {
+ sqlx::query(
+ "INSERT INTO radroots_event_store_food_availability_search_fts(rowid, event_id, pubkey, d_tag, title, summary, content, location) VALUES (1, 'fixture', 'fixture', 'fixture', 'fixture', '', '', '')",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("corrupt FTS seal");
+ }
+ _ => unreachable!("bounded drift fixture"),
+ }
+
+ let error = sqlx::query(
+ "DELETE FROM radroots_event_store_source_rebuild_marker WHERE singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect_err("marker close must reject inconsistent seals");
+ assert!(matches!(
+ error,
+ sqlx::Error::Database(ref database) if database.message() == MARKER_CLOSE_ERROR
+ ));
+ transaction
+ .rollback()
+ .await
+ .expect("roll back marker corruption fixture");
+
+ assert_eq!(
+ store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after rollback"),
+ capacity_before
+ );
+ let residue: (i64, i64) = sqlx::query_as(
+ "SELECT (SELECT COUNT(*) FROM radroots_event_store_source_rebuild_marker), (SELECT COUNT(*) FROM radroots_event_store_food_availability_search_fts)",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("marker and FTS residue counts");
+ assert_eq!(residue, (0, 0));
+ let derived_seals_after: (i64, i64, i64) = sqlx::query_as(
+ "SELECT integrity.last_transition_seq, integrity.transition_count, cursor.projected_row_count FROM radroots_event_store_addressable_feed_integrity_v1 AS integrity JOIN radroots_event_store_source_state AS source ON source.active_generation = integrity.source_generation JOIN radroots_event_store_food_availability_cursor AS cursor ON cursor.source_generation = source.active_generation WHERE source.singleton = 1 AND cursor.singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("derived seals after marker rollback");
+ assert_eq!(derived_seals_after, derived_seals_before);
+ }
+ }
+
+ #[tokio::test]
+ async fn reopen_full_measure_detects_every_persisted_capacity_dimension() {
+ for (index, capacity_assignment, source_assignment) in [
+ (0_u8, "raw_event_count = 1", Some("raw_event_count = 1")),
+ (1, "raw_tag_count = 1", Some("raw_tag_count = 1")),
+ (2, "raw_event_bytes = 1", None),
+ (3, "raw_tag_bytes = 1", None),
+ ] {
+ let directory = tempfile::tempdir().expect("temporary directory");
+ let path = directory.path().join(format!("capacity-{index}.sqlite"));
+ let store = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("open file store");
+ let capacity_guard: String = sqlx::query_scalar(
+ "SELECT sql FROM main.sqlite_schema WHERE type = 'trigger' AND name = 'radroots_event_store_source_capacity_update_guard'",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("capacity update guard SQL");
+ let source_guard: String = sqlx::query_scalar(
+ "SELECT sql FROM main.sqlite_schema WHERE type = 'trigger' AND name = 'radroots_event_store_source_state_authority_update_guard'",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("source-state update guard SQL");
+
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("corruption fixture transaction");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_update_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove capacity guard");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_state_authority_update_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove source-state guard");
+ if let Some(source_assignment) = source_assignment {
+ sqlx::query(sqlx::AssertSqlSafe(format!(
+ "UPDATE radroots_event_store_source_state SET {source_assignment} WHERE singleton = 1"
+ )))
+ .execute(&mut *transaction)
+ .await
+ .expect("corrupt source-state count seal");
+ }
+ sqlx::query(sqlx::AssertSqlSafe(format!(
+ "UPDATE radroots_event_store_source_capacity_v1 SET {capacity_assignment} WHERE singleton = 1"
+ )))
+ .execute(&mut *transaction)
+ .await
+ .expect("corrupt persisted capacity seal");
+ sqlx::raw_sql(sqlx::AssertSqlSafe(source_guard))
+ .execute(&mut *transaction)
+ .await
+ .expect("restore exact source-state guard");
+ sqlx::raw_sql(sqlx::AssertSqlSafe(capacity_guard))
+ .execute(&mut *transaction)
+ .await
+ .expect("restore exact capacity guard");
+ transaction
+ .commit()
+ .await
+ .expect("commit corruption fixture");
+ store.pool().close().await;
+ drop(store);
+
+ assert!(matches!(
+ RadrootsEventStore::open_file(&path).await,
+ Err(RadrootsEventStoreError::SourceCapacityStateDrift { .. })
+ ));
+ }
+ }
+
+ #[tokio::test]
+ async fn reopen_stops_at_the_first_raw_event_one_over_before_ephemeral_probe() {
+ let directory = tempfile::tempdir().expect("temporary directory");
+ let path = directory.path().join("bounded-one-over.sqlite");
+ let store = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("open file store");
+ let capacity_guard: String = sqlx::query_scalar(
+ "SELECT sql FROM main.sqlite_schema WHERE type = 'trigger' AND name = 'radroots_event_store_source_capacity_update_guard'",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("capacity update guard SQL");
+ let source_guard: String = sqlx::query_scalar(
+ "SELECT sql FROM main.sqlite_schema WHERE type = 'trigger' AND name = 'radroots_event_store_source_state_authority_update_guard'",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("source-state update guard SQL");
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("corruption fixture transaction");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_update_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove capacity guard");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_state_authority_update_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove source-state guard");
+ sqlx::query(
+ "WITH RECURSIVE fixture(value) AS (VALUES(1) UNION ALL SELECT value + 1 FROM fixture WHERE value < 25001) INSERT INTO event_envelopes(seq, event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) SELECT value, printf('%064x', value), printf('%064x', 0), value, CASE WHEN value = 25001 THEN 20000 ELSE 1 END, '[]', '', printf('%0128x', value), '{}', 'verified', 'unsupported', NULL, CASE WHEN value = 25001 THEN 'ephemeral' ELSE 'regular' END, 0, value, value FROM fixture",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("install one-over raw authority with a trailing ephemeral row");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET raw_event_count = 25000, raw_high_water_seq = 25001 WHERE singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("seal source state at the accepted count");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_capacity_v1 SET raw_event_count = 25000, raw_high_water_seq = 25001 WHERE singleton = 1",
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("seal capacity at the accepted count");
+ sqlx::raw_sql(sqlx::AssertSqlSafe(source_guard))
+ .execute(&mut *transaction)
+ .await
+ .expect("restore exact source-state guard");
+ sqlx::raw_sql(sqlx::AssertSqlSafe(capacity_guard))
+ .execute(&mut *transaction)
+ .await
+ .expect("restore exact capacity guard");
+ transaction
+ .commit()
+ .await
+ .expect("commit corrupt one-over fixture");
+ store.pool().close().await;
+ drop(store);
+
+ let error = match RadrootsEventStore::open_file(&path).await {
+ Ok(_) => panic!("bounded reopen accepted the first row over capacity"),
+ Err(error) => error,
+ };
+ assert!(
+ matches!(
+ &error,
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ current: 25_000,
+ requested: 1,
+ limit: 25_000,
+ }
+ ),
+ "unexpected bounded reopen error: {error:?}"
+ );
+ }
+}
diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs
@@ -54,12 +54,15 @@ use crate::model::{
#[cfg(test)]
use crate::nip09::reconciliation_v1::ReconciliationProfile;
use crate::nip09::reconciliation_v1::{active_source_generation, generation_from_blob};
-#[cfg(test)]
-use crate::schema::destroy_event_store_schema_for_test;
use crate::schema::{
RadrootsEventStoreSchemaStatus, inspect_event_store_schema_status, migrate_event_store_schema,
rollback_event_store_schema_offline,
};
+#[cfg(test)]
+use crate::schema::{
+ destroy_event_store_schema_for_test,
+ rollback_event_store_schema_offline_destructive_for_migration_test,
+};
use radroots_event::event_head::v1::RadrootsEventHeadCoordinate;
#[cfg(test)]
use radroots_event::event_head::v1::{
@@ -190,6 +193,22 @@ impl RadrootsEventStore {
Ok(generation)
}
+ /// Returns the fast persisted raw-source capacity seal for one snapshot.
+ ///
+ /// This validates the seal against active source and generation metadata
+ /// without rescanning raw rows. Migration and database reopen perform the
+ /// full raw-source recount.
+ pub async fn source_capacity_v1(
+ &self,
+ ) -> Result<crate::RadrootsEventStoreSourceCapacityV1, RadrootsEventStoreError> {
+ let mut tx = self.pool.begin().await?;
+ let capacity =
+ crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1(&mut tx)
+ .await?;
+ tx.commit().await?;
+ Ok(capacity)
+ }
+
/// Begins a serialized write transaction suitable for composed event-store writes.
///
/// Call this before performing any reads that will precede
@@ -1168,6 +1187,7 @@ async fn configure_pool(
filename: main_filename,
});
}
+ validate_main_database_encoding(connection).await?;
crate::schema::validate_event_store_temp_schema(connection).await?;
}
@@ -1195,6 +1215,18 @@ async fn configure_pool(
Ok(())
}
+async fn validate_main_database_encoding(
+ connection: &mut SqliteConnection,
+) -> Result<(), RadrootsEventStoreError> {
+ let actual: String = sqlx::query_scalar("PRAGMA main.encoding")
+ .fetch_one(&mut *connection)
+ .await?;
+ if actual == "UTF-8" {
+ return Ok(());
+ }
+ Err(RadrootsEventStoreError::SqliteMainDatabaseEncodingNotUtf8 { actual })
+}
+
async fn configure_file_journal_mode(
connection: &mut SqliteConnection,
) -> Result<(), RadrootsEventStoreError> {
@@ -1772,8 +1804,12 @@ mod tests {
RadrootsTradeMutationBodyV1, RadrootsTradeMutationEnvelopeV1, canonical_jcs_value,
canonical_trade_mutation_content,
};
- use radroots_event::wire::{RadrootsNip01EventWire, compute_canonical_nip01_event_id};
+ use radroots_event::wire::{
+ DEFAULT_CONTENT_MAX_BYTES, DEFAULT_RAW_JSON_MAX_BYTES, RadrootsNip01EventWire,
+ compute_canonical_nip01_event_id,
+ };
use radroots_event_codec::food_availability::inbound::RadrootsFoodAvailabilityImageDiagnostic;
+ use std::sync::Arc;
const FIXTURE_ALICE_SECRET_KEY_HEX: &str =
"10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
@@ -1803,6 +1839,17 @@ mod tests {
}
}
+ struct PanickingGeneration;
+
+ impl crate::nip09::reconciliation_v1::SourceGenerationProvider for PanickingGeneration {
+ fn fill_generation(
+ &self,
+ _generation: &mut [u8; 32],
+ ) -> Result<(), RadrootsEventStoreError> {
+ panic!("generation entropy was requested after the retained-history preflight")
+ }
+ }
+
fn fixture_keys() -> RadrootsNostrKeys {
let secret_key =
RadrootsNostrSecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("secret key");
@@ -2044,6 +2091,89 @@ mod tests {
RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
}
+ fn raw_source_text_bytes(event: &RadrootsSignedEvent) -> (u64, u64) {
+ let tags = event.tags_as_vec();
+ let tags_json = serde_json::to_string(&tags).expect("tags JSON");
+ let event_bytes = [
+ event.id_str(),
+ event.pubkey_str(),
+ tags_json.as_str(),
+ event.content(),
+ event.sig_str(),
+ event.raw_json(),
+ ]
+ .into_iter()
+ .map(str::len)
+ .sum::<usize>();
+ let tag_bytes = tags
+ .iter()
+ .map(|tag| {
+ let tag_json = serde_json::to_string(tag).expect("tag JSON");
+ event.id_str().len()
+ + tag.first().map_or(0, String::len)
+ + tag.get(1).map_or(0, String::len)
+ + tag_json.len()
+ })
+ .sum::<usize>();
+ (
+ u64::try_from(event_bytes).expect("event byte count fits u64"),
+ u64::try_from(tag_bytes).expect("tag byte count fits u64"),
+ )
+ }
+
+ async fn initialize_utf16le_database(path: &Path) {
+ 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-16LE before schema creation");
+ sqlx::query("CREATE TABLE encoding_anchor (value TEXT NOT NULL)")
+ .execute(&mut connection)
+ .await
+ .expect("materialize UTF-16LE database");
+ sqlx::query("DROP TABLE encoding_anchor")
+ .execute(&mut connection)
+ .await
+ .expect("return UTF-16LE database to empty catalog");
+ let actual: String = sqlx::query_scalar("PRAGMA main.encoding")
+ .fetch_one(&mut connection)
+ .await
+ .expect("read fixture encoding");
+ assert_eq!(actual, "UTF-16le");
+ connection.close().await.expect("close UTF-16 fixture");
+ }
+
+ async fn assert_utf16le_database_was_not_mutated(path: &Path) {
+ let mut connection =
+ SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(path))
+ .await
+ .expect("UTF-16 verification connection");
+ let encoding: String = sqlx::query_scalar("PRAGMA main.encoding")
+ .fetch_one(&mut connection)
+ .await
+ .expect("verification encoding");
+ let journal_mode: String = sqlx::query_scalar("PRAGMA main.journal_mode")
+ .fetch_one(&mut connection)
+ .await
+ .expect("verification journal mode");
+ let event_store_objects: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM main.sqlite_schema WHERE name = 'radroots_event_store_schema_migrations' OR name = 'event_envelopes' OR name LIKE 'radroots_event_store_%'",
+ )
+ .fetch_one(&mut connection)
+ .await
+ .expect("verification event-store catalog");
+ assert_eq!(encoding, "UTF-16le");
+ assert_eq!(journal_mode, "delete");
+ assert_eq!(event_store_objects, 0);
+ connection.close().await.expect("close UTF-16 verifier");
+ }
+
fn synthetic_signed_event(
kind: u32,
created_at: u64,
@@ -2236,9 +2366,9 @@ mod tests {
}
async fn rollback_store_to_v1(store: &RadrootsEventStore) {
- rollback_event_store_schema_offline(store.pool(), 1)
+ rollback_event_store_schema_offline_destructive_for_migration_test(store.pool(), 1)
.await
- .expect("rollback to v1");
+ .expect("test-only destructive rollback to v1");
assert_eq!(
inspect_event_store_schema_status(store.pool())
.await
@@ -2794,6 +2924,184 @@ mod tests {
}
#[tokio::test]
+ async fn current_v4_rebuild_rotates_capacity_and_food_authority_end_to_end() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let food = food_availability_event(
+ 210,
+ "v4-rebuild-carrots",
+ "Nantes Carrots",
+ "Fresh bunches",
+ "active",
+ Vec::new(),
+ );
+ store
+ .ingest_event(RadrootsEventIngest::new(food.clone(), 2_100))
+ .await
+ .expect("FoodAvailability ingest");
+ let before = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity before rebuild");
+ let raw_digest = raw_authority_digest(&store).await;
+ let target_generation = [0x44; 32];
+
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("rebuild transaction");
+ crate::nip09::reconciliation_v1::apply_reconciliation_hook(
+ &mut transaction,
+ &FixedGeneration(target_generation),
+ crate::nip09::reconciliation_v1::ReconciliationCapacityLimits::production(),
+ )
+ .await
+ .expect("current-v4 rebuild");
+ transaction
+ .commit()
+ .await
+ .expect("commit current-v4 rebuild");
+
+ let after = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after rebuild");
+ assert_eq!(after.source_generation().as_bytes(), &target_generation);
+ assert_eq!(after.raw_event_count(), before.raw_event_count());
+ assert_eq!(after.raw_tag_count(), before.raw_tag_count());
+ assert_eq!(after.raw_event_text_bytes(), before.raw_event_text_bytes());
+ assert_eq!(after.raw_tag_text_bytes(), before.raw_tag_text_bytes());
+ assert_eq!(after.raw_high_water_seq(), before.raw_high_water_seq());
+ assert_eq!(
+ after.retained_generation_count(),
+ before.retained_generation_count() + 1
+ );
+ assert_eq!(
+ after.retained_generation_limit(),
+ before.retained_generation_limit()
+ );
+ assert_eq!(raw_authority_digest(&store).await, raw_digest);
+ let marker_count: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM radroots_event_store_source_rebuild_marker")
+ .fetch_one(store.pool())
+ .await
+ .expect("rebuild marker count");
+ assert_eq!(marker_count, 0);
+ let cursor_generation: Vec<u8> = sqlx::query_scalar(
+ "SELECT source_generation FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("FoodAvailability cursor generation");
+ assert_eq!(cursor_generation, target_generation);
+ let projected = store
+ .food_availability_v1(
+ &RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author"),
+ &RadrootsFoodIdentifier::parse("v4-rebuild-carrots").expect("identifier"),
+ )
+ .await
+ .expect("projection lookup")
+ .expect("projection after rebuild");
+ assert_eq!(projected.event_id().as_str(), food.id_str());
+ validate_nip09_authority(&store).await;
+ store
+ .audit_food_availability_projection_v1()
+ .await
+ .expect("FoodAvailability authority after rebuild");
+ }
+
+ #[tokio::test]
+ async fn ninth_current_v4_rebuild_is_typed_and_preflight_atomic() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ for ordinal in 2_u8..=8 {
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("rebuild transaction");
+ crate::nip09::reconciliation_v1::apply_reconciliation_hook(
+ &mut transaction,
+ &FixedGeneration([ordinal; 32]),
+ crate::nip09::reconciliation_v1::ReconciliationCapacityLimits::production(),
+ )
+ .await
+ .expect("rebuild through retained generation eight");
+ transaction.commit().await.expect("commit rebuild");
+ }
+
+ let capacity_before = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity at generation limit");
+ assert_eq!(capacity_before.retained_generation_count(), 8);
+ let source_before = source_authority_snapshot(&store).await;
+ let raw_before = raw_authority_digest(&store).await;
+ let nip09_before = normalized_nip09_snapshot(&store).await;
+ let food_before: (Vec<u8>, i64, i64) = sqlx::query_as(
+ "SELECT source_generation, last_transition_seq, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("FoodAvailability cursor at generation limit");
+ let derived_before: (i64, i64, i64) = sqlx::query_as(
+ "SELECT (SELECT COUNT(*) FROM radroots_event_store_source_generation), (SELECT COUNT(*) FROM radroots_event_store_addressable_head_transition), (SELECT COUNT(*) FROM radroots_event_store_addressable_feed_integrity_v1)",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("derived authority counts at generation limit");
+ assert_eq!(derived_before.0, 8);
+
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("ninth rebuild transaction");
+ let error = crate::nip09::reconciliation_v1::apply_reconciliation_hook(
+ &mut transaction,
+ &PanickingGeneration,
+ crate::nip09::reconciliation_v1::ReconciliationCapacityLimits::production(),
+ )
+ .await
+ .expect_err("ninth rebuild must fail before entropy or mutation");
+ assert!(matches!(
+ error,
+ RadrootsEventStoreError::SourceGenerationHistoryLimitReached {
+ current: 8,
+ limit: 8,
+ }
+ ));
+ transaction
+ .commit()
+ .await
+ .expect("commit mutation-free rejected rebuild transaction");
+
+ assert_eq!(
+ store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after rejected rebuild"),
+ capacity_before
+ );
+ assert_eq!(source_authority_snapshot(&store).await, source_before);
+ assert_eq!(raw_authority_digest(&store).await, raw_before);
+ assert_eq!(normalized_nip09_snapshot(&store).await, nip09_before);
+ let food_after: (Vec<u8>, i64, i64) = sqlx::query_as(
+ "SELECT source_generation, last_transition_seq, projected_row_count FROM radroots_event_store_food_availability_cursor WHERE singleton = 1",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("FoodAvailability cursor after rejected rebuild");
+ assert_eq!(food_after, food_before);
+ let derived_after: (i64, i64, i64, i64) = sqlx::query_as(
+ "SELECT (SELECT COUNT(*) FROM radroots_event_store_source_generation), (SELECT COUNT(*) FROM radroots_event_store_addressable_head_transition), (SELECT COUNT(*) FROM radroots_event_store_addressable_feed_integrity_v1), (SELECT COUNT(*) FROM radroots_event_store_source_rebuild_marker)",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("derived authority counts after rejected rebuild");
+ assert_eq!(
+ derived_after,
+ (derived_before.0, derived_before.1, derived_before.2, 0)
+ );
+ }
+
+ #[tokio::test]
async fn food_projection_migration_backfills_v2_and_survives_rollback_reupgrade() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let food = food_availability_event(
@@ -3001,7 +3309,7 @@ mod tests {
}
);
assert_eq!(
- crate::RadrootsEventStoreReconciliationResource::RawTagBytes.as_str(),
+ crate::RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes.as_str(),
"total retained raw-source tag row text bytes"
);
let store = RadrootsEventStore::open_memory().await.expect("open");
@@ -3039,51 +3347,59 @@ mod tests {
let below_limit_cases = [
(
- crate::RadrootsEventStoreReconciliationResource::RawEvents,
+ crate::RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
crate::nip09::reconciliation_v1::ReconciliationCapacityLimits {
raw_events: 0,
..exact_limits
},
+ 0,
1,
0,
),
(
- crate::RadrootsEventStoreReconciliationResource::RawTags,
+ crate::RadrootsEventStoreSourceCapacityResourceV1::RawTags,
crate::nip09::reconciliation_v1::ReconciliationCapacityLimits {
raw_tags: 0,
..exact_limits
},
+ 0,
1,
0,
),
(
- crate::RadrootsEventStoreReconciliationResource::RawEventBytes,
+ crate::RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
crate::nip09::reconciliation_v1::ReconciliationCapacityLimits {
raw_event_bytes: exact_limits.raw_event_bytes - 1,
..exact_limits
},
+ 0,
exact_limits.raw_event_bytes,
exact_limits.raw_event_bytes - 1,
),
(
- crate::RadrootsEventStoreReconciliationResource::RawTagBytes,
+ crate::RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes,
crate::nip09::reconciliation_v1::ReconciliationCapacityLimits {
raw_tag_bytes: exact_limits.raw_tag_bytes - 1,
..exact_limits
},
+ 0,
exact_limits.raw_tag_bytes,
exact_limits.raw_tag_bytes - 1,
),
];
- for (resource, limits, expected_actual, expected_limit) in below_limit_cases {
+ for (resource, limits, expected_current, expected_requested, expected_limit) in
+ below_limit_cases
+ {
assert!(matches!(
migrate_store_with_generation_and_limits(&store, [0x67; 32], limits).await,
- Err(RadrootsEventStoreError::ReconciliationCapacityExceeded {
+ Err(RadrootsEventStoreError::SourceCapacityExceeded {
resource: actual_resource,
- actual,
+ current,
+ requested,
limit,
}) if actual_resource == resource
- && actual == expected_actual
+ && current == expected_current
+ && requested == expected_requested
&& limit == expected_limit
));
assert_eq!(
@@ -3124,6 +3440,14 @@ mod tests {
.execute(store.pool())
.await
.expect("oversized legacy pubkey");
+ let oversized_raw_event_bytes: i64 = sqlx::query_scalar(
+ "SELECT COALESCE(SUM(length(CAST(event_id AS BLOB)) + length(CAST(pubkey AS BLOB)) + length(CAST(tags_json AS BLOB)) + length(CAST(content AS BLOB)) + length(CAST(sig AS BLOB)) + length(CAST(raw_json AS BLOB))), 0) FROM event_envelopes",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("oversized raw event bytes");
+ let oversized_raw_event_bytes =
+ u64::try_from(oversized_raw_event_bytes).expect("oversized raw event bytes");
let limits = crate::nip09::reconciliation_v1::ReconciliationCapacityLimits {
raw_event_bytes: u64::try_from(prior_raw_event_bytes).expect("prior raw event bytes"),
..crate::nip09::reconciliation_v1::ReconciliationCapacityLimits::production()
@@ -3140,11 +3464,15 @@ mod tests {
);
assert!(matches!(
error,
- RadrootsEventStoreError::ReconciliationCapacityExceeded {
- resource: crate::RadrootsEventStoreReconciliationResource::RawEventBytes,
- actual,
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: crate::RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
+ current,
+ requested,
limit,
- } if actual > limit && limit == limits.raw_event_bytes
+ } if current == 0
+ && requested == oversized_raw_event_bytes
+ && requested > limit
+ && limit == limits.raw_event_bytes
));
assert_eq!(
inspect_event_store_schema_status(store.pool())
@@ -3955,6 +4283,47 @@ mod tests {
}
#[tokio::test]
+ async fn open_file_rejects_utf16_main_database_before_schema_or_journal_mutation() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let path = tempdir.path().join("open-file-utf16.sqlite");
+ initialize_utf16le_database(&path).await;
+
+ let error = match RadrootsEventStore::open_file(&path).await {
+ Ok(_) => panic!("UTF-16 main database must be rejected"),
+ Err(error) => error,
+ };
+ assert!(matches!(
+ error,
+ RadrootsEventStoreError::SqliteMainDatabaseEncodingNotUtf8 { actual }
+ if actual == "UTF-16le"
+ ));
+ assert_utf16le_database_was_not_mutated(&path).await;
+ }
+
+ #[tokio::test]
+ async fn open_pool_rejects_utf16_main_database_before_schema_or_journal_mutation() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let path = tempdir.path().join("open-pool-utf16.sqlite");
+ initialize_utf16le_database(&path).await;
+ let pool = SqlitePoolOptions::new()
+ .max_connections(2)
+ .connect_with(SqliteConnectOptions::new().filename(&path))
+ .await
+ .expect("UTF-16 pool");
+
+ let error = match RadrootsEventStore::open_pool(pool, true).await {
+ Ok(_) => panic!("UTF-16 supplied pool must be rejected"),
+ Err(error) => error,
+ };
+ assert!(matches!(
+ error,
+ RadrootsEventStoreError::SqliteMainDatabaseEncodingNotUtf8 { actual }
+ if actual == "UTF-16le"
+ ));
+ assert_utf16le_database_was_not_mutated(&path).await;
+ }
+
+ #[tokio::test]
async fn open_pool_configures_every_file_connection_and_rejects_multi_connection_memory() {
let memory_options = SqliteConnectOptions::from_str("sqlite::memory:")
.expect("memory options")
@@ -4431,7 +4800,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
let clone = store.clone();
store
- .rollback_to_schema_version_and_close(1)
+ .rollback_to_schema_version_and_close(3)
.await
.expect("terminal rollback");
@@ -4443,6 +4812,68 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
}
#[tokio::test]
+ async fn utf8_file_reopen_preserves_non_ascii_and_nul_capacity_accounting() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let path = tempdir.path().join("utf8-capacity-reopen.sqlite");
+ let store = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("UTF-8 file store");
+ let event = signed_event(
+ KIND_POST,
+ 10,
+ vec![
+ vec!["t".to_owned(), "Victoria vegetables 野菜\0".to_owned()],
+ vec!["location".to_owned(), "Victoria, B.C., Canada".to_owned()],
+ ],
+ "Café-grown carrots 🥕 in Victoria\0",
+ );
+ let event_id = event.id_str().to_owned();
+ let expected_tag_count =
+ u64::try_from(event.tags_as_vec().len()).expect("tag count fits u64");
+ let (expected_event_bytes, expected_tag_bytes) = raw_source_text_bytes(&event);
+
+ store
+ .ingest_event(RadrootsEventIngest::new(event, 1_000))
+ .await
+ .expect("non-ASCII and NUL ingest");
+ let before_reopen = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity before reopen");
+ assert_eq!(before_reopen.raw_event_count(), 1);
+ assert_eq!(before_reopen.raw_tag_count(), expected_tag_count);
+ assert_eq!(before_reopen.raw_event_text_bytes(), expected_event_bytes);
+ assert_eq!(before_reopen.raw_tag_text_bytes(), expected_tag_bytes);
+ store.pool().close().await;
+
+ let reopened = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("reopen UTF-8 file store");
+ assert_eq!(
+ reopened
+ .source_capacity_v1()
+ .await
+ .expect("capacity after reopen"),
+ before_reopen
+ );
+ let stored = reopened
+ .raw_event(&event_id)
+ .await
+ .expect("raw event after reopen")
+ .expect("stored raw event after reopen");
+ assert_eq!(stored.content, "Café-grown carrots 🥕 in Victoria\0");
+ let tags = reopened
+ .tags_for_event(&event_id)
+ .await
+ .expect("stored tags after reopen");
+ assert_eq!(tags.len(), 2);
+ assert_eq!(
+ tags[0].tag_value.as_deref(),
+ Some("Victoria vegetables 野菜\0")
+ );
+ }
+
+ #[tokio::test]
async fn ingest_retains_raw_event_and_ignores_duplicate_rows() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let event = signed_event(
@@ -4457,7 +4888,15 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
.ingest_event(ingest.clone())
.await
.expect("first ingest");
+ let capacity_after_first = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after first ingest");
let second = store.ingest_event(ingest).await.expect("second ingest");
+ let capacity_after_duplicate = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after duplicate ingest");
let stored = store
.raw_event(event.id_str())
.await
@@ -4466,6 +4905,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
assert!(first.persistence.is_inserted());
assert!(second.persistence.is_duplicate());
+ assert_eq!(capacity_after_duplicate, capacity_after_first);
assert_eq!(first.persistence.sequence(), second.persistence.sequence());
assert_eq!(first.persistence.sequence(), Some(stored.seq));
assert_eq!(
@@ -4495,6 +4935,364 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
}
#[tokio::test]
+ async fn independent_file_pools_serialize_the_last_raw_event_byte_capacity_slot() {
+ let tempdir = tempfile::tempdir().expect("tempdir");
+ let path = tempdir.path().join("capacity-last-slot-race.sqlite");
+ let contender_a = signed_event(KIND_POST, 101, Vec::new(), "race-a");
+ let contender_b = signed_event(KIND_POST, 102, Vec::new(), "race-b");
+ let contender_bytes = raw_source_text_bytes(&contender_a).0;
+ assert_eq!(raw_source_text_bytes(&contender_b).0, contender_bytes);
+
+ const FILLER_CREATED_AT_BASE: u32 = 1_000_000;
+ let filler_base = signed_event(KIND_POST, FILLER_CREATED_AT_BASE, Vec::new(), "");
+ let filler_base_bytes = raw_source_text_bytes(&filler_base).0;
+ let filler_target = crate::RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1
+ .checked_sub(contender_bytes)
+ .expect("one contender fits the production byte limit");
+ let full_content_len = DEFAULT_CONTENT_MAX_BYTES - 4_096;
+ let full_content = "v".repeat(full_content_len);
+ let full_filler = signed_event(
+ KIND_POST,
+ FILLER_CREATED_AT_BASE,
+ Vec::new(),
+ full_content.as_str(),
+ );
+ assert!(full_filler.raw_json().len() <= DEFAULT_RAW_JSON_MAX_BYTES);
+ let full_filler_bytes = raw_source_text_bytes(&full_filler).0;
+ let content_shape_for_target = |target: u64| {
+ let adjustment = target.checked_sub(filler_base_bytes)?;
+ let (ascii_len, append_nul) = if adjustment % 2 == 0 {
+ (adjustment / 2, false)
+ } else {
+ (adjustment.checked_sub(7)? / 2, true)
+ };
+ (ascii_len <= u64::try_from(full_content_len).ok()?)
+ .then_some((usize::try_from(ascii_len).ok()?, append_nul))
+ };
+
+ let mut full_filler_count = filler_target / full_filler_bytes;
+ let mut tail_total = filler_target % full_filler_bytes;
+ let tail_targets = if tail_total == 0 {
+ Vec::new()
+ } else if content_shape_for_target(tail_total).is_some() {
+ vec![tail_total]
+ } else {
+ full_filler_count = full_filler_count
+ .checked_sub(1)
+ .expect("filler target admits a two-event exact tail");
+ tail_total += full_filler_bytes;
+ let lower = filler_base_bytes.max(tail_total - full_filler_bytes);
+ let upper = full_filler_bytes.min(tail_total - filler_base_bytes);
+ let first = (lower..=upper)
+ .take(16)
+ .find(|candidate| {
+ content_shape_for_target(*candidate).is_some()
+ && content_shape_for_target(tail_total - *candidate).is_some()
+ })
+ .expect("two bounded filler events can represent the exact tail");
+ vec![first, tail_total - first]
+ };
+
+ let filler_store = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("filler store");
+ let mut filler_transaction = filler_store
+ .begin_write_transaction()
+ .await
+ .expect("filler transaction");
+ let mut filler_count = 0_u64;
+ for index in 0..full_filler_count {
+ let created_at = FILLER_CREATED_AT_BASE
+ + u32::try_from(index).expect("bounded full filler index fits u32");
+ let filler = signed_event(KIND_POST, created_at, Vec::new(), full_content.as_str());
+ assert_eq!(raw_source_text_bytes(&filler).0, full_filler_bytes);
+ filler_store
+ .ingest_event_in_transaction(
+ &mut filler_transaction,
+ RadrootsEventIngest::new(filler, 1_000 + i64::from(created_at)),
+ )
+ .await
+ .expect("coherent full filler ingest");
+ filler_count += 1;
+ }
+ for target in tail_targets {
+ let (ascii_len, append_nul) =
+ content_shape_for_target(target).expect("validated exact tail shape");
+ let mut content = "v".repeat(ascii_len);
+ if append_nul {
+ content.push('\0');
+ }
+ let created_at = FILLER_CREATED_AT_BASE
+ + u32::try_from(filler_count).expect("bounded tail filler index fits u32");
+ let filler = signed_event(KIND_POST, created_at, Vec::new(), content.as_str());
+ assert!(filler.raw_json().len() <= DEFAULT_RAW_JSON_MAX_BYTES);
+ assert_eq!(raw_source_text_bytes(&filler).0, target);
+ filler_store
+ .ingest_event_in_transaction(
+ &mut filler_transaction,
+ RadrootsEventIngest::new(filler, 1_000 + i64::from(created_at)),
+ )
+ .await
+ .expect("coherent exact-tail filler ingest");
+ filler_count += 1;
+ }
+ filler_transaction
+ .commit()
+ .await
+ .expect("commit coherent filler source");
+ let before_race = filler_store
+ .source_capacity_v1()
+ .await
+ .expect("capacity before last-slot race");
+ assert_eq!(before_race.raw_event_count(), filler_count);
+ assert_eq!(before_race.raw_event_text_bytes(), filler_target);
+ filler_store.pool().close().await;
+
+ let contender_a_id = contender_a.id_str().to_owned();
+ let contender_b_id = contender_b.id_str().to_owned();
+ let store_a = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("first independent store");
+ let store_b = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("second independent store");
+ let barrier = Arc::new(tokio::sync::Barrier::new(3));
+ let barrier_a = barrier.clone();
+ let first = tokio::spawn(async move {
+ barrier_a.wait().await;
+ let result = store_a
+ .ingest_event(RadrootsEventIngest::new(contender_a, 1_100))
+ .await;
+ (store_a, result)
+ });
+ let barrier_b = barrier.clone();
+ let second = tokio::spawn(async move {
+ barrier_b.wait().await;
+ let result = store_b
+ .ingest_event(RadrootsEventIngest::new(contender_b, 1_200))
+ .await;
+ (store_b, result)
+ });
+ barrier.wait().await;
+ let (first, second) = tokio::join!(first, second);
+ let (store_a, result_a) = first.expect("first contender task");
+ let (store_b, result_b) = second.expect("second contender task");
+ let (accepted, rejected) = match (result_a, result_b) {
+ (Ok(accepted), Err(rejected)) | (Err(rejected), Ok(accepted)) => (accepted, rejected),
+ _ => panic!("exactly one contender must consume the last capacity slot"),
+ };
+ assert!(accepted.persistence.is_inserted());
+ assert!(matches!(
+ rejected,
+ RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: crate::RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes,
+ current: crate::RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1,
+ requested,
+ limit: crate::RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1,
+ } if requested == contender_bytes
+ ));
+ store_a.pool().close().await;
+ store_b.pool().close().await;
+
+ let reopened = RadrootsEventStore::open_file(&path)
+ .await
+ .expect("clean full reopen after last-slot race");
+ let after_race = reopened
+ .source_capacity_v1()
+ .await
+ .expect("capacity after last-slot race");
+ assert_eq!(after_race.raw_event_count(), filler_count + 1);
+ assert_eq!(
+ after_race.raw_event_text_bytes(),
+ crate::RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1
+ );
+ let retained_contenders: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM event_envelopes WHERE event_id = ? OR event_id = ?",
+ )
+ .bind(contender_a_id)
+ .bind(contender_b_id)
+ .fetch_one(reopened.pool())
+ .await
+ .expect("retained contender count");
+ assert_eq!(retained_contenders, 1);
+ }
+
+ #[tokio::test]
+ async fn exact_capacity_boundary_allows_duplicate_observation_and_ephemeral_noop() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let retained = signed_event(KIND_POST, 20, Vec::new(), "retained at boundary");
+ store
+ .ingest_event(RadrootsEventIngest::new(retained.clone(), 1_000))
+ .await
+ .expect("initial durable ingest");
+
+ let source_guard: String = sqlx::query_scalar(
+ "SELECT sql FROM main.sqlite_schema WHERE type = 'trigger' AND name = 'radroots_event_store_source_state_authority_update_guard'",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("source-state update guard SQL");
+ let capacity_guard: String = sqlx::query_scalar(
+ "SELECT sql FROM main.sqlite_schema WHERE type = 'trigger' AND name = 'radroots_event_store_source_capacity_update_guard'",
+ )
+ .fetch_one(store.pool())
+ .await
+ .expect("capacity update guard SQL");
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("boundary fixture transaction");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_state_authority_update_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove source-state guard in rolled-back fixture");
+ sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_update_guard")
+ .execute(&mut *transaction)
+ .await
+ .expect("remove capacity guard in rolled-back fixture");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_state SET raw_event_count = ? WHERE singleton = 1",
+ )
+ .bind(
+ i64::try_from(crate::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1)
+ .expect("production event-count limit fits SQLite"),
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("place source state at event-count boundary");
+ sqlx::query(
+ "UPDATE radroots_event_store_source_capacity_v1 SET raw_event_count = ? WHERE singleton = 1",
+ )
+ .bind(
+ i64::try_from(crate::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1)
+ .expect("production event-count limit fits SQLite"),
+ )
+ .execute(&mut *transaction)
+ .await
+ .expect("place capacity seal at event-count boundary");
+ sqlx::raw_sql(sqlx::AssertSqlSafe(source_guard))
+ .execute(&mut *transaction)
+ .await
+ .expect("restore exact source-state guard");
+ sqlx::raw_sql(sqlx::AssertSqlSafe(capacity_guard))
+ .execute(&mut *transaction)
+ .await
+ .expect("restore exact capacity guard");
+
+ let at_limit = crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1(
+ &mut transaction,
+ )
+ .await
+ .expect("fast capacity seal at exact limit");
+ assert_eq!(
+ at_limit.raw_event_count(),
+ crate::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1
+ );
+ let duplicate_observation = RadrootsTransportObservation::new(
+ RadrootsTransportKind::Nostr,
+ "wss://capacity-boundary.example.test",
+ RadrootsTransportObservationType::Subscription,
+ 1_100,
+ )
+ .expect("duplicate observation");
+ let duplicate = store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::new(retained.clone(), 1_100)
+ .with_observation(duplicate_observation),
+ )
+ .await
+ .expect("duplicate does not charge capacity at the exact boundary");
+ assert!(duplicate.persistence.is_duplicate());
+ let observation_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM event_transport_observation WHERE event_id = ?",
+ )
+ .bind(retained.id_str())
+ .fetch_one(&mut *transaction)
+ .await
+ .expect("duplicate observation count");
+ assert_eq!(observation_count, 1);
+ assert_eq!(
+ crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1(
+ &mut transaction,
+ )
+ .await
+ .expect("capacity after duplicate"),
+ at_limit
+ );
+
+ let unique = signed_event(KIND_POST, 21, Vec::new(), "one event over boundary");
+ assert!(matches!(
+ store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::new(unique.clone(), 1_200),
+ )
+ .await,
+ Err(RadrootsEventStoreError::SourceCapacityExceeded {
+ resource: crate::RadrootsEventStoreSourceCapacityResourceV1::RawEvents,
+ current: crate::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1,
+ requested: 1,
+ limit: crate::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1,
+ })
+ ));
+ let ephemeral = signed_event(KIND_GEOCHAT, 22, Vec::new(), "live-only boundary event");
+ let ephemeral_observation = RadrootsTransportObservation::new(
+ RadrootsTransportKind::Nostr,
+ "wss://ephemeral-boundary.example.test",
+ RadrootsTransportObservationType::Subscription,
+ 1_300,
+ )
+ .expect("ephemeral observation");
+ let ephemeral_receipt = store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::new(ephemeral.clone(), 1_300)
+ .with_observation(ephemeral_observation),
+ )
+ .await
+ .expect("ephemeral event is not charged at the exact boundary");
+ assert_eq!(
+ ephemeral_receipt.persistence,
+ RadrootsEventPersistence::NotPersisted
+ );
+ let ephemeral_observation_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM event_transport_observation WHERE event_id = ?",
+ )
+ .bind(ephemeral.id_str())
+ .fetch_one(&mut *transaction)
+ .await
+ .expect("ephemeral observation count");
+ assert_eq!(ephemeral_observation_count, 0);
+ assert_eq!(
+ crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1(
+ &mut transaction,
+ )
+ .await
+ .expect("capacity after ephemeral event"),
+ at_limit
+ );
+ transaction
+ .rollback()
+ .await
+ .expect("roll back exact-bound fixture");
+ assert!(
+ store
+ .raw_event(unique.id_str())
+ .await
+ .expect("unique raw event after rollback")
+ .is_none()
+ );
+ assert!(
+ store
+ .raw_event(ephemeral.id_str())
+ .await
+ .expect("ephemeral raw event after rollback")
+ .is_none()
+ );
+ }
+
+ #[tokio::test]
async fn duplicate_preserves_immutable_classification_and_first_raw_bytes() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let first_event = signed_event(
@@ -4531,6 +5329,10 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
.fetch_one(store.pool())
.await
.expect("before");
+ let capacity_before_duplicate = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity before alternate-encoding duplicate");
let receipt = store
.ingest_event(RadrootsEventIngest::new(second_event, 1_200))
@@ -4543,8 +5345,13 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
.fetch_one(store.pool())
.await
.expect("after");
+ let capacity_after_duplicate = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after alternate-encoding duplicate");
assert!(receipt.persistence.is_duplicate());
+ assert_eq!(capacity_after_duplicate, capacity_before_duplicate);
assert_eq!(
receipt.admission_status,
RadrootsEventAdmissionStatus::Admitted
@@ -4779,6 +5586,10 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
)
.await
.expect("prior caller work");
+ let capacity_after_prior =
+ crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1(&mut tx)
+ .await
+ .expect("capacity after prior caller work");
let error = store
.ingest_event_in_transaction(
&mut tx,
@@ -4791,6 +5602,11 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
RadrootsEventStoreError::MigrationHookStateDrift { ref reason, .. }
if reason.contains("post-core extensions changed protocol-owned authority")
));
+ let capacity_after_rollback =
+ crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1(&mut tx)
+ .await
+ .expect("capacity after failed nested ingest");
+ assert_eq!(capacity_after_rollback, capacity_after_prior);
tx.commit()
.await
.expect("caller may commit prior work after failed ingest");
@@ -4855,6 +5671,13 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
.expect("transition count");
assert_eq!(trade_mutation_count, 0);
assert_eq!(transition_count, 0);
+ assert_eq!(
+ store
+ .source_capacity_v1()
+ .await
+ .expect("committed capacity after rollback"),
+ capacity_after_prior
+ );
}
#[tokio::test]
@@ -5306,6 +6129,10 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
async fn unsupported_verified_events_are_stored_but_not_projected() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let event = signed_event(999, 11, Vec::new(), "unsupported");
+ let capacity_before = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity before unsupported ingest");
let receipt = store
.ingest_event(RadrootsEventIngest::new(event.clone(), 2_000))
.await
@@ -5315,6 +6142,10 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
.await
.expect("get")
.expect("stored");
+ let capacity_after_insert = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after unsupported ingest");
assert_eq!(
receipt.admission_status,
@@ -5326,6 +6157,21 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
RadrootsEventAdmissionStatus::Unsupported
);
assert!(!stored.valid_stream_eligible);
+ assert_eq!(
+ capacity_after_insert.raw_event_count(),
+ capacity_before.raw_event_count() + 1
+ );
+ assert_eq!(
+ capacity_after_insert.raw_tag_count(),
+ capacity_before.raw_tag_count()
+ );
+ assert!(
+ capacity_after_insert.raw_event_text_bytes() > capacity_before.raw_event_text_bytes()
+ );
+ assert_eq!(
+ capacity_after_insert.raw_tag_text_bytes(),
+ capacity_before.raw_tag_text_bytes()
+ );
assert!(
store
.valid_event(event.id_str())
@@ -5338,7 +6184,12 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
.ingest_event(RadrootsEventIngest::new(event, 2_100))
.await
.expect("duplicate");
+ let capacity_after_duplicate = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after unsupported duplicate");
assert!(duplicate.persistence.is_duplicate());
+ assert_eq!(capacity_after_duplicate, capacity_after_insert);
assert_eq!(
duplicate.raw_head_decision,
RadrootsRawHeadDecision::NotHeadSelected
@@ -5445,6 +6296,10 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
#[tokio::test]
async fn ephemeral_admission_outcomes_are_never_persisted() {
let store = RadrootsEventStore::open_memory().await.expect("open");
+ let capacity_before = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity before ephemeral ingests");
let admitted = signed_event(KIND_GEOCHAT, 15, Vec::new(), "hello");
let unsupported = signed_event(29_999, 16, Vec::new(), "unsupported");
let invalid = signed_event(KIND_RELAY_AUTH, 17, Vec::new(), "not-json");
@@ -5543,6 +6398,13 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
assert_eq!(status.valid_stream_events, 0);
assert_eq!(status.transport_observations, 0);
assert_eq!(
+ store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after ephemeral ingests"),
+ capacity_before
+ );
+ assert_eq!(
admitted_receipt.raw_head_decision,
RadrootsRawHeadDecision::NotPersisted
);
@@ -5621,6 +6483,10 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
#[tokio::test]
async fn ambiguous_classified_listing_shape_is_invalid_but_still_updates_the_raw_head() {
let store = RadrootsEventStore::open_memory().await.expect("open");
+ let capacity_before = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity before invalid durable ingest");
let event = signed_event(
KIND_CLASSIFIED_LISTING,
17,
@@ -5637,6 +6503,10 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
.ingest_event(RadrootsEventIngest::new(event.clone(), 2_275))
.await
.expect("ingest");
+ let capacity_after = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after invalid durable ingest");
assert_eq!(
receipt.admission_status,
@@ -5648,6 +6518,16 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);",
);
assert!(!receipt.valid_stream_eligible);
assert_eq!(receipt.raw_head_decision, RadrootsRawHeadDecision::Applied);
+ assert_eq!(
+ capacity_after.raw_event_count(),
+ capacity_before.raw_event_count() + 1
+ );
+ assert_eq!(
+ capacity_after.raw_tag_count(),
+ capacity_before.raw_tag_count() + 3
+ );
+ assert!(capacity_after.raw_event_text_bytes() > capacity_before.raw_event_text_bytes());
+ assert!(capacity_after.raw_tag_text_bytes() > capacity_before.raw_tag_text_bytes());
assert!(
store
.raw_event(event.id_str())
diff --git a/crates/event_store/src/store/protocol_reconciliation_v1.rs b/crates/event_store/src/store/protocol_reconciliation_v1.rs
@@ -9,6 +9,10 @@ use crate::nip09::reconciliation_v1::{
EventAdmission, ReconciliationProfile, generation_from_blob,
persist_event_coordinate_after_insert, synchronize_after_insert, validate_source_raw_authority,
};
+use crate::source_maintenance_v1::{
+ advance_source_capacity_after_insert_v1, preflight_unique_raw_source_append_v1,
+ raw_source_capacity_delta_v1, validate_source_capacity_authority_fast_v1,
+};
use radroots_event::contract::registry_v7::RadrootsEventContract;
use radroots_event::envelope::{RadrootsEventEnvelope, RadrootsEventKindClass};
use radroots_event::event_head::v1::{
@@ -54,8 +58,12 @@ struct ProtocolPostExtensionAuthoritySeal {
baseline_raw_high_water_seq: i64,
raw_event_count: i64,
raw_tag_count: i64,
+ raw_event_bytes: u64,
+ raw_tag_bytes: u64,
raw_high_water_seq: i64,
last_transition_seq: i64,
+ retained_generation_count: u32,
+ retained_generation_limit: u32,
actual_raw_high_water_seq: i64,
global_transition_min_seq: Option<i64>,
global_transition_max_seq: Option<i64>,
@@ -71,6 +79,7 @@ pub(super) async fn ingest_event_protocol_reconciliation_v1(
) -> Result<ProtocolReconciliationV1IngestResult, RadrootsEventStoreError> {
acquire_event_store_write_lock(tx).await?;
let profile = validate_source_raw_authority(tx).await?;
+ validate_source_capacity_authority_fast_v1(tx).await?;
let event = ingest.event();
let admission = EventAdmission::for_profile(profile, ingest.verified_event())?;
let kind_class = event.kind_class();
@@ -95,6 +104,18 @@ pub(super) async fn ingest_event_protocol_reconciliation_v1(
let tags = event.tags_as_vec();
let tags_json = serde_json::to_string(&tags)?;
let event_id = event.id_str().to_owned();
+ let existing_raw_event: i64 =
+ sqlx::query_scalar("SELECT EXISTS (SELECT 1 FROM event_envelopes WHERE event_id = ?)")
+ .bind(event_id.as_str())
+ .fetch_one(&mut **tx)
+ .await?;
+ let capacity_delta = if existing_raw_event == 0 {
+ let delta = raw_source_capacity_delta_v1(ingest, tags_json.as_str())?;
+ preflight_unique_raw_source_append_v1(tx, delta).await?;
+ Some(delta)
+ } else {
+ None
+ };
let insert = insert_raw_event(
tx,
ingest,
@@ -113,6 +134,12 @@ pub(super) async fn ingest_event_protocol_reconciliation_v1(
.await?
.decision;
if inserted {
+ let capacity_delta =
+ capacity_delta.ok_or_else(|| RadrootsEventStoreError::SourceCapacityStateDrift {
+ reason: format!(
+ "unique raw event `{event_id}` was inserted after duplicate preflight"
+ ),
+ })?;
synchronize_after_insert(
tx,
ingest,
@@ -123,6 +150,7 @@ pub(super) async fn ingest_event_protocol_reconciliation_v1(
&raw_head_decision,
)
.await?;
+ advance_source_capacity_after_insert_v1(tx, capacity_delta, insert.seq).await?;
}
let post_extension_authority_seal = read_protocol_post_extension_authority_seal(tx).await?;
@@ -149,6 +177,7 @@ pub(super) async fn ingest_event_protocol_reconciliation_v1(
async fn read_protocol_post_extension_authority_seal(
tx: &mut Transaction<'_, Sqlite>,
) -> Result<ProtocolPostExtensionAuthoritySeal, RadrootsEventStoreError> {
+ let source_capacity = validate_source_capacity_authority_fast_v1(tx).await?;
let actual_raw_high_water_seq: i64 =
sqlx::query_scalar("SELECT seq FROM event_envelopes ORDER BY seq DESC LIMIT 1")
.fetch_optional(&mut **tx)
@@ -271,8 +300,12 @@ async fn read_protocol_post_extension_authority_seal(
baseline_raw_high_water_seq: source_row.try_get("baseline_raw_high_water_seq")?,
raw_event_count,
raw_tag_count,
+ raw_event_bytes: source_capacity.raw_event_text_bytes(),
+ raw_tag_bytes: source_capacity.raw_tag_text_bytes(),
raw_high_water_seq,
last_transition_seq,
+ retained_generation_count: source_capacity.retained_generation_count(),
+ retained_generation_limit: source_capacity.retained_generation_limit(),
actual_raw_high_water_seq,
global_transition_min_seq,
global_transition_max_seq,
@@ -315,8 +348,12 @@ fn protocol_post_extension_authority_matches(
baseline_raw_high_water_seq: expected_baseline_raw_high_water_seq,
raw_event_count: expected_raw_event_count,
raw_tag_count: expected_raw_tag_count,
+ raw_event_bytes: expected_raw_event_bytes,
+ raw_tag_bytes: expected_raw_tag_bytes,
raw_high_water_seq: expected_raw_high_water_seq,
last_transition_seq: expected_last_transition_seq,
+ retained_generation_count: expected_retained_generation_count,
+ retained_generation_limit: expected_retained_generation_limit,
actual_raw_high_water_seq: expected_actual_raw_high_water_seq,
global_transition_min_seq: expected_global_transition_min_seq,
global_transition_max_seq: expected_global_transition_max_seq,
@@ -339,8 +376,12 @@ fn protocol_post_extension_authority_matches(
baseline_raw_high_water_seq: actual_baseline_raw_high_water_seq,
raw_event_count: actual_state_raw_event_count,
raw_tag_count: actual_state_raw_tag_count,
+ raw_event_bytes: actual_raw_event_bytes,
+ raw_tag_bytes: actual_raw_tag_bytes,
raw_high_water_seq: actual_state_raw_high_water_seq,
last_transition_seq: actual_last_transition_seq,
+ retained_generation_count: actual_retained_generation_count,
+ retained_generation_limit: actual_retained_generation_limit,
actual_raw_high_water_seq: actual_observed_raw_high_water_seq,
global_transition_min_seq: actual_global_transition_min_seq,
global_transition_max_seq: actual_global_transition_max_seq,
@@ -363,8 +404,12 @@ fn protocol_post_extension_authority_matches(
&& expected_baseline_raw_high_water_seq == actual_baseline_raw_high_water_seq
&& expected_raw_event_count == actual_state_raw_event_count
&& expected_raw_tag_count == actual_state_raw_tag_count
+ && expected_raw_event_bytes == actual_raw_event_bytes
+ && expected_raw_tag_bytes == actual_raw_tag_bytes
&& expected_raw_high_water_seq == actual_state_raw_high_water_seq
&& expected_last_transition_seq == actual_last_transition_seq
+ && expected_retained_generation_count == actual_retained_generation_count
+ && expected_retained_generation_limit == actual_retained_generation_limit
&& expected_actual_raw_high_water_seq == actual_observed_raw_high_water_seq
&& expected_global_transition_min_seq == actual_global_transition_min_seq
&& expected_global_transition_max_seq == actual_global_transition_max_seq
diff --git a/crates/event_store/tests/fixtures/source_maintenance.v1.json b/crates/event_store/tests/fixtures/source_maintenance.v1.json
@@ -0,0 +1,408 @@
+{
+ "schema_version": 1,
+ "contract_id": "radroots_event_store.source_maintenance_v1",
+ "capacity_version": 1,
+ "limits": {
+ "raw_events": 25000,
+ "raw_tags": 250000,
+ "raw_event_text_bytes": 67108864,
+ "raw_tag_text_bytes": 33554432,
+ "retained_source_generations": 8
+ },
+ "accounting": {
+ "algorithm": "sqlite_cast_blob_octet_sum_v1",
+ "raw_event_columns": [
+ "event_id",
+ "pubkey",
+ "tags_json",
+ "content",
+ "sig",
+ "raw_json"
+ ],
+ "raw_tag_columns": [
+ "event_id",
+ "tag_name",
+ "tag_value",
+ "tag_json"
+ ],
+ "nullable_raw_tag_columns": [
+ "tag_value"
+ ]
+ },
+ "cases": [
+ {
+ "id": "fresh_store_zero_authority",
+ "execution": "direct_executor",
+ "authority": "source_maintenance_v1_result_vector",
+ "authority_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "durable_unique_append_updates_all_dimensions",
+ "execution": "direct_executor",
+ "authority": "source_maintenance_v1_result_vector",
+ "authority_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "duplicate_at_exact_boundary_is_idempotent",
+ "execution": "delegated_rust_test",
+ "authority": "exact_capacity_boundary_allows_duplicate_observation_and_ephemeral_noop",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": "raw_events",
+ "boundary": "exact",
+ "expected_outcome": "accepted_without_capacity_delta",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "ephemeral_consumes_no_capacity",
+ "execution": "direct_executor",
+ "authority": "source_maintenance_v1_result_vector",
+ "authority_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted_without_capacity_delta",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_event_count_exact",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_events",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_event_count_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_events",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "raw_tag_count_exact",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_tags",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_tag_count_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_tags",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "raw_event_text_bytes_exact",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_event_text_bytes",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_event_text_bytes_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_event_text_bytes",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "raw_tag_text_bytes_exact",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_tag_text_bytes",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "raw_tag_text_bytes_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_tag_text_bytes",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "outer_transaction_rollback_restores_capacity",
+ "execution": "direct_executor",
+ "authority": "source_maintenance_v1_result_vector",
+ "authority_path": "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rolled_back_without_capacity_delta",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "failed_nested_ingest_rolls_back_savepoint_only",
+ "execution": "delegated_rust_test",
+ "authority": "borrowed_ingest_savepoint_rolls_back_post_core_authority_forge",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "failed_ingest_rolled_back_and_prior_caller_work_preserved",
+ "error_domain": "typed",
+ "expected_error": "MigrationHookStateDrift"
+ },
+ {
+ "id": "v3_to_v4_under_limit_succeeds",
+ "execution": "delegated_rust_test",
+ "authority": "v3_to_v4_under_limit_backfills_exact_capacity_and_preserves_source",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "under_limit",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "v3_to_v4_prior_transition_drift_is_atomic",
+ "execution": "delegated_rust_test",
+ "authority": "v3_to_v4_rejects_prior_transition_drift_atomically",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "corrupt_managed_v3",
+ "expected_outcome": "rejected_before_v4_schema_ledger_or_predecessor_trigger_mutation",
+ "error_domain": "typed",
+ "expected_error": "MigrationHookStateDrift"
+ },
+ {
+ "id": "v3_to_v4_one_over_is_atomic",
+ "execution": "delegated_rust_test",
+ "authority": "source_capacity_is_rechecked_for_every_rebuild_bound_migration",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": "raw_events",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "v3_to_v4_persisted_ephemeral_is_atomic",
+ "execution": "delegated_rust_test",
+ "authority": "v4_rejects_persisted_legacy_ephemeral_rows_atomically",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_before_mutation",
+ "error_domain": "typed",
+ "expected_error": "PersistedEphemeralRawEvent"
+ },
+ {
+ "id": "reopen_rejects_incoherent_capacity_authority",
+ "execution": "delegated_rust_test",
+ "authority": "reopen_full_measure_detects_every_persisted_capacity_dimension",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_on_reopen",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityStateDrift"
+ },
+ {
+ "id": "reopen_stops_at_first_raw_event_one_over",
+ "execution": "delegated_rust_test",
+ "authority": "reopen_stops_at_the_first_raw_event_one_over_before_ephemeral_probe",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "raw_events",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_at_scan_bound",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ },
+ {
+ "id": "retained_generation_rebuild_exact",
+ "execution": "delegated_rust_test",
+ "authority": "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": "retained_source_generations",
+ "boundary": "exact",
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "ninth_rebuild_is_typed_and_atomic",
+ "execution": "delegated_rust_test",
+ "authority": "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": "retained_source_generations",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_before_entropy_or_mutation",
+ "error_domain": "typed",
+ "expected_error": "SourceGenerationHistoryLimitReached"
+ },
+ {
+ "id": "retained_generation_sql_backstop_one_over",
+ "execution": "delegated_sql_test",
+ "authority": "generation_sql_backstop_allows_exact_append_and_is_conflict_safe_one_over",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": "retained_source_generations",
+ "boundary": "one_over",
+ "expected_outcome": "rejected_by_sql_backstop",
+ "error_domain": "sqlite_database",
+ "expected_error": "event-store retained source generation limit reached; replace and resync into a fresh store"
+ },
+ {
+ "id": "rebuild_marker_accepts_consistent_seals",
+ "execution": "delegated_rust_test",
+ "authority": "current_v4_rebuild_rotates_capacity_and_food_authority_end_to_end",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "rebuild_marker_rejects_incoherent_seals",
+ "execution": "delegated_sql_test",
+ "authority": "marker_close_sql_backstop_rejects_each_required_seal_drift",
+ "authority_path": "crates/event_store/src/source_maintenance_v1.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_by_sql_backstop",
+ "error_domain": "sqlite_database",
+ "expected_error": "event-store rebuild marker cannot close before capacity, NIP-09, and FoodAvailability seals agree"
+ },
+ {
+ "id": "v4_marker_repair_binds_exact_prior_and_floor",
+ "execution": "delegated_rust_test",
+ "authority": "v4_marker_open_allows_repairing_prior_transition_high_water_drift",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "managed_v4_rebuild",
+ "expected_outcome": "accepts_derived_high_water_repair_and_rejects_wrong_prior_or_floor",
+ "error_domain": "sqlite_database",
+ "expected_error": "exact raw and prior source authority"
+ },
+ {
+ "id": "v4_food_reset_requires_target_rotation",
+ "execution": "delegated_rust_test",
+ "authority": "v4_food_reset_requires_marker_rotation_and_preserves_target_rows",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "managed_v4_rebuild",
+ "expected_outcome": "historical_rows_deleted_only_after_rotation_and_target_rows_preserved",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "v4_down_restores_predecessor_triggers",
+ "execution": "delegated_rust_test",
+ "authority": "v4_down_restores_exact_predecessor_trigger_sql_and_fingerprint",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": null,
+ "boundary": "v4_to_v3",
+ "expected_outcome": "restored_exact_predecessor_trigger_sql_and_v3_fingerprint",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "utf16_open_file_rejected_before_mutation",
+ "execution": "delegated_rust_test",
+ "authority": "open_file_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_before_schema_or_journal_mutation",
+ "error_domain": "typed",
+ "expected_error": "SqliteMainDatabaseEncodingNotUtf8"
+ },
+ {
+ "id": "utf16_open_pool_rejected_before_mutation",
+ "execution": "delegated_rust_test",
+ "authority": "open_pool_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "rejected_before_schema_or_journal_mutation",
+ "error_domain": "typed",
+ "expected_error": "SqliteMainDatabaseEncodingNotUtf8"
+ },
+ {
+ "id": "utf8_non_ascii_nul_reopen_accounting",
+ "execution": "delegated_rust_test",
+ "authority": "utf8_file_reopen_preserves_non_ascii_and_nul_capacity_accounting",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": null,
+ "boundary": null,
+ "expected_outcome": "accepted_with_exact_capacity_after_reopen",
+ "error_domain": null,
+ "expected_error": null
+ },
+ {
+ "id": "generation_destructive_rollback_rejected",
+ "execution": "delegated_rust_test",
+ "authority": "rollback_rejects_below_floor_ahead_unmanaged_and_generation_destructive_targets",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": "retained_source_generations",
+ "boundary": null,
+ "expected_outcome": "rejected_before_mutation_with_status_and_history_preserved",
+ "error_domain": "typed",
+ "expected_error": "RollbackWouldDiscardSourceGenerationHistory"
+ },
+ {
+ "id": "generation_destructive_two_step_rollback_rejected",
+ "execution": "delegated_rust_test",
+ "authority": "rollback_cannot_bypass_generation_history_guard_through_version_three",
+ "authority_path": "crates/event_store/src/schema.rs",
+ "resource": "retained_source_generations",
+ "boundary": null,
+ "expected_outcome": "rejected_before_mutation_after_history_preserving_intermediate_rollback",
+ "error_domain": "typed",
+ "expected_error": "RollbackWouldDiscardSourceGenerationHistory"
+ },
+ {
+ "id": "independent_pool_last_byte_slot_race",
+ "execution": "delegated_rust_test",
+ "authority": "independent_file_pools_serialize_the_last_raw_event_byte_capacity_slot",
+ "authority_path": "crates/event_store/src/store.rs",
+ "resource": "raw_event_text_bytes",
+ "boundary": "exact",
+ "expected_outcome": "exactly_one_accepted_one_typed_rejection_and_clean_reopen",
+ "error_domain": "typed",
+ "expected_error": "SourceCapacityExceeded"
+ }
+ ]
+}
diff --git a/crates/event_store/tests/source_maintenance_v1_result_vector.rs b/crates/event_store/tests/source_maintenance_v1_result_vector.rs
@@ -0,0 +1,632 @@
+#![forbid(unsafe_code)]
+
+use nostr::{EventBuilder, Keys, Kind, SecretKey, Tag, TagKind, Timestamp};
+use radroots_event_store::{
+ RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1,
+ RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1,
+ RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1, RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1,
+ RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1, RadrootsEventIngest,
+ RadrootsEventStore,
+};
+use serde::Deserialize;
+use std::collections::BTreeSet;
+
+const RESULT_VECTOR_EXECUTOR_ID: &str =
+ "radroots_event_store.source_maintenance_v1.result_vector_executor.v1";
+const RESULT_VECTOR_BYTES: &[u8] =
+ include_bytes!("../../../contracts/conformance/vectors/event_store/source_maintenance.v1.json");
+const FIXTURE_SECRET_KEY_HEX: &str =
+ "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct SourceMaintenanceVector {
+ schema_version: u32,
+ contract_id: String,
+ capacity_version: u32,
+ limits: CapacityLimits,
+ accounting: CapacityAccounting,
+ cases: Vec<VectorCase>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct CapacityLimits {
+ raw_events: u64,
+ raw_tags: u64,
+ raw_event_text_bytes: u64,
+ raw_tag_text_bytes: u64,
+ retained_source_generations: u32,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct CapacityAccounting {
+ algorithm: String,
+ raw_event_columns: Vec<String>,
+ raw_tag_columns: Vec<String>,
+ nullable_raw_tag_columns: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct VectorCase {
+ id: String,
+ execution: String,
+ authority: String,
+ authority_path: String,
+ resource: Option<String>,
+ boundary: Option<String>,
+ expected_outcome: String,
+ error_domain: Option<String>,
+ expected_error: Option<String>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct CapacityDelta {
+ raw_events: u64,
+ raw_tags: u64,
+ raw_event_text_bytes: u64,
+ raw_tag_text_bytes: u64,
+}
+
+#[derive(Clone, Copy)]
+struct ExpectedCase {
+ id: &'static str,
+ execution: &'static str,
+ authority: &'static str,
+ authority_path: &'static str,
+ error_domain: Option<&'static str>,
+}
+
+const DIRECT_EXECUTOR: &str = "direct_executor";
+const RESULT_VECTOR_EXECUTOR_TEST: &str = "source_maintenance_v1_result_vector";
+const RESULT_VECTOR_EXECUTOR_PATH: &str =
+ "crates/event_store/tests/source_maintenance_v1_result_vector.rs";
+
+const EXPECTED_CASES: &[ExpectedCase] = &[
+ ExpectedCase {
+ id: "fresh_store_zero_authority",
+ execution: DIRECT_EXECUTOR,
+ authority: RESULT_VECTOR_EXECUTOR_TEST,
+ authority_path: RESULT_VECTOR_EXECUTOR_PATH,
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "durable_unique_append_updates_all_dimensions",
+ execution: DIRECT_EXECUTOR,
+ authority: RESULT_VECTOR_EXECUTOR_TEST,
+ authority_path: RESULT_VECTOR_EXECUTOR_PATH,
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "duplicate_at_exact_boundary_is_idempotent",
+ execution: "delegated_rust_test",
+ authority: "exact_capacity_boundary_allows_duplicate_observation_and_ephemeral_noop",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "ephemeral_consumes_no_capacity",
+ execution: DIRECT_EXECUTOR,
+ authority: RESULT_VECTOR_EXECUTOR_TEST,
+ authority_path: RESULT_VECTOR_EXECUTOR_PATH,
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "raw_event_count_exact",
+ execution: "delegated_rust_test",
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "raw_event_count_one_over",
+ execution: "delegated_rust_test",
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "raw_tag_count_exact",
+ execution: "delegated_rust_test",
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "raw_tag_count_one_over",
+ execution: "delegated_rust_test",
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "raw_event_text_bytes_exact",
+ execution: "delegated_rust_test",
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "raw_event_text_bytes_one_over",
+ execution: "delegated_rust_test",
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "raw_tag_text_bytes_exact",
+ execution: "delegated_rust_test",
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "raw_tag_text_bytes_one_over",
+ execution: "delegated_rust_test",
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "outer_transaction_rollback_restores_capacity",
+ execution: DIRECT_EXECUTOR,
+ authority: RESULT_VECTOR_EXECUTOR_TEST,
+ authority_path: RESULT_VECTOR_EXECUTOR_PATH,
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "failed_nested_ingest_rolls_back_savepoint_only",
+ execution: "delegated_rust_test",
+ authority: "borrowed_ingest_savepoint_rolls_back_post_core_authority_forge",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "v3_to_v4_under_limit_succeeds",
+ execution: "delegated_rust_test",
+ authority: "v3_to_v4_under_limit_backfills_exact_capacity_and_preserves_source",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "v3_to_v4_prior_transition_drift_is_atomic",
+ execution: "delegated_rust_test",
+ authority: "v3_to_v4_rejects_prior_transition_drift_atomically",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "v3_to_v4_one_over_is_atomic",
+ execution: "delegated_rust_test",
+ authority: "source_capacity_is_rechecked_for_every_rebuild_bound_migration",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "v3_to_v4_persisted_ephemeral_is_atomic",
+ execution: "delegated_rust_test",
+ authority: "v4_rejects_persisted_legacy_ephemeral_rows_atomically",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "reopen_rejects_incoherent_capacity_authority",
+ execution: "delegated_rust_test",
+ authority: "reopen_full_measure_detects_every_persisted_capacity_dimension",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "reopen_stops_at_first_raw_event_one_over",
+ execution: "delegated_rust_test",
+ authority: "reopen_stops_at_the_first_raw_event_one_over_before_ephemeral_probe",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "retained_generation_rebuild_exact",
+ execution: "delegated_rust_test",
+ authority: "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "ninth_rebuild_is_typed_and_atomic",
+ execution: "delegated_rust_test",
+ authority: "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "retained_generation_sql_backstop_one_over",
+ execution: "delegated_sql_test",
+ authority: "generation_sql_backstop_allows_exact_append_and_is_conflict_safe_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: Some("sqlite_database"),
+ },
+ ExpectedCase {
+ id: "rebuild_marker_accepts_consistent_seals",
+ execution: "delegated_rust_test",
+ authority: "current_v4_rebuild_rotates_capacity_and_food_authority_end_to_end",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "rebuild_marker_rejects_incoherent_seals",
+ execution: "delegated_sql_test",
+ authority: "marker_close_sql_backstop_rejects_each_required_seal_drift",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ error_domain: Some("sqlite_database"),
+ },
+ ExpectedCase {
+ id: "v4_marker_repair_binds_exact_prior_and_floor",
+ execution: "delegated_rust_test",
+ authority: "v4_marker_open_allows_repairing_prior_transition_high_water_drift",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: Some("sqlite_database"),
+ },
+ ExpectedCase {
+ id: "v4_food_reset_requires_target_rotation",
+ execution: "delegated_rust_test",
+ authority: "v4_food_reset_requires_marker_rotation_and_preserves_target_rows",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "v4_down_restores_predecessor_triggers",
+ execution: "delegated_rust_test",
+ authority: "v4_down_restores_exact_predecessor_trigger_sql_and_fingerprint",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "utf16_open_file_rejected_before_mutation",
+ execution: "delegated_rust_test",
+ authority: "open_file_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "utf16_open_pool_rejected_before_mutation",
+ execution: "delegated_rust_test",
+ authority: "open_pool_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "utf8_non_ascii_nul_reopen_accounting",
+ execution: "delegated_rust_test",
+ authority: "utf8_file_reopen_preserves_non_ascii_and_nul_capacity_accounting",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: None,
+ },
+ ExpectedCase {
+ id: "generation_destructive_rollback_rejected",
+ execution: "delegated_rust_test",
+ authority: "rollback_rejects_below_floor_ahead_unmanaged_and_generation_destructive_targets",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "generation_destructive_two_step_rollback_rejected",
+ execution: "delegated_rust_test",
+ authority: "rollback_cannot_bypass_generation_history_guard_through_version_three",
+ authority_path: "crates/event_store/src/schema.rs",
+ error_domain: Some("typed"),
+ },
+ ExpectedCase {
+ id: "independent_pool_last_byte_slot_race",
+ execution: "delegated_rust_test",
+ authority: "independent_file_pools_serialize_the_last_raw_event_byte_capacity_slot",
+ authority_path: "crates/event_store/src/store.rs",
+ error_domain: Some("typed"),
+ },
+];
+
+#[tokio::test]
+async fn source_maintenance_v1_result_vector() {
+ assert_eq!(
+ RESULT_VECTOR_EXECUTOR_ID,
+ "radroots_event_store.source_maintenance_v1.result_vector_executor.v1"
+ );
+ let vector: SourceMaintenanceVector =
+ serde_json::from_slice(RESULT_VECTOR_BYTES).expect("strict SourceMaintenance vector");
+ validate_vector_header(&vector);
+ validate_case_inventory(&vector.cases);
+ let mut executed_direct_cases = BTreeSet::new();
+
+ let store = RadrootsEventStore::open_memory()
+ .await
+ .expect("open current in-memory event store");
+ let fresh = store
+ .source_capacity_v1()
+ .await
+ .expect("fresh source capacity");
+ assert_eq!(fresh.raw_event_count(), 0);
+ assert_eq!(fresh.raw_tag_count(), 0);
+ assert_eq!(fresh.raw_event_text_bytes(), 0);
+ assert_eq!(fresh.raw_tag_text_bytes(), 0);
+ assert_eq!(fresh.raw_high_water_seq(), 0);
+ assert_eq!(fresh.retained_generation_count(), 1);
+ assert_eq!(
+ fresh.retained_generation_limit(),
+ RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1
+ );
+ mark_direct_case(&mut executed_direct_cases, "fresh_store_zero_authority");
+
+ let durable_raw = signed_raw_json(
+ 1,
+ 1_750_000_000,
+ vec![vec!["t".to_owned(), "soil".to_owned()]],
+ "Victoria field note",
+ );
+ let expected_delta = capacity_delta(&durable_raw);
+ let first = store
+ .ingest_event(
+ RadrootsEventIngest::from_raw_json(durable_raw.clone(), 1_750_000_001)
+ .expect("verified durable fixture"),
+ )
+ .await
+ .expect("unique durable ingest");
+ assert!(first.persistence.is_inserted());
+ let after_first = store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after unique durable ingest");
+ assert_eq!(after_first.raw_event_count(), expected_delta.raw_events);
+ assert_eq!(after_first.raw_tag_count(), expected_delta.raw_tags);
+ assert_eq!(
+ after_first.raw_event_text_bytes(),
+ expected_delta.raw_event_text_bytes
+ );
+ assert_eq!(
+ after_first.raw_tag_text_bytes(),
+ expected_delta.raw_tag_text_bytes
+ );
+ assert_eq!(after_first.raw_high_water_seq(), 1);
+ mark_direct_case(
+ &mut executed_direct_cases,
+ "durable_unique_append_updates_all_dimensions",
+ );
+
+ let duplicate = store
+ .ingest_event(
+ RadrootsEventIngest::from_raw_json(durable_raw, 1_750_000_002)
+ .expect("verified duplicate fixture"),
+ )
+ .await
+ .expect("duplicate durable ingest");
+ assert!(duplicate.persistence.is_duplicate());
+ assert_eq!(
+ store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after duplicate"),
+ after_first
+ );
+
+ let ephemeral_raw = signed_raw_json(20_001, 1_750_000_003, Vec::new(), "relay-only");
+ let ephemeral = store
+ .ingest_event(
+ RadrootsEventIngest::from_raw_json(ephemeral_raw, 1_750_000_004)
+ .expect("verified ephemeral fixture"),
+ )
+ .await
+ .expect("ephemeral ingest outcome");
+ assert!(!ephemeral.persistence.is_inserted());
+ assert_eq!(
+ store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after ephemeral"),
+ after_first
+ );
+ mark_direct_case(&mut executed_direct_cases, "ephemeral_consumes_no_capacity");
+
+ let rolled_back_raw = signed_raw_json(1, 1_750_000_005, Vec::new(), "rolled back");
+ let rolled_back_id = serde_json::from_str::<serde_json::Value>(&rolled_back_raw)
+ .expect("rolled-back JSON")
+ .get("id")
+ .and_then(serde_json::Value::as_str)
+ .expect("rolled-back event id")
+ .to_owned();
+ let mut transaction = store
+ .begin_write_transaction()
+ .await
+ .expect("begin composed write");
+ let receipt = store
+ .ingest_event_in_transaction(
+ &mut transaction,
+ RadrootsEventIngest::from_raw_json(rolled_back_raw, 1_750_000_006)
+ .expect("verified rollback fixture"),
+ )
+ .await
+ .expect("nested ingest before outer rollback");
+ assert!(receipt.persistence.is_inserted());
+ transaction.rollback().await.expect("rollback outer write");
+ assert_eq!(
+ store
+ .source_capacity_v1()
+ .await
+ .expect("capacity after outer rollback"),
+ after_first
+ );
+ assert!(
+ store
+ .raw_event(&rolled_back_id)
+ .await
+ .expect("rolled-back raw lookup")
+ .is_none()
+ );
+ mark_direct_case(
+ &mut executed_direct_cases,
+ "outer_transaction_rollback_restores_capacity",
+ );
+
+ assert_direct_cases_executed(&vector.cases, &executed_direct_cases);
+}
+
+fn validate_vector_header(vector: &SourceMaintenanceVector) {
+ assert_eq!(vector.schema_version, 1);
+ assert_eq!(
+ vector.contract_id,
+ "radroots_event_store.source_maintenance_v1"
+ );
+ assert_eq!(vector.capacity_version, 1);
+ assert_eq!(
+ vector.limits.raw_events,
+ RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1
+ );
+ assert_eq!(
+ vector.limits.raw_tags,
+ RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1
+ );
+ assert_eq!(
+ vector.limits.raw_event_text_bytes,
+ RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1
+ );
+ assert_eq!(
+ vector.limits.raw_tag_text_bytes,
+ RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1
+ );
+ assert_eq!(
+ vector.limits.retained_source_generations,
+ RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1
+ );
+ assert_eq!(vector.accounting.algorithm, "sqlite_cast_blob_octet_sum_v1");
+ assert_eq!(
+ vector.accounting.raw_event_columns,
+ [
+ "event_id",
+ "pubkey",
+ "tags_json",
+ "content",
+ "sig",
+ "raw_json"
+ ]
+ );
+ assert_eq!(
+ vector.accounting.raw_tag_columns,
+ ["event_id", "tag_name", "tag_value", "tag_json"]
+ );
+ assert_eq!(vector.accounting.nullable_raw_tag_columns, ["tag_value"]);
+}
+
+fn validate_case_inventory(cases: &[VectorCase]) {
+ assert_eq!(cases.len(), EXPECTED_CASES.len());
+ for (case, expected) in cases.iter().zip(EXPECTED_CASES) {
+ assert_eq!(case.id, expected.id);
+ assert_eq!(case.execution, expected.execution, "{}", case.id);
+ assert_eq!(case.authority, expected.authority, "{}", case.id);
+ assert_eq!(case.authority_path, expected.authority_path, "{}", case.id);
+ assert_eq!(
+ case.error_domain.as_deref(),
+ expected.error_domain,
+ "{}",
+ case.id
+ );
+ assert!(!case.expected_outcome.is_empty(), "{}", case.id);
+ match case.boundary.as_deref() {
+ Some(
+ "corrupt_managed_v3" | "exact" | "managed_v4_rebuild" | "one_over" | "under_limit"
+ | "v4_to_v3",
+ )
+ | None => {}
+ other => panic!("{}: invalid boundary {other:?}", case.id),
+ }
+ match (case.error_domain.as_deref(), case.expected_error.as_deref()) {
+ (None, None) => {}
+ (Some("typed" | "sqlite_database"), Some(error)) if !error.is_empty() => {}
+ other => panic!("{}: inconsistent error metadata {other:?}", case.id),
+ }
+ if case.execution == DIRECT_EXECUTOR {
+ assert!(case.resource.is_none(), "{}", case.id);
+ assert!(case.boundary.is_none(), "{}", case.id);
+ }
+ }
+}
+
+fn mark_direct_case(executed: &mut BTreeSet<String>, id: &str) {
+ assert!(
+ executed.insert(id.to_owned()),
+ "direct case executed twice: {id}"
+ );
+}
+
+fn assert_direct_cases_executed(cases: &[VectorCase], executed: &BTreeSet<String>) {
+ let expected = cases
+ .iter()
+ .filter(|case| case.execution == DIRECT_EXECUTOR)
+ .map(|case| case.id.clone())
+ .collect::<BTreeSet<_>>();
+ assert_eq!(executed, &expected);
+}
+
+fn signed_raw_json(kind: u16, created_at: u64, tags: Vec<Vec<String>>, content: &str) -> String {
+ let secret_key = SecretKey::from_hex(FIXTURE_SECRET_KEY_HEX).expect("fixture secret key");
+ let keys = Keys::new(secret_key);
+ let tags = tags
+ .into_iter()
+ .map(|mut values| {
+ let name = values.remove(0);
+ Tag::custom(TagKind::Custom(name.into()), values)
+ })
+ .collect::<Vec<_>>();
+ let event = EventBuilder::new(Kind::Custom(kind), content)
+ .tags(tags)
+ .custom_created_at(Timestamp::from_secs(created_at))
+ .sign_with_keys(&keys)
+ .expect("signed fixture event");
+ serde_json::to_string(&event).expect("fixture event JSON")
+}
+
+fn capacity_delta(raw_json: &str) -> CapacityDelta {
+ let event: serde_json::Value = serde_json::from_str(raw_json).expect("signed fixture JSON");
+ let event_id = text_field(&event, "id");
+ let pubkey = text_field(&event, "pubkey");
+ let content = text_field(&event, "content");
+ let sig = text_field(&event, "sig");
+ let tags = event
+ .get("tags")
+ .and_then(serde_json::Value::as_array)
+ .expect("fixture tags");
+ let tags_json = serde_json::to_string(tags).expect("canonical tags JSON");
+ let raw_event_text_bytes = [event_id, pubkey, tags_json.as_str(), content, sig, raw_json]
+ .into_iter()
+ .map(str::len)
+ .sum::<usize>();
+ let raw_tag_text_bytes = tags
+ .iter()
+ .map(|tag| {
+ let values = tag.as_array().expect("tag array");
+ let name = values
+ .first()
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or("");
+ let value = values
+ .get(1)
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or("");
+ event_id.len()
+ + name.len()
+ + value.len()
+ + serde_json::to_string(values).expect("tag JSON").len()
+ })
+ .sum::<usize>();
+ CapacityDelta {
+ raw_events: 1,
+ raw_tags: u64::try_from(tags.len()).expect("tag count fits u64"),
+ raw_event_text_bytes: u64::try_from(raw_event_text_bytes)
+ .expect("event byte count fits u64"),
+ raw_tag_text_bytes: u64::try_from(raw_tag_text_bytes).expect("tag byte count fits u64"),
+ }
+}
+
+fn text_field<'a>(event: &'a serde_json::Value, name: &str) -> &'a str {
+ event
+ .get(name)
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or_else(|| panic!("fixture event missing {name}"))
+}
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -7,6 +7,7 @@ mod deletion_authority;
mod food_availability_projection;
mod nip09_reconciliation;
mod registry_v7;
+mod source_maintenance;
pub(crate) use food_availability_projection::{
validate_food_availability_projection_manifest, write_food_availability_projection_manifest,
@@ -17,6 +18,9 @@ pub(crate) use nip09_reconciliation::{
pub(crate) use registry_v7::{
validate_event_contract_registry_v7_inventory, write_event_contract_registry_v7_inventory,
};
+pub(crate) use source_maintenance::{
+ validate_source_maintenance_manifest, write_source_maintenance_manifest,
+};
use crate::coverage::{CoveragePolicyFile, CoverageThresholds, read_coverage_policy};
use admission_authority::validate_admission_operation_authority;
@@ -40,6 +44,14 @@ use std::env;
use std::fs;
use std::path::{Path, PathBuf};
+pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), String> {
+ validate_event_contract_registry_v7_inventory(workspace_root)?;
+ validate_nip09_reconciliation_manifest(workspace_root)?;
+ validate_food_availability_projection_manifest(workspace_root)?;
+ validate_source_maintenance_manifest(workspace_root)?;
+ validate_knowledge_contract_manifest(workspace_root)
+}
+
const ROOT_RELEASE_POLICY_RELATIVE: &str =
"foundation/contracts/release_runtime/mounted_rust_crates/publish-policy.toml";
const CONFORMANCE_ROOT_RELATIVE: &str = "contracts/conformance";
@@ -48,9 +60,12 @@ const NIP09_RECONCILIATION_CONFORMANCE_VECTOR_RELATIVE: &str =
"contracts/conformance/vectors/event_store/nip09_reconciliation.v1.json";
const FOOD_AVAILABILITY_PROJECTION_CONFORMANCE_VECTOR_RELATIVE: &str =
"contracts/conformance/vectors/event_store/food_availability_projection.v1.json";
-const SPECIALIZED_CONFORMANCE_VECTOR_RELATIVES: [&str; 2] = [
+const SOURCE_MAINTENANCE_CONFORMANCE_VECTOR_RELATIVE: &str =
+ "contracts/conformance/vectors/event_store/source_maintenance.v1.json";
+const SPECIALIZED_CONFORMANCE_VECTOR_RELATIVES: [&str; 3] = [
NIP09_RECONCILIATION_CONFORMANCE_VECTOR_RELATIVE,
FOOD_AVAILABILITY_PROJECTION_CONFORMANCE_VECTOR_RELATIVE,
+ SOURCE_MAINTENANCE_CONFORMANCE_VECTOR_RELATIVE,
];
const KNOWLEDGE_MANIFEST_RELATIVE: &str =
"contracts/knowledge/knowledge_event_contract_manifest.v2.json";
@@ -71,7 +86,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); 21] = [
+const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 22] = [
(
"contracts/conformance/vectors/blossom/bud11_claims.v1.json",
"crates/blossom/tests/fixtures/bud11_claims.v1.json",
@@ -117,6 +132,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 21] = [
"crates/event_store/tests/fixtures/food_availability_projection.v1.json",
),
(
+ SOURCE_MAINTENANCE_CONFORMANCE_VECTOR_RELATIVE,
+ "crates/event_store/tests/fixtures/source_maintenance.v1.json",
+ ),
+ (
"contracts/conformance/vectors/events/operational_listing_tags_full.v1.json",
"crates/event_codec/tests/fixtures/operational_listing_tags_full.v1.json",
),
diff --git a/tools/xtask/src/contract/food_availability_projection.rs b/tools/xtask/src/contract/food_availability_projection.rs
@@ -1,11 +1,13 @@
+#![allow(dead_code)]
+
use super::artifact_bundle::{
GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction,
};
use super::nip09_reconciliation::{
+ nip09_predecessor_production_source_paths_under_lock,
validate_nip09_predecessor_production_sources_under_lock,
validate_nip09_reconciliation_manifest_under_lock,
};
-use super::registry_v7::validate_event_contract_registry_v7_inventory_under_lock;
use quote::ToTokens;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -75,6 +77,77 @@ const HASH_ALGORITHM: &str = "sha256_bytes_v1";
const EVENT_STORE_LIB_RELATIVE: &str = "crates/event_store/src/lib.rs";
const EVENT_STORE_MODEL_RELATIVE: &str = "crates/event_store/src/model.rs";
+const IMMUTABLE_MANIFEST_BYTES: &[u8] = include_bytes!(
+ "../../../../crates/event_store/contracts/food_availability_projection_v1.manifest.json"
+);
+const IMMUTABLE_MANIFEST_SCHEMA_BYTES: &[u8] = include_bytes!(
+ "../../../../crates/event_store/contracts/food_availability_projection_v1.manifest.schema.json"
+);
+const IMMUTABLE_MANIFEST_SHA256_BYTES: &[u8] = include_bytes!(
+ "../../../../crates/event_store/contracts/food_availability_projection_v1.manifest.sha256"
+);
+const IMMUTABLE_GENERATED_DESCRIPTOR_BYTES: &[u8] = include_bytes!(
+ "../../../../crates/event_store/src/generated/food_availability_projection_manifest.rs"
+);
+const IMMUTABLE_RESULT_VECTOR_BYTES: &[u8] = include_bytes!(
+ "../../../../contracts/conformance/vectors/event_store/food_availability_projection.v1.json"
+);
+
+#[derive(Clone, Copy)]
+struct ImmutableArtifactSpec {
+ relative: &'static str,
+ byte_length: usize,
+ sha256: &'static str,
+}
+
+const IMMUTABLE_PREDECESSOR_ARTIFACTS: [ImmutableArtifactSpec; 9] = [
+ ImmutableArtifactSpec {
+ relative: MANIFEST_RELATIVE,
+ byte_length: 17_455,
+ sha256: "33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23",
+ },
+ ImmutableArtifactSpec {
+ relative: MANIFEST_SCHEMA_RELATIVE,
+ byte_length: 7_964,
+ sha256: "39171f6ef872a8d1483bc3d55049df5e0d110d9131c5adb4450b7c418f546910",
+ },
+ ImmutableArtifactSpec {
+ relative: MANIFEST_SHA256_RELATIVE,
+ byte_length: 65,
+ sha256: "4ac4c79a946ccb1a11726cbafc18e2e016f08f3f6797964400dea3494c66dbc5",
+ },
+ ImmutableArtifactSpec {
+ relative: GENERATED_DESCRIPTOR_RELATIVE,
+ byte_length: 21_437,
+ sha256: "90908da53ab9572f45f5916ccc2652736b7ea26ba6dd202a4f69af1e651b564b",
+ },
+ ImmutableArtifactSpec {
+ relative: RESULT_VECTOR_CANONICAL_RELATIVE,
+ byte_length: 103_659,
+ sha256: "fca2b71b47736ed04ed1e908823b65b3fc3cf0366cb162128369fe328295bb63",
+ },
+ ImmutableArtifactSpec {
+ relative: RESULT_VECTOR_MIRROR_RELATIVE,
+ byte_length: 103_659,
+ sha256: "fca2b71b47736ed04ed1e908823b65b3fc3cf0366cb162128369fe328295bb63",
+ },
+ ImmutableArtifactSpec {
+ relative: RESULT_VECTOR_EXECUTOR_RELATIVE,
+ byte_length: 34_075,
+ sha256: "9e8e11abae7bbc7dda30eab6f0a79074ffc3761aa6b955cff58c4c62fa581aa3",
+ },
+ ImmutableArtifactSpec {
+ relative: MIGRATION_UP_RELATIVE,
+ byte_length: 23_683,
+ sha256: "4e7edfb981b25f76055efc7802ec30b4034eeae9b9c0809ea4ea7c574678748a",
+ },
+ ImmutableArtifactSpec {
+ relative: MIGRATION_DOWN_RELATIVE,
+ byte_length: 1_755,
+ sha256: "29d663320109d9dd0df6a00b6a53d8d988438d01f7a66960a9d4ba3482ffffb8",
+ },
+];
+
const GOVERNED_PUBLIC_API_MODULES: &[&str] = &[
"addressable_transition_feed_v1",
"current_visibility_v1",
@@ -731,11 +804,7 @@ pub(crate) fn write_food_availability_projection_manifest(
workspace_root: &Path,
) -> Result<(), String> {
with_artifact_bundle_transaction(workspace_root, |transaction| {
- validate_nip09_reconciliation_manifest_under_lock(workspace_root)?;
- validate_predecessor_production_source_coverage(workspace_root)?;
- validate_event_contract_registry_v7_inventory_under_lock(workspace_root)?;
- let artifacts = expected_artifacts(workspace_root)?;
- transaction.write(artifacts)?;
+ transaction.write(immutable_generated_artifacts())?;
validate_food_availability_projection_manifest_under_lock(workspace_root)
})
}
@@ -748,12 +817,10 @@ pub(crate) fn validate_food_availability_projection_manifest(
})
}
-fn validate_food_availability_projection_manifest_under_lock(
+pub(super) fn validate_food_availability_projection_manifest_under_lock(
workspace_root: &Path,
) -> Result<(), String> {
validate_nip09_reconciliation_manifest_under_lock(workspace_root)?;
- validate_predecessor_production_source_coverage(workspace_root)?;
- validate_event_contract_registry_v7_inventory_under_lock(workspace_root)?;
let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
let manifest_value: Value = serde_json::from_slice(&manifest_bytes)
@@ -778,16 +845,65 @@ fn validate_food_availability_projection_manifest_under_lock(
));
}
- let expected = expected_artifacts(workspace_root)?;
- for artifact in expected {
+ if manifest.migration.up.sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[7].sha256
+ || manifest.migration.down.sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[8].sha256
+ || manifest.result_vector.sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[4].sha256
+ || manifest.result_vector.executor_sha256 != IMMUTABLE_PREDECESSOR_ARTIFACTS[6].sha256
+ {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} does not describe the immutable FoodAvailability predecessor identity"
+ ));
+ }
+
+ let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?;
+ let mirror_bytes = read_regular_file(workspace_root, RESULT_VECTOR_MIRROR_RELATIVE)?;
+ if vector_bytes != mirror_bytes {
+ return Err(format!(
+ "{RESULT_VECTOR_MIRROR_RELATIVE} must exactly mirror {RESULT_VECTOR_CANONICAL_RELATIVE}"
+ ));
+ }
+ let vector: ProjectionResultVector = serde_json::from_slice(&vector_bytes)
+ .map_err(|error| format!("parse {RESULT_VECTOR_CANONICAL_RELATIVE}: {error}"))?;
+ validate_canonical_json(RESULT_VECTOR_CANONICAL_RELATIVE, &vector_bytes, &vector)?;
+ validate_result_vector(&vector)?;
+
+ for artifact in IMMUTABLE_PREDECESSOR_ARTIFACTS {
let actual = read_regular_file(workspace_root, artifact.relative)?;
- if actual != artifact.contents {
- return Err(stale_error(artifact.relative));
+ if actual.len() != artifact.byte_length || sha256_hex(&actual) != artifact.sha256 {
+ return Err(format!(
+ "immutable FoodAvailability predecessor artifact {} does not match its authenticated byte identity",
+ artifact.relative
+ ));
}
}
Ok(())
}
+fn immutable_generated_artifacts() -> Vec<GeneratedArtifact> {
+ vec![
+ GeneratedArtifact {
+ relative: MANIFEST_RELATIVE,
+ contents: IMMUTABLE_MANIFEST_BYTES.to_vec(),
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SCHEMA_RELATIVE,
+ contents: IMMUTABLE_MANIFEST_SCHEMA_BYTES.to_vec(),
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SHA256_RELATIVE,
+ contents: IMMUTABLE_MANIFEST_SHA256_BYTES.to_vec(),
+ },
+ GeneratedArtifact {
+ relative: GENERATED_DESCRIPTOR_RELATIVE,
+ contents: IMMUTABLE_GENERATED_DESCRIPTOR_BYTES.to_vec(),
+ },
+ GeneratedArtifact {
+ relative: RESULT_VECTOR_MIRROR_RELATIVE,
+ contents: IMMUTABLE_RESULT_VECTOR_BYTES.to_vec(),
+ },
+ ]
+}
+
fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> {
let schema = manifest_schema();
let schema_bytes = canonical_json_bytes(&schema)?;
@@ -826,7 +942,7 @@ fn describe_manifest(
schema_bytes: &[u8],
) -> Result<FoodAvailabilityProjectionManifest, String> {
validate_source_contract(workspace_root)?;
- validate_predecessor_production_source_coverage(workspace_root)?;
+ validate_predecessor_production_source_coverage(workspace_root, &[])?;
let predecessor_bytes = read_regular_file(workspace_root, PREDECESSOR_MANIFEST_RELATIVE)?;
if predecessor_bytes.len() != PREDECESSOR_MANIFEST_BYTE_LENGTH
@@ -921,7 +1037,10 @@ fn describe_manifest(
})
}
-fn validate_predecessor_production_source_coverage(workspace_root: &Path) -> Result<(), String> {
+fn validate_predecessor_production_source_coverage(
+ workspace_root: &Path,
+ downstream_nip09_superseded_paths: &[&str],
+) -> Result<(), String> {
for path in PREDECESSOR_SUPERSEDED_SOURCE_PATHS {
if !SOURCE_SPECS.iter().any(|source| source.path == *path) {
return Err(format!(
@@ -929,10 +1048,152 @@ fn validate_predecessor_production_source_coverage(workspace_root: &Path) -> Res
));
}
}
- validate_nip09_predecessor_production_sources_under_lock(
- workspace_root,
- PREDECESSOR_SUPERSEDED_SOURCE_PATHS,
- )
+ let superseded_paths = PREDECESSOR_SUPERSEDED_SOURCE_PATHS
+ .iter()
+ .copied()
+ .chain(downstream_nip09_superseded_paths.iter().copied())
+ .collect::<BTreeSet<_>>()
+ .into_iter()
+ .collect::<Vec<_>>();
+ validate_nip09_predecessor_production_sources_under_lock(workspace_root, &superseded_paths)
+}
+
+pub(super) fn validate_food_availability_projection_predecessor_production_sources_under_lock(
+ workspace_root: &Path,
+ superseded_paths: &[&str],
+) -> Result<(), String> {
+ validate_food_availability_projection_manifest_under_lock(workspace_root)?;
+ let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
+ let manifest: FoodAvailabilityProjectionManifest = serde_json::from_slice(&manifest_bytes)
+ .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
+
+ let (food_superseded_paths, nip09_superseded_paths) =
+ partition_downstream_predecessor_supersessions(workspace_root, superseded_paths)?;
+ validate_food_predecessor_source_inventory(&manifest, &food_superseded_paths, |spec| {
+ describe_source_file(workspace_root, spec)
+ })?;
+ require_predecessor_file_match(
+ "registry inventory",
+ &manifest.registry_inventory,
+ &descriptor_for_file(workspace_root, REGISTRY_INVENTORY_RELATIVE)?,
+ )?;
+ require_predecessor_file_match(
+ "FoodAvailability profile vector",
+ &manifest.food_profile_vector,
+ &descriptor_for_file(workspace_root, FOOD_PROFILE_VECTOR_RELATIVE)?,
+ )?;
+ validate_predecessor_production_source_coverage(workspace_root, &nip09_superseded_paths)
+}
+
+fn partition_downstream_predecessor_supersessions<'a>(
+ workspace_root: &Path,
+ superseded_paths: &'a [&'a str],
+) -> Result<(Vec<&'a str>, Vec<&'a str>), String> {
+ let superseded = superseded_paths.iter().copied().collect::<BTreeSet<_>>();
+ if superseded.len() != superseded_paths.len() {
+ return Err("successor predecessor-source supersession paths must be unique".to_owned());
+ }
+ let food_paths = SOURCE_SPECS
+ .iter()
+ .map(|source| source.path)
+ .collect::<BTreeSet<_>>();
+ let nip09_paths = nip09_predecessor_production_source_paths_under_lock(workspace_root)?;
+ let mut food_superseded_paths = Vec::new();
+ let mut nip09_superseded_paths = Vec::new();
+ for path in superseded_paths {
+ if food_paths.contains(path) {
+ food_superseded_paths.push(*path);
+ }
+ if nip09_paths.contains(*path) {
+ nip09_superseded_paths.push(*path);
+ }
+ if !food_paths.contains(path) && !nip09_paths.contains(*path) {
+ return Err(format!(
+ "successor supersession path `{path}` is not bound by either the FoodAvailability or NIP-09 predecessor"
+ ));
+ }
+ }
+ Ok((food_superseded_paths, nip09_superseded_paths))
+}
+
+fn validate_food_predecessor_source_inventory<Describe>(
+ manifest: &FoodAvailabilityProjectionManifest,
+ superseded_paths: &[&str],
+ mut describe: Describe,
+) -> Result<(), String>
+where
+ Describe: FnMut(SourceSpec) -> Result<SourceFileDescriptor, String>,
+{
+ let superseded = superseded_paths.iter().copied().collect::<BTreeSet<_>>();
+ if superseded.len() != superseded_paths.len() {
+ return Err("successor predecessor-source supersession paths must be unique".to_owned());
+ }
+
+ let predecessor_paths = SOURCE_SPECS
+ .iter()
+ .map(|source| source.path)
+ .collect::<BTreeSet<_>>();
+ if let Some(path) = superseded
+ .iter()
+ .find(|path| !predecessor_paths.contains(**path))
+ {
+ return Err(format!(
+ "successor supersession path `{path}` is not a FoodAvailability predecessor-bound production source"
+ ));
+ }
+
+ if manifest.source_files.len() != SOURCE_SPECS.len() {
+ return Err(
+ "immutable FoodAvailability predecessor source inventory is incomplete".to_owned(),
+ );
+ }
+ for (expected, spec) in manifest.source_files.iter().zip(SOURCE_SPECS) {
+ if expected.role != spec.role || expected.path != spec.path {
+ return Err(format!(
+ "immutable FoodAvailability predecessor source inventory drifted at `{}`",
+ spec.path
+ ));
+ }
+ if superseded.contains(spec.path) {
+ continue;
+ }
+ let current = describe(*spec)?;
+ if current != *expected {
+ return Err(format!(
+ "unchanged FoodAvailability predecessor source authority `{}` drifted from the immutable manifest",
+ spec.path
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn require_predecessor_file_match(
+ label: &str,
+ expected: &FileDescriptor,
+ current: &FileDescriptor,
+) -> Result<(), String> {
+ if current != expected {
+ return Err(format!(
+ "unchanged FoodAvailability predecessor {label} `{}` drifted from the immutable manifest",
+ expected.path
+ ));
+ }
+ Ok(())
+}
+
+fn describe_source_file(
+ workspace_root: &Path,
+ spec: SourceSpec,
+) -> Result<SourceFileDescriptor, String> {
+ let bytes = read_regular_file(workspace_root, spec.path)?;
+ Ok(SourceFileDescriptor {
+ role: spec.role.to_owned(),
+ path: spec.path.to_owned(),
+ byte_length: byte_length(spec.path, &bytes)?,
+ sha256: sha256_hex(&bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ })
}
fn descriptor_for_file(workspace_root: &Path, relative: &str) -> Result<FileDescriptor, String> {
@@ -3556,6 +3817,7 @@ fn stale_error(relative: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
+ use std::fs;
fn repository_root() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
@@ -3565,6 +3827,228 @@ mod tests {
.to_path_buf()
}
+ fn immutable_manifest() -> FoodAvailabilityProjectionManifest {
+ serde_json::from_slice(IMMUTABLE_MANIFEST_BYTES)
+ .expect("immutable FoodAvailability manifest")
+ }
+
+ fn copy_file(source_root: &Path, destination_root: &Path, relative: &str) {
+ let destination = destination_root.join(relative);
+ fs::create_dir_all(destination.parent().expect("fixture parent"))
+ .expect("create fixture parent");
+ fs::copy(source_root.join(relative), destination).expect("copy fixture");
+ }
+
+ fn immutable_artifact_workspace() -> tempfile::TempDir {
+ const NIP09_ARTIFACTS: &[&str] = &[
+ "crates/event_store/contracts/nip09_reconciliation_v1.manifest.json",
+ "crates/event_store/contracts/nip09_reconciliation_v1.manifest.schema.json",
+ "crates/event_store/contracts/nip09_reconciliation_v1.manifest.sha256",
+ "crates/event_store/src/generated/nip09_reconciliation_manifest.rs",
+ "contracts/conformance/vectors/event_store/nip09_reconciliation.v1.json",
+ "crates/event_store/tests/fixtures/nip09_reconciliation.v1.json",
+ "crates/event_store/src/nip09/reconciliation_v1/result_vector_executor.rs",
+ "crates/event_store/migrations/0001_event_store.up.sql",
+ "crates/event_store/migrations/0001_event_store.down.sql",
+ "crates/event_store/migrations/0002_nip09.up.sql",
+ "crates/event_store/migrations/0002_nip09.down.sql",
+ ];
+
+ let workspace = tempfile::TempDir::new().expect("workspace");
+ let repository = repository_root();
+ for relative in NIP09_ARTIFACTS.iter().copied().chain(
+ IMMUTABLE_PREDECESSOR_ARTIFACTS
+ .iter()
+ .map(|artifact| artifact.relative),
+ ) {
+ copy_file(&repository, workspace.path(), relative);
+ }
+ workspace
+ }
+
+ #[test]
+ fn immutable_food_predecessor_artifacts_match_authenticated_identities() {
+ let root = repository_root();
+ for artifact in IMMUTABLE_PREDECESSOR_ARTIFACTS {
+ let bytes = read_regular_file(&root, artifact.relative).expect("immutable artifact");
+ assert_eq!(bytes.len(), artifact.byte_length, "{}", artifact.relative);
+ assert_eq!(sha256_hex(&bytes), artifact.sha256, "{}", artifact.relative);
+ }
+ }
+
+ #[test]
+ fn legacy_writer_restores_only_generated_immutable_artifacts() {
+ let workspace = immutable_artifact_workspace();
+ fs::write(workspace.path().join(MANIFEST_RELATIVE), b"tampered\n")
+ .expect("tamper manifest");
+ fs::write(
+ workspace.path().join(RESULT_VECTOR_MIRROR_RELATIVE),
+ b"tampered\n",
+ )
+ .expect("tamper result-vector mirror");
+ let changed_source = workspace.path().join("crates/event_store/src/error.rs");
+ fs::create_dir_all(changed_source.parent().expect("source parent"))
+ .expect("create source parent");
+ fs::write(&changed_source, b"successor-owned source bytes\n")
+ .expect("write changed source");
+
+ write_food_availability_projection_manifest(workspace.path())
+ .expect("restore immutable generated artifacts");
+ assert_eq!(
+ fs::read(workspace.path().join(MANIFEST_RELATIVE)).expect("restored manifest"),
+ IMMUTABLE_MANIFEST_BYTES
+ );
+ assert_eq!(
+ fs::read(workspace.path().join(RESULT_VECTOR_MIRROR_RELATIVE))
+ .expect("restored result-vector mirror"),
+ IMMUTABLE_RESULT_VECTOR_BYTES
+ );
+ assert_eq!(
+ fs::read(changed_source).expect("changed source"),
+ b"successor-owned source bytes\n"
+ );
+ }
+
+ #[test]
+ fn legacy_writer_cannot_rebaseline_an_authored_immutable_artifact() {
+ let workspace = immutable_artifact_workspace();
+ fs::write(
+ workspace.path().join(RESULT_VECTOR_EXECUTOR_RELATIVE),
+ b"tampered\n",
+ )
+ .expect("tamper executor");
+ let error = write_food_availability_projection_manifest(workspace.path())
+ .expect_err("immutable executor drift must fail");
+ assert!(
+ error.contains("immutable FoodAvailability predecessor artifact")
+ && error.contains(RESULT_VECTOR_EXECUTOR_RELATIVE),
+ "{error}"
+ );
+ }
+
+ #[test]
+ fn predecessor_source_inventory_rejects_duplicate_unknown_and_unsuperseded_drift() {
+ let manifest = immutable_manifest();
+ let descriptors = manifest.source_files.clone();
+ let describe = |spec: SourceSpec| {
+ descriptors
+ .iter()
+ .find(|source| source.path == spec.path)
+ .cloned()
+ .ok_or_else(|| format!("missing fixture source {}", spec.path))
+ };
+ validate_food_predecessor_source_inventory(&manifest, &[], describe)
+ .expect("complete immutable predecessor inventory");
+
+ let path = SOURCE_SPECS[0].path;
+ let error = validate_food_predecessor_source_inventory(&manifest, &[path, path], |_| {
+ unreachable!("duplicates fail before source reads")
+ })
+ .expect_err("duplicate supersession must fail");
+ assert!(error.contains("must be unique"), "{error}");
+
+ let error = validate_food_predecessor_source_inventory(
+ &manifest,
+ &["crates/event_store/src/not_bound.rs"],
+ |_| unreachable!("unknown paths fail before source reads"),
+ )
+ .expect_err("unknown supersession must fail");
+ assert!(
+ error.contains("not a FoodAvailability predecessor-bound"),
+ "{error}"
+ );
+
+ let drift_path = SOURCE_SPECS[0].path;
+ let descriptors = manifest.source_files.clone();
+ let error = validate_food_predecessor_source_inventory(&manifest, &[], |spec| {
+ let mut source = descriptors
+ .iter()
+ .find(|source| source.path == spec.path)
+ .cloned()
+ .expect("fixture source");
+ if spec.path == drift_path {
+ source.sha256 = "00".repeat(32);
+ }
+ Ok(source)
+ })
+ .expect_err("unsuperseded source drift must fail");
+ assert!(
+ error.contains("unchanged FoodAvailability predecessor source authority")
+ && error.contains(drift_path),
+ "{error}"
+ );
+
+ let descriptors = manifest.source_files.clone();
+ validate_food_predecessor_source_inventory(&manifest, &[drift_path], |spec| {
+ assert_ne!(spec.path, drift_path, "superseded source must not be read");
+ descriptors
+ .iter()
+ .find(|source| source.path == spec.path)
+ .cloned()
+ .ok_or_else(|| format!("missing fixture source {}", spec.path))
+ })
+ .expect("an explicitly superseded source is delegated to the successor");
+ }
+
+ #[test]
+ fn downstream_nip09_only_supersession_is_transitively_validated() {
+ const SOURCE_MAINTENANCE_SUPERSEDED_PATHS: &[&str] = &[
+ "crates/event_store/src/error.rs",
+ "crates/event_store/src/generated.rs",
+ "crates/event_store/src/lib.rs",
+ "crates/event_store/src/migrations.rs",
+ "crates/event_store/src/model.rs",
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "crates/event_store/src/schema.rs",
+ "crates/event_store/src/store.rs",
+ "crates/event_store/src/store/protocol_reconciliation_v1.rs",
+ ];
+
+ let root = repository_root();
+ let (food_paths, nip09_paths) = partition_downstream_predecessor_supersessions(
+ &root,
+ &[
+ "crates/event_store/src/error.rs",
+ "crates/event_store/src/store/protocol_reconciliation_v1.rs",
+ ],
+ )
+ .expect("overlapping and NIP-09-only predecessor ownership");
+ assert_eq!(food_paths, ["crates/event_store/src/error.rs"]);
+ assert_eq!(
+ nip09_paths,
+ [
+ "crates/event_store/src/error.rs",
+ "crates/event_store/src/store/protocol_reconciliation_v1.rs",
+ ]
+ );
+ validate_food_availability_projection_predecessor_production_sources_under_lock(
+ &root,
+ SOURCE_MAINTENANCE_SUPERSEDED_PATHS,
+ )
+ .expect("Food and transitive NIP-09 successor source coverage");
+
+ let mut duplicate = SOURCE_MAINTENANCE_SUPERSEDED_PATHS.to_vec();
+ duplicate.push("crates/event_store/src/store/protocol_reconciliation_v1.rs");
+ let error =
+ validate_food_availability_projection_predecessor_production_sources_under_lock(
+ &root, &duplicate,
+ )
+ .expect_err("duplicate transitive supersession must fail");
+ assert!(error.contains("must be unique"), "{error}");
+
+ let mut unknown = SOURCE_MAINTENANCE_SUPERSEDED_PATHS.to_vec();
+ unknown.push("crates/event_store/src/store/not_predecessor_bound.rs");
+ let error =
+ validate_food_availability_projection_predecessor_production_sources_under_lock(
+ &root, &unknown,
+ )
+ .expect_err("unknown transitive supersession must fail");
+ assert!(
+ error.contains("not bound by either the FoodAvailability or NIP-09 predecessor"),
+ "{error}"
+ );
+ }
+
#[test]
fn food_scope_fingerprint_is_pinned() {
let mut hasher = Sha256::new();
@@ -3691,138 +4175,6 @@ mod tests {
}
#[test]
- fn predecessor_fast_open_validation_remains_constant_cost() {
- let root = repository_root();
- let source = read_regular_file(&root, "crates/event_store/src/nip09/reconciliation_v1.rs")
- .expect("predecessor source");
- let source = std::str::from_utf8(&source).expect("UTF-8 predecessor source");
- validate_fast_active_hook_source(source).expect("constant-cost fast-open validation");
-
- let exhaustive = source.replacen(
- "validate_structural_source_state_fast(connection)",
- "validate_structural_source_state(connection)",
- 1,
- );
- assert_ne!(exhaustive, source, "fast-open mutation must apply");
- let error = validate_fast_active_hook_source(&exhaustive)
- .expect_err("exhaustive open-time scan must fail");
- assert!(error.contains("constant-cost"), "{error}");
- }
-
- #[test]
- fn source_capacity_preflight_and_recheck_authority_is_structurally_sealed() {
- const OUTER_PREFLIGHT: &str = r#" if has_pending_source_capacity_hook(&status, registry) {
- let mut connection = pool.acquire().await?;
- validate_event_store_temp_schema_with_registry(&mut connection, registry).await?;
- validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;
- }
-"#;
- const OUTER_DECOY: &str = r#" if false {
- if has_pending_source_capacity_hook(&status, registry) {
- let mut connection = pool.acquire().await?;
- validate_event_store_temp_schema_with_registry(&mut connection, registry).await?;
- validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;
- }
- }
-"#;
- const INNER_RECHECK_AND_APPLY: &str = r#" if matches!(
- migration.hook,
- EventStoreMigrationHook::Nip09ReconciliationV1
- | EventStoreMigrationHook::FoodAvailabilityProjectionV1
- ) {
- validate_reconciliation_capacity(connection, reconciliation_limits).await?;
- }
- apply_migration_up(connection, registry, migration).await?;
-"#;
- const INNER_APPLY_THEN_RECHECK: &str = r#" apply_migration_up(connection, registry, migration).await?;
- if matches!(
- migration.hook,
- EventStoreMigrationHook::Nip09ReconciliationV1
- | EventStoreMigrationHook::FoodAvailabilityProjectionV1
- ) {
- validate_reconciliation_capacity(connection, reconciliation_limits).await?;
- }
-"#;
- const OUTER_HOOK_SET: &str = r#"EventStoreMigrationHook::Nip09ReconciliationV1
- | EventStoreMigrationHook::FoodAvailabilityProjectionV1"#;
- const INNER_HOOK_SET: &str = r#"EventStoreMigrationHook::Nip09ReconciliationV1
- | EventStoreMigrationHook::FoodAvailabilityProjectionV1"#;
-
- let root = repository_root();
- let source = read_regular_file(&root, "crates/event_store/src/schema.rs")
- .expect("event-store schema source");
- let source = std::str::from_utf8(&source).expect("UTF-8 schema source");
- validate_source_capacity_authority(source).expect("governed source-capacity authority");
-
- let outer_before_begin = format!(
- "{OUTER_PREFLIGHT}\n let mut transaction = pool.begin_with(\"BEGIN IMMEDIATE\").await?;\n"
- );
- let outer_after_begin = format!(
- " let mut transaction = pool.begin_with(\"BEGIN IMMEDIATE\").await?;\n{OUTER_PREFLIGHT}"
- );
- let mutations = [
- (
- "outer capacity removal",
- source.replacen(
- " validate_reconciliation_capacity(&mut connection, reconciliation_limits).await?;\n",
- "",
- 1,
- ),
- ),
- (
- "inner capacity removal",
- source.replacen(
- " validate_reconciliation_capacity(connection, reconciliation_limits).await?;\n",
- "",
- 1,
- ),
- ),
- (
- "unreachable outer decoy",
- source.replacen(OUTER_PREFLIGHT, OUTER_DECOY, 1),
- ),
- (
- "outer preflight after BEGIN IMMEDIATE",
- source.replacen(&outer_before_begin, &outer_after_begin, 1),
- ),
- (
- "inner recheck after migration DDL",
- source.replacen(
- INNER_RECHECK_AND_APPLY,
- INNER_APPLY_THEN_RECHECK,
- 1,
- ),
- ),
- (
- "outer selector covers one hook only",
- source.replacen(
- OUTER_HOOK_SET,
- "EventStoreMigrationHook::FoodAvailabilityProjectionV1",
- 1,
- ),
- ),
- (
- "inner recheck covers one hook only",
- source.replacen(
- INNER_HOOK_SET,
- "EventStoreMigrationHook::Nip09ReconciliationV1",
- 1,
- ),
- ),
- ];
-
- for (label, mutation) in mutations {
- assert_ne!(
- mutation, source,
- "source-capacity mutation must apply: {label}"
- );
- let error = validate_source_capacity_authority(&mutation)
- .expect_err("source-capacity mutation must fail");
- assert!(error.contains("source-capacity"), "{label}: {error}");
- }
- }
-
- #[test]
fn food_read_queries_require_the_exact_persisted_head_authority_join() {
let root = repository_root();
let source = read_regular_file(
@@ -4145,45 +4497,15 @@ mod tests {
}
#[test]
- fn public_api_is_exhaustive_and_structurally_reexported() {
- let root = repository_root();
- let model_bytes =
- read_regular_file(&root, EVENT_STORE_MODEL_RELATIVE).expect("event-store model source");
- let model_source = std::str::from_utf8(&model_bytes).expect("UTF-8 model source");
- let lib_bytes =
- read_regular_file(&root, EVENT_STORE_LIB_RELATIVE).expect("event-store lib source");
- let lib_source = std::str::from_utf8(&lib_bytes).expect("UTF-8 lib source");
- let advertised = PUBLIC_API
- .iter()
- .map(|name| (*name).to_owned())
- .collect::<Vec<_>>();
- validate_public_api_sources(model_source, lib_source, &advertised)
- .expect("governed successor public API");
-
- let mut omitted = advertised.clone();
- omitted.retain(|name| name != "RadrootsAddressableTransitionCauseV1");
- let error = validate_public_api_sources(model_source, lib_source, &omitted)
- .expect_err("omitted manifest symbol must fail");
- assert!(error.contains("PUBLIC_API is not exhaustive"), "{error}");
-
- let removed = lib_source.replacen("RadrootsAddressableTransitionCauseV1,", "", 1);
- assert_ne!(removed, lib_source, "removal mutation must apply");
- let error = validate_public_api_sources(model_source, &removed, &advertised)
- .expect_err("removed crate-root export must fail");
- assert!(error.contains("does not re-export"), "{error}");
-
- let renamed = lib_source.replacen(
- "RadrootsAddressableTransitionCauseV1",
- "RadrootsAddressableTransitionCauseV1 as RadrootsAddressableTransitionCauseRenamedV1",
- 1,
+ fn immutable_public_api_inventory_is_exact() {
+ let mut manifest = immutable_manifest();
+ assert_eq!(
+ manifest.public_api,
+ PUBLIC_API
+ .iter()
+ .map(|name| (*name).to_owned())
+ .collect::<Vec<_>>()
);
- assert_ne!(renamed, lib_source, "rename mutation must apply");
- let error = validate_public_api_sources(model_source, &renamed, &advertised)
- .expect_err("renamed crate-root export must fail");
- assert!(error.contains("non-renamed"), "{error}");
-
- let schema_bytes = canonical_json_bytes(&manifest_schema()).expect("schema bytes");
- let mut manifest = describe_manifest(&root, &schema_bytes).expect("manifest");
manifest
.public_api
.retain(|name| name != "RadrootsAddressableTransitionCauseV1");
@@ -4312,10 +4634,9 @@ mod tests {
#[test]
fn schema_rejects_unknown_manifest_fields() {
- let root = repository_root();
- let schema = manifest_schema();
- let schema_bytes = canonical_json_bytes(&schema).expect("schema bytes");
- let manifest = describe_manifest(&root, &schema_bytes).expect("manifest");
+ let schema: Value = serde_json::from_slice(IMMUTABLE_MANIFEST_SCHEMA_BYTES)
+ .expect("immutable manifest schema");
+ let manifest = immutable_manifest();
let mut value = serde_json::to_value(manifest).expect("manifest value");
value
.as_object_mut()
@@ -4328,12 +4649,9 @@ mod tests {
#[test]
fn generated_descriptor_covers_runtime_pointer_constants() {
- let root = repository_root();
- let schema_bytes = canonical_json_bytes(&manifest_schema()).expect("schema bytes");
- let manifest = describe_manifest(&root, &schema_bytes).expect("manifest");
- let manifest_bytes = canonical_json_bytes(&manifest).expect("manifest bytes");
- let digest = sha256_hex(&manifest_bytes);
- let descriptor = generated_descriptor(&manifest, &manifest_bytes, &digest);
+ let manifest = immutable_manifest();
+ let descriptor = std::str::from_utf8(IMMUTABLE_GENERATED_DESCRIPTOR_BYTES)
+ .expect("UTF-8 immutable generated descriptor");
assert_eq!(manifest.migration.schema_sha256, SCHEMA_SHA256);
for name in [
"FOOD_AVAILABILITY_PROJECTION_MANIFEST_SCHEMA_VERSION",
@@ -4362,6 +4680,6 @@ mod tests {
"{name} must use the rustfmt-stable one-line assignment"
);
}
- syn::parse_file(&descriptor).expect("generated descriptor parses as Rust");
+ syn::parse_file(descriptor).expect("generated descriptor parses as Rust");
}
}
diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs
@@ -17,6 +17,42 @@ const SCHEMA_VERSION: u32 = 1;
const HOOK_ID: &str = "nip09_reconciliation_v1";
const MIGRATION_VERSION: u32 = 2;
const MIGRATION_NAME: &str = "nip09";
+const SOURCE_MAINTENANCE_MIGRATION_VERSION: u32 = 4;
+const SOURCE_MAINTENANCE_MIGRATION_NAME: &str = "source_maintenance";
+const SOURCE_MAINTENANCE_PRIVILEGED_TERMINALS: [&str; 9] = [
+ "apply_source_maintenance_hook_v1",
+ "preflight_unique_raw_source_append_v1",
+ "raw_source_capacity_delta_v1",
+ "advance_source_capacity_after_insert_v1",
+ "preflight_source_generation_append_v1",
+ "bind_source_capacity_to_generation_v1",
+ "validate_source_capacity_authority_fast_v1",
+ "validate_source_capacity_authority_full_v1",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+];
+const PRIVILEGED_TERMINAL_NAMES: [&str; 21] = [
+ "validate_event_store_temp_schema",
+ "validate_main_database_encoding",
+ "validate_rollback_preserves_source_generation_history",
+ "apply_migration_up",
+ "apply_migration_down",
+ "apply_migration_hook",
+ "validate_migration_hook_state",
+ "validate_active_hook_state_fast",
+ "ingest_event_protocol_reconciliation_v1",
+ "dispatch_post_core_extensions",
+ "apply_post_core_extensions_v1",
+ "validate_protocol_post_extensions",
+ "apply_source_maintenance_hook_v1",
+ "preflight_unique_raw_source_append_v1",
+ "raw_source_capacity_delta_v1",
+ "advance_source_capacity_after_insert_v1",
+ "preflight_source_generation_append_v1",
+ "bind_source_capacity_to_generation_v1",
+ "validate_source_capacity_authority_fast_v1",
+ "validate_source_capacity_authority_full_v1",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+];
const RECONCILIATION_VERSION: u32 = 1;
const ADDRESSABLE_FEED_VERSION: u32 = 1;
const EVENT_CONTRACT_REGISTRY_VERSION: u32 = 7;
@@ -357,6 +393,8 @@ const SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS: [&str; 8] = [
"crates/event_store/src/store/post_core_extensions_v2.rs",
"crates/event_store/src/store/post_core_storage_v2.rs",
];
+const SUCCESSOR_08D_SOURCE_PATHS: [&str; 1] = ["crates/event_store/src/source_maintenance_v1.rs"];
+const SUCCESSOR_08D_LIB_MODULES: [&str; 1] = ["source_maintenance_v1"];
const EVENT_STORE_FIXED_PUBLIC_REEXPORTS: [&str; 40] = [
"error::RadrootsEventStoreError",
"error::RadrootsEventStoreReconciliationResource",
@@ -433,6 +471,17 @@ const SUCCESSOR_08C_PUBLIC_REEXPORTS: [&str; 32] = [
"model::RadrootsStoredFoodAvailabilityImageV1",
"model::RadrootsStoredFoodAvailabilityV1",
];
+const SUCCESSOR_08D_RETIRED_PUBLIC_REEXPORTS: [&str; 1] =
+ ["error::RadrootsEventStoreReconciliationResource"];
+const SUCCESSOR_08D_PUBLIC_REEXPORTS: [&str; 7] = [
+ "error::RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1",
+ "error::RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1",
+ "error::RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1",
+ "error::RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1",
+ "error::RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1",
+ "error::RadrootsEventStoreSourceCapacityResourceV1",
+ "source_maintenance_v1::RadrootsEventStoreSourceCapacityV1",
+];
const POST_CORE_STORAGE_METHODS: [&str; 4] = [
"new",
"quarantine_trade",
@@ -2285,19 +2334,22 @@ pub(super) fn validate_nip09_reconciliation_manifest_under_lock(
Ok(())
}
-pub(super) fn validate_nip09_predecessor_production_sources_under_lock(
+pub(super) fn nip09_predecessor_production_source_paths_under_lock(
workspace_root: &Path,
- superseded_paths: &[&str],
-) -> Result<(), String> {
+) -> Result<BTreeSet<String>, String> {
let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
let manifest: Nip09ReconciliationManifest = serde_json::from_slice(&manifest_bytes)
.map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
- let superseded = superseded_paths.iter().copied().collect::<BTreeSet<_>>();
- if superseded.len() != superseded_paths.len() {
- return Err("successor predecessor-source supersession paths must be unique".to_owned());
- }
+ Ok(nip09_predecessor_production_source_paths(&manifest)
+ .into_iter()
+ .map(str::to_owned)
+ .collect())
+}
- let mut predecessor_paths = manifest
+fn nip09_predecessor_production_source_paths(
+ manifest: &Nip09ReconciliationManifest,
+) -> BTreeSet<&str> {
+ let mut paths = manifest
.frozen_sources
.iter()
.map(|source| source.path.as_str())
@@ -2327,12 +2379,28 @@ pub(super) fn validate_nip09_predecessor_production_sources_under_lock(
.map(|source| source.path.as_str()),
)
.collect::<BTreeSet<_>>();
- predecessor_paths.extend([
+ paths.extend([
POST_CORE_CAPABILITIES_SOURCE_RELATIVE,
POST_CORE_DISPATCHER_SOURCE_RELATIVE,
POST_CORE_EXTENSION_SOURCE_RELATIVE,
POST_CORE_STORAGE_SOURCE_RELATIVE,
]);
+ paths
+}
+
+pub(super) fn validate_nip09_predecessor_production_sources_under_lock(
+ workspace_root: &Path,
+ superseded_paths: &[&str],
+) -> Result<(), String> {
+ let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
+ let manifest: Nip09ReconciliationManifest = serde_json::from_slice(&manifest_bytes)
+ .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
+ let superseded = superseded_paths.iter().copied().collect::<BTreeSet<_>>();
+ if superseded.len() != superseded_paths.len() {
+ return Err("successor predecessor-source supersession paths must be unique".to_owned());
+ }
+
+ let predecessor_paths = nip09_predecessor_production_source_paths(&manifest);
if let Some(path) = superseded
.iter()
.find(|path| !predecessor_paths.contains(**path))
@@ -2543,7 +2611,7 @@ fn expected_manifest(workspace_root: &Path) -> Result<Nip09ReconciliationManifes
validate_governed_support_source_tree_baselines(workspace_root)?;
validate_route_facade_baselines(workspace_root)?;
describe_post_core_extension_boundary(workspace_root, true)?;
- validate_privileged_store_authority(workspace_root)?;
+ validate_current_event_store_successor_authority(workspace_root)?;
describe_nip09_v1_manifest(workspace_root)
}
@@ -2673,7 +2741,7 @@ fn describe_frozen_source_bytes(
spec: FrozenSourceSpec,
bytes: &[u8],
) -> Result<FrozenSourceDescriptor, String> {
- let canonical = canonical_rust_ast(spec.path, &bytes, RustAstProfile::Production)?;
+ let canonical = canonical_rust_ast(spec.path, bytes, RustAstProfile::Production)?;
let file = syn::parse_file(
std::str::from_utf8(&canonical)
.map_err(|error| format!("{} canonical source must be UTF-8: {error}", spec.path))?,
@@ -2783,7 +2851,7 @@ fn expected_event_store_migration_compiler_inputs(
));
}
- fn field<'a>(
+ fn raw_field<'a>(
relative: &str,
entry: &'a syn::ExprStruct,
name: &str,
@@ -2798,7 +2866,15 @@ fn expected_event_store_migration_compiler_inputs(
"{relative} future-compatible migration entry must contain field `{name}` exactly once"
));
};
- Ok(peel_expression(&field.expr))
+ Ok(&field.expr)
+ }
+
+ fn field<'a>(
+ relative: &str,
+ entry: &'a syn::ExprStruct,
+ name: &str,
+ ) -> Result<&'a syn::Expr, String> {
+ Ok(peel_expression(raw_field(relative, entry, name)?))
}
fn integer_field(relative: &str, entry: &syn::ExprStruct, name: &str) -> Result<u32, String> {
@@ -2886,9 +2962,9 @@ fn expected_event_store_migration_compiler_inputs(
"{relative} migration {version} name must be non-empty lowercase snake_case"
));
}
- if version > 2 {
- let expected_authority = if version == 3 && name == "food_availability_projection" {
- [
+ let hookless = if version > 2 {
+ let expected_authority = match (version, name.as_str()) {
+ (3, "food_availability_projection") => [
(
"hook",
"EventStoreMigrationHook::FoodAvailabilityProjectionV1",
@@ -2901,14 +2977,25 @@ fn expected_event_store_migration_compiler_inputs(
"event_contract_registry_version",
"Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION,)",
),
- ]
- } else {
- [
+ ],
+ (SOURCE_MAINTENANCE_MIGRATION_VERSION, SOURCE_MAINTENANCE_MIGRATION_NAME) => [
+ ("hook", "EventStoreMigrationHook::SourceMaintenanceV1"),
+ (
+ "hook_manifest_sha256",
+ "Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256,)",
+ ),
+ (
+ "event_contract_registry_version",
+ "Some(source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION,)",
+ ),
+ ],
+ _ => [
("hook", "EventStoreMigrationHook::None"),
("hook_manifest_sha256", "None"),
("event_contract_registry_version", "None"),
- ]
+ ],
};
+ let hookless = expected_authority[0].1 == "EventStoreMigrationHook::None";
for (field_name, expected) in expected_authority {
let actual = compact_tokens(field(relative, entry, field_name)?);
if actual != expected {
@@ -2917,7 +3004,19 @@ fn expected_event_store_migration_compiler_inputs(
));
}
}
- }
+ if hookless {
+ let replacements =
+ compact_tokens(raw_field(relative, entry, "replaced_object_names")?);
+ if replacements != "&[]" {
+ return Err(format!(
+ "{relative} hookless post-v2 migration {version} must not declare predecessor replacements without separately authenticated successor authority; found `{replacements}`"
+ ));
+ }
+ }
+ hookless
+ } else {
+ false
+ };
for direction in ["up", "down"] {
let expected_path = format!("../migrations/{version:04}_{name}.{direction}.sql");
inputs.push(include_str_field(
@@ -2926,7 +3025,7 @@ fn expected_event_store_migration_compiler_inputs(
&format!("{direction}_sql"),
&expected_path,
)?);
- if version > 3 {
+ if version > 2 && hookless {
validate_hookless_post_v2_migration_sql_isolated(
workspace_root,
version,
@@ -3608,6 +3707,10 @@ fn protected_v1_migration_object_names(workspace_root: &Path) -> Result<BTreeSet
read_regular_file(workspace_root, EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE)?;
let migrations =
parse_canonical_production_rust(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &migrations_bytes)?;
+ validate_event_store_migrations_import_authority(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &migrations,
+ )?;
for name in [
"EVENT_STORE_BASELINE_OBJECT_NAMES",
"EVENT_STORE_BASELINE_TABLE_NAMES",
@@ -5735,6 +5838,138 @@ struct PrivilegedStoreCallSite {
route: String,
}
+fn validate_exact_top_level_imports(
+ relative: &str,
+ file: &syn::File,
+ expected: &[&str],
+) -> Result<(), String> {
+ let actual = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ syn::Item::Use(item) => Some(compact_tokens(item)),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let expected = expected
+ .iter()
+ .map(|item| compact_source_tokens(item))
+ .collect::<Vec<_>>();
+ if actual != expected {
+ return Err(format!(
+ "{relative} production top-level import authority drifted: expected {expected:?}, found {actual:?}"
+ ));
+ }
+ Ok(())
+}
+
+fn validate_event_store_migrations_import_authority(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ validate_exact_top_level_imports(
+ relative,
+ file,
+ &[
+ "use crate::RadrootsEventStoreError;",
+ "use crate::generated::food_availability_projection_manifest as food_manifest;",
+ "use crate::generated::nip09_reconciliation_manifest as nip09_manifest;",
+ "use crate::generated::source_maintenance_manifest;",
+ "use sha2::{Digest, Sha256};",
+ "use std::collections::BTreeSet;",
+ ],
+ )
+}
+
+fn validate_event_store_schema_import_authority(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ validate_exact_top_level_imports(
+ relative,
+ file,
+ &[
+ "use crate::RadrootsEventStoreError;",
+ r#"use crate::migrations::{
+ EVENT_STORE_LEDGER_CREATE_DDL, EVENT_STORE_LEDGER_DDL, EVENT_STORE_LEDGER_NAME,
+ EVENT_STORE_MIGRATIONS, EventStoreMigration, EventStoreMigrationHook,
+ RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT, RADROOTS_EVENT_STORE_SCHEMA_VERSION_MIN,
+ is_event_store_governed_schema_name, is_event_store_owned_table_name,
+ migration_for_version, sqlite_identifier_starts_with,
+ validate_embedded_migration_registry, validate_migration_registry,
+ };"#,
+ "use sha2::{Digest, Sha256};",
+ "use sqlx::{Row, Sqlite, SqliteConnection, SqlitePool, Transaction};",
+ "use std::collections::{BTreeMap, BTreeSet};",
+ r#"use crate::nip09::reconciliation_v1::{
+ OsSourceGenerationProvider, ReconciliationCapacityLimits,
+ SourceGenerationProvider, apply_reconciliation_hook,
+ validate_active_hook_state_fast, validate_reconciliation_capacity,
+ };"#,
+ r#"use crate::source_maintenance_v1::{
+ apply_source_maintenance_hook_v1,
+ validate_no_persisted_ephemeral_raw_rows_v1,
+ validate_source_capacity_authority_full_v1,
+ };"#,
+ r#"use crate::store::food_availability_projection_v1::{
+ apply_food_availability_projection_hook_v1,
+ validate_food_availability_projection_hook_state_fast_v1,
+ };"#,
+ ],
+ )
+}
+
+pub(super) fn validate_current_event_store_successor_authority(
+ workspace_root: &Path,
+) -> Result<(), String> {
+ validate_privileged_store_authority(workspace_root)?;
+
+ let migrations_bytes =
+ read_regular_file(workspace_root, EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE)?;
+ let migrations =
+ parse_canonical_production_rust(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &migrations_bytes)?;
+ validate_event_store_migrations_import_authority(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &migrations,
+ )?;
+ let expected_inputs =
+ expected_event_store_migration_compiler_inputs(workspace_root, &migrations)?;
+ validate_compiler_macro_inputs(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &migrations,
+ &expected_inputs,
+ )?;
+ validate_migration_registry_reachability(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &migrations)?;
+ validate_manifest_validator_reachability(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &migrations)?;
+ validate_source_maintenance_manifest_validator_reachability(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &migrations,
+ )?;
+ validate_event_store_schema_name_matchers(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &migrations)?;
+ validate_source_maintenance_migration_bindings(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &migrations,
+ )?;
+ validate_event_store_migration_support_authority(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &migrations,
+ )?;
+
+ let schema_bytes = read_regular_file(workspace_root, EVENT_STORE_SCHEMA_SOURCE_RELATIVE)?;
+ let schema =
+ parse_canonical_production_rust(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &schema_bytes)?;
+ validate_event_store_schema_import_authority(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &schema)?;
+ validate_schema_runtime_reachability(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &schema)?;
+ validate_schema_migration_execution_authority(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &schema)?;
+ validate_source_maintenance_schema_dispatch(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &schema)?;
+ validate_source_generation_rollback_authority(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &schema)?;
+
+ let store_bytes = read_regular_file(workspace_root, EVENT_STORE_STORE_SOURCE_RELATIVE)?;
+ let store = parse_canonical_production_rust(EVENT_STORE_STORE_SOURCE_RELATIVE, &store_bytes)?;
+ validate_sqlite_encoding_preflight_authority(EVENT_STORE_STORE_SOURCE_RELATIVE, &store)?;
+ validate_source_maintenance_runtime_token_authority(workspace_root)
+}
+
fn validate_privileged_store_authority(workspace_root: &Path) -> Result<(), String> {
let lib_bytes = read_regular_file(workspace_root, EVENT_STORE_LIB_SOURCE_RELATIVE)?;
let lib_source = std::str::from_utf8(&lib_bytes).map_err(|error| {
@@ -5859,6 +6094,22 @@ fn validate_privileged_store_authority(workspace_root: &Path) -> Result<(), Stri
EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
"super::protocol_storage_v1::stored_raw_event_from_row",
),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::advance_source_capacity_after_insert_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::preflight_unique_raw_source_append_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::raw_source_capacity_delta_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1",
+ ),
]
.into_iter()
.map(|(relative, route)| (relative.to_owned(), route.to_owned()))
@@ -5871,45 +6122,89 @@ fn validate_privileged_store_authority(workspace_root: &Path) -> Result<(), Stri
let expected_calls = [
(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "associated:source_capacity_v1",
+ "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
"free:inspect_event_store_status",
"crate::schema::validate_event_store_temp_schema",
),
(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "free:configure_pool",
+ "validate_main_database_encoding",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
"free:configure_pool",
"crate::schema::validate_event_store_temp_schema",
),
(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
"free:ingest_event_in_transaction",
"crate::schema::validate_event_store_temp_schema",
),
(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
"free:ingest_event_in_transaction",
"ingest_event_protocol_reconciliation_v1",
),
(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
"free:ingest_event_in_transaction",
"PostCoreExtensionCapabilities::new",
),
(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
"free:ingest_event_in_transaction",
"dispatch_post_core_extensions",
),
(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
"free:ingest_event_in_transaction",
"validate_protocol_post_extensions",
),
- ("associated:apply_v1", "PostCoreStorageV1::new"),
- ("associated:apply_v1", "apply_post_core_extensions_v1"),
+ (
+ POST_CORE_CAPABILITIES_SOURCE_RELATIVE,
+ "associated:apply_v1",
+ "PostCoreStorageV1::new",
+ ),
+ (
+ POST_CORE_CAPABILITIES_SOURCE_RELATIVE,
+ "associated:apply_v1",
+ "apply_post_core_extensions_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:ingest_event_protocol_reconciliation_v1",
+ "validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:ingest_event_protocol_reconciliation_v1",
+ "raw_source_capacity_delta_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:ingest_event_protocol_reconciliation_v1",
+ "preflight_unique_raw_source_append_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:ingest_event_protocol_reconciliation_v1",
+ "advance_source_capacity_after_insert_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:read_protocol_post_extension_authority_seal",
+ "validate_source_capacity_authority_fast_v1",
+ ),
]
.into_iter()
- .enumerate()
- .map(|(index, (function, route))| PrivilegedStoreCallSite {
- relative: if index < 7 {
- EVENT_STORE_STORE_SOURCE_RELATIVE
- } else {
- POST_CORE_CAPABILITIES_SOURCE_RELATIVE
- }
- .to_owned(),
+ .map(|(relative, function, route)| PrivilegedStoreCallSite {
+ relative: relative.to_owned(),
function: function.to_owned(),
route: route.to_owned(),
})
@@ -5933,41 +6228,50 @@ fn validate_event_store_privileged_terminal_authority(workspace_root: &Path) ->
governed_regular_file_inventory(workspace_root, EVENT_STORE_SOURCE_ROOT_RELATIVE)?
.into_iter()
.filter(|relative| relative.ends_with(".rs"))
- .filter(|relative| !SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS.contains(&relative.as_str()))
.collect::<Vec<_>>();
let mut definitions = Vec::new();
let mut calls = Vec::new();
+ let mut imports = Vec::new();
for relative in source_paths {
let bytes = read_regular_file(workspace_root, &relative)?;
let file = parse_canonical_production_rust(&relative, &bytes)?;
- let expected_macro_inputs = match relative.as_str() {
- EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE => {
- expected_event_store_migration_compiler_inputs(workspace_root, &file)?
- }
- RESULT_VECTOR_EXECUTOR_RELATIVE => [
- "include_bytes!(\"../../../tests/fixtures/nip09_reconciliation.v1.json\")",
- "include_str!(\"../../../migrations/0001_event_store.up.sql\")",
- "include_str!(\"../../../migrations/0002_nip09.up.sql\")",
- ]
- .map(str::to_owned)
- .to_vec(),
- _ => Vec::new(),
- };
- validate_compiler_macro_inputs(&relative, &file, &expected_macro_inputs)?;
- validate_event_store_module_source_graph(&relative, &file)?;
- validate_event_store_trait_impl_authority(&relative, &file)?;
+ if !SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS.contains(&relative.as_str()) {
+ let expected_macro_inputs = match relative.as_str() {
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE => {
+ expected_event_store_migration_compiler_inputs(workspace_root, &file)?
+ }
+ RESULT_VECTOR_EXECUTOR_RELATIVE => [
+ "include_bytes!(\"../../../tests/fixtures/nip09_reconciliation.v1.json\")",
+ "include_str!(\"../../../migrations/0001_event_store.up.sql\")",
+ "include_str!(\"../../../migrations/0002_nip09.up.sql\")",
+ ]
+ .map(str::to_owned)
+ .to_vec(),
+ _ => Vec::new(),
+ };
+ validate_compiler_macro_inputs(&relative, &file, &expected_macro_inputs)?;
+ validate_event_store_module_source_graph(&relative, &file)?;
+ validate_event_store_trait_impl_authority(&relative, &file)?;
+ }
let mut audit = PrivilegedTerminalAudit {
relative: &relative,
current_function: None,
direct_callee: false,
+ scope_depth: 0,
definitions: Vec::new(),
calls: Vec::new(),
+ imports: Vec::new(),
error: None,
};
syn::visit::Visit::visit_file(&mut audit, &file);
- let (mut file_definitions, mut file_calls) = audit.finish()?;
+ let PrivilegedTerminalAuthority {
+ definitions: mut file_definitions,
+ calls: mut file_calls,
+ imports: mut file_imports,
+ } = audit.finish()?;
definitions.append(&mut file_definitions);
calls.append(&mut file_calls);
+ imports.append(&mut file_imports);
}
definitions.sort();
calls.sort_by(|left, right| {
@@ -5977,61 +6281,182 @@ fn validate_event_store_privileged_terminal_authority(workspace_root: &Path) ->
&right.route,
))
});
+ imports.sort();
- let mut expected_definitions = [
+ let mut expected_imports = [
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "self::post_core_extension_dispatcher::dispatch_post_core_extensions",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "self::protocol_reconciliation_v1::ingest_event_protocol_reconciliation_v1",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "self::protocol_reconciliation_v1::validate_protocol_post_extensions",
+ ),
+ (
+ POST_CORE_CAPABILITIES_SOURCE_RELATIVE,
+ "super::post_core_extensions_v1::apply_post_core_extensions_v1",
+ ),
(
EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
- "validate_event_store_temp_schema",
+ "crate::nip09::reconciliation_v1::validate_active_hook_state_fast",
),
(
- POST_CORE_DISPATCHER_SOURCE_RELATIVE,
- "dispatch_post_core_extensions",
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::apply_source_maintenance_hook_v1",
),
(
- POST_CORE_EXTENSION_SOURCE_RELATIVE,
- "apply_post_core_extensions_v1",
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::validate_no_persisted_ephemeral_raw_rows_v1",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::validate_source_capacity_authority_full_v1",
),
(
EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
- "ingest_event_protocol_reconciliation_v1",
+ "crate::source_maintenance_v1::advance_source_capacity_after_insert_v1",
),
(
EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
- "validate_protocol_post_extensions",
+ "crate::source_maintenance_v1::preflight_unique_raw_source_append_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::raw_source_capacity_delta_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1",
),
]
.into_iter()
- .map(|(relative, name)| PrivilegedTerminalDefinition {
- relative: relative.to_owned(),
- name: name.to_owned(),
- })
+ .map(|(relative, route)| (relative.to_owned(), route.to_owned()))
.collect::<Vec<_>>();
- expected_definitions.sort();
- if definitions != expected_definitions {
+ expected_imports.sort();
+ if imports != expected_imports {
return Err(format!(
- "event-store privileged terminal definitions drifted: expected {expected_definitions:?}, found {definitions:?}"
+ "event-store SourceMaintenance privileged import authority drifted: expected {expected_imports:?}, found {imports:?}"
));
}
- let mut expected_calls = [
+ let mut expected_definitions = [
+ (EVENT_STORE_SCHEMA_SOURCE_RELATIVE, "apply_migration_down"),
+ (EVENT_STORE_SCHEMA_SOURCE_RELATIVE, "apply_migration_hook"),
+ (EVENT_STORE_SCHEMA_SOURCE_RELATIVE, "apply_migration_up"),
(
- EVENT_STORE_STORE_SOURCE_RELATIVE,
- "free:configure_pool",
- "crate::schema::validate_event_store_temp_schema",
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "validate_event_store_temp_schema",
),
(
- EVENT_STORE_STORE_SOURCE_RELATIVE,
- "free:ingest_event_in_transaction",
- "PostCoreExtensionCapabilities::new",
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "validate_migration_hook_state",
),
(
- EVENT_STORE_STORE_SOURCE_RELATIVE,
- "free:ingest_event_in_transaction",
- "dispatch_post_core_extensions",
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "validate_rollback_preserves_source_generation_history",
),
(
EVENT_STORE_STORE_SOURCE_RELATIVE,
- "free:ingest_event_in_transaction",
+ "validate_main_database_encoding",
+ ),
+ (
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "validate_active_hook_state_fast",
+ ),
+ (
+ POST_CORE_DISPATCHER_SOURCE_RELATIVE,
+ "dispatch_post_core_extensions",
+ ),
+ (
+ POST_CORE_EXTENSION_SOURCE_RELATIVE,
+ "apply_post_core_extensions_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "ingest_event_protocol_reconciliation_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "validate_protocol_post_extensions",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "advance_source_capacity_after_insert_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "apply_source_maintenance_hook_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "bind_source_capacity_to_generation_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "preflight_source_generation_append_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "preflight_unique_raw_source_append_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "raw_source_capacity_delta_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "validate_source_capacity_authority_full_v1",
+ ),
+ ]
+ .into_iter()
+ .map(|(relative, name)| PrivilegedTerminalDefinition {
+ relative: relative.to_owned(),
+ name: name.to_owned(),
+ })
+ .collect::<Vec<_>>();
+ expected_definitions.sort();
+ if definitions != expected_definitions {
+ return Err(format!(
+ "event-store privileged terminal definitions drifted: expected {expected_definitions:?}, found {definitions:?}"
+ ));
+ }
+
+ let mut expected_calls = [
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "free:configure_pool",
+ "validate_main_database_encoding",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "free:configure_pool",
+ "crate::schema::validate_event_store_temp_schema",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "free:ingest_event_in_transaction",
+ "PostCoreExtensionCapabilities::new",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "free:ingest_event_in_transaction",
+ "dispatch_post_core_extensions",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "free:ingest_event_in_transaction",
"crate::schema::validate_event_store_temp_schema",
),
(
@@ -6059,6 +6484,146 @@ fn validate_event_store_privileged_terminal_authority(workspace_root: &Path) ->
"associated:apply_v1",
"apply_post_core_extensions_v1",
),
+ (
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "free:apply_reconciliation_hook",
+ "crate::source_maintenance_v1::bind_source_capacity_to_generation_v1",
+ ),
+ (
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "free:apply_reconciliation_hook",
+ "crate::source_maintenance_v1::preflight_source_generation_append_v1",
+ ),
+ (
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "free:apply_reconciliation_hook",
+ "validate_active_hook_state_fast",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:apply_migration_hook",
+ "apply_source_maintenance_hook_v1",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:migrate_event_store_schema_with_registry_and_generation_provider",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:migrate_schema_on_connection",
+ "apply_migration_hook",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:migrate_schema_on_connection",
+ "apply_migration_up",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:migrate_schema_on_connection",
+ "apply_migration_up",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:migrate_schema_on_connection",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:rollback_schema_on_connection",
+ "apply_migration_down",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:rollback_schema_on_connection",
+ "validate_rollback_preserves_source_generation_history",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:validate_applied_migration_hooks",
+ "validate_migration_hook_state",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:validate_migration_hook_state",
+ "validate_active_hook_state_fast",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "free:validate_migration_hook_state",
+ "validate_source_capacity_authority_full_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "free:advance_source_capacity_after_insert_v1",
+ "validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "free:apply_source_maintenance_hook_v1",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "free:apply_source_maintenance_hook_v1",
+ "validate_source_capacity_authority_full_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "free:bind_source_capacity_to_generation_v1",
+ "validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "free:preflight_source_generation_append_v1",
+ "validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "free:preflight_unique_raw_source_append_v1",
+ "validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "free:validate_source_capacity_authority_full_v1",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+ ),
+ (
+ "crates/event_store/src/source_maintenance_v1.rs",
+ "free:validate_source_capacity_authority_full_v1",
+ "validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "associated:source_capacity_v1",
+ "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:ingest_event_protocol_reconciliation_v1",
+ "advance_source_capacity_after_insert_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:ingest_event_protocol_reconciliation_v1",
+ "preflight_unique_raw_source_append_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:ingest_event_protocol_reconciliation_v1",
+ "raw_source_capacity_delta_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:ingest_event_protocol_reconciliation_v1",
+ "validate_source_capacity_authority_fast_v1",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "free:read_protocol_post_extension_authority_seal",
+ "validate_source_capacity_authority_fast_v1",
+ ),
]
.into_iter()
.map(|(relative, function, route)| PrivilegedStoreCallSite {
@@ -6174,7 +6739,7 @@ fn validate_event_store_trait_impl_authority(
"crates/event_store/src/error.rs" => &[
(
"core::fmt::Display",
- "RadrootsEventStoreReconciliationResource",
+ "RadrootsEventStoreSourceCapacityResourceV1",
),
("From<RadrootsTransportError>", "RadrootsEventStoreError"),
],
@@ -6200,7 +6765,7 @@ fn validate_event_store_trait_impl_authority(
));
}
let expected_inherent_self_types: &[&str] = match relative {
- "crates/event_store/src/error.rs" => &["RadrootsEventStoreReconciliationResource"],
+ "crates/event_store/src/error.rs" => &["RadrootsEventStoreSourceCapacityResourceV1"],
EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE => &["EventStoreMigrationHook"],
"crates/event_store/src/model.rs" => &[
"RadrootsTransportObservationMessage",
@@ -6230,6 +6795,9 @@ fn validate_event_store_trait_impl_authority(
"TransitionOrigin",
],
EVENT_STORE_STORE_SOURCE_RELATIVE => &["RadrootsEventStore"],
+ "crates/event_store/src/source_maintenance_v1.rs" => {
+ &["RadrootsEventStoreSourceCapacityV1"]
+ }
POST_CORE_CAPABILITIES_SOURCE_RELATIVE => &["PostCoreExtensionCapabilities<'borrow,'db>"],
POST_CORE_STORAGE_SOURCE_RELATIVE => {
&["TradeProjectionWrite<'a>", "PostCoreStorageV1<'borrow,'db>"]
@@ -6273,6 +6841,32 @@ fn validate_event_store_trait_impl_authority(
&food_manifest_arm.body,
"Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256)",
)?;
+ let source_id_arm = exact_associated_match_arm(
+ relative,
+ file,
+ "EventStoreMigrationHook",
+ "id",
+ "SourceMaintenanceV1",
+ )?;
+ validate_exact_arm_expression(
+ relative,
+ "EventStoreMigrationHook::id SourceMaintenanceV1 arm",
+ &source_id_arm.body,
+ "source_maintenance_manifest::SOURCE_MAINTENANCE_HOOK_ID",
+ )?;
+ let source_manifest_arm = exact_associated_match_arm(
+ relative,
+ file,
+ "EventStoreMigrationHook",
+ "manifest_sha256",
+ "SourceMaintenanceV1",
+ )?;
+ validate_exact_arm_expression(
+ relative,
+ "EventStoreMigrationHook::manifest_sha256 SourceMaintenanceV1 arm",
+ &source_manifest_arm.body,
+ "Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256)",
+ )?;
let migration_impls = file
.items
@@ -6294,6 +6888,7 @@ fn validate_event_store_trait_impl_authority(
};
let mut predecessor_projection = (*migration_impl).clone();
let mut removed_food_arms = 0usize;
+ let mut removed_source_maintenance_arms = 0usize;
for item in &mut predecessor_projection.items {
let syn::ImplItem::Fn(function) = item else {
continue;
@@ -6310,6 +6905,14 @@ fn validate_event_store_trait_impl_authority(
.cloned()
.collect();
removed_food_arms += before - expression.arms.len();
+ let before = expression.arms.len();
+ expression.arms = expression
+ .arms
+ .iter()
+ .filter(|arm| !syntax_contains_ident(&arm.pat, "SourceMaintenanceV1"))
+ .cloned()
+ .collect();
+ removed_source_maintenance_arms += before - expression.arms.len();
}
}
if removed_food_arms != 2 {
@@ -6317,6 +6920,11 @@ fn validate_event_store_trait_impl_authority(
"{relative} successor migration hook impl must add exactly two FoodAvailabilityProjectionV1 arms; found {removed_food_arms}"
));
}
+ if removed_source_maintenance_arms != 2 {
+ return Err(format!(
+ "{relative} authenticated SourceMaintenance migration hook impl must add exactly two SourceMaintenanceV1 arms; found {removed_source_maintenance_arms}"
+ ));
+ }
let predecessor_inherent_impls = vec![compact_tokens(&predecessor_projection)];
let actual_sha256 = sha256_hex(&canonical_json_bytes(&predecessor_inherent_impls)?);
if actual_sha256 != EVENT_STORE_MIGRATION_IMPL_BASELINE_SHA256 {
@@ -6332,22 +6940,29 @@ struct PrivilegedTerminalAudit<'a> {
relative: &'a str,
current_function: Option<String>,
direct_callee: bool,
+ scope_depth: usize,
definitions: Vec<PrivilegedTerminalDefinition>,
calls: Vec<PrivilegedStoreCallSite>,
+ imports: Vec<(String, String)>,
error: Option<String>,
}
+struct PrivilegedTerminalAuthority {
+ definitions: Vec<PrivilegedTerminalDefinition>,
+ calls: Vec<PrivilegedStoreCallSite>,
+ imports: Vec<(String, String)>,
+}
+
impl PrivilegedTerminalAudit<'_> {
- fn finish(
- self,
- ) -> Result<
- (
- Vec<PrivilegedTerminalDefinition>,
- Vec<PrivilegedStoreCallSite>,
- ),
- String,
- > {
- self.error.map_or(Ok((self.definitions, self.calls)), Err)
+ fn finish(self) -> Result<PrivilegedTerminalAuthority, String> {
+ if let Some(error) = self.error {
+ return Err(error);
+ }
+ Ok(PrivilegedTerminalAuthority {
+ definitions: self.definitions,
+ calls: self.calls,
+ imports: self.imports,
+ })
}
fn fail(&mut self, reason: impl Into<String>) {
@@ -6365,6 +6980,14 @@ impl<'ast> syn::visit::Visit<'ast> for PrivilegedTerminalAudit<'_> {
fn visit_item_fn(&mut self, function: &'ast syn::ItemFn) {
let name = function.sig.ident.to_string();
if is_privileged_terminal(&name) {
+ if self.current_function.is_some()
+ || !is_authoritative_privileged_terminal_definition(self.relative, &name)
+ {
+ self.fail(format!(
+ "shadows privileged authority with function `{name}`"
+ ));
+ return;
+ }
self.definitions.push(PrivilegedTerminalDefinition {
relative: self.relative.to_owned(),
name: name.clone(),
@@ -6376,6 +6999,13 @@ impl<'ast> syn::visit::Visit<'ast> for PrivilegedTerminalAudit<'_> {
}
fn visit_impl_item_fn(&mut self, function: &'ast syn::ImplItemFn) {
+ if is_privileged_terminal(&function.sig.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with associated function `{}`",
+ function.sig.ident
+ ));
+ return;
+ }
let previous = self
.current_function
.replace(format!("associated:{}", function.sig.ident));
@@ -6384,6 +7014,13 @@ impl<'ast> syn::visit::Visit<'ast> for PrivilegedTerminalAudit<'_> {
}
fn visit_trait_item_fn(&mut self, function: &'ast syn::TraitItemFn) {
+ if is_privileged_terminal(&function.sig.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with trait function `{}`",
+ function.sig.ident
+ ));
+ return;
+ }
let previous = self
.current_function
.replace(format!("trait:{}", function.sig.ident));
@@ -6394,89 +7031,216 @@ impl<'ast> syn::visit::Visit<'ast> for PrivilegedTerminalAudit<'_> {
fn visit_item_use(&mut self, item_use: &'ast syn::ItemUse) {
let mut routes = Vec::new();
flatten_use_tree("", &item_use.tree, &mut routes);
+ if let Some(route) = routes.iter().find(|route| route.ends_with("::*")) {
+ self.fail(format!(
+ "uses unauditable glob import `{route}` in privileged source scope"
+ ));
+ return;
+ }
if let Some(route) = routes.iter().find(|route| {
let source = route
.split_once(" as ")
.map_or(route.as_str(), |(source, _)| source);
route.contains(" as ")
- && use_route_local_binding(source)
+ && (use_route_local_binding(source)
.is_some_and(is_privileged_terminal_or_storage_type)
+ || use_route_local_binding(route)
+ .is_some_and(is_privileged_terminal_or_storage_type))
}) {
self.fail(format!(
"aliases or reexports privileged source terminal through `{route}`"
));
return;
}
- syn::visit::visit_item_use(self, item_use);
- }
-
- fn visit_expr_call(&mut self, expression: &'ast syn::ExprCall) {
- if let Some(route) = direct_expression_call_route(expression)
- && (route
- .rsplit("::")
- .next()
- .is_some_and(is_privileged_terminal)
- || route
- .split("::")
- .collect::<Vec<_>>()
- .windows(2)
- .any(|segments| {
- segments == ["PostCoreExtensionCapabilities", "new"]
- || segments == ["PostCoreStorageV1", "new"]
- }))
+ for route in routes
+ .iter()
+ .filter(|route| use_route_local_binding(route).is_some_and(is_privileged_terminal))
{
- let Some(function) = self.current_function.clone() else {
+ if self.scope_depth != 0
+ || !is_inherited_visibility(&item_use.vis)
+ || !item_use.attrs.is_empty()
+ || !is_approved_privileged_terminal_import(self.relative, route)
+ {
self.fail(format!(
- "calls privileged terminal `{route}` outside a function"
+ "privileged terminal import `{route}` must be an exact private top-level approved route"
));
return;
- };
- self.calls.push(PrivilegedStoreCallSite {
- relative: self.relative.to_owned(),
- function,
- route,
- });
- let previous = self.direct_callee;
- self.direct_callee = true;
- syn::visit::Visit::visit_expr(self, expression.func.as_ref());
- self.direct_callee = previous;
- for argument in &expression.args {
- syn::visit::Visit::visit_expr(self, argument);
}
- return;
+ self.imports
+ .push((self.relative.to_owned(), route.to_owned()));
}
- syn::visit::visit_expr_call(self, expression);
- }
-
- fn visit_expr_path(&mut self, expression: &'ast syn::ExprPath) {
- if expression
- .path
- .segments
- .last()
- .is_some_and(|segment| is_privileged_terminal(&segment.ident.to_string()))
- && !self.direct_callee
- {
+ if let Some(route) = routes.iter().find(|route| {
+ use_route_local_binding(route).is_some_and(is_privileged_terminal_or_storage_type)
+ && (self.scope_depth != 0 || !is_inherited_visibility(&item_use.vis))
+ }) {
self.fail(format!(
- "takes or aliases privileged terminal value `{}`",
- compact_tokens(expression)
+ "privileged terminal import `{route}` must remain private and top-level"
));
return;
}
- syn::visit::visit_expr_path(self, expression);
+ syn::visit::visit_item_use(self, item_use);
}
- fn visit_macro(&mut self, item: &'ast syn::Macro) {
- if [
- "validate_event_store_temp_schema",
- "ingest_event_protocol_reconciliation_v1",
- "dispatch_post_core_extensions",
- "apply_post_core_extensions_v1",
- "validate_protocol_post_extensions",
- "PostCoreExtensionCapabilities",
- "PostCoreStorageV1",
- ]
- .iter()
- .any(|name| syntax_contains_ident(item, name))
+ fn visit_block(&mut self, block: &'ast syn::Block) {
+ self.scope_depth += 1;
+ syn::visit::visit_block(self, block);
+ self.scope_depth -= 1;
+ }
+
+ fn visit_item_mod(&mut self, module: &'ast syn::ItemMod) {
+ if is_privileged_terminal(&module.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with module `{}`",
+ module.ident
+ ));
+ return;
+ }
+ if module.content.is_some() {
+ self.scope_depth += 1;
+ syn::visit::visit_item_mod(self, module);
+ self.scope_depth -= 1;
+ } else {
+ syn::visit::visit_item_mod(self, module);
+ }
+ }
+
+ fn visit_pat_ident(&mut self, pattern: &'ast syn::PatIdent) {
+ if is_privileged_terminal(&pattern.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with binding `{}`",
+ pattern.ident
+ ));
+ return;
+ }
+ syn::visit::visit_pat_ident(self, pattern);
+ }
+
+ fn visit_item_const(&mut self, item: &'ast syn::ItemConst) {
+ if is_privileged_terminal(&item.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with const `{}`",
+ item.ident
+ ));
+ return;
+ }
+ syn::visit::visit_item_const(self, item);
+ }
+
+ fn visit_item_static(&mut self, item: &'ast syn::ItemStatic) {
+ if is_privileged_terminal(&item.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with static `{}`",
+ item.ident
+ ));
+ return;
+ }
+ syn::visit::visit_item_static(self, item);
+ }
+
+ fn visit_item_type(&mut self, item: &'ast syn::ItemType) {
+ if is_privileged_terminal(&item.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with type alias `{}`",
+ item.ident
+ ));
+ return;
+ }
+ syn::visit::visit_item_type(self, item);
+ }
+
+ fn visit_item_struct(&mut self, item: &'ast syn::ItemStruct) {
+ if is_privileged_terminal(&item.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with struct constructor `{}`",
+ item.ident
+ ));
+ return;
+ }
+ syn::visit::visit_item_struct(self, item);
+ }
+
+ fn visit_item_enum(&mut self, item: &'ast syn::ItemEnum) {
+ if is_privileged_terminal(&item.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with enum `{}`",
+ item.ident
+ ));
+ return;
+ }
+ syn::visit::visit_item_enum(self, item);
+ }
+
+ fn visit_item_union(&mut self, item: &'ast syn::ItemUnion) {
+ if is_privileged_terminal(&item.ident.to_string()) {
+ self.fail(format!(
+ "shadows privileged authority with union `{}`",
+ item.ident
+ ));
+ return;
+ }
+ syn::visit::visit_item_union(self, item);
+ }
+
+ fn visit_expr_call(&mut self, expression: &'ast syn::ExprCall) {
+ if let Some(route) = direct_expression_call_route(expression)
+ && (route
+ .rsplit("::")
+ .next()
+ .is_some_and(is_privileged_terminal)
+ || route
+ .split("::")
+ .collect::<Vec<_>>()
+ .windows(2)
+ .any(|segments| {
+ segments == ["PostCoreExtensionCapabilities", "new"]
+ || segments == ["PostCoreStorageV1", "new"]
+ }))
+ {
+ let Some(function) = self.current_function.clone() else {
+ self.fail(format!(
+ "calls privileged terminal `{route}` outside a function"
+ ));
+ return;
+ };
+ self.calls.push(PrivilegedStoreCallSite {
+ relative: self.relative.to_owned(),
+ function,
+ route,
+ });
+ let previous = self.direct_callee;
+ self.direct_callee = true;
+ syn::visit::Visit::visit_expr(self, expression.func.as_ref());
+ self.direct_callee = previous;
+ for argument in &expression.args {
+ syn::visit::Visit::visit_expr(self, argument);
+ }
+ return;
+ }
+ syn::visit::visit_expr_call(self, expression);
+ }
+
+ fn visit_expr_path(&mut self, expression: &'ast syn::ExprPath) {
+ if expression
+ .path
+ .segments
+ .last()
+ .is_some_and(|segment| is_privileged_terminal(&segment.ident.to_string()))
+ && !self.direct_callee
+ {
+ self.fail(format!(
+ "takes or aliases privileged terminal value `{}`",
+ compact_tokens(expression)
+ ));
+ return;
+ }
+ syn::visit::visit_expr_path(self, expression);
+ }
+
+ fn visit_macro(&mut self, item: &'ast syn::Macro) {
+ if PRIVILEGED_TERMINAL_NAMES
+ .iter()
+ .chain(["PostCoreExtensionCapabilities", "PostCoreStorageV1"].iter())
+ .any(|name| syntax_contains_ident(item, name))
{
self.fail(format!(
"references privileged terminal through macro `{}`",
@@ -6489,14 +7253,7 @@ impl<'ast> syn::visit::Visit<'ast> for PrivilegedTerminalAudit<'_> {
}
fn is_privileged_terminal(name: &str) -> bool {
- matches!(
- name,
- "validate_event_store_temp_schema"
- | "ingest_event_protocol_reconciliation_v1"
- | "dispatch_post_core_extensions"
- | "apply_post_core_extensions_v1"
- | "validate_protocol_post_extensions"
- )
+ PRIVILEGED_TERMINAL_NAMES.contains(&name)
}
fn is_privileged_terminal_or_storage_type(name: &str) -> bool {
@@ -6504,6 +7261,95 @@ fn is_privileged_terminal_or_storage_type(name: &str) -> bool {
|| matches!(name, "PostCoreExtensionCapabilities" | "PostCoreStorageV1")
}
+fn is_authoritative_privileged_terminal_definition(relative: &str, name: &str) -> bool {
+ matches!(
+ (relative, name),
+ (EVENT_STORE_SCHEMA_SOURCE_RELATIVE, "apply_migration_down")
+ | (EVENT_STORE_SCHEMA_SOURCE_RELATIVE, "apply_migration_hook")
+ | (EVENT_STORE_SCHEMA_SOURCE_RELATIVE, "apply_migration_up")
+ | (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "validate_event_store_temp_schema"
+ )
+ | (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "validate_migration_hook_state"
+ )
+ | (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "validate_rollback_preserves_source_generation_history"
+ )
+ | (
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "validate_active_hook_state_fast"
+ )
+ | (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "validate_main_database_encoding"
+ )
+ | (
+ POST_CORE_DISPATCHER_SOURCE_RELATIVE,
+ "dispatch_post_core_extensions"
+ )
+ | (
+ POST_CORE_EXTENSION_SOURCE_RELATIVE,
+ "apply_post_core_extensions_v1"
+ )
+ | (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "ingest_event_protocol_reconciliation_v1"
+ )
+ | (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "validate_protocol_post_extensions"
+ )
+ ) || (relative == "crates/event_store/src/source_maintenance_v1.rs"
+ && SOURCE_MAINTENANCE_PRIVILEGED_TERMINALS.contains(&name))
+}
+
+fn is_approved_privileged_terminal_import(relative: &str, route: &str) -> bool {
+ matches!(
+ (relative, route),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "self::post_core_extension_dispatcher::dispatch_post_core_extensions"
+ ) | (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "self::protocol_reconciliation_v1::ingest_event_protocol_reconciliation_v1"
+ ) | (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "self::protocol_reconciliation_v1::validate_protocol_post_extensions"
+ ) | (
+ POST_CORE_CAPABILITIES_SOURCE_RELATIVE,
+ "super::post_core_extensions_v1::apply_post_core_extensions_v1"
+ ) | (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "crate::nip09::reconciliation_v1::validate_active_hook_state_fast"
+ ) | (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::apply_source_maintenance_hook_v1"
+ ) | (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::validate_no_persisted_ephemeral_raw_rows_v1"
+ ) | (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::validate_source_capacity_authority_full_v1"
+ ) | (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::advance_source_capacity_after_insert_v1"
+ ) | (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::preflight_unique_raw_source_append_v1"
+ ) | (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::raw_source_capacity_delta_v1"
+ ) | (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1"
+ )
+ )
+}
+
fn validate_event_store_lib_resolution_authority(
relative: &str,
file: &syn::File,
@@ -6552,7 +7398,21 @@ fn validate_event_store_lib_resolution_authority(
));
}
}
- let required_module_names = required_modules.into_iter().collect::<BTreeSet<_>>();
+ let predecessor_module_names = required_modules.into_iter().collect::<BTreeSet<_>>();
+ if !predecessor_module_names.is_subset(
+ &module_names
+ .iter()
+ .map(String::as_str)
+ .collect::<BTreeSet<_>>(),
+ ) {
+ return Err(format!(
+ "{relative} must retain every predecessor-governed private `sqlite` module; found {module_names:?}"
+ ));
+ }
+ let required_module_names = predecessor_module_names
+ .into_iter()
+ .chain(SUCCESSOR_08D_LIB_MODULES)
+ .collect::<BTreeSet<_>>();
if module_names
.iter()
.map(String::as_str)
@@ -6560,7 +7420,7 @@ fn validate_event_store_lib_resolution_authority(
!= required_module_names
{
return Err(format!(
- "{relative} must contain exactly the seven governed private `sqlite` modules; found {module_names:?}"
+ "{relative} must contain exactly the predecessor modules plus the authenticated SourceMaintenance private `sqlite` module; found {module_names:?}"
));
}
@@ -6615,8 +7475,11 @@ fn validate_event_store_lib_resolution_authority(
"{relative} reexports duplicate local binding `{binding}`"
));
}
- if !EVENT_STORE_FIXED_PUBLIC_REEXPORTS.contains(&route.as_str())
+ let inherited_current = EVENT_STORE_FIXED_PUBLIC_REEXPORTS.contains(&route.as_str())
+ && !SUCCESSOR_08D_RETIRED_PUBLIC_REEXPORTS.contains(&route.as_str());
+ if !inherited_current
&& !SUCCESSOR_08C_PUBLIC_REEXPORTS.contains(&route.as_str())
+ && !SUCCESSOR_08D_PUBLIC_REEXPORTS.contains(&route.as_str())
{
return Err(format!(
"{relative} public export inventory is closed for this contract version; found unsupported reexport `{route}`"
@@ -6627,7 +7490,9 @@ fn validate_event_store_lib_resolution_authority(
}
let expected_uses = EVENT_STORE_FIXED_PUBLIC_REEXPORTS
.into_iter()
+ .filter(|route| !SUCCESSOR_08D_RETIRED_PUBLIC_REEXPORTS.contains(route))
.chain(SUCCESSOR_08C_PUBLIC_REEXPORTS)
+ .chain(SUCCESSOR_08D_PUBLIC_REEXPORTS)
.map(str::to_owned)
.collect::<BTreeSet<_>>();
let actual_use_set = actual_uses.iter().cloned().collect::<BTreeSet<_>>();
@@ -6784,8 +7649,12 @@ fn is_privileged_store_import_route(route: &str) -> bool {
]
.iter()
.any(|segment| source_route.split("::").any(|actual| actual == *segment))
- || use_route_local_binding(source_route).is_some_and(is_governed_store_import_binding)
- || use_route_local_binding(route).is_some_and(is_governed_store_import_binding)
+ || use_route_local_binding(source_route).is_some_and(|binding| {
+ is_governed_store_import_binding(binding) || is_privileged_terminal(binding)
+ })
+ || use_route_local_binding(route).is_some_and(|binding| {
+ is_governed_store_import_binding(binding) || is_privileged_terminal(binding)
+ })
|| route.ends_with("::*")
}
@@ -6821,14 +7690,7 @@ fn is_governed_store_import_binding(binding: &str) -> bool {
}
fn is_privileged_store_value_binding(binding: &str) -> bool {
- matches!(
- binding,
- "dispatch_post_core_extensions"
- | "apply_post_core_extensions_v1"
- | "ingest_event_protocol_reconciliation_v1"
- | "validate_event_store_temp_schema"
- | "validate_protocol_post_extensions"
- )
+ is_privileged_terminal(binding)
}
struct PrivilegedStoreReferenceAudit<'a> {
@@ -7215,6 +8077,9 @@ fn is_authoritative_privileged_store_definition(relative: &str, name: &str) -> b
matches!(
(relative, name),
(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "validate_main_database_encoding"
+ ) | (
POST_CORE_DISPATCHER_SOURCE_RELATIVE,
"dispatch_post_core_extensions"
) | (
@@ -7265,35 +8130,24 @@ fn is_allowed_privileged_store_attribute(attribute: &syn::Attribute) -> bool {
}
fn is_privileged_store_call_route(route: &str) -> bool {
- matches!(
- route.rsplit("::").next(),
- Some(
- "ingest_event_protocol_reconciliation_v1"
- | "dispatch_post_core_extensions"
- | "apply_post_core_extensions_v1"
- | "validate_protocol_post_extensions"
- | "validate_event_store_temp_schema"
- )
- ) || route
- .split("::")
- .collect::<Vec<_>>()
- .windows(2)
- .any(|segments| {
- segments == ["PostCoreExtensionCapabilities", "new"]
- || segments == ["PostCoreStorageV1", "new"]
- })
+ route
+ .rsplit("::")
+ .next()
+ .is_some_and(is_privileged_terminal)
+ || route
+ .split("::")
+ .collect::<Vec<_>>()
+ .windows(2)
+ .any(|segments| {
+ segments == ["PostCoreExtensionCapabilities", "new"]
+ || segments == ["PostCoreStorageV1", "new"]
+ })
}
fn expression_contains_privileged_store_authority(node: &impl ToTokens) -> bool {
- [
- "ingest_event_protocol_reconciliation_v1",
- "dispatch_post_core_extensions",
- "apply_post_core_extensions_v1",
- "validate_protocol_post_extensions",
- "validate_event_store_temp_schema",
- ]
- .iter()
- .any(|ident| syntax_contains_ident(node, ident))
+ PRIVILEGED_TERMINAL_NAMES
+ .iter()
+ .any(|ident| syntax_contains_ident(node, ident))
|| (["PostCoreExtensionCapabilities", "PostCoreStorageV1"]
.iter()
.any(|name| syntax_contains_ident(node, name))
@@ -10984,9 +11838,10 @@ fn validate_migration_registry_reachability(
let statements = &function.block.stmts;
let ledger_guard = "if EVENT_STORE_LEDGER_CREATE_DDL.strip_prefix(\"CREATE TABLE main.\")!=EVENT_STORE_LEDGER_DDL.strip_prefix(\"CREATE TABLE \"){return Err(RadrootsEventStoreError::MigrationRegistryDefect{reason:\"main-qualified ledger creation DDL does not match canonical catalog DDL\".to_owned(),});}";
let predecessor_manifest_guard = "if registry.iter().any(|migration|{migration.hook==EventStoreMigrationHook::Nip09ReconciliationV1}){validate_generated_nip09_manifest_descriptor()?;}";
- let successor_manifest_guard = "if registry.iter().any(|migration|{migration.hook==EventStoreMigrationHook::FoodAvailabilityProjectionV1}){validate_generated_food_availability_projection_manifest_descriptor()?;}";
+ let food_manifest_guard = "if registry.iter().any(|migration|{migration.hook==EventStoreMigrationHook::FoodAvailabilityProjectionV1}){validate_generated_food_availability_projection_manifest_descriptor()?;}";
+ let source_maintenance_manifest_guard = "if registry.iter().any(|migration|migration.hook==EventStoreMigrationHook::SourceMaintenanceV1){validate_generated_source_maintenance_manifest_descriptor()?;}";
let range_guard = "if minimum==0||current<minimum||registry.is_empty(){return Err(RadrootsEventStoreError::MigrationRegistryDefect{reason:format!(\"migration version range {minimum}..={current} requires a non-empty positive registry\"),});}";
- let valid = statements.len() == 10
+ let valid = statements.len() == 12
&& statements.first().is_some_and(|statement| {
compact_tokens(statement) == compact_source_tokens(ledger_guard)
})
@@ -10994,41 +11849,50 @@ fn validate_migration_registry_reachability(
compact_tokens(statement) == compact_source_tokens(predecessor_manifest_guard)
})
&& statements.get(2).is_some_and(|statement| {
- compact_tokens(statement) == compact_source_tokens(successor_manifest_guard)
+ compact_tokens(statement) == compact_source_tokens(food_manifest_guard)
})
&& statements.get(3).is_some_and(|statement| {
+ compact_tokens(statement) == compact_source_tokens(source_maintenance_manifest_guard)
+ })
+ && statements.get(4).is_some_and(|statement| {
compact_tokens(statement) == compact_source_tokens(range_guard)
})
&& matches!(
- statements.get(4),
+ statements.get(5),
Some(syn::Stmt::Local(local))
if local_pattern_ident(&local.pat).as_deref() == Some("expected_version")
)
&& matches!(
- statements.get(5),
+ statements.get(6),
Some(syn::Stmt::Local(local))
if local_pattern_ident(&local.pat).as_deref() == Some("owned_object_names")
)
&& matches!(
- statements.get(6),
+ statements.get(7),
Some(syn::Stmt::Local(local))
if local_pattern_ident(&local.pat).as_deref() == Some("owned_table_names")
)
&& matches!(
- statements.get(7),
+ statements.get(8),
+ Some(syn::Stmt::Local(local))
+ if local_pattern_ident(&local.pat).as_deref() == Some("migration_hook_ids")
+ )
+ && matches!(
+ statements.get(9),
Some(syn::Stmt::Expr(syn::Expr::ForLoop(_), _))
)
&& matches!(
- statements.get(8),
+ statements.get(10),
Some(syn::Stmt::Expr(syn::Expr::If(_), _))
)
&& statements
- .get(9)
+ .get(11)
.and_then(direct_statement_expression)
.is_some_and(|expression| compact_tokens(expression) == "Ok(())");
if !valid {
return Err(format!(
- "{relative} `validate_migration_registry` authoritative top-level statement skeleton drifted"
+ "{relative} `validate_migration_registry` authoritative top-level statement skeleton drifted: found {:?}",
+ statements.iter().map(compact_tokens).collect::<Vec<_>>()
));
}
Ok(())
@@ -11097,9 +11961,852 @@ fn validate_manifest_validator_reachability(
Ok(())
}
-fn validate_schema_runtime_reachability<'a>(
+fn validate_source_maintenance_manifest_validator_reachability(
relative: &str,
- file: &'a syn::File,
+ file: &syn::File,
+) -> Result<(), String> {
+ const EXPECTED_TOKEN_SHA256: &str =
+ "711c977666d6a7e3ce3c1759e6ca7a9811bab9690bffda8994b605b8f6c539a2";
+
+ let function = exact_top_level_function(
+ relative,
+ file,
+ "validate_generated_source_maintenance_manifest_descriptor",
+ )?;
+ let statements = &function.block.stmts;
+ let expected_locals = [
+ (1, "bytes"),
+ (5, "manifest"),
+ (6, "expected_numbers"),
+ (7, "expected_strings"),
+ (8, "numbers_match"),
+ (9, "strings_match"),
+ (10, "string_array_matches"),
+ ];
+ let valid = statements.len() == 14
+ && matches!(statements.first(), Some(syn::Stmt::Item(syn::Item::Use(_))))
+ && expected_locals.iter().all(|(index, name)| {
+ matches!(
+ statements.get(*index),
+ Some(syn::Stmt::Local(local))
+ if local_pattern_ident(&local.pat).as_deref() == Some(*name)
+ )
+ })
+ && matches!(
+ statements.get(2),
+ Some(syn::Stmt::Expr(syn::Expr::If(_), _))
+ )
+ && statements
+ .get(3)
+ .and_then(direct_statement_expression)
+ .and_then(|expression| direct_try_function_call(expression, "validate_sha256_literal"))
+ .is_some()
+ && matches!(
+ statements.get(4),
+ Some(syn::Stmt::Expr(syn::Expr::If(_), _))
+ )
+ && matches!(
+ statements.get(11),
+ Some(syn::Stmt::Expr(syn::Expr::If(_), _))
+ )
+ && matches!(
+ statements.get(12),
+ Some(syn::Stmt::Expr(syn::Expr::ForLoop(_), _))
+ )
+ && statements
+ .get(13)
+ .and_then(direct_statement_expression)
+ .is_some_and(|expression| compact_tokens(expression) == "Ok(())");
+ if !valid {
+ return Err(format!(
+ "{relative} generated SourceMaintenance manifest validator authoritative statement skeleton drifted"
+ ));
+ }
+
+ for (binding, accessor) in [("numbers_match", "as_u64"), ("strings_match", "as_str")] {
+ let expression = statements
+ .iter()
+ .find_map(|statement| match statement {
+ syn::Stmt::Local(local)
+ if local_pattern_ident(&local.pat).as_deref() == Some(binding) =>
+ {
+ local.init.as_ref().map(|init| init.expr.as_ref())
+ }
+ _ => None,
+ })
+ .expect("validated descriptor local");
+ validate_manifest_pointer_check(relative, binding, expression, accessor)?;
+ }
+ let array_matcher = statements
+ .get(10)
+ .and_then(direct_statement_expression)
+ .ok_or_else(|| format!("{relative} SourceMaintenance array matcher is missing"))?;
+ use syn::visit::Visit;
+ let mut array_routes = RustCallRouteCollector { routes: Vec::new() };
+ array_routes.visit_expr(array_matcher);
+ for route in [
+ "method:pointer",
+ "method:as_array",
+ "method:is_some_and",
+ "method:zip",
+ ] {
+ if !array_routes
+ .routes
+ .iter()
+ .any(|candidate| candidate == route)
+ {
+ return Err(format!(
+ "{relative} SourceMaintenance array matcher is missing semantic route `{route}`"
+ ));
+ }
+ }
+ let digest_loop = match statements.get(12) {
+ Some(syn::Stmt::Expr(syn::Expr::ForLoop(expression), _)) => expression,
+ _ => unreachable!("validated descriptor loop"),
+ };
+ if digest_loop.body.stmts.len() != 1
+ || digest_loop
+ .body
+ .stmts
+ .first()
+ .and_then(direct_statement_expression)
+ .and_then(|expression| direct_try_function_call(expression, "validate_sha256_literal"))
+ .is_none()
+ {
+ return Err(format!(
+ "{relative} SourceMaintenance descriptor digest loop must directly propagate validate_sha256_literal"
+ ));
+ }
+
+ let actual_sha256 = sha256_hex(compact_tokens(function).as_bytes());
+ if actual_sha256 != EXPECTED_TOKEN_SHA256 {
+ return Err(format!(
+ "{relative} generated SourceMaintenance manifest validator exact token authority drifted: expected {EXPECTED_TOKEN_SHA256}, found {actual_sha256}"
+ ));
+ }
+ Ok(())
+}
+
+fn validate_source_maintenance_migration_bindings(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ let entry = exact_const_struct_array_element(
+ relative,
+ file,
+ "EVENT_STORE_MIGRATIONS",
+ "version",
+ u64::from(SOURCE_MAINTENANCE_MIGRATION_VERSION),
+ )?;
+ let expected_entry = r#"EventStoreMigration {
+ version: 4,
+ name: "source_maintenance",
+ up_sql: include_str!("../migrations/0004_source_maintenance.up.sql"),
+ down_sql: include_str!("../migrations/0004_source_maintenance.down.sql"),
+ up_len: source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_BYTE_LENGTH,
+ down_len: source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_BYTE_LENGTH,
+ up_sha256: source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_SHA256,
+ down_sha256: source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_SHA256,
+ schema_sha256: source_maintenance_manifest::SOURCE_MAINTENANCE_SCHEMA_SHA256,
+ owned_object_names: EVENT_STORE_SOURCE_MAINTENANCE_OBJECT_NAMES,
+ replaced_object_names: EVENT_STORE_SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES,
+ owned_table_names: EVENT_STORE_SOURCE_MAINTENANCE_TABLE_NAMES,
+ fts5_table_names: &[],
+ hook: EventStoreMigrationHook::SourceMaintenanceV1,
+ hook_manifest_sha256: Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256,),
+ event_contract_registry_version: Some(
+ source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION,
+ ),
+ }"#;
+ let actual_entry = compact_tokens(entry);
+ let expected_entry = compact_source_tokens(expected_entry);
+ if actual_entry != expected_entry {
+ return Err(format!(
+ "{relative} SourceMaintenance v4 migration entry authority drifted: expected `{expected_entry}`, found `{actual_entry}`"
+ ));
+ }
+
+ let loop_expression = exact_direct_for_loop(
+ relative,
+ file,
+ "validate_migration_registry",
+ "(index,migration)",
+ "registry.iter().enumerate()",
+ )?;
+ let arm = exact_direct_loop_match_arm(
+ relative,
+ "validate_migration_registry",
+ loop_expression,
+ &[
+ "migration.hook",
+ "migration.hook_manifest_sha256",
+ "migration.event_contract_registry_version",
+ ],
+ "SourceMaintenanceV1",
+ )?;
+ let expected_pattern = r#"(EventStoreMigrationHook::None, None, None)
+ | (
+ EventStoreMigrationHook::Nip09ReconciliationV1,
+ Some(nip09_manifest::NIP09_RECONCILIATION_MANIFEST_SHA256),
+ Some(nip09_manifest::NIP09_RECONCILIATION_EVENT_CONTRACT_REGISTRY_VERSION,),
+ )
+ | (
+ EventStoreMigrationHook::FoodAvailabilityProjectionV1,
+ Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256),
+ Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION,),
+ )
+ | (
+ EventStoreMigrationHook::SourceMaintenanceV1,
+ Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256),
+ Some(source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION,),
+ )"#;
+ if compact_tokens(&arm.pat) != compact_source_tokens(expected_pattern)
+ || arm.guard.is_some()
+ || compact_tokens(&arm.body) != "{}"
+ {
+ return Err(format!(
+ "{relative} SourceMaintenance registry tuple authority drifted: expected pattern `{}`, found pattern `{}`, guard {:?}, body `{}`",
+ compact_source_tokens(expected_pattern),
+ compact_tokens(&arm.pat),
+ arm.guard
+ .as_ref()
+ .map(|(_, expression)| compact_tokens(expression)),
+ compact_tokens(&arm.body),
+ ));
+ }
+ Ok(())
+}
+
+fn validate_event_store_migration_support_authority(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ const EXPECTED: [(&str, &str); 9] = [
+ (
+ "EVENT_STORE_LEDGER_DDL",
+ "adb8845fa244f2d4503fd52eeea9488c214da9375727a0e77905233ad3d5b701",
+ ),
+ (
+ "EVENT_STORE_LEDGER_CREATE_DDL",
+ "ecaced87b78196cc220fb2c785a7ee2db047bf08a27876066758f79a48ea8648",
+ ),
+ (
+ "EVENT_STORE_BASELINE_FTS5_TABLE_NAMES",
+ "4ab01dfd843eb33e82fae3d9503000f9c0292ce4e6f61f18aeee33ebc15360d3",
+ ),
+ (
+ "EventStoreMigration",
+ "3552624482aa3c698ebcc88e5d3497e35d30d3646ea0605d2c192acc21006ee2",
+ ),
+ (
+ "EVENT_STORE_MIGRATIONS[version=1]",
+ "0f763874f3fb73f2a41701ec623bae3629464243d70de797d842cdde45ab847e",
+ ),
+ (
+ "migration_for_version",
+ "896f4fd8a67ba6dc17262f117fd74b0df74ee75f3cf0066bfddd90fb58d84a96",
+ ),
+ (
+ "validate_embedded_migration_input",
+ "526a7971d0736588d66cf95cca4063ebd3a4497c6f0c2c7ba96b39d09ee07259",
+ ),
+ (
+ "validate_migration_registry",
+ "e6cf2795b0308a51ef5958ce91f41877fb9c73f8f4c7008c90b1e5e70b37364a",
+ ),
+ (
+ "validate_generated_nip09_manifest_descriptor",
+ "44d44c3c35a8ea923d9fce80afea4a9db35e9225172090c831fd151bd2c5d4a1",
+ ),
+ ];
+
+ let tokens = [
+ compact_tokens(exact_executor_const(
+ file,
+ relative,
+ "EVENT_STORE_LEDGER_DDL",
+ )?),
+ compact_tokens(exact_executor_const(
+ file,
+ relative,
+ "EVENT_STORE_LEDGER_CREATE_DDL",
+ )?),
+ compact_tokens(exact_executor_const(
+ file,
+ relative,
+ "EVENT_STORE_BASELINE_FTS5_TABLE_NAMES",
+ )?),
+ compact_tokens(exact_top_level_struct(
+ relative,
+ file,
+ "EventStoreMigration",
+ )?),
+ compact_tokens(exact_const_struct_array_element(
+ relative,
+ file,
+ "EVENT_STORE_MIGRATIONS",
+ "version",
+ 1,
+ )?),
+ compact_tokens(exact_top_level_function(
+ relative,
+ file,
+ "migration_for_version",
+ )?),
+ compact_tokens(exact_top_level_function(
+ relative,
+ file,
+ "validate_embedded_migration_input",
+ )?),
+ compact_tokens(exact_top_level_function(
+ relative,
+ file,
+ "validate_migration_registry",
+ )?),
+ compact_tokens(exact_top_level_function(
+ relative,
+ file,
+ "validate_generated_nip09_manifest_descriptor",
+ )?),
+ ];
+ let drift = EXPECTED
+ .iter()
+ .zip(tokens)
+ .filter_map(|((label, expected), tokens)| {
+ let actual = sha256_hex(tokens.as_bytes());
+ (actual != *expected).then(|| format!("{label}={actual} (expected {expected})"))
+ })
+ .collect::<Vec<_>>();
+ if !drift.is_empty() {
+ return Err(format!(
+ "{relative} active migration support token authority drifted: {}",
+ drift.join(", ")
+ ));
+ }
+ Ok(())
+}
+
+fn validate_source_maintenance_schema_dispatch(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ for (function, called) in [
+ ("apply_migration_hook", "apply_source_maintenance_hook_v1"),
+ (
+ "validate_migration_hook_state",
+ "validate_source_capacity_authority_full_v1",
+ ),
+ ] {
+ let arm = exact_tail_match_arm(
+ relative,
+ file,
+ function,
+ "migration.hook",
+ "SourceMaintenanceV1",
+ )?;
+ validate_direct_arm_awaited_call(
+ relative,
+ &format!("`{function}` SourceMaintenanceV1 arm"),
+ &arm.body,
+ called,
+ )?;
+ if arm.guard.is_some() {
+ return Err(format!(
+ "{relative} `{function}` SourceMaintenanceV1 arm must remain unguarded"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_schema_migration_execution_authority(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ for (name, expected) in [
+ (
+ "apply_migration_up",
+ r#"async fn apply_migration_up(
+ connection: &mut SqliteConnection,
+ registry: &[EventStoreMigration],
+ migration: &EventStoreMigration,
+ ) -> Result<(), RadrootsEventStoreError> {
+ let before = read_catalog(connection).await?;
+ sqlx::raw_sql(migration.up_sql)
+ .execute(&mut *connection)
+ .await?;
+ let after = read_catalog(connection).await?;
+ validate_catalog_delta(&before, &after, migration, "up")?;
+ validate_schema_fingerprint(connection, registry, migration).await
+ }"#,
+ ),
+ (
+ "apply_migration_down",
+ r#"async fn apply_migration_down(
+ connection: &mut SqliteConnection,
+ migration: &EventStoreMigration,
+ ) -> Result<(), RadrootsEventStoreError> {
+ let before = read_catalog(connection).await?;
+ sqlx::raw_sql(migration.down_sql)
+ .execute(&mut *connection)
+ .await?;
+ let after = read_catalog(connection).await?;
+ validate_catalog_delta(&before, &after, migration, "down")
+ }"#,
+ ),
+ (
+ "validate_catalog_delta",
+ r#"fn validate_catalog_delta(
+ before: &[CatalogRow],
+ after: &[CatalogRow],
+ migration: &EventStoreMigration,
+ direction: &'static str,
+ ) -> Result<(), RadrootsEventStoreError> {
+ 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 expected_changed = migration
+ .replaced_object_names
+ .iter()
+ .copied()
+ .collect::<BTreeSet<_>>();
+
+ let valid = match direction {
+ "up" => added == expected && removed.is_empty() && changed == expected_changed,
+ "down" => removed == expected && added.is_empty() && changed == expected_changed,
+ _ => false,
+ };
+ if !valid {
+ return Err(RadrootsEventStoreError::MigrationCatalogDeltaMismatch {
+ version: migration.version,
+ direction,
+ reason: format!(
+ "expected {} objects {expected:?} and changed replacement objects {expected_changed:?}; added {added:?}, removed {removed:?}, changed {changed:?}",
+ if direction == "up" {
+ "added"
+ } else {
+ "removed"
+ }
+ ),
+ });
+ }
+ Ok(())
+ }"#,
+ ),
+ (
+ "apply_migration_hook",
+ r#"async fn apply_migration_hook(
+ connection: &mut SqliteConnection,
+ migration: &EventStoreMigration,
+ generation_provider: &dyn SourceGenerationProvider,
+ reconciliation_limits: ReconciliationCapacityLimits,
+ ) -> Result<(), RadrootsEventStoreError> {
+ match migration.hook {
+ EventStoreMigrationHook::None => Ok(()),
+ EventStoreMigrationHook::Nip09ReconciliationV1 => {
+ apply_reconciliation_hook(
+ connection,
+ generation_provider,
+ reconciliation_limits,
+ ).await
+ }
+ EventStoreMigrationHook::FoodAvailabilityProjectionV1 => {
+ apply_food_availability_projection_hook_v1(connection).await
+ }
+ EventStoreMigrationHook::SourceMaintenanceV1 => {
+ apply_source_maintenance_hook_v1(connection).await
+ }
+ }
+ }"#,
+ ),
+ (
+ "validate_migration_hook_state",
+ r#"async fn validate_migration_hook_state(
+ connection: &mut SqliteConnection,
+ migration: &EventStoreMigration,
+ ) -> Result<(), RadrootsEventStoreError> {
+ match migration.hook {
+ EventStoreMigrationHook::None => Ok(()),
+ EventStoreMigrationHook::Nip09ReconciliationV1 => {
+ validate_active_hook_state_fast(connection).await
+ }
+ EventStoreMigrationHook::FoodAvailabilityProjectionV1 => {
+ validate_food_availability_projection_hook_state_fast_v1(connection).await
+ }
+ EventStoreMigrationHook::SourceMaintenanceV1 => {
+ validate_source_capacity_authority_full_v1(connection).await
+ }
+ }
+ }"#,
+ ),
+ ] {
+ let actual = exact_top_level_function(relative, file, name)?;
+ let expected_file = parse_canonical_production_rust(
+ &format!("authoritative {relative}:{name}"),
+ expected.as_bytes(),
+ )?;
+ let expected = exact_top_level_function(relative, &expected_file, name)?;
+ if compact_tokens(actual) != compact_tokens(expected) {
+ return Err(format!(
+ "{relative} authoritative schema migration execution function `{name}` signature or control flow drifted"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_sqlite_encoding_preflight_authority(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ let configure_pool = exact_top_level_function(relative, file, "configure_pool")?;
+ let expected_configure_pool = r#"
+ async fn configure_pool(
+ pool: &SqlitePool,
+ file_backed: bool,
+ ) -> Result<(), RadrootsEventStoreError> {
+ let max_connections = pool.options().get_max_connections();
+ let existing_options = pool.connect_options();
+ if !file_backed && max_connections != 1 {
+ return Err(RadrootsEventStoreError::UnsafeInMemoryPoolConnectionCount {
+ actual: max_connections,
+ });
+ }
+
+ 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(RadrootsEventStoreError::SqlitePoolBackingMismatch {
+ file_backed,
+ filename: main_filename,
+ });
+ }
+ validate_main_database_encoding(connection).await?;
+ crate::schema::validate_event_store_temp_schema(connection).await?;
+ }
+
+ let mut connect_options = existing_options
+ .as_ref()
+ .clone()
+ .foreign_keys(true)
+ .busy_timeout(Duration::from_millis(5_000));
+ if file_backed {
+ connect_options = connect_options.journal_mode(SqliteJournalMode::Wal);
+ }
+ pool.set_connect_options(connect_options);
+
+ for connection in &mut connections {
+ sqlx::query("PRAGMA foreign_keys = ON")
+ .execute(&mut **connection)
+ .await?;
+ sqlx::query("PRAGMA busy_timeout = 5000")
+ .execute(&mut **connection)
+ .await?;
+ if file_backed {
+ configure_file_journal_mode(connection).await?;
+ }
+ }
+ Ok(())
+ }
+ "#;
+ if compact_tokens(configure_pool) != compact_source_tokens(expected_configure_pool) {
+ return Err(format!(
+ "{relative} `configure_pool` must validate every main database as UTF-8 after backing classification and before TEMP-schema, connection-option, PRAGMA, or journal mutation"
+ ));
+ }
+
+ let validator = exact_top_level_function(relative, file, "validate_main_database_encoding")?;
+ let expected_validator = r#"
+ async fn validate_main_database_encoding(
+ connection: &mut SqliteConnection,
+ ) -> Result<(), RadrootsEventStoreError> {
+ let actual: String = sqlx::query_scalar("PRAGMA main.encoding")
+ .fetch_one(&mut *connection)
+ .await?;
+ if actual == "UTF-8" {
+ return Ok(());
+ }
+ Err(RadrootsEventStoreError::SqliteMainDatabaseEncodingNotUtf8 { actual, })
+ }
+ "#;
+ if compact_tokens(validator) != compact_source_tokens(expected_validator) {
+ return Err(format!(
+ "{relative} `validate_main_database_encoding` UTF-8 query or typed failure authority drifted: expected `{}`, found `{}`",
+ compact_source_tokens(expected_validator),
+ compact_tokens(validator),
+ ));
+ }
+ Ok(())
+}
+
+fn validate_source_generation_rollback_authority(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ let policies = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ syn::Item::Enum(item) if item.ident == "SourceGenerationHistoryRollbackPolicy" => {
+ Some(item)
+ }
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [policy] = policies.as_slice() else {
+ return Err(format!(
+ "{relative} must define exactly one production `SourceGenerationHistoryRollbackPolicy`; found {}",
+ policies.len()
+ ));
+ };
+ let expected_policy = r#"
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
+ enum SourceGenerationHistoryRollbackPolicy {
+ Preserve,
+ }
+ "#;
+ if compact_tokens(policy) != compact_source_tokens(expected_policy) {
+ return Err(format!(
+ "{relative} production `SourceGenerationHistoryRollbackPolicy` must contain only `Preserve`"
+ ));
+ }
+
+ let wrapper =
+ exact_top_level_function(relative, file, "rollback_event_store_schema_with_registry")?;
+ let expected_wrapper = r#"
+ async fn rollback_event_store_schema_with_registry(
+ pool: &SqlitePool,
+ registry: &[EventStoreMigration],
+ minimum: u32,
+ supported_current: u32,
+ target: u32,
+ ) -> Result<(), RadrootsEventStoreError> {
+ rollback_event_store_schema_with_registry_inner(
+ pool,
+ registry,
+ minimum,
+ supported_current,
+ target,
+ SourceGenerationHistoryRollbackPolicy::Preserve,
+ )
+ .await
+ }
+ "#;
+ if compact_tokens(wrapper) != compact_source_tokens(expected_wrapper) {
+ return Err(format!(
+ "{relative} production rollback registry wrapper must directly select and await the `Preserve` policy"
+ ));
+ }
+
+ let rollback = exact_top_level_function(relative, file, "rollback_schema_on_connection")?;
+ let expected_signature = compact_source_tokens(
+ r#"async fn rollback_schema_on_connection(
+ connection: &mut SqliteConnection,
+ registry: &[EventStoreMigration],
+ supported_current: u32,
+ target: u32,
+ source_generation_history_policy: SourceGenerationHistoryRollbackPolicy,
+ ) -> Result<(), RadrootsEventStoreError>"#,
+ );
+ if !rollback.attrs.is_empty()
+ || !matches!(rollback.vis, syn::Visibility::Inherited)
+ || compact_tokens(&rollback.sig) != expected_signature
+ || rollback.block.stmts.len() != 6
+ {
+ return Err(format!(
+ "{relative} `rollback_schema_on_connection` signature or six-statement authority skeleton drifted"
+ ));
+ }
+ for (index, expected) in [
+ r#"let RadrootsEventStoreSchemaStatus::Managed {
+ version: current_version
+ } = inspect_schema_on_connection(connection, registry, supported_current,).await?
+ else {
+ return Err(RadrootsEventStoreError::RollbackUnmanaged);
+ };"#,
+ r#"if target > current_version {
+ return Err(RadrootsEventStoreError::RollbackAhead {
+ current: current_version,
+ target,
+ });
+ }"#,
+ r#"if source_generation_history_policy == SourceGenerationHistoryRollbackPolicy::Preserve {
+ validate_rollback_preserves_source_generation_history(
+ registry,
+ current_version,
+ target,
+ )?;
+ }"#,
+ ]
+ .into_iter()
+ .enumerate()
+ {
+ if compact_tokens(&rollback.block.stmts[index]) != compact_source_tokens(expected) {
+ return Err(format!(
+ "{relative} `rollback_schema_on_connection` authoritative preflight statement {} drifted: expected `{}`, found `{}`",
+ index + 1,
+ compact_source_tokens(expected),
+ compact_tokens(&rollback.block.stmts[index]),
+ ));
+ }
+ }
+ if !matches!(
+ rollback.block.stmts.get(3),
+ Some(syn::Stmt::Expr(syn::Expr::ForLoop(loop_expression), _))
+ if compact_tokens(&loop_expression.pat) == "version"
+ && compact_tokens(&loop_expression.expr)
+ == "((target+1)..=current_version).rev()"
+ ) {
+ return Err(format!(
+ "{relative} source-generation rollback guard must remain immediately before the direct down-migration loop"
+ ));
+ }
+
+ let validator = exact_top_level_function(
+ relative,
+ file,
+ "validate_rollback_preserves_source_generation_history",
+ )?;
+ let expected_validator = r#"
+ fn validate_rollback_preserves_source_generation_history(
+ registry: &[EventStoreMigration],
+ current: u32,
+ target: u32,
+ ) -> Result<(), RadrootsEventStoreError> {
+ let Some(floor) = registry
+ .iter()
+ .find(|migration| {
+ migration.hook == EventStoreMigrationHook::Nip09ReconciliationV1
+ })
+ .map(|migration| migration.version)
+ else {
+ return Ok(());
+ };
+ if current < floor || target >= floor {
+ return Ok(());
+ }
+
+ Err(RadrootsEventStoreError::RollbackWouldDiscardSourceGenerationHistory {
+ current,
+ target,
+ floor,
+ })
+ }
+ "#;
+ if compact_tokens(validator) != compact_source_tokens(expected_validator) {
+ return Err(format!(
+ "{relative} source-generation rollback floor derivation or typed rejection authority drifted: expected `{}`, found `{}`",
+ compact_source_tokens(expected_validator),
+ compact_tokens(validator),
+ ));
+ }
+ Ok(())
+}
+
+fn validate_source_maintenance_runtime_token_authority(
+ workspace_root: &Path,
+) -> Result<(), String> {
+ const SOURCE_RUNTIME_AST_SHA256: &str =
+ "181576a5de365cf664b8a87091c30b0389ce0be90e7d1cc16fd7170342f6c2bc";
+ const FUNCTION_SPECS: [(&str, &str, &str); 4] = [
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "ingest_event_protocol_reconciliation_v1",
+ "f8d26e1d4e1a362c7335f1ba58ad6f1bac2f119162b15ca1067391756149d1e3",
+ ),
+ (
+ EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE,
+ "read_protocol_post_extension_authority_seal",
+ "490e59d21fb84f3321c593ffb67a4d1ada1e5cc8373ed41e2c6834114f2a6ef9",
+ ),
+ (
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "apply_reconciliation_hook",
+ "41a0bc1f4e529528f9bc13be28b4a31305156124282c1c7e955ed2e4a56e86d2",
+ ),
+ (
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ "associated:RadrootsEventStore::source_capacity_v1",
+ "176c41b212e8d9d4ae3a53cf61dfade6d34b802f5bb649b18f66ffc769d5bade",
+ ),
+ ];
+
+ let source_relative = "crates/event_store/src/source_maintenance_v1.rs";
+ let source_bytes = read_regular_file(workspace_root, source_relative)?;
+ let canonical_source =
+ canonical_rust_ast(source_relative, &source_bytes, RustAstProfile::Production)?;
+ let mut drift = Vec::new();
+ let source_sha256 = sha256_hex(&canonical_source);
+ if source_sha256 != SOURCE_RUNTIME_AST_SHA256 {
+ drift.push(format!(
+ "{source_relative}={source_sha256} (expected {SOURCE_RUNTIME_AST_SHA256})"
+ ));
+ }
+
+ for (relative, function, expected_sha256) in FUNCTION_SPECS {
+ let bytes = read_regular_file(workspace_root, relative)?;
+ let file = parse_canonical_production_rust(relative, &bytes)?;
+ let tokens = if let Some(method) = function.strip_prefix("associated:") {
+ let (owner, method) = method.split_once("::").ok_or_else(|| {
+ format!("invalid associated SourceMaintenance witness `{function}`")
+ })?;
+ compact_tokens(exact_associated_function(relative, &file, owner, method)?)
+ } else {
+ compact_tokens(exact_top_level_function(relative, &file, function)?)
+ };
+ let actual_sha256 = sha256_hex(tokens.as_bytes());
+ if actual_sha256 != expected_sha256 {
+ drift.push(format!(
+ "{relative}:{function}={actual_sha256} (expected {expected_sha256})"
+ ));
+ }
+ }
+ if !drift.is_empty() {
+ return Err(format!(
+ "current SourceMaintenance runtime exact token authority drifted: {}",
+ drift.join(", ")
+ ));
+ }
+ Ok(())
+}
+
+fn validate_schema_runtime_reachability<'a>(
+ relative: &str,
+ file: &'a syn::File,
) -> Result<Vec<&'a syn::ItemFn>, String> {
const EXPECTED: [(&str, &str); 9] = [
(
@@ -11204,6 +12911,11 @@ fn validate_schema_runtime_reachability<'a>(
&mut connection,
reconciliation_limits
).await?;
+ if has_pending_source_maintenance_hook(&status, registry) {
+ validate_no_persisted_ephemeral_raw_rows_v1(
+ &mut connection
+ ).await?;
+ }
}
let mut transaction = pool.begin_with("BEGIN IMMEDIATE").await?;
let result = migrate_schema_on_connection(
@@ -11285,7 +12997,7 @@ fn validate_schema_runtime_reachability<'a>(
),
];
- EXPECTED
+ let functions = EXPECTED
.iter()
.map(|(name, expected)| {
let function = exact_top_level_function(relative, file, name)?;
@@ -11298,7 +13010,120 @@ fn validate_schema_runtime_reachability<'a>(
}
Ok(function)
})
- .collect()
+ .collect::<Result<Vec<_>, String>>()?;
+ let migrate = exact_top_level_function(relative, file, "migrate_schema_on_connection")?;
+ let current_version = migrate
+ .block
+ .stmts
+ .iter()
+ .find(|statement| {
+ matches!(
+ statement,
+ syn::Stmt::Local(local)
+ if local_pattern_ident(&local.pat).as_deref() == Some("current_version")
+ )
+ })
+ .ok_or_else(|| {
+ format!(
+ "{relative} authoritative schema runtime is missing the direct `current_version` initializer"
+ )
+ })?;
+ let expected_current_version = parse_canonical_production_rust(
+ "authoritative migrate_schema_on_connection current_version initializer",
+ br#"fn expected() {
+ let current_version = match status {
+ RadrootsEventStoreSchemaStatus::Uninitialized => {
+ apply_migration_up(connection, registry, ®istry[0]).await?;
+ create_ledger(connection).await?;
+ insert_ledger_row(connection, ®istry[0]).await?;
+ registry[0].version
+ }
+ RadrootsEventStoreSchemaStatus::UnledgeredBaseline => {
+ create_ledger(connection).await?;
+ insert_ledger_row(connection, ®istry[0]).await?;
+ registry[0].version
+ }
+ RadrootsEventStoreSchemaStatus::Managed { version }
+ if version == supported_current =>
+ {
+ return Ok(());
+ }
+ RadrootsEventStoreSchemaStatus::Managed { version } => version,
+ };
+ }"#,
+ )?;
+ let expected_current_version =
+ exact_top_level_function(relative, &expected_current_version, "expected")?
+ .block
+ .stmts
+ .first()
+ .expect("authoritative current-version initializer");
+ if compact_tokens(current_version) != compact_tokens(expected_current_version) {
+ return Err(format!(
+ "{relative} authoritative schema runtime `migrate_schema_on_connection` current-version control flow drifted"
+ ));
+ }
+ validate_migration_hook_loop_reachability(relative, file)?;
+ Ok(functions)
+}
+
+fn validate_migration_hook_loop_reachability(
+ relative: &str,
+ file: &syn::File,
+) -> Result<(), String> {
+ let apply_loop = exact_direct_for_loop(
+ relative,
+ file,
+ "migrate_schema_on_connection",
+ "migration",
+ "registry.iter().filter(|migration|migration.version>current_version)",
+ )?;
+ let source_preflight = r#"if matches!(
+ migration.hook,
+ EventStoreMigrationHook::Nip09ReconciliationV1
+ | EventStoreMigrationHook::FoodAvailabilityProjectionV1
+ | EventStoreMigrationHook::SourceMaintenanceV1
+ ) {
+ validate_reconciliation_capacity(connection, reconciliation_limits).await?;
+ if migration.hook == EventStoreMigrationHook::SourceMaintenanceV1 {
+ validate_no_persisted_ephemeral_raw_rows_v1(connection).await?;
+ }
+ }"#;
+ if apply_loop.body.stmts.first().is_none_or(|statement| {
+ compact_tokens(statement) != compact_source_tokens(source_preflight)
+ }) {
+ return Err(format!(
+ "{relative} `migrate_schema_on_connection` SourceMaintenance preflight or error propagation drifted"
+ ));
+ }
+ for called in [
+ "apply_migration_up",
+ "apply_migration_hook",
+ "validate_applied_migration_hooks",
+ "insert_ledger_row",
+ ] {
+ exact_direct_loop_awaited_call(
+ relative,
+ "migrate_schema_on_connection",
+ apply_loop,
+ called,
+ )?;
+ }
+
+ let validate_loop = exact_direct_for_loop(
+ relative,
+ file,
+ "validate_applied_migration_hooks",
+ "migration",
+ "registry.iter().filter(|migration|migration.version<=current)",
+ )?;
+ exact_direct_loop_awaited_call(
+ relative,
+ "validate_applied_migration_hooks",
+ validate_loop,
+ "validate_migration_hook_state",
+ )?;
+ Ok(())
}
fn validate_no_diverging_control_flow(
@@ -11739,7 +13564,7 @@ fn exact_direct_for_loop<'a>(
));
};
let (expected_statement_count, expected_index) = match function {
- "validate_migration_registry" => (9, 6),
+ "validate_migration_registry" => (12, 9),
"migrate_schema_on_connection" => (5, 2),
"validate_applied_migration_hooks" => (2, 0),
_ => {
@@ -11772,9 +13597,9 @@ fn exact_direct_loop_match_arm<'a>(
variant: &str,
) -> Result<&'a syn::Arm, String> {
if function == "validate_migration_registry"
- && (loop_expression.body.stmts.len() != 14
+ && (loop_expression.body.stmts.len() != 20
|| !matches!(
- loop_expression.body.stmts.get(12),
+ loop_expression.body.stmts.get(18),
Some(syn::Stmt::Expr(syn::Expr::Match(_), _))
))
{
@@ -11849,9 +13674,12 @@ fn exact_direct_loop_awaited_call<'a>(
loop_expression: &'a syn::ExprForLoop,
called: &str,
) -> Result<&'a syn::ExprAwait, String> {
- let (expected_statement_count, expected_index) = match function {
- "migrate_schema_on_connection" => (5, 2),
- "validate_applied_migration_hooks" => (1, 0),
+ let (expected_statement_count, expected_index) = match (function, called) {
+ ("migrate_schema_on_connection", "apply_migration_up") => (5, 1),
+ ("migrate_schema_on_connection", "apply_migration_hook") => (5, 2),
+ ("migrate_schema_on_connection", "validate_applied_migration_hooks") => (5, 3),
+ ("migrate_schema_on_connection", "insert_ledger_row") => (5, 4),
+ ("validate_applied_migration_hooks", "validate_migration_hook_state") => (1, 0),
_ => {
return Err(format!(
"{relative} `{function}` is not an approved awaited-loop witness"
@@ -15257,6 +17085,8 @@ mod tests {
MIGRATION_V1_DOWN_RELATIVE,
MIGRATION_UP_RELATIVE,
MIGRATION_DOWN_RELATIVE,
+ "crates/event_store/migrations/0004_source_maintenance.up.sql",
+ "crates/event_store/migrations/0004_source_maintenance.down.sql",
REGISTRY_INVENTORY_RELATIVE,
"contracts/event_store/event_contract_registry_v7.inventory.sha256",
RESULT_VECTOR_CANONICAL_RELATIVE,
@@ -15266,6 +17096,13 @@ mod tests {
MANIFEST_SCHEMA_RELATIVE,
MANIFEST_SHA256_RELATIVE,
GENERATED_DESCRIPTOR_RELATIVE,
+ "contracts/conformance/vectors/event_store/source_maintenance.v1.json",
+ "crates/event_store/tests/fixtures/source_maintenance.v1.json",
+ "crates/event_store/tests/source_maintenance_v1_result_vector.rs",
+ "crates/event_store/contracts/source_maintenance_v1.manifest.json",
+ "crates/event_store/contracts/source_maintenance_v1.manifest.schema.json",
+ "crates/event_store/contracts/source_maintenance_v1.manifest.sha256",
+ "crates/event_store/src/generated/source_maintenance_manifest.rs",
];
for dependency in SEMANTIC_DEPENDENCY_SPECS {
paths.push(dependency.canonical_path);
@@ -15276,6 +17113,8 @@ mod tests {
paths.extend(FROZEN_SOURCE_SPECS.iter().map(|source| source.path));
paths.extend(SOURCE_ROUTE_WITNESS_SPECS.iter().map(|source| source.path));
paths.extend(SUCCESSOR_08C_EXCLUSIVE_SOURCE_PATHS);
+ paths.extend(SUCCESSOR_08D_SOURCE_PATHS);
+ paths.extend(super::super::source_maintenance::source_contract_fixture_source_paths());
paths.sort_unstable();
paths.dedup();
paths
@@ -15580,7 +17419,7 @@ route!(r#hex);
"#[cfg(any())]\nuse self::post_core_extension_dispatcher::dispatch_post_core_extensions;",
1,
),
- "privileged cross-module import routes drifted",
+ "SourceMaintenance privileged import authority drifted",
),
(
"extra associated core bypass call",
@@ -15791,7 +17630,8 @@ route!(r#hex);
.expect_err("privileged sibling source must fail");
assert!(
error.contains("privileged cross-module import routes drifted")
- || error.contains("source inventory is closed"),
+ || error.contains("source inventory is closed")
+ || error.contains("privileged terminal import"),
"{error}"
);
@@ -15980,8 +17820,92 @@ route!(r#hex);
error.contains(EVENT_STORE_LIB_SOURCE_RELATIVE),
"unexpected ancestor error: {error}"
);
- fs::write(&lib_path, &lib_original).expect("restore event-store lib source");
+ fs::write(&lib_path, &lib_original).expect("restore event-store lib source");
+ }
+
+ let food_path = workspace
+ .path()
+ .join("crates/event_store/src/store/food_availability_projection_v1.rs");
+ let food_original = fs::read_to_string(&food_path).expect("08C Food store source");
+ let food_mutations = [
+ (
+ "08C direct SourceMaintenance terminal call",
+ format!(
+ "{food_original}\nasync fn source_terminal_bypass(connection: &mut sqlx::SqliteConnection) {{\n let _ = crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1(connection).await;\n}}\n"
+ ),
+ ),
+ (
+ "08C SourceMaintenance alias import",
+ format!(
+ "{food_original}\nuse crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1 as bypass;\n"
+ ),
+ ),
+ (
+ "08C SourceMaintenance glob import",
+ format!("{food_original}\nuse crate::source_maintenance_v1::*;\n"),
+ ),
+ (
+ "08C SourceMaintenance macro reference",
+ format!(
+ "{food_original}\nfn source_terminal_macro_bypass() {{ stringify!(validate_source_capacity_authority_fast_v1); }}\n"
+ ),
+ ),
+ (
+ "08C SourceMaintenance associated-function shadow",
+ format!(
+ "{food_original}\nstruct SourceTerminalShadow;\nimpl SourceTerminalShadow {{ fn validate_source_capacity_authority_fast_v1() {{}} }}\n"
+ ),
+ ),
+ (
+ "08C SourceMaintenance trait-function shadow",
+ format!(
+ "{food_original}\ntrait SourceTerminalShadow {{ fn preflight_unique_raw_source_append_v1(); }}\n"
+ ),
+ ),
+ ];
+ for (label, mutation) in food_mutations {
+ fs::write(&food_path, mutation).expect("write 08C terminal bypass");
+ let error = validate_privileged_store_authority(workspace.path())
+ .expect_err("08C SourceMaintenance terminal bypass must fail");
+ assert!(
+ error.contains("privileged terminal")
+ || error.contains("privileged authority")
+ || error.contains("glob import"),
+ "{label} produced unexpected error: {error}"
+ );
+ fs::write(&food_path, &food_original).expect("restore 08C Food source");
}
+
+ let protocol_path = workspace
+ .path()
+ .join(EVENT_STORE_PROTOCOL_RECONCILIATION_SOURCE_RELATIVE);
+ let protocol_original =
+ fs::read_to_string(&protocol_path).expect("protocol reconciliation source");
+ let mut protocol_shadow =
+ syn::parse_file(&protocol_original).expect("protocol reconciliation AST");
+ let ingest = protocol_shadow
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function)
+ if function.sig.ident == "ingest_event_protocol_reconciliation_v1" =>
+ {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("approved SourceMaintenance caller");
+ ingest.block.stmts.insert(
+ 0,
+ syn::parse_str("let validate_source_capacity_authority_fast_v1 = || ();")
+ .expect("terminal shadow binding"),
+ );
+ fs::write(&protocol_path, prettyplease::unparse(&protocol_shadow))
+ .expect("write approved-caller terminal shadow");
+ let error = validate_privileged_store_authority(workspace.path())
+ .expect_err("approved-caller terminal shadow must fail");
+ assert!(error.contains("shadows privileged authority"), "{error}");
+ fs::write(&protocol_path, protocol_original).expect("restore protocol source");
}
#[test]
@@ -16078,7 +18002,7 @@ route!(r#hex);
#[test]
fn migration_reachability_requires_guard_order_and_error_propagation() {
let source = repository_source(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE);
- let mut file = parse_canonical_production_rust(
+ let file = parse_canonical_production_rust(
EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
source.as_bytes(),
)
@@ -16087,6 +18011,16 @@ route!(r#hex);
.expect("authoritative registry reachability");
validate_manifest_validator_reachability(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &file)
.expect("authoritative manifest-validator reachability");
+ validate_source_maintenance_manifest_validator_reachability(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &file,
+ )
+ .expect("authoritative SourceMaintenance descriptor reachability");
+ validate_source_maintenance_migration_bindings(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &file,
+ )
+ .expect("authoritative SourceMaintenance migration bindings");
let mut early_return_file = file.clone();
let early_return_registry = early_return_file
@@ -16112,7 +18046,8 @@ route!(r#hex);
"an early success return before the generated-manifest guard must fail"
);
- let registry = file
+ let mut reordered_predecessor_guards = file.clone();
+ let registry = reordered_predecessor_guards
.items
.iter_mut()
.find_map(|item| match item {
@@ -16126,10 +18061,76 @@ route!(r#hex);
assert!(
validate_migration_registry_reachability(
EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
- &file,
+ &reordered_predecessor_guards,
+ )
+ .is_err(),
+ "reordering the predecessor manifest guards must fail"
+ );
+
+ let mut missing_source_guard = file.clone();
+ let registry = missing_source_guard
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function) if function.sig.ident == "validate_migration_registry" => {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("registry validator");
+ registry.block.stmts.remove(3);
+ assert!(
+ validate_migration_registry_reachability(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &missing_source_guard,
+ )
+ .is_err(),
+ "removing the SourceMaintenance descriptor guard must fail"
+ );
+
+ let mut reordered_source_guard = file.clone();
+ let registry = reordered_source_guard
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function) if function.sig.ident == "validate_migration_registry" => {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("registry validator");
+ registry.block.stmts.swap(3, 4);
+ assert!(
+ validate_migration_registry_reachability(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &reordered_source_guard,
+ )
+ .is_err(),
+ "moving the SourceMaintenance guard behind the range guard must fail"
+ );
+
+ let mut discarded_source_guard_result = file.clone();
+ let registry = discarded_source_guard_result
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function) if function.sig.ident == "validate_migration_registry" => {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("registry validator");
+ let syn::Stmt::Expr(syn::Expr::If(source_guard), _) = &mut registry.block.stmts[3] else {
+ panic!("SourceMaintenance manifest guard");
+ };
+ strip_outer_try(&mut source_guard.then_branch.stmts[0]);
+ assert!(
+ validate_migration_registry_reachability(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &discarded_source_guard_result,
)
.is_err(),
- "moving the manifest guard behind the range guard must fail"
+ "discarding the SourceMaintenance descriptor validator Result must fail"
);
let mut file = parse_canonical_production_rust(
@@ -16186,44 +18187,44 @@ route!(r#hex);
.is_err(),
"discarding manifest SHA validation must fail"
);
+
+ let mut source_descriptor_bypass = parse_canonical_production_rust(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ source.as_bytes(),
+ )
+ .expect("migration AST");
+ let descriptor = source_descriptor_bypass
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function)
+ if function.sig.ident
+ == "validate_generated_source_maintenance_manifest_descriptor" =>
+ {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("SourceMaintenance descriptor validator");
+ descriptor.block = syn::parse_str("{ Ok(()) }").expect("no-op descriptor body");
+ assert!(
+ validate_source_maintenance_manifest_validator_reachability(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &source_descriptor_bypass,
+ )
+ .is_err(),
+ "a no-op SourceMaintenance descriptor body must fail while its registry call remains"
+ );
}
#[test]
fn migration_hook_loops_require_awaited_question_mark_propagation() {
let source = repository_source(EVENT_STORE_SCHEMA_SOURCE_RELATIVE);
- let validate = |file: &syn::File| -> Result<(), String> {
- let apply_loop = exact_direct_for_loop(
- EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
- file,
- "migrate_schema_on_connection",
- "migration",
- "registry.iter().filter(|migration|migration.version>current_version)",
- )?;
- exact_direct_loop_awaited_call(
- EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
- "migrate_schema_on_connection",
- apply_loop,
- "apply_migration_hook",
- )?;
- let validate_loop = exact_direct_for_loop(
- EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
- file,
- "validate_applied_migration_hooks",
- "migration",
- "registry.iter().filter(|migration|migration.version<=current)",
- )?;
- exact_direct_loop_awaited_call(
- EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
- "validate_applied_migration_hooks",
- validate_loop,
- "validate_migration_hook_state",
- )?;
- Ok(())
- };
let file =
parse_canonical_production_rust(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, source.as_bytes())
.expect("schema AST");
- validate(&file).expect("authoritative hook propagation");
+ validate_migration_hook_loop_reachability(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &file)
+ .expect("authoritative hook propagation");
for (function_name, loop_statement_index) in [
("migrate_schema_on_connection", 2usize),
@@ -16255,7 +18256,11 @@ route!(r#hex);
.expect("hook loop");
strip_outer_try(&mut loop_expression.body.stmts[loop_statement_index]);
assert!(
- validate(&file).is_err(),
+ validate_migration_hook_loop_reachability(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &file,
+ )
+ .is_err(),
"discarding `{function_name}` hook Result must fail"
);
}
@@ -16299,39 +18304,319 @@ route!(r#hex);
);
}
- for (label, mutation) in [
- (
- "TEMP LIKE wildcard filter",
- source.replacen(
- "SELECT type, name, tbl_name FROM temp.sqlite_schema ORDER BY type, name, tbl_name",
- "SELECT type, name, tbl_name FROM temp.sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name, tbl_name",
- 1,
- ),
- ),
- (
- "main catalog LIKE wildcard filter",
- source.replacen(
- "SELECT type, name, tbl_name, sql FROM main.sqlite_schema",
- "SELECT type, name, tbl_name, sql FROM main.sqlite_schema WHERE name NOT LIKE 'sqlite_%'",
- 1,
- ),
- ),
- ] {
- assert_ne!(mutation, source, "{label} fixture must mutate");
- let file = parse_canonical_production_rust(
+ let source_preflight = r#" if has_pending_source_maintenance_hook(&status, registry) {
+ validate_no_persisted_ephemeral_raw_rows_v1(&mut connection).await?;
+ }
+"#;
+ let begin_immediate =
+ " let mut transaction = pool.begin_with(\"BEGIN IMMEDIATE\").await?;\n";
+ let preflight_mutations = [
+ (
+ "removed SourceMaintenance persisted-ephemeral preflight",
+ source.replacen(source_preflight, "", 1),
+ ),
+ (
+ "discarded SourceMaintenance persisted-ephemeral preflight Result",
+ source.replacen(
+ "validate_no_persisted_ephemeral_raw_rows_v1(&mut connection).await?;",
+ "validate_no_persisted_ephemeral_raw_rows_v1(&mut connection).await;",
+ 1,
+ ),
+ ),
+ (
+ "SourceMaintenance preflight moved after BEGIN IMMEDIATE",
+ source.replacen(source_preflight, "", 1).replacen(
+ begin_immediate,
+ &format!("{begin_immediate}{source_preflight}"),
+ 1,
+ ),
+ ),
+ (
+ "unguarded SourceMaintenance persisted-ephemeral preflight",
+ source.replacen(
+ source_preflight,
+ " validate_no_persisted_ephemeral_raw_rows_v1(&mut connection).await?;\n",
+ 1,
+ ),
+ ),
+ ];
+ for (label, mutation) in preflight_mutations {
+ assert_ne!(mutation, source, "{label} fixture must mutate");
+ let file = parse_canonical_production_rust(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ mutation.as_bytes(),
+ )
+ .expect("mutated SourceMaintenance preflight AST");
+ assert!(
+ validate_schema_runtime_reachability(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &file)
+ .is_err(),
+ "{label} must fail closed"
+ );
+ }
+
+ for (label, mutation) in [
+ (
+ "TEMP LIKE wildcard filter",
+ source.replacen(
+ "SELECT type, name, tbl_name FROM temp.sqlite_schema ORDER BY type, name, tbl_name",
+ "SELECT type, name, tbl_name FROM temp.sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name, tbl_name",
+ 1,
+ ),
+ ),
+ (
+ "main catalog LIKE wildcard filter",
+ source.replacen(
+ "SELECT type, name, tbl_name, sql FROM main.sqlite_schema",
+ "SELECT type, name, tbl_name, sql FROM main.sqlite_schema WHERE name NOT LIKE 'sqlite_%'",
+ 1,
+ ),
+ ),
+ ] {
+ assert_ne!(mutation, source, "{label} fixture must mutate");
+ let file = parse_canonical_production_rust(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ mutation.as_bytes(),
+ )
+ .expect("mutated schema AST");
+ let error =
+ validate_schema_runtime_reachability(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &file)
+ .err()
+ .expect("LIKE wildcard catalog filter must fail");
+ assert!(
+ error.contains("authoritative schema runtime"),
+ "{label} produced unexpected error: {error}"
+ );
+ }
+ }
+
+ #[test]
+ fn sqlite_encoding_preflight_rejects_ordering_and_propagation_bypasses() {
+ let source = repository_source(EVENT_STORE_STORE_SOURCE_RELATIVE);
+ let baseline =
+ parse_canonical_production_rust(EVENT_STORE_STORE_SOURCE_RELATIVE, source.as_bytes())
+ .expect("store AST");
+ validate_sqlite_encoding_preflight_authority(EVENT_STORE_STORE_SOURCE_RELATIVE, &baseline)
+ .expect("authoritative SQLite encoding preflight");
+
+ for mutation in ["remove", "discard", "after_temp", "before_backing"] {
+ let mut file = baseline.clone();
+ let configure_pool = file
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function) if function.sig.ident == "configure_pool" => {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("configure_pool");
+ let syn::Stmt::Expr(syn::Expr::ForLoop(preflight), _) =
+ &mut configure_pool.block.stmts[5]
+ else {
+ panic!("encoding preflight loop");
+ };
+ match mutation {
+ "remove" => {
+ preflight.body.stmts.remove(3);
+ }
+ "discard" => strip_outer_try(&mut preflight.body.stmts[3]),
+ "after_temp" => preflight.body.stmts.swap(3, 4),
+ "before_backing" => preflight.body.stmts.swap(2, 3),
+ _ => unreachable!(),
+ }
+ assert!(
+ validate_sqlite_encoding_preflight_authority(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ &file,
+ )
+ .is_err(),
+ "SQLite encoding `{mutation}` bypass must fail closed"
+ );
+ }
+
+ let mut no_op = baseline;
+ let validator = no_op
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function)
+ if function.sig.ident == "validate_main_database_encoding" =>
+ {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("encoding validator");
+ validator.block = syn::parse_str("{ Ok(()) }").expect("no-op encoding validator");
+ assert!(
+ validate_sqlite_encoding_preflight_authority(
+ EVENT_STORE_STORE_SOURCE_RELATIVE,
+ &no_op,
+ )
+ .is_err(),
+ "a no-op encoding validator must fail closed"
+ );
+ }
+
+ #[test]
+ fn source_generation_rollback_guard_rejects_policy_and_ordering_bypasses() {
+ let source = repository_source(EVENT_STORE_SCHEMA_SOURCE_RELATIVE);
+ let baseline =
+ parse_canonical_production_rust(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, source.as_bytes())
+ .expect("schema AST");
+ validate_source_generation_rollback_authority(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &baseline,
+ )
+ .expect("authoritative source-generation rollback guard");
+
+ let mut wrapper_bypass = baseline.clone();
+ let wrapper = wrapper_bypass
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function)
+ if function.sig.ident == "rollback_event_store_schema_with_registry" =>
+ {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("production rollback wrapper");
+ wrapper.block = syn::parse_str("{ Ok(()) }").expect("rollback wrapper bypass");
+ assert!(
+ validate_source_generation_rollback_authority(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &wrapper_bypass,
+ )
+ .is_err(),
+ "a production rollback wrapper that omits `Preserve` must fail closed"
+ );
+
+ for mutation in ["remove", "discard", "after_down_loop"] {
+ let mut file = baseline.clone();
+ let rollback = file
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function)
+ if function.sig.ident == "rollback_schema_on_connection" =>
+ {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("rollback implementation");
+ match mutation {
+ "remove" => {
+ rollback.block.stmts.remove(2);
+ }
+ "discard" => {
+ let syn::Stmt::Expr(syn::Expr::If(guard), _) = &mut rollback.block.stmts[2]
+ else {
+ panic!("source-generation rollback guard");
+ };
+ strip_outer_try(&mut guard.then_branch.stmts[0]);
+ }
+ "after_down_loop" => rollback.block.stmts.swap(2, 3),
+ _ => unreachable!(),
+ }
+ assert!(
+ validate_source_generation_rollback_authority(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &file,
+ )
+ .is_err(),
+ "source-generation rollback `{mutation}` bypass must fail closed"
+ );
+ }
+
+ let mut policy_bypass = baseline.clone();
+ let policy = policy_bypass
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Enum(item) if item.ident == "SourceGenerationHistoryRollbackPolicy" => {
+ Some(item)
+ }
+ _ => None,
+ })
+ .expect("rollback policy enum");
+ policy
+ .variants
+ .push(syn::parse_str("AllowDestructive").expect("production bypass variant"));
+ assert!(
+ validate_source_generation_rollback_authority(
EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
- mutation.as_bytes(),
+ &policy_bypass,
)
- .expect("mutated schema AST");
- let error =
- validate_schema_runtime_reachability(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &file)
- .err()
- .expect("LIKE wildcard catalog filter must fail");
- assert!(
- error.contains("authoritative schema runtime"),
- "{label} produced unexpected error: {error}"
- );
- }
+ .is_err(),
+ "a second production rollback policy must fail closed"
+ );
+
+ let mut no_op = baseline;
+ let validator = no_op
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function)
+ if function.sig.ident
+ == "validate_rollback_preserves_source_generation_history" =>
+ {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("rollback floor validator");
+ validator.block = syn::parse_str("{ Ok(()) }").expect("no-op rollback validator");
+ assert!(
+ validate_source_generation_rollback_authority(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &no_op,
+ )
+ .is_err(),
+ "a no-op rollback floor validator must fail closed"
+ );
+ }
+
+ #[test]
+ fn standalone_source_contract_rejects_marker_preserving_schema_early_return() {
+ let workspace = synthetic_workspace();
+ super::super::source_maintenance::validate_schema_capacity_authority(workspace.path())
+ .expect("baseline SourceMaintenance marker layer");
+
+ let schema_path = workspace.path().join(EVENT_STORE_SCHEMA_SOURCE_RELATIVE);
+ let source = fs::read_to_string(&schema_path).expect("schema source");
+ let mut mutation = syn::parse_file(&source).expect("schema AST");
+ let outer = mutation
+ .items
+ .iter_mut()
+ .find_map(|item| match item {
+ syn::Item::Fn(function)
+ if function.sig.ident
+ == "migrate_event_store_schema_with_registry_and_generation_provider" =>
+ {
+ Some(function)
+ }
+ _ => None,
+ })
+ .expect("outer migration route");
+ outer.block.stmts.insert(
+ 0,
+ syn::parse_str("if std::hint::black_box(false) { return Ok(()); }")
+ .expect("marker-preserving early return"),
+ );
+ fs::write(&schema_path, prettyplease::unparse(&mutation))
+ .expect("write marker-preserving schema bypass");
+
+ super::super::source_maintenance::validate_schema_capacity_authority(workspace.path())
+ .expect("marker-only layer intentionally preserves all ordered witnesses");
+ let error = super::super::source_maintenance::validate_source_contract(workspace.path())
+ .expect_err("standalone SourceMaintenance authority must reject the early return");
+ assert!(
+ error.contains("authoritative schema runtime"),
+ "unexpected active governance error: {error}"
+ );
}
#[test]
@@ -16425,6 +18710,15 @@ route!(r#hex);
let import_rebind = format!(
"{import_rebind}\nasync fn validate_active_hook_state_fast(\n connection: &mut SqliteConnection,\n) -> Result<(), RadrootsEventStoreError> {{\n sqlx::query(\"DELETE FROM event_envelopes\").execute(&mut *connection).await?;\n Ok(())\n}}\n"
);
+ let catalog_delta_bypass = source.replacen(
+ "changed == expected_changed",
+ "changed.is_subset(&expected_changed)",
+ 1,
+ );
+ assert_ne!(
+ catalog_delta_bypass, source,
+ "catalog-delta bypass fixture must mutate"
+ );
for (label, mutation) in [
(
@@ -16440,19 +18734,57 @@ route!(r#hex);
prettyplease::unparse(&call_path_bypass),
),
("import-rebound hook validator", import_rebind),
+ ("widened replacement catalog delta", catalog_delta_bypass),
] {
fs::write(&schema_path, mutation).expect("write schema authority mutation");
+ let mutated_bytes = fs::read(&schema_path).expect("mutated schema bytes");
assert_ne!(
- sha256_hex(&fs::read(&schema_path).expect("mutated schema bytes")),
+ sha256_hex(&mutated_bytes),
baseline_sha256,
"{label} must rotate the successor's exact schema source descriptor"
);
+ let mutated =
+ parse_canonical_production_rust(EVENT_STORE_SCHEMA_SOURCE_RELATIVE, &mutated_bytes)
+ .expect("mutated schema authority AST");
+ let (structural_error, expected_error) = match label {
+ "malicious hookless migration arm"
+ | "bypassed migration SQL application"
+ | "widened replacement catalog delta" => (
+ validate_schema_migration_execution_authority(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &mutated,
+ )
+ .expect_err("migration execution mutation must fail closed"),
+ "authoritative schema migration execution",
+ ),
+ "migration call-path early return" => (
+ validate_schema_runtime_reachability(
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ &mutated,
+ )
+ .err()
+ .expect("migration call-path mutation must fail closed"),
+ "authoritative schema runtime",
+ ),
+ "import-rebound hook validator" => (
+ validate_privileged_store_authority(workspace.path())
+ .expect_err("hook-validator rebind must fail closed"),
+ "privileged",
+ ),
+ _ => unreachable!(),
+ };
+ assert!(
+ structural_error.contains(expected_error),
+ "unexpected {label} structural error: {structural_error}"
+ );
+ let active_error =
+ super::super::source_maintenance::validate_source_contract(workspace.path())
+ .expect_err("standalone SourceMaintenance contract must reject schema bypass");
+ assert!(
+ active_error.contains(expected_error),
+ "unexpected active {label} error: {active_error}"
+ );
if label == "migration call-path early return" {
- let mutated = parse_canonical_production_rust(
- EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
- &fs::read(&schema_path).expect("mutated schema source"),
- )
- .expect("mutated schema authority AST");
let migrate = exact_top_level_function(
EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
&mutated,
@@ -16487,6 +18819,78 @@ route!(r#hex);
}
#[test]
+ fn successor_import_authority_rejects_direct_external_rebindings() {
+ let workspace = synthetic_workspace();
+ for (relative, label, needle, replacement) in [
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "NIP-09 apply and validation routes",
+ "use crate::nip09::reconciliation_v1::{",
+ "use arbitrary_external::{",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "Food apply and validation routes",
+ "use crate::store::food_availability_projection_v1::{",
+ "use arbitrary_external::{",
+ ),
+ (
+ EVENT_STORE_SCHEMA_SOURCE_RELATIVE,
+ "migration helper routes",
+ "use crate::migrations::{",
+ "use arbitrary_external::{",
+ ),
+ (
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ "NIP-09 generated manifest route",
+ "use crate::generated::nip09_reconciliation_manifest as nip09_manifest;",
+ "use arbitrary_external::nip09_reconciliation_manifest as nip09_manifest;",
+ ),
+ (
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ "Food generated manifest route",
+ "use crate::generated::food_availability_projection_manifest as food_manifest;",
+ "use arbitrary_external::food_availability_projection_manifest as food_manifest;",
+ ),
+ (
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ "SourceMaintenance generated manifest route",
+ "use crate::generated::source_maintenance_manifest;",
+ "use arbitrary_external::source_maintenance_manifest;",
+ ),
+ ] {
+ let path = workspace.path().join(relative);
+ let source = fs::read_to_string(&path).expect("successor import authority source");
+ let mutation = source.replacen(needle, replacement, 1);
+ assert_ne!(mutation, source, "{label} fixture must mutate");
+ fs::write(&path, &mutation).expect("write external import rebind");
+ let mutation = parse_canonical_production_rust(relative, mutation.as_bytes())
+ .expect("external import rebind AST");
+ let structural_error = if relative == EVENT_STORE_SCHEMA_SOURCE_RELATIVE {
+ validate_event_store_schema_import_authority(relative, &mutation)
+ } else {
+ validate_event_store_migrations_import_authority(relative, &mutation)
+ }
+ .expect_err("external import rebind must fail exact route authority");
+ assert!(
+ structural_error.contains("production top-level import authority"),
+ "unexpected {label} structural error: {structural_error}"
+ );
+ let active_error =
+ super::super::source_maintenance::validate_source_contract(workspace.path())
+ .expect_err(
+ "standalone SourceMaintenance contract must reject external rebind",
+ );
+ assert!(
+ active_error.contains("production top-level import authority")
+ || active_error.contains("privileged terminal import"),
+ "unexpected active {label} error: {active_error}"
+ );
+ fs::write(&path, source).expect("restore successor import authority source");
+ }
+ }
+
+ #[test]
fn schema_name_matcher_witness_rejects_case_and_prefix_regressions() {
let source = repository_source(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE);
let file = parse_canonical_production_rust(
@@ -17165,11 +19569,81 @@ route!(r#hex);
.expect("current successor registry reachability");
validate_manifest_validator_reachability(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE, &baseline)
.expect("immutable predecessor descriptor reachability");
+ validate_source_maintenance_manifest_validator_reachability(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &baseline,
+ )
+ .expect("SourceMaintenance descriptor reachability");
+ validate_source_maintenance_migration_bindings(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &baseline,
+ )
+ .expect("SourceMaintenance v4 binding authority");
+ validate_event_store_migrations_import_authority(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &baseline,
+ )
+ .expect("migration import authority");
+ validate_event_store_migration_support_authority(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &baseline,
+ )
+ .expect("active migration support authority");
expected_event_store_migration_compiler_inputs(workspace.path(), &baseline)
.expect("current versioned migration compiler inputs");
for (label, needle, replacement) in [
(
+ "SourceMaintenance v4 version",
+ " version: 4,\n name: \"source_maintenance\",",
+ " version: 5,\n name: \"source_maintenance\",",
+ ),
+ (
+ "SourceMaintenance v4 hook",
+ " hook: EventStoreMigrationHook::SourceMaintenanceV1,",
+ " hook: EventStoreMigrationHook::None,",
+ ),
+ (
+ "SourceMaintenance v4 manifest hash",
+ " hook_manifest_sha256: Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256),",
+ " hook_manifest_sha256: Some(food_manifest::FOOD_AVAILABILITY_PROJECTION_MANIFEST_SHA256),",
+ ),
+ (
+ "SourceMaintenance v4 registry version",
+ " source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION,",
+ " food_manifest::FOOD_AVAILABILITY_PROJECTION_EVENT_CONTRACT_REGISTRY_VERSION,",
+ ),
+ (
+ "SourceMaintenance v4 replacement inventory binding",
+ " replaced_object_names: EVENT_STORE_SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES,",
+ " replaced_object_names: &[],",
+ ),
+ ] {
+ let mutation = source.replacen(needle, replacement, 1);
+ assert_ne!(mutation, source, "{label} fixture must mutate");
+ fs::write(&migrations_path, &mutation).expect("write v4 authority mutation");
+ let mutation = parse_canonical_production_rust(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ mutation.as_bytes(),
+ )
+ .expect("mutated SourceMaintenance v4 AST");
+ assert!(
+ expected_event_store_migration_compiler_inputs(workspace.path(), &mutation)
+ .is_err()
+ || validate_source_maintenance_migration_bindings(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &mutation,
+ )
+ .is_err(),
+ "{label} drift must fail closed"
+ );
+ super::super::source_maintenance::validate_source_contract(workspace.path())
+ .expect_err("standalone SourceMaintenance contract must reject v4 binding drift");
+ fs::write(&migrations_path, &source).expect("restore migration authority source");
+ }
+
+ for (label, needle, replacement) in [
+ (
"ledger DDL",
") STRICT, WITHOUT ROWID\";",
") STRICT, WITHOUT ROWID /* authority mutation */\";",
@@ -17209,6 +19683,26 @@ route!(r#hex);
" let mut expected_version = minimum;",
" let mut expected_version = { return Ok(()); minimum };",
),
+ (
+ "duplicate hook reuse guard",
+ " if !migration_hook_ids.insert(migration.hook.id()) {",
+ " if false && !migration_hook_ids.insert(migration.hook.id()) {",
+ ),
+ (
+ "canonical hook migration binding",
+ " if migration.version != canonical_version || migration.name != canonical_name {",
+ " if false && (migration.version != canonical_version || migration.name != canonical_name) {",
+ ),
+ (
+ "exact predecessor replacement ownership",
+ " if prior_owners.len() != 1 {",
+ " if prior_owners.is_empty() {",
+ ),
+ (
+ "predecessor table replacement prohibition",
+ " if prior_owner.owned_table_names.contains(object_name)\n || prior_owner.fts5_table_names.contains(object_name)",
+ " if prior_owner.owned_table_names.contains(object_name)\n && prior_owner.fts5_table_names.contains(object_name)",
+ ),
] {
let mutation = source.replacen(needle, replacement, 1);
assert_ne!(mutation, source, "{label} fixture must mutate");
@@ -17218,38 +19712,51 @@ route!(r#hex);
baseline_sha256,
"{label} must rotate the successor's exact migration source descriptor"
);
+ let mutation = parse_canonical_production_rust(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &fs::read(&migrations_path).expect("mutated migration bytes"),
+ )
+ .expect("mutated migration support AST");
+ let structural_error = match validate_event_store_migration_support_authority(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &mutation,
+ ) {
+ Ok(()) => panic!("{label} active migration support mutation must fail closed"),
+ Err(error) => error,
+ };
+ assert!(
+ structural_error.contains("active migration support token authority"),
+ "unexpected {label} structural error: {structural_error}"
+ );
+ let active_error =
+ super::super::source_maintenance::validate_source_contract(workspace.path())
+ .expect_err(
+ "standalone SourceMaintenance contract must reject migration support drift",
+ );
+ assert!(
+ active_error.contains("active migration support token authority"),
+ "unexpected active {label} error: {active_error}"
+ );
fs::write(&migrations_path, &source).expect("restore migration authority source");
}
- let mut mutation = syn::parse_file(&source).expect("migration authority AST");
- let validator = mutation
- .items
- .iter_mut()
- .find_map(|item| match item {
- syn::Item::Fn(function)
- if function.sig.ident == "validate_generated_nip09_manifest_descriptor" =>
- {
- Some(function)
- }
- _ => None,
- })
- .expect("generated-manifest validator");
- let initializer = validator
- .block
- .stmts
- .iter_mut()
- .find_map(|statement| match statement {
- syn::Stmt::Local(local)
- if local_pattern_ident(&local.pat).as_deref() == Some("up_byte_length") =>
- {
- local.init.as_mut()
- }
- _ => None,
- })
- .expect("up-byte-length initializer");
- *initializer.expr =
- syn::parse_str("{ return Ok(()); 0_u64 }").expect("early-return initializer");
- let mutation = prettyplease::unparse(&mutation);
+ let mutation = source.replacen(
+ r#" let up_byte_length = u64::try_from(
+ nip09_manifest::NIP09_RECONCILIATION_MIGRATION_UP_BYTE_LENGTH,
+ )
+ .map_err(|_| RadrootsEventStoreError::MigrationRegistryDefect {
+ reason: "generated NIP-09 migration up byte length is out of range".to_owned(),
+ })?;"#,
+ r#" let up_byte_length = {
+ return Ok(());
+ 0_u64
+ };"#,
+ 1,
+ );
+ assert_ne!(
+ mutation, source,
+ "generated-manifest validator fixture must mutate only its initializer"
+ );
fs::write(&migrations_path, &mutation)
.expect("write manifest-validator early-return mutation");
assert_ne!(
@@ -17290,10 +19797,79 @@ route!(r#hex);
.is_err(),
"the structural divergence audit must reject an early return"
);
+ let structural_error = validate_event_store_migration_support_authority(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ &mutation,
+ )
+ .expect_err("predecessor descriptor bypass must fail active support authority");
+ assert!(structural_error.contains("active migration support token authority"));
+ let active_error =
+ super::super::source_maintenance::validate_source_contract(workspace.path())
+ .expect_err("standalone SourceMaintenance contract must reject predecessor bypass");
+ assert!(
+ active_error.contains("active migration support token authority"),
+ "unexpected active predecessor descriptor error: {active_error}"
+ );
fs::write(&migrations_path, &source).expect("restore migration authority source");
}
#[test]
+ fn source_replacement_inventory_and_sql_restoration_are_active_authority() {
+ let workspace = synthetic_workspace();
+ let migrations_path = workspace
+ .path()
+ .join(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE);
+ let migrations = fs::read_to_string(&migrations_path).expect("migration registry source");
+ let replacement_mutation = migrations.replacen(
+ " \"radroots_event_store_food_availability_image_delete_guard\",\n",
+ "",
+ 1,
+ );
+ assert_ne!(
+ replacement_mutation, migrations,
+ "replacement inventory fixture must mutate"
+ );
+ fs::write(&migrations_path, replacement_mutation)
+ .expect("write replacement inventory mutation");
+ let error = super::super::source_maintenance::validate_source_contract(workspace.path())
+ .expect_err("replacement inventory drift must fail active SourceMaintenance authority");
+ assert!(
+ error.contains("migration catalog differs"),
+ "unexpected replacement inventory error: {error}"
+ );
+ fs::write(&migrations_path, migrations).expect("restore migration registry source");
+
+ for (relative, label, needle, replacement) in [
+ (
+ "crates/event_store/migrations/0004_source_maintenance.up.sql",
+ "widened managed-v4 marker predicate",
+ " AND NEW.prior_last_transition_seq = state.last_transition_seq\n",
+ " AND NEW.prior_last_transition_seq >= state.last_transition_seq\n",
+ ),
+ (
+ "crates/event_store/migrations/0004_source_maintenance.down.sql",
+ "omitted exact v3 marker restoration predicate",
+ " AND NEW.transition_floor_seq = state.last_transition_seq\n",
+ "",
+ ),
+ ] {
+ let path = workspace.path().join(relative);
+ let source = fs::read_to_string(&path).expect("replacement SQL source");
+ let mutation = source.replacen(needle, replacement, 1);
+ assert_ne!(mutation, source, "{label} fixture must mutate");
+ fs::write(&path, mutation).expect("write replacement SQL mutation");
+ let error =
+ super::super::source_maintenance::validate_source_contract(workspace.path())
+ .expect_err("replacement SQL drift must fail exact migration identity");
+ assert!(
+ error.contains("reviewed v4 identity"),
+ "unexpected {label} error: {error}"
+ );
+ fs::write(&path, source).expect("restore replacement SQL source");
+ }
+ }
+
+ #[test]
fn hookless_future_migration_does_not_churn_nip09_v1_artifacts() {
let workspace = synthetic_workspace();
let bundle_paths = [
@@ -17314,14 +19890,14 @@ route!(r#hex);
fs::write(
workspace
.path()
- .join("crates/event_store/migrations/0004_future_probe.up.sql"),
+ .join("crates/event_store/migrations/0005_future_probe.up.sql"),
up_sql,
)
.expect("write future up migration");
fs::write(
workspace
.path()
- .join("crates/event_store/migrations/0004_future_probe.down.sql"),
+ .join("crates/event_store/migrations/0005_future_probe.down.sql"),
down_sql,
)
.expect("write future down migration");
@@ -17331,24 +19907,25 @@ route!(r#hex);
.join(EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE);
let migrations = fs::read_to_string(&migrations_path).expect("migration registry source");
let migrations = migrations.replacen(
- "pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 3;",
"pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 4;",
+ "pub const RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT: u32 = 5;",
1,
);
let registry_tail = " },\n];\n\npub(crate) fn migration_for_version";
let future_entry = format!(
r#" }},
EventStoreMigration {{
- version: 4,
+ version: 5,
name: "future_probe",
- up_sql: include_str!("../migrations/0004_future_probe.up.sql"),
- down_sql: include_str!("../migrations/0004_future_probe.down.sql"),
+ up_sql: include_str!("../migrations/0005_future_probe.up.sql"),
+ down_sql: include_str!("../migrations/0005_future_probe.down.sql"),
up_len: {},
down_len: {},
up_sha256: "{}",
down_sha256: "{}",
schema_sha256: "0000000000000000000000000000000000000000000000000000000000000000",
owned_object_names: &["radroots_event_store_future_probe"],
+ replaced_object_names: &[],
owned_table_names: &["radroots_event_store_future_probe"],
fts5_table_names: &[],
hook: EventStoreMigrationHook::None,
@@ -17365,10 +19942,11 @@ pub(crate) fn migration_for_version"#,
);
let migrations = migrations.replacen(registry_tail, &future_entry, 1);
assert!(
- migrations.contains("version: 4"),
+ migrations.contains("version: 5")
+ && migrations.contains("../migrations/0005_future_probe.up.sql"),
"future migration fixture must extend the registry"
);
- fs::write(&migrations_path, migrations).expect("write future migration registry");
+ fs::write(&migrations_path, &migrations).expect("write future migration registry");
let registry = parse_canonical_production_rust(
EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
@@ -17378,6 +19956,60 @@ pub(crate) fn migration_for_version"#,
expected_event_store_migration_compiler_inputs(workspace.path(), ®istry)
.expect("versioned successor and isolated future compiler inputs");
+ for (field, needle, replacement) in [
+ (
+ "hook_manifest_sha256",
+ "hook_manifest_sha256: None",
+ "hook_manifest_sha256: Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256)",
+ ),
+ (
+ "event_contract_registry_version",
+ "event_contract_registry_version: None",
+ "event_contract_registry_version: Some(source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION)",
+ ),
+ ] {
+ let mut invalid = migrations.clone();
+ let index = invalid
+ .rfind(needle)
+ .expect("future hookless field must be the final matching field");
+ invalid.replace_range(index..index + needle.len(), replacement);
+ let invalid = parse_canonical_production_rust(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ invalid.as_bytes(),
+ )
+ .expect("invalid future hook authority AST");
+ let error = expected_event_store_migration_compiler_inputs(workspace.path(), &invalid)
+ .expect_err("hookless v5 migration must reject non-None authority");
+ assert!(
+ error.contains(field) && error.contains("versioned hook authority"),
+ "unexpected v5 `{field}` error: {error}"
+ );
+ }
+
+ let mut invalid_replacements = migrations.clone();
+ let needle = "replaced_object_names: &[]";
+ let index = invalid_replacements
+ .rfind(needle)
+ .expect("future hookless replacement field must be the final matching field");
+ invalid_replacements.replace_range(
+ index..index + needle.len(),
+ "replaced_object_names: &[\"radroots_event_store_food_availability_projection_delete_guard\"]",
+ );
+ let invalid_replacements = parse_canonical_production_rust(
+ EVENT_STORE_MIGRATIONS_SOURCE_RELATIVE,
+ invalid_replacements.as_bytes(),
+ )
+ .expect("invalid future replacement authority AST");
+ let error =
+ expected_event_store_migration_compiler_inputs(workspace.path(), &invalid_replacements)
+ .expect_err("hookless v5 migration must reject predecessor replacements");
+ assert!(
+ error.contains("hookless post-v2 migration 5")
+ && error.contains("predecessor replacements")
+ && error.contains("separately authenticated successor authority"),
+ "unexpected v5 replacement authority error: {error}"
+ );
+
for (relative, before) in bundle_paths.into_iter().zip(before) {
let after = read_regular_file(workspace.path(), relative).expect("future artifact");
assert_eq!(
@@ -17388,7 +20020,7 @@ pub(crate) fn migration_for_version"#,
}
let future_up_path = workspace
.path()
- .join("crates/event_store/migrations/0004_future_probe.up.sql");
+ .join("crates/event_store/migrations/0005_future_probe.up.sql");
for malicious_sql in [
"INSERT INTO event_envelopes(raw_json) VALUES ('{}');\n",
"INSERT INTO 'event_envelopes'(raw_json) VALUES ('{}');\n",
@@ -17431,9 +20063,9 @@ pub(crate) fn migration_for_version"#,
fs::write(&future_up_path, malicious_sql).expect("write coupled future migration");
let error = validate_hookless_post_v2_migration_sql_isolated(
workspace.path(),
- 4,
+ 5,
"up",
- "crates/event_store/migrations/0004_future_probe.up.sql",
+ "crates/event_store/migrations/0005_future_probe.up.sql",
)
.expect_err("hookless future migration must not couple to v1 authority");
assert!(
diff --git a/tools/xtask/src/contract/source_maintenance.rs b/tools/xtask/src/contract/source_maintenance.rs
@@ -0,0 +1,4284 @@
+#![allow(dead_code)]
+
+use super::artifact_bundle::{
+ GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction,
+};
+use super::food_availability_projection::{
+ validate_food_availability_projection_manifest_under_lock,
+ validate_food_availability_projection_predecessor_production_sources_under_lock,
+};
+use super::nip09_reconciliation::validate_current_event_store_successor_authority;
+use quote::ToTokens;
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
+use sha2::{Digest, Sha256};
+use std::collections::{BTreeMap, BTreeSet};
+use std::path::Path;
+use syn::{Expr, Item, UseTree};
+
+const SCHEMA_VERSION: u32 = 1;
+const CONTRACT_ID: &str = "radroots_event_store.source_maintenance_v1";
+const HOOK_ID: &str = "source_maintenance_v1";
+const MIGRATION_VERSION: u32 = 4;
+const MIGRATION_NAME: &str = "source_maintenance";
+const CAPACITY_VERSION: u32 = 1;
+const EVENT_CONTRACT_REGISTRY_VERSION: u32 = 7;
+const CAPACITY_AUTHORITY_ID: &str = "radroots_event_store_source_capacity_v1";
+const ACCOUNTING_ALGORITHM: &str = "sqlite_cast_blob_octet_sum_v1";
+const REOPEN_VALIDATION_MODE: &str = "bounded_full_raw_recount_v1";
+const GENERATION_HISTORY_VALIDATION: &str = "bounded_count_plus_active_ordinal_v1";
+const RAW_EVENT_COUNT_LIMIT: u64 = 25_000;
+const RAW_TAG_COUNT_LIMIT: u64 = 250_000;
+const RAW_EVENT_TEXT_BYTES_LIMIT: u64 = 67_108_864;
+const RAW_TAG_TEXT_BYTES_LIMIT: u64 = 33_554_432;
+const RETAINED_SOURCE_GENERATION_LIMIT: u32 = 8;
+const RAW_EVENT_REJECTION_SCAN_BOUND: u64 = RAW_EVENT_COUNT_LIMIT + 1;
+const RAW_TAG_REJECTION_SCAN_BOUND: u64 = RAW_TAG_COUNT_LIMIT + 1;
+const RETAINED_GENERATION_REJECTION_SCAN_BOUND: u32 = RETAINED_SOURCE_GENERATION_LIMIT + 1;
+const SCHEMA_SHA256: &str = "d526d96ea02be12b4b0aed99e97cfdde17c4474ace67111506a7b900ee78b186";
+const HASH_ALGORITHM: &str = "sha256_bytes_v1";
+const WRITE_COMMAND: &str = "cargo xtask contract source-maintenance-manifest --write";
+
+const PREDECESSOR_HOOK_ID: &str = "food_availability_projection_v1";
+const PREDECESSOR_MANIFEST_RELATIVE: &str =
+ "crates/event_store/contracts/food_availability_projection_v1.manifest.json";
+const PREDECESSOR_MANIFEST_BYTE_LENGTH: usize = 17_455;
+const PREDECESSOR_MANIFEST_SHA256: &str =
+ "33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23";
+const NIP09_HOOK_ID: &str = "nip09_reconciliation_v1";
+const NIP09_MANIFEST_SHA256: &str =
+ "74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77";
+const FOOD_SCOPE_FINGERPRINT_SHA256: &str =
+ "8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0";
+const ACTIVE_GENERATION_AUTHORITY: &str = "radroots_event_store_source_state";
+const MARKER_CLOSE_AUTHORITY: &str = "radroots_event_store_source_capacity_marker_close_guard";
+
+const MANIFEST_RELATIVE: &str = "crates/event_store/contracts/source_maintenance_v1.manifest.json";
+const MANIFEST_SCHEMA_RELATIVE: &str =
+ "crates/event_store/contracts/source_maintenance_v1.manifest.schema.json";
+const MANIFEST_SHA256_RELATIVE: &str =
+ "crates/event_store/contracts/source_maintenance_v1.manifest.sha256";
+const GENERATED_DESCRIPTOR_RELATIVE: &str =
+ "crates/event_store/src/generated/source_maintenance_manifest.rs";
+const MIGRATIONS_SOURCE_RELATIVE: &str = "crates/event_store/src/migrations.rs";
+const MIGRATION_UP_RELATIVE: &str = "crates/event_store/migrations/0004_source_maintenance.up.sql";
+const MIGRATION_DOWN_RELATIVE: &str =
+ "crates/event_store/migrations/0004_source_maintenance.down.sql";
+const RESULT_VECTOR_CANONICAL_RELATIVE: &str =
+ "contracts/conformance/vectors/event_store/source_maintenance.v1.json";
+const RESULT_VECTOR_MIRROR_RELATIVE: &str =
+ "crates/event_store/tests/fixtures/source_maintenance.v1.json";
+const RESULT_VECTOR_EXECUTOR_RELATIVE: &str =
+ "crates/event_store/tests/source_maintenance_v1_result_vector.rs";
+const RESULT_VECTOR_EXECUTOR_ID: &str =
+ "radroots_event_store.source_maintenance_v1.result_vector_executor.v1";
+const RESULT_VECTOR_EXECUTOR_TEST: &str = "source_maintenance_v1_result_vector";
+const CONTRACT_COMMAND_SOURCE_RELATIVE: &str = "tools/xtask/src/contract.rs";
+const XTASK_MAIN_SOURCE_RELATIVE: &str = "tools/xtask/src/main.rs";
+const XTASK_MAIN_FULL_AST_SHA256: &str =
+ "b48c71c7f40f45c89bd7c83935d48eac3a1a367c8f73f62262e8ee14404616b4";
+
+const RAW_EVENT_COLUMNS: &[&str] = &[
+ "event_id",
+ "pubkey",
+ "tags_json",
+ "content",
+ "sig",
+ "raw_json",
+];
+const RAW_TAG_COLUMNS: &[&str] = &["event_id", "tag_name", "tag_value", "tag_json"];
+const NULLABLE_RAW_TAG_COLUMNS: &[&str] = &["tag_value"];
+
+const EXPECTED_CATALOG_OBJECTS: &[&str] = &[
+ "radroots_event_store_source_capacity_delete_guard",
+ "radroots_event_store_source_capacity_insert_guard",
+ "radroots_event_store_source_capacity_marker_close_guard",
+ "radroots_event_store_source_capacity_update_guard",
+ "radroots_event_store_source_capacity_v1",
+ "radroots_event_store_source_generation_capacity_advance",
+ "radroots_event_store_source_generation_capacity_guard",
+];
+const EXPECTED_CATALOG_TABLES: &[&str] = &["radroots_event_store_source_capacity_v1"];
+const EXPECTED_REPLACED_CATALOG_OBJECTS: &[&str] = &[
+ "radroots_event_store_food_availability_image_delete_guard",
+ "radroots_event_store_food_availability_projection_delete_guard",
+ "radroots_event_store_source_rebuild_marker_insert_guard",
+];
+
+const INHERITED_PUBLIC_API: &[&str] = &[
+ "RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1",
+ "RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1",
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1",
+ "RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1",
+ "RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1",
+ "RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1",
+ "RadrootsAddressableTransitionCauseV1",
+ "RadrootsAddressableTransitionCoordinateV1",
+ "RadrootsAddressableTransitionCursorV1",
+ "RadrootsAddressableTransitionEventReferenceV1",
+ "RadrootsAddressableTransitionOriginV1",
+ "RadrootsAddressableTransitionPageV1",
+ "RadrootsAddressableTransitionRawHeadDecisionV1",
+ "RadrootsAddressableTransitionScopeFingerprintV1",
+ "RadrootsAddressableTransitionScopeV1",
+ "RadrootsAddressableTransitionV1",
+ "RadrootsAddressableTransitionVisibilityV1",
+ "RadrootsCurrentEventVisibilityV1",
+ "RadrootsCurrentVisibilityDecisionV1",
+ "RadrootsFoodAvailabilitySearchQueryV1",
+ "RadrootsFoodAvailabilityStatusFilterV1",
+ "RadrootsNip09SuppressionEvidenceV1",
+ "RadrootsNip09SuppressionOutcome",
+ "RadrootsNip09SuppressionReason",
+ "RadrootsStoreProducedCanonicalEventV1",
+ "RadrootsStoredFoodAvailabilityImageV1",
+ "RadrootsStoredFoodAvailabilityV1",
+];
+
+const ADDED_PUBLIC_API: &[&str] = &[
+ "RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1",
+ "RadrootsEventStoreSourceCapacityResourceV1",
+ "RadrootsEventStoreSourceCapacityV1",
+];
+
+const PUBLIC_METHODS: &[&str] = &[
+ "RadrootsEventStore::source_capacity_v1",
+ "RadrootsEventStoreSourceCapacityResourceV1::as_str",
+ "RadrootsEventStoreSourceCapacityV1::source_generation",
+ "RadrootsEventStoreSourceCapacityV1::raw_event_count",
+ "RadrootsEventStoreSourceCapacityV1::raw_tag_count",
+ "RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes",
+ "RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes",
+ "RadrootsEventStoreSourceCapacityV1::raw_high_water_seq",
+ "RadrootsEventStoreSourceCapacityV1::retained_generation_count",
+ "RadrootsEventStoreSourceCapacityV1::retained_generation_limit",
+];
+const ERROR_VARIANTS: &[&str] = &[
+ "SourceCapacityExceeded",
+ "SourceGenerationHistoryLimitReached",
+ "PersistedEphemeralRawEvent",
+ "SourceCapacityStateDrift",
+ "SqliteMainDatabaseEncodingNotUtf8",
+ "RollbackWouldDiscardSourceGenerationHistory",
+];
+const REMOVED_PUBLIC_API: &[&str] = &[
+ "RadrootsEventStoreReconciliationResource",
+ "RadrootsEventStoreError::ReconciliationCapacityExceeded",
+];
+const BREAKING_PUBLIC_API_REPLACEMENTS: &[(&str, &str)] = &[
+ (
+ "RadrootsEventStoreReconciliationResource",
+ "RadrootsEventStoreSourceCapacityResourceV1",
+ ),
+ (
+ "RadrootsEventStoreError::ReconciliationCapacityExceeded",
+ "RadrootsEventStoreError::SourceCapacityExceeded",
+ ),
+];
+
+const GOVERNED_MODEL_MODULES: &[&str] = &[
+ "addressable_transition_feed_v1",
+ "current_visibility_v1",
+ "food_availability_projection_v1",
+];
+
+const ENTRY_POINTS: &[(&str, &str)] = &[
+ (
+ "migration_registry",
+ "radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]",
+ ),
+ (
+ "migration_apply_hook",
+ "radroots_event_store::schema::apply_migration_hook",
+ ),
+ (
+ "migration_validation_hook",
+ "radroots_event_store::schema::validate_migration_hook_state",
+ ),
+ (
+ "capacity_query",
+ "radroots_event_store::RadrootsEventStore::source_capacity_v1",
+ ),
+ (
+ "raw_append_preflight",
+ "radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1",
+ ),
+ (
+ "raw_append_advance",
+ "radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1",
+ ),
+ (
+ "generation_append_preflight",
+ "radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1",
+ ),
+ (
+ "generation_rebuild_bind",
+ "radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1",
+ ),
+ (
+ "sqlite_encoding_preflight",
+ "radroots_event_store::store::validate_main_database_encoding",
+ ),
+ (
+ "source_generation_history_rollback_guard",
+ "radroots_event_store::schema::validate_rollback_preserves_source_generation_history",
+ ),
+ ("result_vector_executor", RESULT_VECTOR_EXECUTOR_TEST),
+];
+
+#[derive(Clone, Copy)]
+struct SourceSpec {
+ role: &'static str,
+ path: &'static str,
+}
+
+const SOURCE_SPECS: &[SourceSpec] = &[
+ SourceSpec {
+ role: "event_store_error_and_limits",
+ path: "crates/event_store/src/error.rs",
+ },
+ SourceSpec {
+ role: "generated_descriptor_registration",
+ path: "crates/event_store/src/generated.rs",
+ },
+ SourceSpec {
+ role: "public_surface",
+ path: "crates/event_store/src/lib.rs",
+ },
+ SourceSpec {
+ role: "migration_registry",
+ path: MIGRATIONS_SOURCE_RELATIVE,
+ },
+ SourceSpec {
+ role: "predecessor_model_public_surface",
+ path: "crates/event_store/src/model.rs",
+ },
+ SourceSpec {
+ role: "source_generation_rebuild_authority",
+ path: "crates/event_store/src/nip09/reconciliation_v1.rs",
+ },
+ SourceSpec {
+ role: "schema_migration_and_reopen_authority",
+ path: "crates/event_store/src/schema.rs",
+ },
+ SourceSpec {
+ role: "public_store_and_transaction_authority",
+ path: "crates/event_store/src/store.rs",
+ },
+ SourceSpec {
+ role: "raw_ingest_capacity_authority",
+ path: "crates/event_store/src/store/protocol_reconciliation_v1.rs",
+ },
+ SourceSpec {
+ role: "source_maintenance_runtime",
+ path: "crates/event_store/src/source_maintenance_v1.rs",
+ },
+ SourceSpec {
+ role: "artifact_transaction_authority",
+ path: "tools/xtask/src/contract/artifact_bundle.rs",
+ },
+ SourceSpec {
+ role: "predecessor_successor_governance",
+ path: "tools/xtask/src/contract/food_availability_projection.rs",
+ },
+ SourceSpec {
+ role: "transitive_predecessor_membership_governance",
+ path: "tools/xtask/src/contract/nip09_reconciliation.rs",
+ },
+ SourceSpec {
+ role: "source_maintenance_governance",
+ path: "tools/xtask/src/contract/source_maintenance.rs",
+ },
+ SourceSpec {
+ role: "contract_command_authority",
+ path: "tools/xtask/src/contract.rs",
+ },
+ SourceSpec {
+ role: "xtask_dispatch_and_release_preflight",
+ path: "tools/xtask/src/main.rs",
+ },
+];
+
+#[cfg(test)]
+pub(super) fn source_contract_fixture_source_paths() -> Vec<&'static str> {
+ SOURCE_SPECS.iter().map(|source| source.path).collect()
+}
+
+const PREDECESSOR_SUPERSEDED_SOURCE_PATHS: &[&str] = &[
+ "crates/event_store/src/error.rs",
+ "crates/event_store/src/generated.rs",
+ "crates/event_store/src/lib.rs",
+ "crates/event_store/src/migrations.rs",
+ "crates/event_store/src/model.rs",
+ "crates/event_store/src/nip09/reconciliation_v1.rs",
+ "crates/event_store/src/schema.rs",
+ "crates/event_store/src/store.rs",
+ "crates/event_store/src/store/protocol_reconciliation_v1.rs",
+];
+
+const GENERATED_ARTIFACT_PATHS: &[&str] = &[
+ MANIFEST_RELATIVE,
+ MANIFEST_SCHEMA_RELATIVE,
+ MANIFEST_SHA256_RELATIVE,
+ GENERATED_DESCRIPTOR_RELATIVE,
+ RESULT_VECTOR_MIRROR_RELATIVE,
+];
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct SourceMaintenanceManifest {
+ schema_version: u32,
+ contract_id: String,
+ hook_id: String,
+ manifest_schema: FileDescriptor,
+ predecessor: PredecessorDescriptor,
+ migration: MigrationDescriptor,
+ source_maintenance: SourceMaintenanceDescriptor,
+ entry_points: Vec<EntryPointDescriptor>,
+ source_files: Vec<SourceFileDescriptor>,
+ public_api: PublicApiDescriptor,
+ result_vector: ResultVectorDescriptor,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct FileDescriptor {
+ path: String,
+ byte_length: u64,
+ sha256: String,
+ hash_algorithm: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct PredecessorDescriptor {
+ hook_id: String,
+ manifest: FileDescriptor,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct MigrationDescriptor {
+ version: u32,
+ name: String,
+ up: FileDescriptor,
+ down: FileDescriptor,
+ schema_sha256: String,
+ catalog: CatalogDescriptor,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct CatalogDescriptor {
+ objects: Vec<String>,
+ replaced_objects: Vec<String>,
+ tables: Vec<String>,
+ fts5_tables: Vec<String>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct SourceMaintenanceDescriptor {
+ version: u32,
+ event_contract_registry_version: u32,
+ capacity_authority_id: String,
+ accounting: AccountingDescriptor,
+ limits: LimitDescriptor,
+ reopen_validation: ReopenValidationDescriptor,
+ rebuild_seal: RebuildSealDescriptor,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct AccountingDescriptor {
+ algorithm: String,
+ raw_event_columns: Vec<String>,
+ raw_tag_columns: Vec<String>,
+ nullable_raw_tag_columns: Vec<String>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct LimitDescriptor {
+ raw_events: u64,
+ raw_tags: u64,
+ raw_event_text_bytes: u64,
+ raw_tag_text_bytes: u64,
+ retained_source_generations: u32,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ReopenValidationDescriptor {
+ mode: String,
+ raw_event_rejection_scan_bound: u64,
+ raw_tag_rejection_scan_bound: u64,
+ generation_history_validation: String,
+ retained_generation_rejection_scan_bound: u32,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct RebuildSealDescriptor {
+ nip09_hook_id: String,
+ nip09_manifest_sha256: String,
+ food_hook_id: String,
+ food_manifest_sha256: String,
+ food_scope_fingerprint_sha256: String,
+ active_generation_authority: String,
+ marker_close_authority: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct EntryPointDescriptor {
+ role: String,
+ rust_path: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct SourceFileDescriptor {
+ role: String,
+ path: String,
+ byte_length: u64,
+ sha256: String,
+ hash_algorithm: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct PublicApiDescriptor {
+ inherited_predecessor_symbols: Vec<String>,
+ added_symbols: Vec<String>,
+ methods: Vec<String>,
+ error_variants: Vec<String>,
+ removed_symbols: Vec<String>,
+ breaking_replacements: Vec<PublicApiReplacementDescriptor>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct PublicApiReplacementDescriptor {
+ removed: String,
+ replacement: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct ResultVectorDescriptor {
+ canonical_path: String,
+ mirror_path: String,
+ byte_length: u64,
+ sha256: String,
+ hash_algorithm: String,
+ executor_id: String,
+ executor_path: String,
+ executor_test: String,
+ executor_byte_length: u64,
+ executor_sha256: String,
+ executor_hash_algorithm: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct SourceMaintenanceVector {
+ schema_version: u32,
+ contract_id: String,
+ capacity_version: u32,
+ limits: LimitDescriptor,
+ accounting: AccountingDescriptor,
+ cases: Vec<VectorCase>,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+struct VectorCase {
+ id: String,
+ execution: String,
+ authority: String,
+ authority_path: String,
+ resource: Option<String>,
+ boundary: Option<String>,
+ expected_outcome: String,
+ error_domain: Option<String>,
+ expected_error: Option<String>,
+}
+
+pub(crate) fn write_source_maintenance_manifest(workspace_root: &Path) -> Result<(), String> {
+ with_artifact_bundle_transaction(workspace_root, |transaction| {
+ let artifacts = expected_artifacts(workspace_root)?;
+ transaction.write(artifacts)?;
+ validate_source_maintenance_manifest_under_lock(workspace_root)
+ })
+}
+
+pub(crate) fn validate_source_maintenance_manifest(workspace_root: &Path) -> Result<(), String> {
+ with_artifact_bundle_transaction(workspace_root, |_| {
+ validate_source_maintenance_manifest_under_lock(workspace_root)
+ })
+}
+
+pub(super) fn validate_source_maintenance_manifest_under_lock(
+ workspace_root: &Path,
+) -> Result<(), String> {
+ let expected = expected_artifacts(workspace_root)?;
+ for artifact in expected {
+ let actual = read_regular_file(workspace_root, artifact.relative)?;
+ if actual != artifact.contents {
+ return Err(stale_error(artifact.relative));
+ }
+ }
+
+ let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
+ let manifest_value: Value = serde_json::from_slice(&manifest_bytes)
+ .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
+ let manifest: SourceMaintenanceManifest = serde_json::from_value(manifest_value.clone())
+ .map_err(|error| format!("parse typed {MANIFEST_RELATIVE}: {error}"))?;
+ validate_canonical_json(MANIFEST_RELATIVE, &manifest_bytes, &manifest)?;
+ validate_manifest_shape(&manifest)?;
+
+ let schema_bytes = read_regular_file(workspace_root, MANIFEST_SCHEMA_RELATIVE)?;
+ let schema: Value = serde_json::from_slice(&schema_bytes)
+ .map_err(|error| format!("parse {MANIFEST_SCHEMA_RELATIVE}: {error}"))?;
+ validate_canonical_json(MANIFEST_SCHEMA_RELATIVE, &schema_bytes, &schema)?;
+ validate_manifest_json_schema(&schema, &manifest_value)?;
+
+ let digest = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?;
+ validate_digest_sidecar(MANIFEST_SHA256_RELATIVE, &digest)?;
+ if digest != format!("{}\n", sha256_hex(&manifest_bytes)).as_bytes() {
+ return Err(format!(
+ "{MANIFEST_SHA256_RELATIVE} must match the checked-in manifest bytes"
+ ));
+ }
+
+ let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?;
+ let mirror_bytes = read_regular_file(workspace_root, RESULT_VECTOR_MIRROR_RELATIVE)?;
+ if vector_bytes != mirror_bytes {
+ return Err(format!(
+ "{RESULT_VECTOR_MIRROR_RELATIVE} must exactly mirror {RESULT_VECTOR_CANONICAL_RELATIVE}"
+ ));
+ }
+ let vector: SourceMaintenanceVector = serde_json::from_slice(&vector_bytes)
+ .map_err(|error| format!("parse {RESULT_VECTOR_CANONICAL_RELATIVE}: {error}"))?;
+ validate_canonical_json(RESULT_VECTOR_CANONICAL_RELATIVE, &vector_bytes, &vector)?;
+ validate_result_vector(workspace_root, &vector)?;
+ Ok(())
+}
+
+fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> {
+ let schema = manifest_schema();
+ let schema_bytes = canonical_json_bytes(&schema)?;
+ let manifest = describe_manifest(workspace_root, &schema_bytes)?;
+ let manifest_bytes = canonical_json_bytes(&manifest)?;
+ let manifest_sha256 = sha256_hex(&manifest_bytes);
+ let descriptor = generated_descriptor(&manifest, &manifest_bytes, &manifest_sha256);
+ let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?;
+
+ Ok(vec![
+ GeneratedArtifact {
+ relative: MANIFEST_RELATIVE,
+ contents: manifest_bytes,
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SCHEMA_RELATIVE,
+ contents: schema_bytes,
+ },
+ GeneratedArtifact {
+ relative: MANIFEST_SHA256_RELATIVE,
+ contents: format!("{manifest_sha256}\n").into_bytes(),
+ },
+ GeneratedArtifact {
+ relative: GENERATED_DESCRIPTOR_RELATIVE,
+ contents: descriptor.into_bytes(),
+ },
+ GeneratedArtifact {
+ relative: RESULT_VECTOR_MIRROR_RELATIVE,
+ contents: vector_bytes,
+ },
+ ])
+}
+
+fn describe_manifest(
+ workspace_root: &Path,
+ schema_bytes: &[u8],
+) -> Result<SourceMaintenanceManifest, String> {
+ validate_food_availability_projection_manifest_under_lock(workspace_root)?;
+ validate_source_contract(workspace_root)?;
+ validate_predecessor_production_source_coverage(workspace_root)?;
+
+ let predecessor_bytes = read_regular_file(workspace_root, PREDECESSOR_MANIFEST_RELATIVE)?;
+ if predecessor_bytes.len() != PREDECESSOR_MANIFEST_BYTE_LENGTH
+ || sha256_hex(&predecessor_bytes) != PREDECESSOR_MANIFEST_SHA256
+ {
+ return Err(format!(
+ "{PREDECESSOR_MANIFEST_RELATIVE} does not match the immutable predecessor identity"
+ ));
+ }
+ validate_predecessor_public_api(&predecessor_bytes)?;
+
+ let vector_bytes = read_regular_file(workspace_root, RESULT_VECTOR_CANONICAL_RELATIVE)?;
+ let vector: SourceMaintenanceVector = serde_json::from_slice(&vector_bytes)
+ .map_err(|error| format!("parse {RESULT_VECTOR_CANONICAL_RELATIVE}: {error}"))?;
+ validate_canonical_json(RESULT_VECTOR_CANONICAL_RELATIVE, &vector_bytes, &vector)?;
+ validate_result_vector(workspace_root, &vector)?;
+
+ let migration_source = read_regular_file(workspace_root, MIGRATIONS_SOURCE_RELATIVE)?;
+ let catalog = catalog_from_migration_source(&migration_source)?;
+ validate_catalog(&catalog)?;
+ let executor = descriptor_for_file(workspace_root, RESULT_VECTOR_EXECUTOR_RELATIVE)?;
+ let migration_up = descriptor_for_file(workspace_root, MIGRATION_UP_RELATIVE)?;
+ let migration_down = descriptor_for_file(workspace_root, MIGRATION_DOWN_RELATIVE)?;
+ validate_migration_identity(&migration_up, &migration_down)?;
+
+ let source_files = SOURCE_SPECS
+ .iter()
+ .map(|spec| {
+ let bytes = if spec.path == MIGRATIONS_SOURCE_RELATIVE {
+ migration_source.clone()
+ } else {
+ read_regular_file(workspace_root, spec.path)?
+ };
+ Ok(SourceFileDescriptor {
+ role: spec.role.to_owned(),
+ path: spec.path.to_owned(),
+ byte_length: byte_length(spec.path, &bytes)?,
+ sha256: sha256_hex(&bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ })
+ })
+ .collect::<Result<Vec<_>, String>>()?;
+
+ Ok(SourceMaintenanceManifest {
+ schema_version: SCHEMA_VERSION,
+ contract_id: CONTRACT_ID.to_owned(),
+ hook_id: HOOK_ID.to_owned(),
+ manifest_schema: descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, schema_bytes)?,
+ predecessor: PredecessorDescriptor {
+ hook_id: PREDECESSOR_HOOK_ID.to_owned(),
+ manifest: descriptor_for_bytes(PREDECESSOR_MANIFEST_RELATIVE, &predecessor_bytes)?,
+ },
+ migration: MigrationDescriptor {
+ version: MIGRATION_VERSION,
+ name: MIGRATION_NAME.to_owned(),
+ up: migration_up,
+ down: migration_down,
+ schema_sha256: SCHEMA_SHA256.to_owned(),
+ catalog,
+ },
+ source_maintenance: SourceMaintenanceDescriptor {
+ version: CAPACITY_VERSION,
+ event_contract_registry_version: EVENT_CONTRACT_REGISTRY_VERSION,
+ capacity_authority_id: CAPACITY_AUTHORITY_ID.to_owned(),
+ accounting: AccountingDescriptor {
+ algorithm: ACCOUNTING_ALGORITHM.to_owned(),
+ raw_event_columns: owned(RAW_EVENT_COLUMNS),
+ raw_tag_columns: owned(RAW_TAG_COLUMNS),
+ nullable_raw_tag_columns: owned(NULLABLE_RAW_TAG_COLUMNS),
+ },
+ limits: expected_limits(),
+ reopen_validation: ReopenValidationDescriptor {
+ mode: REOPEN_VALIDATION_MODE.to_owned(),
+ raw_event_rejection_scan_bound: RAW_EVENT_REJECTION_SCAN_BOUND,
+ raw_tag_rejection_scan_bound: RAW_TAG_REJECTION_SCAN_BOUND,
+ generation_history_validation: GENERATION_HISTORY_VALIDATION.to_owned(),
+ retained_generation_rejection_scan_bound: RETAINED_GENERATION_REJECTION_SCAN_BOUND,
+ },
+ rebuild_seal: RebuildSealDescriptor {
+ nip09_hook_id: NIP09_HOOK_ID.to_owned(),
+ nip09_manifest_sha256: NIP09_MANIFEST_SHA256.to_owned(),
+ food_hook_id: PREDECESSOR_HOOK_ID.to_owned(),
+ food_manifest_sha256: PREDECESSOR_MANIFEST_SHA256.to_owned(),
+ food_scope_fingerprint_sha256: FOOD_SCOPE_FINGERPRINT_SHA256.to_owned(),
+ active_generation_authority: ACTIVE_GENERATION_AUTHORITY.to_owned(),
+ marker_close_authority: MARKER_CLOSE_AUTHORITY.to_owned(),
+ },
+ },
+ entry_points: ENTRY_POINTS
+ .iter()
+ .map(|(role, rust_path)| EntryPointDescriptor {
+ role: (*role).to_owned(),
+ rust_path: (*rust_path).to_owned(),
+ })
+ .collect(),
+ source_files,
+ public_api: expected_public_api(),
+ result_vector: ResultVectorDescriptor {
+ canonical_path: RESULT_VECTOR_CANONICAL_RELATIVE.to_owned(),
+ mirror_path: RESULT_VECTOR_MIRROR_RELATIVE.to_owned(),
+ byte_length: byte_length(RESULT_VECTOR_CANONICAL_RELATIVE, &vector_bytes)?,
+ sha256: sha256_hex(&vector_bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ executor_id: RESULT_VECTOR_EXECUTOR_ID.to_owned(),
+ executor_path: RESULT_VECTOR_EXECUTOR_RELATIVE.to_owned(),
+ executor_test: RESULT_VECTOR_EXECUTOR_TEST.to_owned(),
+ executor_byte_length: executor.byte_length,
+ executor_sha256: executor.sha256,
+ executor_hash_algorithm: HASH_ALGORITHM.to_owned(),
+ },
+ })
+}
+
+fn expected_limits() -> LimitDescriptor {
+ LimitDescriptor {
+ raw_events: RAW_EVENT_COUNT_LIMIT,
+ raw_tags: RAW_TAG_COUNT_LIMIT,
+ raw_event_text_bytes: RAW_EVENT_TEXT_BYTES_LIMIT,
+ raw_tag_text_bytes: RAW_TAG_TEXT_BYTES_LIMIT,
+ retained_source_generations: RETAINED_SOURCE_GENERATION_LIMIT,
+ }
+}
+
+fn expected_public_api() -> PublicApiDescriptor {
+ PublicApiDescriptor {
+ inherited_predecessor_symbols: owned(INHERITED_PUBLIC_API),
+ added_symbols: owned(ADDED_PUBLIC_API),
+ methods: owned(PUBLIC_METHODS),
+ error_variants: owned(ERROR_VARIANTS),
+ removed_symbols: owned(REMOVED_PUBLIC_API),
+ breaking_replacements: BREAKING_PUBLIC_API_REPLACEMENTS
+ .iter()
+ .map(|(removed, replacement)| PublicApiReplacementDescriptor {
+ removed: (*removed).to_owned(),
+ replacement: (*replacement).to_owned(),
+ })
+ .collect(),
+ }
+}
+
+fn owned(values: &[&str]) -> Vec<String> {
+ values.iter().map(|value| (*value).to_owned()).collect()
+}
+
+fn validate_predecessor_production_source_coverage(workspace_root: &Path) -> Result<(), String> {
+ let source_paths = SOURCE_SPECS
+ .iter()
+ .map(|source| source.path)
+ .collect::<Vec<_>>();
+ let unique_source_paths = source_paths.iter().copied().collect::<BTreeSet<_>>();
+ if unique_source_paths.len() != source_paths.len() {
+ return Err("SourceMaintenance SOURCE_SPECS paths must be unique".to_owned());
+ }
+ for path in PREDECESSOR_SUPERSEDED_SOURCE_PATHS {
+ let count = source_paths
+ .iter()
+ .filter(|candidate| **candidate == *path)
+ .count();
+ if count != 1 {
+ return Err(format!(
+ "SourceMaintenance successor must current-byte-bind superseded predecessor path `{path}` exactly once; found {count}"
+ ));
+ }
+ }
+ let superseded = PREDECESSOR_SUPERSEDED_SOURCE_PATHS
+ .iter()
+ .copied()
+ .collect::<BTreeSet<_>>();
+ if superseded.len() != PREDECESSOR_SUPERSEDED_SOURCE_PATHS.len() {
+ return Err("SourceMaintenance predecessor supersession paths must be unique".to_owned());
+ }
+ validate_food_availability_projection_predecessor_production_sources_under_lock(
+ workspace_root,
+ PREDECESSOR_SUPERSEDED_SOURCE_PATHS,
+ )
+}
+
+fn validate_predecessor_public_api(predecessor_bytes: &[u8]) -> Result<(), String> {
+ let predecessor: Value = serde_json::from_slice(predecessor_bytes)
+ .map_err(|error| format!("parse {PREDECESSOR_MANIFEST_RELATIVE}: {error}"))?;
+ let actual = predecessor
+ .pointer("/public_api")
+ .and_then(Value::as_array)
+ .ok_or_else(|| format!("{PREDECESSOR_MANIFEST_RELATIVE} has no public_api array"))?
+ .iter()
+ .map(|value| {
+ value.as_str().map(str::to_owned).ok_or_else(|| {
+ format!("{PREDECESSOR_MANIFEST_RELATIVE} public_api values must be strings")
+ })
+ })
+ .collect::<Result<Vec<_>, String>>()?;
+ if actual != owned(INHERITED_PUBLIC_API) {
+ return Err(
+ "SourceMaintenance inherited public API must exactly equal the immutable FoodAvailability public API"
+ .to_owned(),
+ );
+ }
+ Ok(())
+}
+
+fn descriptor_for_file(workspace_root: &Path, relative: &str) -> Result<FileDescriptor, String> {
+ descriptor_for_bytes(relative, &read_regular_file(workspace_root, relative)?)
+}
+
+fn descriptor_for_bytes(relative: &str, bytes: &[u8]) -> Result<FileDescriptor, String> {
+ Ok(FileDescriptor {
+ path: relative.to_owned(),
+ byte_length: byte_length(relative, bytes)?,
+ sha256: sha256_hex(bytes),
+ hash_algorithm: HASH_ALGORITHM.to_owned(),
+ })
+}
+
+fn byte_length(relative: &str, bytes: &[u8]) -> Result<u64, String> {
+ u64::try_from(bytes.len()).map_err(|_| format!("{relative} byte length does not fit u64"))
+}
+
+fn catalog_from_migration_source(bytes: &[u8]) -> Result<CatalogDescriptor, String> {
+ let source = std::str::from_utf8(bytes)
+ .map_err(|error| format!("{MIGRATIONS_SOURCE_RELATIVE} must be UTF-8: {error}"))?;
+ let syntax = syn::parse_file(source)
+ .map_err(|error| format!("parse {MIGRATIONS_SOURCE_RELATIVE}: {error}"))?;
+ Ok(CatalogDescriptor {
+ objects: extract_string_array_const(
+ &syntax,
+ "EVENT_STORE_SOURCE_MAINTENANCE_OBJECT_NAMES",
+ )?,
+ replaced_objects: extract_string_array_const(
+ &syntax,
+ "EVENT_STORE_SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES",
+ )?,
+ tables: extract_string_array_const(&syntax, "EVENT_STORE_SOURCE_MAINTENANCE_TABLE_NAMES")?,
+ fts5_tables: Vec::new(),
+ })
+}
+
+fn extract_string_array_const(syntax: &syn::File, name: &str) -> Result<Vec<String>, String> {
+ let expression = syntax
+ .items
+ .iter()
+ .find_map(|item| match item {
+ Item::Const(item) if item.ident == name => Some(item.expr.as_ref()),
+ _ => None,
+ })
+ .ok_or_else(|| format!("{MIGRATIONS_SOURCE_RELATIVE} must define `{name}`"))?;
+ let Expr::Array(array) = strip_expression_wrappers(expression) else {
+ return Err(format!(
+ "{MIGRATIONS_SOURCE_RELATIVE} `{name}` must be a literal array"
+ ));
+ };
+ array
+ .elems
+ .iter()
+ .map(|element| match strip_expression_wrappers(element) {
+ Expr::Lit(syn::ExprLit {
+ lit: syn::Lit::Str(value),
+ ..
+ }) => Ok(value.value()),
+ _ => Err(format!(
+ "{MIGRATIONS_SOURCE_RELATIVE} `{name}` values must be string literals"
+ )),
+ })
+ .collect()
+}
+
+fn strip_expression_wrappers(mut expression: &Expr) -> &Expr {
+ loop {
+ match expression {
+ Expr::Reference(reference) => expression = &reference.expr,
+ Expr::Group(group) => expression = &group.expr,
+ Expr::Paren(paren) => expression = &paren.expr,
+ _ => return expression,
+ }
+ }
+}
+
+fn validate_catalog(catalog: &CatalogDescriptor) -> Result<(), String> {
+ if catalog.objects != owned(EXPECTED_CATALOG_OBJECTS)
+ || catalog.replaced_objects != owned(EXPECTED_REPLACED_CATALOG_OBJECTS)
+ || catalog.tables != owned(EXPECTED_CATALOG_TABLES)
+ || !catalog.fts5_tables.is_empty()
+ {
+ return Err(format!(
+ "SourceMaintenance migration catalog differs: expected objects {:?}, replacements {:?}, tables {:?}, no FTS5; found {catalog:?}",
+ EXPECTED_CATALOG_OBJECTS, EXPECTED_REPLACED_CATALOG_OBJECTS, EXPECTED_CATALOG_TABLES,
+ ));
+ }
+ validate_unique(
+ "SourceMaintenance catalog objects",
+ catalog.objects.iter().map(String::as_str),
+ )?;
+ validate_unique(
+ "SourceMaintenance replaced catalog objects",
+ catalog.replaced_objects.iter().map(String::as_str),
+ )?;
+ validate_unique(
+ "SourceMaintenance catalog tables",
+ catalog.tables.iter().map(String::as_str),
+ )?;
+ if catalog
+ .replaced_objects
+ .iter()
+ .any(|name| catalog.objects.contains(name) || catalog.tables.contains(name))
+ {
+ return Err(
+ "SourceMaintenance replaced catalog objects must be disjoint from newly owned objects and tables"
+ .to_owned(),
+ );
+ }
+ Ok(())
+}
+
+fn validate_migration_identity(up: &FileDescriptor, down: &FileDescriptor) -> Result<(), String> {
+ const UP_BYTE_LENGTH: u64 = 19_841;
+ const UP_SHA256: &str = "425dc799f392b87f265a6fb81f89c4a1c7a5db8391ab0380970708cb9c66704d";
+ const DOWN_BYTE_LENGTH: u64 = 5_172;
+ const DOWN_SHA256: &str = "fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860";
+ if up.path != MIGRATION_UP_RELATIVE
+ || up.byte_length != UP_BYTE_LENGTH
+ || up.sha256 != UP_SHA256
+ || down.path != MIGRATION_DOWN_RELATIVE
+ || down.byte_length != DOWN_BYTE_LENGTH
+ || down.sha256 != DOWN_SHA256
+ {
+ return Err(
+ "SourceMaintenance migration bytes do not match the reviewed v4 identity".to_owned(),
+ );
+ }
+ Ok(())
+}
+
+pub(super) fn validate_source_contract(workspace_root: &Path) -> Result<(), String> {
+ validate_source_inventory()?;
+ let migration_up = descriptor_for_file(workspace_root, MIGRATION_UP_RELATIVE)?;
+ let migration_down = descriptor_for_file(workspace_root, MIGRATION_DOWN_RELATIVE)?;
+ validate_migration_identity(&migration_up, &migration_down)?;
+ validate_public_api_authority(workspace_root)?;
+ validate_error_and_limit_authority(workspace_root)?;
+ validate_migration_registry_authority(workspace_root)?;
+ validate_capacity_runtime_authority(workspace_root)?;
+ validate_ingest_capacity_authority(workspace_root)?;
+ validate_schema_capacity_authority(workspace_root)?;
+ validate_generation_rebuild_authority(workspace_root)?;
+ validate_sql_capacity_authority(workspace_root)?;
+ validate_contract_command_reachability_authority(workspace_root)?;
+ validate_current_event_store_successor_authority(workspace_root)
+}
+
+fn validate_contract_command_reachability_authority(workspace_root: &Path) -> Result<(), String> {
+ let contract = rust_source(workspace_root, CONTRACT_COMMAND_SOURCE_RELATIVE)?;
+ let main = rust_source(workspace_root, XTASK_MAIN_SOURCE_RELATIVE)?;
+ validate_contract_command_reachability_sources(&contract, &main)
+}
+
+fn validate_contract_command_reachability_sources(
+ contract_source: &str,
+ main_source: &str,
+) -> Result<(), String> {
+ let contract = syn::parse_file(contract_source)
+ .map_err(|error| format!("parse {CONTRACT_COMMAND_SOURCE_RELATIVE}: {error}"))?;
+ let main = syn::parse_file(main_source)
+ .map_err(|error| format!("parse {XTASK_MAIN_SOURCE_RELATIVE}: {error}"))?;
+ let main_ast_sha256 = sha256_hex(compact_tokens(&main).as_bytes());
+ if main_ast_sha256 != XTASK_MAIN_FULL_AST_SHA256 {
+ return Err(format!(
+ "{XTASK_MAIN_SOURCE_RELATIVE} full dispatch AST authority drifted: expected {XTASK_MAIN_FULL_AST_SHA256}, found {main_ast_sha256}"
+ ));
+ }
+ for (relative, file, name, expected) in [
+ (
+ CONTRACT_COMMAND_SOURCE_RELATIVE,
+ &contract,
+ "validate_artifact_contracts",
+ r#"pub(crate) fn validate_artifact_contracts(
+ workspace_root: &Path
+ ) -> Result<(), String> {
+ validate_event_contract_registry_v7_inventory(workspace_root)?;
+ validate_nip09_reconciliation_manifest(workspace_root)?;
+ validate_food_availability_projection_manifest(workspace_root)?;
+ validate_source_maintenance_manifest(workspace_root)?;
+ validate_knowledge_contract_manifest(workspace_root)
+ }"#,
+ ),
+ (
+ XTASK_MAIN_SOURCE_RELATIVE,
+ &main,
+ "validate_contract",
+ r#"fn validate_contract() -> Result<(), String> {
+ radroots_protocol_contract_v1::validate_protocol_contract_v1()
+ .map_err(|error| error.to_string())?;
+ let root = workspace_root();
+ dto_roots::check(&root)?;
+ contract::load_contract_bundle(&root)
+ .and_then(|bundle| contract::validate_contract_bundle(&bundle))
+ .and_then(|_| contract::validate_canonical_event_boundary(&root))
+ .and_then(|_| contract::validate_artifact_contracts(&root))
+ }"#,
+ ),
+ (
+ XTASK_MAIN_SOURCE_RELATIVE,
+ &main,
+ "release_preflight_at",
+ r#"fn release_preflight_at(root: &Path) -> Result<(), String> {
+ dto_roots::check(root)?;
+ contract::validate_artifact_contracts(root)?;
+ contract::validate_release_preflight(root)
+ }"#,
+ ),
+ ] {
+ let actual = compact_tokens(exact_top_level_function(file, name)?);
+ let expected_file = syn::parse_file(expected)
+ .map_err(|error| format!("parse authoritative `{name}` function: {error}"))?;
+ let expected = compact_tokens(exact_top_level_function(&expected_file, name)?);
+ if actual != expected {
+ return Err(format!(
+ "{relative} `{name}` SourceMaintenance validation call-path authority drifted: expected `{expected}`, found `{actual}`"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_source_inventory() -> Result<(), String> {
+ validate_unique(
+ "SourceMaintenance source roles",
+ SOURCE_SPECS.iter().map(|spec| spec.role),
+ )?;
+ validate_unique(
+ "SourceMaintenance source paths",
+ SOURCE_SPECS.iter().map(|spec| spec.path),
+ )?;
+ let source_paths = SOURCE_SPECS
+ .iter()
+ .map(|spec| spec.path)
+ .collect::<BTreeSet<_>>();
+ for path in GENERATED_ARTIFACT_PATHS {
+ if source_paths.contains(path) {
+ return Err(format!(
+ "SourceMaintenance generated artifact `{path}` must not participate in its own source hash graph"
+ ));
+ }
+ }
+ if source_paths.contains(RESULT_VECTOR_MIRROR_RELATIVE)
+ || source_paths.contains(MANIFEST_RELATIVE)
+ || source_paths.contains(MANIFEST_SCHEMA_RELATIVE)
+ {
+ return Err("SourceMaintenance source inventory contains a self-hashed output".to_owned());
+ }
+ Ok(())
+}
+
+fn validate_error_and_limit_authority(workspace_root: &Path) -> Result<(), String> {
+ let source = rust_source(workspace_root, "crates/event_store/src/error.rs")?;
+ validate_error_and_limit_source(&source)
+}
+
+fn validate_error_and_limit_source(source: &str) -> Result<(), String> {
+ let syntax = syn::parse_file(source)
+ .map_err(|error| format!("parse crates/event_store/src/error.rs: {error}"))?;
+ let public_items = top_level_public_item_names(&syntax);
+ for symbol in &ADDED_PUBLIC_API[..6] {
+ if !public_items.contains(*symbol) {
+ return Err(format!(
+ "crates/event_store/src/error.rs must publicly define `{symbol}`"
+ ));
+ }
+ }
+ validate_source_capacity_resource_authority(&syntax)?;
+ validate_source_capacity_limit_authority(&syntax)?;
+ let error_enum = exact_top_level_enum(&syntax, "RadrootsEventStoreError")?;
+ if error_enum
+ .attrs
+ .iter()
+ .any(|attribute| attribute.path().is_ident("cfg") || attribute.path().is_ident("cfg_attr"))
+ {
+ return Err(
+ "top-level enum `RadrootsEventStoreError` must not have conditional attributes"
+ .to_owned(),
+ );
+ }
+ let variants = error_enum
+ .variants
+ .iter()
+ .map(|variant| variant.ident.to_string())
+ .collect::<BTreeSet<_>>();
+ for variant in ERROR_VARIANTS {
+ if !variants.contains(*variant) {
+ return Err(format!(
+ "RadrootsEventStoreError must define SourceMaintenance variant `{variant}`"
+ ));
+ }
+ }
+ if variants.contains("ReconciliationCapacityExceeded") {
+ return Err(
+ "removed error variant `RadrootsEventStoreError::ReconciliationCapacityExceeded` must remain absent"
+ .to_owned(),
+ );
+ }
+ for (name, expected) in [
+ (
+ "SourceCapacityExceeded",
+ r#"#[error(
+ "event-store retained source {resource} capacity exceeded: current {current}, requested additional {requested}, limit {limit}; durable append refused, retain a bounded source set in a new disposable cache"
+ )]
+ SourceCapacityExceeded {
+ resource: RadrootsEventStoreSourceCapacityResourceV1,
+ current: u64,
+ requested: u64,
+ limit: u64,
+ }"#,
+ ),
+ (
+ "SourceGenerationHistoryLimitReached",
+ r#"#[error(
+ "event-store retained source generation limit reached: current {current}, limit {limit}; replace and resync into a fresh store"
+ )]
+ SourceGenerationHistoryLimitReached { current: u32, limit: u32 }"#,
+ ),
+ (
+ "PersistedEphemeralRawEvent",
+ r#"#[error(
+ "event-store retained source contains ephemeral event `{event_id}` of kind {kind}; ephemeral events must be discarded"
+ )]
+ PersistedEphemeralRawEvent { event_id: String, kind: i64 }"#,
+ ),
+ (
+ "SourceCapacityStateDrift",
+ r#"#[error("event-store retained source capacity authority is inconsistent: {reason}")]
+ SourceCapacityStateDrift { reason: String }"#,
+ ),
+ (
+ "SqliteMainDatabaseEncodingNotUtf8",
+ r#"#[error(
+ "event-store SQLite main database must use UTF-8 encoding; reported `{actual}`"
+ )]
+ SqliteMainDatabaseEncodingNotUtf8 { actual: String }"#,
+ ),
+ (
+ "RollbackWouldDiscardSourceGenerationHistory",
+ r#"#[error(
+ "event-store rollback from version {current} to {target} would discard retained source-generation history; minimum retained-history schema version is {floor}"
+ )]
+ RollbackWouldDiscardSourceGenerationHistory {
+ current: u32,
+ target: u32,
+ floor: u32,
+ }"#,
+ ),
+ ] {
+ let actual = error_enum
+ .variants
+ .iter()
+ .find(|variant| variant.ident == name)
+ .ok_or_else(|| format!("RadrootsEventStoreError must define `{name}`"))?;
+ let mut actual = actual.clone();
+ actual
+ .attrs
+ .retain(|attribute| !attribute.path().is_ident("doc"));
+ let expected = syn::parse_str::<syn::ItemEnum>(&format!("enum Expected {{ {expected} }}"))
+ .map_err(|error| format!("parse authoritative `{name}` variant: {error}"))?;
+ let expected = expected
+ .variants
+ .first()
+ .expect("authoritative error enum contains one variant");
+ if compact_tokens(&actual) != compact_tokens(expected) {
+ return Err(format!(
+ "RadrootsEventStoreError::{name} typed fields or display contract drifted"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_source_capacity_limit_authority(file: &syn::File) -> Result<(), String> {
+ for (name, expected) in [
+ (
+ "RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1",
+ "pub const RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1: u64 = 25_000;",
+ ),
+ (
+ "RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1",
+ "pub const RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1: u64 = 250_000;",
+ ),
+ (
+ "RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1",
+ "pub const RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1: u64 = 64 * 1024 * 1024;",
+ ),
+ (
+ "RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1",
+ "pub const RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1: u64 = 32 * 1024 * 1024;",
+ ),
+ (
+ "RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1",
+ "pub const RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1: u32 = 8;",
+ ),
+ ] {
+ let matches = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Const(item) if item.ident == name => Some(item),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [actual] = matches.as_slice() else {
+ return Err(format!(
+ "event-store capacity limit authority must define top-level `{name}` exactly once; found {}",
+ matches.len()
+ ));
+ };
+ let mut actual = (*actual).clone();
+ actual
+ .attrs
+ .retain(|attribute| !attribute.path().is_ident("doc"));
+ let expected = syn::parse_str::<syn::ItemConst>(expected)
+ .map_err(|error| format!("parse authoritative capacity limit `{name}`: {error}"))?;
+ if compact_tokens(&actual) != compact_tokens(&expected) {
+ return Err(format!(
+ "event-store capacity limit authority `{name}` visibility, attributes, type, or value drifted"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_source_capacity_resource_authority(file: &syn::File) -> Result<(), String> {
+ let resources = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Enum(item) if item.ident == "RadrootsEventStoreSourceCapacityResourceV1" => {
+ Some(item)
+ }
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [resource] = resources.as_slice() else {
+ return Err(format!(
+ "crates/event_store/src/error.rs must define `RadrootsEventStoreSourceCapacityResourceV1` exactly once; found {}",
+ resources.len()
+ ));
+ };
+ let mut resource = (*resource).clone();
+ resource
+ .attrs
+ .retain(|attribute| !attribute.path().is_ident("doc"));
+ for variant in &mut resource.variants {
+ variant
+ .attrs
+ .retain(|attribute| !attribute.path().is_ident("doc"));
+ }
+ let expected = syn::parse_str::<syn::ItemEnum>(
+ r#"#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+ #[non_exhaustive]
+ pub enum RadrootsEventStoreSourceCapacityResourceV1 {
+ RawEvents,
+ RawTags,
+ RawEventBytes,
+ RawTagBytes,
+ }"#,
+ )
+ .map_err(|error| format!("parse source-capacity resource authority: {error}"))?;
+ if compact_tokens(&resource) != compact_tokens(&expected) {
+ return Err(
+ "RadrootsEventStoreSourceCapacityResourceV1 variants, visibility, or attributes drifted"
+ .to_owned(),
+ );
+ }
+
+ let inherent = exact_top_level_impl(file, None, "RadrootsEventStoreSourceCapacityResourceV1")?;
+ let mut inherent = inherent.clone();
+ strip_doc_attributes_from_impl(&mut inherent);
+ let expected = syn::parse_str::<syn::ItemImpl>(
+ r#"impl RadrootsEventStoreSourceCapacityResourceV1 {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::RawEvents => "raw event count",
+ Self::RawTags => "raw tag count",
+ Self::RawEventBytes => "total retained raw-source event row text bytes",
+ Self::RawTagBytes => "total retained raw-source tag row text bytes",
+ }
+ }
+ }"#,
+ )
+ .map_err(|error| format!("parse source-capacity label authority: {error}"))?;
+ if compact_tokens(&inherent) != compact_tokens(&expected) {
+ return Err(
+ "RadrootsEventStoreSourceCapacityResourceV1::as_str label authority drifted".to_owned(),
+ );
+ }
+
+ let display = exact_top_level_impl(
+ file,
+ Some("core::fmt::Display"),
+ "RadrootsEventStoreSourceCapacityResourceV1",
+ )?;
+ let mut display = display.clone();
+ strip_doc_attributes_from_impl(&mut display);
+ let expected = syn::parse_str::<syn::ItemImpl>(
+ r#"impl core::fmt::Display for RadrootsEventStoreSourceCapacityResourceV1 {
+ fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ formatter.write_str(self.as_str())
+ }
+ }"#,
+ )
+ .map_err(|error| format!("parse source-capacity Display authority: {error}"))?;
+ if compact_tokens(&display) != compact_tokens(&expected) {
+ return Err(
+ "RadrootsEventStoreSourceCapacityResourceV1 Display authority drifted".to_owned(),
+ );
+ }
+ Ok(())
+}
+
+fn validate_source_capacity_snapshot_authority(file: &syn::File) -> Result<(), String> {
+ let snapshots = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Struct(item) if item.ident == "RadrootsEventStoreSourceCapacityV1" => Some(item),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [snapshot] = snapshots.as_slice() else {
+ return Err(format!(
+ "crates/event_store/src/source_maintenance_v1.rs must define `RadrootsEventStoreSourceCapacityV1` exactly once; found {}",
+ snapshots.len()
+ ));
+ };
+ let mut snapshot = (*snapshot).clone();
+ snapshot
+ .attrs
+ .retain(|attribute| !attribute.path().is_ident("doc"));
+ for field in &mut snapshot.fields {
+ field
+ .attrs
+ .retain(|attribute| !attribute.path().is_ident("doc"));
+ }
+ let expected = syn::parse_str::<syn::ItemStruct>(
+ r#"#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+ pub struct RadrootsEventStoreSourceCapacityV1 {
+ source_generation: RadrootsEventStoreSourceGeneration,
+ capacity: ReconciliationCapacity,
+ raw_high_water_seq: i64,
+ retained_generation_count: u32,
+ retained_generation_limit: u32,
+ }"#,
+ )
+ .map_err(|error| format!("parse source-capacity snapshot authority: {error}"))?;
+ if compact_tokens(&snapshot) != compact_tokens(&expected) {
+ return Err(
+ "RadrootsEventStoreSourceCapacityV1 derives, visibility, or private field authority drifted"
+ .to_owned(),
+ );
+ }
+
+ let inherent = exact_top_level_impl(file, None, "RadrootsEventStoreSourceCapacityV1")?;
+ let mut inherent = inherent.clone();
+ strip_doc_attributes_from_impl(&mut inherent);
+ let expected = syn::parse_str::<syn::ItemImpl>(
+ r#"impl RadrootsEventStoreSourceCapacityV1 {
+ pub const fn source_generation(&self) -> RadrootsEventStoreSourceGeneration {
+ self.source_generation
+ }
+
+ pub const fn raw_event_count(&self) -> u64 {
+ self.capacity.raw_events
+ }
+
+ pub const fn raw_tag_count(&self) -> u64 {
+ self.capacity.raw_tags
+ }
+
+ pub const fn raw_event_text_bytes(&self) -> u64 {
+ self.capacity.raw_event_bytes
+ }
+
+ pub const fn raw_tag_text_bytes(&self) -> u64 {
+ self.capacity.raw_tag_bytes
+ }
+
+ pub const fn raw_high_water_seq(&self) -> i64 {
+ self.raw_high_water_seq
+ }
+
+ pub const fn retained_generation_count(&self) -> u32 {
+ self.retained_generation_count
+ }
+
+ pub const fn retained_generation_limit(&self) -> u32 {
+ self.retained_generation_limit
+ }
+ }"#,
+ )
+ .map_err(|error| format!("parse source-capacity snapshot accessor authority: {error}"))?;
+ if compact_tokens(&inherent) != compact_tokens(&expected) {
+ return Err(
+ "RadrootsEventStoreSourceCapacityV1 public accessor authority drifted".to_owned(),
+ );
+ }
+ Ok(())
+}
+
+fn exact_top_level_impl<'a>(
+ file: &'a syn::File,
+ trait_path: Option<&str>,
+ self_type: &str,
+) -> Result<&'a syn::ItemImpl, String> {
+ let matches = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Impl(item)
+ if compact_tokens(item.self_ty.as_ref()) == self_type
+ && item
+ .trait_
+ .as_ref()
+ .map(|(_, path, _)| compact_tokens(path))
+ .as_deref()
+ == trait_path =>
+ {
+ Some(item)
+ }
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [item] = matches.as_slice() else {
+ return Err(format!(
+ "governed Rust source must define impl `{}` for `{self_type}` exactly once; found {}",
+ trait_path.unwrap_or("inherent"),
+ matches.len()
+ ));
+ };
+ Ok(item)
+}
+
+fn strip_doc_attributes_from_impl(item: &mut syn::ItemImpl) {
+ item.attrs
+ .retain(|attribute| !attribute.path().is_ident("doc"));
+ for member in &mut item.items {
+ if let syn::ImplItem::Fn(function) = member {
+ function
+ .attrs
+ .retain(|attribute| !attribute.path().is_ident("doc"));
+ }
+ }
+}
+
+fn validate_migration_registry_authority(workspace_root: &Path) -> Result<(), String> {
+ let source = rust_source(workspace_root, MIGRATIONS_SOURCE_RELATIVE)?;
+ let compact = compact_rust(&source, MIGRATIONS_SOURCE_RELATIVE)?;
+ for marker in [
+ "pubconstRADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT:u32=4",
+ "SourceMaintenanceV1",
+ "version:4",
+ "name:\"source_maintenance\"",
+ "up_len:source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_BYTE_LENGTH",
+ "down_len:source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_BYTE_LENGTH",
+ "up_sha256:source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_UP_SHA256",
+ "down_sha256:source_maintenance_manifest::SOURCE_MAINTENANCE_MIGRATION_DOWN_SHA256",
+ "schema_sha256:source_maintenance_manifest::SOURCE_MAINTENANCE_SCHEMA_SHA256",
+ "replaced_object_names:EVENT_STORE_SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES",
+ "hook_manifest_sha256:Some(source_maintenance_manifest::SOURCE_MAINTENANCE_MANIFEST_SHA256)",
+ "event_contract_registry_version:Some(source_maintenance_manifest::SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION,)",
+ ] {
+ require_marker("SourceMaintenance migration registry", &compact, marker)?;
+ }
+ let catalog = catalog_from_migration_source(source.as_bytes())?;
+ validate_catalog(&catalog)
+}
+
+fn validate_capacity_runtime_authority(workspace_root: &Path) -> Result<(), String> {
+ let relative = "crates/event_store/src/source_maintenance_v1.rs";
+ let source = rust_source(workspace_root, relative)?;
+ let syntax = syn::parse_file(&source).map_err(|error| format!("parse {relative}: {error}"))?;
+ validate_source_capacity_snapshot_authority(&syntax)?;
+
+ let full = exact_free_function_tokens(&syntax, "validate_source_capacity_authority_full_v1")?;
+ require_ordered_markers(
+ "full SourceMaintenance reopen authority",
+ &full,
+ &[
+ "validate_source_capacity_authority_fast_v1(connection).await?",
+ "measure_reconciliation_capacity_bounded(connection,ReconciliationCapacityLimits::production(),).await?",
+ "validate_measured_capacity(measured)?",
+ "validate_no_persisted_ephemeral_raw_rows_v1(connection).await?",
+ "ifmeasured!=persisted.capacity",
+ ],
+ )?;
+
+ let fast = exact_free_function_tokens(&syntax, "validate_source_capacity_authority_fast_v1")?;
+ require_marker(
+ "bounded generation-history validation",
+ &fast,
+ "(SELECTCOUNT(*)FROM(SELECT1FROMradroots_event_store_source_generationLIMIT9))ASretained_generation_count",
+ )?;
+ require_marker(
+ "active generation ordinal validation",
+ &fast,
+ "generation.generation_ordinal",
+ )?;
+ if fast.contains("fetch_all") || fast.contains("Vec<") {
+ return Err(
+ "fast SourceMaintenance validation must not materialize generation history".to_owned(),
+ );
+ }
+
+ for function in [
+ "raw_source_capacity_delta_v1",
+ "preflight_unique_raw_source_append_v1",
+ "advance_source_capacity_after_insert_v1",
+ "apply_source_maintenance_hook_v1",
+ "validate_source_capacity_authority_fast_v1",
+ "validate_source_capacity_authority_full_v1",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+ "preflight_source_generation_append_v1",
+ "bind_source_capacity_to_generation_v1",
+ ] {
+ exact_free_function_tokens(&syntax, function)?;
+ }
+ Ok(())
+}
+
+fn validate_ingest_capacity_authority(workspace_root: &Path) -> Result<(), String> {
+ let relative = "crates/event_store/src/store/protocol_reconciliation_v1.rs";
+ let source = rust_source(workspace_root, relative)?;
+ let syntax = syn::parse_file(&source).map_err(|error| format!("parse {relative}: {error}"))?;
+ let ingest = exact_free_function_tokens(&syntax, "ingest_event_protocol_reconciliation_v1")?;
+ require_ordered_markers(
+ "SourceMaintenance ingest authority",
+ &ingest,
+ &[
+ "acquire_event_store_write_lock(tx).await?",
+ "validate_source_raw_authority(tx).await?",
+ "validate_source_capacity_authority_fast_v1(tx).await?",
+ "ifkind_class==RadrootsEventKindClass::Ephemeral",
+ "SELECTEXISTS(SELECT1FROMevent_envelopesWHEREevent_id=?)",
+ "raw_source_capacity_delta_v1(ingest,tags_json.as_str())?",
+ "preflight_unique_raw_source_append_v1(tx,delta).await?",
+ "insert_raw_event",
+ "synchronize_after_insert",
+ "advance_source_capacity_after_insert_v1(tx,capacity_delta,insert.seq).await?",
+ "read_protocol_post_extension_authority_seal(tx).await?",
+ ],
+ )
+}
+
+pub(super) fn validate_schema_capacity_authority(workspace_root: &Path) -> Result<(), String> {
+ let relative = "crates/event_store/src/schema.rs";
+ let source = rust_source(workspace_root, relative)?;
+ let syntax = syn::parse_file(&source).map_err(|error| format!("parse {relative}: {error}"))?;
+ let outer = exact_free_function_tokens(
+ &syntax,
+ "migrate_event_store_schema_with_registry_and_generation_provider",
+ )?;
+ require_ordered_markers(
+ "SourceMaintenance outer migration preflight",
+ &outer,
+ &[
+ "inspect_event_store_schema_status_with_registry",
+ "ifhas_pending_source_capacity_hook",
+ "validate_event_store_temp_schema_with_registry",
+ "validate_reconciliation_capacity",
+ "ifhas_pending_source_maintenance_hook",
+ "validate_no_persisted_ephemeral_raw_rows_v1",
+ "begin_with(\"BEGINIMMEDIATE\")",
+ ],
+ )?;
+ let inner = exact_free_function_tokens(&syntax, "migrate_schema_on_connection")?;
+ require_ordered_markers(
+ "SourceMaintenance in-transaction migration recheck",
+ &inner,
+ &[
+ "EventStoreMigrationHook::SourceMaintenanceV1",
+ "validate_reconciliation_capacity(connection,reconciliation_limits).await?",
+ "validate_no_persisted_ephemeral_raw_rows_v1(connection).await?",
+ "apply_migration_up(connection,registry,migration).await?",
+ "apply_migration_hook",
+ "validate_applied_migration_hooks",
+ "insert_ledger_row",
+ ],
+ )?;
+ let inspect = exact_free_function_tokens(&syntax, "inspect_schema_on_connection")?;
+ require_ordered_markers(
+ "managed-store reopen validation",
+ &inspect,
+ &[
+ "validate_history_against_registry",
+ "ifactual_schema_sha256!=expected.schema_sha256",
+ "validate_applied_migration_hooks(connection,registry,current).await?",
+ "RadrootsEventStoreSchemaStatus::Managed",
+ ],
+ )?;
+ let hook = exact_free_function_tokens(&syntax, "validate_migration_hook_state")?;
+ require_ordered_markers(
+ "SourceMaintenance hook validation dispatch",
+ &hook,
+ &[
+ "EventStoreMigrationHook::SourceMaintenanceV1",
+ "validate_source_capacity_authority_full_v1(connection).await",
+ ],
+ )
+}
+
+fn validate_generation_rebuild_authority(workspace_root: &Path) -> Result<(), String> {
+ let relative = "crates/event_store/src/nip09/reconciliation_v1.rs";
+ let source = rust_source(workspace_root, relative)?;
+ let syntax = syn::parse_file(&source).map_err(|error| format!("parse {relative}: {error}"))?;
+ let rebuild = exact_free_function_tokens(&syntax, "apply_reconciliation_hook")?;
+ require_ordered_markers(
+ "SourceMaintenance source-generation rebuild authority",
+ &rebuild,
+ &[
+ "preflight_source_generation_append_v1(connection).await?",
+ "open_source_rebuild_marker",
+ "append_source_generation",
+ "bind_source_capacity_to_generation_v1",
+ "apply_food_availability_projection_hook_v1",
+ "close_source_rebuild_marker",
+ "validate_sqlite_integrity_after_rebuild",
+ "validate_active_hook_state_fast",
+ ],
+ )?;
+
+ let measure = exact_free_function_tokens(&syntax, "measure_reconciliation_capacity_bounded")?;
+ require_ordered_markers(
+ "SourceMaintenance bounded raw-source recount",
+ &measure,
+ &[
+ "bounded_capacity_page_len(capacity.raw_events,limits.raw_events)",
+ ".bind(page_size)",
+ "ifrow_count<page_len",
+ "bounded_capacity_page_len(capacity.raw_tags,limits.raw_tags)",
+ ".bind(page_size)",
+ "ifrow_count<page_len",
+ ],
+ )?;
+ let page_len = exact_free_function_tokens(&syntax, "bounded_capacity_page_len")?;
+ require_ordered_markers(
+ "SourceMaintenance rejection-probe page bound",
+ &page_len,
+ &[
+ "limit.saturating_sub(current)",
+ ".saturating_add(1)",
+ ".min(RECONCILIATION_SNAPSHOT_BATCH_COUNT)",
+ "i64::try_from(page_count).unwrap_or(RECONCILIATION_SNAPSHOT_BATCH_SIZE)",
+ "usize::try_from(page_count).unwrap_or(RECONCILIATION_SNAPSHOT_BATCH_LEN)",
+ ],
+ )
+}
+
+fn validate_sql_capacity_authority(workspace_root: &Path) -> Result<(), String> {
+ let sql = read_regular_file(workspace_root, MIGRATION_UP_RELATIVE)?;
+ let sql = std::str::from_utf8(&sql)
+ .map_err(|error| format!("{MIGRATION_UP_RELATIVE} must be UTF-8 SQL: {error}"))?;
+ for marker in [
+ "DROP TRIGGER radroots_event_store_source_rebuild_marker_insert_guard",
+ "CREATE TRIGGER radroots_event_store_source_rebuild_marker_insert_guard",
+ "DROP TRIGGER radroots_event_store_food_availability_projection_delete_guard",
+ "CREATE TRIGGER radroots_event_store_food_availability_projection_delete_guard",
+ "DROP TRIGGER radroots_event_store_food_availability_image_delete_guard",
+ "CREATE TRIGGER radroots_event_store_food_availability_image_delete_guard",
+ "source.active_generation = marker.target_generation",
+ "OLD.source_generation != source.active_generation",
+ "raw_event_count >= 0 AND raw_event_count <= 25000",
+ "raw_tag_count >= 0 AND raw_tag_count <= 250000",
+ "raw_event_bytes >= 0 AND raw_event_bytes <= 67108864",
+ "raw_tag_bytes >= 0 AND raw_tag_bytes <= 33554432",
+ "retained_generation_count >= 1 AND retained_generation_count <= 8",
+ "retained_generation_limit = 8",
+ "CREATE TRIGGER radroots_event_store_source_generation_capacity_guard",
+ "retained_generation_count >= retained_generation_limit",
+ "CREATE TRIGGER radroots_event_store_source_generation_capacity_advance",
+ "CREATE TRIGGER radroots_event_store_source_capacity_marker_close_guard",
+ "33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23",
+ "8B63C5DDC48A2CC7DB69295238B96D5F814DBA50427C80B4D0079F061E6D3DE0",
+ "capacity.retained_generation_count = (\n SELECT COUNT(*)\n FROM (\n SELECT 1\n FROM radroots_event_store_source_generation\n LIMIT 9\n )\n )",
+ "generation.generation_ordinal = capacity.retained_generation_count",
+ ] {
+ require_marker("SourceMaintenance SQL capacity authority", sql, marker)?;
+ }
+ if sql.contains("AND NEW.transition_floor_seq = state.last_transition_seq") {
+ return Err(
+ "SourceMaintenance v4 marker replacement must derive the transition floor from retained transitions rather than stale source-state high-water"
+ .to_owned(),
+ );
+ }
+
+ let down = read_regular_file(workspace_root, MIGRATION_DOWN_RELATIVE)?;
+ let down = std::str::from_utf8(&down)
+ .map_err(|error| format!("{MIGRATION_DOWN_RELATIVE} must be UTF-8 SQL: {error}"))?;
+ for marker in [
+ "DROP TRIGGER radroots_event_store_food_availability_image_delete_guard",
+ "CREATE TRIGGER radroots_event_store_food_availability_image_delete_guard",
+ "DROP TRIGGER radroots_event_store_food_availability_projection_delete_guard",
+ "CREATE TRIGGER radroots_event_store_food_availability_projection_delete_guard",
+ "DROP TRIGGER radroots_event_store_source_rebuild_marker_insert_guard",
+ "CREATE TRIGGER radroots_event_store_source_rebuild_marker_insert_guard",
+ "AND NEW.transition_floor_seq = state.last_transition_seq",
+ "event-store FoodAvailability image delete is not backed by a pending retraction",
+ "event-store FoodAvailability projection delete is not backed by a pending retraction",
+ ] {
+ require_marker(
+ "SourceMaintenance exact v3 SQL restoration authority",
+ down,
+ marker,
+ )?;
+ }
+ Ok(())
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+struct PublicUseRoute {
+ segments: Vec<String>,
+ exported_name: String,
+ renamed: bool,
+ glob: bool,
+ absolute: bool,
+ attributes: Vec<String>,
+}
+
+fn validate_public_api_authority(workspace_root: &Path) -> Result<(), String> {
+ let model_source = rust_source(workspace_root, "crates/event_store/src/model.rs")?;
+ let lib_source = rust_source(workspace_root, "crates/event_store/src/lib.rs")?;
+ let error_source = rust_source(workspace_root, "crates/event_store/src/error.rs")?;
+ let maintenance_source = rust_source(
+ workspace_root,
+ "crates/event_store/src/source_maintenance_v1.rs",
+ )?;
+ let store_source = rust_source(workspace_root, "crates/event_store/src/store.rs")?;
+ validate_public_api_sources(
+ &model_source,
+ &lib_source,
+ &error_source,
+ &maintenance_source,
+ &store_source,
+ )
+}
+
+fn validate_public_api_sources(
+ model_source: &str,
+ lib_source: &str,
+ error_source: &str,
+ maintenance_source: &str,
+ store_source: &str,
+) -> Result<(), String> {
+ let model = syn::parse_file(model_source)
+ .map_err(|error| format!("parse crates/event_store/src/model.rs: {error}"))?;
+ let lib = syn::parse_file(lib_source)
+ .map_err(|error| format!("parse crates/event_store/src/lib.rs: {error}"))?;
+ let error = syn::parse_file(error_source)
+ .map_err(|error| format!("parse crates/event_store/src/error.rs: {error}"))?;
+ let maintenance = syn::parse_file(maintenance_source).map_err(|error| {
+ format!("parse crates/event_store/src/source_maintenance_v1.rs: {error}")
+ })?;
+ let store = syn::parse_file(store_source)
+ .map_err(|error| format!("parse crates/event_store/src/store.rs: {error}"))?;
+
+ let governed_modules = GOVERNED_MODEL_MODULES
+ .iter()
+ .copied()
+ .collect::<BTreeSet<_>>();
+ let mut model_exports = BTreeSet::new();
+ let mut represented_modules = BTreeSet::new();
+ for route in collect_top_level_public_use_routes(&model) {
+ let Some(module) = route.segments.first().map(String::as_str) else {
+ continue;
+ };
+ if !governed_modules.contains(module) {
+ continue;
+ }
+ if route.absolute || route.renamed || route.glob || route.segments.len() != 2 {
+ return Err(format!(
+ "model predecessor export `{}` must be a direct, non-renamed public re-export",
+ route.segments.join("::")
+ ));
+ }
+ represented_modules.insert(module.to_owned());
+ if !model_exports.insert(route.exported_name.clone()) {
+ return Err(format!(
+ "model predecessor symbol `{}` is exported more than once",
+ route.exported_name
+ ));
+ }
+ }
+ let expected_modules = GOVERNED_MODEL_MODULES
+ .iter()
+ .map(|value| (*value).to_owned())
+ .collect::<BTreeSet<_>>();
+ let expected_inherited = INHERITED_PUBLIC_API
+ .iter()
+ .map(|value| (*value).to_owned())
+ .collect::<BTreeSet<_>>();
+ if represented_modules != expected_modules || model_exports != expected_inherited {
+ return Err(format!(
+ "model inherited FoodAvailability export authority differs: modules={represented_modules:?}, symbols={model_exports:?}"
+ ));
+ }
+
+ let sqlite_cfg = "#[cfg(feature=\"sqlite\")]";
+ let routes = collect_top_level_public_use_routes(&lib);
+ if routes
+ .iter()
+ .any(|route| route.exported_name == "RadrootsEventStoreReconciliationResource")
+ {
+ return Err(
+ "removed public symbol `RadrootsEventStoreReconciliationResource` must remain absent from the crate root"
+ .to_owned(),
+ );
+ }
+ let mut expected_root_routes = BTreeMap::new();
+ for symbol in INHERITED_PUBLIC_API {
+ expected_root_routes.insert((*symbol).to_owned(), "model");
+ }
+ for symbol in &ADDED_PUBLIC_API[..6] {
+ expected_root_routes.insert((*symbol).to_owned(), "error");
+ }
+ expected_root_routes.insert(
+ "RadrootsEventStoreSourceCapacityV1".to_owned(),
+ "source_maintenance_v1",
+ );
+ for (symbol, expected_module) in expected_root_routes {
+ let matches = routes
+ .iter()
+ .filter(|route| route.exported_name == symbol)
+ .collect::<Vec<_>>();
+ if matches.len() != 1 {
+ return Err(format!(
+ "crate root must export governed symbol `{symbol}` exactly once; found {}",
+ matches.len()
+ ));
+ }
+ let route = matches[0];
+ if route.attributes.as_slice() != [sqlite_cfg]
+ || route.absolute
+ || route.renamed
+ || route.glob
+ || route.segments.len() != 2
+ || route.segments[0] != expected_module
+ || route.segments[1] != symbol
+ {
+ return Err(format!(
+ "crate-root governed export `{symbol}` must be direct, non-renamed, sqlite-gated, and sourced from `{expected_module}`"
+ ));
+ }
+ }
+
+ let error_public = top_level_public_item_names(&error);
+ if error_public.contains("RadrootsEventStoreReconciliationResource") {
+ return Err(
+ "removed public symbol `RadrootsEventStoreReconciliationResource` must remain absent from the error module"
+ .to_owned(),
+ );
+ }
+ for symbol in &ADDED_PUBLIC_API[..6] {
+ if !error_public.contains(*symbol) {
+ return Err(format!("error module does not publicly define `{symbol}`"));
+ }
+ }
+ validate_source_capacity_snapshot_authority(&maintenance)?;
+
+ let methods = associated_method_tokens(&store, "RadrootsEventStore", "source_capacity_v1")?;
+ if methods.len() != 1 {
+ return Err(format!(
+ "RadrootsEventStore must define source_capacity_v1 exactly once; found {}",
+ methods.len()
+ ));
+ }
+ let expected = syn::parse_str::<syn::ImplItemFn>(
+ r#"pub async fn source_capacity_v1(
+ &self,
+ ) -> Result<crate::RadrootsEventStoreSourceCapacityV1, RadrootsEventStoreError> {
+ let mut tx = self.pool.begin().await?;
+ let capacity =
+ crate::source_maintenance_v1::validate_source_capacity_authority_fast_v1(
+ &mut tx
+ ).await?;
+ tx.commit().await?;
+ Ok(capacity)
+ }"#,
+ )
+ .map_err(|error| format!("parse authoritative source_capacity_v1 method: {error}"))?;
+ let expected = compact_tokens(&expected);
+ if methods[0] != expected {
+ return Err(format!(
+ "public capacity query signature or four-statement transaction authority drifted: expected `{expected}`, found `{}`",
+ methods[0]
+ ));
+ }
+ Ok(())
+}
+
+fn collect_top_level_public_use_routes(file: &syn::File) -> Vec<PublicUseRoute> {
+ let mut routes = Vec::new();
+ for item in &file.items {
+ let Item::Use(item_use) = item else {
+ continue;
+ };
+ if !matches!(item_use.vis, syn::Visibility::Public(_)) {
+ continue;
+ }
+ let attributes = item_use
+ .attrs
+ .iter()
+ .map(compact_tokens)
+ .collect::<Vec<_>>();
+ let mut segments = Vec::new();
+ flatten_public_use_tree(
+ &item_use.tree,
+ &mut segments,
+ item_use.leading_colon.is_some(),
+ &attributes,
+ &mut routes,
+ );
+ }
+ routes
+}
+
+fn flatten_public_use_tree(
+ tree: &UseTree,
+ prefix: &mut Vec<String>,
+ absolute: bool,
+ attributes: &[String],
+ routes: &mut Vec<PublicUseRoute>,
+) {
+ match tree {
+ UseTree::Path(path) => {
+ prefix.push(path.ident.to_string());
+ flatten_public_use_tree(&path.tree, prefix, absolute, attributes, routes);
+ prefix.pop();
+ }
+ UseTree::Name(name) => {
+ let mut segments = prefix.clone();
+ segments.push(name.ident.to_string());
+ routes.push(PublicUseRoute {
+ exported_name: name.ident.to_string(),
+ segments,
+ renamed: false,
+ glob: false,
+ absolute,
+ attributes: attributes.to_vec(),
+ });
+ }
+ UseTree::Rename(rename) => {
+ let mut segments = prefix.clone();
+ segments.push(rename.ident.to_string());
+ routes.push(PublicUseRoute {
+ exported_name: rename.rename.to_string(),
+ segments,
+ renamed: true,
+ glob: false,
+ absolute,
+ attributes: attributes.to_vec(),
+ });
+ }
+ UseTree::Glob(_) => routes.push(PublicUseRoute {
+ exported_name: "*".to_owned(),
+ segments: prefix.clone(),
+ renamed: false,
+ glob: true,
+ absolute,
+ attributes: attributes.to_vec(),
+ }),
+ UseTree::Group(group) => {
+ for item in &group.items {
+ flatten_public_use_tree(item, prefix, absolute, attributes, routes);
+ }
+ }
+ }
+}
+
+fn top_level_public_item_names(file: &syn::File) -> BTreeSet<String> {
+ file.items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Const(item) if matches!(item.vis, syn::Visibility::Public(_)) => {
+ Some(item.ident.to_string())
+ }
+ Item::Enum(item) if matches!(item.vis, syn::Visibility::Public(_)) => {
+ Some(item.ident.to_string())
+ }
+ Item::Fn(item) if matches!(item.vis, syn::Visibility::Public(_)) => {
+ Some(item.sig.ident.to_string())
+ }
+ Item::Struct(item) if matches!(item.vis, syn::Visibility::Public(_)) => {
+ Some(item.ident.to_string())
+ }
+ Item::Type(item) if matches!(item.vis, syn::Visibility::Public(_)) => {
+ Some(item.ident.to_string())
+ }
+ _ => None,
+ })
+ .collect()
+}
+
+fn exact_top_level_enum<'a>(file: &'a syn::File, name: &str) -> Result<&'a syn::ItemEnum, String> {
+ let matches = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Enum(item) if item.ident == name => Some(item),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [item] = matches.as_slice() else {
+ return Err(format!(
+ "Rust source must define top-level enum `{name}` exactly once; found {}",
+ matches.len()
+ ));
+ };
+ Ok(item)
+}
+
+fn associated_method_tokens(
+ file: &syn::File,
+ owner: &str,
+ method: &str,
+) -> Result<Vec<String>, String> {
+ let mut matches = Vec::new();
+ for item in &file.items {
+ let Item::Impl(item_impl) = item else {
+ continue;
+ };
+ if compact_tokens(item_impl.self_ty.as_ref()) != owner {
+ continue;
+ }
+ for item in &item_impl.items {
+ let syn::ImplItem::Fn(function) = item else {
+ continue;
+ };
+ if function.sig.ident == method {
+ let mut function = function.clone();
+ function.attrs.clear();
+ matches.push(compact_tokens(&function));
+ }
+ }
+ }
+ Ok(matches)
+}
+
+struct FreeFunctionCollector<'name, 'ast> {
+ name: &'name str,
+ matches: Vec<&'ast syn::ItemFn>,
+}
+
+impl<'ast> syn::visit::Visit<'ast> for FreeFunctionCollector<'_, 'ast> {
+ fn visit_item_fn(&mut self, function: &'ast syn::ItemFn) {
+ if function.sig.ident == self.name {
+ self.matches.push(function);
+ }
+ syn::visit::visit_item_fn(self, function);
+ }
+}
+
+fn exact_free_function<'a>(file: &'a syn::File, name: &str) -> Result<&'a syn::ItemFn, String> {
+ use syn::visit::Visit;
+ let mut collector = FreeFunctionCollector {
+ name,
+ matches: Vec::new(),
+ };
+ collector.visit_file(file);
+ if collector.matches.len() != 1 {
+ return Err(format!(
+ "governed Rust source must define free function `{name}` exactly once; found {}",
+ collector.matches.len()
+ ));
+ }
+ Ok(collector.matches.pop().expect("one function"))
+}
+
+fn exact_free_function_tokens(file: &syn::File, name: &str) -> Result<String, String> {
+ Ok(compact_tokens(exact_free_function(file, name)?))
+}
+
+fn exact_top_level_function<'a>(
+ file: &'a syn::File,
+ name: &str,
+) -> Result<&'a syn::ItemFn, String> {
+ let matches = file
+ .items
+ .iter()
+ .filter_map(|item| match item {
+ Item::Fn(function) if function.sig.ident == name => Some(function),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+ let [function] = matches.as_slice() else {
+ return Err(format!(
+ "governed Rust source must define top-level function `{name}` exactly once; found {}",
+ matches.len()
+ ));
+ };
+ Ok(function)
+}
+
+fn rust_source(workspace_root: &Path, relative: &str) -> Result<String, String> {
+ let bytes = read_regular_file(workspace_root, relative)?;
+ std::str::from_utf8(&bytes)
+ .map(str::to_owned)
+ .map_err(|error| format!("{relative} must be UTF-8 Rust: {error}"))
+}
+
+fn compact_rust(source: &str, relative: &str) -> Result<String, String> {
+ let syntax = syn::parse_file(source).map_err(|error| format!("parse {relative}: {error}"))?;
+ Ok(compact_tokens(&syntax))
+}
+
+fn compact_tokens(tokens: &impl ToTokens) -> String {
+ tokens.to_token_stream().to_string().replace(' ', "")
+}
+
+fn require_marker(label: &str, source: &str, marker: &str) -> Result<(), String> {
+ if !source.contains(marker) {
+ return Err(format!("{label} is missing exact witness `{marker}`"));
+ }
+ Ok(())
+}
+
+fn require_ordered_markers(label: &str, source: &str, markers: &[&str]) -> Result<(), String> {
+ let mut offset = 0;
+ for marker in markers {
+ let Some(found) = source[offset..].find(marker) else {
+ return Err(format!(
+ "{label} is missing ordered witness `{marker}` after byte {offset}"
+ ));
+ };
+ offset += found + marker.len();
+ }
+ Ok(())
+}
+
+fn validate_manifest_shape(manifest: &SourceMaintenanceManifest) -> Result<(), String> {
+ if manifest.schema_version != SCHEMA_VERSION
+ || manifest.contract_id != CONTRACT_ID
+ || manifest.hook_id != HOOK_ID
+ || manifest.manifest_schema.path != MANIFEST_SCHEMA_RELATIVE
+ || manifest.predecessor.hook_id != PREDECESSOR_HOOK_ID
+ || manifest.predecessor.manifest.path != PREDECESSOR_MANIFEST_RELATIVE
+ || manifest.predecessor.manifest.byte_length
+ != u64::try_from(PREDECESSOR_MANIFEST_BYTE_LENGTH)
+ .map_err(|_| "predecessor length does not fit u64".to_owned())?
+ || manifest.predecessor.manifest.sha256 != PREDECESSOR_MANIFEST_SHA256
+ || manifest.migration.version != MIGRATION_VERSION
+ || manifest.migration.name != MIGRATION_NAME
+ || manifest.migration.up.path != MIGRATION_UP_RELATIVE
+ || manifest.migration.down.path != MIGRATION_DOWN_RELATIVE
+ || manifest.migration.schema_sha256 != SCHEMA_SHA256
+ || manifest.source_maintenance
+ != (SourceMaintenanceDescriptor {
+ version: CAPACITY_VERSION,
+ event_contract_registry_version: EVENT_CONTRACT_REGISTRY_VERSION,
+ capacity_authority_id: CAPACITY_AUTHORITY_ID.to_owned(),
+ accounting: AccountingDescriptor {
+ algorithm: ACCOUNTING_ALGORITHM.to_owned(),
+ raw_event_columns: owned(RAW_EVENT_COLUMNS),
+ raw_tag_columns: owned(RAW_TAG_COLUMNS),
+ nullable_raw_tag_columns: owned(NULLABLE_RAW_TAG_COLUMNS),
+ },
+ limits: expected_limits(),
+ reopen_validation: ReopenValidationDescriptor {
+ mode: REOPEN_VALIDATION_MODE.to_owned(),
+ raw_event_rejection_scan_bound: RAW_EVENT_REJECTION_SCAN_BOUND,
+ raw_tag_rejection_scan_bound: RAW_TAG_REJECTION_SCAN_BOUND,
+ generation_history_validation: GENERATION_HISTORY_VALIDATION.to_owned(),
+ retained_generation_rejection_scan_bound:
+ RETAINED_GENERATION_REJECTION_SCAN_BOUND,
+ },
+ rebuild_seal: RebuildSealDescriptor {
+ nip09_hook_id: NIP09_HOOK_ID.to_owned(),
+ nip09_manifest_sha256: NIP09_MANIFEST_SHA256.to_owned(),
+ food_hook_id: PREDECESSOR_HOOK_ID.to_owned(),
+ food_manifest_sha256: PREDECESSOR_MANIFEST_SHA256.to_owned(),
+ food_scope_fingerprint_sha256: FOOD_SCOPE_FINGERPRINT_SHA256.to_owned(),
+ active_generation_authority: ACTIVE_GENERATION_AUTHORITY.to_owned(),
+ marker_close_authority: MARKER_CLOSE_AUTHORITY.to_owned(),
+ },
+ })
+ || manifest.public_api != expected_public_api()
+ {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} has inconsistent SourceMaintenance identity or semantics"
+ ));
+ }
+ validate_catalog(&manifest.migration.catalog)?;
+ validate_migration_identity(&manifest.migration.up, &manifest.migration.down)?;
+
+ let expected_entry_points = ENTRY_POINTS
+ .iter()
+ .map(|(role, rust_path)| EntryPointDescriptor {
+ role: (*role).to_owned(),
+ rust_path: (*rust_path).to_owned(),
+ })
+ .collect::<Vec<_>>();
+ if manifest.entry_points != expected_entry_points {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} entry-point inventory is not exact"
+ ));
+ }
+ let expected_source_identity = SOURCE_SPECS
+ .iter()
+ .map(|spec| (spec.role, spec.path))
+ .collect::<Vec<_>>();
+ let actual_source_identity = manifest
+ .source_files
+ .iter()
+ .map(|source| (source.role.as_str(), source.path.as_str()))
+ .collect::<Vec<_>>();
+ if actual_source_identity != expected_source_identity {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} source-file inventory is not exact"
+ ));
+ }
+ validate_unique(
+ "SourceMaintenance manifest source roles",
+ manifest
+ .source_files
+ .iter()
+ .map(|source| source.role.as_str()),
+ )?;
+ validate_unique(
+ "SourceMaintenance manifest source paths",
+ manifest
+ .source_files
+ .iter()
+ .map(|source| source.path.as_str()),
+ )?;
+ for source in &manifest.source_files {
+ if GENERATED_ARTIFACT_PATHS.contains(&source.path.as_str()) {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} recursively hashes generated artifact `{}`",
+ source.path
+ ));
+ }
+ validate_sha256(source.path.as_str(), source.sha256.as_str())?;
+ if source.hash_algorithm != HASH_ALGORITHM || source.byte_length == 0 {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} source descriptor `{}` is invalid",
+ source.path
+ ));
+ }
+ }
+ for descriptor in [
+ &manifest.manifest_schema,
+ &manifest.predecessor.manifest,
+ &manifest.migration.up,
+ &manifest.migration.down,
+ ] {
+ validate_sha256(descriptor.path.as_str(), descriptor.sha256.as_str())?;
+ if descriptor.hash_algorithm != HASH_ALGORITHM || descriptor.byte_length == 0 {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} file descriptor `{}` is invalid",
+ descriptor.path
+ ));
+ }
+ }
+ validate_sha256(
+ "migration schema",
+ manifest.migration.schema_sha256.as_str(),
+ )?;
+ validate_sha256("result vector", manifest.result_vector.sha256.as_str())?;
+ validate_sha256(
+ "result-vector executor",
+ manifest.result_vector.executor_sha256.as_str(),
+ )?;
+ if manifest.result_vector.canonical_path != RESULT_VECTOR_CANONICAL_RELATIVE
+ || manifest.result_vector.mirror_path != RESULT_VECTOR_MIRROR_RELATIVE
+ || manifest.result_vector.hash_algorithm != HASH_ALGORITHM
+ || manifest.result_vector.executor_id != RESULT_VECTOR_EXECUTOR_ID
+ || manifest.result_vector.executor_path != RESULT_VECTOR_EXECUTOR_RELATIVE
+ || manifest.result_vector.executor_test != RESULT_VECTOR_EXECUTOR_TEST
+ || manifest.result_vector.executor_hash_algorithm != HASH_ALGORITHM
+ || manifest.result_vector.byte_length == 0
+ || manifest.result_vector.executor_byte_length == 0
+ {
+ return Err(format!(
+ "{MANIFEST_RELATIVE} result-vector descriptor is invalid"
+ ));
+ }
+ Ok(())
+}
+
+fn generated_descriptor(
+ manifest: &SourceMaintenanceManifest,
+ manifest_bytes: &[u8],
+ manifest_sha256: &str,
+) -> String {
+ let manifest_json = std::str::from_utf8(manifest_bytes).expect("canonical manifest is UTF-8");
+ let manifest_literal = format!("{manifest_json:?}");
+ format!(
+ "// @generated by `cargo xtask contract source-maintenance-manifest --write`; do not edit.\n\
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = {manifest_literal};\n\
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH: usize = {};\n\
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SHA256: &str =\n \"{manifest_sha256}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION: u32 = {SCHEMA_VERSION};\n\
+pub(crate) const SOURCE_MAINTENANCE_CONTRACT_ID: &str =\n \"{CONTRACT_ID}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_HOOK_ID: &str = \"{HOOK_ID}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_VERSION: u32 = {MIGRATION_VERSION};\n\
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_NAME: &str = \"{MIGRATION_NAME}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_UP_BYTE_LENGTH: usize = {};\n\
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_UP_SHA256: &str =\n \"{}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_DOWN_BYTE_LENGTH: usize = {};\n\
+pub(crate) const SOURCE_MAINTENANCE_MIGRATION_DOWN_SHA256: &str =\n \"{}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_SCHEMA_SHA256: &str =\n \"{SCHEMA_SHA256}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_EVENT_CONTRACT_REGISTRY_VERSION: u32 = {EVENT_CONTRACT_REGISTRY_VERSION};\n\
+pub(crate) const SOURCE_MAINTENANCE_CAPACITY_VERSION: u32 = {CAPACITY_VERSION};\n\
+pub(crate) const SOURCE_MAINTENANCE_CAPACITY_AUTHORITY_ID: &str =\n \"{CAPACITY_AUTHORITY_ID}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_ACCOUNTING_ALGORITHM: &str = \"{ACCOUNTING_ALGORITHM}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_COLUMNS: &[&str] = &{};\n\
+pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_COLUMNS: &[&str] =\n &{};\n\
+pub(crate) const SOURCE_MAINTENANCE_NULLABLE_RAW_TAG_COLUMNS: &[&str] = &{};\n\
+pub(crate) const SOURCE_MAINTENANCE_REPLACED_OBJECT_NAMES: &[&str] = &{};\n\
+pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_COUNT_LIMIT: u64 = {RAW_EVENT_COUNT_LIMIT};\n\
+pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_COUNT_LIMIT: u64 = {RAW_TAG_COUNT_LIMIT};\n\
+pub(crate) const SOURCE_MAINTENANCE_RAW_EVENT_TEXT_BYTES_LIMIT: u64 = {RAW_EVENT_TEXT_BYTES_LIMIT};\n\
+pub(crate) const SOURCE_MAINTENANCE_RAW_TAG_TEXT_BYTES_LIMIT: u64 = {RAW_TAG_TEXT_BYTES_LIMIT};\n\
+pub(crate) const SOURCE_MAINTENANCE_RETAINED_SOURCE_GENERATION_LIMIT: u32 = {RETAINED_SOURCE_GENERATION_LIMIT};\n\
+pub(crate) const SOURCE_MAINTENANCE_PREDECESSOR_HOOK_ID: &str = \"{PREDECESSOR_HOOK_ID}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_PREDECESSOR_MANIFEST_SHA256: &str =\n \"{PREDECESSOR_MANIFEST_SHA256}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_SHA256: &str =\n \"{}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_ID: &str =\n \"{RESULT_VECTOR_EXECUTOR_ID}\";\n\
+pub(crate) const SOURCE_MAINTENANCE_RESULT_VECTOR_EXECUTOR_SHA256: &str =\n \"{}\";\n",
+ manifest_bytes.len(),
+ usize::try_from(manifest.migration.up.byte_length).expect("up length fits usize"),
+ manifest.migration.up.sha256,
+ usize::try_from(manifest.migration.down.byte_length).expect("down length fits usize"),
+ manifest.migration.down.sha256,
+ rust_multiline_string_slice(RAW_EVENT_COLUMNS),
+ rust_string_slice(RAW_TAG_COLUMNS),
+ rust_string_slice(NULLABLE_RAW_TAG_COLUMNS),
+ rust_multiline_string_slice(EXPECTED_REPLACED_CATALOG_OBJECTS),
+ manifest.result_vector.sha256,
+ manifest.result_vector.executor_sha256,
+ )
+}
+
+fn rust_string_slice(values: &[&str]) -> String {
+ let values = values
+ .iter()
+ .map(|value| format!("{value:?}"))
+ .collect::<Vec<_>>()
+ .join(", ");
+ format!("[{values}]")
+}
+
+fn rust_multiline_string_slice(values: &[&str]) -> String {
+ let values = values
+ .iter()
+ .map(|value| format!(" {value:?},"))
+ .collect::<Vec<_>>()
+ .join("\n");
+ format!("[\n{values}\n]")
+}
+
+fn manifest_schema() -> Value {
+ let path_pattern = "^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$";
+ let file = json!({
+ "type": "object",
+ "required": ["path", "byte_length", "sha256", "hash_algorithm"],
+ "properties": {
+ "path": {"type": "string", "pattern": path_pattern},
+ "byte_length": {"type": "integer", "minimum": 1},
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "hash_algorithm": {"const": HASH_ALGORITHM}
+ },
+ "additionalProperties": false
+ });
+ let string_array = json!({
+ "type": "array",
+ "items": {"type": "string", "minLength": 1}
+ });
+ json!({
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://radroots.org/contracts/event-store/source-maintenance-v1-manifest.schema.json",
+ "title": "Radroots event-store SourceMaintenance v1 manifest",
+ "type": "object",
+ "required": [
+ "schema_version", "contract_id", "hook_id", "manifest_schema", "predecessor",
+ "migration", "source_maintenance", "entry_points", "source_files", "public_api",
+ "result_vector"
+ ],
+ "properties": {
+ "schema_version": {"const": SCHEMA_VERSION},
+ "contract_id": {"const": CONTRACT_ID},
+ "hook_id": {"const": HOOK_ID},
+ "manifest_schema": {"$ref": "#/$defs/file"},
+ "predecessor": {
+ "type": "object",
+ "required": ["hook_id", "manifest"],
+ "properties": {
+ "hook_id": {"const": PREDECESSOR_HOOK_ID},
+ "manifest": {"$ref": "#/$defs/file"}
+ },
+ "additionalProperties": false
+ },
+ "migration": {
+ "type": "object",
+ "required": ["version", "name", "up", "down", "schema_sha256", "catalog"],
+ "properties": {
+ "version": {"const": MIGRATION_VERSION},
+ "name": {"const": MIGRATION_NAME},
+ "up": {"$ref": "#/$defs/file"},
+ "down": {"$ref": "#/$defs/file"},
+ "schema_sha256": {"const": SCHEMA_SHA256},
+ "catalog": {
+ "type": "object",
+ "required": ["objects", "replaced_objects", "tables", "fts5_tables"],
+ "properties": {
+ "objects": string_array.clone(),
+ "replaced_objects": string_array.clone(),
+ "tables": string_array.clone(),
+ "fts5_tables": string_array.clone()
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "source_maintenance": {
+ "type": "object",
+ "required": [
+ "version", "event_contract_registry_version", "capacity_authority_id",
+ "accounting", "limits", "reopen_validation", "rebuild_seal"
+ ],
+ "properties": {
+ "version": {"const": CAPACITY_VERSION},
+ "event_contract_registry_version": {"const": EVENT_CONTRACT_REGISTRY_VERSION},
+ "capacity_authority_id": {"const": CAPACITY_AUTHORITY_ID},
+ "accounting": {"$ref": "#/$defs/accounting"},
+ "limits": {"$ref": "#/$defs/limits"},
+ "reopen_validation": {
+ "type": "object",
+ "required": [
+ "mode", "raw_event_rejection_scan_bound",
+ "raw_tag_rejection_scan_bound", "generation_history_validation",
+ "retained_generation_rejection_scan_bound"
+ ],
+ "properties": {
+ "mode": {"const": REOPEN_VALIDATION_MODE},
+ "raw_event_rejection_scan_bound": {
+ "const": RAW_EVENT_REJECTION_SCAN_BOUND
+ },
+ "raw_tag_rejection_scan_bound": {
+ "const": RAW_TAG_REJECTION_SCAN_BOUND
+ },
+ "generation_history_validation": {"const": GENERATION_HISTORY_VALIDATION},
+ "retained_generation_rejection_scan_bound": {
+ "const": RETAINED_GENERATION_REJECTION_SCAN_BOUND
+ }
+ },
+ "additionalProperties": false
+ },
+ "rebuild_seal": {
+ "type": "object",
+ "required": [
+ "nip09_hook_id", "nip09_manifest_sha256", "food_hook_id",
+ "food_manifest_sha256", "food_scope_fingerprint_sha256",
+ "active_generation_authority", "marker_close_authority"
+ ],
+ "properties": {
+ "nip09_hook_id": {"const": NIP09_HOOK_ID},
+ "nip09_manifest_sha256": {"const": NIP09_MANIFEST_SHA256},
+ "food_hook_id": {"const": PREDECESSOR_HOOK_ID},
+ "food_manifest_sha256": {"const": PREDECESSOR_MANIFEST_SHA256},
+ "food_scope_fingerprint_sha256": {"const": FOOD_SCOPE_FINGERPRINT_SHA256},
+ "active_generation_authority": {"const": ACTIVE_GENERATION_AUTHORITY},
+ "marker_close_authority": {"const": MARKER_CLOSE_AUTHORITY}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "entry_points": {
+ "type": "array", "minItems": 1,
+ "items": {
+ "type": "object", "required": ["role", "rust_path"],
+ "properties": {
+ "role": {"type": "string", "minLength": 1},
+ "rust_path": {"type": "string", "minLength": 1}
+ },
+ "additionalProperties": false
+ }
+ },
+ "source_files": {
+ "type": "array", "minItems": 1,
+ "items": {"$ref": "#/$defs/source_file"}
+ },
+ "public_api": {
+ "type": "object",
+ "required": [
+ "inherited_predecessor_symbols", "added_symbols", "methods", "error_variants",
+ "removed_symbols", "breaking_replacements"
+ ],
+ "properties": {
+ "inherited_predecessor_symbols": string_array.clone(),
+ "added_symbols": string_array.clone(),
+ "methods": string_array.clone(),
+ "error_variants": string_array.clone(),
+ "removed_symbols": string_array.clone(),
+ "breaking_replacements": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["removed", "replacement"],
+ "properties": {
+ "removed": {"type": "string", "minLength": 1},
+ "replacement": {"type": "string", "minLength": 1}
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "result_vector": {
+ "type": "object",
+ "required": [
+ "canonical_path", "mirror_path", "byte_length", "sha256", "hash_algorithm",
+ "executor_id", "executor_path", "executor_test", "executor_byte_length",
+ "executor_sha256", "executor_hash_algorithm"
+ ],
+ "properties": {
+ "canonical_path": {"const": RESULT_VECTOR_CANONICAL_RELATIVE},
+ "mirror_path": {"const": RESULT_VECTOR_MIRROR_RELATIVE},
+ "byte_length": {"type": "integer", "minimum": 1},
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "hash_algorithm": {"const": HASH_ALGORITHM},
+ "executor_id": {"const": RESULT_VECTOR_EXECUTOR_ID},
+ "executor_path": {"const": RESULT_VECTOR_EXECUTOR_RELATIVE},
+ "executor_test": {"const": RESULT_VECTOR_EXECUTOR_TEST},
+ "executor_byte_length": {"type": "integer", "minimum": 1},
+ "executor_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "executor_hash_algorithm": {"const": HASH_ALGORITHM}
+ },
+ "additionalProperties": false
+ }
+ },
+ "$defs": {
+ "file": file,
+ "source_file": {
+ "type": "object",
+ "required": ["role", "path", "byte_length", "sha256", "hash_algorithm"],
+ "properties": {
+ "role": {"type": "string", "minLength": 1},
+ "path": {"type": "string", "pattern": path_pattern},
+ "byte_length": {"type": "integer", "minimum": 1},
+ "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
+ "hash_algorithm": {"const": HASH_ALGORITHM}
+ },
+ "additionalProperties": false
+ },
+ "accounting": {
+ "type": "object",
+ "required": [
+ "algorithm", "raw_event_columns", "raw_tag_columns", "nullable_raw_tag_columns"
+ ],
+ "properties": {
+ "algorithm": {"const": ACCOUNTING_ALGORITHM},
+ "raw_event_columns": string_array.clone(),
+ "raw_tag_columns": string_array.clone(),
+ "nullable_raw_tag_columns": string_array.clone()
+ },
+ "additionalProperties": false
+ },
+ "limits": {
+ "type": "object",
+ "required": [
+ "raw_events", "raw_tags", "raw_event_text_bytes", "raw_tag_text_bytes",
+ "retained_source_generations"
+ ],
+ "properties": {
+ "raw_events": {"const": RAW_EVENT_COUNT_LIMIT},
+ "raw_tags": {"const": RAW_TAG_COUNT_LIMIT},
+ "raw_event_text_bytes": {"const": RAW_EVENT_TEXT_BYTES_LIMIT},
+ "raw_tag_text_bytes": {"const": RAW_TAG_TEXT_BYTES_LIMIT},
+ "retained_source_generations": {"const": RETAINED_SOURCE_GENERATION_LIMIT}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ })
+}
+
+fn validate_manifest_json_schema(schema: &Value, manifest: &Value) -> Result<(), String> {
+ jsonschema::draft202012::meta::validate(schema).map_err(|error| {
+ format!(
+ "{MANIFEST_SCHEMA_RELATIVE} is not a valid JSON Schema Draft 2020-12 document: {error}"
+ )
+ })?;
+ let validator = jsonschema::draft202012::options()
+ .build(schema)
+ .map_err(|error| {
+ format!("compile {MANIFEST_SCHEMA_RELATIVE} as JSON Schema Draft 2020-12: {error}")
+ })?;
+ validator.validate(manifest).map_err(|error| {
+ format!(
+ "{MANIFEST_RELATIVE} violates {MANIFEST_SCHEMA_RELATIVE} at {}: {error}",
+ error.instance_path()
+ )
+ })
+}
+
+fn canonical_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
+ let mut bytes =
+ serde_json::to_vec_pretty(value).map_err(|error| format!("serialize JSON: {error}"))?;
+ bytes.push(b'\n');
+ Ok(bytes)
+}
+
+fn validate_canonical_json<T: Serialize>(
+ relative: &str,
+ actual: &[u8],
+ value: &T,
+) -> Result<(), String> {
+ let canonical = canonical_json_bytes(value)?;
+ if actual != canonical {
+ return Err(format!(
+ "{relative} must use canonical pretty JSON with one trailing newline"
+ ));
+ }
+ Ok(())
+}
+
+fn validate_unique<'a>(label: &str, values: impl Iterator<Item = &'a str>) -> Result<(), String> {
+ let mut seen = BTreeSet::new();
+ for value in values {
+ if !seen.insert(value) {
+ return Err(format!("{label} contains duplicate `{value}`"));
+ }
+ }
+ Ok(())
+}
+
+fn validate_digest_sidecar(relative: &str, bytes: &[u8]) -> Result<(), String> {
+ let value =
+ std::str::from_utf8(bytes).map_err(|error| format!("{relative} must be UTF-8: {error}"))?;
+ let Some(digest) = value.strip_suffix('\n') else {
+ return Err(format!("{relative} must end in exactly one newline"));
+ };
+ if digest.contains('\n') || digest.contains('\r') {
+ return Err(format!("{relative} must contain one SHA-256 digest line"));
+ }
+ validate_sha256(relative, digest)
+}
+
+fn validate_sha256(label: &str, value: &str) -> Result<(), String> {
+ if value.len() != 64
+ || !value
+ .as_bytes()
+ .iter()
+ .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
+ {
+ return Err(format!("{label} must be a lowercase 64-hex SHA-256 digest"));
+ }
+ Ok(())
+}
+
+fn sha256_hex(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
+}
+
+fn stale_error(relative: &str) -> String {
+ format!("{relative} is stale; run `{WRITE_COMMAND}`")
+}
+
+#[derive(Clone, Copy)]
+struct ExpectedVectorCase {
+ id: &'static str,
+ execution: &'static str,
+ authority: &'static str,
+ authority_path: &'static str,
+ resource: Option<&'static str>,
+ boundary: Option<&'static str>,
+ expected_outcome: &'static str,
+ error_domain: Option<&'static str>,
+ expected_error: Option<&'static str>,
+}
+
+const DIRECT_EXECUTOR: &str = "direct_executor";
+const DELEGATED_RUST_TEST: &str = "delegated_rust_test";
+const DELEGATED_SQL_TEST: &str = "delegated_sql_test";
+
+const EXPECTED_VECTOR_CASES: &[ExpectedVectorCase] = &[
+ ExpectedVectorCase {
+ id: "fresh_store_zero_authority",
+ execution: DIRECT_EXECUTOR,
+ authority: RESULT_VECTOR_EXECUTOR_TEST,
+ authority_path: RESULT_VECTOR_EXECUTOR_RELATIVE,
+ resource: None,
+ boundary: None,
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "durable_unique_append_updates_all_dimensions",
+ execution: DIRECT_EXECUTOR,
+ authority: RESULT_VECTOR_EXECUTOR_TEST,
+ authority_path: RESULT_VECTOR_EXECUTOR_RELATIVE,
+ resource: None,
+ boundary: None,
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "duplicate_at_exact_boundary_is_idempotent",
+ execution: DELEGATED_RUST_TEST,
+ authority: "exact_capacity_boundary_allows_duplicate_observation_and_ephemeral_noop",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: Some("raw_events"),
+ boundary: Some("exact"),
+ expected_outcome: "accepted_without_capacity_delta",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "ephemeral_consumes_no_capacity",
+ execution: DIRECT_EXECUTOR,
+ authority: RESULT_VECTOR_EXECUTOR_TEST,
+ authority_path: RESULT_VECTOR_EXECUTOR_RELATIVE,
+ resource: None,
+ boundary: None,
+ expected_outcome: "accepted_without_capacity_delta",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "raw_event_count_exact",
+ execution: DELEGATED_RUST_TEST,
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_events"),
+ boundary: Some("exact"),
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "raw_event_count_one_over",
+ execution: DELEGATED_RUST_TEST,
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_events"),
+ boundary: Some("one_over"),
+ expected_outcome: "rejected_before_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceCapacityExceeded"),
+ },
+ ExpectedVectorCase {
+ id: "raw_tag_count_exact",
+ execution: DELEGATED_RUST_TEST,
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_tags"),
+ boundary: Some("exact"),
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "raw_tag_count_one_over",
+ execution: DELEGATED_RUST_TEST,
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_tags"),
+ boundary: Some("one_over"),
+ expected_outcome: "rejected_before_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceCapacityExceeded"),
+ },
+ ExpectedVectorCase {
+ id: "raw_event_text_bytes_exact",
+ execution: DELEGATED_RUST_TEST,
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_event_text_bytes"),
+ boundary: Some("exact"),
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "raw_event_text_bytes_one_over",
+ execution: DELEGATED_RUST_TEST,
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_event_text_bytes"),
+ boundary: Some("one_over"),
+ expected_outcome: "rejected_before_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceCapacityExceeded"),
+ },
+ ExpectedVectorCase {
+ id: "raw_tag_text_bytes_exact",
+ execution: DELEGATED_RUST_TEST,
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_tag_text_bytes"),
+ boundary: Some("exact"),
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "raw_tag_text_bytes_one_over",
+ execution: DELEGATED_RUST_TEST,
+ authority: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_tag_text_bytes"),
+ boundary: Some("one_over"),
+ expected_outcome: "rejected_before_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceCapacityExceeded"),
+ },
+ ExpectedVectorCase {
+ id: "outer_transaction_rollback_restores_capacity",
+ execution: DIRECT_EXECUTOR,
+ authority: RESULT_VECTOR_EXECUTOR_TEST,
+ authority_path: RESULT_VECTOR_EXECUTOR_RELATIVE,
+ resource: None,
+ boundary: None,
+ expected_outcome: "rolled_back_without_capacity_delta",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "failed_nested_ingest_rolls_back_savepoint_only",
+ execution: DELEGATED_RUST_TEST,
+ authority: "borrowed_ingest_savepoint_rolls_back_post_core_authority_forge",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: None,
+ boundary: None,
+ expected_outcome: "failed_ingest_rolled_back_and_prior_caller_work_preserved",
+ error_domain: Some("typed"),
+ expected_error: Some("MigrationHookStateDrift"),
+ },
+ ExpectedVectorCase {
+ id: "v3_to_v4_under_limit_succeeds",
+ execution: DELEGATED_RUST_TEST,
+ authority: "v3_to_v4_under_limit_backfills_exact_capacity_and_preserves_source",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: None,
+ boundary: Some("under_limit"),
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "v3_to_v4_prior_transition_drift_is_atomic",
+ execution: DELEGATED_RUST_TEST,
+ authority: "v3_to_v4_rejects_prior_transition_drift_atomically",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: None,
+ boundary: Some("corrupt_managed_v3"),
+ expected_outcome: "rejected_before_v4_schema_ledger_or_predecessor_trigger_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("MigrationHookStateDrift"),
+ },
+ ExpectedVectorCase {
+ id: "v3_to_v4_one_over_is_atomic",
+ execution: DELEGATED_RUST_TEST,
+ authority: "source_capacity_is_rechecked_for_every_rebuild_bound_migration",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: Some("raw_events"),
+ boundary: Some("one_over"),
+ expected_outcome: "rejected_before_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceCapacityExceeded"),
+ },
+ ExpectedVectorCase {
+ id: "v3_to_v4_persisted_ephemeral_is_atomic",
+ execution: DELEGATED_RUST_TEST,
+ authority: "v4_rejects_persisted_legacy_ephemeral_rows_atomically",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: None,
+ boundary: None,
+ expected_outcome: "rejected_before_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("PersistedEphemeralRawEvent"),
+ },
+ ExpectedVectorCase {
+ id: "reopen_rejects_incoherent_capacity_authority",
+ execution: DELEGATED_RUST_TEST,
+ authority: "reopen_full_measure_detects_every_persisted_capacity_dimension",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: None,
+ boundary: None,
+ expected_outcome: "rejected_on_reopen",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceCapacityStateDrift"),
+ },
+ ExpectedVectorCase {
+ id: "reopen_stops_at_first_raw_event_one_over",
+ execution: DELEGATED_RUST_TEST,
+ authority: "reopen_stops_at_the_first_raw_event_one_over_before_ephemeral_probe",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("raw_events"),
+ boundary: Some("one_over"),
+ expected_outcome: "rejected_at_scan_bound",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceCapacityExceeded"),
+ },
+ ExpectedVectorCase {
+ id: "retained_generation_rebuild_exact",
+ execution: DELEGATED_RUST_TEST,
+ authority: "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: Some("retained_source_generations"),
+ boundary: Some("exact"),
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "ninth_rebuild_is_typed_and_atomic",
+ execution: DELEGATED_RUST_TEST,
+ authority: "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: Some("retained_source_generations"),
+ boundary: Some("one_over"),
+ expected_outcome: "rejected_before_entropy_or_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceGenerationHistoryLimitReached"),
+ },
+ ExpectedVectorCase {
+ id: "retained_generation_sql_backstop_one_over",
+ execution: DELEGATED_SQL_TEST,
+ authority: "generation_sql_backstop_allows_exact_append_and_is_conflict_safe_one_over",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: Some("retained_source_generations"),
+ boundary: Some("one_over"),
+ expected_outcome: "rejected_by_sql_backstop",
+ error_domain: Some("sqlite_database"),
+ expected_error: Some(
+ "event-store retained source generation limit reached; replace and resync into a fresh store",
+ ),
+ },
+ ExpectedVectorCase {
+ id: "rebuild_marker_accepts_consistent_seals",
+ execution: DELEGATED_RUST_TEST,
+ authority: "current_v4_rebuild_rotates_capacity_and_food_authority_end_to_end",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: None,
+ boundary: None,
+ expected_outcome: "accepted",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "rebuild_marker_rejects_incoherent_seals",
+ execution: DELEGATED_SQL_TEST,
+ authority: "marker_close_sql_backstop_rejects_each_required_seal_drift",
+ authority_path: "crates/event_store/src/source_maintenance_v1.rs",
+ resource: None,
+ boundary: None,
+ expected_outcome: "rejected_by_sql_backstop",
+ error_domain: Some("sqlite_database"),
+ expected_error: Some(
+ "event-store rebuild marker cannot close before capacity, NIP-09, and FoodAvailability seals agree",
+ ),
+ },
+ ExpectedVectorCase {
+ id: "v4_marker_repair_binds_exact_prior_and_floor",
+ execution: DELEGATED_RUST_TEST,
+ authority: "v4_marker_open_allows_repairing_prior_transition_high_water_drift",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: None,
+ boundary: Some("managed_v4_rebuild"),
+ expected_outcome: "accepts_derived_high_water_repair_and_rejects_wrong_prior_or_floor",
+ error_domain: Some("sqlite_database"),
+ expected_error: Some("exact raw and prior source authority"),
+ },
+ ExpectedVectorCase {
+ id: "v4_food_reset_requires_target_rotation",
+ execution: DELEGATED_RUST_TEST,
+ authority: "v4_food_reset_requires_marker_rotation_and_preserves_target_rows",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: None,
+ boundary: Some("managed_v4_rebuild"),
+ expected_outcome: "historical_rows_deleted_only_after_rotation_and_target_rows_preserved",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "v4_down_restores_predecessor_triggers",
+ execution: DELEGATED_RUST_TEST,
+ authority: "v4_down_restores_exact_predecessor_trigger_sql_and_fingerprint",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: None,
+ boundary: Some("v4_to_v3"),
+ expected_outcome: "restored_exact_predecessor_trigger_sql_and_v3_fingerprint",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "utf16_open_file_rejected_before_mutation",
+ execution: DELEGATED_RUST_TEST,
+ authority: "open_file_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: None,
+ boundary: None,
+ expected_outcome: "rejected_before_schema_or_journal_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("SqliteMainDatabaseEncodingNotUtf8"),
+ },
+ ExpectedVectorCase {
+ id: "utf16_open_pool_rejected_before_mutation",
+ execution: DELEGATED_RUST_TEST,
+ authority: "open_pool_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: None,
+ boundary: None,
+ expected_outcome: "rejected_before_schema_or_journal_mutation",
+ error_domain: Some("typed"),
+ expected_error: Some("SqliteMainDatabaseEncodingNotUtf8"),
+ },
+ ExpectedVectorCase {
+ id: "utf8_non_ascii_nul_reopen_accounting",
+ execution: DELEGATED_RUST_TEST,
+ authority: "utf8_file_reopen_preserves_non_ascii_and_nul_capacity_accounting",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: None,
+ boundary: None,
+ expected_outcome: "accepted_with_exact_capacity_after_reopen",
+ error_domain: None,
+ expected_error: None,
+ },
+ ExpectedVectorCase {
+ id: "generation_destructive_rollback_rejected",
+ execution: DELEGATED_RUST_TEST,
+ authority: "rollback_rejects_below_floor_ahead_unmanaged_and_generation_destructive_targets",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: Some("retained_source_generations"),
+ boundary: None,
+ expected_outcome: "rejected_before_mutation_with_status_and_history_preserved",
+ error_domain: Some("typed"),
+ expected_error: Some("RollbackWouldDiscardSourceGenerationHistory"),
+ },
+ ExpectedVectorCase {
+ id: "generation_destructive_two_step_rollback_rejected",
+ execution: DELEGATED_RUST_TEST,
+ authority: "rollback_cannot_bypass_generation_history_guard_through_version_three",
+ authority_path: "crates/event_store/src/schema.rs",
+ resource: Some("retained_source_generations"),
+ boundary: None,
+ expected_outcome: "rejected_before_mutation_after_history_preserving_intermediate_rollback",
+ error_domain: Some("typed"),
+ expected_error: Some("RollbackWouldDiscardSourceGenerationHistory"),
+ },
+ ExpectedVectorCase {
+ id: "independent_pool_last_byte_slot_race",
+ execution: DELEGATED_RUST_TEST,
+ authority: "independent_file_pools_serialize_the_last_raw_event_byte_capacity_slot",
+ authority_path: "crates/event_store/src/store.rs",
+ resource: Some("raw_event_text_bytes"),
+ boundary: Some("exact"),
+ expected_outcome: "exactly_one_accepted_one_typed_rejection_and_clean_reopen",
+ error_domain: Some("typed"),
+ expected_error: Some("SourceCapacityExceeded"),
+ },
+];
+
+fn validate_result_vector(
+ workspace_root: &Path,
+ vector: &SourceMaintenanceVector,
+) -> Result<(), String> {
+ if vector.schema_version != SCHEMA_VERSION
+ || vector.contract_id != CONTRACT_ID
+ || vector.capacity_version != CAPACITY_VERSION
+ || vector.limits != expected_limits()
+ || vector.accounting
+ != (AccountingDescriptor {
+ algorithm: ACCOUNTING_ALGORITHM.to_owned(),
+ raw_event_columns: owned(RAW_EVENT_COLUMNS),
+ raw_tag_columns: owned(RAW_TAG_COLUMNS),
+ nullable_raw_tag_columns: owned(NULLABLE_RAW_TAG_COLUMNS),
+ })
+ {
+ return Err(format!(
+ "{RESULT_VECTOR_CANONICAL_RELATIVE} header, limits, or accounting authority is invalid"
+ ));
+ }
+ if vector.cases.len() != EXPECTED_VECTOR_CASES.len() {
+ return Err(format!(
+ "{RESULT_VECTOR_CANONICAL_RELATIVE} must contain exactly {} cases; found {}",
+ EXPECTED_VECTOR_CASES.len(),
+ vector.cases.len()
+ ));
+ }
+ validate_unique(
+ "SourceMaintenance result-vector case IDs",
+ vector.cases.iter().map(|case| case.id.as_str()),
+ )?;
+ for (actual, expected) in vector.cases.iter().zip(EXPECTED_VECTOR_CASES) {
+ if actual.id != expected.id
+ || actual.execution != expected.execution
+ || actual.authority != expected.authority
+ || actual.authority_path != expected.authority_path
+ || actual.resource.as_deref() != expected.resource
+ || actual.boundary.as_deref() != expected.boundary
+ || actual.expected_outcome != expected.expected_outcome
+ || actual.error_domain.as_deref() != expected.error_domain
+ || actual.expected_error.as_deref() != expected.expected_error
+ {
+ return Err(format!(
+ "{RESULT_VECTOR_CANONICAL_RELATIVE} case `{}` does not match its exact governed expectation",
+ expected.id
+ ));
+ }
+ match (
+ actual.error_domain.as_deref(),
+ actual.expected_error.as_deref(),
+ ) {
+ (None, None) => {}
+ (Some("typed" | "sqlite_database"), Some(error)) if !error.is_empty() => {}
+ _ => {
+ return Err(format!(
+ "{RESULT_VECTOR_CANONICAL_RELATIVE} case `{}` has inconsistent error domain",
+ actual.id
+ ));
+ }
+ }
+ }
+ validate_delegated_test_authorities(workspace_root, vector)
+}
+
+#[derive(Clone, Copy)]
+struct DelegatedAuthoritySpec {
+ path: &'static str,
+ test: &'static str,
+ ordered_markers: &'static [&'static str],
+}
+
+const EXECUTABLE_AUTHORITY_AST_SHA256: &str =
+ "19c405fc91468997aa53f08ebbaed82c526bf4810b2175ad9034d95dc95b8597";
+const BOUND_AUTHORITY_SOURCE_AST_SHA256: &str =
+ "ba44c729ebba14e658ec7b48f7ba395fd4e8fb3d597d306df5cfa7010a4f9bac";
+
+#[derive(Clone, Debug, Serialize)]
+struct ExecutableAuthorityIdentity {
+ path: String,
+ test: String,
+ tokens: String,
+}
+
+#[derive(Clone, Debug, Serialize)]
+struct BoundAuthoritySourceIdentity {
+ path: String,
+ tokens: String,
+}
+
+const DELEGATED_AUTHORITIES: &[DelegatedAuthoritySpec] = &[
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "exact_capacity_boundary_allows_duplicate_observation_and_ephemeral_noop",
+ ordered_markers: &[
+ "RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1",
+ "validate_source_capacity_authority_fast_v1",
+ "duplicate.persistence.is_duplicate()",
+ "SourceCapacityExceeded",
+ "RadrootsEventPersistence::NotPersisted",
+ "assert_eq!(ephemeral_observation_count,0)",
+ "transaction.rollback().await",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "borrowed_ingest_savepoint_rolls_back_post_core_authority_forge",
+ ordered_markers: &[
+ "priorcallerwork",
+ "capacity_after_prior",
+ "expect_err(\"post-corerawauthoritymutationmustfail\")",
+ "MigrationHookStateDrift",
+ "capacity_after_rollback,capacity_after_prior",
+ "callermaycommitpriorworkafterfailedingest",
+ "raw_event(prior_event.id_str())",
+ "raw_event(trigger_event.id_str())",
+ "raw_event(forged_event.id_str())",
+ "trade_mutation_count,0",
+ "transition_count,0",
+ "capacity_after_prior",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/source_maintenance_v1.rs",
+ test: "every_prospective_capacity_dimension_accepts_exact_and_rejects_one_over",
+ ordered_markers: &[
+ "RadrootsEventStoreSourceCapacityResourceV1::RawEvents",
+ "RadrootsEventStoreSourceCapacityResourceV1::RawTags",
+ "RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes",
+ "RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes",
+ "capacity_with(resource,limit-1)",
+ "delta_with(resource,1)",
+ "capacity_with(resource,limit)",
+ "SourceCapacityExceeded",
+ "requested:1",
+ "capacity_with(resource,limit+1)",
+ "requested:0",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "v3_to_v4_under_limit_backfills_exact_capacity_and_preserves_source",
+ ordered_markers: &[
+ "&EVENT_STORE_MIGRATIONS[..3]",
+ "event_envelopes",
+ "active_generation",
+ "RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT",
+ "Managed{version:4}",
+ "radroots_event_store_source_capacity_v1",
+ "assert_eq!(capacity,(generation_before,1,0,event_bytes,0,1,8))",
+ "preserved",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "v3_to_v4_rejects_prior_transition_drift_atomically",
+ ordered_markers: &[
+ "&EVENT_STORE_MIGRATIONS[..3]",
+ "predecessor_trigger_sql",
+ "corruption.commit().await",
+ "expect_err(\"v4upgrademustnotrepaircorruptmanaged-v3hookstate\")",
+ "MigrationHookStateDrift{hook_id:\"nip09_reconciliation_v1\"",
+ "ledger_after,ledger_before",
+ "v4_objects,0",
+ "v4_ledger_rows,0",
+ "schema_object_sql(&pool,name).await,*sql",
+ "Managed{version:3}",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "source_capacity_is_rechecked_for_every_rebuild_bound_migration",
+ ordered_markers: &[
+ "raw_events:0",
+ "UnledgeredBaseline",
+ "&EVENT_STORE_MIGRATIONS[..3]",
+ "expect_err(\"v4capacityexcessmustfail\")",
+ "SourceCapacityExceeded",
+ "Managed{version:3}",
+ "radroots_event_store_source_capacity_v1",
+ "assert_eq!(v4_object_count,0)",
+ "assert_eq!(v4_ledger_count,0)",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "v4_rejects_persisted_legacy_ephemeral_rows_atomically",
+ ordered_markers: &[
+ "&EVENT_STORE_MIGRATIONS[..3]",
+ "kind,tags_json,content",
+ "20000",
+ "begin_with(\"BEGINIMMEDIATE\")",
+ "expect_err(\"persistedephemeralsourcemustrejectv4\")",
+ "PersistedEphemeralRawEvent",
+ "Managed{version:3}",
+ "radroots_event_store_source_capacity_v1",
+ "assert_eq!(v4_object_count,0)",
+ "assert_eq!(v4_ledger_count,0)",
+ "assert_eq!(raw_count,1)",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/source_maintenance_v1.rs",
+ test: "reopen_full_measure_detects_every_persisted_capacity_dimension",
+ ordered_markers: &[
+ "raw_event_count=1",
+ "raw_tag_count=1",
+ "raw_event_bytes=1",
+ "raw_tag_bytes=1",
+ "RadrootsEventStore::open_file(&path).await",
+ "SourceCapacityStateDrift",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/source_maintenance_v1.rs",
+ test: "reopen_stops_at_the_first_raw_event_one_over_before_ephemeral_probe",
+ ordered_markers: &[
+ "value<25001",
+ "CASEWHENvalue=25001THEN20000ELSE1END",
+ "RadrootsEventStore::open_file(&path).await",
+ "SourceCapacityExceeded",
+ "current:25_000",
+ "requested:1",
+ "limit:25_000",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "ninth_current_v4_rebuild_is_typed_and_preflight_atomic",
+ ordered_markers: &[
+ "forordinalin2_u8..=8",
+ "assert_eq!(capacity_before.retained_generation_count(),8)",
+ "PanickingGeneration",
+ "expect_err(\"ninthrebuildmustfailbeforeentropyormutation\")",
+ "SourceGenerationHistoryLimitReached{current:8,limit:8,}",
+ "assert_eq!(source_authority_snapshot(&store).await,source_before)",
+ "assert_eq!(raw_authority_digest(&store).await,raw_before)",
+ "assert_eq!(normalized_nip09_snapshot(&store).await,nip09_before)",
+ "assert_eq!(food_after,food_before)",
+ "assert_eq!(derived_after,(derived_before.0,derived_before.1,derived_before.2,0))",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/source_maintenance_v1.rs",
+ test: "generation_sql_backstop_allows_exact_append_and_is_conflict_safe_one_over",
+ ordered_markers: &[
+ "retained_generation_count=retained_generation_limit-1",
+ "exactgenerationboundarymustappend",
+ "assert_eq!(retained_count,8)",
+ "sourcegenerationalreadyexists",
+ "uniquegenerationappendmusthittheSQLcapacitybackstop",
+ "sqlx::Error::Database",
+ "retainedsourcegenerationlimitreached",
+ "transaction.rollback().await",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "current_v4_rebuild_rotates_capacity_and_food_authority_end_to_end",
+ ordered_markers: &[
+ "apply_reconciliation_hook",
+ "after.retained_generation_count()",
+ "before.retained_generation_count()+1",
+ "assert_eq!(marker_count,0)",
+ "audit_food_availability_projection_v1",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/source_maintenance_v1.rs",
+ test: "marker_close_sql_backstop_rejects_each_required_seal_drift",
+ ordered_markers: &[
+ "rebuildmarkercannotclosebeforecapacity,NIP-09,andFoodAvailabilitysealsagree",
+ "fordriftin[\"capacity\",\"nip09\",\"food\",\"fts\"]",
+ "radroots_event_store_source_capacity_update_guard",
+ "raw_event_bytes=raw_event_bytes+1",
+ "radroots_event_store_addressable_feed_integrity_v1",
+ "last_transition_seq=last_transition_seq+1",
+ "radroots_event_store_food_availability_cursor_update_guard",
+ "projected_row_count=projected_row_count+1",
+ "radroots_event_store_food_availability_search_fts",
+ "DELETEFROMradroots_event_store_source_rebuild_marker",
+ "sqlx::Error::Database",
+ "database.message()==MARKER_CLOSE_ERROR",
+ "transaction.rollback().await",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "v4_marker_open_allows_repairing_prior_transition_high_water_drift",
+ ordered_markers: &[
+ "last_transition_seq=7",
+ "validate_schema_fingerprint",
+ "wrong_prior",
+ "wrong_floor",
+ "expect(\"derivedtransitiondriftisrepairableunderv4\")",
+ "repaired.0.as_slice(),target_generation.as_slice()",
+ "repaired.1,repaired.2",
+ "repaired.1,0",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "v4_food_reset_requires_marker_rotation_and_preserves_target_rows",
+ ordered_markers: &[
+ "expect_err(\"marker-freeFoodresetmustfail\")",
+ "expect_err(\"markeralonemustnotauthorizeFoodreset\")",
+ "expect(\"rotatesourcestate\")",
+ "expect(\"post-rotationhistoricalFoodreset\")",
+ "expect_err(\"activetarget-generationFoodrowsmustremainguarded\")",
+ "remaining,(1,1)",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "v4_down_restores_exact_predecessor_trigger_sql_and_fingerprint",
+ ordered_markers: &[
+ "&EVENT_STORE_MIGRATIONS[..3]",
+ "predecessor_sql",
+ "assert_ne!(schema_object_sql(&pool,name).await,predecessor_sql[*name])",
+ "rollback_event_store_schema_with_registry",
+ "assert_eq!(schema_object_sql(&pool,name).await,predecessor_sql[*name])",
+ "Managed{version:3}",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "open_file_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ ordered_markers: &[
+ "initialize_utf16le_database(&path).await",
+ "RadrootsEventStore::open_file(&path).await",
+ "SqliteMainDatabaseEncodingNotUtf8{actual}",
+ "actual==\"UTF-16le\"",
+ "assert_utf16le_database_was_not_mutated(&path).await",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "open_pool_rejects_utf16_main_database_before_schema_or_journal_mutation",
+ ordered_markers: &[
+ "initialize_utf16le_database(&path).await",
+ "max_connections(2)",
+ "RadrootsEventStore::open_pool(pool,true).await",
+ "SqliteMainDatabaseEncodingNotUtf8{actual}",
+ "actual==\"UTF-16le\"",
+ "assert_utf16le_database_was_not_mutated(&path).await",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "utf8_file_reopen_preserves_non_ascii_and_nul_capacity_accounting",
+ ordered_markers: &[
+ "letexpected_tag_count=",
+ "raw_source_text_bytes(&event)",
+ "expect(\"non-ASCIIandNULingest\")",
+ "before_reopen.raw_event_count(),1",
+ "before_reopen.raw_tag_count(),expected_tag_count",
+ "before_reopen.raw_event_text_bytes(),expected_event_bytes",
+ "before_reopen.raw_tag_text_bytes(),expected_tag_bytes",
+ "store.pool().close().await",
+ "RadrootsEventStore::open_file(&path).await",
+ "source_capacity_v1()",
+ "before_reopen",
+ "raw_event(&event_id)",
+ "tags_for_event(&event_id)",
+ "assert_eq!(tags.len(),2)",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "rollback_rejects_below_floor_ahead_unmanaged_and_generation_destructive_targets",
+ ordered_markers: &[
+ "lethistory_before:Vec<(Vec<u8>,i64)>=",
+ "rollback_event_store_schema_offline(&managed,1).await",
+ "RollbackWouldDiscardSourceGenerationHistory{current:RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,target:1,floor:2,}",
+ "inspect_event_store_schema_status(&managed).await",
+ "Managed{version:RADROOTS_EVENT_STORE_SCHEMA_VERSION_CURRENT,}",
+ "source-generationhistoryafterrejectedrollback",
+ "history_before",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/schema.rs",
+ test: "rollback_cannot_bypass_generation_history_guard_through_version_three",
+ ordered_markers: &[
+ "rollback_event_store_schema_offline(&managed,3).await",
+ "lethistory_before:Vec<(Vec<u8>,i64)>=",
+ "rollback_event_store_schema_offline(&managed,1).await",
+ "RollbackWouldDiscardSourceGenerationHistory{current:3,target:1,floor:2,}",
+ "inspect_event_store_schema_status(&managed).await",
+ "Managed{version:3}",
+ "v3source-generationhistoryafterrejectedbypass",
+ "history_before",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "independent_file_pools_serialize_the_last_raw_event_byte_capacity_slot",
+ ordered_markers: &[
+ "RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1",
+ "before_race.raw_event_text_bytes(),filler_target",
+ "Barrier::new(3)",
+ "tokio::spawn",
+ "tokio::spawn",
+ "tokio::join!",
+ "(Ok(accepted),Err(rejected))|(Err(rejected),Ok(accepted))",
+ "accepted.persistence.is_inserted()",
+ "SourceCapacityExceeded{resource:crate::RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes",
+ "requested==contender_bytes",
+ "cleanfullreopenafterlast-slotrace",
+ "after_race.raw_event_count(),filler_count+1",
+ "after_race.raw_event_text_bytes(),crate::RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1",
+ "assert_eq!(retained_contenders,1)",
+ ],
+ },
+];
+
+const MANDATORY_BOUND_AUTHORITIES: &[DelegatedAuthoritySpec] = &[
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/nip09/reconciliation_v1.rs",
+ test: "bounded_capacity_page_len_caps_gross_source_probe_at_one_over",
+ ordered_markers: &[
+ "forlimitin[25_000_u64,250_000_u64]",
+ "bounded_capacity_page_len(current,limit)",
+ "(1..=RECONCILIATION_SNAPSHOT_BATCH_LEN).contains(&fetched_len)",
+ "assert_eq!(fetched,limit+1)",
+ "bounded_capacity_page_len(24_576,25_000),(425,425)",
+ "bounded_capacity_page_len(249_856,250_000),(145,145)",
+ "bounded_capacity_page_len(25_000,25_000),(1,1)",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/source_maintenance_v1.rs",
+ test: "generation_append_limit_returns_the_typed_current_and_limit",
+ ordered_markers: &[
+ "validate_source_generation_append_available_v1(7,8)",
+ "validate_source_generation_append_available_v1(8,8)",
+ "SourceGenerationHistoryLimitReached{current:8,limit:8,}",
+ ],
+ },
+ DelegatedAuthoritySpec {
+ path: "crates/event_store/src/source_maintenance_v1.rs",
+ test: "retained_generation_nip09_logical_rows_have_an_audited_upper_bound",
+ ordered_markers: &[
+ "RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1",
+ "RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1",
+ "letper_generation=",
+ "4*events+2*tags+2",
+ "RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1",
+ "4_800_016",
+ "EVENT_STORE_MIGRATIONS",
+ ],
+ },
+];
+
+const MANDATORY_BOUND_HELPER_AUTHORITIES: &[DelegatedAuthoritySpec] = &[DelegatedAuthoritySpec {
+ path: "crates/event_store/src/store.rs",
+ test: "assert_utf16le_database_was_not_mutated",
+ ordered_markers: &[
+ "PRAGMAmain.encoding",
+ "PRAGMAmain.journal_mode",
+ "SELECTCOUNT(*)FROMmain.sqlite_schemaWHEREname='radroots_event_store_schema_migrations'ORname='event_envelopes'ORnameLIKE'radroots_event_store_%'",
+ "assert_eq!(encoding,\"UTF-16le\")",
+ "assert_eq!(journal_mode,\"delete\")",
+ "assert_eq!(event_store_objects,0)",
+ ],
+}];
+
+fn validate_delegated_test_authorities(
+ workspace_root: &Path,
+ vector: &SourceMaintenanceVector,
+) -> Result<(), String> {
+ let delegated = vector
+ .cases
+ .iter()
+ .filter(|case| case.execution != DIRECT_EXECUTOR)
+ .map(|case| (case.authority_path.as_str(), case.authority.as_str()))
+ .collect::<BTreeSet<_>>();
+ let expected = DELEGATED_AUTHORITIES
+ .iter()
+ .map(|spec| (spec.path, spec.test))
+ .collect::<BTreeSet<_>>();
+ if delegated != expected {
+ return Err(format!(
+ "SourceMaintenance delegated-test inventory differs: expected {expected:?}, found {delegated:?}"
+ ));
+ }
+ let mut executable_identities = Vec::new();
+ for spec in DELEGATED_AUTHORITIES
+ .iter()
+ .chain(MANDATORY_BOUND_AUTHORITIES)
+ {
+ executable_identities.push(ExecutableAuthorityIdentity {
+ path: spec.path.to_owned(),
+ test: spec.test.to_owned(),
+ tokens: validate_delegated_authority(workspace_root, *spec)?,
+ });
+ }
+ for spec in MANDATORY_BOUND_HELPER_AUTHORITIES {
+ executable_identities.push(ExecutableAuthorityIdentity {
+ path: spec.path.to_owned(),
+ test: spec.test.to_owned(),
+ tokens: validate_bound_function_authority(workspace_root, *spec)?,
+ });
+ }
+
+ let executor_source = rust_source(workspace_root, RESULT_VECTOR_EXECUTOR_RELATIVE)?;
+ let executor = syn::parse_file(&executor_source)
+ .map_err(|error| format!("parse {RESULT_VECTOR_EXECUTOR_RELATIVE}: {error}"))?;
+ let executor = exact_free_function(&executor, RESULT_VECTOR_EXECUTOR_TEST)?;
+ validate_executable_test_authority(
+ RESULT_VECTOR_EXECUTOR_RELATIVE,
+ RESULT_VECTOR_EXECUTOR_TEST,
+ executor,
+ )?;
+ let executor = compact_tokens(executor);
+ for case in vector
+ .cases
+ .iter()
+ .filter(|case| case.execution == DIRECT_EXECUTOR)
+ {
+ require_marker(
+ "SourceMaintenance direct result-vector executor",
+ &executor,
+ case.id.as_str(),
+ )?;
+ }
+ require_marker(
+ "SourceMaintenance direct result-vector executor",
+ &executor,
+ "assert_direct_cases_executed",
+ )?;
+ executable_identities.push(ExecutableAuthorityIdentity {
+ path: RESULT_VECTOR_EXECUTOR_RELATIVE.to_owned(),
+ test: RESULT_VECTOR_EXECUTOR_TEST.to_owned(),
+ tokens: executor,
+ });
+ validate_executable_authority_identities(&executable_identities)?;
+ let source_identities = bound_authority_source_identities(workspace_root)?;
+ validate_bound_authority_source_identities(&source_identities)?;
+ Ok(())
+}
+
+fn validate_delegated_authority(
+ workspace_root: &Path,
+ spec: DelegatedAuthoritySpec,
+) -> Result<String, String> {
+ let source = rust_source(workspace_root, spec.path)?;
+ let syntax = syn::parse_file(&source)
+ .map_err(|error| format!("parse delegated authority {}: {error}", spec.path))?;
+ let function = exact_free_function(&syntax, spec.test)?;
+ validate_executable_test_authority(spec.path, spec.test, function)?;
+ let function = compact_tokens(function);
+ require_ordered_markers(
+ &format!("delegated authority {}::{}", spec.path, spec.test),
+ &function,
+ spec.ordered_markers,
+ )?;
+ Ok(function)
+}
+
+fn validate_executable_authority_identities(
+ identities: &[ExecutableAuthorityIdentity],
+) -> Result<(), String> {
+ let bytes = canonical_json_bytes(&identities)?;
+ let actual = sha256_hex(&bytes);
+ if actual != EXECUTABLE_AUTHORITY_AST_SHA256 {
+ return Err(format!(
+ "SourceMaintenance executable direct/delegated authority AST identity drifted: expected {EXECUTABLE_AUTHORITY_AST_SHA256}, found {actual}"
+ ));
+ }
+ Ok(())
+}
+
+fn bound_authority_source_identities(
+ workspace_root: &Path,
+) -> Result<Vec<BoundAuthoritySourceIdentity>, String> {
+ let paths = DELEGATED_AUTHORITIES
+ .iter()
+ .chain(MANDATORY_BOUND_AUTHORITIES)
+ .chain(MANDATORY_BOUND_HELPER_AUTHORITIES)
+ .map(|spec| spec.path)
+ .chain([RESULT_VECTOR_EXECUTOR_RELATIVE])
+ .collect::<BTreeSet<_>>();
+ paths
+ .into_iter()
+ .map(|path| {
+ let source = rust_source(workspace_root, path)?;
+ let file = syn::parse_file(&source)
+ .map_err(|error| format!("parse bound authority source {path}: {error}"))?;
+ Ok(BoundAuthoritySourceIdentity {
+ path: path.to_owned(),
+ tokens: compact_tokens(&file),
+ })
+ })
+ .collect()
+}
+
+fn validate_bound_authority_source_identities(
+ identities: &[BoundAuthoritySourceIdentity],
+) -> Result<(), String> {
+ let bytes = canonical_json_bytes(&identities)?;
+ let actual = sha256_hex(&bytes);
+ if actual != BOUND_AUTHORITY_SOURCE_AST_SHA256 {
+ return Err(format!(
+ "SourceMaintenance bound executor/test-module/helper source AST identity drifted: expected {BOUND_AUTHORITY_SOURCE_AST_SHA256}, found {actual}"
+ ));
+ }
+ Ok(())
+}
+
+#[derive(Default)]
+struct EarlyReturnAudit {
+ count: usize,
+}
+
+impl<'ast> syn::visit::Visit<'ast> for EarlyReturnAudit {
+ fn visit_expr_return(&mut self, expression: &'ast syn::ExprReturn) {
+ self.count += 1;
+ syn::visit::visit_expr_return(self, expression);
+ }
+}
+
+fn validate_executable_test_authority(
+ relative: &str,
+ name: &str,
+ function: &syn::ItemFn,
+) -> Result<(), String> {
+ let expected_attribute = if function.sig.asyncness.is_some() {
+ "#[tokio::test]"
+ } else {
+ "#[test]"
+ };
+ let attributes = function
+ .attrs
+ .iter()
+ .map(compact_tokens)
+ .collect::<Vec<_>>();
+ if attributes != [expected_attribute] {
+ return Err(format!(
+ "executable test authority {relative}::{name} must have exactly `{expected_attribute}` and no disabling or conditional attributes; found {attributes:?}"
+ ));
+ }
+ if !matches!(function.vis, syn::Visibility::Inherited)
+ || function.sig.constness.is_some()
+ || function.sig.unsafety.is_some()
+ || function.sig.abi.is_some()
+ || !function.sig.inputs.is_empty()
+ || !function.sig.generics.params.is_empty()
+ || function.sig.generics.where_clause.is_some()
+ || !matches!(function.sig.output, syn::ReturnType::Default)
+ || function.sig.variadic.is_some()
+ {
+ return Err(format!(
+ "executable test authority {relative}::{name} must remain a private zero-argument test with no generic, ABI, unsafe, variadic, or return-type escape"
+ ));
+ }
+
+ use syn::visit::Visit;
+ let mut returns = EarlyReturnAudit::default();
+ returns.visit_block(&function.block);
+ if returns.count != 0 {
+ return Err(format!(
+ "executable test authority {relative}::{name} must not contain early return control flow; found {} return expression(s)",
+ returns.count
+ ));
+ }
+ Ok(())
+}
+
+fn validate_bound_function_authority(
+ workspace_root: &Path,
+ spec: DelegatedAuthoritySpec,
+) -> Result<String, String> {
+ let source = rust_source(workspace_root, spec.path)?;
+ let syntax = syn::parse_file(&source)
+ .map_err(|error| format!("parse bound authority {}: {error}", spec.path))?;
+ let function = exact_free_function_tokens(&syntax, spec.test)?;
+ require_ordered_markers(
+ &format!("bound authority {}::{}", spec.path, spec.test),
+ &function,
+ spec.ordered_markers,
+ )?;
+ Ok(function)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::path::PathBuf;
+
+ fn workspace_root() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .and_then(Path::parent)
+ .expect("xtask lives under tools/ in the workspace")
+ .to_path_buf()
+ }
+
+ fn public_api_sources(root: &Path) -> (String, String, String, String, String) {
+ (
+ rust_source(root, "crates/event_store/src/model.rs").expect("model source"),
+ rust_source(root, "crates/event_store/src/lib.rs").expect("lib source"),
+ rust_source(root, "crates/event_store/src/error.rs").expect("error source"),
+ rust_source(root, "crates/event_store/src/source_maintenance_v1.rs")
+ .expect("maintenance source"),
+ rust_source(root, "crates/event_store/src/store.rs").expect("store source"),
+ )
+ }
+
+ #[test]
+ fn source_inventory_excludes_outputs_and_exactly_supersedes_predecessors() {
+ let root = workspace_root();
+ validate_source_inventory().expect("source inventory");
+ validate_predecessor_production_source_coverage(&root)
+ .expect("exact predecessor supersession");
+
+ let source_paths = SOURCE_SPECS
+ .iter()
+ .map(|spec| spec.path)
+ .collect::<BTreeSet<_>>();
+ for generated in GENERATED_ARTIFACT_PATHS {
+ assert!(!source_paths.contains(generated));
+ }
+ assert_eq!(
+ PREDECESSOR_SUPERSEDED_SOURCE_PATHS
+ .iter()
+ .copied()
+ .collect::<BTreeSet<_>>()
+ .len(),
+ PREDECESSOR_SUPERSEDED_SOURCE_PATHS.len()
+ );
+ }
+
+ #[test]
+ fn public_surface_rejects_renames_and_retired_api_reintroduction() {
+ let root = workspace_root();
+ let (model, lib, error, maintenance, store) = public_api_sources(&root);
+ validate_public_api_sources(&model, &lib, &error, &maintenance, &store)
+ .expect("current public API");
+
+ let renamed_model = model.replacen(
+ "RadrootsAddressableTransitionCauseV1,",
+ "RadrootsAddressableTransitionCauseV1 as RenamedCause,",
+ 1,
+ );
+ let renamed_error =
+ validate_public_api_sources(&renamed_model, &lib, &error, &maintenance, &store)
+ .expect_err("renamed inherited symbol must fail");
+ assert!(renamed_error.contains("direct, non-renamed"));
+
+ let retired_type =
+ format!("{error}\npub struct RadrootsEventStoreReconciliationResource;\n");
+ let retired_type_error =
+ validate_public_api_sources(&model, &lib, &retired_type, &maintenance, &store)
+ .expect_err("retired public type must fail");
+ assert!(retired_type_error.contains("must remain absent"));
+
+ let retired_variant = error.replacen(
+ "pub enum RadrootsEventStoreError {",
+ "pub enum RadrootsEventStoreError {\n ReconciliationCapacityExceeded,",
+ 1,
+ );
+ let retired_variant_error =
+ validate_error_and_limit_source(&retired_variant).expect_err("retired variant");
+ assert!(retired_variant_error.contains("must remain absent"));
+ }
+
+ #[test]
+ fn manifest_schema_rejects_unknown_fields() {
+ let root = workspace_root();
+ let schema = manifest_schema();
+ let manifest_bytes =
+ read_regular_file(&root, MANIFEST_RELATIVE).expect("generated manifest bytes");
+ let mut manifest: Value =
+ serde_json::from_slice(&manifest_bytes).expect("generated manifest JSON");
+ validate_manifest_json_schema(&schema, &manifest).expect("current manifest schema");
+
+ manifest
+ .pointer_mut("/source_maintenance/reopen_validation")
+ .and_then(Value::as_object_mut)
+ .expect("reopen validation object")
+ .insert("unbounded_scan".to_owned(), Value::Bool(true));
+ let error = validate_manifest_json_schema(&schema, &manifest)
+ .expect_err("unknown reopen field must fail");
+ assert!(error.contains("violates"));
+ }
+
+ #[test]
+ fn generated_bundle_render_is_rerunnable_without_byte_drift() {
+ let root = workspace_root();
+ let before = expected_artifacts(&root)
+ .expect("first in-memory generated bundle render")
+ .into_iter()
+ .map(|artifact| (artifact.relative, artifact.contents))
+ .collect::<Vec<_>>();
+ let after = expected_artifacts(&root)
+ .expect("second in-memory generated bundle render")
+ .into_iter()
+ .map(|artifact| (artifact.relative, artifact.contents))
+ .collect::<Vec<_>>();
+ assert_eq!(before, after);
+ }
+
+ fn current_executable_authority_identities(root: &Path) -> Vec<ExecutableAuthorityIdentity> {
+ let mut identities = DELEGATED_AUTHORITIES
+ .iter()
+ .chain(MANDATORY_BOUND_AUTHORITIES)
+ .map(|spec| ExecutableAuthorityIdentity {
+ path: spec.path.to_owned(),
+ test: spec.test.to_owned(),
+ tokens: validate_delegated_authority(root, *spec)
+ .expect("current delegated executable authority"),
+ })
+ .collect::<Vec<_>>();
+ identities.extend(MANDATORY_BOUND_HELPER_AUTHORITIES.iter().map(|spec| {
+ ExecutableAuthorityIdentity {
+ path: spec.path.to_owned(),
+ test: spec.test.to_owned(),
+ tokens: validate_bound_function_authority(root, *spec)
+ .expect("current bound helper authority"),
+ }
+ }));
+ let source = rust_source(root, RESULT_VECTOR_EXECUTOR_RELATIVE)
+ .expect("direct result-vector executor source");
+ let file = syn::parse_file(&source).expect("direct result-vector executor AST");
+ let function = exact_free_function(&file, RESULT_VECTOR_EXECUTOR_TEST)
+ .expect("direct result-vector executor function");
+ validate_executable_test_authority(
+ RESULT_VECTOR_EXECUTOR_RELATIVE,
+ RESULT_VECTOR_EXECUTOR_TEST,
+ function,
+ )
+ .expect("current direct executable authority");
+ identities.push(ExecutableAuthorityIdentity {
+ path: RESULT_VECTOR_EXECUTOR_RELATIVE.to_owned(),
+ test: RESULT_VECTOR_EXECUTOR_TEST.to_owned(),
+ tokens: compact_tokens(function),
+ });
+ identities
+ }
+
+ fn assert_bound_source_mutation_rejected(
+ baseline: &[BoundAuthoritySourceIdentity],
+ path: &str,
+ source: &str,
+ label: &str,
+ ) {
+ let mut identities = baseline.to_vec();
+ let identity = identities
+ .iter_mut()
+ .find(|identity| identity.path == path)
+ .unwrap_or_else(|| panic!("missing bound source identity for {path}"));
+ let file = syn::parse_file(source)
+ .unwrap_or_else(|error| panic!("parse {label} mutation for {path}: {error}"));
+ let tokens = compact_tokens(&file);
+ assert_ne!(tokens, identity.tokens, "{label} fixture must mutate");
+ identity.tokens = tokens;
+ let error = validate_bound_authority_source_identities(&identities)
+ .expect_err("bound authority source-context drift must fail closed");
+ assert!(
+ error.contains("bound executor/test-module/helper source AST identity drifted"),
+ "unexpected {label} error: {error}"
+ );
+ }
+
+ #[test]
+ fn bound_executor_and_test_module_sources_are_exact_ast_authority() {
+ let root = workspace_root();
+ let baseline =
+ bound_authority_source_identities(&root).expect("current bound source identities");
+ validate_bound_authority_source_identities(&baseline)
+ .expect("current bound source aggregate identity");
+
+ let executor = rust_source(&root, RESULT_VECTOR_EXECUTOR_RELATIVE)
+ .expect("direct result-vector executor source");
+ let crate_disabled = executor.replacen(
+ "#![forbid(unsafe_code)]",
+ "#![forbid(unsafe_code)]\n#![cfg(any())]",
+ 1,
+ );
+ assert_bound_source_mutation_rejected(
+ &baseline,
+ RESULT_VECTOR_EXECUTOR_RELATIVE,
+ &crate_disabled,
+ "crate-disabled direct executor",
+ );
+
+ let delegated_path = "crates/event_store/src/source_maintenance_v1.rs";
+ let delegated =
+ rust_source(&root, delegated_path).expect("delegated authority module source");
+ let module_disabled = delegated.replacen(
+ "#[cfg(test)]\nmod tests {",
+ "#[cfg(all(test, any()))]\nmod tests {",
+ 1,
+ );
+ assert_bound_source_mutation_rejected(
+ &baseline,
+ delegated_path,
+ &module_disabled,
+ "disabled delegated test module",
+ );
+
+ let macro_shadowed = executor.replacen(
+ "#![forbid(unsafe_code)]",
+ "#![forbid(unsafe_code)]\nmacro_rules! assert_eq { ($($tokens:tt)*) => {}; }",
+ 1,
+ );
+ assert_bound_source_mutation_rejected(
+ &baseline,
+ RESULT_VECTOR_EXECUTOR_RELATIVE,
+ ¯o_shadowed,
+ "shadowing direct-executor assertion macro",
+ );
+ }
+
+ #[test]
+ fn executable_authority_rejects_disabled_missing_and_unreachable_tests() {
+ let root = workspace_root();
+ let source = rust_source(&root, RESULT_VECTOR_EXECUTOR_RELATIVE)
+ .expect("direct result-vector executor source");
+ for (label, mutation) in [
+ (
+ "ignored direct executor",
+ source.replacen("#[tokio::test]", "#[ignore]\n#[tokio::test]", 1),
+ ),
+ (
+ "missing direct executor attribute",
+ source.replacen("#[tokio::test]\n", "", 1),
+ ),
+ (
+ "early-returning direct executor",
+ source.replacen(
+ "async fn source_maintenance_v1_result_vector() {",
+ "async fn source_maintenance_v1_result_vector() {\n return;",
+ 1,
+ ),
+ ),
+ ] {
+ assert_ne!(mutation, source, "{label} fixture must mutate");
+ let file = syn::parse_file(&mutation).expect("mutated direct executor AST");
+ let function = exact_free_function(&file, RESULT_VECTOR_EXECUTOR_TEST)
+ .expect("mutated direct executor function");
+ let error = validate_executable_test_authority(
+ RESULT_VECTOR_EXECUTOR_RELATIVE,
+ RESULT_VECTOR_EXECUTOR_TEST,
+ function,
+ )
+ .expect_err("disabled or early-returning direct executor must fail closed");
+ assert!(
+ error.contains("executable test authority"),
+ "unexpected {label} error: {error}"
+ );
+ }
+
+ let mut identities = current_executable_authority_identities(&root);
+ validate_executable_authority_identities(&identities)
+ .expect("current aggregate executable identity");
+ let direct = identities
+ .last_mut()
+ .expect("direct executable identity is terminal");
+ let file = syn::parse_file(&source).expect("direct executor AST");
+ let function = exact_free_function(&file, RESULT_VECTOR_EXECUTOR_TEST)
+ .expect("direct executor function");
+ let mut function = function.clone();
+ let body = function.block.clone();
+ *function.block = syn::parse_quote!({ if false #body; });
+ direct.tokens = compact_tokens(&function);
+ let error = validate_executable_authority_identities(&identities)
+ .expect_err("non-returning unreachable test body must fail exact AST authority");
+ assert!(error.contains("executable direct/delegated authority AST identity drifted"));
+
+ let mut identities = current_executable_authority_identities(&root);
+ let helper_spec = MANDATORY_BOUND_HELPER_AUTHORITIES[0];
+ let helper_source = rust_source(&root, helper_spec.path).expect("bound helper source");
+ let helper_file = syn::parse_file(&helper_source).expect("bound helper AST");
+ let helper = exact_free_function(&helper_file, helper_spec.test).expect("bound helper");
+ let mut bypass = helper.clone();
+ let body = bypass.block.clone();
+ *bypass.block = syn::parse_quote!({ if false #body; });
+ let identity = identities
+ .iter_mut()
+ .find(|identity| identity.path == helper_spec.path && identity.test == helper_spec.test)
+ .expect("bound helper identity");
+ identity.tokens = compact_tokens(&bypass);
+ validate_executable_authority_identities(&identities)
+ .expect_err("unreachable bound helper body must fail exact AST authority");
+ }
+
+ #[test]
+ fn command_and_release_validation_reachability_is_exact() {
+ let root = workspace_root();
+ let contract =
+ rust_source(&root, CONTRACT_COMMAND_SOURCE_RELATIVE).expect("contract command source");
+ let main = rust_source(&root, XTASK_MAIN_SOURCE_RELATIVE).expect("xtask main source");
+ validate_contract_command_reachability_sources(&contract, &main)
+ .expect("current aggregate and release validation reachability");
+
+ let mutations = [
+ (
+ "aggregate removal",
+ contract.replacen(
+ " validate_source_maintenance_manifest(workspace_root)?;\n",
+ "",
+ 1,
+ ),
+ main.clone(),
+ ),
+ (
+ "aggregate discarded result",
+ contract.replacen(
+ " validate_source_maintenance_manifest(workspace_root)?;",
+ " let _ = validate_source_maintenance_manifest(workspace_root);",
+ 1,
+ ),
+ main.clone(),
+ ),
+ (
+ "aggregate reordering",
+ contract.replacen(
+ " validate_food_availability_projection_manifest(workspace_root)?;\n validate_source_maintenance_manifest(workspace_root)?;",
+ " validate_source_maintenance_manifest(workspace_root)?;\n validate_food_availability_projection_manifest(workspace_root)?;",
+ 1,
+ ),
+ main.clone(),
+ ),
+ (
+ "contract validation removal",
+ contract.clone(),
+ main.replacen(
+ " .and_then(|_| contract::validate_artifact_contracts(&root))",
+ " .map(|_| ())",
+ 1,
+ ),
+ ),
+ (
+ "contract validate dispatch bypass",
+ contract.clone(),
+ main.replacen(
+ " Some(\"validate\") => validate_contract(),",
+ " Some(\"validate\") => Ok(()),",
+ 1,
+ ),
+ ),
+ (
+ "release preflight dispatch bypass",
+ contract.clone(),
+ main.replacen(
+ " Some(\"preflight\") => release_preflight(),",
+ " Some(\"preflight\") => Ok(()),",
+ 1,
+ ),
+ ),
+ (
+ "release validation removal",
+ contract.clone(),
+ main.replacen(" contract::validate_artifact_contracts(root)?;\n", "", 1),
+ ),
+ (
+ "release validation discarded result",
+ contract.clone(),
+ main.replacen(
+ " contract::validate_artifact_contracts(root)?;",
+ " let _ = contract::validate_artifact_contracts(root);",
+ 1,
+ ),
+ ),
+ (
+ "release validation reordering",
+ contract.clone(),
+ main.replacen(
+ " dto_roots::check(root)?;\n contract::validate_artifact_contracts(root)?;",
+ " contract::validate_artifact_contracts(root)?;\n dto_roots::check(root)?;",
+ 1,
+ ),
+ ),
+ ];
+ for (label, contract_mutation, main_mutation) in mutations {
+ assert!(
+ contract_mutation != contract || main_mutation != main,
+ "{label} fixture must mutate"
+ );
+ let error =
+ validate_contract_command_reachability_sources(&contract_mutation, &main_mutation)
+ .expect_err("validation reachability drift must fail closed");
+ assert!(
+ error.contains("validation call-path authority drifted")
+ || error.contains("full dispatch AST authority drifted"),
+ "unexpected {label} error: {error}"
+ );
+ }
+ }
+
+ #[test]
+ fn capacity_snapshot_struct_and_public_accessors_are_exact_authority() {
+ let root = workspace_root();
+ let source = rust_source(&root, "crates/event_store/src/source_maintenance_v1.rs")
+ .expect("source-capacity snapshot authority");
+ let baseline = syn::parse_file(&source).expect("source-capacity snapshot AST");
+ validate_source_capacity_snapshot_authority(&baseline)
+ .expect("current source-capacity snapshot authority");
+
+ for (label, needle, replacement) in [
+ (
+ "snapshot derive",
+ "#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub struct RadrootsEventStoreSourceCapacityV1",
+ "#[derive(Clone, Debug, PartialEq, Eq)]\npub struct RadrootsEventStoreSourceCapacityV1",
+ ),
+ (
+ "private field type",
+ " raw_high_water_seq: i64,\n retained_generation_count: u32,",
+ " raw_high_water_seq: u64,\n retained_generation_count: u32,",
+ ),
+ (
+ "public accessor signature",
+ " pub const fn raw_event_count(&self) -> u64 {",
+ " pub fn raw_event_count(&mut self) -> u64 {",
+ ),
+ (
+ "public accessor body",
+ " pub const fn raw_event_count(&self) -> u64 {\n self.capacity.raw_events\n }",
+ " pub const fn raw_event_count(&self) -> u64 {\n self.capacity.raw_tags\n }",
+ ),
+ ] {
+ let mutation = source.replacen(needle, replacement, 1);
+ assert_ne!(mutation, source, "{label} fixture must mutate");
+ let file = syn::parse_file(&mutation).expect("mutated source-capacity snapshot AST");
+ let error = validate_source_capacity_snapshot_authority(&file)
+ .expect_err("source-capacity snapshot drift must fail closed");
+ assert!(
+ error.contains("RadrootsEventStoreSourceCapacityV1"),
+ "unexpected {label} error: {error}"
+ );
+ }
+ }
+
+ #[test]
+ fn capacity_resource_and_all_typed_errors_are_exact_authority() {
+ let root = workspace_root();
+ let source = rust_source(&root, "crates/event_store/src/error.rs")
+ .expect("event-store error authority");
+ validate_error_and_limit_source(&source).expect("current capacity/error authority");
+ for (label, needle, replacement) in [
+ (
+ "conditional duplicate capacity constant",
+ "pub const RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1: u64 = 25_000;",
+ "#[cfg(any())]\npub const RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1: u64 = 25_000;\n#[cfg(not(any()))]\npub const RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1: u64 = 1;",
+ ),
+ (
+ "conditional duplicate error enum",
+ "#[derive(Debug, thiserror::Error)]\npub enum RadrootsEventStoreError {",
+ "#[cfg(any())]\npub enum RadrootsEventStoreError {}\n\n#[derive(Debug, thiserror::Error)]\npub enum RadrootsEventStoreError {",
+ ),
+ (
+ "capacity constant value",
+ "pub const RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1: u64 = 250_000;",
+ "pub const RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1: u64 = 249_999;",
+ ),
+ ("resource non-exhaustive", "#[non_exhaustive]", ""),
+ ("resource variant", " RawTagBytes,", " TagBytes,"),
+ ("resource label", "\"raw tag count\"", "\"raw tags\""),
+ (
+ "capacity requested type",
+ " requested: u64,\n limit: u64,",
+ " requested: u32,\n limit: u64,",
+ ),
+ (
+ "generation history type",
+ " SourceGenerationHistoryLimitReached { current: u32, limit: u32 },",
+ " SourceGenerationHistoryLimitReached { current: u64, limit: u32 },",
+ ),
+ (
+ "ephemeral kind type",
+ " PersistedEphemeralRawEvent { event_id: String, kind: i64 },",
+ " PersistedEphemeralRawEvent { event_id: String, kind: u64 },",
+ ),
+ (
+ "capacity drift reason type",
+ " SourceCapacityStateDrift { reason: String },",
+ " SourceCapacityStateDrift { reason: &'static str },",
+ ),
+ (
+ "UTF-8 diagnostic type",
+ " SqliteMainDatabaseEncodingNotUtf8 { actual: String },",
+ " SqliteMainDatabaseEncodingNotUtf8 { actual: &'static str },",
+ ),
+ (
+ "rollback floor type",
+ " floor: u32,\n },\n #[error(\"event-store rollback requires a managed schema\")]",
+ " floor: u64,\n },\n #[error(\"event-store rollback requires a managed schema\")]",
+ ),
+ (
+ "capacity error display",
+ "requested additional {requested}",
+ "requested {requested}",
+ ),
+ ] {
+ let mutation = source.replacen(needle, replacement, 1);
+ assert_ne!(mutation, source, "{label} fixture must mutate");
+ let error = validate_error_and_limit_source(&mutation)
+ .expect_err("typed capacity/error authority drift must fail closed");
+ assert!(!error.is_empty(), "{label} must return a diagnostic");
+ }
+ }
+}
diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs
@@ -20,6 +20,7 @@ fn usage() {
eprintln!(" cargo xtask contract event-contract-registry-v7 [--write]");
eprintln!(" cargo xtask contract nip09-reconciliation-manifest [--write]");
eprintln!(" cargo xtask contract food-availability-projection-manifest [--write]");
+ eprintln!(" cargo xtask contract source-maintenance-manifest [--write]");
eprintln!(" cargo xtask contract knowledge-manifest [--write]");
eprintln!(" cargo xtask dto-roots --check|--write");
eprintln!(" cargo xtask release preflight");
@@ -64,10 +65,7 @@ fn validate_contract() -> Result<(), String> {
contract::load_contract_bundle(&root)
.and_then(|bundle| contract::validate_contract_bundle(&bundle))
.and_then(|_| contract::validate_canonical_event_boundary(&root))
- .and_then(|_| contract::validate_event_contract_registry_v7_inventory(&root))
- .and_then(|_| contract::validate_nip09_reconciliation_manifest(&root))
- .and_then(|_| contract::validate_food_availability_projection_manifest(&root))
- .and_then(|_| contract::validate_knowledge_contract_manifest(&root))
+ .and_then(|_| contract::validate_artifact_contracts(&root))
}
#[cfg_attr(coverage_nightly, coverage(off))]
@@ -78,6 +76,7 @@ fn release_preflight() -> Result<(), String> {
fn release_preflight_at(root: &Path) -> Result<(), String> {
dto_roots::check(root)?;
+ contract::validate_artifact_contracts(root)?;
contract::validate_release_preflight(root)
}
@@ -120,6 +119,15 @@ fn run_contract(args: &[String]) -> Result<(), String> {
.to_string(),
),
},
+ Some("source-maintenance-manifest") => match &args[1..] {
+ [] => contract::validate_source_maintenance_manifest(&workspace_root()),
+ [flag] if flag == "--write" => {
+ contract::write_source_maintenance_manifest(&workspace_root())
+ }
+ _ => Err(
+ "source-maintenance-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())
@@ -234,6 +242,12 @@ mod tests {
])
.expect_err("invalid FoodAvailability projection manifest mode");
assert!(invalid_food.contains("exactly --write"));
+ let invalid_source_maintenance = run_contract(&[
+ "source-maintenance-manifest".to_string(),
+ "--invalid".to_string(),
+ ])
+ .expect_err("invalid SourceMaintenance manifest mode");
+ assert!(invalid_source_maintenance.contains("exactly --write"));
let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command");
assert!(unknown_root.contains("unknown command"));
@@ -331,6 +345,8 @@ mod tests {
.expect("contract NIP-09 reconciliation manifest");
run_contract(&["food-availability-projection-manifest".to_string()])
.expect("contract FoodAvailability projection manifest");
+ run_contract(&["source-maintenance-manifest".to_string()])
+ .expect("contract SourceMaintenance manifest");
run_contract(&["knowledge-manifest".to_string()]).expect("contract knowledge manifest");
}
}