Evidence Graph¶
How Ogur turns unstructured sources into structured records and links those records into a graph. This page formally introduces every schema-based extraction object, shows how they connect, walks the construction pipeline, and ends with an honest list of what the graph does not yet do.
The design is evidence-centric: the record is the node, not a flattened (subject, predicate, object) triple. That framing is borrowed from the evidence-graph literature — most directly EvidenceNet (Zong et al., 2026), which builds disease-specific biomedical knowledge graphs whose nodes are PICO-structured evidence records rather than entities. Ogur generalizes the same idea beyond biomedicine: the same record-as-node discipline ingests clinical registrations, regulatory filings, congress abstracts, SEC 10-Ks, and patents under one schema family.
These diagrams are developer documentation, not the product
Core principle #2 — the knowledge graph is infrastructure, never UI (CLAUDE.md, ux-spec §6.4). The product surfaces typed cards and EntityChips; it never renders node-edge graphs to analysts. The graphs below exist so engineers can reason about retrieval, attribution, and provenance. They are not a frontend spec.
§1 Design principle — the record is the node¶
A general knowledge graph flattens a finding into dupilumab —treats→ atopic dermatitis. That loses who was studied, against what comparator, at what endpoint, with what statistical support, and from which source — exactly the context a competitive-intelligence analyst needs to judge whether a number matters.
EvidenceNet keeps that context by making the evidence record a first-class node:
G = ( V_E ∪ V_T , R )
where V_E are evidence nodes, V_T are normalized biomedical entity nodes, and R are evidence→entity and evidence→evidence relations.
Ogur uses the same shape with three node families, because intelligence arrives as discrete events before it is ever distilled into evidence:
G = ( V_S ∪ V_E ∪ V_T , R )
| Family | Ogur tables | Role | EvidenceNet analog |
|---|---|---|---|
V_S — Signal nodes |
Signal |
The atomic intelligence event — one per source occurrence, content-hash unique | (no analog — EvidenceNet starts at the extracted record) |
V_E — Evidence nodes |
ProtocolProfile, EvidenceRecord |
The PICO context + the quantitative outcome distilled from a source | the evidence node (evidence_id …) |
V_T — Entity nodes |
DrugProfile, Target, CompanyProfile |
Normalized, deduplicated canonical entities | normalized entity nodes (Cetuximab, RAS, …) |
R — typed edges |
SignalDrug, DrugTarget, DrugSynonym |
Provenance- and confidence-carrying links | LINKED_TO + evidence↔evidence relations |
The one structural difference worth flagging up front: EvidenceNet fuses PICO and outcome into a single node; Ogur splits them into ProtocolProfile (the trial design, one row per trial) and EvidenceRecord (one quantitative result, N rows per trial), joined on trial_id. The split lets one protocol carry many endpoint readouts without duplicating the design fields.
§2 The schema-extraction objects¶
Every object below is a structured extraction from unstructured or semi-structured input. Grouped by graph layer. The class diagram introduces them together and shows how they link; the field cards that follow give the extracted columns and the PICO / provenance / confidence mapping for each.
classDiagram
direction LR
class Signal {
+id PK
+source, source_id
+signal_type, severity
+drug_name, company, target, indication
+title, summary, raw_data
+event_date, detected_at
+content_hash UNIQUE
}
class ProtocolProfile {
+trial_id PK
+population
+histology, biomarker_selection
+comparator
+primary_endpoint
+line_of_therapy, blinding, dosing
+source_nct_url, model_used
}
class EvidenceRecord {
+id PK
+trial_id, drug_name, arm, subgroup
+endpoint, endpoint_type
+value, unit, ci_lower, ci_upper
+hr, p_value, comparator_value
+raw_excerpt, source_url
+confidence
+content_hash UNIQUE
}
class DrugProfile {
+normalized_name PK
+brand_name, company
+target, moa, phase
}
class Target {
+normalized_name PK
+uniprot_id, target_class
}
class CompanyProfile {
+normalized_name PK
+hq_country, stage_focus, market_cap_tier
}
class Briefing {
+id PK
+landscape_id
+executive_summary, signal_analyses
+kiq_answers, predictions
+schema_valid, schema_errors
}
class KIQ {
+id PK
+question, time_horizon, priority
}
Signal "1" --> "0..*" EvidenceRecord : trial_id
ProtocolProfile "1" --> "0..*" EvidenceRecord : trial_id
DrugProfile "1" --> "0..*" Signal : SignalDrug M2M
DrugProfile "*" --> "*" Target : DrugTarget weighted
DrugProfile "1" --> "*" DrugSynonym : alias resolution
CompanyProfile "1" --> "0..*" Signal : company
KIQ "1" --> "0..*" Briefing : answered in kiq_answers
Briefing ..> Signal : cites by id, prose not FK
Intelligence layer¶
Signal (signal.py) — the atomic unit every source normalizes to. Some signals are themselves LLM extractions: the Holo3 visual-intelligence sources mine earnings_narrative, job_posting, and kol_activity out of non-API surfaces (earnings calls, careers pages, congress programmes).
source · source_id where it came from (NCT, PMID, accession, …)
signal_type · severity 20+ types (trial / regulatory / corporate / IP / visual)
drug_name · company source-authored entity attribution (scalar, being
target · indication · moa superseded by edge tables — see §5)
title · summary human-readable
raw_data full source payload — re-parse if normalization changes
event_date when the underlying event happened (provenance)
content_hash sha256(source+source_id+signal_type)[:16], UNIQUE
Evidence layer — the PICO + outcome analog¶
This is the layer that maps most directly onto EvidenceNet's evidence node. It is built by a separate, CLI-only evidence pipeline (§3), not during a normal briefing.
ProtocolProfile (evidence.py) — one row per trial, parsed from a ClinicalTrials.gov v2 record. It is PICO:
population → P Population
histology → I Indication / disease context
biomarker_selection → I enrollment biomarker (e.g. "EGFR-mutant")
comparator → C Comparator arm
primary_endpoint → O Outcome measure
line_of_therapy · blinding · dosing study-design context
source_nct_url · extracted_at · model_used provenance
EvidenceRecord (evidence.py) — N rows per trial; each is one quantitative outcome with its provenance string, the analog of EvidenceNet's Key statistics block:
endpoint · endpoint_type · arm · subgroup what was measured, for whom
value · unit · ci_lower · ci_upper the number + its interval
hr · p_value · comparator_value effect size, significance, control arm
raw_excerpt · source_url PROVENANCE — "this number came from here"
confidence extraction-confidence label (string)
content_hash = sha256(trial+drug+endpoint+arm+subgroup+source)[:16], UNIQUE
Extracted by outcomes_extractor.py (Haiku tool-use) from abstract / label / conference text, and entity spans by entity_extractor.py (GLiNER, F1 ≈ 0.88, with a Claude fallback).
Normalized entity layer¶
DrugProfile + DrugSynonym (drug.py) — the canonical drug node plus its alias table ("Keytruda", "MK-3475", "lambrolizumab" → pembrolizumab). This is Ogur's entity normalization step, EvidenceNet's "normalize against a biomedical vocabulary."
Target (target.py) — gene/protein node, HGNC symbol PK, UniProt id from Open Targets. The cleanest normalized vocabulary in the system.
CompanyProfile (company.py) — company aggregate, normalized_name PK (name is the natural identifier). Normalization here is incomplete — see §6.
Synthesis layer¶
KIQ (kiq.py) — a Key Intelligence Question; the analyst-authored intent the graph is queried against. Not an extraction, but it shapes which records the synthesizer must answer with.
Briefing (briefing.py) — the synthesizer's structured JSON, persisted as columns. Each active KIQ produces one structured kiq_answers block — {finding, evidence, uncertainty, implication, confidence} — gated by validate_kiq_answers before persistence. This is a schema-constrained generation step: same record-as-node discipline, applied to the synthesis output.
§3 The extraction pipeline¶
The four-phase flow from raw source to graph. Read it as the Ogur counterpart of EvidenceNet's workflow figure: Ingest → Extract → Normalize & Score → Construct. Dashed red boxes are capabilities that are proposed or missing (§6); the 🧠 badge marks an LLM-driven step.
flowchart LR
subgraph P1["1 · Ingest and Normalize"]
direction TB
SRC["10 source adapters<br/>CT.gov · PubMed · OpenFDA<br/>SEC · Lens/EPO · Holo3 🧠"]
SIG["Signal<br/>content_hash dedup"]
SRC --> SIG
end
subgraph P2["2 · LLM-Driven Extraction"]
direction TB
FILT["Eligibility pre-filter<br/>OUTCOME_BEARING types"]
CT["ct_gov_v2 parser<br/>pure Python"]
OUT["outcomes_extractor 🧠<br/>Haiku tool-use"]
NER["entity_extractor 🧠<br/>GLiNER / Claude"]
PP["ProtocolProfile<br/>PICO"]
ER["EvidenceRecord<br/>outcome"]
FILT --> CT --> PP
FILT --> OUT --> ER
FILT --> NER
end
subgraph P3["3 · Normalize and Score"]
direction TB
SYN["DrugSynonym<br/>alias to generic"]
TGT["Target<br/>HGNC / UniProt"]
SCORE["composite quality score<br/>design · impact · stats · n<br/>to A-D grade"]
end
subgraph P4["4 · Graph Construction"]
direction TB
E1["SignalDrug · DrugTarget<br/>link_method + confidence"]
E2["SignalTarget · SignalCompany<br/>SignalIndication"]
EE["evidence-evidence relations<br/>SUPPORTS · CONTRADICTS · REFINES"]
G[("Evidence Graph")]
E1 --> G
E2 --> G
EE --> G
end
SIG --> FILT
ER --> SYN
PP --> SYN
NER --> TGT
SYN --> E1
TGT --> E1
SYN -.-> SCORE
G --> BRIEF["Briefing 🧠<br/>Sonnet synthesis"]
classDef gap fill:#F2D2CC,stroke:#8B2820,color:#2E0D10,stroke-dasharray:5 3;
class SCORE,E2,EE gap;
Two boundaries carry over from architecture.md:
- Ingestion is separate from synthesis. Phases 1 and 2–4 never run in the same process.
Signalrows are written once by seed scripts; everything downstream reads them. - Dedup is load-bearing. Both
SignalandEvidenceRecordcarry a deterministiccontent_hashwith a DB-level UNIQUE constraint. Re-running extraction (prompt iteration) cannot silently duplicate a record.
§4 The transformation — one source, three views¶
The Ogur counterpart of EvidenceNet's transformation figure: a single source statement traced from raw text → structured record → graph. The worked example is an immunology head-to-head readout (dupilumab vs. an IL-13 comparator on EASI-75).
flowchart LR
subgraph RAW["1 · Raw source"]
direction TB
NCT["NCT05012345<br/>CT.gov v2 protocol JSON"]
ABS["Conference abstract<br/>'EASI-75 58% vs 40%<br/>at wk 16; p = .18'"]
end
subgraph STRUCT["2 · Structured extraction"]
direction TB
PP["ProtocolProfile<br/>population: mod-severe AD<br/>comparator: IL-13 mAb<br/>primary_endpoint: EASI-75<br/>blinding: double-blind"]
ER["EvidenceRecord<br/>endpoint: EASI-75<br/>value: 58 · comparator_value: 40<br/>p_value: .18 · confidence: medium<br/>raw_excerpt: '…58% vs 40%…'"]
end
subgraph GRAPH["3 · Graph"]
direction TB
ERN["EvidenceRecord node"]
DRUG(("dupilumab"))
TGT(("IL-4Rα"))
CO(("Sanofi / Regeneron"))
CMP["competing EvidenceRecord<br/>IL-13 comparator"]
ERN -->|trial_id| PP
ERN -. LINKED_TO .-> DRUG
DRUG -->|"DrugTarget · conf 0.9"| TGT
DRUG -. SignalCompany .-> CO
ERN -. CONTRADICTS .-> CMP
end
NCT --> PP
ABS --> ER
ER --> ERN
PP --> ERN
classDef gap fill:#F2D2CC,stroke:#8B2820,color:#2E0D10,stroke-dasharray:5 3;
classDef solid fill:#D4DDD3,stroke:#345042,color:#0E1F17;
class DRUG,TGT,ERN,PP solid;
class CO,CMP gap;
The graph layer carries two edge families, exactly as EvidenceNet does — but Ogur has built only one of them:
- Record → entity edges (
LINKED_TOanalog): solid.SignalDrug,DrugTarget,DrugSynonym. Ogur's version is richer than EvidenceNet's bareLINKED_TO— every edge carrieslink_method,confidence, andmatched_evidence(§5). - Record → record edges (
SUPPORTS/CONTRADICTS/REFINES/ …): dashed. These do not exist in Ogur. TheCONTRADICTSedge above is computed on read by the head-to-head route, never stored. This is the central gap (§6.1).
§5 Graph construction — nodes and edges¶
The edge model is specified in full by ADR-0002 — Knowledge Graph Roadmap; this section is the introductory view. The decision recorded there: typed entity tables + typed edge tables in SQL + a thin Python traversal helper — no graph database. At ~4k signals and ~50 drugs, composite primary keys and indexes give cheap lookups; Python composes the traversals.
flowchart TB
subgraph NODES["Entity nodes (V_T)"]
direction LR
D(("Drug")) ; T(("Target")) ; C(("Company")) ; I(("Indication")) ; PW(("Pathway"))
end
subgraph RECS["Record nodes (V_S, V_E)"]
direction LR
S(("Signal")) ; EV(("EvidenceRecord")) ; PR(("ProtocolProfile"))
end
S -->|"SignalDrug ✅"| D
D -->|"DrugTarget ✅"| T
D -->|"DrugSynonym ✅"| D
EV -->|"trial_id ✅"| PR
S -.->|"SignalTarget ⛔"| T
S -.->|"SignalCompany ⛔"| C
S -.->|"SignalIndication ⛔"| I
D -.->|"DrugIndication ⛔"| I
T -.->|"TargetPathway ⛔"| PW
EV -.->|"SUPPORTS / CONTRADICTS ⛔"| EV
classDef built fill:#D4DDD3,stroke:#345042,color:#0E1F17;
class D,T,C,I,PW,S,EV,PR built;
✅ = built · ⛔ = proposed in ADR-0002, not yet implemented.
Canonical edge shape. Every edge follows the SignalDrug pattern — and this is where Ogur's provenance discipline (principle #1) becomes structural rather than textual:
class FromTo(SQLModel, table=True):
from_id: str = Field(foreign_key=..., primary_key=True)
to_id: str = Field(foreign_key=..., primary_key=True)
link_method: str # source_column | synonym_text_match | llm_extraction | open_targets | manual
confidence: float # 0.0–1.0, calibrated per link_method
matched_evidence: str | None # the snippet / OT id / PMID that justified the edge
created_at: datetime
link_method is a small closed enum, not free text — every method has a calibrated confidence range, so an edge can be defended on the page where it surfaces. The composite primary key makes every backfill idempotent. DrugTarget additionally carries evidence_count, a corroboration counter that increments each time a new source confirms the same pair — a weighted edge in all but name.
§6 Gaps versus an evidence-centric graph¶
Ogur's entity-attribution layer is more mature than EvidenceNet's (provenance + calibrated confidence on every edge). The gaps are in the evidence layer — the parts of EvidenceNet that turn isolated records into a reasoning fabric. Each gap below is mapped to existing code or an ADR so it is actionable, not aspirational.
| # | EvidenceNet capability | Ogur status | Where it would live |
|---|---|---|---|
| 1 | Evidence ↔ evidence relations (SUPPORTS / CONTRADICTS / REFINES / EXTENDS / REPLICATES / CAUSAL_CHAIN) | Missing | new EvidenceEvidence edge table; relation induction stage |
| 2 | Composite evidence-quality score S(e) → A–D grade | Missing — only a string confidence |
evidence.py, cf. ADR-0003 |
| 3 | Cross-document semantic fusion + merged-record provenance / versioning | Partial — exact-key dedup only | compute_evidence_content_hash, matcher.py |
| 4 | Entity normalization to a canonical vocabulary | Partial — drugs/targets yes, companies/indications no | ADR-0002 Phase 2–3 |
| 5 | Materialized, queryable graph (serialized + traversal API) | Missing — edges in SQL, no traversal layer | ogur/graph/ (ADR-0002 Phase 4) |
| 6 | Structural provenance link synthesis → evidence | Missing — citation is prose, not an edge | briefing.py, synthesizer.py |
§6.1 — No evidence ↔ evidence relations (the biggest gap). This is EvidenceNet's signature contribution and Ogur's most conspicuous absence. Ogur links records to entities but never to each other. Two EvidenceRecords reporting the same endpoint for competing drugs are not joined by a CONTRADICTS or comparative edge — the head-to-head card in routes/evidence.py computes that comparison deterministically on read and throws it away. Cross-trial corroboration (REPLICATES), a mature readout superseding an interim (REFINES), and mechanistic chains (CAUSAL_CHAIN) are all invisible. Building this means a relation-induction stage (deterministic proposal on shared entity + endpoint, LLM verification on ambiguous pairs — EvidenceNet's hybrid strategy) writing to a new EvidenceEvidence table that reuses the §5 edge shape.
§6.2 — No composite quality score. EvidenceNet computes a weighted score — roughly S(e) = (w1·design + w2·impact + w3·stats + w4·sample)·(1−λ) + λ·LLM_conf — and maps it to A–D grades. Ogur's EvidenceRecord.confidence is a bare extraction-confidence string ("medium") — it says nothing about study design or statistical support, even though ProtocolProfile already stores blinding, comparator, and line_of_therapy. There is no way to rank a phase-3 double-blind RCT readout above a single-arm conference abstract. ADR-0003 ranks drugs; an evidence-level S(e) is the missing sibling. (Note: because Ogur spans domains, S(e) must be domain-aware — a 10-K narrative and an RCT readout cannot share one weighting.)
§6.3 — Exact-key dedup, no semantic fusion. content_hash collapses identical re-extractions, but the same outcome reported first in an abstract and later in a full paper yields two records with different source_id and is never fused. EvidenceNet does fingerprint + semantic dedup + attribute merging with version provenance (~38% of its records have version > 1). Ogur has the raw material for this — matcher.py already scores protocol similarity — but it is used for competitive callouts, not for canonical-record fusion with a lineage trail.
§6.4 — Partial entity normalization. Drugs (DrugSynonym) and targets (HGNC + UniProt) are well normalized. Companies and indications are not: "REGENERON PHARMACEUTICALS, INC." and "Regeneron" are two distinct nodes today, and indications are stringly-typed. ADR-0002 Phase 2 (SignalCompany) ships the first company-normalization pass; indication clustering (the atopic march) is Phase 3.
§6.5 — The graph is not yet a queryable object. Nodes exist and a few edge types exist, but "graph construction" is only partial: there is no serialized graph and no traversal layer. Multi-hop questions ("all signals across the IL-4Rα / type-2 axis") still require ad-hoc Python joins. ADR-0002 Phase 4 (ogur/graph/ with signals_for_drug(drug, via="pathway", hops=3)) is the migration point.
§6.6 — Provenance is textual, not structural, at the synthesis boundary. A Briefing references trials by NCT id inside prose (signal_analyses, predictions), with no foreign key into the evidence layer — intentionally one-way (architecture.md §2). This satisfies principle #1 at the UI level (every claim gets a <SourceChip>) but not structurally: there is no graph edge from a synthesized claim back to the EvidenceRecord that grounds it, so "show me the evidence behind this implication" cannot be answered by traversal.
Fidelity baseline
EvidenceNet reports component-level audits (98.3% field extraction, 100% high-confidence entity-link, 87.5% fusion integrity, 90% relation-type accuracy). Ogur's analogs today are GLiNER entity F1 ≈ 0.88 and the per-drug evidence-pipeline pilot report (confidence distribution, null-field rate). There is no fusion-integrity or relation-type-accuracy metric — because, per §6.1 and §6.3, those layers are not built yet. Standing up gaps 1 and 3 should ship with the eval harness that measures them.
Related¶
- Architecture §2 — Data model — the canonical table-by-table reference these objects come from.
- ADR-0002 — Knowledge Graph Roadmap — the full edge model and the five-phase migration plan behind §5 and §6.
- ADR-0003 — Candidate-drug ranking — adjacent to the §6.2 scoring gap.
- Evaluation — the extraction-fidelity harness referenced in the fidelity note.