replication-radar 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {replication_radar-0.3.0 → replication_radar-0.3.2}/PKG-INFO +2 -2
- {replication_radar-0.3.0 → replication_radar-0.3.2}/docs/openaire-mcp.md +9 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/docs/readiness-scoring-plan.md +5 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/pyproject.toml +2 -2
- {replication_radar-0.3.0 → replication_radar-0.3.2}/site/app.js +276 -90
- replication_radar-0.3.2/site/curated.json +12 -0
- replication_radar-0.3.2/site/index.html +58 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/site/style.css +69 -31
- {replication_radar-0.3.0 → replication_radar-0.3.2}/src/replication_radar/__init__.py +1 -1
- {replication_radar-0.3.0 → replication_radar-0.3.2}/src/replication_radar/network.py +25 -1
- replication_radar-0.3.0/site/index.html +0 -99
- {replication_radar-0.3.0 → replication_radar-0.3.2}/.github/workflows/publish-pypi.yml +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/.gitignore +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/CLAUDE.md +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/LICENSE +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/README.md +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/STORY.md +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/demo_sdm.py +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/docs/app-ui.md +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/docs/link-types.md +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/docs/next-layers-plan.md +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/netlify.toml +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/scripts/build_verdicts.py +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/site/README.md +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/site/verdicts.json +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/src/replication_radar/data/verdicts.json +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/src/replication_radar/openaire.py +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/src/replication_radar/radar.py +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/src/replication_radar/server.py +0 -0
- {replication_radar-0.3.0 → replication_radar-0.3.2}/src/replication_radar/verdicts.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: replication-radar
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: MCP server that turns the OpenAIRE Graph into a ranked replication queue — impact-ranked targets
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: MCP server that turns the OpenAIRE Graph into a ranked replication queue — impact-ranked targets and the Science Live verification overlay (retraction/supersession-aware).
|
|
5
5
|
Project-URL: Homepage, https://github.com/ScienceLiveHub/replication-radar
|
|
6
6
|
Project-URL: Repository, https://github.com/ScienceLiveHub/replication-radar
|
|
7
7
|
Project-URL: Science Live, https://sciencelive4all.org
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
> So MCP-backed features belong in the **`src/replication_radar` MCP server / agentic exploration**,
|
|
10
10
|
> **not** in the browser app. The browser app stays on public CORS APIs (nanopub SPARQL + the
|
|
11
11
|
> public OpenAIRE Graph REST API). Keep this split in mind when reading the "direction" column.
|
|
12
|
+
>
|
|
13
|
+
> **Confirmed (live `_debug.api_urls_called`, 2026-06-14):** the MCP queries the **same open
|
|
14
|
+
> OpenAIRE Graph** as the app — `api.openaire.eu/graph/v2/researchProducts` (search) and
|
|
15
|
+
> `api.openaire.eu/graph/v1/researchProducts/links` (relationships). There is **no private/fuller
|
|
16
|
+
> dataset** behind the gateway; OAuth is access control, not a different graph. The app uses v1
|
|
17
|
+
> search and does **not** call `/links` — that relationship endpoint (public, ~1.2M paper↔software
|
|
18
|
+
> edges globally) is the one capability the app could adopt directly, no MCP needed. For
|
|
19
|
+
> biodiversity/EO papers it returns only paper→paper `cites` (verified), so it does not close the
|
|
20
|
+
> materials gap for this domain.
|
|
12
21
|
|
|
13
22
|
Directions: **(i)** verified-knowledge graph (typed links between results) · **(ii)** CoARA
|
|
14
23
|
(assess researchers/projects by output *diversity* + reproducibility + reuse, not citations) ·
|
|
@@ -79,6 +79,11 @@ readiness = 0.45 * materials // per-paper (2a); null→excluded & flagged,
|
|
|
79
79
|
- **P1 · Status taxonomy (live):** compute & group by status; surface ⭐ Replication-ready.
|
|
80
80
|
- **P2 · Curated demo enrichment (baked-to-static):** for the demo set resolve materials fully (repo
|
|
81
81
|
from paper, FAIR, RO-Crate, citation-network "missing edge", transfer-headroom). See `app-ui.md` §8.
|
|
82
|
+
**▶ STARTED 2026-06-14** (`site/curated.json` + `app.js`): paper-resolved materials baked as just
|
|
83
|
+
the repo link (grounded, from the paper's Data/Code statement); app overrides materials, injects the
|
|
84
|
+
paper so it appears for its topic, and computes **FAIR live** via existing `assessSoftware()`.
|
|
85
|
+
**WiSDM lit**: `code ✓` + resolved-from-paper provenance + FAIR 3/5, readiness 0.4→0.49.
|
|
86
|
+
TODO: add Soroye/Bombus + a HEALPix-transfer entry; then the "missing edge" graph + transfer-headroom.
|
|
82
87
|
- **P3 · Close the loop:** "Start a replication" → `forrt-replication-template` (scoped) → publish
|
|
83
88
|
chain → re-search shows the paper flip to ✅ Validated.
|
|
84
89
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "replication-radar"
|
|
3
|
-
version = "0.3.
|
|
4
|
-
description = "MCP server that turns the OpenAIRE Graph into a ranked replication queue — impact-ranked targets
|
|
3
|
+
version = "0.3.2"
|
|
4
|
+
description = "MCP server that turns the OpenAIRE Graph into a ranked replication queue — impact-ranked targets and the Science Live verification overlay (retraction/supersession-aware)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = { text = "MIT" }
|
|
@@ -7,8 +7,9 @@ const CLASS_SCORE = { C1: 1, C2: 0.8, C3: 0.6, C4: 0.4, C5: 0.2 };
|
|
|
7
7
|
const EXAMPLES = ["species distribution", "marine heatwave", "bumble bee climate", "presence-only", "range maps scale"];
|
|
8
8
|
|
|
9
9
|
let VERDICTS = {}; // doi -> [verifications]
|
|
10
|
+
let CLAIMS = {}; // outcome-hash -> { label, aida, type } (what exactly was replicated)
|
|
10
11
|
let VERIFIED = []; // enriched: {doi, title, citations, verifications}
|
|
11
|
-
let
|
|
12
|
+
let CURATED = {}; // doi -> paper-resolved materials (the links OpenAIRE lacks; from the paper)
|
|
12
13
|
|
|
13
14
|
// ---------- OpenAIRE helpers (same shape as openaire.py) ----------
|
|
14
15
|
const doiOf = (r) => {
|
|
@@ -56,11 +57,85 @@ const materialsOf = (r) => {
|
|
|
56
57
|
// Transparent composite: materials is the largest term; when materials is UNKNOWN
|
|
57
58
|
// (null) we DROP that term and renormalise the rest — never score a missing link as 0.
|
|
58
59
|
const readinessFrom = (matScore, impactScore, momentum) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// No renormalisation when materials are unknown: you can't even reproduce a paper whose
|
|
61
|
+
// code/data aren't surfaced, so the materials term simply contributes 0 (worth caps at 0.55).
|
|
62
|
+
// A paper WITH materials therefore always out-ranks an equally-cited one without.
|
|
63
|
+
const r = 0.45 * (matScore || 0) + 0.35 * impactScore + 0.20 * momentum;
|
|
62
64
|
return Math.round(r * 100) / 100;
|
|
63
65
|
};
|
|
66
|
+
// Status taxonomy — "not replicated" is DISAMBIGUATED, not penalised.
|
|
67
|
+
const STATUS = {
|
|
68
|
+
robust: { label: "✅ Robustly validated", cls: "st-val", tip: "multiple independent replications, all confirmed — a settled, reliable result" },
|
|
69
|
+
validated: { label: "✅ Validated", cls: "st-val", tip: "independently replicated and it held up" },
|
|
70
|
+
contested: { label: "⚠️ Contested", cls: "st-con", tip: "independent replications DISAGREE (some confirm, some contradict/partial) — worth re-checking" },
|
|
71
|
+
refuted: { label: "❌ Refuted", cls: "st-con", tip: "independent replication(s) contradicted it, none confirmed" },
|
|
72
|
+
reproducible: { label: "🔁 Reproducible", cls: "st-ready", tip: "original code/data are available, so it can be RE-RUN (reproduced). Note: replication ≠ reproduction — replication tests the same claim with DIFFERENT data/methods (FORRT)." },
|
|
73
|
+
needs: { label: "❔ Needs check", cls: "st-needs", tip: "not yet replicated and OpenAIRE links no materials — unknown (not absent); resolve from the paper" },
|
|
74
|
+
dormant: { label: "💤 Dormant", cls: "st-dorm", tip: "no verdict, older, low momentum, no materials surfaced — likely dormant" },
|
|
75
|
+
};
|
|
76
|
+
// Distinct replication OUTCOMES for a paper — one signed nanopub per independent replication
|
|
77
|
+
// (deduped by outcome URI, since several replications target the same claim).
|
|
78
|
+
const outcomesFor = (doi) => {
|
|
79
|
+
const seen = new Set(), out = [];
|
|
80
|
+
(VERDICTS[doi] || []).forEach((v, i) => {
|
|
81
|
+
const key = v.outcome_np || `__${i}`;
|
|
82
|
+
if (seen.has(key)) return;
|
|
83
|
+
seen.add(key);
|
|
84
|
+
out.push({ np: v.outcome_np || null, verdict: v.verdict || "" });
|
|
85
|
+
});
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
// Colour an outcome chip by its verdict so the agreement pattern is legible at a glance.
|
|
89
|
+
const verdictClass = (v) => /contradict|notsupport|refut/i.test(v) ? "v-con"
|
|
90
|
+
: /partial/i.test(v) ? "v-part"
|
|
91
|
+
: /validat|confirm|support/i.test(v) ? "v-ok" : "v-other";
|
|
92
|
+
// Short, human verdict label for a chip (no arbitrary numbers — the verdict is the meaning).
|
|
93
|
+
const verdictLabel = (v) => /contradict/i.test(v) ? "Contradicted"
|
|
94
|
+
: /notsupport/i.test(v) ? "Not supported"
|
|
95
|
+
: /partial/i.test(v) ? "Partial"
|
|
96
|
+
: /validat|confirm|support/i.test(v) ? "Validated" : (v || "outcome");
|
|
97
|
+
// Each replication outcome is a verdict-labelled, colour-coded chip linking to its signed nanopub
|
|
98
|
+
// — navy = confirmed, amber = partial, red = contradicted. No cryptic index numbers.
|
|
99
|
+
const outcomeLinks = (outs) => {
|
|
100
|
+
if (!outs.length) return "";
|
|
101
|
+
const lead = outs.length === 1 ? "replication outcome:" : `${outs.length} replication outcomes:`;
|
|
102
|
+
return ` · ${lead} ` + outs.map((o) =>
|
|
103
|
+
`<a class="onp ${verdictClass(o.verdict)}" href="${o.np}" target="_blank" rel="noopener" title="${esc(o.verdict)} — open the signed nanopub">${verdictLabel(o.verdict)}</a>`).join(" ");
|
|
104
|
+
};
|
|
105
|
+
// Agreement pattern across the independent replication verdicts — many-agree ≠ disagree.
|
|
106
|
+
const agreementOf = (doi) => {
|
|
107
|
+
const vs = outcomesFor(doi).map((o) => o.verdict);
|
|
108
|
+
const contra = vs.filter((v) => /contradict|notsupport|refut/i.test(v)).length;
|
|
109
|
+
const partial = vs.filter((v) => /partial/i.test(v)).length;
|
|
110
|
+
const confirm = vs.filter((v) => /validat|confirm|support/i.test(v) && !/partial|contradict|notsupport|refut/i.test(v)).length;
|
|
111
|
+
const n = vs.length;
|
|
112
|
+
if (contra && (confirm || partial)) return { key: "contested", why: `${n} replications disagree — ${confirm} confirm · ${partial} partial · ${contra} contradict` };
|
|
113
|
+
if (contra) return { key: "refuted", why: `contradicted by ${contra} replication${contra > 1 ? "s" : ""}` };
|
|
114
|
+
if (confirm >= 2 && !partial) return { key: "robust", why: `${confirm} independent replications, all confirmed` };
|
|
115
|
+
if (confirm) return { key: "validated", why: partial ? `confirmed (${n} replications, ${partial} partial)` : (n > 1 ? `confirmed (${n} replications)` : "confirmed once") };
|
|
116
|
+
if (partial) return { key: "validated", why: `partially supported (${partial} of ${n})` };
|
|
117
|
+
return { key: "validated", why: "independently checked" };
|
|
118
|
+
};
|
|
119
|
+
const statusOf = (t) => {
|
|
120
|
+
if (t.status === "VERIFIED") return agreementOf(t.doi).key; // robust / validated / contested / refuted
|
|
121
|
+
if (t.mat && t.mat.score != null) return "reproducible"; // original materials present → can re-run
|
|
122
|
+
const old = t.year && (new Date().getFullYear() - t.year) >= 8; // had time to be replicated
|
|
123
|
+
const hot = t.impl === "C1" || t.impl === "C2"; // still gaining momentum
|
|
124
|
+
return (old && !hot) ? "dormant" : "needs";
|
|
125
|
+
};
|
|
126
|
+
// Replication PRIORITY (0..1) — how much this would benefit from (further) replication, so the
|
|
127
|
+
// number and the order agree. OPEN = worth × feasible (the readiness already computed). VERIFIED =
|
|
128
|
+
// impact modulated by the agreement: contested/unsettled rises, robustly-validated sinks (it's done).
|
|
129
|
+
const VERDICT_WEIGHT = { robust: 0.2, validated: 0.4, contested: 0.95, refuted: 0.55 };
|
|
130
|
+
const priorityOf = (t) => {
|
|
131
|
+
if (t.status !== "VERIFIED") {
|
|
132
|
+
// dormant = old, cold, no materials → low ACTIONABILITY, so it sinks below live targets
|
|
133
|
+
// however high its historic citation count, matching what the "💤 Dormant" badge signals.
|
|
134
|
+
return t.statusKey === "dormant" ? Math.round(t.readiness * 0.5 * 100) / 100 : t.readiness;
|
|
135
|
+
}
|
|
136
|
+
const imp = Math.max(CLASS_SCORE[t.infl] || 0.2, CLASS_SCORE[t.cls] || 0.2);
|
|
137
|
+
return Math.round(imp * (VERDICT_WEIGHT[t.statusKey] ?? 0.4) * 100) / 100;
|
|
138
|
+
};
|
|
64
139
|
|
|
65
140
|
async function search(topic, type, size) {
|
|
66
141
|
const u = `${API}/researchProducts?search=${encodeURIComponent(topic)}&type=${type}&pageSize=${size}`;
|
|
@@ -135,9 +210,18 @@ const NP_SPARQL = "https://query.knowledgepixels.com/repo/full";
|
|
|
135
210
|
const TPL_OUTCOME = "https://w3id.org/np/RA2zljn0Nw9SadppOyxZoh-_Rxosslrq-vYG-p9SttnJE";
|
|
136
211
|
const TPL_CITO = "https://w3id.org/np/RA43F9EoOuzF0xoNUnCMNyFsfIqlsuWDdPHCnN0wCdCAw";
|
|
137
212
|
const VERDICT_RELS = new Set(["confirms", "qualifies", "disputes", "critiques", "extends", "supports", "refutes"]);
|
|
213
|
+
// CiTO relations that are METHOD/DATA/CREDIT provenance — they point to a SOURCE paper, not a
|
|
214
|
+
// verdict on it. A verdict must never attach via these: a study that `usesMethodIn` Phillips 2009
|
|
215
|
+
// and was Contradicted does NOT contradict Phillips 2009 (it reused its method). (lowercased compare)
|
|
216
|
+
const NONVERDICT_RELS = new Set(["usesmethodin", "usesdatafrom", "citesasdatasource", "citesasevidence", "credits", "citesforinformation", "obtainsbackgroundfrom", "obtainssupportfrom", "citesasauthority", "citesasrelated", "citesassourcedocument", "includesquotationfrom", "sharesauthorinstitutionwith"]);
|
|
138
217
|
const npHash = (u) => (u || "").replace(/.*\/np\//, "");
|
|
139
218
|
const doiPart = (u) => (u || "").replace(/.*doi\.org\//, "").toLowerCase();
|
|
140
219
|
const cleanRepo = (r) => (!r ? null : r.includes("doi.org/") ? doiPart(r) : /^10\./.test(r) ? r.toLowerCase() : r);
|
|
220
|
+
// AIDA statement URI → the atomic claim sentence (mixed +/%20 encoding in the wild).
|
|
221
|
+
const aidaText = (u) => { if (!u) return ""; try { return decodeURIComponent(u.replace(/.*\/aida\//, "").replace(/\+/g, " ")); } catch (e) { return ""; } };
|
|
222
|
+
// e.g. ".../terms/model_performance-FORRT-Claim" → "model performance"
|
|
223
|
+
const claimType = (u) => (!u ? "" : u.replace(/.*\/terms\//, "").replace(/-FORRT-Claim$/, "").replace(/_/g, " "));
|
|
224
|
+
const claimFor = (outcome_np) => CLAIMS[npHash(outcome_np)] || null;
|
|
141
225
|
|
|
142
226
|
async function sparqlCsv(query) {
|
|
143
227
|
const r = await fetch(`${NP_SPARQL}?query=${encodeURIComponent(query)}`, { headers: { Accept: "text/csv" } });
|
|
@@ -155,8 +239,28 @@ async function buildIndexFromNetwork() {
|
|
|
155
239
|
SELECT DISTINCT ?outcome ?status ?repo WHERE { GRAPH ?g { ?outcome ntpl:wasCreatedFromTemplate <${TPL_OUTCOME}> . } ?outcome np:hasAssertion ?oa . GRAPH ?oa { ?oc slt:hasValidationStatus ?s . OPTIONAL { ?oc slt:hasOutcomeRepository ?repo . } } BIND(STRAFTER(STR(?s),"/terms/") AS ?status) }`;
|
|
156
240
|
const QB = `PREFIX np: <http://www.nanopub.org/nschema#> PREFIX ntpl: <https://w3id.org/np/o/ntemplate/> PREFIX cito: <http://purl.org/spar/cito/>
|
|
157
241
|
SELECT DISTINCT ?cito ?subj ?rel ?orig WHERE { GRAPH ?g { ?cito ntpl:wasCreatedFromTemplate <${TPL_CITO}> . } ?cito np:hasAssertion ?ca . GRAPH ?ca { ?subj ?rel ?orig . } FILTER(STRSTARTS(STR(?rel),STR(cito:))) FILTER(CONTAINS(STR(?orig),"doi.org/10.")) } LIMIT 3000`;
|
|
242
|
+
// QC: what exactly was replicated — traverse Outcome →isOutcomeOf→ Study →targetsClaim→ Claim,
|
|
243
|
+
// pulling the claim label, its AIDA statement (the atomic claim sentence), and its FORRT type.
|
|
244
|
+
const QC = `PREFIX np: <http://www.nanopub.org/nschema#> PREFIX ntpl: <https://w3id.org/np/o/ntemplate/> PREFIX slt: <https://w3id.org/sciencelive/o/terms/> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
245
|
+
SELECT DISTINCT ?outcome ?claimLabel ?aida ?ctype WHERE { GRAPH ?og { ?outcome ntpl:wasCreatedFromTemplate <${TPL_OUTCOME}> . } ?outcome np:hasAssertion ?oa . GRAPH ?oa { ?oc slt:isOutcomeOf ?study . } GRAPH ?sg { ?study slt:targetsClaim ?claim . } GRAPH ?cg { ?claim rdfs:label ?claimLabel . } OPTIONAL { GRAPH ?cg { ?claim slt:asAidaStatement ?aida . } } OPTIONAL { GRAPH ?cg { ?claim a ?ctype . FILTER(CONTAINS(STR(?ctype),"-FORRT-Claim")) } } } LIMIT 500`;
|
|
246
|
+
// QV: validity guard — OUR Outcomes retracted / invalidated / superseded by a nanopub from the
|
|
247
|
+
// SAME creator (only the original author can retract their own work; a third-party `retracts`
|
|
248
|
+
// must not suppress someone else's). Disapproval is deliberately excluded — that's disagreement,
|
|
249
|
+
// not retraction. Anchored on the Outcome template alone to stay under the endpoint timeout.
|
|
250
|
+
const QV = `PREFIX ntpl: <https://w3id.org/np/o/ntemplate/> PREFIX npx: <http://purl.org/nanopub/x/> PREFIX dct: <http://purl.org/dc/terms/>
|
|
251
|
+
SELECT DISTINCT ?np WHERE { GRAPH ?g { ?np ntpl:wasCreatedFromTemplate <${TPL_OUTCOME}> . } GRAPH ?supg { ?sup ?act ?np . } VALUES ?act { npx:retracts npx:invalidates npx:supersedes } GRAPH ?cg1 { ?sup dct:creator ?cc . } GRAPH ?cg2 { ?np dct:creator ?cc . } }`;
|
|
158
252
|
const A = await sparqlCsv(QA); // sequential: concurrent queries truncate the endpoint
|
|
159
253
|
const B = await sparqlCsv(QB);
|
|
254
|
+
let invalid = new Set();
|
|
255
|
+
try { invalid = new Set((await sparqlCsv(QV)).map((r) => npHash(r.np))); } // best-effort: never break verdicts
|
|
256
|
+
catch (e) { /* no guard this load */ }
|
|
257
|
+
CLAIMS = {};
|
|
258
|
+
try { // claim enrichment is best-effort — never break verdicts
|
|
259
|
+
for (const r of await sparqlCsv(QC)) {
|
|
260
|
+
const h = npHash(r.outcome);
|
|
261
|
+
if (!CLAIMS[h]) CLAIMS[h] = { label: r.claimLabel || "", aida: aidaText(r.aida), type: claimType(r.ctype) };
|
|
262
|
+
}
|
|
263
|
+
} catch (e) { /* claims stay empty; cards still show verdict + why */ }
|
|
160
264
|
const byHash = {};
|
|
161
265
|
for (const r of B) {
|
|
162
266
|
const h = npHash(r.subj);
|
|
@@ -164,9 +268,11 @@ SELECT DISTINCT ?cito ?subj ?rel ?orig WHERE { GRAPH ?g { ?cito ntpl:wasCreatedF
|
|
|
164
268
|
}
|
|
165
269
|
const V = {};
|
|
166
270
|
for (const o of A) {
|
|
271
|
+
if (invalid.has(npHash(o.outcome))) continue; // drop a superseded/retracted Outcome
|
|
167
272
|
const cs = byHash[npHash(o.outcome)] || [];
|
|
168
273
|
const verdictCitos = cs.filter((c) => VERDICT_RELS.has(c.rel) && !c.orig.startsWith("10.5281/"));
|
|
169
|
-
const targets = verdictCitos.length ? verdictCitos
|
|
274
|
+
const targets = verdictCitos.length ? verdictCitos
|
|
275
|
+
: cs.filter((c) => !NONVERDICT_RELS.has((c.rel || "").toLowerCase()) && !c.orig.startsWith("10.5281/"));
|
|
170
276
|
for (const c of targets) {
|
|
171
277
|
(V[c.orig] = V[c.orig] || []).push({
|
|
172
278
|
verdict: o.status || "Published", cito: [c.rel], repo_doi: cleanRepo(o.repo),
|
|
@@ -206,6 +312,23 @@ async function loadVerdicts() {
|
|
|
206
312
|
}));
|
|
207
313
|
}
|
|
208
314
|
|
|
315
|
+
// ---------- curated paper-resolved materials (the links OpenAIRE doesn't hold) ----------
|
|
316
|
+
// curated.json carries only the repo/RO-Crate link, taken from each paper's Data/Code
|
|
317
|
+
// statement (grounded). FAIR is still computed LIVE by assessSoftware() against the repo.
|
|
318
|
+
async function loadCurated() {
|
|
319
|
+
try { CURATED = (await (await fetch("curated.json")).json()).resolved || {}; }
|
|
320
|
+
catch (e) { CURATED = {}; return; }
|
|
321
|
+
// enrich each with its OpenAIRE node (title / impact / year) so it can be injected + ranked
|
|
322
|
+
await Promise.all(Object.entries(CURATED).map(async ([doi, c]) => {
|
|
323
|
+
const rec = await fetchByDoi(doi);
|
|
324
|
+
if (rec) {
|
|
325
|
+
c.title = rec.mainTitle || doi; c.citations = impact(rec).citationCount || 0;
|
|
326
|
+
c.cls = impact(rec).citationClass; c.infl = impact(rec).influenceClass;
|
|
327
|
+
c.impl = impact(rec).impulseClass; c.year = yearOf(rec);
|
|
328
|
+
} else { c.title = c.title || doi; }
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
|
|
209
332
|
// ---------- the radar ----------
|
|
210
333
|
// OpenAIRE returns peer-review reports, comments, errata etc. as "publications" —
|
|
211
334
|
// not replication targets. Drop them, and collapse versions/duplicates by title.
|
|
@@ -232,12 +355,12 @@ async function radar(topic) {
|
|
|
232
355
|
const rank = (r) => [CLASS_SCORE[impact(r).influenceClass] || .2, CLASS_SCORE[impact(r).citationClass] || .2, (impact(r).citationCount || 0)];
|
|
233
356
|
pubs.sort((a, b) => { const A = rank(a), B = rank(b); return (B[0] - A[0]) || (B[1] - A[1]) || (B[2] - A[2]); });
|
|
234
357
|
|
|
235
|
-
const sw = await search(topic, "software", 25); // field-level reusable tooling (shown once, below)
|
|
236
|
-
|
|
237
358
|
const targets = pubs.slice(0, 50).map((p) => {
|
|
238
359
|
const doi = doiOf(p);
|
|
239
360
|
const verified = doi && VERDICTS[doi];
|
|
240
|
-
|
|
361
|
+
let mat = materialsOf(p); // per-paper, grounded (unknown != absent)
|
|
362
|
+
const cur = doi && CURATED[doi]; // paper-resolved materials override (P2)
|
|
363
|
+
if (cur) mat = { score: cur.state === "rocrate" ? 1.0 : 0.6, state: cur.state, code: cur.code || null, resolved: true, data: cur.data || [], source: cur.source || "the paper", lang: cur.lang };
|
|
241
364
|
const impactScore = classScore(p), momentum = momentumScore(p);
|
|
242
365
|
const readiness = readinessFrom(mat.score, impactScore, momentum); // computed for ALL (incl. verified)
|
|
243
366
|
return {
|
|
@@ -289,17 +412,6 @@ async function radar(topic) {
|
|
|
289
412
|
.map((v) => ({ title: v.title, citations: v.citations, status: "VERIFIED" }));
|
|
290
413
|
const chartItems = [...poolItems, ...extra].sort((a, b) => (b.citations || 0) - (a.citations || 0)).slice(0, 12);
|
|
291
414
|
|
|
292
|
-
// FIELD-LEVEL independent tooling — shown ONCE, not per paper (it isn't paper-specific).
|
|
293
|
-
// Reuse-ranked, de-duplicated, and we drop repos merely named after the query.
|
|
294
|
-
const slug = topic.toLowerCase().replace(/\s+/g, "-");
|
|
295
|
-
const seenT = new Set();
|
|
296
|
-
const tooling = sw
|
|
297
|
-
.filter((s) => reuse(s) >= 2 && !(s.mainTitle || "").toLowerCase().includes(slug))
|
|
298
|
-
.sort((a, b) => reuse(b) - reuse(a))
|
|
299
|
-
.map((s) => ({ title: s.mainTitle || "", link: s.codeRepositoryUrl || urlOf(s), swh: swh(s), swhUrl: swhUrlOf(s) }))
|
|
300
|
-
.filter((t) => { const k = t.link || t.title; if (!t.title || seenT.has(k)) return false; seenT.add(k); return true; })
|
|
301
|
-
.slice(0, 5);
|
|
302
|
-
|
|
303
415
|
// Inject matched VERIFIED papers that OpenAIRE's keyword search didn't return, so
|
|
304
416
|
// the paper you replicated always appears in the table (at its replicability rank).
|
|
305
417
|
const have = new Set(targets.map((t) => t.doi));
|
|
@@ -307,21 +419,50 @@ async function radar(topic) {
|
|
|
307
419
|
if (!v.doi || have.has(v.doi)) continue;
|
|
308
420
|
const cs = Math.max(CLASS_SCORE[v.infl] || 0.2, CLASS_SCORE[v.cls] || 0.2);
|
|
309
421
|
const momentum = CLASS_SCORE[v.impl] || 0.2;
|
|
310
|
-
|
|
422
|
+
// verified: the materials score is about reproducing the ORIGINAL (unknown here) — NOT the
|
|
423
|
+
// replication's own repo. Keep that repo only so the FAIR badge can assess it.
|
|
311
424
|
targets.push({
|
|
312
425
|
title: v.title, doi: v.doi, citations: v.citations, cls: v.cls, infl: v.infl,
|
|
313
426
|
year: v.year || null, impl: v.impl || null,
|
|
314
|
-
mat: { score:
|
|
315
|
-
parts: { mat:
|
|
427
|
+
mat: { score: null, state: "unknown", code: (v.repl && v.repl.code) || null },
|
|
428
|
+
parts: { mat: null, impact: cs, momentum },
|
|
316
429
|
status: "VERIFIED",
|
|
317
|
-
readiness: readinessFrom(
|
|
430
|
+
readiness: readinessFrom(null, cs, momentum),
|
|
318
431
|
verification: [...new Set(v.verdicts)].join(", "),
|
|
319
432
|
outcome_np: v.outcome_np,
|
|
320
433
|
});
|
|
321
434
|
}
|
|
322
|
-
|
|
435
|
+
// Inject curated, paper-resolved candidates that match the field but OpenAIRE search missed,
|
|
436
|
+
// so the fully-resolved (materials-verified) papers always appear for their topic.
|
|
437
|
+
for (const [doi, c] of Object.entries(CURATED)) {
|
|
438
|
+
if (targets.some((t) => t.doi === doi)) continue;
|
|
439
|
+
const tw = new Set((c.title || "").toLowerCase().split(/\W+/));
|
|
440
|
+
if (!terms.some((t) => tw.has(t))) continue; // only when it matches the search
|
|
441
|
+
const impactScore = Math.max(CLASS_SCORE[c.infl] || 0.2, CLASS_SCORE[c.cls] || 0.2);
|
|
442
|
+
const momentum = CLASS_SCORE[c.impl] || 0.2;
|
|
443
|
+
const matScore = c.state === "rocrate" ? 1.0 : 0.6;
|
|
444
|
+
targets.push({
|
|
445
|
+
title: c.title, doi, citations: c.citations || 0, cls: c.cls, infl: c.infl,
|
|
446
|
+
year: c.year || null, impl: c.impl || null,
|
|
447
|
+
mat: { score: matScore, state: c.state, code: c.code || null, resolved: true, data: c.data || [], source: c.source || "the paper", lang: c.lang },
|
|
448
|
+
parts: { mat: matScore, impact: impactScore, momentum },
|
|
449
|
+
status: VERDICTS[doi] ? "VERIFIED" : "OPEN",
|
|
450
|
+
readiness: readinessFrom(matScore, impactScore, momentum),
|
|
451
|
+
verification: VERDICTS[doi] ? [...new Set(VERDICTS[doi].map((v) => v.verdict))].join(", ") : null,
|
|
452
|
+
outcome_np: VERDICTS[doi] ? ((VERDICTS[doi].find((v) => v.outcome_np) || {}).outcome_np || null) : null,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
targets.forEach((t) => { t.statusKey = statusOf(t); t.priority = priorityOf(t); });
|
|
456
|
+
// sort by replication PRIORITY — so the number and the order agree (contested/unchecked rise,
|
|
457
|
+
// robustly-validated sinks). No status-rank override.
|
|
458
|
+
targets.sort((a, b) => (b.priority || 0) - (a.priority || 0) || (b.citations || 0) - (a.citations || 0));
|
|
459
|
+
|
|
460
|
+
// FAIR computed LIVE (same assessSoftware the verified path uses) for paper-resolved repos
|
|
461
|
+
await Promise.all(targets.map(async (t) => {
|
|
462
|
+
if (t.mat && t.mat.resolved && t.mat.code && parseGitHub(t.mat.code)) t.fair = await assessSoftware(t.mat.code);
|
|
463
|
+
}));
|
|
323
464
|
|
|
324
|
-
return { topic, targets, inField, chartItems
|
|
465
|
+
return { topic, targets, inField, chartItems };
|
|
325
466
|
}
|
|
326
467
|
|
|
327
468
|
// ---------- rendering ----------
|
|
@@ -329,55 +470,90 @@ const el = (id) => document.getElementById(id);
|
|
|
329
470
|
const esc = (s) => (s || "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
|
330
471
|
|
|
331
472
|
const PER_PAGE = 10;
|
|
332
|
-
let _targets = [], _tpage = 0;
|
|
473
|
+
let _targets = [], _tpage = 0, _tfilter = new Set();
|
|
333
474
|
|
|
334
475
|
function targetRow(t) {
|
|
335
476
|
const p = t.parts || {};
|
|
336
|
-
const scoreTitle =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const score = `<div class="score" title="${esc(scoreTitle)}"><span>${t.
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
477
|
+
const scoreTitle = t.status === "VERIFIED"
|
|
478
|
+
? `replication priority — already checked: impact modulated by agreement (${t.statusKey}). A robustly-validated result sinks (it's settled); a contested one rises (worth re-checking).`
|
|
479
|
+
: `replication priority = 0.45·materials + 0.35·impact + 0.20·momentum — materials ${p.mat == null ? "unverified" : p.mat.toFixed(2)} · impact ${(p.impact || 0).toFixed(2)} · momentum ${(p.momentum || 0).toFixed(2)}`;
|
|
480
|
+
const score = `<div class="score" title="${esc(scoreTitle)}"><span>${t.priority != null ? t.priority.toFixed(2) : "—"}</span><small>PRIORITY</small></div>`;
|
|
481
|
+
const st = STATUS[t.statusKey] || STATUS.needs;
|
|
482
|
+
const badge = `<span class="badge ${st.cls}" title="${esc(st.tip)}">${st.label}</span>`
|
|
483
|
+
+ (t.cls ? `<span class="badge cls" title="OpenAIRE BIP! impact class — C1 = top 0.01% most-cited globally, C5 = the rest">${t.cls}</span>` : "");
|
|
484
|
+
// Materials badge ONLY when positively known. OpenAIRE rarely links code/data to a
|
|
485
|
+
// paper, so 'unknown' is the norm in live search and would be noise on every row —
|
|
486
|
+
// it's carried in the score breakdown tooltip, and resolved in the baked demo set.
|
|
487
|
+
const matMeta = (t.mat && t.mat.state === "rocrate") ? `<span class="badge mok" title="RO-Crate research object — code + data + provenance bundled">RO-Crate ✓</span>`
|
|
488
|
+
: (t.mat && t.mat.state === "code") ? `<span class="badge mok" title="code repository linked to this paper">code ✓</span>`
|
|
489
|
+
: "";
|
|
347
490
|
const meta = `<div class="t-meta">`
|
|
348
491
|
+ (t.year ? `<span class="badge yr">${t.year}</span>` : "")
|
|
349
492
|
+ (t.impl ? `<span class="badge imp" title="OpenAIRE BIP! impulse class — early citation momentum (C1 highest)">impulse ${t.impl}</span>` : "")
|
|
350
493
|
+ matMeta + `</div>`;
|
|
351
494
|
const link = t.doi ? `<a href="https://doi.org/${t.doi}" target="_blank" rel="noopener">${t.doi}</a>` : "";
|
|
352
|
-
|
|
353
|
-
|
|
495
|
+
// what EXACTLY was replicated — the claim's AIDA statement (atomic sentence) + its FORRT type
|
|
496
|
+
const cl = t.status === "VERIFIED" ? claimFor(t.outcome_np) : null;
|
|
497
|
+
const claimLine = (cl && (cl.aida || cl.label))
|
|
498
|
+
? `<div class="tclaim"><span class="claimlbl">claim:</span> <span class="claimq">“${esc(cl.aida || cl.label)}”</span>${cl.type ? ` <span class="badge ctype" title="FORRT claim type">${esc(cl.type)}</span>` : ""}</div>` : "";
|
|
499
|
+
const outs = t.status === "VERIFIED" ? outcomesFor(t.doi).filter((o) => o.np) : [];
|
|
500
|
+
const verdictLink = (t.status === "VERIFIED")
|
|
501
|
+
? `<div class="tverdict">independently checked by Science Live — <b>${esc(agreementOf(t.doi).why)}</b>${outcomeLinks(outs)}</div>` : "";
|
|
502
|
+
const resolvedNote = (t.mat && t.mat.resolved)
|
|
503
|
+
? `<div class="tresolved">↳ materials resolved from ${esc(t.mat.source || "the paper")} (not in OpenAIRE): <a href="${esc(t.mat.code || "")}" target="_blank" rel="noopener">code repo</a>${(t.mat.data && t.mat.data.length) ? ` · data: ${t.mat.data.map(esc).join(", ")}` : ""}</div>`
|
|
504
|
+
: "";
|
|
505
|
+
const fairNote = t.fair
|
|
506
|
+
? `<div class="tfair">FAIR software <b>${t.fair.score}/5</b> · ⭐ ${t.fair.stars}${t.fair.swh ? " · in Software Heritage" : ""}</div>`
|
|
507
|
+
: "";
|
|
508
|
+
// OPEN targets get a next step: discovery here → the FORRT template handles the nanopub chain.
|
|
509
|
+
const replicateCTA = (t.status !== "VERIFIED")
|
|
510
|
+
? `<div class="treplicate"><a href="https://github.com/ScienceLiveHub/forrt-replication-template" target="_blank" rel="noopener" title="Start a replication from the FORRT template — it scaffolds the repo and the signed nanopub chain (Claim · Study · Outcome)">▷ Replicate this with the template →</a></div>`
|
|
511
|
+
: "";
|
|
354
512
|
return `<div class="target ${t.status === "VERIFIED" ? "verified" : ""}">
|
|
355
513
|
${score}
|
|
356
|
-
<div class="t-main">${badge}<br><b>${esc(t.title)}</b>${meta}${verdictLink}</div>
|
|
514
|
+
<div class="t-main">${badge}<br><b>${esc(t.title)}</b>${meta}${claimLine}${verdictLink}${resolvedNote}${fairNote}${replicateCTA}</div>
|
|
357
515
|
<div class="t-right">${t.citations.toLocaleString()} cites<br>${link}</div>
|
|
358
516
|
</div>`;
|
|
359
517
|
}
|
|
360
518
|
|
|
519
|
+
const FILTER_ORDER = ["reproducible", "robust", "validated", "contested", "refuted", "needs", "dormant"];
|
|
520
|
+
const visibleTargets = () => (_tfilter.size ? _targets.filter((t) => _tfilter.has(t.statusKey)) : _targets);
|
|
521
|
+
|
|
522
|
+
function paintFilters() {
|
|
523
|
+
const counts = {};
|
|
524
|
+
for (const t of _targets) counts[t.statusKey] = (counts[t.statusKey] || 0) + 1;
|
|
525
|
+
const present = FILTER_ORDER.filter((k) => counts[k]);
|
|
526
|
+
if (present.length < 2) { el("tfilters").innerHTML = ""; return; } // nothing to filter
|
|
527
|
+
const chip = (on, key, label, count) =>
|
|
528
|
+
`<button class="tfilter${on ? " on" : ""}" onclick="filterTargets(${key ? `'${key}'` : "null"})"${key ? ` title="${esc(STATUS[key].tip)}"` : ""}>${label} <span>${count}</span></button>`;
|
|
529
|
+
el("tfilters").innerHTML = chip(_tfilter.size === 0, null, "All", _targets.length)
|
|
530
|
+
+ present.map((k) => chip(_tfilter.has(k), k, STATUS[k].label, counts[k])).join("");
|
|
531
|
+
}
|
|
532
|
+
|
|
361
533
|
function paintTargets() {
|
|
362
|
-
|
|
534
|
+
paintFilters();
|
|
535
|
+
const list = visibleTargets();
|
|
536
|
+
const pages = Math.max(1, Math.ceil(list.length / PER_PAGE));
|
|
363
537
|
if (_tpage >= pages) _tpage = pages - 1;
|
|
364
|
-
|
|
365
|
-
el("
|
|
538
|
+
if (_tpage < 0) _tpage = 0;
|
|
539
|
+
el("tcount").textContent = `${_targets.length} candidate${_targets.length === 1 ? "" : "s"}`;
|
|
540
|
+
el("targets").innerHTML = list.length
|
|
541
|
+
? list.slice(_tpage * PER_PAGE, _tpage * PER_PAGE + PER_PAGE).map(targetRow).join("")
|
|
542
|
+
: `<p class="hint" style="padding:10px 2px">No candidates in this category — <a href="#" onclick="filterTargets(null);return false">show all</a>.</p>`;
|
|
366
543
|
el("tpager").innerHTML = pages > 1
|
|
367
544
|
? `<button ${_tpage === 0 ? "disabled" : ""} onclick="pageTargets(-1)">← Prev</button><span>Page ${_tpage + 1} of ${pages}</span><button ${_tpage >= pages - 1 ? "disabled" : ""} onclick="pageTargets(1)">Next →</button>`
|
|
368
545
|
: "";
|
|
369
546
|
}
|
|
370
547
|
|
|
371
|
-
function renderTargets(targets) { _targets = targets; _tpage = 0; paintTargets(); }
|
|
548
|
+
function renderTargets(targets) { _targets = targets; _tpage = 0; _tfilter.clear(); paintTargets(); }
|
|
372
549
|
window.pageTargets = (d) => { _tpage += d; paintTargets(); el("targets").scrollIntoView({ behavior: "smooth", block: "start" }); };
|
|
550
|
+
window.filterTargets = (k) => {
|
|
551
|
+
if (k === null) _tfilter.clear();
|
|
552
|
+
else if (_tfilter.has(k)) _tfilter.delete(k);
|
|
553
|
+
else _tfilter.add(k);
|
|
554
|
+
_tpage = 0; paintTargets();
|
|
555
|
+
};
|
|
373
556
|
|
|
374
|
-
function renderTooling(tooling) {
|
|
375
|
-
if (!tooling || !tooling.length) { el("fieldtools").innerHTML = ""; return; }
|
|
376
|
-
const items = tooling.map((t) =>
|
|
377
|
-
`${t.link ? `<a href="${t.link}" target="_blank" rel="noopener">${esc(t.title).slice(0, 40)}</a>` : esc(t.title).slice(0, 40)}${t.swh ? ` <a href="${t.swhUrl || t.link}" target="_blank" rel="noopener" class="swh" title="archived in Software Heritage">⬡</a>` : ""}`
|
|
378
|
-
).join(" · ");
|
|
379
|
-
el("fieldtools").innerHTML = `<b>Independent reusable tooling in this area</b> — engines for replicating by a different route, not tied to any one paper below: ${items}`;
|
|
380
|
-
}
|
|
381
557
|
|
|
382
558
|
function renderVerified(inField) {
|
|
383
559
|
const field = VERIFIED.filter((v) => inField.has(v.doi)).sort((a, b) => b.citations - a.citations);
|
|
@@ -389,7 +565,7 @@ function renderVerified(inField) {
|
|
|
389
565
|
// rich card: the trust edge shown as TWO OpenAIRE nodes (original + replication)
|
|
390
566
|
const matchCard = (v) => {
|
|
391
567
|
const chips = [
|
|
392
|
-
v.oa ? `<span class="ochip oa">${esc(v.oa)} OA</span>` : "",
|
|
568
|
+
v.oa ? `<span class="ochip oa" title="Open-access route (OpenAIRE) — how the paper is free to read: gold/diamond = OA journal, hybrid = OA in a subscription journal, green = self-archived copy, bronze = free on the publisher site with no open licence">${esc(v.oa)} OA</span>` : "",
|
|
393
569
|
...(v.fos || []).map((f) => `<span class="ochip">${esc(f).slice(0, 24)}</span>`),
|
|
394
570
|
...(v.sdg || []).map((s) => `<span class="ochip sdg">${esc(s).slice(0, 22)}</span>`),
|
|
395
571
|
`<span class="ochip cites">${v.citations.toLocaleString()} cites</span>`,
|
|
@@ -406,54 +582,56 @@ function renderVerified(inField) {
|
|
|
406
582
|
} else if (v.repo_doi) {
|
|
407
583
|
repl = `<div class="vrepl muted">↳ replication deposit: <a href="${v.repo_doi.startsWith("http") ? esc(v.repo_doi) : "https://doi.org/" + esc(v.repo_doi)}" target="_blank" rel="noopener">${esc(v.repo_doi)}</a> <span class="ochip wait">awaiting OpenAIRE harvest</span></div>`;
|
|
408
584
|
}
|
|
585
|
+
const vouts = outcomesFor(v.doi).filter((o) => o.np);
|
|
586
|
+
const vcl = claimFor(v.outcome_np);
|
|
587
|
+
const vClaimLine = (vcl && (vcl.aida || vcl.label))
|
|
588
|
+
? `<div class="vclaim"><span class="claimlbl">claim replicated</span> <span class="claimq">“${esc(vcl.aida || vcl.label)}”</span>${vcl.type ? ` <span class="badge ctype" title="FORRT claim type">${esc(vcl.type)}</span>` : ""}</div>` : "";
|
|
409
589
|
return `<li class="match">
|
|
410
590
|
<span class="nodelabel">original paper · OpenAIRE</span>
|
|
411
591
|
<span class="vt">${esc(v.title).slice(0, 82)}</span>
|
|
412
592
|
<div class="ochips">${chips}</div>
|
|
413
|
-
|
|
593
|
+
${vClaimLine}
|
|
594
|
+
<div class="vline"><span class="vv ${partialOf(v) ? "partial" : ""}">${v.verdicts.join(", ")}</span> — independently checked by Science Live ${vouts.length ? outcomeLinks(vouts) : (v.cito_np ? `· <a href="${v.cito_np}" target="_blank" rel="noopener">verdict chain →</a>` : "")}</div>
|
|
414
595
|
${repl}
|
|
415
596
|
</li>`;
|
|
416
597
|
};
|
|
417
598
|
const fieldHtml = field.length
|
|
418
599
|
? `<ul class="vlist">${field.map(matchCard).join("")}</ul>`
|
|
419
600
|
: `<p class="vnone">No Science Live verdict matching your search yet — every paper on the left is an <b>open</b> replication opportunity.</p>`;
|
|
420
|
-
const moreHtml = `<p class="vmore-stat">Verdicts are
|
|
601
|
+
const moreHtml = `<p class="vmore-stat">Verdicts are pulled <b>live</b> from the nanopub network (any signer), filtered for retracted/superseded versions. <a href="https://github.com/ScienceLiveHub/replication-radar/blob/main/src/replication_radar/data/verdicts.json" target="_blank" rel="noopener">offline fallback index →</a></p>`;
|
|
421
602
|
el("verified").innerHTML = fieldHtml + moreHtml;
|
|
422
603
|
}
|
|
423
604
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
},
|
|
452
|
-
},
|
|
453
|
-
});
|
|
605
|
+
// The replication-gap map: a status-composition bar over the ranked list (not a per-paper
|
|
606
|
+
// citation chart — the insight is how MUCH of the field has been checked, not who's most cited).
|
|
607
|
+
const GAP_ORDER = ["reproducible", "robust", "validated", "contested", "refuted", "needs", "dormant"];
|
|
608
|
+
// Distinguishable by hue AND lightness — the two "unchecked" states (needs/dormant) were
|
|
609
|
+
// previously near-identical light blue-greys; now a clear medium slate vs a light warm grey.
|
|
610
|
+
// Platform chart colormap: chart-1 pink, chart-3 green, chart-4 orange, destructive red,
|
|
611
|
+
// chart-5 slate + a light grey for the two "unchecked" states (which should recede).
|
|
612
|
+
// reproducible = brand pink · robust/validated (confirmed) = navy + brighter blue · contested =
|
|
613
|
+
// amber · refuted = red · the two unchecked states = DESATURATED neutral greys (no blue tint) so
|
|
614
|
+
// the navy/blue "checked" segments read clearly apart from them.
|
|
615
|
+
const GAP_COLOR = { reproducible: "#be2e78", robust: "#1f4d8f", validated: "#4a7bc0", contested: "#f59e09", refuted: "#ff6b6b", needs: "#9aa0a8", dormant: "#d7d9dc" };
|
|
616
|
+
const CHECKED = new Set(["robust", "validated", "contested", "refuted"]);
|
|
617
|
+
function renderChart() {
|
|
618
|
+
const counts = {};
|
|
619
|
+
for (const t of _targets) counts[t.statusKey] = (counts[t.statusKey] || 0) + 1;
|
|
620
|
+
const total = _targets.length;
|
|
621
|
+
if (!total) { el("gap").innerHTML = ""; return; }
|
|
622
|
+
const present = GAP_ORDER.filter((k) => counts[k]);
|
|
623
|
+
const checked = present.filter((k) => CHECKED.has(k)).reduce((s, k) => s + counts[k], 0);
|
|
624
|
+
const seg = present.map((k) =>
|
|
625
|
+
`<span class="gapseg" style="width:${(counts[k] / total * 100).toFixed(1)}%;background:${GAP_COLOR[k]}" title="${esc(STATUS[k].label)} — ${counts[k]}"></span>`).join("");
|
|
626
|
+
const key = present.map((k) =>
|
|
627
|
+
`<button class="gapkeyi" onclick="filterTargets('${k}')" title="filter the list to ${esc(STATUS[k].label)}"><i style="background:${GAP_COLOR[k]}"></i>${STATUS[k].label} <b>${counts[k]}</b></button>`).join("");
|
|
628
|
+
const head = checked === 0
|
|
629
|
+
? `<b>0</b> of <b>${total}</b> independently checked — <span class="gapnone">the whole field is an open replication gap</span>`
|
|
630
|
+
: `<b>${checked}</b> of <b>${total}</b> independently checked — <span class="gapsub">the other ${total - checked} are the replication gap</span>`;
|
|
631
|
+
el("gap").innerHTML = `<div class="gaphead">${head}</div><div class="gapbar">${seg}</div><div class="gapkey">${key}</div>`;
|
|
454
632
|
}
|
|
455
633
|
|
|
456
|
-
async function run(topic) {
|
|
634
|
+
async function run(topic, isExample) {
|
|
457
635
|
topic = (topic || "").trim();
|
|
458
636
|
if (!topic) return;
|
|
459
637
|
el("go").disabled = true;
|
|
@@ -462,12 +640,12 @@ async function run(topic) {
|
|
|
462
640
|
const r = await radar(topic);
|
|
463
641
|
el("results").hidden = false;
|
|
464
642
|
renderTargets(r.targets);
|
|
465
|
-
renderTooling(r.tooling);
|
|
466
643
|
renderVerified(r.inField);
|
|
467
|
-
renderChart(
|
|
468
|
-
|
|
644
|
+
renderChart();
|
|
645
|
+
const note = isExample ? ` <i>— showing an example; search your own field above.</i>` : "";
|
|
646
|
+
el("status").innerHTML = (r.inField.size
|
|
469
647
|
? `“${topic}”: ${r.targets.length} candidates · ${r.inField.size} already checked, matching your search (green) — the rest are open.`
|
|
470
|
-
: `“${topic}”: ${r.targets.length} candidates · none matching your search have been checked yet — every one is an open replication opportunity
|
|
648
|
+
: `“${topic}”: ${r.targets.length} candidates · none matching your search have been checked yet — every one is an open replication opportunity.`) + note;
|
|
471
649
|
} catch (e) {
|
|
472
650
|
el("status").textContent = `Could not reach the OpenAIRE Graph (${e.message}). Try again or a shorter topic.`;
|
|
473
651
|
} finally {
|
|
@@ -482,4 +660,12 @@ el("go").addEventListener("click", () => run(el("topic").value));
|
|
|
482
660
|
el("topic").addEventListener("keydown", (e) => { if (e.key === "Enter") run(el("topic").value); });
|
|
483
661
|
|
|
484
662
|
el("status").textContent = "Loading the Science Live verdict layer live from the nanopub network …";
|
|
485
|
-
loadVerdicts().then(() => {
|
|
663
|
+
Promise.all([loadVerdicts(), loadCurated()]).then(() => {
|
|
664
|
+
// Default landing state: populate the radar with a sample field so first-time visitors see the
|
|
665
|
+
// value immediately, not a blank page — but never clobber a user who has already started.
|
|
666
|
+
if (!el("topic").value.trim() && el("results").hidden) {
|
|
667
|
+
run("species distribution", true);
|
|
668
|
+
} else {
|
|
669
|
+
el("status").textContent = "Type a research field and hit Scan — or try an example above.";
|
|
670
|
+
}
|
|
671
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Paper-resolved materials that OpenAIRE does NOT hold as links (verified: search records + relationship layer carry only paper->paper cites for these). Each repo URL is taken from the paper's own Data/Code Availability statement (grounded, not guessed). FAIR is computed LIVE by the app's assessSoftware() against the repo; we only store the resolved link here. This is the P2 'bake-to-static' bridge.",
|
|
3
|
+
"resolved": {
|
|
4
|
+
"10.3389/fevo.2024.1148895": {
|
|
5
|
+
"state": "code",
|
|
6
|
+
"code": "https://github.com/trias-project/risk-modelling-and-mapping",
|
|
7
|
+
"lang": "R",
|
|
8
|
+
"data": ["GBIF", "CHELSA", "EURO-CORDEX", "CORINE"],
|
|
9
|
+
"source": "the paper's Data & Code Availability statement"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Replication Radar — what to replicate next, from the OpenAIRE Graph</title>
|
|
7
|
+
<meta name="description" content="A live tool that turns the OpenAIRE Graph into a ranked replication queue: impact-ranked targets, the claim each one makes, and whether it has already been independently checked — with what verdict." />
|
|
8
|
+
<link rel="stylesheet" href="style.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header>
|
|
12
|
+
<div class="wrap">
|
|
13
|
+
<h1>Replication Radar</h1>
|
|
14
|
+
<p class="tag">What's worth replicating next — read straight off the OpenAIRE Graph.</p>
|
|
15
|
+
<p class="sub">Science Live × OpenAIRE · impact-ranked targets · the claim each makes · already-checked overlay</p>
|
|
16
|
+
</div>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<main class="wrap">
|
|
20
|
+
<section class="search">
|
|
21
|
+
<input id="topic" type="text" placeholder="a topic — e.g. species distribution" autocomplete="off" />
|
|
22
|
+
<button id="go">Scan</button>
|
|
23
|
+
<div class="chips" id="chips"></div>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<p id="status" class="status"></p>
|
|
27
|
+
|
|
28
|
+
<section id="results" hidden>
|
|
29
|
+
<div class="card chartcard">
|
|
30
|
+
<h2>The replication-gap map</h2>
|
|
31
|
+
<p class="hint">How much of the high-impact work matching your search has actually been <b class="verified">independently checked</b> — the field at a glance. Everything <b class="open">not yet checked</b> is the replication gap.</p>
|
|
32
|
+
<div class="chartbox" id="gap"></div>
|
|
33
|
+
</div>
|
|
34
|
+
<aside class="card verifiedcard">
|
|
35
|
+
<h2>Verified, matching your search <span id="vcount" class="pill"></span></h2>
|
|
36
|
+
<p class="hint">The <b class="verified">checked</b> papers matching your search, with the claim that was tested and the verdict — the reliability signal the Graph can't hold. Click any outcome to open its signed nanopub.</p>
|
|
37
|
+
<div id="verified"></div>
|
|
38
|
+
</aside>
|
|
39
|
+
<div class="card">
|
|
40
|
+
<h2>Top replication targets <span class="pill" id="tcount"></span></h2>
|
|
41
|
+
<p class="hint">Candidates matching your search, ranked by <b>replication priority</b> (the PRIORITY score — the number and the order agree). OPEN = an opportunity; VERIFIED = already checked, with its agreement pattern. Paged, 10 at a time. Hover any badge or the score for what it means.</p>
|
|
42
|
+
<div id="tfilters" class="tfilters"></div>
|
|
43
|
+
<div id="targets" class="targets"></div>
|
|
44
|
+
<div id="tpager" class="tpager"></div>
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
</main>
|
|
48
|
+
|
|
49
|
+
<footer>
|
|
50
|
+
<div class="wrap">
|
|
51
|
+
<span>Live against <a href="https://graph.openaire.eu/">OpenAIRE Graph</a> · verdicts from <a href="https://sciencelive4all.org">Science Live</a> nanopublications</span>
|
|
52
|
+
<span><a href="https://github.com/ScienceLiveHub/replication-radar">code (MIT)</a> · <a href="https://pypi.org/project/replication-radar/">PyPI</a> · <a href="https://github.com/ScienceLiveHub/replication-radar/blob/main/STORY.md">story (CC-BY)</a></span>
|
|
53
|
+
</div>
|
|
54
|
+
</footer>
|
|
55
|
+
|
|
56
|
+
<script src="app.js"></script>
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
:root{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
--
|
|
2
|
+
/* Science Live palette — matched to the platform.sciencelive4all.org / sciencelive4all.org
|
|
3
|
+
design tokens (primary pink, navy foreground, chart green/orange/slate, destructive red). */
|
|
4
|
+
--navy:#0c2c59; --navy2:#193967; --ink:#15294a; --mut:#505960;
|
|
5
|
+
--magenta:#be2e78; --magenta-soft:#f7e6ef;
|
|
6
|
+
/* "verified/confirmed" rides on navy (no green) — the ✅ glyph carries the positive cue.
|
|
7
|
+
--verified* keep the old --green* names only so existing rules need no churn. */
|
|
8
|
+
--green:#1f4d8f; --green-soft:#eaf0f9;
|
|
9
|
+
--line:#e2e5ea; --bg:#fafbfc; --card:#fff;
|
|
5
10
|
}
|
|
6
11
|
*{box-sizing:border-box}
|
|
7
12
|
body{margin:0;font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--ink);background:var(--bg)}
|
|
@@ -18,7 +23,7 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
|
|
|
18
23
|
#topic{flex:1;min-width:240px;padding:13px 16px;border:1.5px solid var(--line);border-radius:10px;font-size:16px;background:#fff}
|
|
19
24
|
#topic:focus{outline:none;border-color:var(--magenta)}
|
|
20
25
|
#go{padding:13px 26px;border:0;border-radius:10px;background:var(--magenta);color:#fff;font-size:16px;font-weight:600;cursor:pointer}
|
|
21
|
-
#go:hover{background:#
|
|
26
|
+
#go:hover{background:#a32a6a}
|
|
22
27
|
#go:disabled{opacity:.5;cursor:default}
|
|
23
28
|
.chips{display:flex;gap:8px;flex-wrap:wrap;width:100%}
|
|
24
29
|
.chip{padding:5px 12px;border:1px solid var(--line);border-radius:20px;background:#fff;color:var(--mut);font-size:13px;cursor:pointer}
|
|
@@ -26,29 +31,35 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
|
|
|
26
31
|
|
|
27
32
|
.status{color:var(--mut);min-height:22px;margin:10px 0}
|
|
28
33
|
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
.legend>summary:hover{color:var(--magenta)}
|
|
32
|
-
.legend-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 30px;padding:6px 0 16px;font-size:13px}
|
|
33
|
-
@media(max-width:680px){.legend-grid{grid-template-columns:1fr}}
|
|
34
|
-
.legend-item{display:flex;flex-direction:column;gap:5px}
|
|
35
|
-
.legend-h{font-weight:700;color:var(--navy2);font-size:10.5px;text-transform:uppercase;letter-spacing:.5px}
|
|
36
|
-
.legend-body{color:var(--mut);line-height:1.7}
|
|
37
|
-
.legend-body em{color:#9aa7bd;font-size:12px}
|
|
38
|
-
.vv.contra{color:#b3005f}
|
|
39
|
-
.grid{display:grid;grid-template-columns:1.5fr 1fr;gap:18px;margin-bottom:18px}
|
|
34
|
+
.vv.contra{color:#cf3a3a}
|
|
35
|
+
.grid{display:grid;grid-template-columns:1.5fr 1fr;gap:18px;margin-bottom:18px;align-items:start}
|
|
40
36
|
@media(max-width:820px){.grid{grid-template-columns:1fr}}
|
|
41
37
|
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin-bottom:18px}
|
|
42
38
|
.card h2{margin:0 0 4px;font-size:17px;color:var(--navy)}
|
|
43
39
|
.hint{margin:.2rem 0 14px;font-size:13px;color:var(--mut)}
|
|
44
40
|
.hint .open{color:var(--magenta)} .hint .verified{color:var(--green)}
|
|
45
41
|
.pill{display:inline-block;background:var(--navy);color:#fff;font-size:12px;border-radius:20px;padding:1px 9px;vertical-align:middle}
|
|
46
|
-
.chartbox{position:relative;
|
|
42
|
+
.chartbox{position:relative;margin-top:10px}
|
|
43
|
+
.gaphead{font-size:15px;color:var(--ink);margin-bottom:12px}
|
|
44
|
+
.gaphead b{font-size:22px;color:var(--navy);font-weight:800}
|
|
45
|
+
.gapsub{color:var(--mut);font-size:13px}
|
|
46
|
+
.gapnone{color:var(--magenta);font-weight:600;font-size:13px}
|
|
47
|
+
.gapbar{display:flex;height:28px;border-radius:8px;overflow:hidden;background:#eef2f9;border:1px solid var(--line)}
|
|
48
|
+
.gapseg{display:block;height:100%;min-width:7px}
|
|
49
|
+
.gapseg:not(:last-child){border-right:1.5px solid #fff}
|
|
50
|
+
.gapkey{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:14px}
|
|
51
|
+
.gapkeyi{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--mut);background:none;border:0;padding:0;cursor:pointer}
|
|
52
|
+
.gapkeyi:hover{color:var(--navy2)}
|
|
53
|
+
.gapkeyi i{width:11px;height:11px;border-radius:3px;display:inline-block}
|
|
54
|
+
.gapkeyi b{color:var(--navy2);font-weight:700}
|
|
47
55
|
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
.
|
|
56
|
+
.tfilters{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 14px}
|
|
57
|
+
.tfilters:empty{display:none}
|
|
58
|
+
.tfilter{border:1px solid var(--line);background:#fff;border-radius:20px;padding:5px 12px;font-size:12.5px;font-weight:600;color:var(--mut);cursor:pointer;display:inline-flex;align-items:center;gap:6px;line-height:1.4}
|
|
59
|
+
.tfilter span{background:#eef2f9;color:var(--navy2);border-radius:10px;padding:0 7px;font-size:11px;font-weight:700}
|
|
60
|
+
.tfilter:hover{border-color:var(--magenta);color:var(--navy2)}
|
|
61
|
+
.tfilter.on{border-color:var(--navy2);background:var(--navy);color:#fff}
|
|
62
|
+
.tfilter.on span{background:rgba(255,255,255,.22);color:#fff}
|
|
52
63
|
.targets{display:flex;flex-direction:column;gap:10px}
|
|
53
64
|
.tpager{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:16px;font-size:13px;color:var(--mut)}
|
|
54
65
|
.tpager:empty{display:none}
|
|
@@ -56,22 +67,48 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
|
|
|
56
67
|
.tpager button:hover:not(:disabled){border-color:var(--magenta);color:var(--magenta)}
|
|
57
68
|
.tpager button:disabled{opacity:.4;cursor:default}
|
|
58
69
|
.target{display:grid;grid-template-columns:auto 1fr auto;gap:14px;align-items:center;padding:12px 14px;border:1px solid var(--line);border-radius:10px}
|
|
59
|
-
.target.verified{background:var(--green-soft);border-color:#
|
|
70
|
+
.target.verified{background:var(--green-soft);border-color:#cbd9f0}
|
|
60
71
|
.score{width:54px;height:54px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px;color:#fff;background:var(--magenta);flex-direction:column;line-height:1}
|
|
61
72
|
.score small{font-size:8px;font-weight:600;opacity:.85;letter-spacing:.4px}
|
|
62
73
|
.target.verified .score{background:var(--green)}
|
|
63
74
|
.t-main b{font-size:15px;color:var(--ink)}
|
|
64
75
|
.t-meta{font-size:12.5px;color:var(--mut);margin-top:3px}
|
|
65
76
|
.badge{display:inline-block;font-size:11px;font-weight:700;padding:1px 7px;border-radius:5px;margin-right:6px}
|
|
66
|
-
.badge.open{background:var(--magenta-soft);color:#
|
|
67
|
-
.badge.verified{background:#
|
|
77
|
+
.badge.open{background:var(--magenta-soft);color:#a32a6a}
|
|
78
|
+
.badge.verified{background:#e7eefb;color:var(--green)}
|
|
68
79
|
.badge.cls{background:#eef2f9;color:var(--navy2)}
|
|
69
80
|
.badge.yr{background:#eef2f9;color:var(--mut)}
|
|
70
81
|
.badge.imp{background:#e7edf6;color:var(--navy2)}
|
|
71
82
|
.badge.mok{background:var(--green-soft);color:var(--green)}
|
|
72
|
-
.badge.munk{background:#
|
|
83
|
+
.badge.munk{background:#fdecc8;color:#9a6206}
|
|
73
84
|
.score .unv{font-size:7.5px;font-weight:700;letter-spacing:.2px;opacity:.95}
|
|
85
|
+
.badge.st-ready{background:var(--magenta);color:#fff}
|
|
86
|
+
.badge.st-val{background:#e7eefb;color:var(--green)}
|
|
87
|
+
.badge.st-con{background:#fdecc8;color:#9a6206}
|
|
88
|
+
.badge.st-needs{background:#eef2f9;color:var(--mut)}
|
|
89
|
+
.badge.st-dorm{background:#f0f1f4;color:#9aa7bd}
|
|
90
|
+
.tclaim{font-size:12.5px;color:var(--ink);margin-top:6px;line-height:1.5}
|
|
91
|
+
.claimlbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--green)}
|
|
92
|
+
.claimq{font-style:italic}
|
|
93
|
+
.badge.ctype{background:#e7edf6;color:var(--navy2);text-transform:capitalize}
|
|
74
94
|
.tverdict{font-size:12px;color:var(--mut);margin-top:4px}
|
|
95
|
+
.onp{display:inline-block;min-width:18px;text-align:center;border:1px solid var(--line);border-radius:4px;padding:0 6px;margin:0 2px;font-weight:700;font-size:11px;line-height:1.6}
|
|
96
|
+
.onp:hover{text-decoration:none}
|
|
97
|
+
/* white body so the chip is visible on tinted (verified) cards; verdict lives in border+text+hover */
|
|
98
|
+
.onp.v-ok{border-color:#9db6df;color:var(--green);background:#fff}
|
|
99
|
+
.onp.v-ok:hover{background:var(--green);color:#fff;border-color:var(--green)}
|
|
100
|
+
.onp.v-part{border-color:#f0c06a;color:#9a6206;background:#fff}
|
|
101
|
+
.onp.v-part:hover{background:#f59e09;color:#fff;border-color:#f59e09}
|
|
102
|
+
.onp.v-con{border-color:#eaa6a0;color:#cf3a3a;background:#fff}
|
|
103
|
+
.onp.v-con:hover{background:#d64545;color:#fff;border-color:#d64545}
|
|
104
|
+
.onp.v-other{border-color:var(--line);color:var(--mut);background:#fff}
|
|
105
|
+
.tresolved{font-size:12px;color:var(--green);margin-top:4px}
|
|
106
|
+
.tresolved a{font-weight:600}
|
|
107
|
+
.tfair{font-size:12px;color:var(--mut);margin-top:3px}
|
|
108
|
+
.tfair b{color:var(--navy2)}
|
|
109
|
+
.treplicate{margin-top:7px}
|
|
110
|
+
.treplicate a{font-size:12px;font-weight:700;background:var(--magenta-soft);color:#a32a6a;padding:4px 11px;border-radius:7px;display:inline-block}
|
|
111
|
+
.treplicate a:hover{background:var(--magenta);color:#fff;text-decoration:none}
|
|
75
112
|
.tool{font-size:12px;color:var(--mut)}
|
|
76
113
|
.tool .swh{color:var(--green);font-weight:600}
|
|
77
114
|
.t-right{text-align:right;font-size:12px;color:var(--mut);white-space:nowrap}
|
|
@@ -82,18 +119,19 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
|
|
|
82
119
|
.vlist li:last-child{border:0}
|
|
83
120
|
.vt{color:var(--ink);font-weight:600;display:block;margin-bottom:2px}
|
|
84
121
|
.vv{color:var(--green);font-weight:700;font-size:12px}
|
|
85
|
-
.vv.partial{color:#
|
|
122
|
+
.vv.partial{color:#9a6206}
|
|
86
123
|
#verified a{font-size:12px}
|
|
87
124
|
.vlist li.match{background:var(--green-soft);margin:0 -8px 10px;padding:12px 13px;border-radius:8px;border:0}
|
|
88
125
|
.nodelabel{display:block;font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--green);margin-bottom:3px}
|
|
89
126
|
.ochips{display:flex;flex-wrap:wrap;gap:5px;margin:6px 0}
|
|
90
127
|
.ochip{font-size:11px;background:#e7edf6;color:var(--navy2);border-radius:5px;padding:1px 7px;white-space:nowrap}
|
|
91
|
-
.ochip.oa{background:#
|
|
128
|
+
.ochip.oa{background:#e7edf6;color:var(--navy2);text-transform:capitalize}
|
|
92
129
|
.ochip.sdg{background:#fff1d3;color:#8a6d1b}
|
|
93
130
|
.ochip.type{background:var(--navy);color:#fff;text-transform:capitalize}
|
|
94
131
|
.ochip.cites{background:transparent;color:var(--mut);padding-left:0}
|
|
132
|
+
.vclaim{font-size:12.5px;color:var(--ink);margin:7px 0 4px;line-height:1.5}
|
|
95
133
|
.vline{font-size:12.5px;color:var(--mut);margin-top:5px}
|
|
96
|
-
.vrepl{font-size:12.5px;margin-top:8px;padding-top:8px;border-top:1px dashed #
|
|
134
|
+
.vrepl{font-size:12.5px;margin-top:8px;padding-top:8px;border-top:1px dashed #cbd9f0;color:var(--ink)}
|
|
97
135
|
.vrepl.muted{color:var(--mut);border-top-color:var(--line)}
|
|
98
136
|
.vrepl a{font-weight:600}
|
|
99
137
|
.fairline{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--mut);margin-top:6px}
|
|
@@ -101,13 +139,13 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
|
|
|
101
139
|
.fairrecs{display:flex;flex-wrap:wrap;align-items:center;gap:3px 10px;font-size:11.5px;margin-top:5px}
|
|
102
140
|
.fairrecs b{color:var(--navy2);font-size:12px}
|
|
103
141
|
.fairrecs .rok{color:var(--green)}
|
|
104
|
-
.fairrecs .rno{color:#
|
|
142
|
+
.fairrecs .rno{color:#9a6206}
|
|
105
143
|
.fairbar{display:inline-block;width:64px;height:7px;border-radius:4px;background:#e4e9f2;overflow:hidden}
|
|
106
144
|
.fairbar i{display:block;height:100%;background:var(--green)}
|
|
107
145
|
.swhok{color:var(--green);font-weight:600}
|
|
108
|
-
.swhno{color:#
|
|
109
|
-
.ochip.wait{background:#
|
|
110
|
-
.vnone{color:#
|
|
146
|
+
.swhno{color:#9a6206}
|
|
147
|
+
.ochip.wait{background:#fdecc8;color:#9a6206}
|
|
148
|
+
.vnone{color:#a32a6a;background:var(--magenta-soft);padding:12px 14px;border-radius:8px;font-size:13.5px}
|
|
111
149
|
.vmore-stat{margin-top:14px;padding-top:12px;border-top:1px solid var(--line);font-size:12.5px;color:var(--mut)}
|
|
112
150
|
|
|
113
151
|
footer{border-top:1px solid var(--line);margin-top:30px;padding:20px 0;color:var(--mut);font-size:13px;background:#fff}
|
|
@@ -7,4 +7,4 @@ OpenAIRE impact + Software Heritage reuse signals + Science Live nanopub verdict
|
|
|
7
7
|
from .radar import radar, find_independent_software, replication_status, verified_claims
|
|
8
8
|
|
|
9
9
|
__all__ = ["radar", "find_independent_software", "replication_status", "verified_claims"]
|
|
10
|
-
__version__ = "0.3.
|
|
10
|
+
__version__ = "0.3.1"
|
|
@@ -21,6 +21,12 @@ SPARQL = os.environ.get("RADAR_NANOPUB_SPARQL", "https://query.knowledgepixels.c
|
|
|
21
21
|
TPL_OUTCOME = "https://w3id.org/np/RA2zljn0Nw9SadppOyxZoh-_Rxosslrq-vYG-p9SttnJE"
|
|
22
22
|
TPL_CITO = "https://w3id.org/np/RA43F9EoOuzF0xoNUnCMNyFsfIqlsuWDdPHCnN0wCdCAw"
|
|
23
23
|
VERDICT_RELS = {"confirms", "qualifies", "disputes", "critiques", "extends", "supports", "refutes"}
|
|
24
|
+
# CiTO relations that are METHOD/DATA/CREDIT provenance — they point to a SOURCE paper, not a
|
|
25
|
+
# verdict on it. A verdict must NEVER attach via these: a study that `usesMethodIn` Phillips 2009
|
|
26
|
+
# and was Contradicted does not contradict Phillips 2009 (it reused its method). (lowercased compare)
|
|
27
|
+
NONVERDICT_RELS = {"usesmethodin", "usesdatafrom", "citesasdatasource", "citesasevidence", "credits",
|
|
28
|
+
"citesforinformation", "obtainsbackgroundfrom", "obtainssupportfrom", "citesasauthority",
|
|
29
|
+
"citesasrelated", "citesassourcedocument", "includesquotationfrom", "sharesauthorinstitutionwith"}
|
|
24
30
|
_CANON = {"validated": "Validated", "partiallysupported": "PartiallySupported", "contradicted": "Contradicted",
|
|
25
31
|
"notsupported": "NotSupported", "mixed": "Mixed", "inconclusive": "Inconclusive"}
|
|
26
32
|
_TIMEOUT = float(os.environ.get("RADAR_HTTP_TIMEOUT", "30"))
|
|
@@ -31,6 +37,18 @@ SELECT DISTINCT ?outcome ?status ?repo WHERE {{ GRAPH ?g {{ ?outcome ntpl:wasCre
|
|
|
31
37
|
_QB = f"""PREFIX np: <http://www.nanopub.org/nschema#> PREFIX ntpl: <https://w3id.org/np/o/ntemplate/> PREFIX cito: <http://purl.org/spar/cito/>
|
|
32
38
|
SELECT DISTINCT ?cito ?subj ?rel ?orig WHERE {{ GRAPH ?g {{ ?cito ntpl:wasCreatedFromTemplate <{TPL_CITO}> . }} ?cito np:hasAssertion ?ca . GRAPH ?ca {{ ?subj ?rel ?orig . }} FILTER(STRSTARTS(STR(?rel),STR(cito:))) FILTER(CONTAINS(STR(?orig),"doi.org/10.")) }} LIMIT 3000"""
|
|
33
39
|
|
|
40
|
+
# Validity guard, run as its OWN lightweight query (anchored on the Outcome template, so it stays
|
|
41
|
+
# fast — anchoring on two templates, or an inline FILTER NOT EXISTS on the full repo, times the
|
|
42
|
+
# endpoint out at 504). Returns OUR Outcomes that have been retracted / invalidated / superseded by
|
|
43
|
+
# a nanopub from the SAME creator. Same-creator is essential: only the original author can retract
|
|
44
|
+
# their own work — a third party publishing `retracts` must not be able to suppress someone else's.
|
|
45
|
+
# Disapproval (`disapprovesOf`) is deliberately NOT here: that is a third party disagreeing, not a
|
|
46
|
+
# retraction, so it must never hide a verdict. (Only Outcomes are affected in the current network;
|
|
47
|
+
# extend VALUES ?tpl to the CiTO template if a superseded CiTO ever appears — but keep it one
|
|
48
|
+
# query per template to stay under the endpoint timeout.)
|
|
49
|
+
_QV = f"""PREFIX ntpl: <https://w3id.org/np/o/ntemplate/> PREFIX npx: <http://purl.org/nanopub/x/> PREFIX dct: <http://purl.org/dc/terms/>
|
|
50
|
+
SELECT DISTINCT ?np WHERE {{ GRAPH ?g {{ ?np ntpl:wasCreatedFromTemplate <{TPL_OUTCOME}> . }} GRAPH ?supg {{ ?sup ?act ?np . }} VALUES ?act {{ npx:retracts npx:invalidates npx:supersedes }} GRAPH ?cg1 {{ ?sup dct:creator ?cc . }} GRAPH ?cg2 {{ ?np dct:creator ?cc . }} }}"""
|
|
51
|
+
|
|
34
52
|
|
|
35
53
|
def _sparql(query: str) -> list[dict]:
|
|
36
54
|
url = f"{SPARQL}?{urllib.parse.urlencode({'query': query})}"
|
|
@@ -64,6 +82,10 @@ def build_index() -> dict:
|
|
|
64
82
|
"""
|
|
65
83
|
outcomes = _sparql(_QA) # sequential: concurrent queries truncate the endpoint
|
|
66
84
|
citos = _sparql(_QB)
|
|
85
|
+
try: # best-effort: a guard timeout must not
|
|
86
|
+
invalid = {_hash(r.get("np")) for r in _sparql(_QV)} # take down live verdicts
|
|
87
|
+
except Exception:
|
|
88
|
+
invalid = set()
|
|
67
89
|
by_hash: dict[str, list] = {}
|
|
68
90
|
for r in citos:
|
|
69
91
|
by_hash.setdefault(_hash(r.get("subj")), []).append({
|
|
@@ -73,9 +95,11 @@ def build_index() -> dict:
|
|
|
73
95
|
})
|
|
74
96
|
index: dict[str, list] = {}
|
|
75
97
|
for o in outcomes:
|
|
98
|
+
if _hash(o.get("outcome")) in invalid: # drop a superseded/retracted Outcome
|
|
99
|
+
continue
|
|
76
100
|
cs = by_hash.get(_hash(o.get("outcome")), [])
|
|
77
101
|
verdict_citos = [c for c in cs if c["rel"] in VERDICT_RELS and not c["orig"].startswith("10.5281/")]
|
|
78
|
-
targets = verdict_citos or [c for c in cs if not c["orig"].startswith("10.5281/")]
|
|
102
|
+
targets = verdict_citos or [c for c in cs if c["rel"].lower() not in NONVERDICT_RELS and not c["orig"].startswith("10.5281/")]
|
|
79
103
|
repo_doi = _clean_repo(o.get("repo") or "")
|
|
80
104
|
for c in targets:
|
|
81
105
|
index.setdefault(c["orig"], []).append({
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>Replication Radar — what to replicate next, from the OpenAIRE Graph</title>
|
|
7
|
-
<meta name="description" content="A live tool that turns the OpenAIRE Graph into a ranked replication queue: impact-ranked targets, independent reusable tooling, and whether a claim has already been checked." />
|
|
8
|
-
<link rel="stylesheet" href="style.css" />
|
|
9
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
10
|
-
</head>
|
|
11
|
-
<body>
|
|
12
|
-
<header>
|
|
13
|
-
<div class="wrap">
|
|
14
|
-
<h1>Replication Radar</h1>
|
|
15
|
-
<p class="tag">What's worth replicating next — read straight off the OpenAIRE Graph.</p>
|
|
16
|
-
<p class="sub">Science Live × OpenAIRE · impact-ranked targets · independent tooling · already-checked overlay</p>
|
|
17
|
-
</div>
|
|
18
|
-
</header>
|
|
19
|
-
|
|
20
|
-
<main class="wrap">
|
|
21
|
-
<section class="search">
|
|
22
|
-
<input id="topic" type="text" placeholder="a topic — e.g. species distribution" autocomplete="off" />
|
|
23
|
-
<button id="go">Scan</button>
|
|
24
|
-
<div class="chips" id="chips"></div>
|
|
25
|
-
</section>
|
|
26
|
-
|
|
27
|
-
<p id="status" class="status"></p>
|
|
28
|
-
|
|
29
|
-
<section id="results" hidden>
|
|
30
|
-
<div class="grid">
|
|
31
|
-
<div class="card chartcard">
|
|
32
|
-
<h2>The replication-gap map</h2>
|
|
33
|
-
<p class="hint">High-impact papers matching your search, ranked by citations. <b class="verified">Green = already checked</b> by Science Live; <b class="open">pink = open</b> — a replication opportunity. All-pink means nothing here has been independently checked yet.</p>
|
|
34
|
-
<div class="chartbox"><canvas id="gap"></canvas></div>
|
|
35
|
-
</div>
|
|
36
|
-
<aside class="card verifiedcard">
|
|
37
|
-
<h2>Verified, matching your search <span id="vcount" class="pill"></span></h2>
|
|
38
|
-
<p class="hint">Science Live replications matching your search — these are the <b class="verified">green bars</b>. The verdict is the reliability signal the Graph can't hold; click through to the signed nanopub.</p>
|
|
39
|
-
<div id="verified"></div>
|
|
40
|
-
</aside>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<details class="legend" open>
|
|
44
|
-
<summary>Legend — what the labels mean</summary>
|
|
45
|
-
<div class="legend-grid">
|
|
46
|
-
<div class="legend-item">
|
|
47
|
-
<span class="legend-h">Impact class · OpenAIRE BIP!</span>
|
|
48
|
-
<span class="legend-body">
|
|
49
|
-
<span class="ochip">C1</span> top 0.01% <span class="ochip">C2</span> top 0.1% <span class="ochip">C3</span> top 1% <span class="ochip">C4</span> top 10% <span class="ochip">C5</span> the rest
|
|
50
|
-
<em>— global percentile of citations, across all of science</em>
|
|
51
|
-
</span>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="legend-item">
|
|
54
|
-
<span class="legend-h">Status</span>
|
|
55
|
-
<span class="legend-body"><span class="badge open">OPEN</span> a replication opportunity <span class="badge verified">VERIFIED</span> already independently checked</span>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="legend-item">
|
|
58
|
-
<span class="legend-h">Verdict · Science Live</span>
|
|
59
|
-
<span class="legend-body"><span class="vv">Validated</span> held up <span class="vv partial">PartiallySupported</span> in part <span class="vv contra">Contradicted</span> did not hold</span>
|
|
60
|
-
</div>
|
|
61
|
-
<div class="legend-item">
|
|
62
|
-
<span class="legend-h">Replicability (the REPLIC. score)</span>
|
|
63
|
-
<span class="legend-body">a 0–1 potential = impact × independent tooling available × reference data — higher means more worth, and more feasible, to replicate. Shown for every paper, including already-verified ones.</span>
|
|
64
|
-
</div>
|
|
65
|
-
<div class="legend-item">
|
|
66
|
-
<span class="legend-h">Open access · OpenAIRE</span>
|
|
67
|
-
<span class="legend-body">All mean the paper is <b>free to read</b>; they differ in <b>who paid to publish</b>: <b>gold</b> OA journal (author/funder fee) · <b>hybrid</b> OA in a subscription journal (author fee) · <b>green</b> free self-archived copy · <b>bronze</b> free on the publisher site, no open licence · <b>diamond</b> no fee for authors or readers</span>
|
|
68
|
-
</div>
|
|
69
|
-
<div class="legend-item">
|
|
70
|
-
<span class="legend-h">Subject tags · OpenAIRE</span>
|
|
71
|
-
<span class="legend-body">auto-classified by OpenAIRE: research field (e.g. <em>biological sciences</em>) and UN Sustainable Development Goal (e.g. <em>13. Climate action</em>)</span>
|
|
72
|
-
</div>
|
|
73
|
-
<div class="legend-item">
|
|
74
|
-
<span class="legend-h">FAIR-software · fair-software.eu</span>
|
|
75
|
-
<span class="legend-body">a replication's code scored N/5 on the <b>fair-software.eu</b> recommendations — public repository · open license · in a registry · citable (CITATION.cff/DOI) · quality artefacts — computed live from GitHub + Software Heritage. Hover the score for the breakdown.</span>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</details>
|
|
79
|
-
|
|
80
|
-
<div class="card">
|
|
81
|
-
<h2>Top replication targets <span class="pill" id="tcount"></span></h2>
|
|
82
|
-
<p class="hint">Candidates matching your search, ranked by <b>replicability</b> potential (the REPLIC. score). OPEN = an opportunity; VERIFIED = already done (shown at its rank, not buried). Paged, 10 at a time. See the legend above for the badges.</p>
|
|
83
|
-
<p id="fieldtools" class="fieldtools"></p>
|
|
84
|
-
<div id="targets" class="targets"></div>
|
|
85
|
-
<div id="tpager" class="tpager"></div>
|
|
86
|
-
</div>
|
|
87
|
-
</section>
|
|
88
|
-
</main>
|
|
89
|
-
|
|
90
|
-
<footer>
|
|
91
|
-
<div class="wrap">
|
|
92
|
-
<span>Live against <a href="https://graph.openaire.eu/">OpenAIRE Graph</a> · verdicts from <a href="https://sciencelive4all.org">Science Live</a> nanopublications</span>
|
|
93
|
-
<span><a href="https://github.com/ScienceLiveHub/replication-radar">code (MIT)</a> · <a href="https://pypi.org/project/replication-radar/">PyPI</a> · <a href="https://github.com/ScienceLiveHub/replication-radar/blob/main/STORY.md">story (CC-BY)</a></span>
|
|
94
|
-
</div>
|
|
95
|
-
</footer>
|
|
96
|
-
|
|
97
|
-
<script src="app.js"></script>
|
|
98
|
-
</body>
|
|
99
|
-
</html>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{replication_radar-0.3.0 → replication_radar-0.3.2}/src/replication_radar/data/verdicts.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|