Repository navigation
Research Gap Detection
Author: Ciprian Ștefan Pleșca
This page documents src/openlongevity/gaps.py, which implements ResearchGapDetector. The detector applies a fixed sequence of explicit, inspectable heuristics to a topic-filtered subset of EvidenceRecord objects, producing a list of ResearchGap instances. Every heuristic here is deterministic and rule-based — there is no statistical inference or machine learning involved, which is itself a design choice: a reviewer can read the source and know exactly why a gap was or was not flagged.
Before any gap logic runs, detect(topic, records) filters the input to records whose title, endpoint, source, or tags contain the topic string (case-insensitive):
flowchart LR
R[All EvidenceRecord inputs] --> F{"topic.casefold() in\ntitle+endpoint+source+tags?"}
F -- no match for any record --> NoEvidence["ResearchGap('no_indexed_evidence', priority=high)"]
F -- at least one match --> Selected[selected records]
Selected --> G1[translational_gap check]
Selected --> G2[validation_gap check]
Selected --> G3[replication_gap check]
Selected --> G4[concentration check]
Selected --> G5[small_sample check]
Selected --> G6[outdated_evidence check]
If no record matches the topic at all, the detector short-circuits and returns a single high-priority no_indexed_evidence gap — this is the only case where the function returns early rather than running all six checks.
flowchart TD
subgraph Inputs["selected: EvidenceRecord[] matching topic"]
T[study_type set]
RS[replication_status values]
SRC[distinct sources]
SS[sample_size values]
MD[metadata.outdated flags]
end
T --> C1{"ANIMAL present\nAND\nCLINICAL or RCT absent?"}
C1 -- yes --> GAP1["translational_gap\npriority: high, confidence: 0.81"]
T --> C2{"IN_VITRO present\nAND ANIMAL absent?"}
C2 -- yes --> GAP2["validation_gap\npriority: high, confidence: 0.78"]
RS --> C3{"ALL records\nunknown/unreplicated?"}
C3 -- yes --> GAP3["replication_gap\npriority: medium, confidence: 0.72"]
SRC --> C4{"exactly 1 distinct source\nAND more than 1 record?"}
C4 -- yes --> GAP4["concentration\npriority: medium, confidence: 0.68"]
SS --> C5{"any sample_size < 50?"}
C5 -- yes --> GAP5["small_sample\npriority: medium, confidence: 0.70\n(only the small-sample record IDs)"]
MD --> C6{"any metadata.outdated == True?"}
C6 -- yes --> GAP6["outdated_evidence\npriority: low, confidence: 0.65\n(only the outdated record IDs)"]
-
translational_gap— fires when animal evidence exists for the topic but no clinical or RCT evidence does. This encodes a standard translational-research concern: findings in animal models that have not yet been tested in humans. -
validation_gap— fires when in-vitro evidence exists without animal-level validation, one step earlier in the same translational chain. -
replication_gap— fires only when every selected record hasreplication_statusin{"unknown", "unreplicated"}. A single replicated record among many is enough to suppress this gap, which means the heuristic answers "is there any replicated evidence at all for this topic," not "how much of the evidence is replicated." -
concentration— fires when more than one record exists but they all originate from a singlesourcestring, flagging a lack of independent corroboration. Note this is single-source concentration, not single-provider concentration —sourcehere is the free-form provenance/source field onEvidenceRecord, not the adapter name. -
small_sample— unlike the previous checks, this one attaches only the specific record IDs withsample_size < 50, not the full selected set, so a reviewer can go directly to the underrepresented records. -
outdated_evidence— reads a boolean flag from the free-formmetadatadictionary (metadata.get("outdated") is True), again attaching only the flagged record IDs. This is the lowest-priority gap and depends entirely on an external refresh process populating that metadata key correctly.
A single topic can trigger multiple gaps simultaneously — a topic represented only by three in-vitro studies from the same source, none replicated, would trigger validation_gap, replication_gap, and concentration all at once. The detector does not attempt to rank or deduplicate across categories; it surfaces every applicable signal and leaves prioritization to the reader, consistent with the project's broader stance that automated heuristics should surface, not resolve, research questions.
sequenceDiagram
autonumber
participant Client
participant Det as ResearchGapDetector
participant Eng as EvidenceEngine
Client->>Det: detect("cellular senescence", records)
Det->>Det: filter by topic match (_search_text)
alt no matches
Det-->>Client: [ResearchGap(no_indexed_evidence)]
else matches found
Det->>Det: compute types, replication set, sources, sample sizes, outdated flags
Det->>Det: evaluate all 6 heuristics independently
Det-->>Client: list[ResearchGap] (0 to 6 entries)
end
Note over Det,Eng: EvidenceEngine is accepted in the constructor\nbut is not currently used by detect() — reserved\nfor future score-aware gap heuristics.
ResearchGapDetector.__init__ accepts an EvidenceEngine instance and stores it as self.engine, but the current detect() method does not call any EvidenceEngine method — the gap heuristics operate directly on raw EvidenceRecord fields (study_type, replication_status, sample_size, metadata), not on grades or navigation scores. This is consistent with the module's design as a set of independent, literal heuristics, but it also means score-weighted gap detection (for example, "flag topics where all evidence has a navigation score below 0.3") is not yet implemented, despite the constructor's dependency suggesting a natural extension point.
See Evidence-Grading-Engine for the scoring logic referenced above, or API-Reference for how gap detection is exposed over HTTP.
© 2026 Ciprian Ștefan Pleșca. All rights reserved.
Licensed under the Apache License 2.0 — see LICENSE in the repository root.
OpenLongevity is research software. It does not provide medical advice, diagnosis, or clinical treatment recommendations.