replication-radar 0.3.1__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.1 → replication_radar-0.3.2}/PKG-INFO +2 -2
- {replication_radar-0.3.1 → replication_radar-0.3.2}/pyproject.toml +2 -2
- {replication_radar-0.3.1 → replication_radar-0.3.2}/site/app.js +203 -93
- replication_radar-0.3.2/site/index.html +58 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/site/style.css +62 -33
- {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/network.py +18 -0
- replication_radar-0.3.1/site/index.html +0 -99
- {replication_radar-0.3.1 → replication_radar-0.3.2}/.github/workflows/publish-pypi.yml +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/.gitignore +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/CLAUDE.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/LICENSE +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/README.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/STORY.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/demo_sdm.py +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/app-ui.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/link-types.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/next-layers-plan.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/openaire-mcp.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/readiness-scoring-plan.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/netlify.toml +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/scripts/build_verdicts.py +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/site/README.md +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/site/curated.json +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/site/verdicts.json +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/__init__.py +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/data/verdicts.json +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/openaire.py +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/radar.py +0 -0
- {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/server.py +0 -0
- {replication_radar-0.3.1 → 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
|
|
@@ -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,9 +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
12
|
let CURATED = {}; // doi -> paper-resolved materials (the links OpenAIRE lacks; from the paper)
|
|
12
|
-
let chart = null;
|
|
13
13
|
|
|
14
14
|
// ---------- OpenAIRE helpers (same shape as openaire.py) ----------
|
|
15
15
|
const doiOf = (r) => {
|
|
@@ -57,26 +57,85 @@ const materialsOf = (r) => {
|
|
|
57
57
|
// Transparent composite: materials is the largest term; when materials is UNKNOWN
|
|
58
58
|
// (null) we DROP that term and renormalise the rest — never score a missing link as 0.
|
|
59
59
|
const readinessFrom = (matScore, impactScore, momentum) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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;
|
|
63
64
|
return Math.round(r * 100) / 100;
|
|
64
65
|
};
|
|
65
66
|
// Status taxonomy — "not replicated" is DISAMBIGUATED, not penalised.
|
|
66
67
|
const STATUS = {
|
|
67
|
-
|
|
68
|
-
validated:
|
|
69
|
-
contested:
|
|
70
|
-
|
|
71
|
-
|
|
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" };
|
|
72
118
|
};
|
|
73
119
|
const statusOf = (t) => {
|
|
74
|
-
if (t.status === "VERIFIED") return
|
|
75
|
-
if (t.mat && t.mat.score != null) return "
|
|
76
|
-
const old = t.year && (new Date().getFullYear() - t.year) >= 8;
|
|
77
|
-
const hot = t.impl === "C1" || t.impl === "C2";
|
|
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
|
|
78
124
|
return (old && !hot) ? "dormant" : "needs";
|
|
79
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
|
+
};
|
|
80
139
|
|
|
81
140
|
async function search(topic, type, size) {
|
|
82
141
|
const u = `${API}/researchProducts?search=${encodeURIComponent(topic)}&type=${type}&pageSize=${size}`;
|
|
@@ -158,6 +217,11 @@ const NONVERDICT_RELS = new Set(["usesmethodin", "usesdatafrom", "citesasdatasou
|
|
|
158
217
|
const npHash = (u) => (u || "").replace(/.*\/np\//, "");
|
|
159
218
|
const doiPart = (u) => (u || "").replace(/.*doi\.org\//, "").toLowerCase();
|
|
160
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;
|
|
161
225
|
|
|
162
226
|
async function sparqlCsv(query) {
|
|
163
227
|
const r = await fetch(`${NP_SPARQL}?query=${encodeURIComponent(query)}`, { headers: { Accept: "text/csv" } });
|
|
@@ -175,8 +239,28 @@ async function buildIndexFromNetwork() {
|
|
|
175
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) }`;
|
|
176
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/>
|
|
177
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 . } }`;
|
|
178
252
|
const A = await sparqlCsv(QA); // sequential: concurrent queries truncate the endpoint
|
|
179
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 */ }
|
|
180
264
|
const byHash = {};
|
|
181
265
|
for (const r of B) {
|
|
182
266
|
const h = npHash(r.subj);
|
|
@@ -184,6 +268,7 @@ SELECT DISTINCT ?cito ?subj ?rel ?orig WHERE { GRAPH ?g { ?cito ntpl:wasCreatedF
|
|
|
184
268
|
}
|
|
185
269
|
const V = {};
|
|
186
270
|
for (const o of A) {
|
|
271
|
+
if (invalid.has(npHash(o.outcome))) continue; // drop a superseded/retracted Outcome
|
|
187
272
|
const cs = byHash[npHash(o.outcome)] || [];
|
|
188
273
|
const verdictCitos = cs.filter((c) => VERDICT_RELS.has(c.rel) && !c.orig.startsWith("10.5281/"));
|
|
189
274
|
const targets = verdictCitos.length ? verdictCitos
|
|
@@ -270,8 +355,6 @@ async function radar(topic) {
|
|
|
270
355
|
const rank = (r) => [CLASS_SCORE[impact(r).influenceClass] || .2, CLASS_SCORE[impact(r).citationClass] || .2, (impact(r).citationCount || 0)];
|
|
271
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]); });
|
|
272
357
|
|
|
273
|
-
const sw = await search(topic, "software", 25); // field-level reusable tooling (shown once, below)
|
|
274
|
-
|
|
275
358
|
const targets = pubs.slice(0, 50).map((p) => {
|
|
276
359
|
const doi = doiOf(p);
|
|
277
360
|
const verified = doi && VERDICTS[doi];
|
|
@@ -329,17 +412,6 @@ async function radar(topic) {
|
|
|
329
412
|
.map((v) => ({ title: v.title, citations: v.citations, status: "VERIFIED" }));
|
|
330
413
|
const chartItems = [...poolItems, ...extra].sort((a, b) => (b.citations || 0) - (a.citations || 0)).slice(0, 12);
|
|
331
414
|
|
|
332
|
-
// FIELD-LEVEL independent tooling — shown ONCE, not per paper (it isn't paper-specific).
|
|
333
|
-
// Reuse-ranked, de-duplicated, and we drop repos merely named after the query.
|
|
334
|
-
const slug = topic.toLowerCase().replace(/\s+/g, "-");
|
|
335
|
-
const seenT = new Set();
|
|
336
|
-
const tooling = sw
|
|
337
|
-
.filter((s) => reuse(s) >= 2 && !(s.mainTitle || "").toLowerCase().includes(slug))
|
|
338
|
-
.sort((a, b) => reuse(b) - reuse(a))
|
|
339
|
-
.map((s) => ({ title: s.mainTitle || "", link: s.codeRepositoryUrl || urlOf(s), swh: swh(s), swhUrl: swhUrlOf(s) }))
|
|
340
|
-
.filter((t) => { const k = t.link || t.title; if (!t.title || seenT.has(k)) return false; seenT.add(k); return true; })
|
|
341
|
-
.slice(0, 5);
|
|
342
|
-
|
|
343
415
|
// Inject matched VERIFIED papers that OpenAIRE's keyword search didn't return, so
|
|
344
416
|
// the paper you replicated always appears in the table (at its replicability rank).
|
|
345
417
|
const have = new Set(targets.map((t) => t.doi));
|
|
@@ -347,14 +419,15 @@ async function radar(topic) {
|
|
|
347
419
|
if (!v.doi || have.has(v.doi)) continue;
|
|
348
420
|
const cs = Math.max(CLASS_SCORE[v.infl] || 0.2, CLASS_SCORE[v.cls] || 0.2);
|
|
349
421
|
const momentum = CLASS_SCORE[v.impl] || 0.2;
|
|
350
|
-
|
|
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.
|
|
351
424
|
targets.push({
|
|
352
425
|
title: v.title, doi: v.doi, citations: v.citations, cls: v.cls, infl: v.infl,
|
|
353
426
|
year: v.year || null, impl: v.impl || null,
|
|
354
|
-
mat: { score:
|
|
355
|
-
parts: { mat:
|
|
427
|
+
mat: { score: null, state: "unknown", code: (v.repl && v.repl.code) || null },
|
|
428
|
+
parts: { mat: null, impact: cs, momentum },
|
|
356
429
|
status: "VERIFIED",
|
|
357
|
-
readiness: readinessFrom(
|
|
430
|
+
readiness: readinessFrom(null, cs, momentum),
|
|
358
431
|
verification: [...new Set(v.verdicts)].join(", "),
|
|
359
432
|
outcome_np: v.outcome_np,
|
|
360
433
|
});
|
|
@@ -379,16 +452,17 @@ async function radar(topic) {
|
|
|
379
452
|
outcome_np: VERDICTS[doi] ? ((VERDICTS[doi].find((v) => v.outcome_np) || {}).outcome_np || null) : null,
|
|
380
453
|
});
|
|
381
454
|
}
|
|
382
|
-
targets.forEach((t) => { t.statusKey = statusOf(t); });
|
|
383
|
-
//
|
|
384
|
-
|
|
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));
|
|
385
459
|
|
|
386
460
|
// FAIR computed LIVE (same assessSoftware the verified path uses) for paper-resolved repos
|
|
387
461
|
await Promise.all(targets.map(async (t) => {
|
|
388
462
|
if (t.mat && t.mat.resolved && t.mat.code && parseGitHub(t.mat.code)) t.fair = await assessSoftware(t.mat.code);
|
|
389
463
|
}));
|
|
390
464
|
|
|
391
|
-
return { topic, targets, inField, chartItems
|
|
465
|
+
return { topic, targets, inField, chartItems };
|
|
392
466
|
}
|
|
393
467
|
|
|
394
468
|
// ---------- rendering ----------
|
|
@@ -396,16 +470,16 @@ const el = (id) => document.getElementById(id);
|
|
|
396
470
|
const esc = (s) => (s || "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
|
397
471
|
|
|
398
472
|
const PER_PAGE = 10;
|
|
399
|
-
let _targets = [], _tpage = 0;
|
|
473
|
+
let _targets = [], _tpage = 0, _tfilter = new Set();
|
|
400
474
|
|
|
401
475
|
function targetRow(t) {
|
|
402
476
|
const p = t.parts || {};
|
|
403
|
-
const scoreTitle =
|
|
404
|
-
|
|
405
|
-
|
|
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>`;
|
|
406
481
|
const st = STATUS[t.statusKey] || STATUS.needs;
|
|
407
482
|
const badge = `<span class="badge ${st.cls}" title="${esc(st.tip)}">${st.label}</span>`
|
|
408
|
-
+ (t.status === "VERIFIED" && t.verification ? `<span class="badge cls">${esc(t.verification)}</span>` : "")
|
|
409
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>` : "");
|
|
410
484
|
// Materials badge ONLY when positively known. OpenAIRE rarely links code/data to a
|
|
411
485
|
// paper, so 'unknown' is the norm in live search and would be noise on every row —
|
|
@@ -418,42 +492,68 @@ function targetRow(t) {
|
|
|
418
492
|
+ (t.impl ? `<span class="badge imp" title="OpenAIRE BIP! impulse class — early citation momentum (C1 highest)">impulse ${t.impl}</span>` : "")
|
|
419
493
|
+ matMeta + `</div>`;
|
|
420
494
|
const link = t.doi ? `<a href="https://doi.org/${t.doi}" target="_blank" rel="noopener">${t.doi}</a>` : "";
|
|
421
|
-
|
|
422
|
-
|
|
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>` : "";
|
|
423
502
|
const resolvedNote = (t.mat && t.mat.resolved)
|
|
424
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>`
|
|
425
504
|
: "";
|
|
426
505
|
const fairNote = t.fair
|
|
427
506
|
? `<div class="tfair">FAIR software <b>${t.fair.score}/5</b> · ⭐ ${t.fair.stars}${t.fair.swh ? " · in Software Heritage" : ""}</div>`
|
|
428
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
|
+
: "";
|
|
429
512
|
return `<div class="target ${t.status === "VERIFIED" ? "verified" : ""}">
|
|
430
513
|
${score}
|
|
431
|
-
<div class="t-main">${badge}<br><b>${esc(t.title)}</b>${meta}${verdictLink}${resolvedNote}${fairNote}</div>
|
|
514
|
+
<div class="t-main">${badge}<br><b>${esc(t.title)}</b>${meta}${claimLine}${verdictLink}${resolvedNote}${fairNote}${replicateCTA}</div>
|
|
432
515
|
<div class="t-right">${t.citations.toLocaleString()} cites<br>${link}</div>
|
|
433
516
|
</div>`;
|
|
434
517
|
}
|
|
435
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
|
+
|
|
436
533
|
function paintTargets() {
|
|
437
|
-
|
|
534
|
+
paintFilters();
|
|
535
|
+
const list = visibleTargets();
|
|
536
|
+
const pages = Math.max(1, Math.ceil(list.length / PER_PAGE));
|
|
438
537
|
if (_tpage >= pages) _tpage = pages - 1;
|
|
439
|
-
|
|
440
|
-
el("tcount").textContent = `${
|
|
441
|
-
el("targets").innerHTML =
|
|
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>`;
|
|
442
543
|
el("tpager").innerHTML = pages > 1
|
|
443
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>`
|
|
444
545
|
: "";
|
|
445
546
|
}
|
|
446
547
|
|
|
447
|
-
function renderTargets(targets) { _targets = targets; _tpage = 0; paintTargets(); }
|
|
548
|
+
function renderTargets(targets) { _targets = targets; _tpage = 0; _tfilter.clear(); paintTargets(); }
|
|
448
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
|
+
};
|
|
449
556
|
|
|
450
|
-
function renderTooling(tooling) {
|
|
451
|
-
if (!tooling || !tooling.length) { el("fieldtools").innerHTML = ""; return; }
|
|
452
|
-
const items = tooling.map((t) =>
|
|
453
|
-
`${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>` : ""}`
|
|
454
|
-
).join(" · ");
|
|
455
|
-
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}`;
|
|
456
|
-
}
|
|
457
557
|
|
|
458
558
|
function renderVerified(inField) {
|
|
459
559
|
const field = VERIFIED.filter((v) => inField.has(v.doi)).sort((a, b) => b.citations - a.citations);
|
|
@@ -465,7 +565,7 @@ function renderVerified(inField) {
|
|
|
465
565
|
// rich card: the trust edge shown as TWO OpenAIRE nodes (original + replication)
|
|
466
566
|
const matchCard = (v) => {
|
|
467
567
|
const chips = [
|
|
468
|
-
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>` : "",
|
|
469
569
|
...(v.fos || []).map((f) => `<span class="ochip">${esc(f).slice(0, 24)}</span>`),
|
|
470
570
|
...(v.sdg || []).map((s) => `<span class="ochip sdg">${esc(s).slice(0, 22)}</span>`),
|
|
471
571
|
`<span class="ochip cites">${v.citations.toLocaleString()} cites</span>`,
|
|
@@ -482,54 +582,56 @@ function renderVerified(inField) {
|
|
|
482
582
|
} else if (v.repo_doi) {
|
|
483
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>`;
|
|
484
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>` : "";
|
|
485
589
|
return `<li class="match">
|
|
486
590
|
<span class="nodelabel">original paper · OpenAIRE</span>
|
|
487
591
|
<span class="vt">${esc(v.title).slice(0, 82)}</span>
|
|
488
592
|
<div class="ochips">${chips}</div>
|
|
489
|
-
|
|
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>
|
|
490
595
|
${repl}
|
|
491
596
|
</li>`;
|
|
492
597
|
};
|
|
493
598
|
const fieldHtml = field.length
|
|
494
599
|
? `<ul class="vlist">${field.map(matchCard).join("")}</ul>`
|
|
495
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>`;
|
|
496
|
-
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>`;
|
|
497
602
|
el("verified").innerHTML = fieldHtml + moreHtml;
|
|
498
603
|
}
|
|
499
604
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
});
|
|
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>`;
|
|
530
632
|
}
|
|
531
633
|
|
|
532
|
-
async function run(topic) {
|
|
634
|
+
async function run(topic, isExample) {
|
|
533
635
|
topic = (topic || "").trim();
|
|
534
636
|
if (!topic) return;
|
|
535
637
|
el("go").disabled = true;
|
|
@@ -538,12 +640,12 @@ async function run(topic) {
|
|
|
538
640
|
const r = await radar(topic);
|
|
539
641
|
el("results").hidden = false;
|
|
540
642
|
renderTargets(r.targets);
|
|
541
|
-
renderTooling(r.tooling);
|
|
542
643
|
renderVerified(r.inField);
|
|
543
|
-
renderChart(
|
|
544
|
-
|
|
644
|
+
renderChart();
|
|
645
|
+
const note = isExample ? ` <i>— showing an example; search your own field above.</i>` : "";
|
|
646
|
+
el("status").innerHTML = (r.inField.size
|
|
545
647
|
? `“${topic}”: ${r.targets.length} candidates · ${r.inField.size} already checked, matching your search (green) — the rest are open.`
|
|
546
|
-
: `“${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;
|
|
547
649
|
} catch (e) {
|
|
548
650
|
el("status").textContent = `Could not reach the OpenAIRE Graph (${e.message}). Try again or a shorter topic.`;
|
|
549
651
|
} finally {
|
|
@@ -558,4 +660,12 @@ el("go").addEventListener("click", () => run(el("topic").value));
|
|
|
558
660
|
el("topic").addEventListener("keydown", (e) => { if (e.key === "Enter") run(el("topic").value); });
|
|
559
661
|
|
|
560
662
|
el("status").textContent = "Loading the Science Live verdict layer live from the nanopub network …";
|
|
561
|
-
Promise.all([loadVerdicts(), loadCurated()]).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,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,31 +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}
|
|
74
85
|
.badge.st-ready{background:var(--magenta);color:#fff}
|
|
75
|
-
.badge.st-val{background:#
|
|
76
|
-
.badge.st-con{background:#
|
|
86
|
+
.badge.st-val{background:#e7eefb;color:var(--green)}
|
|
87
|
+
.badge.st-con{background:#fdecc8;color:#9a6206}
|
|
77
88
|
.badge.st-needs{background:#eef2f9;color:var(--mut)}
|
|
78
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}
|
|
79
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}
|
|
80
105
|
.tresolved{font-size:12px;color:var(--green);margin-top:4px}
|
|
81
106
|
.tresolved a{font-weight:600}
|
|
82
107
|
.tfair{font-size:12px;color:var(--mut);margin-top:3px}
|
|
83
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}
|
|
84
112
|
.tool{font-size:12px;color:var(--mut)}
|
|
85
113
|
.tool .swh{color:var(--green);font-weight:600}
|
|
86
114
|
.t-right{text-align:right;font-size:12px;color:var(--mut);white-space:nowrap}
|
|
@@ -91,18 +119,19 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
|
|
|
91
119
|
.vlist li:last-child{border:0}
|
|
92
120
|
.vt{color:var(--ink);font-weight:600;display:block;margin-bottom:2px}
|
|
93
121
|
.vv{color:var(--green);font-weight:700;font-size:12px}
|
|
94
|
-
.vv.partial{color:#
|
|
122
|
+
.vv.partial{color:#9a6206}
|
|
95
123
|
#verified a{font-size:12px}
|
|
96
124
|
.vlist li.match{background:var(--green-soft);margin:0 -8px 10px;padding:12px 13px;border-radius:8px;border:0}
|
|
97
125
|
.nodelabel{display:block;font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--green);margin-bottom:3px}
|
|
98
126
|
.ochips{display:flex;flex-wrap:wrap;gap:5px;margin:6px 0}
|
|
99
127
|
.ochip{font-size:11px;background:#e7edf6;color:var(--navy2);border-radius:5px;padding:1px 7px;white-space:nowrap}
|
|
100
|
-
.ochip.oa{background:#
|
|
128
|
+
.ochip.oa{background:#e7edf6;color:var(--navy2);text-transform:capitalize}
|
|
101
129
|
.ochip.sdg{background:#fff1d3;color:#8a6d1b}
|
|
102
130
|
.ochip.type{background:var(--navy);color:#fff;text-transform:capitalize}
|
|
103
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}
|
|
104
133
|
.vline{font-size:12.5px;color:var(--mut);margin-top:5px}
|
|
105
|
-
.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)}
|
|
106
135
|
.vrepl.muted{color:var(--mut);border-top-color:var(--line)}
|
|
107
136
|
.vrepl a{font-weight:600}
|
|
108
137
|
.fairline{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--mut);margin-top:6px}
|
|
@@ -110,13 +139,13 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
|
|
|
110
139
|
.fairrecs{display:flex;flex-wrap:wrap;align-items:center;gap:3px 10px;font-size:11.5px;margin-top:5px}
|
|
111
140
|
.fairrecs b{color:var(--navy2);font-size:12px}
|
|
112
141
|
.fairrecs .rok{color:var(--green)}
|
|
113
|
-
.fairrecs .rno{color:#
|
|
142
|
+
.fairrecs .rno{color:#9a6206}
|
|
114
143
|
.fairbar{display:inline-block;width:64px;height:7px;border-radius:4px;background:#e4e9f2;overflow:hidden}
|
|
115
144
|
.fairbar i{display:block;height:100%;background:var(--green)}
|
|
116
145
|
.swhok{color:var(--green);font-weight:600}
|
|
117
|
-
.swhno{color:#
|
|
118
|
-
.ochip.wait{background:#
|
|
119
|
-
.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}
|
|
120
149
|
.vmore-stat{margin-top:14px;padding-top:12px;border-top:1px solid var(--line);font-size:12.5px;color:var(--mut)}
|
|
121
150
|
|
|
122
151
|
footer{border-top:1px solid var(--line);margin-top:30px;padding:20px 0;color:var(--mut);font-size:13px;background:#fff}
|
|
@@ -37,6 +37,18 @@ SELECT DISTINCT ?outcome ?status ?repo WHERE {{ GRAPH ?g {{ ?outcome ntpl:wasCre
|
|
|
37
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/>
|
|
38
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"""
|
|
39
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
|
+
|
|
40
52
|
|
|
41
53
|
def _sparql(query: str) -> list[dict]:
|
|
42
54
|
url = f"{SPARQL}?{urllib.parse.urlencode({'query': query})}"
|
|
@@ -70,6 +82,10 @@ def build_index() -> dict:
|
|
|
70
82
|
"""
|
|
71
83
|
outcomes = _sparql(_QA) # sequential: concurrent queries truncate the endpoint
|
|
72
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()
|
|
73
89
|
by_hash: dict[str, list] = {}
|
|
74
90
|
for r in citos:
|
|
75
91
|
by_hash.setdefault(_hash(r.get("subj")), []).append({
|
|
@@ -79,6 +95,8 @@ def build_index() -> dict:
|
|
|
79
95
|
})
|
|
80
96
|
index: dict[str, list] = {}
|
|
81
97
|
for o in outcomes:
|
|
98
|
+
if _hash(o.get("outcome")) in invalid: # drop a superseded/retracted Outcome
|
|
99
|
+
continue
|
|
82
100
|
cs = by_hash.get(_hash(o.get("outcome")), [])
|
|
83
101
|
verdict_citos = [c for c in cs if c["rel"] in VERDICT_RELS and not c["orig"].startswith("10.5281/")]
|
|
84
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/")]
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{replication_radar-0.3.1 → 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
|