commit 13e3d81d9f58f4aa2a161e71cdd8b66dfdc164e0 parent 7b459b637bd1b3c7132737c44380e0125798bc82 Author: triesap <tyson@radroots.org> Date: Mon, 27 Jul 2026 11:33:28 +0000 architecture: enforce package dependency direction - encode the synchronized seven-tier public package policy - lint Cargo-resolved identities across features targets and dependency kinds - cover forbidden upward edges and valid downward fixtures - refresh governed xtask dispatch and source-maintenance authority Diffstat:
12 files changed, 1004 insertions(+), 7 deletions(-)
diff --git a/contracts/releases/package_tiers.toml b/contracts/releases/package_tiers.toml @@ -0,0 +1,78 @@ +schema_version = 1 +spec_id = "radroots.crates.release.v1" +direction = "same_or_lower" +enforced_dependency_kinds = ["normal", "dev", "build"] + +[[tier]] +id = "foundation" +rank = 0 +packages = [ + "radroots-core", + "radroots-identity", + "radroots-blossom", + "radroots-protocol", +] + +[[tier]] +id = "domain" +rank = 1 +packages = ["radroots-event", "radroots-event-codec", "radroots-trade"] + +[[tier]] +id = "spi" +rank = 2 +packages = [ + "radroots-signing", + "radroots-transport", + "radroots-secrets", + "radroots-storage", +] + +[[tier]] +id = "adapter" +rank = 3 +packages = [ + "radroots-nostr", + "radroots-nostr-connect", + "radroots-storage-sqlite", + "radroots-transport-nostr", + "radroots-geonames", +] + +[[tier]] +id = "orchestration" +rank = 4 +packages = ["radroots-sync"] + +[[tier]] +id = "sdk" +rank = 5 +packages = ["radroots-sdk"] + +[[tier]] +id = "facade" +rank = 6 +packages = ["radroots"] + +# These exact development-only edges predate the final trade boundary and are +# removed by the manifest-alignment checkpoint. Normal, build, target, or +# feature-broadened forms remain forbidden. +[[temporary_exception]] +owner = "radroots-trade" +dependency = "radroots-nostr" +kind = "dev" +target = "all" +features = ["events", "std"] +uses_default_features = false +removal_step = 88 +rationale = "Existing trade conformance tests consume Nostr vectors until the Step 088 package boundary alignment." + +[[temporary_exception]] +owner = "radroots-trade" +dependency = "radroots-transport" +kind = "dev" +target = "all" +features = [] +uses_default_features = false +removal_step = 88 +rationale = "Existing trade workflow tests use transport test types until the Step 088 package boundary alignment." diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.json b/crates/event_store/contracts/source_maintenance_v1.manifest.json @@ -251,7 +251,7 @@ "role": "source_maintenance_governance", "path": "tools/xtask/src/contract/source_maintenance.rs", "byte_length": 176811, - "sha256": "cc6125aaeba8d7dab83c2413ca0f1a80d6f9f26876f0117e4c8fceae156304e5", + "sha256": "cff3fabf05989de7d6420720e493965ff7aa71d079babe7a9565492f54c76630", "hash_algorithm": "sha256_bytes_v1" }, { @@ -264,8 +264,8 @@ { "role": "xtask_dispatch_and_release_preflight", "path": "tools/xtask/src/main.rs", - "byte_length": 14364, - "sha256": "27096ad2c226c6402313621712db6af23a4fdda5117c3e732cf3a3fdf3d9b434", + "byte_length": 14578, + "sha256": "b33b9139db58a89adaf21448b0f0de1f5b23ce0af93378b05b59eb01d5069b41", "hash_algorithm": "sha256_bytes_v1" } ], diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 b/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 @@ -1 +1 @@ -ffb6ec82c763730a69d24ee8e8194e3e0911ee590df58e5fe78a23ae68122652 +a3e74030495447a44574a0e688ae3dbb4261d97be9b5e6913fbf807c33147643 diff --git a/crates/event_store/src/generated/source_maintenance_manifest.rs b/crates/event_store/src/generated/source_maintenance_manifest.rs @@ -1,8 +1,8 @@ // @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\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 11546,\n \"sha256\": \"0378afe281a46e02e83e128324efefc3da950abf3e1d3d06c75b628f69810bd2\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\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\": 195036,\n \"sha256\": \"d3c567ea3b4ea709723af2d5b6a2dc47993c6ba42c1d62895bd26fa101ebf829\",\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\": 835585,\n \"sha256\": \"51d524cb00d2d96dc0da35fc9133c54f2e5328a830d56b8d5e83a7f1e9654ddc\",\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\": 176811,\n \"sha256\": \"cc6125aaeba8d7dab83c2413ca0f1a80d6f9f26876f0117e4c8fceae156304e5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 500480,\n \"sha256\": \"84739ad8b78bae49ea1f992672101bd83135415042cb22a85dbcbf5c878fc097\",\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\": 14364,\n \"sha256\": \"27096ad2c226c6402313621712db6af23a4fdda5117c3e732cf3a3fdf3d9b434\",\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_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\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 11546,\n \"sha256\": \"0378afe281a46e02e83e128324efefc3da950abf3e1d3d06c75b628f69810bd2\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\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\": 195036,\n \"sha256\": \"d3c567ea3b4ea709723af2d5b6a2dc47993c6ba42c1d62895bd26fa101ebf829\",\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\": 835585,\n \"sha256\": \"51d524cb00d2d96dc0da35fc9133c54f2e5328a830d56b8d5e83a7f1e9654ddc\",\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\": 176811,\n \"sha256\": \"cff3fabf05989de7d6420720e493965ff7aa71d079babe7a9565492f54c76630\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 500480,\n \"sha256\": \"84739ad8b78bae49ea1f992672101bd83135415042cb22a85dbcbf5c878fc097\",\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\": 14578,\n \"sha256\": \"b33b9139db58a89adaf21448b0f0de1f5b23ce0af93378b05b59eb01d5069b41\",\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 = 14459; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SHA256: &str = - "ffb6ec82c763730a69d24ee8e8194e3e0911ee590df58e5fe78a23ae68122652"; + "a3e74030495447a44574a0e688ae3dbb4261d97be9b5e6913fbf807c33147643"; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION: u32 = 1; pub(crate) const SOURCE_MAINTENANCE_CONTRACT_ID: &str = "radroots_event_store.source_maintenance_v1"; diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs @@ -6,6 +6,8 @@ use std::{ use serde::Deserialize; +mod dependency_boundary; + const DEVIATIONS_RELATIVE: &str = "docs/implementation/deviations.toml"; const ARCHITECTURE_RELATIVE: &str = "docs/specs/radroots_crates_release_v1.toml"; const ARCHITECTURE_ID: &str = "radroots.crates.release.v1"; @@ -151,6 +153,12 @@ pub fn validate(workspace_root: &Path) -> Result<(), String> { let architecture = toml::from_str::<ArchitectureIdentity>(&architecture_raw) .map_err(|error| format!("parse {}: {error}", architecture_path.display()))?; + let architecture_packages = architecture + .package + .iter() + .map(|package| package.name.clone()) + .collect::<BTreeSet<_>>(); + dependency_boundary::validate_policy_catalog(workspace_root, &architecture_packages)?; validate_workspace_toolchain(workspace_root, &architecture)?; validate_public_package_metadata(workspace_root, &architecture)?; validate_no_production_sibling_paths(workspace_root)?; @@ -162,6 +170,11 @@ pub fn validate(workspace_root: &Path) -> Result<(), String> { validate_ledger(workspace_root, &architecture.spec_id, &ledger_raw) } +pub fn validate_dependency_boundaries(workspace_root: &Path) -> Result<(), String> { + validate(workspace_root)?; + dependency_boundary::validate_resolved_boundaries(workspace_root) +} + fn validate_workspace_toolchain( workspace_root: &Path, architecture: &ArchitectureIdentity, diff --git a/tools/xtask/src/architecture/dependency_boundary.rs b/tools/xtask/src/architecture/dependency_boundary.rs @@ -0,0 +1,718 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::Path, + process::Command, +}; + +use serde::Deserialize; + +const PACKAGE_TIERS_RELATIVE: &str = "contracts/releases/package_tiers.toml"; +const SPEC_ID: &str = "radroots.crates.release.v1"; +const DIRECTION: &str = "same_or_lower"; +const POLICY_SCHEMA_VERSION: u16 = 1; +const CURRENT_STEP: u16 = 24; +const EXPECTED_TIERS: &[(&str, u8)] = &[ + ("foundation", 0), + ("domain", 1), + ("spi", 2), + ("adapter", 3), + ("orchestration", 4), + ("sdk", 5), + ("facade", 6), +]; +const EXPECTED_DEPENDENCY_KINDS: &[&str] = &["build", "dev", "normal"]; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DependencyBoundaryPolicy { + schema_version: u16, + spec_id: String, + direction: String, + enforced_dependency_kinds: Vec<String>, + tier: Vec<PackageTier>, + #[serde(default)] + temporary_exception: Vec<TemporaryException>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct PackageTier { + id: String, + rank: u8, + packages: Vec<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct TemporaryException { + owner: String, + dependency: String, + kind: String, + target: String, + features: Vec<String>, + uses_default_features: bool, + removal_step: u16, + rationale: String, +} + +#[derive(Debug, Deserialize)] +struct CargoMetadata { + packages: Vec<CargoPackage>, + resolve: Option<CargoResolve>, +} + +#[derive(Debug, Deserialize)] +struct CargoPackage { + id: String, + name: String, + #[serde(default)] + dependencies: Vec<CargoDependency>, +} + +#[derive(Debug, Deserialize)] +struct CargoDependency { + name: String, + #[serde(default)] + kind: Option<String>, + #[serde(default)] + features: Vec<String>, + #[serde(default)] + target: Option<String>, + #[serde(default = "default_true")] + uses_default_features: bool, +} + +#[derive(Debug, Deserialize)] +struct CargoResolve { + nodes: Vec<CargoNode>, +} + +#[derive(Debug, Deserialize)] +struct CargoNode { + id: String, + #[serde(default)] + deps: Vec<CargoNodeDependency>, +} + +#[derive(Debug, Deserialize)] +struct CargoNodeDependency { + name: String, + pkg: String, + #[serde(default)] + dep_kinds: Vec<CargoDependencyKind>, +} + +#[derive(Debug, Deserialize)] +struct CargoDependencyKind { + #[serde(default)] + kind: Option<String>, + #[serde(default)] + target: Option<String>, +} + +#[derive(Clone, Copy)] +struct PackagePlacement<'a> { + tier: &'a str, + rank: u8, +} + +fn default_true() -> bool { + true +} + +pub(super) fn validate_policy_catalog( + workspace_root: &Path, + expected_packages: &BTreeSet<String>, +) -> Result<(), String> { + let policy = load_policy(workspace_root)?; + validate_policy(&policy, expected_packages).map(|_| ()) +} + +pub(super) fn validate_resolved_boundaries(workspace_root: &Path) -> Result<(), String> { + let policy = load_policy(workspace_root)?; + let expected_packages = policy + .tier + .iter() + .flat_map(|tier| tier.packages.iter().cloned()) + .collect::<BTreeSet<_>>(); + let placements = validate_policy(&policy, &expected_packages)?; + let metadata = load_metadata(workspace_root)?; + validate_metadata(&policy, &placements, &metadata) +} + +fn load_policy(workspace_root: &Path) -> Result<DependencyBoundaryPolicy, String> { + let path = workspace_root.join(PACKAGE_TIERS_RELATIVE); + let raw = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("parse {}: {error}", path.display())) +} + +fn validate_policy<'a>( + policy: &'a DependencyBoundaryPolicy, + expected_packages: &BTreeSet<String>, +) -> Result<BTreeMap<&'a str, PackagePlacement<'a>>, String> { + if policy.schema_version != POLICY_SCHEMA_VERSION { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} schema_version must be {POLICY_SCHEMA_VERSION}" + )); + } + if policy.spec_id != SPEC_ID { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} spec_id must be {SPEC_ID}" + )); + } + if policy.direction != DIRECTION { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} direction must be {DIRECTION}" + )); + } + + let actual_kinds = unique_strings( + "enforced_dependency_kinds", + &policy.enforced_dependency_kinds, + )?; + let expected_kinds = EXPECTED_DEPENDENCY_KINDS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + if actual_kinds != expected_kinds { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} enforced_dependency_kinds must be build, dev, normal" + )); + } + + let actual_tiers = policy + .tier + .iter() + .map(|tier| (tier.id.as_str(), tier.rank)) + .collect::<Vec<_>>(); + if actual_tiers != EXPECTED_TIERS { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} tiers must match the approved ordered architecture" + )); + } + + let mut placements = BTreeMap::new(); + for tier in &policy.tier { + if tier.packages.is_empty() { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} tier {} must allocate at least one package", + tier.id + )); + } + for package in &tier.packages { + if placements + .insert( + package.as_str(), + PackagePlacement { + tier: &tier.id, + rank: tier.rank, + }, + ) + .is_some() + { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} package {package} is allocated more than once" + )); + } + } + } + + let actual_packages = placements + .keys() + .map(|package| (*package).to_owned()) + .collect::<BTreeSet<_>>(); + if &actual_packages != expected_packages { + return Err(package_set_mismatch(expected_packages, &actual_packages)); + } + + let mut exception_keys = BTreeSet::new(); + for exception in &policy.temporary_exception { + let owner = placements.get(exception.owner.as_str()).ok_or_else(|| { + format!( + "{PACKAGE_TIERS_RELATIVE} exception owner {} is not an approved package", + exception.owner + ) + })?; + let dependency = placements + .get(exception.dependency.as_str()) + .ok_or_else(|| { + format!( + "{PACKAGE_TIERS_RELATIVE} exception dependency {} is not an approved package", + exception.dependency + ) + })?; + if dependency.rank <= owner.rank { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} does not describe an upward edge", + exception.owner, exception.dependency + )); + } + if !expected_kinds.contains(exception.kind.as_str()) { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} uses unknown dependency kind {}", + exception.owner, exception.dependency, exception.kind + )); + } + if exception.target.trim().is_empty() { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} must name its exact target", + exception.owner, exception.dependency + )); + } + let normalized_features = normalized_features(&exception.features); + if normalized_features.len() != exception.features.len() + || normalized_features.iter().ne(exception.features.iter()) + { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} features must be sorted and unique", + exception.owner, exception.dependency + )); + } + if exception.removal_step <= CURRENT_STEP { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} expired at Step {}", + exception.owner, exception.dependency, exception.removal_step + )); + } + if exception.rationale.trim().is_empty() { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} must include a rationale", + exception.owner, exception.dependency + )); + } + let key = ( + exception.owner.as_str(), + exception.dependency.as_str(), + exception.kind.as_str(), + exception.target.as_str(), + exception.features.clone(), + exception.uses_default_features, + ); + if !exception_keys.insert(key) { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} contains a duplicate exception for {} -> {}", + exception.owner, exception.dependency + )); + } + } + + Ok(placements) +} + +fn unique_strings<'a>(label: &str, values: &'a [String]) -> Result<BTreeSet<&'a str>, String> { + let unique = values.iter().map(String::as_str).collect::<BTreeSet<_>>(); + if unique.len() != values.len() { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} {label} must not contain duplicates" + )); + } + Ok(unique) +} + +fn package_set_mismatch(expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> String { + let missing = expected + .difference(actual) + .cloned() + .collect::<Vec<_>>() + .join(", "); + let extra = actual + .difference(expected) + .cloned() + .collect::<Vec<_>>() + .join(", "); + format!("{PACKAGE_TIERS_RELATIVE} package catalog mismatch; missing: {missing}; extra: {extra}") +} + +fn load_metadata(workspace_root: &Path) -> Result<CargoMetadata, String> { + let output = Command::new("cargo") + .args([ + "metadata", + "--format-version", + "1", + "--locked", + "--all-features", + ]) + .current_dir(workspace_root) + .output() + .map_err(|error| format!("run cargo metadata: {error}"))?; + if !output.status.success() { + return Err(format!( + "cargo metadata failed while checking dependency boundaries: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + serde_json::from_slice(&output.stdout) + .map_err(|error| format!("parse cargo metadata for dependency boundaries: {error}")) +} + +fn validate_metadata( + policy: &DependencyBoundaryPolicy, + placements: &BTreeMap<&str, PackagePlacement<'_>>, + metadata: &CargoMetadata, +) -> Result<(), String> { + let resolve = metadata + .resolve + .as_ref() + .ok_or_else(|| "cargo metadata did not include a resolved dependency graph".to_owned())?; + let mut packages_by_id = BTreeMap::new(); + for package in &metadata.packages { + if packages_by_id + .insert(package.id.as_str(), package) + .is_some() + { + return Err(format!( + "cargo metadata contains duplicate package id {}", + package.id + )); + } + } + + let mut nodes_by_id = BTreeMap::new(); + for node in &resolve.nodes { + if nodes_by_id.insert(node.id.as_str(), node).is_some() { + return Err(format!( + "cargo metadata contains duplicate resolve node {}", + node.id + )); + } + } + + let enforced_kinds = policy + .enforced_dependency_kinds + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let mut violations = Vec::new(); + + for node in &resolve.nodes { + let Some(owner_package) = packages_by_id.get(node.id.as_str()).copied() else { + return Err(format!( + "cargo metadata resolve node {} has no package record", + node.id + )); + }; + let Some(owner_placement) = placements.get(owner_package.name.as_str()).copied() else { + continue; + }; + + for resolved_dependency in &node.deps { + let dependency_package = packages_by_id + .get(resolved_dependency.pkg.as_str()) + .copied() + .ok_or_else(|| { + format!( + "cargo metadata resolved dependency {} has no package record", + resolved_dependency.pkg + ) + })?; + let Some(dependency_placement) = + placements.get(dependency_package.name.as_str()).copied() + else { + continue; + }; + if resolved_dependency.dep_kinds.is_empty() { + return Err(format!( + "cargo metadata edge {} -> {} has no dependency kind", + owner_package.name, dependency_package.name + )); + } + + for dependency_kind in &resolved_dependency.dep_kinds { + let kind = normalized_kind(dependency_kind.kind.as_deref()); + if !enforced_kinds.contains(kind) { + continue; + } + let declaration = + resolve_declaration(owner_package, dependency_package, dependency_kind)?; + if dependency_placement.rank <= owner_placement.rank { + continue; + } + if exception_matches( + &policy.temporary_exception, + owner_package, + dependency_package, + dependency_kind, + declaration, + ) { + continue; + } + violations.push(format_violation( + owner_package, + owner_placement, + dependency_package, + dependency_placement, + resolved_dependency, + dependency_kind, + declaration, + )); + } + } + } + + if violations.is_empty() { + Ok(()) + } else { + violations.sort(); + Err(format!( + "forbidden package dependency direction(s):\n{}", + violations.join("\n") + )) + } +} + +fn resolve_declaration<'a>( + owner: &'a CargoPackage, + dependency: &CargoPackage, + dependency_kind: &CargoDependencyKind, +) -> Result<&'a CargoDependency, String> { + let kind = normalized_kind(dependency_kind.kind.as_deref()); + let candidates = owner + .dependencies + .iter() + .filter(|candidate| { + candidate.name == dependency.name + && normalized_kind(candidate.kind.as_deref()) == kind + && candidate.target == dependency_kind.target + }) + .collect::<Vec<_>>(); + match candidates.as_slice() { + [declaration] => Ok(*declaration), + [] => Err(format!( + "cargo metadata edge {} -> {} kind={kind} target={} has no matching package dependency declaration", + owner.name, + dependency.name, + target_label(dependency_kind.target.as_deref()) + )), + _ => Err(format!( + "cargo metadata edge {} -> {} kind={kind} target={} has ambiguous package dependency declarations", + owner.name, + dependency.name, + target_label(dependency_kind.target.as_deref()) + )), + } +} + +fn exception_matches( + exceptions: &[TemporaryException], + owner: &CargoPackage, + dependency: &CargoPackage, + dependency_kind: &CargoDependencyKind, + declaration: &CargoDependency, +) -> bool { + let kind = normalized_kind(dependency_kind.kind.as_deref()); + let target = target_label(dependency_kind.target.as_deref()); + let features = normalized_features(&declaration.features); + exceptions.iter().any(|exception| { + exception.owner == owner.name + && exception.dependency == dependency.name + && exception.kind == kind + && exception.target == target + && exception.features == features + && exception.uses_default_features == declaration.uses_default_features + }) +} + +fn format_violation( + owner: &CargoPackage, + owner_placement: PackagePlacement<'_>, + dependency: &CargoPackage, + dependency_placement: PackagePlacement<'_>, + resolved_dependency: &CargoNodeDependency, + dependency_kind: &CargoDependencyKind, + declaration: &CargoDependency, +) -> String { + let features = normalized_features(&declaration.features); + format!( + "{} (tier={} rank={}) -> {} (tier={} rank={}); owner_id={}; dependency_id={}; alias={}; kind={}; target={}; features=[{}]; default_features={}", + owner.name, + owner_placement.tier, + owner_placement.rank, + dependency.name, + dependency_placement.tier, + dependency_placement.rank, + owner.id, + dependency.id, + resolved_dependency.name, + normalized_kind(dependency_kind.kind.as_deref()), + target_label(dependency_kind.target.as_deref()), + features.join(","), + declaration.uses_default_features, + ) +} + +fn normalized_kind(kind: Option<&str>) -> &str { + kind.unwrap_or("normal") +} + +fn target_label(target: Option<&str>) -> &str { + target.unwrap_or("all") +} + +fn normalized_features(features: &[String]) -> Vec<String> { + features + .iter() + .cloned() + .collect::<BTreeSet<_>>() + .into_iter() + .collect() +} + +#[cfg(test)] +mod tests { + use super::{ + CargoMetadata, DependencyBoundaryPolicy, exception_matches, validate_metadata, + validate_policy, + }; + use serde::Deserialize; + use std::collections::BTreeSet; + + const POLICY: &str = include_str!("../../../../contracts/releases/package_tiers.toml"); + const ARCHITECTURE: &str = + include_str!("../../../../docs/specs/radroots_crates_release_v1.toml"); + const DOMAIN_TO_STORAGE: &str = + include_str!("../../tests/fixtures/dependency-boundaries/domain-to-storage.json"); + const SPI_TO_ADAPTER: &str = + include_str!("../../tests/fixtures/dependency-boundaries/spi-to-adapter.json"); + const SERVICE_TO_SDK: &str = + include_str!("../../tests/fixtures/dependency-boundaries/service-to-sdk.json"); + const VALID_DOWNWARD: &str = + include_str!("../../tests/fixtures/dependency-boundaries/valid-downward.json"); + + #[derive(Deserialize)] + struct ArchitectureCatalog { + package: Vec<ArchitecturePackage>, + } + + #[derive(Deserialize)] + struct ArchitecturePackage { + name: String, + } + + fn policy() -> DependencyBoundaryPolicy { + toml::from_str(POLICY).expect("package tier policy") + } + + fn metadata(raw: &str) -> CargoMetadata { + serde_json::from_str(raw).expect("Cargo metadata fixture") + } + + fn placements<'a>( + policy: &'a DependencyBoundaryPolicy, + ) -> std::collections::BTreeMap<&'a str, super::PackagePlacement<'a>> { + let architecture = + toml::from_str::<ArchitectureCatalog>(ARCHITECTURE).expect("architecture catalog"); + let expected = architecture + .package + .into_iter() + .map(|package| package.name) + .collect::<BTreeSet<_>>(); + validate_policy(policy, &expected).expect("valid package tier policy") + } + + #[test] + fn policy_covers_the_exact_architecture_catalog() { + let policy = policy(); + let placements = placements(&policy); + assert_eq!(placements.len(), 19); + } + + #[test] + fn domain_to_storage_reports_resolved_alias_kind_target_and_features() { + let policy = policy(); + let error = validate_metadata(&policy, &placements(&policy), &metadata(DOMAIN_TO_STORAGE)) + .expect_err("domain must not depend on storage"); + assert!(error.contains("radroots-event (tier=domain rank=1)")); + assert!(error.contains("radroots-storage (tier=spi rank=2)")); + assert!(error.contains("alias=storage_alias")); + assert!(error.contains("kind=normal")); + assert!(error.contains("target=cfg(unix)")); + assert!(error.contains("features=[memory]")); + } + + #[test] + fn spi_to_adapter_reports_build_and_target_specific_edge() { + let policy = policy(); + let error = validate_metadata(&policy, &placements(&policy), &metadata(SPI_TO_ADAPTER)) + .expect_err("SPI must not depend on adapter"); + assert!(error.contains("radroots-transport (tier=spi rank=2)")); + assert!(error.contains("radroots-nostr (tier=adapter rank=3)")); + assert!(error.contains("alias=protocol_adapter")); + assert!(error.contains("kind=build")); + assert!(error.contains("target=cfg(target_arch = \"wasm32\")")); + assert!(error.contains("features=[events]")); + } + + #[test] + fn service_orchestration_to_sdk_is_forbidden() { + let policy = policy(); + let error = validate_metadata(&policy, &placements(&policy), &metadata(SERVICE_TO_SDK)) + .expect_err("service orchestration must not depend on SDK"); + assert!(error.contains("radroots-sync (tier=orchestration rank=4)")); + assert!(error.contains("radroots-sdk (tier=sdk rank=5)")); + assert!(error.contains("alias=client_engine")); + } + + #[test] + fn valid_downward_dev_target_edge_passes() { + let policy = policy(); + validate_metadata(&policy, &placements(&policy), &metadata(VALID_DOWNWARD)) + .expect("adapter dev dependency on SPI points downward"); + } + + #[test] + fn migration_exception_is_exact_about_features() { + let policy = policy(); + let owner = super::CargoPackage { + id: "owner".to_owned(), + name: "radroots-trade".to_owned(), + dependencies: Vec::new(), + }; + let dependency = super::CargoPackage { + id: "dependency".to_owned(), + name: "radroots-nostr".to_owned(), + dependencies: Vec::new(), + }; + let kind = super::CargoDependencyKind { + kind: Some("dev".to_owned()), + target: None, + }; + let exact = super::CargoDependency { + name: "radroots-nostr".to_owned(), + kind: Some("dev".to_owned()), + features: vec!["std".to_owned(), "events".to_owned()], + target: None, + uses_default_features: false, + }; + assert!(exception_matches( + &policy.temporary_exception, + &owner, + &dependency, + &kind, + &exact + )); + + let broadened = super::CargoDependency { + features: vec!["client".to_owned(), "events".to_owned(), "std".to_owned()], + ..exact + }; + assert!(!exception_matches( + &policy.temporary_exception, + &owner, + &dependency, + &kind, + &broadened + )); + } + + #[test] + fn metadata_without_resolve_graph_fails_closed() { + let policy = policy(); + let mut fixture = metadata(VALID_DOWNWARD); + fixture.resolve = None; + let error = validate_metadata(&policy, &placements(&policy), &fixture) + .expect_err("missing resolved graph must fail"); + assert!(error.contains("did not include a resolved dependency graph")); + } +} diff --git a/tools/xtask/src/contract/source_maintenance.rs b/tools/xtask/src/contract/source_maintenance.rs @@ -76,7 +76,7 @@ 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 = - "888df7c6f0df0ce0df255d0f563eb366faa2c53b64f9ac1814ab33dbeeebdb03"; + "6caf782b040039992da219173a397e62653e5b155f836a2ebb42cff4e532d18a"; const RAW_EVENT_COLUMNS: &[&str] = &[ "event_id", diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -19,6 +19,7 @@ use std::process::ExitCode; fn usage() { eprintln!("usage:"); eprintln!(" cargo xtask architecture"); + eprintln!(" cargo xtask check-dependency-boundaries"); eprintln!(" cargo xtask contract validate"); eprintln!(" cargo xtask contract event-contract-registry-v7 [--write]"); eprintln!(" cargo xtask contract nip09-reconciliation-manifest [--write]"); @@ -145,6 +146,9 @@ fn run_contract(args: &[String]) -> Result<(), String> { fn run(args: &[String]) -> Result<(), String> { match args.first().map(String::as_str) { Some("architecture") if args.len() == 1 => architecture::validate(&workspace_root()), + Some("check-dependency-boundaries") if args.len() == 1 => { + architecture::validate_dependency_boundaries(&workspace_root()) + } Some("contract") => run_contract(&args[1..]), Some("coverage") => coverage::run(&args[1..]), Some("dto-roots") => dto_roots::run(&args[1..], &workspace_root()), diff --git a/tools/xtask/tests/fixtures/dependency-boundaries/domain-to-storage.json b/tools/xtask/tests/fixtures/dependency-boundaries/domain-to-storage.json @@ -0,0 +1,46 @@ +{ + "packages": [ + { + "id": "fixture#radroots-event@0.1.0", + "name": "radroots-event", + "dependencies": [ + { + "name": "radroots-storage", + "rename": "storage_alias", + "kind": null, + "features": ["memory"], + "target": "cfg(unix)", + "uses_default_features": false + } + ] + }, + { + "id": "fixture#radroots-storage@0.1.0", + "name": "radroots-storage", + "dependencies": [] + } + ], + "resolve": { + "nodes": [ + { + "id": "fixture#radroots-event@0.1.0", + "deps": [ + { + "name": "storage_alias", + "pkg": "fixture#radroots-storage@0.1.0", + "dep_kinds": [ + { + "kind": null, + "target": "cfg(unix)" + } + ] + } + ] + }, + { + "id": "fixture#radroots-storage@0.1.0", + "deps": [] + } + ] + } +} diff --git a/tools/xtask/tests/fixtures/dependency-boundaries/service-to-sdk.json b/tools/xtask/tests/fixtures/dependency-boundaries/service-to-sdk.json @@ -0,0 +1,46 @@ +{ + "packages": [ + { + "id": "fixture#radroots-sync@0.1.0", + "name": "radroots-sync", + "dependencies": [ + { + "name": "radroots-sdk", + "rename": "client_engine", + "kind": null, + "features": ["memory"], + "target": null, + "uses_default_features": false + } + ] + }, + { + "id": "fixture#radroots-sdk@0.1.0", + "name": "radroots-sdk", + "dependencies": [] + } + ], + "resolve": { + "nodes": [ + { + "id": "fixture#radroots-sync@0.1.0", + "deps": [ + { + "name": "client_engine", + "pkg": "fixture#radroots-sdk@0.1.0", + "dep_kinds": [ + { + "kind": null, + "target": null + } + ] + } + ] + }, + { + "id": "fixture#radroots-sdk@0.1.0", + "deps": [] + } + ] + } +} diff --git a/tools/xtask/tests/fixtures/dependency-boundaries/spi-to-adapter.json b/tools/xtask/tests/fixtures/dependency-boundaries/spi-to-adapter.json @@ -0,0 +1,46 @@ +{ + "packages": [ + { + "id": "fixture#radroots-transport@0.1.0", + "name": "radroots-transport", + "dependencies": [ + { + "name": "radroots-nostr", + "rename": "protocol_adapter", + "kind": "build", + "features": ["events"], + "target": "cfg(target_arch = \"wasm32\")", + "uses_default_features": false + } + ] + }, + { + "id": "fixture#radroots-nostr@0.1.0", + "name": "radroots-nostr", + "dependencies": [] + } + ], + "resolve": { + "nodes": [ + { + "id": "fixture#radroots-transport@0.1.0", + "deps": [ + { + "name": "protocol_adapter", + "pkg": "fixture#radroots-nostr@0.1.0", + "dep_kinds": [ + { + "kind": "build", + "target": "cfg(target_arch = \"wasm32\")" + } + ] + } + ] + }, + { + "id": "fixture#radroots-nostr@0.1.0", + "deps": [] + } + ] + } +} diff --git a/tools/xtask/tests/fixtures/dependency-boundaries/valid-downward.json b/tools/xtask/tests/fixtures/dependency-boundaries/valid-downward.json @@ -0,0 +1,46 @@ +{ + "packages": [ + { + "id": "fixture#radroots-transport-nostr@0.1.0", + "name": "radroots-transport-nostr", + "dependencies": [ + { + "name": "radroots-transport", + "rename": "transport_spi", + "kind": "dev", + "features": [], + "target": "cfg(unix)", + "uses_default_features": false + } + ] + }, + { + "id": "fixture#radroots-transport@0.1.0", + "name": "radroots-transport", + "dependencies": [] + } + ], + "resolve": { + "nodes": [ + { + "id": "fixture#radroots-transport-nostr@0.1.0", + "deps": [ + { + "name": "transport_spi", + "pkg": "fixture#radroots-transport@0.1.0", + "dep_kinds": [ + { + "kind": "dev", + "target": "cfg(unix)" + } + ] + } + ] + }, + { + "id": "fixture#radroots-transport@0.1.0", + "deps": [] + } + ] + } +}