Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import EmailVerified from "./pages/EmailVerified";
import { Home } from "./pages/Home";
import NotFound from "./pages/NotFound";
import CreateNanopub from "./pages/np/create/CreateNanopub";
import ReplicationSummary from "./pages/np/ReplicationSummary";
import ViewNanopub from "./pages/np/ViewNanopub";
import Policies from "./pages/Policies";
import AccountSettings from "./pages/settings/AccountSettings";
Expand Down Expand Up @@ -57,6 +58,7 @@ function App() {
{/* Main App Pages */}
<Route path="/" element={<Home />} />
<Route path="/np/" element={<ViewNanopub />} />
<Route path="/np/replications" element={<ReplicationSummary />} />
<Route path="/np/create" element={<CreateNanopub />} />
<Route path="/np/create/:uri" element={<CreateNanopub />} />
<Route path="/email-verified" element={<EmailVerified />} />
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as NANOPUB_REFERENCES } from "./nanopub-references.rq";
export { default as NANOPUB_REFERS_TO } from "./nanopub-refers-to.rq";
export { default as NANOPUB_STATUS } from "./nanopub-status.rq";
export { default as NANOPUB_TYPES } from "./nanopub-types.rq";
export { default as REPLICATIONS_BY_PAPER } from "./replications-by-paper.rq";
export { default as SEARCH_NANOPUBS } from "./search-nanopubs.rq";
export { default as SEARCH_NANOPUBS_BY_TEMPLATES } from "./search-nanopubs-by-templates.rq";
export { default as SEARCH_NANOPUBS_BY_TYPE } from "./search-nanopubs-by-type.rq";
Expand Down
62 changes: 62 additions & 0 deletions frontend/src/lib/queries/replications-by-paper.rq
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Every independent replication of a paper's claims, with the verdict each reached.
#
# Enumerates by the CiTO->DOI method: a replication Outcome makes a verdict-citation
# (cito:confirms / qualifies / disputes / …) to the original paper's DOI. This is the
# reliable "all replications of paper X" set — unlike a graph BFS from the paper, which
# bleeds into adjacent chains and misses disconnected replications.
#
# Each matching Outcome is then walked up its FORRT chain for display:
# Outcome -> verdict, confidence, conclusion, deposit repository
# isOutcomeOf -> Study -> label, scope, methodology, deviations
# targetsClaim -> Claim -> label, AIDA statement (the atomic claim sentence), FORRT type
#
# Outcomes that have been retracted / invalidated / superseded by their own signer are
# excluded via the admin graph (npx:invalidates covers retract + supersede; the matching
# public-key hash ensures only the original signer can retract their own work — a third
# party cannot suppress someone else's outcome). Disapproval is deliberately not a filter.
#
# Placeholder: `?_doiUri` - URI: the paper DOI as an IRI, e.g. https://doi.org/10.1126/science.aax8591

prefix np: <http://www.nanopub.org/nschema#>
prefix nt: <https://w3id.org/np/o/ntemplate/>
prefix npa: <http://purl.org/nanopub/admin/>
prefix npx: <http://purl.org/nanopub/x/>
prefix cito: <http://purl.org/spar/cito/>
prefix slt: <https://w3id.org/sciencelive/o/terms/>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>

select distinct ?outcome ?status ?rel ?confidence ?conclusion ?repo
?study ?studyLabel ?scope ?method ?deviation
?claim ?claimLabel ?aida ?ctype where {
# A CiTO nanopub whose assertion verdict-cites the paper DOI; its subject is the Outcome.
values ?relP { cito:confirms cito:qualifies cito:disputes cito:critiques cito:extends cito:supports cito:refutes }
graph ?ca { ?outcome ?relP ?_doiUri . }
?citoNp np:hasAssertion ?ca .
graph npa:networkGraph { ?citoNp nt:wasCreatedFromTemplate <https://w3id.org/np/RA43F9EoOuzF0xoNUnCMNyFsfIqlsuWDdPHCnN0wCdCAw> . }
bind(strafter(str(?relP), "http://purl.org/spar/cito/") as ?rel)

# The Outcome must be valid: not invalidated/superseded by a nanopub from the same signer.
graph npa:graph {
?outcome npa:hasValidSignatureForPublicKeyHash ?pubkey ; np:hasAssertion ?oa .
filter not exists { ?inv npx:invalidates ?outcome ; npa:hasValidSignatureForPublicKeyHash ?pubkey . }
}
graph ?oa {
?oc slt:hasValidationStatus ?s ; slt:isOutcomeOf ?study .
optional { ?oc slt:hasConfidenceLevel ?cf . }
optional { ?oc slt:hasConclusionDescription ?conclusion . }
optional { ?oc slt:hasOutcomeRepository ?repo . }
}
bind(strafter(str(?s), "/terms/") as ?status)
bind(strafter(str(?cf), "/terms/") as ?confidence)

optional { graph ?sg { ?study rdfs:label ?studyLabel . } }
optional { graph ?sg2 { ?study slt:hasScopeDescription ?scope . } }
optional { graph ?sg3 { ?study slt:hasMethodologyDescription ?method . } }
optional { graph ?sg4 { ?study slt:hasDeviationDescription ?deviation . } }
optional {
graph ?sg5 { ?study slt:targetsClaim ?claim . }
graph ?clg { ?claim rdfs:label ?claimLabel . }
optional { graph ?clg { ?claim slt:asAidaStatement ?aida . } }
optional { graph ?clg { ?claim a ?ctype . filter(contains(str(?ctype), "-FORRT-Claim")) } }
}
}
25 changes: 25 additions & 0 deletions frontend/src/lib/queries/replications-by-paper.rq.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Every independent replication of a paper's claims, with the verdict each reached. Enumerates by the CiTO->DOI method: a replication Outcome makes a verdict-citation (cito:confirms / qualifies / disputes / …) to the original paper's DOI. This is the reliable "all replications of paper X" set — unlike a graph BFS from the paper, which bleeds into adjacent chains and misses disconnected replications. Each matching Outcome is then walked up its FORRT chain for display: Outcome -> verdict, confidence, conclusion, deposit repository isOutcomeOf -> Study -> label, scope, methodology, deviations targetsClaim -> Claim -> label, AIDA statement (the atomic claim sentence), FORRT type Outcomes that have been retracted / invalidated / superseded by their own signer are excluded via the admin graph (npx:invalidates covers retract + supersede; the matching public-key hash ensures only the original signer can retract their own work — a third party cannot suppress someone else's outcome). Disapproval is deliberately not a filter.
* @see ./replications-by-paper.rq
* @generator Generated by `npm run generate:query-types`
*/
declare const query: import("../sparql").SparqlQuery<{
doiUri: "uri";
}, {
outcome: "string";
status: "string";
rel: "string";
confidence: "string";
conclusion: "string";
repo: "string";
study: "string";
studyLabel: "string";
scope: "string";
method: "string";
deviation: "string";
claim: "string";
claimLabel: "string";
aida: "string";
ctype: "string";
}>;
export default query;
124 changes: 124 additions & 0 deletions frontend/src/lib/replications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Replication summary — given a paper DOI, find every independent replication of its
* claims on the nanopub network and the verdict each reached.
*
* Deliberately client-side and unauthenticated: it reads only public nanopub-network data,
* so the page works for anyone (the authenticated API stays for programmatic/registered use).
*
* The SPARQL lives in `queries/replications-by-paper.rq` (CiTO→DOI enumeration + chain walk +
* admin-graph validity guard); here we just bind the DOI, run it, and shape the rows.
*/
import { REPLICATIONS_BY_PAPER } from "@/lib/queries";
import { executeBindSparql, NANOPUB_SPARQL_ENDPOINT_FULL } from "@/lib/sparql";

export type Replication = {
outcomeNp: string;
verdict: string; // Validated | PartiallySupported | Contradicted | NotSupported | …
relation: string; // confirms | qualifies | disputes | …
confidence: string; // HighConfidence | …
conclusion: string;
repo: string;
study: {
uri: string;
label: string;
scope: string;
method: string;
deviation: string;
};
claim: { uri: string; label: string; aida: string; type: string };
};

export type ReplicationSummary = {
doi: string;
count: number;
byVerdict: { validated: number; partial: number; contradicted: number };
replications: Replication[];
};

// AIDA statement IRI → the atomic claim sentence (mixed +/%20 encoding in the wild).
const decodeAida = (u: string): string => {
if (!u) return "";
try {
return decodeURIComponent(u.replace(/.*\/aida\//, "").replace(/\+/g, " "));
} catch {
return "";
}
};

// ".../terms/model_performance-FORRT-Claim" → "model performance"
const claimTypeLabel = (u: string): string =>
u
? u
.replace(/.*\/terms\//, "")
.replace(/-FORRT-Claim$/, "")
.replace(/_/g, " ")
: "";

export const isContradicted = (v: string) =>
/contradict|notsupport|refut/i.test(v);
export const isPartial = (v: string) => /partial/i.test(v);
export const isConfirmed = (v: string) =>
/validat|confirm|support/i.test(v) && !isContradicted(v) && !isPartial(v);

const DOI_RE = /^10\.\d{3,}\/\S+$/;

export async function fetchReplications(
doi: string,
signal?: AbortSignal,
): Promise<ReplicationSummary> {
const clean = doi.trim().replace(/^https?:\/\/doi\.org\//i, "");
if (!DOI_RE.test(clean)) {
throw new Error(`"${doi}" is not a valid DOI (expected 10.xxxx/…).`);
}

const rows = await executeBindSparql(
REPLICATIONS_BY_PAPER,
{ doiUri: `https://doi.org/${clean}` },
NANOPUB_SPARQL_ENDPOINT_FULL,
signal,
);

// One row per (outcome, optional-binding) — collapse to one Replication per outcome.
const byNp = new Map<string, Replication>();
for (const r of rows) {
const np = r.outcome;
if (!np || byNp.has(np)) continue;
byNp.set(np, {
outcomeNp: np,
verdict: r.status || "",
relation: r.rel || "",
confidence: r.confidence || "",
conclusion: r.conclusion || "",
repo: r.repo || "",
study: {
uri: r.study || "",
label: r.studyLabel || "",
scope: r.scope || "",
method: r.method || "",
deviation: r.deviation || "",
},
claim: {
uri: r.claim || "",
label: r.claimLabel || "",
aida: decodeAida(r.aida || ""),
type: claimTypeLabel(r.ctype || ""),
},
});
}

const replications = [...byNp.values()].sort((a, b) =>
a.claim.type.localeCompare(b.claim.type),
);

return {
doi: clean,
count: replications.length,
byVerdict: {
validated: replications.filter((r) => isConfirmed(r.verdict)).length,
partial: replications.filter((r) => isPartial(r.verdict)).length,
contradicted: replications.filter((r) => isContradicted(r.verdict))
.length,
},
replications,
};
}
Loading
Loading