replication-radar 0.4.2__tar.gz → 0.4.3__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 (50) hide show
  1. {replication_radar-0.4.2 → replication_radar-0.4.3}/PKG-INFO +2 -2
  2. {replication_radar-0.4.2 → replication_radar-0.4.3}/README.md +1 -1
  3. {replication_radar-0.4.2 → replication_radar-0.4.3}/pyproject.toml +1 -1
  4. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/methodology.json +9 -4
  5. {replication_radar-0.4.2 → replication_radar-0.4.3}/src/replication_radar/__init__.py +1 -1
  6. replication_radar-0.4.3/src/replication_radar/github.py +98 -0
  7. {replication_radar-0.4.2 → replication_radar-0.4.3}/src/replication_radar/radar.py +40 -5
  8. {replication_radar-0.4.2 → replication_radar-0.4.3}/src/replication_radar/server.py +6 -3
  9. {replication_radar-0.4.2 → replication_radar-0.4.3}/.github/ISSUE_TEMPLATE/join-the-team.md +0 -0
  10. {replication_radar-0.4.2 → replication_radar-0.4.3}/.github/workflows/publish-pypi.yml +0 -0
  11. {replication_radar-0.4.2 → replication_radar-0.4.3}/.gitignore +0 -0
  12. {replication_radar-0.4.2 → replication_radar-0.4.3}/CITATION.cff +0 -0
  13. {replication_radar-0.4.2 → replication_radar-0.4.3}/CLAUDE.md +0 -0
  14. {replication_radar-0.4.2 → replication_radar-0.4.3}/CODE_OF_CONDUCT.md +0 -0
  15. {replication_radar-0.4.2 → replication_radar-0.4.3}/CONTRIBUTING.md +0 -0
  16. {replication_radar-0.4.2 → replication_radar-0.4.3}/DEMO.md +0 -0
  17. {replication_radar-0.4.2 → replication_radar-0.4.3}/LICENSE +0 -0
  18. {replication_radar-0.4.2 → replication_radar-0.4.3}/STORY.md +0 -0
  19. {replication_radar-0.4.2 → replication_radar-0.4.3}/demo_sdm.py +0 -0
  20. {replication_radar-0.4.2 → replication_radar-0.4.3}/docs/app-ui.md +0 -0
  21. {replication_radar-0.4.2 → replication_radar-0.4.3}/docs/architecture.mmd +0 -0
  22. {replication_radar-0.4.2 → replication_radar-0.4.3}/docs/link-types.md +0 -0
  23. {replication_radar-0.4.2 → replication_radar-0.4.3}/docs/next-layers-plan.md +0 -0
  24. {replication_radar-0.4.2 → replication_radar-0.4.3}/docs/openaire-mcp.md +0 -0
  25. {replication_radar-0.4.2 → replication_radar-0.4.3}/docs/readiness-scoring-plan.md +0 -0
  26. {replication_radar-0.4.2 → replication_radar-0.4.3}/netlify/functions/gh.mjs +0 -0
  27. {replication_radar-0.4.2 → replication_radar-0.4.3}/netlify.toml +0 -0
  28. {replication_radar-0.4.2 → replication_radar-0.4.3}/scripts/build-team-index.mjs +0 -0
  29. {replication_radar-0.4.2 → replication_radar-0.4.3}/scripts/build_verdicts.py +0 -0
  30. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/README.md +0 -0
  31. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/app.js +0 -0
  32. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/bios/Saranjeet-Kaur-Bhogal.md +0 -0
  33. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/bios/anne-fouilloux.md +0 -0
  34. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/bios/index.json +0 -0
  35. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/bios/jean-iaquinta.md +0 -0
  36. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/curated.json +0 -0
  37. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/index.html +0 -0
  38. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/methodology.html +0 -0
  39. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/replicate.html +0 -0
  40. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/replicate.md +0 -0
  41. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/story.html +0 -0
  42. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/story.md +0 -0
  43. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/style.css +0 -0
  44. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/team.html +0 -0
  45. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/team.json +0 -0
  46. {replication_radar-0.4.2 → replication_radar-0.4.3}/site/verdicts.json +0 -0
  47. {replication_radar-0.4.2 → replication_radar-0.4.3}/src/replication_radar/data/verdicts.json +0 -0
  48. {replication_radar-0.4.2 → replication_radar-0.4.3}/src/replication_radar/network.py +0 -0
  49. {replication_radar-0.4.2 → replication_radar-0.4.3}/src/replication_radar/openaire.py +0 -0
  50. {replication_radar-0.4.2 → replication_radar-0.4.3}/src/replication_radar/verdicts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: replication-radar
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
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
@@ -54,7 +54,7 @@ The Radar joins three sources to add a **replication layer** on top:
54
54
  | Tool | What it answers |
55
55
  |---|---|
56
56
  | `radar(topic)` | Impact-ranked replication targets in a field — each **OPEN** (opportunity) or **VERIFIED** (done, with verdict) + independent tooling + funder context |
57
- | `find_independent_software(doi, topic)` | Reusable engines **not authored by the original team** (author-disjoint = *replication*, not *reproduction*), ranked by reuse signal not citations |
57
+ | `find_independent_software(doi, topic)` | Reusable engines **not authored by the original team** (author-disjoint = *replication*, not *reproduction*), ranked by reuse signal — repo · Software Heritage · downloads · **GitHub stars** — not citations (returns `stars` + `rank_score`) |
58
58
  | `replication_status(doi)` | Has this DOI been replicated, did it hold? Verdict(s) — **live from the nanopub network, any signer** — with status, CiTO relation, repo, and signed Outcome/CiTO nanopub links; `open` if not |
59
59
  | `verified_claims()` | The whole **verified-knowledge corpus** — every claim the network holds a verdict for (author-agnostic) |
60
60
 
@@ -31,7 +31,7 @@ The Radar joins three sources to add a **replication layer** on top:
31
31
  | Tool | What it answers |
32
32
  |---|---|
33
33
  | `radar(topic)` | Impact-ranked replication targets in a field — each **OPEN** (opportunity) or **VERIFIED** (done, with verdict) + independent tooling + funder context |
34
- | `find_independent_software(doi, topic)` | Reusable engines **not authored by the original team** (author-disjoint = *replication*, not *reproduction*), ranked by reuse signal not citations |
34
+ | `find_independent_software(doi, topic)` | Reusable engines **not authored by the original team** (author-disjoint = *replication*, not *reproduction*), ranked by reuse signal — repo · Software Heritage · downloads · **GitHub stars** — not citations (returns `stars` + `rank_score`) |
35
35
  | `replication_status(doi)` | Has this DOI been replicated, did it hold? Verdict(s) — **live from the nanopub network, any signer** — with status, CiTO relation, repo, and signed Outcome/CiTO nanopub links; `open` if not |
36
36
  | `verified_claims()` | The whole **verified-knowledge corpus** — every claim the network holds a verdict for (author-agnostic) |
37
37
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "replication-radar"
3
- version = "0.4.2"
3
+ version = "0.4.3"
4
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"
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "title": "Replication Radar — methodology & data provenance",
3
3
  "summary": "Replication Radar joins three public, grounded sources into one view of a research field: the OpenAIRE Graph (what exists and how cited it is), the Science Live nanopublication network (whether a claim has been independently replicated and with what verdict), and FAIR-software signals (whether the replication's code is reusable). This document states, for every label and score in the interface, exactly where it comes from and how it is computed. It is served as JSON (this file) and rendered as a page at /methodology.html.",
4
- "version": "0.3.2",
5
- "updated": "2026-06-16",
4
+ "version": "0.4.3",
5
+ "updated": "2026-08-13",
6
6
  "license": "CC-BY-4.0",
7
7
  "sources": [
8
8
  { "id": "openaire", "name": "OpenAIRE Graph", "url": "https://graph.openaire.eu/", "api": "https://api.openaire.eu/graph/v1", "role": "Research products (papers, software, datasets), citation-impact indicators, open-access status, subject classification (FOS, SDG), and code/data links. Queried live from the browser; no key required." },
9
9
  { "id": "bip", "name": "BIP! citation indicators (via OpenAIRE)", "url": "https://bip.imsi.athenarc.gr/", "role": "The C1–C5 impact and impulse classes are BIP! indicators carried in the OpenAIRE record." },
10
10
  { "id": "sciencelive", "name": "Science Live nanopublication network", "url": "https://sciencelive4all.org", "api": "https://query.knowledgepixels.com/repo/full", "role": "FORRT replication chains as signed nanopublications: the Outcome (verdict), the Study (method), the Claim (what was tested) and CiTO citations to the original paper. Queried live via SPARQL; author-agnostic (any signer)." },
11
11
  { "id": "fairsoftware", "name": "fair-software.eu recommendations", "url": "https://fair-software.eu/", "role": "The five FAIR-for-research-software recommendations we score a replication's code against." },
12
- { "id": "github", "name": "GitHub REST API", "url": "https://docs.github.com/rest", "role": "Repository existence, licence, citation file, release/quality signals, stars and forks for the FAIR-software check (unauthenticated, 60 requests/hour)." },
12
+ { "id": "github", "name": "GitHub REST API", "url": "https://docs.github.com/rest", "role": "Repository existence, licence, citation file, release/quality signals, and stars/forks — used both for the FAIR-software check and as the GitHub-star reuse signal in the reusable-software ranking (unauthenticated, 60 requests/hour; set GITHUB_TOKEN to raise the limit)." },
13
13
  { "id": "swh", "name": "Software Heritage API", "url": "https://archive.softwareheritage.org/api/", "role": "Whether the replication's code is archived in Software Heritage (long-term preservation), for the FAIR-software check." },
14
14
  { "id": "zenodo", "name": "Zenodo API", "url": "https://developers.zenodo.org/", "role": "Resolving a Zenodo deposit to its source GitHub repository and DOI when OpenAIRE lacks a direct code link." },
15
15
  { "id": "turingway", "name": "The Turing Way", "url": "https://the-turing-way.netlify.app/", "role": "A community handbook for reproducible, ethical and collaborative research — the language-agnostic reference behind the 'RSE practices' explanations (documentation, testing, continuous integration)." },
@@ -41,6 +41,8 @@
41
41
  "means": "Whether independent replications agree. Many-agree differs from disagree.", "computed": "From the verdict counts across all of a paper's outcomes — see the 'agreement' score below.", "limits": "Depends on how many replications exist; one confirmation is weaker evidence than several." },
42
42
  { "id": "fair", "name": "FAIR-software score (N/5)", "shownAs": "FAIR software N/5 + per-recommendation checks", "source": "fairsoftware",
43
43
  "means": "How reusable the replication's code is, on the five fair-software.eu recommendations: public repository, open licence, in a registry, citable (CITATION.cff / DOI), and quality artefacts.", "computed": "Computed live from the GitHub API (repository, licence, citation file, quality), Software Heritage (archival/registry) and Zenodo (DOI). One point per met recommendation. Only ever run on a replication we have a real repository URL for.", "limits": "GitHub unauthenticated rate limit is 60/hour; a check can be skipped if the limit is hit." },
44
+ { "id": "reusablesoftware", "name": "Reusable software for a field", "shownAs": "the reusable-software list (and the MCP find_independent_software tool)", "source": "openaire",
45
+ "means": "Software an RSE could pick up to replicate work in this field, restricted to engines NOT authored by the paper's own team (author-disjoint = replication, not reproduction).", "computed": "OpenAIRE type=software matching the field, filtered to author-disjoint records, ranked by the reusable-software score below (resolvable repository · Software Heritage · downloads · citations · GitHub stars), with the repository resolved via Zenodo when OpenAIRE omits it.", "limits": "Keyword recall is OpenAIRE's — a tool whose record doesn't contain the search terms won't appear (e.g. a narrow topic can miss an established tool); the field's software may be thin or dominated by one-off study deposits." },
44
46
  { "id": "gap", "name": "The replication-gap map", "shownAs": "the composition bar at the top", "source": "sciencelive",
45
47
  "means": "How much of the matched high-impact work has actually been independently checked — the rest is the replication gap.", "computed": "A composition of the ranked list by status (reproducible / validated / contested / needs check / dormant).", "limits": "Reflects the current search and the current network coverage." }
46
48
  ],
@@ -53,7 +55,10 @@
53
55
  "explanation": "Each outcome is one signed nanopublication; records without a signed outcome are not counted." },
54
56
  { "id": "fair", "name": "FAIR-software score",
55
57
  "formula": "1 point for each met recommendation, out of 5: public repository · open licence · in a registry · citable (CITATION.cff or DOI) · quality artefacts.",
56
- "explanation": "fair-software.eu's five recommendations, evaluated from live GitHub / Software Heritage / Zenodo signals." }
58
+ "explanation": "fair-software.eu's five recommendations, evaluated from live GitHub / Software Heritage / Zenodo signals." },
59
+ { "id": "softwarereuse", "name": "Reusable-software ranking",
60
+ "formula": "In the web app: a quality composite a one-off deposit can't fake — the FAIR-software score (0–5) + the count of RSE good-practice signals (0–5) + a GitHub-star bonus = min(3, log10(stars+1)·1.6) − 1.5 if the software needs a proprietary/paid runtime, plus 0.4×the OpenAIRE reuse signal and +0.5 if Software Heritage archived. In the MCP find_independent_software tool (no per-repo FAIR assessment): a resolvable code repository (+2) + Software Heritage (+2) + has downloads (+1) + has citations (+1) + the same GitHub-star bonus. Both rank author-disjoint software only.",
61
+ "explanation": "Citation class is useless for research software (nearly everything is C5 / 0-citation), so software is ranked by grounded REUSE signals instead. GitHub stars are the signal that separates an established, widely-used tool from a one-off study deposit — read from the repo, falling back to the Zenodo→GitHub link when OpenAIRE omits the repo URL, and capped on a log scale so a handful of very-starred repos can't dominate. A star lookup that is rate-limited or unreachable falls back to the other signals, never a guess. GitHub's unauthenticated rate limit is 60/hour; set GITHUB_TOKEN to raise it." }
57
62
  ],
58
63
  "practices": [
59
64
  { "id": "documented", "name": "Documented",
@@ -7,4 +7,4 @@ OpenAIRE impact + Software Heritage reuse signals + Science Live nanopub verdict
7
7
  from .radar import radar, find_independent_software, replication_status, verified_claims
8
8
 
9
9
  __all__ = ["radar", "find_independent_software", "replication_status", "verified_claims"]
10
- __version__ = "0.4.2"
10
+ __version__ = "0.4.3"
@@ -0,0 +1,98 @@
1
+ """GitHub + Zenodo helpers so reusable-software ranking reflects real reuse (stars),
2
+ not just OpenAIRE metadata.
3
+
4
+ Why: OpenAIRE's software records are uniformly low-citation and often omit the code
5
+ repository URL, so a one-off study repo can look as "reusable" as an established tool.
6
+ GitHub stars are the missing signal — the web app already ranks its software lens with
7
+ them; this brings the MCP in line.
8
+
9
+ Resolution mirrors the web app: prefer OpenAIRE's codeRepositoryUrl, else the Zenodo
10
+ record's related_identifiers (many Zenodo-published tools omit the repo URL in OpenAIRE —
11
+ e.g. XMHW). Stars come from the GitHub REST API (unauth 60/hr; set GITHUB_TOKEN to lift to
12
+ 5000/hr). Every lookup is cached and BEST-EFFORT — a failed/rate-limited call returns None
13
+ and the caller falls back to the OpenAIRE-only signal, so results stay grounded and never
14
+ crash. Stdlib only.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import math
20
+ import os
21
+ import re
22
+ import urllib.request
23
+
24
+ _TIMEOUT = float(os.environ.get("RADAR_HTTP_TIMEOUT", "30"))
25
+ _TOKEN = os.environ.get("GITHUB_TOKEN") or os.environ.get("RADAR_GITHUB_TOKEN")
26
+ _stars_cache: dict[str, int | None] = {}
27
+ _zenodo_repo_cache: dict[str, str | None] = {}
28
+
29
+
30
+ def _parse(url: str | None) -> tuple[str, str] | None:
31
+ m = re.search(r"github\.com/([^/]+)/([^/#?]+)", url or "", re.I)
32
+ return (m.group(1), m.group(2).replace(".git", "")) if m else None
33
+
34
+
35
+ def _get_json(url: str, headers: dict) -> dict | None:
36
+ try:
37
+ req = urllib.request.Request(url, headers=headers)
38
+ with urllib.request.urlopen(req, timeout=_TIMEOUT) as r:
39
+ return json.load(r)
40
+ except Exception:
41
+ return None
42
+
43
+
44
+ def _github_from_zenodo(doi: str | None) -> str | None:
45
+ m = re.search(r"zenodo\.(\d+)", doi or "", re.I)
46
+ if not m:
47
+ return None
48
+ key = m.group(1)
49
+ if key in _zenodo_repo_cache:
50
+ return _zenodo_repo_cache[key]
51
+ found = None
52
+ d = _get_json(
53
+ f"https://zenodo.org/api/records/{key}",
54
+ {"Accept": "application/json", "User-Agent": "replication-radar"},
55
+ )
56
+ for rel in ((d or {}).get("metadata") or {}).get("related_identifiers") or []:
57
+ ident = rel.get("identifier") or ""
58
+ if "github.com" in ident:
59
+ found = ident
60
+ break
61
+ _zenodo_repo_cache[key] = found
62
+ return found
63
+
64
+
65
+ def resolve_repo(code_repo: str | None, doi: str | None = None) -> str | None:
66
+ """Normalised ``https://github.com/owner/repo`` for this software, or None.
67
+
68
+ Prefers OpenAIRE's ``codeRepositoryUrl``; falls back to the Zenodo record's
69
+ ``related_identifiers`` (many Zenodo-published tools omit the repo URL in OpenAIRE)."""
70
+ g = _parse(code_repo) or _parse(_github_from_zenodo(doi))
71
+ return f"https://github.com/{g[0]}/{g[1]}" if g else None
72
+
73
+
74
+ def stars(repo_url: str | None) -> int | None:
75
+ """GitHub stargazers for a repo URL (cached); None if unknown/unreachable/rate-limited.
76
+
77
+ Set ``GITHUB_TOKEN`` (or ``RADAR_GITHUB_TOKEN``) to lift the 60/hr unauth limit to 5000/hr."""
78
+ g = _parse(repo_url)
79
+ if not g:
80
+ return None
81
+ key = f"{g[0]}/{g[1]}".lower()
82
+ if key in _stars_cache:
83
+ return _stars_cache[key]
84
+ headers = {"Accept": "application/vnd.github+json", "User-Agent": "replication-radar"}
85
+ if _TOKEN:
86
+ headers["Authorization"] = f"Bearer {_TOKEN}"
87
+ d = _get_json(f"https://api.github.com/repos/{g[0]}/{g[1]}", headers)
88
+ val = int(d["stargazers_count"]) if d and "stargazers_count" in d else None
89
+ _stars_cache[key] = val
90
+ return val
91
+
92
+
93
+ def star_bonus(n: int | None) -> float:
94
+ """Bounded log-scale star contribution to a reuse score, mirroring the web app
95
+ (``min(3, log10(stars+1) * 1.6)``): 0 stars → 0; ~10 → 1.7; ~30 → 2.4; ≥250 → 3.0."""
96
+ if not n or n <= 0:
97
+ return 0.0
98
+ return min(3.0, math.log10(n + 1) * 1.6)
@@ -11,7 +11,7 @@ Three capabilities (exposed as MCP tools in server.py):
11
11
  """
12
12
  from __future__ import annotations
13
13
 
14
- from . import openaire, verdicts
14
+ from . import github, openaire, verdicts
15
15
 
16
16
  # impact class -> 0..1 (C1 best). Used in the readiness score.
17
17
  _CLASS_SCORE = {"C1": 1.0, "C2": 0.8, "C3": 0.6, "C4": 0.4, "C5": 0.2, None: 0.2}
@@ -89,6 +89,21 @@ def verified_claims() -> dict:
89
89
  return {"count": len(claims), "claims": claims}
90
90
 
91
91
 
92
+ _SW_POOL = 100 # software candidates to fetch (established tools are often relevance-buried)
93
+ _RESOLVE_CAP = 40 # max GitHub/Zenodo lookups per call (cached; set GITHUB_TOKEN for headroom)
94
+
95
+
96
+ def _sw_rank_score(p, repo: str | None, stars: int | None) -> float:
97
+ """Reuse score used to RANK software: the OpenAIRE signal plus a bounded GitHub-star bonus.
98
+ Uses the resolved `repo` (so a Zenodo-linked tool OpenAIRE omitted still gets repo credit)."""
99
+ s = 0.0
100
+ if repo: s += 2 # a real, resolvable code repository
101
+ if p.swh_archived: s += 2 # Software Heritage archived
102
+ if p.downloads > 0: s += 1
103
+ if p.citation_count > 0: s += 1
104
+ return s + github.star_bonus(stars) # stars separate established tools from one-off study repos
105
+
106
+
92
107
  def find_independent_software(
93
108
  doi: str | None = None,
94
109
  topic: str | None = None,
@@ -109,7 +124,9 @@ def find_independent_software(
109
124
  # derive a short topic from the title's leading words
110
125
  topic = " ".join((paper.title if paper else "").split()[:3]) or "software"
111
126
 
112
- pool = openaire.search_products(topic, "software", size=25)
127
+ # Wide pool: established tools are often relevance-buried in OpenAIRE's software index
128
+ # (e.g. XMHW ranks ~60th for "marine heatwave"), so a small pool never sees them.
129
+ pool = openaire.search_products(topic, "software", size=_SW_POOL)
113
130
  rows = []
114
131
  for p in pool:
115
132
  rows.append(
@@ -118,14 +135,32 @@ def find_independent_software(
118
135
  "doi": p.doi,
119
136
  "authors": p.authors,
120
137
  "independent": _independence(original_authors, p.authors),
121
- "reuse_score": p.reuse_score,
138
+ "reuse_score": p.reuse_score, # OpenAIRE-only signal (kept for transparency)
122
139
  "code_repo": p.code_repo,
123
140
  "swh_archived": p.swh_archived,
124
141
  "downloads": p.downloads,
142
+ "stars": None,
143
+ "_p": p,
125
144
  }
126
145
  )
127
- # independent first, then most-reusable
128
- rows.sort(key=lambda r: (not r["independent"], -r["reuse_score"]))
146
+ # Resolve GitHub stars for the independent candidates (bounded + cached, best-effort).
147
+ # Stars are the missing reuse signal: without them a 0-star one-off study repo ranks like
148
+ # an established, widely-used tool. Resolve highest OpenAIRE-signal first so the budget is
149
+ # spent well; the Zenodo fallback recovers repos OpenAIRE omits (that is how XMHW resolves).
150
+ resolvable = [
151
+ r for r in rows
152
+ if r["independent"] and (r["code_repo"] or "zenodo" in (r["doi"] or "").lower())
153
+ ]
154
+ resolvable.sort(key=lambda r: (-r["reuse_score"], -r["downloads"]))
155
+ for r in resolvable[:_RESOLVE_CAP]:
156
+ repo = github.resolve_repo(r["code_repo"], r["doi"])
157
+ if repo:
158
+ r["code_repo"] = repo
159
+ r["stars"] = github.stars(repo)
160
+ for r in rows:
161
+ r["rank_score"] = round(_sw_rank_score(r.pop("_p"), r["code_repo"], r["stars"]), 2)
162
+ # independent first, then real (stars-augmented) reuse, then downloads
163
+ rows.sort(key=lambda r: (not r["independent"], -r["rank_score"], -r["downloads"]))
129
164
  return {
130
165
  "query_topic": topic,
131
166
  "original_authors": original_authors,
@@ -40,9 +40,12 @@ def radar(topic: str, limit: int = 8) -> dict:
40
40
  @mcp.tool()
41
41
  def find_independent_software(doi: str = "", topic: str = "", limit: int = 8) -> dict:
42
42
  """Reusable method software for *replicating* a claim — engines NOT authored by
43
- the original paper's team (author-disjoint), ranked by reuse signal (code repo +
44
- Software Heritage archival + usage), not citations. Pass the original paper's DOI
45
- (authors are looked up) and a short topic."""
43
+ the original paper's team (author-disjoint), ranked by reuse signal — a resolvable
44
+ code repository + Software Heritage archival + downloads + GitHub stars (the signal
45
+ that separates an established tool from a one-off study repo) — not citations. Each
46
+ result carries its `stars` and `rank_score`. Pass the original paper's DOI (authors
47
+ are looked up) and a short topic; keep the topic broad (a narrow one can miss an
48
+ established tool whose OpenAIRE record doesn't contain the extra words)."""
46
49
  return _find_software(doi=doi or None, topic=topic or None, limit=limit)
47
50
 
48
51