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.
Files changed (30) hide show
  1. {replication_radar-0.3.1 → replication_radar-0.3.2}/PKG-INFO +2 -2
  2. {replication_radar-0.3.1 → replication_radar-0.3.2}/pyproject.toml +2 -2
  3. {replication_radar-0.3.1 → replication_radar-0.3.2}/site/app.js +203 -93
  4. replication_radar-0.3.2/site/index.html +58 -0
  5. {replication_radar-0.3.1 → replication_radar-0.3.2}/site/style.css +62 -33
  6. {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/network.py +18 -0
  7. replication_radar-0.3.1/site/index.html +0 -99
  8. {replication_radar-0.3.1 → replication_radar-0.3.2}/.github/workflows/publish-pypi.yml +0 -0
  9. {replication_radar-0.3.1 → replication_radar-0.3.2}/.gitignore +0 -0
  10. {replication_radar-0.3.1 → replication_radar-0.3.2}/CLAUDE.md +0 -0
  11. {replication_radar-0.3.1 → replication_radar-0.3.2}/LICENSE +0 -0
  12. {replication_radar-0.3.1 → replication_radar-0.3.2}/README.md +0 -0
  13. {replication_radar-0.3.1 → replication_radar-0.3.2}/STORY.md +0 -0
  14. {replication_radar-0.3.1 → replication_radar-0.3.2}/demo_sdm.py +0 -0
  15. {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/app-ui.md +0 -0
  16. {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/link-types.md +0 -0
  17. {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/next-layers-plan.md +0 -0
  18. {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/openaire-mcp.md +0 -0
  19. {replication_radar-0.3.1 → replication_radar-0.3.2}/docs/readiness-scoring-plan.md +0 -0
  20. {replication_radar-0.3.1 → replication_radar-0.3.2}/netlify.toml +0 -0
  21. {replication_radar-0.3.1 → replication_radar-0.3.2}/scripts/build_verdicts.py +0 -0
  22. {replication_radar-0.3.1 → replication_radar-0.3.2}/site/README.md +0 -0
  23. {replication_radar-0.3.1 → replication_radar-0.3.2}/site/curated.json +0 -0
  24. {replication_radar-0.3.1 → replication_radar-0.3.2}/site/verdicts.json +0 -0
  25. {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/__init__.py +0 -0
  26. {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/data/verdicts.json +0 -0
  27. {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/openaire.py +0 -0
  28. {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/radar.py +0 -0
  29. {replication_radar-0.3.1 → replication_radar-0.3.2}/src/replication_radar/server.py +0 -0
  30. {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.1
4
- Summary: MCP server that turns the OpenAIRE Graph into a ranked replication queue — impact-ranked targets, independent reusable tooling, and the Science Live verification overlay.
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.1"
4
- description = "MCP server that turns the OpenAIRE Graph into a ranked replication queue — impact-ranked targets, independent reusable tooling, and the Science Live verification overlay."
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
- const r = matScore == null
61
- ? (0.35 * impactScore + 0.20 * momentum) / 0.55
62
- : (0.45 * matScore + 0.35 * impactScore + 0.20 * momentum);
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
- ready: { rank: 0, label: "⭐ Replication-ready", cls: "st-ready", tip: "no verdict yet, but materials (code/data) are available and it's recent/active — gap + means + momentum" },
68
- validated: { rank: 1, label: "✅ Validated", cls: "st-val", tip: "independently replicated and it held up — a solid result to build on or extend" },
69
- contested: { rank: 2, label: "⚠️ Contested", cls: "st-con", tip: "an independent replication contradicted or only partially supported this — worth re-checking" },
70
- needs: { rank: 3, label: "❔ Needs check", cls: "st-needs", tip: "no verdict, and OpenAIRE holds no materials link — unknown (not absent); resolve from the paper" },
71
- dormant: { rank: 4, label: "💤 Dormant", cls: "st-dorm", tip: "no verdict, older, low momentum, no materials surfaced — likely dormant" },
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 /contradict|partial|disput|refut|critiqu/i.test(t.verification || "") ? "contested" : "validated";
75
- if (t.mat && t.mat.score != null) return "ready"; // materials present (resolved or linked)
76
- const old = t.year && (new Date().getFullYear() - t.year) >= 8; // had time to be replicated
77
- const hot = t.impl === "C1" || t.impl === "C2"; // still gaining momentum
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
- const matScore = (v.repl && v.repl.code) ? 1.0 : (v.repo_doi ? 0.8 : null); // its replication deposit
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: matScore, state: matScore == null ? "unknown" : "code", code: (v.repl && v.repl.code) || null },
355
- parts: { mat: matScore, impact: cs, momentum },
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(matScore, cs, momentum),
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
- // group by status (⭐ replication-ready first — the actionable candidates), then by readiness
384
- targets.sort((a, b) => (STATUS[a.statusKey].rank - STATUS[b.statusKey].rank) || (b.readiness || 0) - (a.readiness || 0) || (b.citations || 0) - (a.citations || 0));
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, tooling };
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[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 = `replicability = 0.45·materials + 0.35·impact + 0.20·momentum — `
404
- + `materials ${p.mat == null ? "unverified" : p.mat.toFixed(2)} · impact ${(p.impact || 0).toFixed(2)} · momentum ${(p.momentum || 0).toFixed(2)}`;
405
- const score = `<div class="score" title="${esc(scoreTitle)}"><span>${t.readiness != null ? t.readiness.toFixed(2) : "—"}</span><small>REPLIC.</small></div>`;
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
- const verdictLink = (t.status === "VERIFIED" && t.outcome_np)
422
- ? `<div class="tverdict">independently checked by Science Live · <a href="${t.outcome_np}" target="_blank" rel="noopener">replication outcome →</a></div>` : "";
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
- const pages = Math.max(1, Math.ceil(_targets.length / PER_PAGE));
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
- const n = (k) => _targets.filter((t) => t.statusKey === k).length;
440
- el("tcount").textContent = `${n("ready")} replication-ready · ${n("validated")} validated · ${_targets.length} candidates`;
441
- el("targets").innerHTML = _targets.slice(_tpage * PER_PAGE, _tpage * PER_PAGE + PER_PAGE).map(targetRow).join("");
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(" &nbsp;·&nbsp; ");
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
- <div class="vline"><span class="vv ${partialOf(v) ? "partial" : ""}">${v.verdicts.join(", ")}</span> — independently checked by Science Live ${v.outcome_np ? `· <a href="${v.outcome_np}" target="_blank" rel="noopener">replication outcome →</a>` : (v.cito_np ? `· <a href="${v.cito_np}" target="_blank" rel="noopener">verdict chain →</a>` : "")}</div>
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 drawn from the Science Live verification index bundled with this Radar. <a href="https://github.com/ScienceLiveHub/replication-radar/blob/main/src/replication_radar/data/verdicts.json" target="_blank" rel="noopener">browse the index →</a></p>`;
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
- function renderChart(items) {
501
- const short = (s) => { s = s || ""; return s.length > 46 ? s.slice(0, 44) + "…" : s; };
502
- if (chart) chart.destroy();
503
- if (!items.length) { el("gap").parentElement.querySelector(".empty")?.remove(); return; }
504
- chart = new Chart(el("gap"), {
505
- type: "bar",
506
- data: {
507
- labels: items.map((i) => short(i.title)),
508
- datasets: [{
509
- data: items.map((i) => Math.max(1, i.citations || 0)),
510
- backgroundColor: items.map((i) => (i.status === "VERIFIED" ? "#11875a" : "#e6007e")),
511
- borderRadius: 5, barThickness: 16,
512
- }],
513
- },
514
- options: {
515
- indexAxis: "y",
516
- maintainAspectRatio: false,
517
- scales: {
518
- x: { type: "logarithmic", title: { display: true, text: "citation impact (count, log)" }, grid: { color: "#eef2f9" } },
519
- y: { ticks: { font: { size: 11 } }, grid: { display: false } },
520
- },
521
- plugins: {
522
- legend: { display: false },
523
- tooltip: { callbacks: {
524
- title: (ti) => items[ti[0].dataIndex].title, // full (untruncated) paper title on hover
525
- label: (c) => `${items[c.dataIndex].status === "VERIFIED" ? "✓ already checked" : "open — worth replicating"} · ${items[c.dataIndex].citations.toLocaleString()} cites`,
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(r.chartItems);
544
- el("status").textContent = r.inField.size
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(() => { el("status").textContent = "Type a research field and hit Scan — or try an example above."; });
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&nbsp;Radar</h1>
14
+ <p class="tag">What's worth replicating next — read straight off the OpenAIRE&nbsp;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
- --navy:#0c1f3f; --navy2:#16305c; --ink:#1b2433; --mut:#5a6b85;
3
- --magenta:#e6007e; --magenta-soft:#fde4f1; --green:#11875a; --green-soft:#def0e7;
4
- --line:#e4e9f2; --bg:#f6f8fc; --card:#fff;
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:#c40069}
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
- .legend{background:#fff;border:1px solid var(--line);border-radius:12px;margin:16px 0 4px;padding:4px 18px}
30
- .legend>summary{cursor:pointer;font-weight:600;color:var(--navy);padding:11px 0;font-size:13.5px;list-style-position:inside}
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;height:340px;margin-top:8px}
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
- .fieldtools{font-size:12.5px;color:var(--mut);background:#f3f6fb;border:1px solid var(--line);border-radius:9px;padding:10px 13px;margin:0 0 14px;line-height:1.7}
49
- .fieldtools:empty{display:none}
50
- .fieldtools b{color:var(--navy2)}
51
- .fieldtools .swh{color:var(--green);text-decoration:none;font-size:13px}
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:#bfe3d2}
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:#b3005f}
67
- .badge.verified{background:#cfeadd;color:var(--green)}
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:#fde8cf;color:#8a5a1b}
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:#cfeadd;color:var(--green)}
76
- .badge.st-con{background:#fde8cf;color:#8a5a1b}
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:#b5820a}
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:#d3ecdf;color:var(--green);text-transform:capitalize}
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 #bfe3d2;color:var(--ink)}
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:#b5820a}
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:#b5820a}
118
- .ochip.wait{background:#fde8cf;color:#8a5a1b}
119
- .vnone{color:#9c0356;background:var(--magenta-soft);padding:12px 14px;border-radius:8px;font-size:13.5px}
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&nbsp;Radar</h1>
15
- <p class="tag">What's worth replicating next — read straight off the OpenAIRE&nbsp;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% &nbsp;<span class="ochip">C2</span> top 0.1% &nbsp;<span class="ochip">C3</span> top 1% &nbsp;<span class="ochip">C4</span> top 10% &nbsp;<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 &nbsp; <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 &nbsp; <span class="vv partial">PartiallySupported</span> in part &nbsp; <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>