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 ` + + + + Knowledge Graph Claim Qualifier Guard + Curator packet for ${escapeXml(result.graphId)} + + ${escapeXml(result.status)} + ${rows} + Publication decision + ${escapeXml(result.curatorPacket.recommendation)} + Synthetic data only. Dependency-free local demo. + +`; +} + +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 @@ + + + + + Knowledge Graph Claim Qualifier Guard + Curator packet for pubmed-crispr-neuro-graph + + hold_claim_edges + + Edges + + + 3 + + Blockers + + + 3 + + Warnings + + + 3 + + Held + + + 2 + + Publishable + + + 1 + + Publication decision + Hold unsafe graph edges and downgrade or qualify claims before publication. + Synthetic data only. Dependency-free local demo. + 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("