lib

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

source_boundary.rs (9985B)


      1 use std::fs;
      2 use std::path::{Path, PathBuf};
      3 
      4 struct ForbiddenEventName {
      5     pattern: &'static str,
      6     reason: &'static str,
      7 }
      8 
      9 const FORBIDDEN_EVENT_NAMES: &[ForbiddenEventName] = &[
     10     ForbiddenEventName {
     11         pattern: "RadrootsPublicKey",
     12         reason: "public author keys must use radroots_identity::PublicKey",
     13     },
     14     ForbiddenEventName {
     15         pattern: "pub fn from_wire_unchecked",
     16         reason: "unchecked signed-event construction must not be public API",
     17     },
     18     ForbiddenEventName {
     19         pattern: "WireEventParts",
     20         reason: "event construction must use Nip01EventWireParts",
     21     },
     22     ForbiddenEventName {
     23         pattern: "RadrootsFrozenEventDraft",
     24         reason: "generic event construction must use GenericEventDraft",
     25     },
     26     ForbiddenEventName {
     27         pattern: "RadrootsNostrEvent",
     28         reason: "product-level event surfaces must use protocol-neutral domain names",
     29     },
     30     ForbiddenEventName {
     31         pattern: "RadrootsNostrEventRef",
     32         reason: "product-level event references must use EventRef",
     33     },
     34     ForbiddenEventName {
     35         pattern: "RadrootsNostrEventPtr",
     36         reason: "product-level event pointers must use EventPtr",
     37     },
     38     ForbiddenEventName {
     39         pattern: "RadrootsSignedNostrEvent",
     40         reason: "signed-event surfaces must use SignedEvent",
     41     },
     42     ForbiddenEventName {
     43         pattern: "RadrootsSignedNostrEventParts",
     44         reason: "signed-event parts must use protocol-neutral domain names",
     45     },
     46     ForbiddenEventName {
     47         pattern: "KIND_TRADE_VALIDATION_RECEIPT",
     48         reason: "the prototype receipt kind is retired in favor of typed RHI attestations",
     49     },
     50     ForbiddenEventName {
     51         pattern: "TRADE_VALIDATION_EVENT_KINDS",
     52         reason: "the prototype receipt family is retired",
     53     },
     54     ForbiddenEventName {
     55         pattern: concat!("is_trade_validation_", "receipt_kind"),
     56         reason: "the prototype receipt classifier is retired",
     57     },
     58     ForbiddenEventName {
     59         pattern: "is_trade_validation_event_kind",
     60         reason: "the prototype validation-event classifier is retired",
     61     },
     62     ForbiddenEventName {
     63         pattern: "is_trade_validation_service_event_kind",
     64         reason: "the unused prototype service-event classifier is retired",
     65     },
     66     ForbiddenEventName {
     67         pattern: "is_trade_validation_service_request_kind",
     68         reason: "the unused prototype service-request classifier is retired",
     69     },
     70     ForbiddenEventName {
     71         pattern: "is_trade_validation_service_result_kind",
     72         reason: "the unused prototype service-result classifier is retired",
     73     },
     74     ForbiddenEventName {
     75         pattern: "trade_validation_service_result_kind_for_request",
     76         reason: "the unused prototype service-kind mapper is retired",
     77     },
     78     ForbiddenEventName {
     79         pattern: "trade_validation_service_request_kind_for_result",
     80         reason: "the unused prototype service-kind mapper is retired",
     81     },
     82     ForbiddenEventName {
     83         pattern: "RadrootsTradeValidationReceipt",
     84         reason: "the prototype receipt DTO is retired in favor of the final evidence report",
     85     },
     86     ForbiddenEventName {
     87         pattern: "radroots.trade.validation_receipt.v1",
     88         reason: "the prototype receipt contract is retired",
     89     },
     90 ];
     91 
     92 const RETIRED_EVENT_MODULE_PATHS: &[&str] = &[
     93     "radroots_event::event_head",
     94     "radroots_event::events",
     95     "radroots_event::ids",
     96     "radroots_event::kinds",
     97 ];
     98 
     99 #[test]
    100 fn generic_event_source_does_not_reintroduce_old_core_nostr_event_names() {
    101     let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
    102         .parent()
    103         .and_then(Path::parent)
    104         .expect("lib repo root");
    105     let mut findings = Vec::new();
    106 
    107     for path in source_boundary_guard_files(repo_root) {
    108         let relative_path = relative_path(repo_root, path.as_path());
    109         let source = fs::read_to_string(path.as_path()).expect("read source file");
    110         for forbidden in FORBIDDEN_EVENT_NAMES {
    111             if forbidden_event_name_allowed(relative_path.as_str(), forbidden.pattern) {
    112                 continue;
    113             }
    114             if contains_forbidden_event_name(source.as_str(), forbidden.pattern) {
    115                 findings.push(format!(
    116                     "{} contains retired event concept `{}`: {}",
    117                     relative_path, forbidden.pattern, forbidden.reason
    118                 ));
    119             }
    120         }
    121     }
    122 
    123     assert!(
    124         findings.is_empty(),
    125         "radroots_event source-boundary violations:\n{}",
    126         findings.join("\n")
    127     );
    128 }
    129 
    130 #[test]
    131 fn first_party_sources_do_not_reintroduce_retired_event_module_paths() {
    132     let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
    133         .parent()
    134         .and_then(Path::parent)
    135         .expect("lib repo root");
    136     let mut findings = Vec::new();
    137 
    138     for path in source_boundary_guard_files(repo_root) {
    139         let relative_path = relative_path(repo_root, path.as_path());
    140         let source = fs::read_to_string(path.as_path()).expect("read source file");
    141         for retired_path in RETIRED_EVENT_MODULE_PATHS {
    142             if source.contains(retired_path) {
    143                 findings.push(format!(
    144                     "{relative_path} contains retired event module path `{retired_path}`"
    145                 ));
    146             }
    147         }
    148     }
    149 
    150     assert!(
    151         findings.is_empty(),
    152         "retired radroots_event module paths remain:\n{}",
    153         findings.join("\n")
    154     );
    155 }
    156 
    157 #[test]
    158 fn public_api_has_no_redundant_radroots_type_prefixes() {
    159     let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
    160         .parent()
    161         .and_then(Path::parent)
    162         .expect("lib repo root");
    163     let baseline = fs::read_to_string(repo_root.join("contracts/api_baselines/radroots_event.txt"))
    164         .expect("read radroots_event public API baseline");
    165     let mut prefixed = baseline
    166         .split(|character: char| !is_identifier_character(character))
    167         .filter(|identifier| {
    168             identifier
    169                 .strip_prefix("Radroots")
    170                 .and_then(|suffix| suffix.chars().next())
    171                 .is_some_and(|character| character.is_ascii_uppercase())
    172         })
    173         .collect::<Vec<_>>();
    174     prefixed.sort_unstable();
    175     prefixed.dedup();
    176 
    177     assert!(
    178         prefixed.is_empty(),
    179         "radroots_event public API contains redundant prefixed identifiers: {}",
    180         prefixed.join(", ")
    181     );
    182 }
    183 
    184 #[test]
    185 fn typed_authoring_mapping_is_not_implemented_in_the_generic_event_draft() {
    186     let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
    187         .parent()
    188         .and_then(Path::parent)
    189         .expect("lib repo root");
    190     let draft = fs::read_to_string(repo_root.join("crates/event/src/draft.rs"))
    191         .expect("read generic event draft source");
    192 
    193     for retired in [
    194         "from_authored_update",
    195         "from_authored_reply",
    196         "from_authored_profile",
    197         "from_typed_parts",
    198         "TypedAuthoringKind",
    199         "typed_authoring",
    200     ] {
    201         assert!(
    202             !draft.contains(retired),
    203             "generic authored input retains codec-owned typed mapping `{retired}`"
    204         );
    205     }
    206 }
    207 
    208 fn source_boundary_guard_files(repo_root: &Path) -> Vec<PathBuf> {
    209     let mut paths = Vec::new();
    210     for relative_root in ["crates", "contracts"] {
    211         let root = repo_root.join(relative_root);
    212         if root.exists() {
    213             collect_source_boundary_guard_files(root.as_path(), &mut paths);
    214         }
    215     }
    216     paths.sort();
    217     paths
    218 }
    219 
    220 fn collect_source_boundary_guard_files(root: &Path, paths: &mut Vec<PathBuf>) {
    221     for entry in fs::read_dir(root).expect("read source directory") {
    222         let entry = entry.expect("source entry");
    223         let path = entry.path();
    224         if path.is_dir() {
    225             if matches!(
    226                 path.file_name().and_then(|file_name| file_name.to_str()),
    227                 Some("generated" | "target")
    228             ) {
    229                 continue;
    230             }
    231             collect_source_boundary_guard_files(path.as_path(), paths);
    232             continue;
    233         }
    234 
    235         if path.file_name().and_then(|file_name| file_name.to_str()) == Some("source_boundary.rs") {
    236             continue;
    237         }
    238 
    239         if path.ends_with("event_store/src/nip09/reconciliation_v1/result_vector_executor.rs") {
    240             // This predecessor is an authenticated immutable artifact. Active NIP-09
    241             // execution uses the maintained successor surface; the frozen source must
    242             // retain its historical bytes so its provenance check remains meaningful.
    243             continue;
    244         }
    245 
    246         if matches!(
    247             path.extension().and_then(|extension| extension.to_str()),
    248             Some("rs") | Some("toml") | Some("md")
    249         ) {
    250             paths.push(path);
    251         }
    252     }
    253 }
    254 
    255 fn forbidden_event_name_allowed(relative_path: &str, pattern: &str) -> bool {
    256     pattern == "RadrootsNostrEvent" && is_nostr_protocol_context(relative_path)
    257 }
    258 
    259 fn is_nostr_protocol_context(relative_path: &str) -> bool {
    260     relative_path.starts_with("crates/nostr/")
    261         || relative_path.starts_with("crates/nostrdb/")
    262         || relative_path.starts_with("crates/nostr_signer/")
    263         || relative_path.starts_with("crates/transport_nostr/")
    264 }
    265 
    266 fn contains_forbidden_event_name(source: &str, pattern: &str) -> bool {
    267     source.match_indices(pattern).any(|(index, _)| {
    268         let before = source[..index].chars().next_back();
    269         let after = source[index + pattern.len()..].chars().next();
    270         before.is_none_or(|character| !is_identifier_character(character))
    271             && after.is_none_or(|character| !is_identifier_character(character))
    272     })
    273 }
    274 
    275 fn is_identifier_character(character: char) -> bool {
    276     character == '_' || character.is_ascii_alphanumeric()
    277 }
    278 
    279 fn relative_path(root: &Path, path: &Path) -> String {
    280     path.strip_prefix(root)
    281         .expect("source path is under repo root")
    282         .to_string_lossy()
    283         .replace('\\', "/")
    284 }