diff --git a/knowledge-graph-claim-qualifier-guard/README.md b/knowledge-graph-claim-qualifier-guard/README.md
new file mode 100644
index 0000000..d2ee6a8
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/README.md
@@ -0,0 +1,33 @@
+# Knowledge Graph Claim Qualifier Guard
+
+This module is a focused slice for issue #17, Scientific Knowledge Graph Integration. It validates extracted graph edges before they are published to entity pages, discovery mode, or recommendation payloads.
+
+It is intentionally not another extractor, graph navigator, freshness checker, diversity guard, or ontology alias module. The guard focuses on claim qualification:
+
+- causal predicates backed only by associative evidence
+- negative or null-result evidence missing explicit polarity
+- recommendation copy that overstates the evidence
+- graph edges missing required experimental context qualifiers
+
+The output is a deterministic curator packet with blockers, warnings, held edge IDs, publishable edge IDs, and a release recommendation.
+
+## Run
+
+```bash
+node knowledge-graph-claim-qualifier-guard/test.js
+node knowledge-graph-claim-qualifier-guard/demo.js
+```
+
+The demo writes:
+
+- `reports/claim-qualifier-packet.json`
+- `reports/claim-qualifier-report.md`
+- `reports/summary.svg`
+- `reports/summary.png`
+- `reports/demo.mp4`
+
+## Design Notes
+
+- Dependency-free Node.js implementation.
+- Synthetic data only. No external services or credentials.
+- Blocks publication of unsafe graph edges while allowing well-qualified controlled-experiment edges through.
diff --git a/knowledge-graph-claim-qualifier-guard/acceptance-notes.md b/knowledge-graph-claim-qualifier-guard/acceptance-notes.md
new file mode 100644
index 0000000..988c8a8
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/acceptance-notes.md
@@ -0,0 +1,25 @@
+# Acceptance Notes
+
+## What To Review
+
+- `index.js` implements claim qualifier checks for scientific graph edges.
+- `sample-data.js` includes risky causal overclaim, negative-result polarity, and clean controlled-experiment examples.
+- `test.js` validates blocker, warning, clean-pass, and report-rendering behavior.
+- `demo.js` generates the curator packet and visual demo artifact.
+
+## Verification
+
+```bash
+node knowledge-graph-claim-qualifier-guard/test.js
+node knowledge-graph-claim-qualifier-guard/demo.js
+node --check knowledge-graph-claim-qualifier-guard/index.js
+node --check knowledge-graph-claim-qualifier-guard/sample-data.js
+node --check knowledge-graph-claim-qualifier-guard/test.js
+node --check knowledge-graph-claim-qualifier-guard/demo.js
+git diff --check
+ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,duration -of default=noprint_wrappers=1 knowledge-graph-claim-qualifier-guard/reports/demo.mp4
+```
+
+## Expected Demo Result
+
+The risky packet returns `hold_claim_edges` with blockers for causal overclaim, missing negative-result polarity, and unsupported recommendation language. The clean packet returns `graph_edges_ready`.
diff --git a/knowledge-graph-claim-qualifier-guard/demo.js b/knowledge-graph-claim-qualifier-guard/demo.js
new file mode 100644
index 0000000..d12f578
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/demo.js
@@ -0,0 +1,51 @@
+const { spawnSync } = require("child_process");
+const fs = require("fs");
+const path = require("path");
+const { evaluateClaimQualifiers, writeReportBundle } = require("./index");
+const { riskyPacket } = require("./sample-data");
+
+const reportsDir = path.join(__dirname, "reports");
+const result = evaluateClaimQualifiers(riskyPacket);
+const paths = writeReportBundle(result, reportsDir);
+const pngPath = path.join(reportsDir, "summary.png");
+const mp4Path = path.join(reportsDir, "demo.mp4");
+
+function runCommand(command, args) {
+ const child = spawnSync(command, args, { encoding: "utf8" });
+ if (child.status !== 0) {
+ throw new Error(`${command} failed: ${child.stderr || child.stdout}`);
+ }
+}
+
+if (fs.existsSync(paths.svgPath)) {
+ runCommand("rsvg-convert", ["-w", "1280", "-h", "720", paths.svgPath, "-o", pngPath]);
+ runCommand("ffmpeg", [
+ "-y",
+ "-loop",
+ "1",
+ "-i",
+ pngPath,
+ "-t",
+ "12",
+ "-vf",
+ "format=yuv420p",
+ "-c:v",
+ "libx264",
+ "-movflags",
+ "+faststart",
+ mp4Path
+ ]);
+}
+
+console.log(
+ [
+ `status=${result.status}`,
+ `blockers=${result.summary.blockers}`,
+ `warnings=${result.summary.warnings}`,
+ `heldEdges=${result.summary.heldEdges}`,
+ `publishableEdges=${result.summary.publishableEdges}`,
+ `report=${paths.markdownPath}`,
+ `packet=${paths.jsonPath}`,
+ `demo=${mp4Path}`
+ ].join("\n")
+);
diff --git a/knowledge-graph-claim-qualifier-guard/index.js b/knowledge-graph-claim-qualifier-guard/index.js
new file mode 100644
index 0000000..67dff42
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/index.js
@@ -0,0 +1,254 @@
+const fs = require("fs");
+const path = require("path");
+
+const DEFAULT_POLICY = {
+ causalPredicates: ["causes", "drives", "increases", "decreases", "inhibits", "activates", "prevents"],
+ associationTerms: ["associated with", "correlates with", "linked to", "observed with", "co-occurs with"],
+ negativeTerms: ["no significant", "not associated", "failed to reproduce", "did not improve", "null result", "inconclusive"],
+ experimentalEvidenceTypes: ["randomized_trial", "controlled_experiment", "mechanistic_assay", "replication_success"],
+ requiredContextFields: ["species", "assay", "dataset"]
+};
+
+function text(value) {
+ return String(value || "").replace(/\s+/g, " ").trim();
+}
+
+function lower(value) {
+ return text(value).toLowerCase();
+}
+
+function issue(kind, severity, edgeId, message, evidence = {}) {
+ return { kind, severity, edgeId, message, evidence };
+}
+
+function includesAny(value, terms) {
+ const normalized = lower(value);
+ return terms.filter((term) => normalized.includes(lower(term)));
+}
+
+function isCausalPredicate(predicate, policy) {
+ return policy.causalPredicates.includes(lower(predicate));
+}
+
+function hasExperimentalEvidence(edge, policy) {
+ return policy.experimentalEvidenceTypes.includes(lower(edge.evidenceType));
+}
+
+function missingContext(edge, policy) {
+ const context = edge.context || {};
+ return policy.requiredContextFields.filter((field) => !text(context[field]));
+}
+
+function evaluateEdge(edge, policy) {
+ const id = edge.id || "edge";
+ const evidenceSentence = text(edge.evidenceSentence);
+ const recommendationText = text(edge.recommendationText);
+ const predicate = lower(edge.predicate);
+ const qualifier = lower(edge.qualifier || edge.claimQualifier);
+ const blockers = [];
+ const warnings = [];
+
+ const associationHits = includesAny(evidenceSentence, policy.associationTerms);
+ if (isCausalPredicate(predicate, policy) && associationHits.length && !hasExperimentalEvidence(edge, policy)) {
+ blockers.push(
+ issue("causal_overclaim_from_association", "blocker", id, "Associational evidence cannot publish as a causal graph edge.", {
+ predicate,
+ evidenceType: edge.evidenceType,
+ associationHits
+ })
+ );
+ }
+
+ const negativeHits = includesAny(evidenceSentence, policy.negativeTerms);
+ if (negativeHits.length && !["negative", "null", "inconclusive"].includes(qualifier)) {
+ blockers.push(
+ issue("missing_negative_result_polarity", "blocker", id, "Negative or inconclusive evidence needs explicit graph-edge polarity.", {
+ negativeHits,
+ qualifier: edge.qualifier || null
+ })
+ );
+ }
+
+ if (isCausalPredicate(predicate, policy) && !hasExperimentalEvidence(edge, policy)) {
+ warnings.push(
+ issue("causal_edge_without_experimental_evidence", "warning", id, "Causal-looking edge should be downgraded or curator-reviewed without experimental evidence.", {
+ predicate,
+ evidenceType: edge.evidenceType
+ })
+ );
+ }
+
+ const missing = missingContext(edge, policy);
+ if (missing.length) {
+ warnings.push(
+ issue("missing_experimental_context", "warning", id, "Graph edge is missing context needed for entity pages and recommendation filters.", {
+ missing
+ })
+ );
+ }
+
+ if (
+ recommendationText &&
+ includesAny(recommendationText, ["use this to prove", "will cause", "guarantees", "definitively"]).length &&
+ !hasExperimentalEvidence(edge, policy)
+ ) {
+ blockers.push(
+ issue("unsupported_recommendation_language", "blocker", id, "Recommendation text is stronger than the linked evidence supports.", {
+ recommendationText
+ })
+ );
+ }
+
+ return {
+ id,
+ subject: edge.subject,
+ predicate: edge.predicate,
+ object: edge.object,
+ qualifier: edge.qualifier || null,
+ evidenceType: edge.evidenceType,
+ blockers,
+ warnings,
+ publishable: blockers.length === 0
+ };
+}
+
+function evaluateClaimQualifiers(packet, policyOverrides = {}) {
+ const policy = { ...DEFAULT_POLICY, ...(packet.policy || {}), ...policyOverrides };
+ const edges = Array.isArray(packet.edges) ? packet.edges : [];
+ const decisions = edges.map((edge) => evaluateEdge(edge, policy));
+ const blockers = decisions.flatMap((decision) => decision.blockers);
+ const warnings = decisions.flatMap((decision) => decision.warnings);
+ const heldEdges = decisions.filter((decision) => !decision.publishable).map((decision) => decision.id);
+ const publishableEdges = decisions.filter((decision) => decision.publishable).map((decision) => decision.id);
+
+ return {
+ graphId: packet.graphId || "scientific-knowledge-graph",
+ generatedAt: packet.generatedAt || new Date().toISOString(),
+ status: blockers.length ? "hold_claim_edges" : warnings.length ? "curator_review_recommended" : "graph_edges_ready",
+ summary: {
+ edgesChecked: edges.length,
+ blockers: blockers.length,
+ warnings: warnings.length,
+ heldEdges: heldEdges.length,
+ publishableEdges: publishableEdges.length
+ },
+ curatorPacket: {
+ blockers,
+ warnings,
+ heldEdges,
+ publishableEdges,
+ recommendation: blockers.length
+ ? "Hold unsafe graph edges and downgrade or qualify claims before publication."
+ : warnings.length
+ ? "Publish after curator confirms context qualifiers."
+ : "Publish graph edges and allow recommendation use."
+ },
+ decisions
+ };
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function buildMarkdownReport(result) {
+ const lines = [
+ "# Knowledge Graph Claim Qualifier Guard Report",
+ "",
+ `Graph: ${result.graphId}`,
+ `Status: ${result.status}`,
+ `Generated: ${result.generatedAt}`,
+ "",
+ "## Summary",
+ "",
+ `- Edges checked: ${result.summary.edgesChecked}`,
+ `- Blockers: ${result.summary.blockers}`,
+ `- Warnings: ${result.summary.warnings}`,
+ `- Held edges: ${result.summary.heldEdges}`,
+ `- Publishable edges: ${result.summary.publishableEdges}`,
+ "",
+ "## Recommendation",
+ "",
+ result.curatorPacket.recommendation,
+ "",
+ "## Blockers",
+ ""
+ ];
+
+ if (!result.curatorPacket.blockers.length) {
+ lines.push("- None");
+ } else {
+ for (const blocker of result.curatorPacket.blockers) {
+ lines.push(`- ${blocker.edgeId}: ${blocker.kind} - ${blocker.message}`);
+ }
+ }
+
+ lines.push("", "## Edge Decisions", "");
+ for (const decision of result.decisions) {
+ lines.push(`- ${decision.id}: ${decision.subject} ${decision.predicate} ${decision.object} -> ${decision.publishable ? "publish" : "hold"}`);
+ }
+
+ return `${lines.join("\n")}\n`;
+}
+
+function buildSvgSummary(result) {
+ const statusColor = result.status === "graph_edges_ready" ? "#1f8f5f" : result.status === "curator_review_recommended" ? "#b26b00" : "#b42318";
+ const bars = [
+ ["Edges", result.summary.edgesChecked, "#2563eb"],
+ ["Blockers", result.summary.blockers, "#b42318"],
+ ["Warnings", result.summary.warnings, "#b26b00"],
+ ["Held", result.summary.heldEdges, "#7c3aed"],
+ ["Publishable", result.summary.publishableEdges, "#0f766e"]
+ ];
+ const maxValue = Math.max(1, ...bars.map((bar) => bar[1]));
+ const rows = bars
+ .map(([label, value, color], index) => {
+ const y = 180 + index * 54;
+ const width = Math.max(8, Math.round((value / maxValue) * 520));
+ return `
+ ${escapeXml(label)}
+
+
+ ${value}
+ `;
+ })
+ .join("");
+
+ return `
+
+`;
+}
+
+function writeReportBundle(result, outputDir) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ const jsonPath = path.join(outputDir, "claim-qualifier-packet.json");
+ const markdownPath = path.join(outputDir, "claim-qualifier-report.md");
+ const svgPath = path.join(outputDir, "summary.svg");
+ fs.writeFileSync(jsonPath, `${JSON.stringify(result, null, 2)}\n`);
+ fs.writeFileSync(markdownPath, buildMarkdownReport(result));
+ fs.writeFileSync(svgPath, buildSvgSummary(result));
+ return { jsonPath, markdownPath, svgPath };
+}
+
+module.exports = {
+ DEFAULT_POLICY,
+ evaluateClaimQualifiers,
+ buildMarkdownReport,
+ buildSvgSummary,
+ writeReportBundle
+};
diff --git a/knowledge-graph-claim-qualifier-guard/reports/claim-qualifier-packet.json b/knowledge-graph-claim-qualifier-guard/reports/claim-qualifier-packet.json
new file mode 100644
index 0000000..e53b840
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/reports/claim-qualifier-packet.json
@@ -0,0 +1,197 @@
+{
+ "graphId": "pubmed-crispr-neuro-graph",
+ "generatedAt": "2026-05-21T09:50:00.000Z",
+ "status": "hold_claim_edges",
+ "summary": {
+ "edgesChecked": 3,
+ "blockers": 3,
+ "warnings": 3,
+ "heldEdges": 2,
+ "publishableEdges": 1
+ },
+ "curatorPacket": {
+ "blockers": [
+ {
+ "kind": "causal_overclaim_from_association",
+ "severity": "blocker",
+ "edgeId": "edge-overclaim-001",
+ "message": "Associational evidence cannot publish as a causal graph edge.",
+ "evidence": {
+ "predicate": "causes",
+ "evidenceType": "observational_cooccurrence",
+ "associationHits": [
+ "associated with"
+ ]
+ }
+ },
+ {
+ "kind": "unsupported_recommendation_language",
+ "severity": "blocker",
+ "edgeId": "edge-overclaim-001",
+ "message": "Recommendation text is stronger than the linked evidence supports.",
+ "evidence": {
+ "recommendationText": "Use this to prove CRISPR inhibition causes neuronal repair."
+ }
+ },
+ {
+ "kind": "missing_negative_result_polarity",
+ "severity": "blocker",
+ "edgeId": "edge-negative-002",
+ "message": "Negative or inconclusive evidence needs explicit graph-edge polarity.",
+ "evidence": {
+ "negativeHits": [
+ "no significant",
+ "failed to reproduce"
+ ],
+ "qualifier": "positive"
+ }
+ }
+ ],
+ "warnings": [
+ {
+ "kind": "causal_edge_without_experimental_evidence",
+ "severity": "warning",
+ "edgeId": "edge-overclaim-001",
+ "message": "Causal-looking edge should be downgraded or curator-reviewed without experimental evidence.",
+ "evidence": {
+ "predicate": "causes",
+ "evidenceType": "observational_cooccurrence"
+ }
+ },
+ {
+ "kind": "missing_experimental_context",
+ "severity": "warning",
+ "edgeId": "edge-overclaim-001",
+ "message": "Graph edge is missing context needed for entity pages and recommendation filters.",
+ "evidence": {
+ "missing": [
+ "assay"
+ ]
+ }
+ },
+ {
+ "kind": "causal_edge_without_experimental_evidence",
+ "severity": "warning",
+ "edgeId": "edge-negative-002",
+ "message": "Causal-looking edge should be downgraded or curator-reviewed without experimental evidence.",
+ "evidence": {
+ "predicate": "increases",
+ "evidenceType": "replication_failure"
+ }
+ }
+ ],
+ "heldEdges": [
+ "edge-overclaim-001",
+ "edge-negative-002"
+ ],
+ "publishableEdges": [
+ "edge-ready-003"
+ ],
+ "recommendation": "Hold unsafe graph edges and downgrade or qualify claims before publication."
+ },
+ "decisions": [
+ {
+ "id": "edge-overclaim-001",
+ "subject": "CRISPR inhibition protocol",
+ "predicate": "causes",
+ "object": "improved neuronal repair",
+ "qualifier": "positive",
+ "evidenceType": "observational_cooccurrence",
+ "blockers": [
+ {
+ "kind": "causal_overclaim_from_association",
+ "severity": "blocker",
+ "edgeId": "edge-overclaim-001",
+ "message": "Associational evidence cannot publish as a causal graph edge.",
+ "evidence": {
+ "predicate": "causes",
+ "evidenceType": "observational_cooccurrence",
+ "associationHits": [
+ "associated with"
+ ]
+ }
+ },
+ {
+ "kind": "unsupported_recommendation_language",
+ "severity": "blocker",
+ "edgeId": "edge-overclaim-001",
+ "message": "Recommendation text is stronger than the linked evidence supports.",
+ "evidence": {
+ "recommendationText": "Use this to prove CRISPR inhibition causes neuronal repair."
+ }
+ }
+ ],
+ "warnings": [
+ {
+ "kind": "causal_edge_without_experimental_evidence",
+ "severity": "warning",
+ "edgeId": "edge-overclaim-001",
+ "message": "Causal-looking edge should be downgraded or curator-reviewed without experimental evidence.",
+ "evidence": {
+ "predicate": "causes",
+ "evidenceType": "observational_cooccurrence"
+ }
+ },
+ {
+ "kind": "missing_experimental_context",
+ "severity": "warning",
+ "edgeId": "edge-overclaim-001",
+ "message": "Graph edge is missing context needed for entity pages and recommendation filters.",
+ "evidence": {
+ "missing": [
+ "assay"
+ ]
+ }
+ }
+ ],
+ "publishable": false
+ },
+ {
+ "id": "edge-negative-002",
+ "subject": "Method X",
+ "predicate": "increases",
+ "object": "reproducibility score",
+ "qualifier": "positive",
+ "evidenceType": "replication_failure",
+ "blockers": [
+ {
+ "kind": "missing_negative_result_polarity",
+ "severity": "blocker",
+ "edgeId": "edge-negative-002",
+ "message": "Negative or inconclusive evidence needs explicit graph-edge polarity.",
+ "evidence": {
+ "negativeHits": [
+ "no significant",
+ "failed to reproduce"
+ ],
+ "qualifier": "positive"
+ }
+ }
+ ],
+ "warnings": [
+ {
+ "kind": "causal_edge_without_experimental_evidence",
+ "severity": "warning",
+ "edgeId": "edge-negative-002",
+ "message": "Causal-looking edge should be downgraded or curator-reviewed without experimental evidence.",
+ "evidence": {
+ "predicate": "increases",
+ "evidenceType": "replication_failure"
+ }
+ }
+ ],
+ "publishable": false
+ },
+ {
+ "id": "edge-ready-003",
+ "subject": "Assay calibration protocol",
+ "predicate": "improves",
+ "object": "measurement consistency",
+ "qualifier": "context_limited_positive",
+ "evidenceType": "controlled_experiment",
+ "blockers": [],
+ "warnings": [],
+ "publishable": true
+ }
+ ]
+}
diff --git a/knowledge-graph-claim-qualifier-guard/reports/claim-qualifier-report.md b/knowledge-graph-claim-qualifier-guard/reports/claim-qualifier-report.md
new file mode 100644
index 0000000..a5e40de
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/reports/claim-qualifier-report.md
@@ -0,0 +1,29 @@
+# Knowledge Graph Claim Qualifier Guard Report
+
+Graph: pubmed-crispr-neuro-graph
+Status: hold_claim_edges
+Generated: 2026-05-21T09:50:00.000Z
+
+## Summary
+
+- Edges checked: 3
+- Blockers: 3
+- Warnings: 3
+- Held edges: 2
+- Publishable edges: 1
+
+## Recommendation
+
+Hold unsafe graph edges and downgrade or qualify claims before publication.
+
+## Blockers
+
+- edge-overclaim-001: causal_overclaim_from_association - Associational evidence cannot publish as a causal graph edge.
+- edge-overclaim-001: unsupported_recommendation_language - Recommendation text is stronger than the linked evidence supports.
+- edge-negative-002: missing_negative_result_polarity - Negative or inconclusive evidence needs explicit graph-edge polarity.
+
+## Edge Decisions
+
+- edge-overclaim-001: CRISPR inhibition protocol causes improved neuronal repair -> hold
+- edge-negative-002: Method X increases reproducibility score -> hold
+- edge-ready-003: Assay calibration protocol improves measurement consistency -> publish
diff --git a/knowledge-graph-claim-qualifier-guard/reports/demo.mp4 b/knowledge-graph-claim-qualifier-guard/reports/demo.mp4
new file mode 100644
index 0000000..f73f3a7
Binary files /dev/null and b/knowledge-graph-claim-qualifier-guard/reports/demo.mp4 differ
diff --git a/knowledge-graph-claim-qualifier-guard/reports/summary.png b/knowledge-graph-claim-qualifier-guard/reports/summary.png
new file mode 100644
index 0000000..8379783
Binary files /dev/null and b/knowledge-graph-claim-qualifier-guard/reports/summary.png differ
diff --git a/knowledge-graph-claim-qualifier-guard/reports/summary.svg b/knowledge-graph-claim-qualifier-guard/reports/summary.svg
new file mode 100644
index 0000000..71c6c9c
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/reports/summary.svg
@@ -0,0 +1,38 @@
+
+
diff --git a/knowledge-graph-claim-qualifier-guard/requirements-map.md b/knowledge-graph-claim-qualifier-guard/requirements-map.md
new file mode 100644
index 0000000..743feb0
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/requirements-map.md
@@ -0,0 +1,21 @@
+# Requirements Map
+
+Issue #17 asks for entity extraction, linked data, schema.org metadata, knowledge navigation, and AI research recommendations. This submission covers the safety layer between extracted evidence and published graph/recommendation edges.
+
+| Requirement | Coverage |
+| --- | --- |
+| Entity extraction outputs linked data | Validates extracted subject-predicate-object edges before publication. |
+| Relationship evidence | Detects when associative evidence is overpromoted into causal graph predicates. |
+| Entity pages | Holds unsafe or underqualified edges before they appear on entity pages. |
+| Knowledge navigation | Ensures graph search paths preserve negative/null-result polarity. |
+| AI recommendations | Blocks recommendation language that is stronger than the evidence supports. |
+| Filters by domain/time/reproducibility | Requires context qualifiers such as species, assay, and dataset. |
+| Explainable recommendations | Emits JSON and Markdown curator packets with blocker reasons. |
+
+## Non-Goals
+
+- This is not a general entity extractor.
+- This is not an ontology migration or alias module.
+- This is not a recommendation diversity or visibility guard.
+- This is not a measurement harmonization module.
+- This is not a broad graph navigator.
diff --git a/knowledge-graph-claim-qualifier-guard/sample-data.js b/knowledge-graph-claim-qualifier-guard/sample-data.js
new file mode 100644
index 0000000..e86de9b
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/sample-data.js
@@ -0,0 +1,78 @@
+const riskyPacket = {
+ graphId: "pubmed-crispr-neuro-graph",
+ generatedAt: "2026-05-21T09:50:00.000Z",
+ edges: [
+ {
+ id: "edge-overclaim-001",
+ subject: "CRISPR inhibition protocol",
+ predicate: "causes",
+ object: "improved neuronal repair",
+ qualifier: "positive",
+ evidenceType: "observational_cooccurrence",
+ evidenceSentence:
+ "The protocol was associated with improved neuronal repair markers in a small retrospective dataset.",
+ recommendationText: "Use this to prove CRISPR inhibition causes neuronal repair.",
+ context: {
+ species: "mouse",
+ dataset: "dataset:neuro-12"
+ }
+ },
+ {
+ id: "edge-negative-002",
+ subject: "Method X",
+ predicate: "increases",
+ object: "reproducibility score",
+ qualifier: "positive",
+ evidenceType: "replication_failure",
+ evidenceSentence:
+ "Independent labs reported no significant improvement and failed to reproduce the original score increase.",
+ recommendationText: "Researchers in your field are citing Method X.",
+ context: {
+ species: "human cohort",
+ assay: "meta-analysis",
+ dataset: "dataset:replication-7"
+ }
+ },
+ {
+ id: "edge-ready-003",
+ subject: "Assay calibration protocol",
+ predicate: "improves",
+ object: "measurement consistency",
+ qualifier: "context_limited_positive",
+ evidenceType: "controlled_experiment",
+ evidenceSentence:
+ "A controlled experiment showed improved measurement consistency in Table 2 under the stated calibration protocol.",
+ recommendationText: "Consider this protocol for related calibration experiments.",
+ context: {
+ species: "cell line",
+ assay: "fluorescence calibration",
+ dataset: "dataset:calibration-4"
+ }
+ }
+ ]
+};
+
+const cleanPacket = {
+ graphId: "clean-kg-release",
+ generatedAt: "2026-05-21T09:55:00.000Z",
+ edges: [
+ {
+ id: "edge-clean-001",
+ subject: "Assay calibration protocol",
+ predicate: "improves",
+ object: "measurement consistency",
+ qualifier: "context_limited_positive",
+ evidenceType: "controlled_experiment",
+ evidenceSentence:
+ "A controlled experiment showed improved measurement consistency under matched instrument settings.",
+ recommendationText: "Consider this protocol where the same assay and dataset conditions apply.",
+ context: {
+ species: "cell line",
+ assay: "fluorescence calibration",
+ dataset: "dataset:calibration-4"
+ }
+ }
+ ]
+};
+
+module.exports = { riskyPacket, cleanPacket };
diff --git a/knowledge-graph-claim-qualifier-guard/test.js b/knowledge-graph-claim-qualifier-guard/test.js
new file mode 100644
index 0000000..6961335
--- /dev/null
+++ b/knowledge-graph-claim-qualifier-guard/test.js
@@ -0,0 +1,67 @@
+const assert = require("assert");
+const fs = require("fs");
+const os = require("os");
+const path = require("path");
+const {
+ evaluateClaimQualifiers,
+ buildMarkdownReport,
+ buildSvgSummary,
+ writeReportBundle
+} = require("./index");
+const { riskyPacket, cleanPacket } = require("./sample-data");
+
+function testCausalOverclaimBlocked() {
+ const result = evaluateClaimQualifiers(riskyPacket);
+ assert.strictEqual(result.status, "hold_claim_edges");
+ assert(result.curatorPacket.blockers.some((blocker) => blocker.kind === "causal_overclaim_from_association"));
+ assert(result.curatorPacket.blockers.some((blocker) => blocker.kind === "unsupported_recommendation_language"));
+ assert(result.curatorPacket.heldEdges.includes("edge-overclaim-001"));
+}
+
+function testNegativePolarityBlocked() {
+ const result = evaluateClaimQualifiers(riskyPacket);
+ const blocker = result.curatorPacket.blockers.find((entry) => entry.kind === "missing_negative_result_polarity");
+ assert(blocker, "expected missing negative polarity blocker");
+ assert.strictEqual(blocker.edgeId, "edge-negative-002");
+}
+
+function testMissingContextWarns() {
+ const result = evaluateClaimQualifiers(riskyPacket);
+ const warning = result.curatorPacket.warnings.find((entry) => entry.kind === "missing_experimental_context");
+ assert(warning, "expected missing context warning");
+ assert(warning.evidence.missing.includes("assay"));
+}
+
+function testCleanPacketPublishes() {
+ const result = evaluateClaimQualifiers(cleanPacket);
+ assert.strictEqual(result.status, "graph_edges_ready");
+ assert.strictEqual(result.summary.blockers, 0);
+ assert.strictEqual(result.curatorPacket.publishableEdges.length, 1);
+}
+
+function testReportsRender() {
+ const result = evaluateClaimQualifiers(riskyPacket);
+ const markdown = buildMarkdownReport(result);
+ const svg = buildSvgSummary(result);
+ assert(markdown.includes("Knowledge Graph Claim Qualifier Guard Report"));
+ assert(markdown.includes("hold_claim_edges"));
+ assert(svg.includes("