Skip to main content

nexcore_pv_intelligence/
lib.rs

1//! # nexcore-pv-intelligence
2//!
3//! Pharmacovigilance intelligence graph — Company × Drug × Disease
4//! composition with competitive analysis queries.
5//!
6//! ## Architecture
7//!
8//! ```text
9//! Company --owns--> Drug --treats--> Disease
10//!    |                |                |
11//!    v                v                v
12//! CompanyAnalysis  DrugAnalysis   DiseaseAnalysis
13//!          \          |           /
14//!           nexcore-pv-intelligence
15//! ```
16//!
17//! The graph uses **string IDs** throughout. Entity crates
18//! (`nexcore-pharma`, `nexcore-drug`, `nexcore-disease`) supply the data;
19//! this crate supplies the query engine. They connect at runtime through
20//! the [`builder::GraphBuilder`].
21//!
22//! ## Quick Start
23//!
24//! ```rust
25//! use nexcore_pv_intelligence::build_top10_graph;
26//!
27//! let graph = build_top10_graph();
28//! let rankings = graph.safest_company_for_disease("t2dm");
29//! assert!(!rankings.is_empty());
30//! ```
31//!
32//! ## T1 Primitive Grounding
33//!
34//! | Concept | Primitive | Symbol |
35//! |---------|-----------|--------|
36//! | Graph node collections | State | ς |
37//! | Edge variants | Sum (Coproduct) | Σ |
38//! | Query pipelines | Sequence | σ |
39//! | Node → result transforms | Mapping | μ |
40//! | PRR / count comparisons | Comparison | κ |
41//! | Signal thresholds | Boundary | ∂ |
42//! | Numeric scores | Quantity | N |
43//! | Optional fields | Void | ∅ |
44//! | Constructors / builders | Existence | ∃ |
45//! | Serialize / Deserialize | Persistence | π |
46//! | Query trigger → result | Causality | → |
47//! | `build()` finalization | Irreversibility | ∝ |
48//!
49//! ## Modules
50//!
51//! - [`graph`]: Node types (`CompanyNode`, `DrugNode`, `DiseaseNode`) and `Edge` enum
52//! - [`builder`]: `GraphBuilder` — consuming builder for constructing graphs
53//! - [`ranking`]: Result types for all query methods
54//! - [`queries`]: `IntelligenceGraph` query methods (impl block)
55
56#![forbid(unsafe_code)]
57#![warn(missing_docs)]
58#![cfg_attr(
59    not(test),
60    deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
61)]
62
63pub mod builder;
64pub mod graph;
65pub mod queries;
66pub mod ranking;
67
68pub use builder::GraphBuilder;
69pub use graph::{CompanyNode, DiseaseNode, DrugNode, Edge, IntelligenceGraph};
70pub use ranking::{
71    ClassEffectResult, CompanyRanking, HeadToHead, PipelineOverlap, SafetyGap, SharedSignal,
72    TherapeuticLandscape,
73};
74
75/// Build a pre-seeded graph with top-10 pharma companies, key drugs,
76/// and the 10 major disease states with all ownership and indication edges.
77///
78/// This is the seed graph — enriched at runtime by Station tools.
79///
80/// # Examples
81///
82/// ```
83/// use nexcore_pv_intelligence::build_top10_graph;
84///
85/// let graph = build_top10_graph();
86/// assert!(graph.companies.len() >= 10);
87/// assert!(graph.drugs.len() >= 10);
88/// assert!(graph.diseases.len() >= 10);
89/// ```
90pub fn build_top10_graph() -> IntelligenceGraph {
91    GraphBuilder::new()
92        // ── Companies ──────────────────────────────────────────────────────
93        .add_company(
94            "lilly",
95            "Eli Lilly",
96            &["tirzepatide", "donanemab", "dulaglutide"],
97        )
98        .add_company(
99            "novo-nordisk",
100            "Novo Nordisk",
101            &["semaglutide", "liraglutide"],
102        )
103        .add_company("pfizer", "Pfizer", &["paxlovid", "eliquis"])
104        .add_company("bms", "Bristol Myers Squibb", &["nivolumab", "apixaban"])
105        .add_company("merck", "Merck", &["pembrolizumab", "sitagliptin"])
106        .add_company("abbvie", "AbbVie", &["adalimumab", "upadacitinib"])
107        .add_company(
108            "astrazeneca",
109            "AstraZeneca",
110            &["dapagliflozin", "osimertinib"],
111        )
112        .add_company(
113            "roche",
114            "Roche",
115            &["pembrolizumab-biosimilar", "ocrelizumab"],
116        )
117        .add_company("jnj", "Johnson & Johnson", &["ibrutinib", "ustekinumab"])
118        .add_company("novartis", "Novartis", &["secukinumab", "ribociclib"])
119        // ── Drugs ──────────────────────────────────────────────────────────
120        .add_drug(
121            "tirzepatide",
122            "tirzepatide",
123            "GLP-1/GIP Dual Agonist",
124            Some("lilly"),
125        )
126        .add_drug(
127            "donanemab",
128            "donanemab",
129            "Anti-Amyloid Antibody",
130            Some("lilly"),
131        )
132        .add_drug(
133            "dulaglutide",
134            "dulaglutide",
135            "GLP-1 Receptor Agonist",
136            Some("lilly"),
137        )
138        .add_drug(
139            "semaglutide",
140            "semaglutide",
141            "GLP-1 Receptor Agonist",
142            Some("novo-nordisk"),
143        )
144        .add_drug(
145            "liraglutide",
146            "liraglutide",
147            "GLP-1 Receptor Agonist",
148            Some("novo-nordisk"),
149        )
150        .add_drug(
151            "paxlovid",
152            "nirmatrelvir/ritonavir",
153            "Protease Inhibitor",
154            Some("pfizer"),
155        )
156        .add_drug("eliquis", "apixaban", "Anticoagulant", Some("pfizer"))
157        .add_drug(
158            "nivolumab",
159            "nivolumab",
160            "PD-1 Checkpoint Inhibitor",
161            Some("bms"),
162        )
163        .add_drug("apixaban", "apixaban", "Anticoagulant", Some("bms"))
164        .add_drug(
165            "pembrolizumab",
166            "pembrolizumab",
167            "PD-1 Checkpoint Inhibitor",
168            Some("merck"),
169        )
170        .add_drug(
171            "sitagliptin",
172            "sitagliptin",
173            "DPP-4 Inhibitor",
174            Some("merck"),
175        )
176        .add_drug("adalimumab", "adalimumab", "Anti-TNF", Some("abbvie"))
177        .add_drug(
178            "upadacitinib",
179            "upadacitinib",
180            "JAK Inhibitor",
181            Some("abbvie"),
182        )
183        .add_drug(
184            "dapagliflozin",
185            "dapagliflozin",
186            "SGLT2 Inhibitor",
187            Some("astrazeneca"),
188        )
189        .add_drug(
190            "osimertinib",
191            "osimertinib",
192            "EGFR TKI",
193            Some("astrazeneca"),
194        )
195        .add_drug(
196            "pembrolizumab-biosimilar",
197            "pembrolizumab biosimilar",
198            "PD-1 Checkpoint Inhibitor",
199            Some("roche"),
200        )
201        .add_drug("ocrelizumab", "ocrelizumab", "Anti-CD20", Some("roche"))
202        .add_drug("ibrutinib", "ibrutinib", "BTK Inhibitor", Some("jnj"))
203        .add_drug("ustekinumab", "ustekinumab", "Anti-IL-12/23", Some("jnj"))
204        .add_drug(
205            "secukinumab",
206            "secukinumab",
207            "Anti-IL-17A",
208            Some("novartis"),
209        )
210        .add_drug(
211            "ribociclib",
212            "ribociclib",
213            "CDK4/6 Inhibitor",
214            Some("novartis"),
215        )
216        // ── Diseases ───────────────────────────────────────────────────────
217        .add_disease("t2dm", "Type 2 Diabetes Mellitus", "Metabolic")
218        .add_disease("obesity", "Obesity", "Metabolic")
219        .add_disease("alzheimers", "Alzheimer's Disease", "Neuroscience")
220        .add_disease("nsclc", "Non-Small Cell Lung Cancer", "Oncology")
221        .add_disease("ra", "Rheumatoid Arthritis", "Immunology")
222        .add_disease("breast-cancer", "Breast Cancer (HR+/HER2-)", "Oncology")
223        .add_disease("ms", "Multiple Sclerosis", "Neuroscience")
224        .add_disease("cll", "Chronic Lymphocytic Leukaemia", "Hematology")
225        .add_disease("psoriasis", "Psoriasis", "Dermatology")
226        .add_disease("covid19", "COVID-19", "Infectious")
227        .add_disease("afib", "Atrial Fibrillation", "Cardiovascular")
228        // ── Indication edges ───────────────────────────────────────────────
229        .add_treats("tirzepatide", "t2dm")
230        .add_treats("tirzepatide", "obesity")
231        .add_treats("donanemab", "alzheimers")
232        .add_treats("dulaglutide", "t2dm")
233        .add_treats("semaglutide", "t2dm")
234        .add_treats("semaglutide", "obesity")
235        .add_treats("liraglutide", "t2dm")
236        .add_treats("liraglutide", "obesity")
237        .add_treats("paxlovid", "covid19")
238        .add_treats("eliquis", "afib")
239        .add_treats("nivolumab", "nsclc")
240        .add_treats("apixaban", "afib")
241        .add_treats("pembrolizumab", "nsclc")
242        .add_treats("pembrolizumab", "breast-cancer")
243        .add_treats("sitagliptin", "t2dm")
244        .add_treats("adalimumab", "ra")
245        .add_treats("adalimumab", "psoriasis")
246        .add_treats("upadacitinib", "ra")
247        .add_treats("dapagliflozin", "t2dm")
248        .add_treats("osimertinib", "nsclc")
249        .add_treats("pembrolizumab-biosimilar", "nsclc")
250        .add_treats("ocrelizumab", "ms")
251        .add_treats("ibrutinib", "cll")
252        .add_treats("ustekinumab", "psoriasis")
253        .add_treats("secukinumab", "psoriasis")
254        .add_treats("secukinumab", "ra")
255        .add_treats("ribociclib", "breast-cancer")
256        // ── Competition edges ──────────────────────────────────────────────
257        .add_competes("semaglutide", "tirzepatide", "t2dm")
258        .add_competes("semaglutide", "tirzepatide", "obesity")
259        .add_competes("pembrolizumab", "nivolumab", "nsclc")
260        .add_competes("adalimumab", "upadacitinib", "ra")
261        .add_competes("eliquis", "apixaban", "afib")
262        .build()
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn build_top10_graph_has_expected_counts() {
271        let graph = build_top10_graph();
272        assert!(graph.companies.len() >= 10, "expected >= 10 companies");
273        assert!(graph.drugs.len() >= 10, "expected >= 10 drugs");
274        assert!(graph.diseases.len() >= 10, "expected >= 10 diseases");
275        assert!(!graph.edges.is_empty(), "expected edges");
276    }
277
278    #[test]
279    fn build_top10_graph_owns_edges_present() {
280        let graph = build_top10_graph();
281        let owns_count = graph
282            .edges
283            .iter()
284            .filter(|e| matches!(e, Edge::Owns { .. }))
285            .count();
286        assert!(
287            owns_count >= 10,
288            "expected >= 10 Owns edges, got {owns_count}"
289        );
290    }
291
292    #[test]
293    fn build_top10_graph_treats_edges_present() {
294        let graph = build_top10_graph();
295        let treats_count = graph
296            .edges
297            .iter()
298            .filter(|e| matches!(e, Edge::Treats { .. }))
299            .count();
300        assert!(
301            treats_count >= 10,
302            "expected >= 10 Treats edges, got {treats_count}"
303        );
304    }
305
306    #[test]
307    fn build_top10_graph_lilly_owns_tirzepatide() {
308        let graph = build_top10_graph();
309        let has_edge = graph.edges.iter().any(|e| {
310            matches!(e, Edge::Owns { company, drug }
311                if company == "lilly" && drug == "tirzepatide")
312        });
313        assert!(has_edge, "Lilly should own tirzepatide");
314    }
315
316    #[test]
317    fn build_top10_graph_t2dm_rankings_non_empty() {
318        let graph = build_top10_graph();
319        let rankings = graph.safest_company_for_disease("t2dm");
320        assert!(
321            !rankings.is_empty(),
322            "safest_company_for_disease(t2dm) should return results"
323        );
324    }
325
326    #[test]
327    fn build_top10_graph_serializes() {
328        let graph = build_top10_graph();
329        let json = serde_json::to_string(&graph).expect("graph must serialize");
330        assert!(json.contains("tirzepatide"));
331        assert!(json.contains("t2dm"));
332    }
333}