diff --git a/knowledge-graph-evidence-freshness/README.md b/knowledge-graph-evidence-freshness/README.md new file mode 100644 index 0000000..1ebc934 --- /dev/null +++ b/knowledge-graph-evidence-freshness/README.md @@ -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. diff --git a/knowledge-graph-evidence-freshness/acceptance-notes.md b/knowledge-graph-evidence-freshness/acceptance-notes.md new file mode 100644 index 0000000..43fcd00 --- /dev/null +++ b/knowledge-graph-evidence-freshness/acceptance-notes.md @@ -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. diff --git a/knowledge-graph-evidence-freshness/demo.js b/knowledge-graph-evidence-freshness/demo.js new file mode 100644 index 0000000..3b2dffb --- /dev/null +++ b/knowledge-graph-evidence-freshness/demo.js @@ -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)); diff --git a/knowledge-graph-evidence-freshness/demo.mp4 b/knowledge-graph-evidence-freshness/demo.mp4 new file mode 100644 index 0000000..663bb45 Binary files /dev/null and b/knowledge-graph-evidence-freshness/demo.mp4 differ diff --git a/knowledge-graph-evidence-freshness/demo.svg b/knowledge-graph-evidence-freshness/demo.svg new file mode 100644 index 0000000..0b41f07 --- /dev/null +++ b/knowledge-graph-evidence-freshness/demo.svg @@ -0,0 +1,24 @@ + + Knowledge graph evidence freshness workflow + Evidence status is scored, graph edges are reviewed, unsafe recommendations are suppressed, and replacement candidates are surfaced. + + + + + + Evidence + corrected, stale, + retracted, failed + Edge Review + risk by relation + Decision + suppress or review + Replace + candidate IDs + + + + + edge-crispr-method + retracted evidence suppresses recommendation, replacement paper-crispr-qc-v3 is surfaced + diff --git a/knowledge-graph-evidence-freshness/index.js b/knowledge-graph-evidence-freshness/index.js new file mode 100644 index 0000000..8ecedb0 --- /dev/null +++ b/knowledge-graph-evidence-freshness/index.js @@ -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 +}; diff --git a/knowledge-graph-evidence-freshness/requirements-map.md b/knowledge-graph-evidence-freshness/requirements-map.md new file mode 100644 index 0000000..a828f75 --- /dev/null +++ b/knowledge-graph-evidence-freshness/requirements-map.md @@ -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. diff --git a/knowledge-graph-evidence-freshness/test.js b/knowledge-graph-evidence-freshness/test.js new file mode 100644 index 0000000..6a2b73e --- /dev/null +++ b/knowledge-graph-evidence-freshness/test.js @@ -0,0 +1,111 @@ +"use strict"; + +const assert = require("assert"); +const { + buildFreshnessReview, + digest, + evaluateEvidence +} = require("./index"); + +const 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: "paper-crispr-qc-v3", + title: "CRISPR QC protocol corrected release", + status: "active", + publishedAt: "2026-03-04T00:00:00Z" + }, + { + id: "dataset-single-cell-22", + title: "Single-cell atlas v22", + status: "corrected", + publishedAt: "2025-06-01T00:00:00Z", + replacementEvidenceIds: ["dataset-single-cell-23"] + }, + { + id: "dataset-single-cell-23", + title: "Single-cell atlas v23", + status: "active", + publishedAt: "2026-04-14T00:00:00Z" + }, + { + id: "protocol-culture-old", + title: "Organoid culture protocol 2019", + status: "active", + publishedAt: "2019-03-01T00:00:00Z" + }, + { + id: "replication-failure-9", + title: "External failed replication of marker panel", + status: "active", + replicationStatus: "failed", + publishedAt: "2026-01-11T00:00:00Z" + } +]; + +const 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-organoid-culture", + source: "neuroinflammation assay", + target: "organoid culture", + relation: "uses_protocol", + evidenceIds: ["protocol-culture-old"] + }, + { + id: "edge-marker-panel", + source: "biomarker panel", + target: "clinical classifier", + relation: "supports_claim", + evidenceIds: ["replication-failure-9"] + } +]; + +const retraction = evaluateEvidence(evidence[0], { + asOf: "2026-05-16T00:00:00Z", + maxAgeDays: 730 +}); +assert.strictEqual(retraction.status, "retracted"); +assert.ok(retraction.riskScore >= 100); +assert.deepStrictEqual(retraction.replacementEvidenceIds, ["paper-crispr-qc-v3"]); + +const review = buildFreshnessReview({ + evidence, + edges, + asOf: "2026-05-16T00:00:00Z", + maxAgeDays: 730 +}); + +assert.deepStrictEqual(review.suppressedEdges, ["edge-crispr-method", "edge-marker-panel"]); +assert.deepStrictEqual(review.reviewRequiredEdges, ["edge-atlas-dataset", "edge-organoid-culture"]); +assert.deepStrictEqual(review.recommendationEdges, []); +assert.deepStrictEqual(review.replacementCandidates, ["dataset-single-cell-23", "paper-crispr-qc-v3"]); + +const correctedEdge = review.edgeReviews.find((edge) => edge.edgeId === "edge-atlas-dataset"); +assert.strictEqual(correctedEdge.decision, "review_before_recommending"); +assert.strictEqual(correctedEdge.recommendationAllowed, false); + +const staleEdge = review.edgeReviews.find((edge) => edge.edgeId === "edge-organoid-culture"); +assert.ok(staleEdge.evidenceReviews[0].reasons.includes("stale_by_age")); +assert.strictEqual(digest(review).length, 64); + +console.log("knowledge-graph-evidence-freshness tests passed");