diff --git a/causal-claim-validity-guard/README.md b/causal-claim-validity-guard/README.md new file mode 100644 index 00000000..fbe4195c --- /dev/null +++ b/causal-claim-validity-guard/README.md @@ -0,0 +1,48 @@ +# Causal Claim Validity Guard + +This module adds a focused causal-claim review gate for the AI-Powered Research Assistant Suite in issue `#16`. + +Scientific manuscripts often drift from careful associational language into causal conclusions. The guard inspects synthetic review packets and determines whether AI-generated peer-review output should be released, revised, or held until the causal claim is supported. + +## Scope + +- Detects causal-language overreach in manuscript claims and conclusions. +- Checks whether the study design can support causal language. +- Flags missing comparator/control support, temporal-order gaps, confounder handling gaps, and effect-size uncertainty. +- Produces evidence-linked remediation items suitable for authors, internal reviewers, or automated pre-submission checks. +- Uses synthetic data only. +- Uses no external AI APIs, credentials, private manuscripts, payment systems, or network calls. + +## Requirement Map + +| Issue #16 capability | Implementation | +| --- | --- | +| Auto peer review reports | Emits structured reviewer findings, severity, decision, and remediation. | +| Methodological red flags | Detects design/evidence mismatch, missing controls, confounder gaps, and uncertainty gaps. | +| Claims vs evidence alignment | Compares claim intent, causal wording, study design, temporal evidence, and comparator evidence. | +| Adaptive templates per domain | Supports packet-level `domain` and domain-specific reviewer note generation. | +| Reproducible deterministic output | Demo and tests use fixed synthetic packets and deterministic scoring. | + +## Run + +```bash +npm test +npm run demo +npm run check +``` + +Demo artifacts are written to `artifacts/`: + +- `causal-claim-review.json` +- `causal-claim-review.md` +- `causal-claim-summary.svg` + +## Decisions + +- `RELEASE`: evidence supports the causal wording. +- `REVISE`: the manuscript can likely be repaired with clearer language or additional evidence. +- `HOLD`: the causal claim is not safe to release without material methodological support. + +## AI Assistance Disclosure + +Implemented with Codex assistance and locally verified with deterministic tests and generated artifacts. diff --git a/causal-claim-validity-guard/artifacts/causal-claim-review.json b/causal-claim-validity-guard/artifacts/causal-claim-review.json new file mode 100644 index 00000000..6d2a7c8a --- /dev/null +++ b/causal-claim-validity-guard/artifacts/causal-claim-review.json @@ -0,0 +1,129 @@ +{ + "generatedAt": "2026-08-15T20:24:14.343Z", + "packetCount": 4, + "decisionCounts": { + "RELEASE": 2, + "REVISE": 1, + "HOLD": 1 + }, + "reviews": [ + { + "id": "cardio-rct-001", + "title": "Remote coaching reduces systolic blood pressure in high-risk adults", + "domain": "clinical trials", + "decision": "RELEASE", + "confidence": 0.95, + "causalLanguageDetected": true, + "findingCount": 0, + "findings": [], + "reviewerNote": "clinical trials: causal wording is aligned with the study design and evidence packet." + }, + { + "id": "education-cross-sectional-002", + "title": "Daily tutoring app usage improves mathematics scores", + "domain": "education", + "decision": "HOLD", + "confidence": 0.15, + "causalLanguageDetected": true, + "findingCount": 5, + "findings": [ + { + "code": "DESIGN_CANNOT_SUPPORT_CAUSAL_LANGUAGE", + "severity": "critical", + "message": "The manuscript uses causal language, but the stated design does not independently support causal inference.", + "evidence": { + "design": "cross_sectional", + "claim": "Daily tutoring app usage improves mathematics scores." + }, + "remediation": "Replace causal verbs with associational language or add a causal identification strategy with assumptions and diagnostics." + }, + { + "code": "MISSING_COMPARATOR", + "severity": "major", + "message": "Causal review requires a comparator, control, counterfactual, or explicit quasi-experimental contrast.", + "evidence": { + "hasComparator": false + }, + "remediation": "Add a comparator/control description and explain why it approximates the counterfactual." + }, + { + "code": "TEMPORAL_ORDER_GAP", + "severity": "major", + "message": "The evidence packet does not show that exposure or intervention preceded the outcome.", + "evidence": { + "temporalOrder": false + }, + "remediation": "Document timing of exposure, intervention, measurement, and outcome windows." + }, + { + "code": "CONFOUNDER_STRATEGY_MISSING", + "severity": "major", + "message": "No confounder strategy is described for a causal claim.", + "evidence": { + "confounderStrategy": null + }, + "remediation": "Describe randomization, matching, adjustment, stratification, sensitivity analysis, or a defensible natural experiment." + }, + { + "code": "UNCERTAINTY_NOT_REPORTED", + "severity": "minor", + "message": "The packet reports an effect size without an uncertainty interval.", + "evidence": { + "effectSize": "0.31 SD", + "confidenceInterval": null + }, + "remediation": "Add confidence, credible, or bootstrap intervals and specify the estimation method." + } + ], + "reviewerNote": "education: HOLD before release because the manuscript uses causal language, but the stated design does not independently support causal inference." + }, + { + "id": "ecology-longitudinal-003", + "title": "Wetland restoration is associated with higher amphibian diversity", + "domain": "ecology", + "decision": "RELEASE", + "confidence": 0.95, + "causalLanguageDetected": false, + "findingCount": 0, + "findings": [], + "reviewerNote": "ecology: causal wording is aligned with the study design and evidence packet." + }, + { + "id": "oncology-subgroup-004", + "title": "Dose timing prevents relapse in a small oncology subgroup", + "domain": "oncology", + "decision": "REVISE", + "confidence": 0.79, + "causalLanguageDetected": true, + "findingCount": 2, + "findings": [ + { + "code": "UNCERTAINTY_NOT_REPORTED", + "severity": "minor", + "message": "The packet reports an effect size without an uncertainty interval.", + "evidence": { + "effectSize": "hazard ratio 0.72", + "confidenceInterval": null + }, + "remediation": "Add confidence, credible, or bootstrap intervals and specify the estimation method." + }, + { + "code": "UNDERPOWERED_SUBGROUP_CAUSAL_CLAIM", + "severity": "minor", + "message": "At least one subgroup claim uses causal language without a powered subgroup analysis.", + "evidence": { + "subgroups": [ + { + "name": "biomarker-positive subgroup", + "claim": "dose timing prevents relapse", + "powered": false + } + ] + }, + "remediation": "Mark subgroup findings exploratory unless power and multiplicity controls are documented." + } + ], + "reviewerNote": "oncology: REVISE before release because the packet reports an effect size without an uncertainty interval." + } + ] +} diff --git a/causal-claim-validity-guard/artifacts/causal-claim-review.md b/causal-claim-validity-guard/artifacts/causal-claim-review.md new file mode 100644 index 00000000..87285146 --- /dev/null +++ b/causal-claim-validity-guard/artifacts/causal-claim-review.md @@ -0,0 +1,46 @@ +# Causal Claim Validity Review + +Generated: 2026-08-15T20:24:14.343Z + +## Decision Counts + +- RELEASE: 2 +- REVISE: 1 +- HOLD: 1 + +## Packet Reviews + +### Remote coaching reduces systolic blood pressure in high-risk adults +Decision: RELEASE +Confidence: 0.95 +Reviewer note: clinical trials: causal wording is aligned with the study design and evidence packet. + +### Daily tutoring app usage improves mathematics scores +Decision: HOLD +Confidence: 0.15 +Reviewer note: education: HOLD before release because the manuscript uses causal language, but the stated design does not independently support causal inference. +- CRITICAL DESIGN_CANNOT_SUPPORT_CAUSAL_LANGUAGE: The manuscript uses causal language, but the stated design does not independently support causal inference. + Remediation: Replace causal verbs with associational language or add a causal identification strategy with assumptions and diagnostics. +- MAJOR MISSING_COMPARATOR: Causal review requires a comparator, control, counterfactual, or explicit quasi-experimental contrast. + Remediation: Add a comparator/control description and explain why it approximates the counterfactual. +- MAJOR TEMPORAL_ORDER_GAP: The evidence packet does not show that exposure or intervention preceded the outcome. + Remediation: Document timing of exposure, intervention, measurement, and outcome windows. +- MAJOR CONFOUNDER_STRATEGY_MISSING: No confounder strategy is described for a causal claim. + Remediation: Describe randomization, matching, adjustment, stratification, sensitivity analysis, or a defensible natural experiment. +- MINOR UNCERTAINTY_NOT_REPORTED: The packet reports an effect size without an uncertainty interval. + Remediation: Add confidence, credible, or bootstrap intervals and specify the estimation method. + +### Wetland restoration is associated with higher amphibian diversity +Decision: RELEASE +Confidence: 0.95 +Reviewer note: ecology: causal wording is aligned with the study design and evidence packet. + +### Dose timing prevents relapse in a small oncology subgroup +Decision: REVISE +Confidence: 0.79 +Reviewer note: oncology: REVISE before release because the packet reports an effect size without an uncertainty interval. +- MINOR UNCERTAINTY_NOT_REPORTED: The packet reports an effect size without an uncertainty interval. + Remediation: Add confidence, credible, or bootstrap intervals and specify the estimation method. +- MINOR UNDERPOWERED_SUBGROUP_CAUSAL_CLAIM: At least one subgroup claim uses causal language without a powered subgroup analysis. + Remediation: Mark subgroup findings exploratory unless power and multiplicity controls are documented. + diff --git a/causal-claim-validity-guard/artifacts/causal-claim-summary.svg b/causal-claim-validity-guard/artifacts/causal-claim-summary.svg new file mode 100644 index 00000000..3ea5aa3e --- /dev/null +++ b/causal-claim-validity-guard/artifacts/causal-claim-summary.svg @@ -0,0 +1,14 @@ + + + Causal Claim Validity Guard + AI peer-review gate for causal language, design support, comparator evidence, and uncertainty. + RELEASE: 2 + + + REVISE: 1 + + + HOLD: 1 + + + \ No newline at end of file diff --git a/causal-claim-validity-guard/artifacts/demo.mp4 b/causal-claim-validity-guard/artifacts/demo.mp4 new file mode 100644 index 00000000..3e19b58d Binary files /dev/null and b/causal-claim-validity-guard/artifacts/demo.mp4 differ diff --git a/causal-claim-validity-guard/data/sample_review_packets.json b/causal-claim-validity-guard/data/sample_review_packets.json new file mode 100644 index 00000000..d19271ca --- /dev/null +++ b/causal-claim-validity-guard/data/sample_review_packets.json @@ -0,0 +1,85 @@ +[ + { + "id": "cardio-rct-001", + "title": "Remote coaching reduces systolic blood pressure in high-risk adults", + "domain": "clinical trials", + "claim": { + "intent": "causal", + "text": "Remote coaching reduces systolic blood pressure after twelve weeks." + }, + "evidence": { + "design": "randomized_controlled_trial", + "hasComparator": true, + "temporalOrder": true, + "confounderStrategy": "blocked randomization with prespecified covariate balance checks", + "effectSize": "-7.4 mmHg", + "confidenceInterval": "95% CI -10.1 to -4.9", + "subgroups": [ + { + "name": "participants over 65", + "claim": "coaching may reduce systolic blood pressure", + "powered": true + } + ] + }, + "conclusion": "Remote coaching reduces systolic blood pressure in the randomized study population." + }, + { + "id": "education-cross-sectional-002", + "title": "Daily tutoring app usage improves mathematics scores", + "domain": "education", + "claim": { + "intent": "causal", + "text": "Daily tutoring app usage improves mathematics scores." + }, + "evidence": { + "design": "cross_sectional", + "hasComparator": false, + "temporalOrder": false, + "effectSize": "0.31 SD" + }, + "conclusion": "The tutoring app drives higher scores across schools." + }, + { + "id": "ecology-longitudinal-003", + "title": "Wetland restoration is associated with higher amphibian diversity", + "domain": "ecology", + "claim": { + "intent": "associational", + "text": "Wetland restoration is associated with higher amphibian diversity." + }, + "evidence": { + "design": "longitudinal_observational", + "hasComparator": true, + "temporalOrder": true, + "confounderStrategy": "matched watersheds and rainfall adjustment", + "effectSize": "18% higher species richness", + "confidenceInterval": "95% CI 6% to 29%" + }, + "conclusion": "Wetland restoration is associated with higher observed amphibian diversity after adjustment." + }, + { + "id": "oncology-subgroup-004", + "title": "Dose timing prevents relapse in a small oncology subgroup", + "domain": "oncology", + "claim": { + "intent": "causal", + "text": "Dose timing prevents relapse in the biomarker-positive subgroup." + }, + "evidence": { + "design": "quasi_experimental", + "hasComparator": true, + "temporalOrder": true, + "confounderStrategy": "propensity score weighting", + "effectSize": "hazard ratio 0.72", + "subgroups": [ + { + "name": "biomarker-positive subgroup", + "claim": "dose timing prevents relapse", + "powered": false + } + ] + }, + "conclusion": "Dose timing prevents relapse for biomarker-positive patients." + } +] diff --git a/causal-claim-validity-guard/package.json b/causal-claim-validity-guard/package.json new file mode 100644 index 00000000..90b5945f --- /dev/null +++ b/causal-claim-validity-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "causal-claim-validity-guard", + "version": "1.0.0", + "description": "Dependency-free causal claim validity guard for AI research assistant review packets.", + "main": "src/index.js", + "scripts": { + "test": "node --test", + "demo": "node scripts/demo.js", + "video": "node scripts/render-demo-video.js", + "check": "node --check src/index.js && node --check scripts/demo.js" + }, + "license": "MIT" +} diff --git a/causal-claim-validity-guard/scripts/demo.js b/causal-claim-validity-guard/scripts/demo.js new file mode 100644 index 00000000..afedd27e --- /dev/null +++ b/causal-claim-validity-guard/scripts/demo.js @@ -0,0 +1,77 @@ +"use strict"; + +const fs = require("node:fs"); +const path = require("node:path"); +const { analyzePackets } = require("../src"); + +const root = path.resolve(__dirname, ".."); +const packets = JSON.parse(fs.readFileSync(path.join(root, "data", "sample_review_packets.json"), "utf8")); +const report = analyzePackets(packets); +const outDir = path.join(root, "artifacts"); + +fs.mkdirSync(outDir, { recursive: true }); +fs.writeFileSync(path.join(outDir, "causal-claim-review.json"), `${JSON.stringify(report, null, 2)}\n`); +fs.writeFileSync(path.join(outDir, "causal-claim-review.md"), renderMarkdown(report)); +fs.writeFileSync(path.join(outDir, "causal-claim-summary.svg"), renderSvg(report)); + +console.log(JSON.stringify({ + packetCount: report.packetCount, + decisionCounts: report.decisionCounts, + artifacts: [ + "artifacts/causal-claim-review.json", + "artifacts/causal-claim-review.md", + "artifacts/causal-claim-summary.svg" + ] +}, null, 2)); + +function renderMarkdown(report) { + const lines = [ + "# Causal Claim Validity Review", + "", + `Generated: ${report.generatedAt}`, + "", + "## Decision Counts", + "", + `- RELEASE: ${report.decisionCounts.RELEASE}`, + `- REVISE: ${report.decisionCounts.REVISE}`, + `- HOLD: ${report.decisionCounts.HOLD}`, + "", + "## Packet Reviews", + "" + ]; + + for (const review of report.reviews) { + lines.push(`### ${review.title}`); + lines.push(`Decision: ${review.decision}`); + lines.push(`Confidence: ${review.confidence}`); + lines.push(`Reviewer note: ${review.reviewerNote}`); + for (const item of review.findings) { + lines.push(`- ${item.severity.toUpperCase()} ${item.code}: ${item.message}`); + lines.push(` Remediation: ${item.remediation}`); + } + lines.push(""); + } + + return `${lines.join("\n")}\n`; +} + +function renderSvg(report) { + const hold = report.decisionCounts.HOLD; + const revise = report.decisionCounts.REVISE; + const release = report.decisionCounts.RELEASE; + return ` + + Causal Claim Validity Guard + AI peer-review gate for causal language, design support, comparator evidence, and uncertainty. + ${bar("RELEASE", release, 48, 148, "#4fbf9f")} + ${bar("REVISE", revise, 48, 214, "#d6a137")} + ${bar("HOLD", hold, 48, 280, "#d85f73")} +`; +} + +function bar(label, count, x, y, color) { + const width = 96 + count * 110; + return `${label}: ${count} + + `; +} diff --git a/causal-claim-validity-guard/scripts/render-demo-video.js b/causal-claim-validity-guard/scripts/render-demo-video.js new file mode 100644 index 00000000..630d3393 --- /dev/null +++ b/causal-claim-validity-guard/scripts/render-demo-video.js @@ -0,0 +1,52 @@ +"use strict"; + +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const root = path.resolve(__dirname, ".."); +const ffmpeg = path.resolve( + root, + "..", + "..", + "tool_downloads", + "video_tools", + "node_modules", + "ffmpeg-static", + "ffmpeg.exe" +); +const out = path.join(root, "artifacts", "demo.mp4"); +const font = "C\\:/Windows/Fonts/arial.ttf"; + +const draw = [ + `drawtext=fontfile=${font}:text='Causal Claim Validity Guard':x=64:y=56:fontsize=46:fontcolor=black`, + `drawtext=fontfile=${font}:text='AI peer-review gate for causal overclaiming':x=64:y=122:fontsize=25:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='Synthetic packets analyzed 4':x=64:y=212:fontsize=34:fontcolor=black`, + `drawtext=fontfile=${font}:text='RELEASE 2 REVISE 1 HOLD 1':x=64:y=270:fontsize=38:fontcolor=0x245fd6`, + `drawtext=fontfile=${font}:text='Checks design support comparator evidence temporal order confounders uncertainty':x=64:y=360:fontsize=26:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='Validation passed npm test plus demo plus syntax check':x=64:y=424:fontsize=26:fontcolor=0x34413b`, + `drawtext=fontfile=${font}:text='No external APIs credentials private manuscripts or live data':x=64:y=488:fontsize=26:fontcolor=0x34413b` +].join(","); + +const result = spawnSync(ffmpeg, [ + "-y", + "-f", + "lavfi", + "-i", + "color=c=0xf8faf7:s=1280x720:d=8:r=30", + "-vf", + draw, + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + out +], { encoding: "utf8" }); + +if (result.status !== 0) { + process.stderr.write(result.stderr || result.stdout); + process.exit(result.status || 1); +} + +console.log(JSON.stringify({ out, bytes: require("node:fs").statSync(out).size }, null, 2)); diff --git a/causal-claim-validity-guard/src/index.js b/causal-claim-validity-guard/src/index.js new file mode 100644 index 00000000..6bba8742 --- /dev/null +++ b/causal-claim-validity-guard/src/index.js @@ -0,0 +1,195 @@ +"use strict"; + +const CAUSAL_TERMS = [ + "cause", + "causes", + "caused", + "causal", + "effect of", + "leads to", + "resulted in", + "prevents", + "reduces", + "increases", + "drives", + "improves", + "protects against" +]; + +const CAUSAL_DESIGNS = new Set([ + "randomized_controlled_trial", + "quasi_experimental", + "instrumental_variable", + "regression_discontinuity", + "difference_in_differences", + "natural_experiment" +]); + +function includesCausalLanguage(value) { + const text = String(value || "").toLowerCase(); + return CAUSAL_TERMS.some((term) => text.includes(term)); +} + +function finding(code, severity, message, evidence, remediation) { + return { code, severity, message, evidence, remediation }; +} + +function severityWeight(severity) { + if (severity === "critical") return 5; + if (severity === "major") return 3; + return 1; +} + +function decisionFromFindings(findings) { + const score = findings.reduce((total, item) => total + severityWeight(item.severity), 0); + const critical = findings.some((item) => item.severity === "critical"); + const major = findings.filter((item) => item.severity === "major").length; + if (critical || major >= 2 || score >= 7) return "HOLD"; + if (major >= 1 || score >= 2) return "REVISE"; + return "RELEASE"; +} + +function confidenceFromFindings(findings) { + const penalty = findings.reduce((total, item) => total + severityWeight(item.severity), 0); + return Math.max(0.15, Number((0.95 - penalty * 0.08).toFixed(2))); +} + +function normalizePacket(packet) { + return { + id: packet.id, + title: packet.title, + domain: packet.domain || "general science", + claim: packet.claim || {}, + evidence: packet.evidence || {}, + conclusion: packet.conclusion || "", + reviewerContext: packet.reviewerContext || {} + }; +} + +function analyzePacket(input) { + const packet = normalizePacket(input); + const claimText = packet.claim.text || ""; + const conclusion = packet.conclusion || ""; + const evidence = packet.evidence; + const design = evidence.design || "unspecified"; + const causalIntent = packet.claim.intent === "causal" || includesCausalLanguage(claimText); + const conclusionCausal = includesCausalLanguage(conclusion); + const findings = []; + + if (causalIntent && !CAUSAL_DESIGNS.has(design)) { + findings.push(finding( + "DESIGN_CANNOT_SUPPORT_CAUSAL_LANGUAGE", + design === "cross_sectional" || design === "case_series" ? "critical" : "major", + "The manuscript uses causal language, but the stated design does not independently support causal inference.", + { design, claim: claimText }, + "Replace causal verbs with associational language or add a causal identification strategy with assumptions and diagnostics." + )); + } + + if ((causalIntent || conclusionCausal) && evidence.hasComparator !== true) { + findings.push(finding( + "MISSING_COMPARATOR", + "major", + "Causal review requires a comparator, control, counterfactual, or explicit quasi-experimental contrast.", + { hasComparator: evidence.hasComparator }, + "Add a comparator/control description and explain why it approximates the counterfactual." + )); + } + + if ((causalIntent || conclusionCausal) && evidence.temporalOrder !== true) { + findings.push(finding( + "TEMPORAL_ORDER_GAP", + "major", + "The evidence packet does not show that exposure or intervention preceded the outcome.", + { temporalOrder: evidence.temporalOrder }, + "Document timing of exposure, intervention, measurement, and outcome windows." + )); + } + + if ((causalIntent || conclusionCausal) && !evidence.confounderStrategy) { + findings.push(finding( + "CONFOUNDER_STRATEGY_MISSING", + "major", + "No confounder strategy is described for a causal claim.", + { confounderStrategy: evidence.confounderStrategy || null }, + "Describe randomization, matching, adjustment, stratification, sensitivity analysis, or a defensible natural experiment." + )); + } + + if ((causalIntent || conclusionCausal) && evidence.effectSize && !evidence.confidenceInterval) { + findings.push(finding( + "UNCERTAINTY_NOT_REPORTED", + "minor", + "The packet reports an effect size without an uncertainty interval.", + { effectSize: evidence.effectSize, confidenceInterval: evidence.confidenceInterval || null }, + "Add confidence, credible, or bootstrap intervals and specify the estimation method." + )); + } + + if (packet.claim.intent === "associational" && conclusionCausal) { + findings.push(finding( + "CONCLUSION_OVERSTATES_ASSOCIATION", + "major", + "The claim is labeled associational but the conclusion uses causal wording.", + { intent: packet.claim.intent, conclusion }, + "Align the conclusion with the claim intent or provide a causal design and evidence upgrade." + )); + } + + if (Array.isArray(evidence.subgroups) && evidence.subgroups.some((group) => group.powered !== true && includesCausalLanguage(group.claim))) { + findings.push(finding( + "UNDERPOWERED_SUBGROUP_CAUSAL_CLAIM", + "minor", + "At least one subgroup claim uses causal language without a powered subgroup analysis.", + { subgroups: evidence.subgroups }, + "Mark subgroup findings exploratory unless power and multiplicity controls are documented." + )); + } + + const decision = decisionFromFindings(findings); + return { + id: packet.id, + title: packet.title, + domain: packet.domain, + decision, + confidence: confidenceFromFindings(findings), + causalLanguageDetected: causalIntent || conclusionCausal, + findingCount: findings.length, + findings, + reviewerNote: reviewerNote(packet, decision, findings) + }; +} + +function reviewerNote(packet, decision, findings) { + if (decision === "RELEASE") { + return `${packet.domain}: causal wording is aligned with the study design and evidence packet.`; + } + const first = findings[0]; + return `${packet.domain}: ${decision} before release because ${first.message.toLowerCase()}`; +} + +function analyzePackets(packets) { + const reviews = packets.map(analyzePacket); + const counts = reviews.reduce((acc, review) => { + acc[review.decision] = (acc[review.decision] || 0) + 1; + return acc; + }, {}); + return { + generatedAt: new Date().toISOString(), + packetCount: reviews.length, + decisionCounts: { + RELEASE: counts.RELEASE || 0, + REVISE: counts.REVISE || 0, + HOLD: counts.HOLD || 0 + }, + reviews + }; +} + +module.exports = { + CAUSAL_TERMS, + CAUSAL_DESIGNS, + analyzePacket, + analyzePackets, + includesCausalLanguage +}; diff --git a/causal-claim-validity-guard/test/causal-claim-validity-guard.test.js b/causal-claim-validity-guard/test/causal-claim-validity-guard.test.js new file mode 100644 index 00000000..012e5f89 --- /dev/null +++ b/causal-claim-validity-guard/test/causal-claim-validity-guard.test.js @@ -0,0 +1,80 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const test = require("node:test"); +const { analyzePacket, analyzePackets, includesCausalLanguage } = require("../src"); + +test("detects causal language in common manuscript wording", () => { + assert.equal(includesCausalLanguage("The intervention reduces risk."), true); + assert.equal(includesCausalLanguage("The marker is associated with risk."), false); +}); + +test("holds causal claims from cross-sectional evidence without comparator or temporal order", () => { + const review = analyzePacket({ + id: "bad-causal", + title: "App usage improves outcomes", + claim: { + intent: "causal", + text: "The app improves outcomes." + }, + evidence: { + design: "cross_sectional", + hasComparator: false, + temporalOrder: false, + effectSize: "0.4 SD" + }, + conclusion: "The app causes better outcomes." + }); + + assert.equal(review.decision, "HOLD"); + assert.ok(review.findings.some((item) => item.code === "DESIGN_CANNOT_SUPPORT_CAUSAL_LANGUAGE")); + assert.ok(review.findings.some((item) => item.code === "MISSING_COMPARATOR")); + assert.ok(review.findings.some((item) => item.code === "TEMPORAL_ORDER_GAP")); +}); + +test("releases supported randomized causal claims", () => { + const review = analyzePacket({ + id: "good-rct", + title: "Coaching reduces blood pressure", + domain: "clinical trials", + claim: { + intent: "causal", + text: "Coaching reduces blood pressure." + }, + evidence: { + design: "randomized_controlled_trial", + hasComparator: true, + temporalOrder: true, + confounderStrategy: "blocked randomization", + effectSize: "-7 mmHg", + confidenceInterval: "95% CI -10 to -4" + }, + conclusion: "Coaching reduces blood pressure in this randomized trial." + }); + + assert.equal(review.decision, "RELEASE"); + assert.equal(review.findingCount, 0); +}); + +test("summarizes packet decisions deterministically", () => { + const report = analyzePackets([ + { + id: "a", + title: "A", + claim: { intent: "associational", text: "A is associated with B." }, + evidence: { design: "cross_sectional" }, + conclusion: "A is associated with B." + }, + { + id: "b", + title: "B", + claim: { intent: "causal", text: "A prevents B." }, + evidence: { design: "case_series", hasComparator: false, temporalOrder: false }, + conclusion: "A prevents B." + } + ]); + + assert.equal(report.packetCount, 2); + assert.equal(report.decisionCounts.RELEASE, 1); + assert.equal(report.decisionCounts.HOLD, 1); +});