Skip to content
Open
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
24 changes: 24 additions & 0 deletions knowledge-graph-evidence-freshness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Knowledge Graph Evidence Freshness

This submission targets [SCIBASE issue #17](https://github.com/SCIBASE-AI/SCIBASE.AI/issues/17) with a focused Scientific Knowledge Graph Integration module.

It evaluates whether graph edges are still safe to recommend when the papers, datasets, protocols, or replication records behind those edges become stale, corrected, superseded, retracted, or failed in replication.

## What It Adds

- Evidence-level risk scoring for retractions, corrections, supersession, stale age, failed replication, and missing evidence.
- Edge-level recommendation decisions: recommend, recommend with note, review before recommending, or suppress.
- Replacement evidence candidates for entity pages and discovery workflows.
- Review packets with stable digests so graph freshness decisions are auditable.
- Focused tests and demo data.

## Demo

```powershell
node knowledge-graph-evidence-freshness/test.js
node knowledge-graph-evidence-freshness/demo.js
```

`demo.mp4` is the reviewer-facing video artifact for the bounty submission. It walks through the problem, implementation, acceptance path, and command validation in 8.4 seconds. `demo.svg` provides a static workflow diagram.

See `acceptance-notes.md` for the payout-gate evidence checklist.
31 changes: 31 additions & 0 deletions knowledge-graph-evidence-freshness/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Acceptance Notes

This is a focused implementation for SCIBASE issue #17, not a generic AI-generated content drop. The slice targets a specific knowledge-graph risk: graph edges and AI recommendations can keep pointing researchers toward outdated, corrected, retracted, or failed evidence unless freshness status is propagated into recommendation decisions.

## What Changed

- Added evidence-level risk scoring for retractions, corrections, supersession, stale age, failed replication, and missing evidence.
- Added edge-level decisions for recommendation suppression and review gating.
- Added replacement evidence candidates for entity pages and discovery flows.
- Added stable review packet digests for auditability.
- Added focused dependency-free tests and demo data.

## Video Demo

- `demo.mp4` shows the problem, implementation, acceptance behavior, and validation command.
- `demo.svg` provides a static workflow diagram.

## Validation

Run from the repository root:

```powershell
node knowledge-graph-evidence-freshness/test.js
node knowledge-graph-evidence-freshness/demo.js
```

Expected result: the test prints `knowledge-graph-evidence-freshness tests passed`, and the demo prints suppressed edges, review-required edges, replacement candidates, and a review packet digest.

## Integration Notes

The module is dependency-free and uses plain evidence and edge objects. The next integration step is wiring these decisions into SCIBASE entity pages, graph search filters, and recommendation generation.
61 changes: 61 additions & 0 deletions knowledge-graph-evidence-freshness/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use strict";

const { buildFreshnessReview } = require("./index");

const review = buildFreshnessReview({
asOf: "2026-05-16T00:00:00Z",
maxAgeDays: 730,
evidence: [
{
id: "paper-crispr-qc-v1",
title: "CRISPR QC protocol first release",
status: "retracted",
publishedAt: "2024-02-10T00:00:00Z",
replacementEvidenceIds: ["paper-crispr-qc-v3"]
},
{
id: "dataset-single-cell-22",
title: "Single-cell atlas v22",
status: "corrected",
publishedAt: "2025-06-01T00:00:00Z",
replacementEvidenceIds: ["dataset-single-cell-23"]
},
{
id: "replication-failure-9",
title: "External failed replication of marker panel",
status: "active",
replicationStatus: "failed",
publishedAt: "2026-01-11T00:00:00Z"
}
],
edges: [
{
id: "edge-crispr-method",
source: "CRISPR editing",
target: "QC protocol",
relation: "uses_method",
evidenceIds: ["paper-crispr-qc-v1"]
},
{
id: "edge-atlas-dataset",
source: "immune relapse",
target: "single-cell atlas",
relation: "uses_dataset",
evidenceIds: ["dataset-single-cell-22"]
},
{
id: "edge-marker-panel",
source: "biomarker panel",
target: "clinical classifier",
relation: "supports_claim",
evidenceIds: ["replication-failure-9"]
}
]
});

console.log(JSON.stringify({
suppressedEdges: review.suppressedEdges,
reviewRequiredEdges: review.reviewRequiredEdges,
replacementCandidates: review.replacementCandidates,
reviewPacketDigest: review.reviewPacketDigest
}, null, 2));
Binary file added knowledge-graph-evidence-freshness/demo.mp4
Binary file not shown.
24 changes: 24 additions & 0 deletions knowledge-graph-evidence-freshness/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
188 changes: 188 additions & 0 deletions knowledge-graph-evidence-freshness/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"use strict";

const crypto = require("crypto");

function stableJson(value) {
if (Array.isArray(value)) return value.map(stableJson);
if (value && typeof value === "object") {
return Object.keys(value).sort().reduce((accumulator, key) => {
accumulator[key] = stableJson(value[key]);
return accumulator;
}, {});
}
return value;
}

function digest(value) {
return crypto
.createHash("sha256")
.update(JSON.stringify(stableJson(value)))
.digest("hex");
}

function asArray(value) {
return Array.isArray(value) ? value : [];
}

function ageInDays(publishedAt, asOf) {
const start = Date.parse(publishedAt);
const end = Date.parse(asOf);
if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
return Math.max(0, Math.floor((end - start) / 86400000));
}

function evaluateEvidence(evidence, options = {}) {
const asOf = options.asOf || new Date().toISOString();
const maxAgeDays = options.maxAgeDays || 730;
const status = evidence.status || "active";
const ageDays = ageInDays(evidence.publishedAt, asOf);
const reasons = [];
let riskScore = 0;

if (status === "retracted") {
reasons.push("retracted_evidence");
riskScore += 100;
}

if (status === "corrected") {
reasons.push("corrected_evidence");
riskScore += 45;
}

if (status === "superseded") {
reasons.push("superseded_evidence");
riskScore += 35;
}

if (ageDays > maxAgeDays) {
reasons.push("stale_by_age");
riskScore += Math.min(35, Math.floor((ageDays - maxAgeDays) / 30) + 10);
}

if (evidence.replicationStatus === "failed") {
reasons.push("failed_replication");
riskScore += 50;
}

const replacementEvidenceIds = asArray(evidence.replacementEvidenceIds).slice().sort();

return {
evidenceId: evidence.id,
title: evidence.title,
status,
ageDays,
riskScore,
reasons,
replacementEvidenceIds,
digest: digest({
id: evidence.id,
status,
ageDays,
reasons,
replacementEvidenceIds
})
};
}

function evaluateEdge(edge, evidenceIndex, options = {}) {
const evidenceReviews = asArray(edge.evidenceIds).map((id) => {
const evidence = evidenceIndex.get(id);
if (!evidence) {
return {
evidenceId: id,
status: "missing",
ageDays: 0,
riskScore: 70,
reasons: ["missing_evidence"],
replacementEvidenceIds: [],
digest: digest({ missing: id })
};
}
return evaluateEvidence(evidence, options);
});

const totalRisk = evidenceReviews.reduce((sum, item) => sum + item.riskScore, 0);
const replacementEvidenceIds = Array.from(new Set(
evidenceReviews.flatMap((item) => item.replacementEvidenceIds)
)).sort();

const hasRetraction = evidenceReviews.some((item) => item.reasons.includes("retracted_evidence"));
const hasFailedReplication = evidenceReviews.some((item) => item.reasons.includes("failed_replication"));
const hasCorrection = evidenceReviews.some((item) =>
item.reasons.includes("corrected_evidence") || item.reasons.includes("superseded_evidence")
);
const hasStaleEvidence = evidenceReviews.some((item) =>
item.reasons.includes("stale_by_age")
);

const decision = hasRetraction || hasFailedReplication
? "suppress_recommendation"
: hasCorrection || hasStaleEvidence || totalRisk >= 45
? "review_before_recommending"
: totalRisk > 0
? "recommend_with_freshness_note"
: "recommend";

return {
edgeId: edge.id,
source: edge.source,
target: edge.target,
relation: edge.relation,
decision,
totalRisk,
evidenceReviews,
replacementEvidenceIds,
recommendationAllowed: decision === "recommend" || decision === "recommend_with_freshness_note",
reviewDigest: digest({
edge,
decision,
totalRisk,
evidenceReviews,
replacementEvidenceIds
})
};
}

function buildFreshnessReview({ evidence, edges, asOf, maxAgeDays }) {
const evidenceIndex = new Map(asArray(evidence).map((item) => [item.id, item]));
const edgeReviews = asArray(edges).map((edge) =>
evaluateEdge(edge, evidenceIndex, { asOf, maxAgeDays })
);

const suppressedEdges = edgeReviews
.filter((edge) => edge.decision === "suppress_recommendation")
.map((edge) => edge.edgeId)
.sort();
const reviewRequiredEdges = edgeReviews
.filter((edge) => edge.decision === "review_before_recommending")
.map((edge) => edge.edgeId)
.sort();
const recommendationEdges = edgeReviews
.filter((edge) => edge.recommendationAllowed)
.map((edge) => edge.edgeId)
.sort();
const replacementCandidates = Array.from(new Set(
edgeReviews.flatMap((edge) => edge.replacementEvidenceIds)
)).sort();

return {
generatedAt: asOf,
suppressedEdges,
reviewRequiredEdges,
recommendationEdges,
replacementCandidates,
edgeReviews,
reviewPacketDigest: digest({
asOf,
maxAgeDays,
edgeReviews
})
};
}

module.exports = {
buildFreshnessReview,
digest,
evaluateEdge,
evaluateEvidence
};
18 changes: 18 additions & 0 deletions knowledge-graph-evidence-freshness/requirements-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Requirements Map

| Issue requirement | Implementation |
| --- | --- |
| Entity extraction and linked data | Graph edges reference evidence IDs behind concepts, datasets, protocols, and claims. |
| Knowledge navigation | Edge decisions protect graph search and entity-page recommendations from stale evidence. |
| AI research recommendations | `recommendationAllowed` suppresses risky recommendations and produces replacement guidance. |
| Filters by reproducibility and time | Evidence scoring includes age and failed replication status. |
| Entity pages with aggregated data | `edgeReviews` provide evidence-level reasons and replacement candidates for UI display. |
| Structured intelligence from scattered documents | Review packet digests make freshness decisions auditable and repeatable. |

## Reviewer Checklist

- Run `node knowledge-graph-evidence-freshness/test.js`.
- Run `node knowledge-graph-evidence-freshness/demo.js`.
- Confirm retracted and failed-replication evidence suppress graph recommendations.
- Confirm corrected and stale evidence require review before recommendation.
- Confirm replacement candidates are exposed for entity pages.
Loading