From 2c2533d8c28fa2899a3449e87521c4bea6d672ef Mon Sep 17 00:00:00 2001 From: taherd <183945978+taherdhanera@users.noreply.github.com> Date: Fri, 22 May 2026 15:13:20 +0530 Subject: [PATCH 1/2] Add external validity transfer assistant --- .../README.md | 38 ++ external-validity-transfer-assistant/demo.js | 22 ++ external-validity-transfer-assistant/index.js | 360 ++++++++++++++++++ .../package.json | 13 + .../reports/reviewer-packet.md | 66 ++++ .../reports/summary.json | 263 +++++++++++++ .../reports/summary.svg | 21 + .../requirements-map.md | 29 ++ .../sample-data.js | 149 ++++++++ external-validity-transfer-assistant/test.js | 57 +++ 10 files changed, 1018 insertions(+) create mode 100644 external-validity-transfer-assistant/README.md create mode 100644 external-validity-transfer-assistant/demo.js create mode 100644 external-validity-transfer-assistant/index.js create mode 100644 external-validity-transfer-assistant/package.json create mode 100644 external-validity-transfer-assistant/reports/reviewer-packet.md create mode 100644 external-validity-transfer-assistant/reports/summary.json create mode 100644 external-validity-transfer-assistant/reports/summary.svg create mode 100644 external-validity-transfer-assistant/requirements-map.md create mode 100644 external-validity-transfer-assistant/sample-data.js create mode 100644 external-validity-transfer-assistant/test.js diff --git a/external-validity-transfer-assistant/README.md b/external-validity-transfer-assistant/README.md new file mode 100644 index 00000000..fd799619 --- /dev/null +++ b/external-validity-transfer-assistant/README.md @@ -0,0 +1,38 @@ +# External Validity Transfer Assistant + +This module is a focused slice for SCIBASE issue #16, AI-Powered Research Assistant Suite. + +It adds a deterministic research-assistant review gate for external validity and population-transfer risk before AI-generated peer-review packets are shown to authors, reviewers, funders, or lab leads. + +## What It Checks + +- Whether broad manuscript claims are backed by evidence from the asserted populations. +- Whether claimed deployment settings are covered by linked study artifacts. +- Whether assay or instrument contexts match the manuscript language. +- Whether runtime environments have reproducible rerun evidence. +- Whether strong claims are missing required subgroup coverage. +- Whether a broad transfer claim lacks external validation. + +## Outputs + +The demo creates: + +- `reports/summary.json`: structured review packet. +- `reports/reviewer-packet.md`: reviewer-facing findings, actions, and research gaps. +- `reports/summary.svg`: visual transfer-risk summary. + +## Why This Is Distinct + +This is not another broad AI assistant, preregistration checker, retraction sentinel, prompt-safety guard, statistical review, benchmark-leakage auditor, figure/table checker, supplement-readiness module, funding/COI checker, or evidence-trace assistant. + +It focuses specifically on whether a manuscript's claims transfer beyond the exact population, setting, assay, and runtime contexts represented by the linked evidence. + +## Local Validation + +```bash +npm run check +npm test +npm run demo +``` + +The module uses synthetic data only. It makes no network calls and uses no credentials, private manuscripts, protected health information, payment data, or external APIs. diff --git a/external-validity-transfer-assistant/demo.js b/external-validity-transfer-assistant/demo.js new file mode 100644 index 00000000..b1c7c2d5 --- /dev/null +++ b/external-validity-transfer-assistant/demo.js @@ -0,0 +1,22 @@ +const fs = require("fs"); +const path = require("path"); +const { project } = require("./sample-data"); +const { buildReviewPacket, renderMarkdownReport, renderSvgSummary } = require("./index"); + +const reportDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportDir, { recursive: true }); + +const packet = buildReviewPacket(project); + +fs.writeFileSync( + path.join(reportDir, "summary.json"), + `${JSON.stringify(packet, null, 2)}\n`, + "utf8" +); +fs.writeFileSync(path.join(reportDir, "reviewer-packet.md"), renderMarkdownReport(packet), "utf8"); +fs.writeFileSync(path.join(reportDir, "summary.svg"), renderSvgSummary(packet), "utf8"); + +console.log(`Generated reports for ${packet.assistant}`); +console.log(`Decision: ${packet.decision}`); +console.log(`Average score: ${packet.averageScore}`); +console.log(`Findings: ${packet.peerReviewSuggestions.length}`); diff --git a/external-validity-transfer-assistant/index.js b/external-validity-transfer-assistant/index.js new file mode 100644 index 00000000..aba2a708 --- /dev/null +++ b/external-validity-transfer-assistant/index.js @@ -0,0 +1,360 @@ +const DEFAULT_WEIGHTS = { + critical: 35, + high: 22, + medium: 12, + low: 6 +}; + +function unique(values) { + return [...new Set((values || []).filter(Boolean))]; +} + +function toScopeSet(records, key) { + return new Set(records.map((record) => record[key]).filter(Boolean)); +} + +function missingFromScope(expected, observed) { + const observedSet = observed instanceof Set ? observed : new Set(observed); + return unique(expected).filter((value) => !observedSet.has(value)); +} + +function hasExternalValidation(records) { + return records.some((record) => record.externalValidation || record.type === "external-validation"); +} + +function hasRunnableEvidence(records) { + return records.some((record) => record.reproducible && record.environment); +} + +function severityForMissing(count, expectedCount) { + if (count === 0) { + return null; + } + if (expectedCount >= 3 && count >= 2) { + return "high"; + } + if (count === expectedCount) { + return "high"; + } + return "medium"; +} + +function addFinding(findings, severity, rule, message, action) { + findings.push({ severity, rule, message, action }); +} + +function evaluateClaim(claim, evidence, manuscript) { + const linkedEvidence = evidence.filter((record) => claim.evidenceIds.includes(record.id)); + const findings = []; + const scope = claim.assertedScope || {}; + + if (linkedEvidence.length === 0) { + addFinding( + findings, + "critical", + "missing-evidence", + `Claim ${claim.id} has no linked evidence artifacts.`, + "Link at least one dataset, runbook, protocol, or validation artifact before review." + ); + } + + const observedPopulations = toScopeSet(linkedEvidence, "population"); + const observedSettings = toScopeSet(linkedEvidence, "setting"); + const observedInstruments = toScopeSet(linkedEvidence, "instrument"); + const observedEnvironments = toScopeSet(linkedEvidence, "environment"); + + const missingPopulations = missingFromScope(scope.populations, observedPopulations); + const missingSettings = missingFromScope(scope.settings, observedSettings); + const missingInstruments = missingFromScope(scope.instruments, observedInstruments); + const missingEnvironments = missingFromScope(scope.environments, observedEnvironments); + + const populationSeverity = severityForMissing(missingPopulations.length, (scope.populations || []).length); + if (populationSeverity) { + addFinding( + findings, + populationSeverity, + "population-transfer-gap", + `Claim ${claim.id} asserts populations not represented in linked evidence: ${missingPopulations.join(", ")}.`, + "Narrow the claim wording or add external validation for each missing population." + ); + } + + const settingSeverity = severityForMissing(missingSettings.length, (scope.settings || []).length); + if (settingSeverity) { + addFinding( + findings, + settingSeverity, + "setting-transfer-gap", + `Claim ${claim.id} asserts settings not represented in linked evidence: ${missingSettings.join(", ")}.`, + "Add site-level validation evidence or mark the setting as a future research gap." + ); + } + + if (missingInstruments.length > 0) { + addFinding( + findings, + "medium", + "assay-transfer-gap", + `Claim ${claim.id} references unsupported assay or instrument contexts: ${missingInstruments.join(", ")}.`, + "Separate assay-specific claims and document conversion limits before reviewer release." + ); + } + + if (missingEnvironments.length > 0) { + addFinding( + findings, + "medium", + "runtime-transfer-gap", + `Claim ${claim.id} references runtime environments not covered by rerun evidence: ${missingEnvironments.join(", ")}.`, + "Run the pipeline in each deployment-like environment or downgrade deployment readiness language." + ); + } + + const broadScope = (scope.populations || []).length > 1 || (scope.settings || []).length > 1; + if (broadScope && !hasExternalValidation(linkedEvidence)) { + addFinding( + findings, + "high", + "no-external-validation", + `Claim ${claim.id} has broad transfer language without external validation evidence.`, + "Hold broad generalizability language until at least one independent validation artifact is linked." + ); + } + + if (!hasRunnableEvidence(linkedEvidence)) { + addFinding( + findings, + "high", + "no-runnable-transfer-evidence", + `Claim ${claim.id} lacks reproducible runtime evidence for the asserted context.`, + "Add a deterministic runbook, manifest, or notebook rerun before the assistant marks the claim reproducible." + ); + } + + const missingRequiredSubgroups = missingFromScope(manuscript.requiredSubgroups || [], observedPopulations); + if (claim.confidence === "strong" && missingRequiredSubgroups.length > 0) { + addFinding( + findings, + "medium", + "strong-claim-subgroup-undercoverage", + `Strong claim ${claim.id} does not cover required subgroup evidence: ${missingRequiredSubgroups.join(", ")}.`, + "Convert the claim to qualified language or create a subgroup-specific validation plan." + ); + } + + const score = Math.max( + 0, + 100 - findings.reduce((total, finding) => total + DEFAULT_WEIGHTS[finding.severity], 0) + ); + + return { + id: claim.id, + text: claim.text, + score, + decision: decisionFromScore(score), + evidenceCount: linkedEvidence.length, + observedScope: { + populations: [...observedPopulations], + settings: [...observedSettings], + instruments: [...observedInstruments], + environments: [...observedEnvironments], + externalValidation: hasExternalValidation(linkedEvidence), + runnableEvidence: hasRunnableEvidence(linkedEvidence) + }, + findings + }; +} + +function decisionFromScore(score) { + if (score >= 82) { + return "review-ready"; + } + if (score >= 62) { + return "revise-before-release"; + } + if (score >= 42) { + return "hold-for-transfer-evidence"; + } + return "quarantine-from-review-packet"; +} + +function summarizeSeverity(claimReviews) { + const summary = { critical: 0, high: 0, medium: 0, low: 0 }; + for (const review of claimReviews) { + for (const finding of review.findings) { + summary[finding.severity] += 1; + } + } + return summary; +} + +function createResearchGaps(project, claimReviews) { + const missingTerms = new Set(); + for (const review of claimReviews) { + for (const finding of review.findings) { + if (finding.rule.includes("population") || finding.rule.includes("subgroup")) { + for (const value of review.text.match(/pediatric|underrepresented ancestry|adult/g) || []) { + missingTerms.add(value); + } + } + if (finding.rule.includes("setting") || finding.rule.includes("runtime")) { + for (const value of review.text.match(/low-resource clinic|community hospital|international site|cpu-only/g) || []) { + missingTerms.add(value); + } + } + } + } + + const generated = project.corpusSignals + .filter((signal) => signal.labFit.some((capability) => project.labCapabilities.includes(capability))) + .map((signal) => ({ + id: signal.id, + topic: signal.topic, + reason: signal.reason, + priority: missingTerms.size > 0 ? "high" : "medium", + suggestedNextStep: `Use ${signal.topic} as a targeted validation or grant-planning workstream.` + })); + + return generated.slice(0, 5); +} + +function createReproducibilityActions(claimReviews) { + const actions = []; + for (const review of claimReviews) { + for (const finding of review.findings) { + if (finding.rule.includes("runtime") || finding.rule.includes("runnable") || finding.rule.includes("external")) { + actions.push({ + claimId: review.id, + priority: finding.severity === "high" || finding.severity === "critical" ? "blocking" : "recommended", + action: finding.action + }); + } + } + } + return actions; +} + +function buildReviewPacket(project) { + const claimReviews = project.manuscript.claims.map((claim) => + evaluateClaim(claim, project.evidence, project.manuscript) + ); + const severitySummary = summarizeSeverity(claimReviews); + const averageScore = Math.round( + claimReviews.reduce((total, review) => total + review.score, 0) / claimReviews.length + ); + + return { + projectId: project.id, + title: project.title, + assistant: "external-validity-transfer-assistant", + issue: "SCIBASE-AI/SCIBASE.AI#16", + averageScore, + decision: decisionFromScore(averageScore), + severitySummary, + claimReviews, + peerReviewSuggestions: claimReviews.flatMap((review) => + review.findings.map((finding) => ({ + claimId: review.id, + severity: finding.severity, + suggestion: finding.message, + action: finding.action + })) + ), + reproducibilityActions: createReproducibilityActions(claimReviews), + researchGaps: createResearchGaps(project, claimReviews), + safetyNotes: [ + "Synthetic data only.", + "No external APIs, credentials, private manuscripts, or live clinical data are used.", + "The assistant produces deterministic review packets suitable for pre-submission review." + ] + }; +} + +function renderMarkdownReport(packet) { + const lines = [ + `# ${packet.title}`, + "", + `Assistant: ${packet.assistant}`, + `Overall decision: ${packet.decision}`, + `Average transfer score: ${packet.averageScore}`, + "", + "## Severity Summary", + "", + `- Critical: ${packet.severitySummary.critical}`, + `- High: ${packet.severitySummary.high}`, + `- Medium: ${packet.severitySummary.medium}`, + `- Low: ${packet.severitySummary.low}`, + "", + "## Claim Reviews", + "" + ]; + + for (const review of packet.claimReviews) { + lines.push(`### ${review.id}`); + lines.push(""); + lines.push(`Decision: ${review.decision}`); + lines.push(`Score: ${review.score}`); + lines.push(`Evidence artifacts: ${review.evidenceCount}`); + if (review.findings.length === 0) { + lines.push("- No transfer-risk findings."); + } else { + for (const finding of review.findings) { + lines.push(`- ${finding.severity.toUpperCase()} ${finding.rule}: ${finding.message}`); + lines.push(` Action: ${finding.action}`); + } + } + lines.push(""); + } + + lines.push("## Reproducibility Actions"); + lines.push(""); + for (const action of packet.reproducibilityActions) { + lines.push(`- ${action.priority}: ${action.claimId} - ${action.action}`); + } + + lines.push(""); + lines.push("## Research Gap Prompts"); + lines.push(""); + for (const gap of packet.researchGaps) { + lines.push(`- ${gap.priority}: ${gap.topic} - ${gap.reason}`); + } + + return `${lines.join("\n")}\n`; +} + +function renderSvgSummary(packet) { + const barWidth = Math.max(10, packet.averageScore * 4); + const statusColor = packet.averageScore >= 62 ? "#2563eb" : "#b91c1c"; + const rows = packet.claimReviews + .map((review, index) => { + const y = 130 + index * 52; + const width = Math.max(10, review.score * 4); + return [ + `${review.id}`, + ``, + ``, + `${review.score} - ${review.decision}` + ].join("\n"); + }) + .join("\n"); + + return [ + ``, + ``, + `External Validity Transfer Assistant`, + `Average transfer score`, + ``, + ``, + `${packet.averageScore} - ${packet.decision}`, + rows, + `Synthetic deterministic demo. No credentials, private manuscripts, or external APIs.`, + `` + ].join("\n"); +} + +module.exports = { + buildReviewPacket, + evaluateClaim, + renderMarkdownReport, + renderSvgSummary +}; diff --git a/external-validity-transfer-assistant/package.json b/external-validity-transfer-assistant/package.json new file mode 100644 index 00000000..5c1126cb --- /dev/null +++ b/external-validity-transfer-assistant/package.json @@ -0,0 +1,13 @@ +{ + "name": "external-validity-transfer-assistant", + "version": "1.0.0", + "description": "Deterministic AI research assistant slice for external validity and population transfer review.", + "main": "index.js", + "private": true, + "type": "commonjs", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js", + "test": "node test.js", + "demo": "node demo.js" + } +} diff --git a/external-validity-transfer-assistant/reports/reviewer-packet.md b/external-validity-transfer-assistant/reports/reviewer-packet.md new file mode 100644 index 00000000..595d6676 --- /dev/null +++ b/external-validity-transfer-assistant/reports/reviewer-packet.md @@ -0,0 +1,66 @@ +# Portable biomarker triage assistant for multi-site oncology cohorts + +Assistant: external-validity-transfer-assistant +Overall decision: hold-for-transfer-evidence +Average transfer score: 44 + +## Severity Summary + +- Critical: 0 +- High: 5 +- Medium: 5 +- Low: 0 + +## Claim Reviews + +### claim-generalizable-oncology + +Decision: quarantine-from-review-packet +Score: 0 +Evidence artifacts: 2 +- HIGH population-transfer-gap: Claim claim-generalizable-oncology asserts populations not represented in linked evidence: pediatric, underrepresented ancestry. + Action: Narrow the claim wording or add external validation for each missing population. +- HIGH setting-transfer-gap: Claim claim-generalizable-oncology asserts settings not represented in linked evidence: community hospital, international site. + Action: Add site-level validation evidence or mark the setting as a future research gap. +- MEDIUM assay-transfer-gap: Claim claim-generalizable-oncology references unsupported assay or instrument contexts: single-cell-rna-seq. + Action: Separate assay-specific claims and document conversion limits before reviewer release. +- MEDIUM runtime-transfer-gap: Claim claim-generalizable-oncology references runtime environments not covered by rerun evidence: cuda, cpu-only. + Action: Run the pipeline in each deployment-like environment or downgrade deployment readiness language. +- HIGH no-external-validation: Claim claim-generalizable-oncology has broad transfer language without external validation evidence. + Action: Hold broad generalizability language until at least one independent validation artifact is linked. +- MEDIUM strong-claim-subgroup-undercoverage: Strong claim claim-generalizable-oncology does not cover required subgroup evidence: pediatric, underrepresented ancestry. + Action: Convert the claim to qualified language or create a subgroup-specific validation plan. + +### claim-reproducible-pipeline + +Decision: review-ready +Score: 100 +Evidence artifacts: 3 +- No transfer-risk findings. + +### claim-deployment-ready + +Decision: quarantine-from-review-packet +Score: 32 +Evidence artifacts: 1 +- MEDIUM setting-transfer-gap: Claim claim-deployment-ready asserts settings not represented in linked evidence: low-resource clinic. + Action: Add site-level validation evidence or mark the setting as a future research gap. +- HIGH no-external-validation: Claim claim-deployment-ready has broad transfer language without external validation evidence. + Action: Hold broad generalizability language until at least one independent validation artifact is linked. +- HIGH no-runnable-transfer-evidence: Claim claim-deployment-ready lacks reproducible runtime evidence for the asserted context. + Action: Add a deterministic runbook, manifest, or notebook rerun before the assistant marks the claim reproducible. +- MEDIUM strong-claim-subgroup-undercoverage: Strong claim claim-deployment-ready does not cover required subgroup evidence: pediatric, underrepresented ancestry. + Action: Convert the claim to qualified language or create a subgroup-specific validation plan. + +## Reproducibility Actions + +- recommended: claim-generalizable-oncology - Run the pipeline in each deployment-like environment or downgrade deployment readiness language. +- blocking: claim-generalizable-oncology - Hold broad generalizability language until at least one independent validation artifact is linked. +- blocking: claim-deployment-ready - Hold broad generalizability language until at least one independent validation artifact is linked. +- blocking: claim-deployment-ready - Add a deterministic runbook, manifest, or notebook rerun before the assistant marks the claim reproducible. + +## Research Gap Prompts + +- high: pediatric oncology RNA-seq validation - frequently cited limitation with low replication coverage +- high: CPU-only low-resource clinic reproducibility run - deployment claim depends on clinic-like runtime evidence +- high: ancestry-balanced external validation - underrepresented ancestry is asserted but not evidenced diff --git a/external-validity-transfer-assistant/reports/summary.json b/external-validity-transfer-assistant/reports/summary.json new file mode 100644 index 00000000..ccc8f2c5 --- /dev/null +++ b/external-validity-transfer-assistant/reports/summary.json @@ -0,0 +1,263 @@ +{ + "projectId": "project-transfer-001", + "title": "Portable biomarker triage assistant for multi-site oncology cohorts", + "assistant": "external-validity-transfer-assistant", + "issue": "SCIBASE-AI/SCIBASE.AI#16", + "averageScore": 44, + "decision": "hold-for-transfer-evidence", + "severitySummary": { + "critical": 0, + "high": 5, + "medium": 5, + "low": 0 + }, + "claimReviews": [ + { + "id": "claim-generalizable-oncology", + "text": "The model generalizes across oncology patient populations and hospital settings.", + "score": 0, + "decision": "quarantine-from-review-packet", + "evidenceCount": 2, + "observedScope": { + "populations": [ + "adult" + ], + "settings": [ + "academic hospital" + ], + "instruments": [ + "bulk-rna-seq" + ], + "environments": [ + "python-3.11" + ], + "externalValidation": false, + "runnableEvidence": true + }, + "findings": [ + { + "severity": "high", + "rule": "population-transfer-gap", + "message": "Claim claim-generalizable-oncology asserts populations not represented in linked evidence: pediatric, underrepresented ancestry.", + "action": "Narrow the claim wording or add external validation for each missing population." + }, + { + "severity": "high", + "rule": "setting-transfer-gap", + "message": "Claim claim-generalizable-oncology asserts settings not represented in linked evidence: community hospital, international site.", + "action": "Add site-level validation evidence or mark the setting as a future research gap." + }, + { + "severity": "medium", + "rule": "assay-transfer-gap", + "message": "Claim claim-generalizable-oncology references unsupported assay or instrument contexts: single-cell-rna-seq.", + "action": "Separate assay-specific claims and document conversion limits before reviewer release." + }, + { + "severity": "medium", + "rule": "runtime-transfer-gap", + "message": "Claim claim-generalizable-oncology references runtime environments not covered by rerun evidence: cuda, cpu-only.", + "action": "Run the pipeline in each deployment-like environment or downgrade deployment readiness language." + }, + { + "severity": "high", + "rule": "no-external-validation", + "message": "Claim claim-generalizable-oncology has broad transfer language without external validation evidence.", + "action": "Hold broad generalizability language until at least one independent validation artifact is linked." + }, + { + "severity": "medium", + "rule": "strong-claim-subgroup-undercoverage", + "message": "Strong claim claim-generalizable-oncology does not cover required subgroup evidence: pediatric, underrepresented ancestry.", + "action": "Convert the claim to qualified language or create a subgroup-specific validation plan." + } + ] + }, + { + "id": "claim-reproducible-pipeline", + "text": "The manuscript includes enough artifacts for an independent lab to rerun the triage pipeline.", + "score": 100, + "decision": "review-ready", + "evidenceCount": 3, + "observedScope": { + "populations": [ + "adult" + ], + "settings": [ + "academic hospital", + "community hospital" + ], + "instruments": [ + "bulk-rna-seq" + ], + "environments": [ + "python-3.11" + ], + "externalValidation": true, + "runnableEvidence": true + }, + "findings": [] + }, + { + "id": "claim-deployment-ready", + "text": "The assistant is ready for deployment guidance in low-resource clinics.", + "score": 32, + "decision": "quarantine-from-review-packet", + "evidenceCount": 1, + "observedScope": { + "populations": [ + "adult" + ], + "settings": [ + "community hospital" + ], + "instruments": [ + "bulk-rna-seq" + ], + "environments": [ + "cpu-only" + ], + "externalValidation": false, + "runnableEvidence": false + }, + "findings": [ + { + "severity": "medium", + "rule": "setting-transfer-gap", + "message": "Claim claim-deployment-ready asserts settings not represented in linked evidence: low-resource clinic.", + "action": "Add site-level validation evidence or mark the setting as a future research gap." + }, + { + "severity": "high", + "rule": "no-external-validation", + "message": "Claim claim-deployment-ready has broad transfer language without external validation evidence.", + "action": "Hold broad generalizability language until at least one independent validation artifact is linked." + }, + { + "severity": "high", + "rule": "no-runnable-transfer-evidence", + "message": "Claim claim-deployment-ready lacks reproducible runtime evidence for the asserted context.", + "action": "Add a deterministic runbook, manifest, or notebook rerun before the assistant marks the claim reproducible." + }, + { + "severity": "medium", + "rule": "strong-claim-subgroup-undercoverage", + "message": "Strong claim claim-deployment-ready does not cover required subgroup evidence: pediatric, underrepresented ancestry.", + "action": "Convert the claim to qualified language or create a subgroup-specific validation plan." + } + ] + } + ], + "peerReviewSuggestions": [ + { + "claimId": "claim-generalizable-oncology", + "severity": "high", + "suggestion": "Claim claim-generalizable-oncology asserts populations not represented in linked evidence: pediatric, underrepresented ancestry.", + "action": "Narrow the claim wording or add external validation for each missing population." + }, + { + "claimId": "claim-generalizable-oncology", + "severity": "high", + "suggestion": "Claim claim-generalizable-oncology asserts settings not represented in linked evidence: community hospital, international site.", + "action": "Add site-level validation evidence or mark the setting as a future research gap." + }, + { + "claimId": "claim-generalizable-oncology", + "severity": "medium", + "suggestion": "Claim claim-generalizable-oncology references unsupported assay or instrument contexts: single-cell-rna-seq.", + "action": "Separate assay-specific claims and document conversion limits before reviewer release." + }, + { + "claimId": "claim-generalizable-oncology", + "severity": "medium", + "suggestion": "Claim claim-generalizable-oncology references runtime environments not covered by rerun evidence: cuda, cpu-only.", + "action": "Run the pipeline in each deployment-like environment or downgrade deployment readiness language." + }, + { + "claimId": "claim-generalizable-oncology", + "severity": "high", + "suggestion": "Claim claim-generalizable-oncology has broad transfer language without external validation evidence.", + "action": "Hold broad generalizability language until at least one independent validation artifact is linked." + }, + { + "claimId": "claim-generalizable-oncology", + "severity": "medium", + "suggestion": "Strong claim claim-generalizable-oncology does not cover required subgroup evidence: pediatric, underrepresented ancestry.", + "action": "Convert the claim to qualified language or create a subgroup-specific validation plan." + }, + { + "claimId": "claim-deployment-ready", + "severity": "medium", + "suggestion": "Claim claim-deployment-ready asserts settings not represented in linked evidence: low-resource clinic.", + "action": "Add site-level validation evidence or mark the setting as a future research gap." + }, + { + "claimId": "claim-deployment-ready", + "severity": "high", + "suggestion": "Claim claim-deployment-ready has broad transfer language without external validation evidence.", + "action": "Hold broad generalizability language until at least one independent validation artifact is linked." + }, + { + "claimId": "claim-deployment-ready", + "severity": "high", + "suggestion": "Claim claim-deployment-ready lacks reproducible runtime evidence for the asserted context.", + "action": "Add a deterministic runbook, manifest, or notebook rerun before the assistant marks the claim reproducible." + }, + { + "claimId": "claim-deployment-ready", + "severity": "medium", + "suggestion": "Strong claim claim-deployment-ready does not cover required subgroup evidence: pediatric, underrepresented ancestry.", + "action": "Convert the claim to qualified language or create a subgroup-specific validation plan." + } + ], + "reproducibilityActions": [ + { + "claimId": "claim-generalizable-oncology", + "priority": "recommended", + "action": "Run the pipeline in each deployment-like environment or downgrade deployment readiness language." + }, + { + "claimId": "claim-generalizable-oncology", + "priority": "blocking", + "action": "Hold broad generalizability language until at least one independent validation artifact is linked." + }, + { + "claimId": "claim-deployment-ready", + "priority": "blocking", + "action": "Hold broad generalizability language until at least one independent validation artifact is linked." + }, + { + "claimId": "claim-deployment-ready", + "priority": "blocking", + "action": "Add a deterministic runbook, manifest, or notebook rerun before the assistant marks the claim reproducible." + } + ], + "researchGaps": [ + { + "id": "gap-pediatric-oncology-rnaseq", + "topic": "pediatric oncology RNA-seq validation", + "reason": "frequently cited limitation with low replication coverage", + "priority": "high", + "suggestedNextStep": "Use pediatric oncology RNA-seq validation as a targeted validation or grant-planning workstream." + }, + { + "id": "gap-cpu-only-clinic-run", + "topic": "CPU-only low-resource clinic reproducibility run", + "reason": "deployment claim depends on clinic-like runtime evidence", + "priority": "high", + "suggestedNextStep": "Use CPU-only low-resource clinic reproducibility run as a targeted validation or grant-planning workstream." + }, + { + "id": "gap-ancestry-balanced-evaluation", + "topic": "ancestry-balanced external validation", + "reason": "underrepresented ancestry is asserted but not evidenced", + "priority": "high", + "suggestedNextStep": "Use ancestry-balanced external validation as a targeted validation or grant-planning workstream." + } + ], + "safetyNotes": [ + "Synthetic data only.", + "No external APIs, credentials, private manuscripts, or live clinical data are used.", + "The assistant produces deterministic review packets suitable for pre-submission review." + ] +} diff --git a/external-validity-transfer-assistant/reports/summary.svg b/external-validity-transfer-assistant/reports/summary.svg new file mode 100644 index 00000000..f07fd95f --- /dev/null +++ b/external-validity-transfer-assistant/reports/summary.svg @@ -0,0 +1,21 @@ + + +External Validity Transfer Assistant +Average transfer score + + +44 - hold-for-transfer-evidence +claim-generalizable-oncology + + +0 - quarantine-from-review-packet +claim-reproducible-pipeline + + +100 - review-ready +claim-deployment-ready + + +32 - quarantine-from-review-packet +Synthetic deterministic demo. No credentials, private manuscripts, or external APIs. + \ No newline at end of file diff --git a/external-validity-transfer-assistant/requirements-map.md b/external-validity-transfer-assistant/requirements-map.md new file mode 100644 index 00000000..8650ed6f --- /dev/null +++ b/external-validity-transfer-assistant/requirements-map.md @@ -0,0 +1,29 @@ +# Requirements Map + +## SCIBASE #16 Capability Mapping + +### Auto Peer Review Reports + +- Produces structured claim-level peer-review findings. +- Flags clarity and scope mismatch where claims overstate population, setting, assay, or runtime support. +- Emits reviewer-ready actions for claim wording, validation evidence, and transfer-risk holds. + +### Reproducibility Checker + +- Checks whether linked evidence includes reproducible runtime artifacts. +- Flags claims that depend on deployment environments without matching rerun evidence. +- Produces blocking or recommended reproducibility actions for reviewer packets. + +### Research Gap Finder + +- Converts missing transfer contexts into research-gap prompts. +- Ranks gaps that fit the lab's declared capabilities. +- Highlights pediatric validation, ancestry-balanced evaluation, and CPU-only clinic rerun gaps from synthetic corpus signals. + +## Acceptance Notes + +- Dependency-free CommonJS module. +- Deterministic synthetic sample data. +- Local tests for broad-claim holds, missing evidence quarantine, external validation handling, report rendering, and research-gap output. +- Generated JSON, Markdown, and SVG artifacts via `npm run demo`. +- No external API calls, credentials, private research data, or live clinical records. diff --git a/external-validity-transfer-assistant/sample-data.js b/external-validity-transfer-assistant/sample-data.js new file mode 100644 index 00000000..23f571e2 --- /dev/null +++ b/external-validity-transfer-assistant/sample-data.js @@ -0,0 +1,149 @@ +const project = { + id: "project-transfer-001", + title: "Portable biomarker triage assistant for multi-site oncology cohorts", + domain: "clinical-ai", + labCapabilities: ["rna-seq", "clinical-metadata", "containerized-python", "external-cohort-review"], + manuscript: { + title: "A generalizable biomarker triage model for oncology screening", + targetUse: "pre-submission peer review", + claims: [ + { + id: "claim-generalizable-oncology", + text: "The model generalizes across oncology patient populations and hospital settings.", + assertedScope: { + populations: ["adult", "pediatric", "underrepresented ancestry"], + settings: ["academic hospital", "community hospital", "international site"], + instruments: ["bulk-rna-seq", "single-cell-rna-seq"], + environments: ["python-3.11", "cuda", "cpu-only"] + }, + evidenceIds: ["internal-adult-cohort", "academic-hospital-runbook"], + confidence: "strong" + }, + { + id: "claim-reproducible-pipeline", + text: "The manuscript includes enough artifacts for an independent lab to rerun the triage pipeline.", + assertedScope: { + populations: ["adult"], + settings: ["academic hospital"], + instruments: ["bulk-rna-seq"], + environments: ["python-3.11"] + }, + evidenceIds: ["container-runbook", "clean-data-manifest", "external-adult-validation"], + confidence: "moderate" + }, + { + id: "claim-deployment-ready", + text: "The assistant is ready for deployment guidance in low-resource clinics.", + assertedScope: { + populations: ["adult"], + settings: ["low-resource clinic", "community hospital"], + instruments: ["bulk-rna-seq"], + environments: ["cpu-only"] + }, + evidenceIds: ["community-hospital-protocol"], + confidence: "strong" + } + ], + requiredSubgroups: ["adult", "pediatric", "underrepresented ancestry"], + declaredLimitations: [ + "The pilot cohort is single-country.", + "No pediatric external cohort is currently available." + ] + }, + evidence: [ + { + id: "internal-adult-cohort", + type: "internal-cohort", + population: "adult", + setting: "academic hospital", + instrument: "bulk-rna-seq", + environment: "python-3.11", + sampleSize: 820, + reproducible: true, + externalValidation: false, + limitations: ["single-country", "academic-site-only"] + }, + { + id: "academic-hospital-runbook", + type: "runbook", + population: "adult", + setting: "academic hospital", + instrument: "bulk-rna-seq", + environment: "python-3.11", + sampleSize: 0, + reproducible: true, + externalValidation: false, + limitations: ["no-community-site-smoke-test"] + }, + { + id: "container-runbook", + type: "runtime-evidence", + population: "adult", + setting: "academic hospital", + instrument: "bulk-rna-seq", + environment: "python-3.11", + sampleSize: 0, + reproducible: true, + externalValidation: false, + limitations: [] + }, + { + id: "clean-data-manifest", + type: "artifact-manifest", + population: "adult", + setting: "academic hospital", + instrument: "bulk-rna-seq", + environment: "python-3.11", + sampleSize: 820, + reproducible: true, + externalValidation: false, + limitations: [] + }, + { + id: "external-adult-validation", + type: "external-validation", + population: "adult", + setting: "community hospital", + instrument: "bulk-rna-seq", + environment: "python-3.11", + sampleSize: 290, + reproducible: true, + externalValidation: true, + limitations: ["adult-only"] + }, + { + id: "community-hospital-protocol", + type: "protocol", + population: "adult", + setting: "community hospital", + instrument: "bulk-rna-seq", + environment: "cpu-only", + sampleSize: 0, + reproducible: false, + externalValidation: false, + limitations: ["protocol-only", "no-completed-run"] + } + ], + corpusSignals: [ + { + id: "gap-pediatric-oncology-rnaseq", + topic: "pediatric oncology RNA-seq validation", + reason: "frequently cited limitation with low replication coverage", + labFit: ["rna-seq", "external-cohort-review"] + }, + { + id: "gap-cpu-only-clinic-run", + topic: "CPU-only low-resource clinic reproducibility run", + reason: "deployment claim depends on clinic-like runtime evidence", + labFit: ["containerized-python"] + }, + { + id: "gap-ancestry-balanced-evaluation", + topic: "ancestry-balanced external validation", + reason: "underrepresented ancestry is asserted but not evidenced", + labFit: ["clinical-metadata", "external-cohort-review"] + } + ] +}; + +module.exports = { project }; diff --git a/external-validity-transfer-assistant/test.js b/external-validity-transfer-assistant/test.js new file mode 100644 index 00000000..265d7471 --- /dev/null +++ b/external-validity-transfer-assistant/test.js @@ -0,0 +1,57 @@ +const assert = require("assert"); +const { project } = require("./sample-data"); +const { buildReviewPacket, evaluateClaim, renderMarkdownReport, renderSvgSummary } = require("./index"); + +const packet = buildReviewPacket(project); + +assert.strictEqual(packet.assistant, "external-validity-transfer-assistant"); +assert.strictEqual(packet.issue, "SCIBASE-AI/SCIBASE.AI#16"); +assert.ok(packet.peerReviewSuggestions.length >= 6, "expected transfer-risk peer review suggestions"); +assert.ok(packet.reproducibilityActions.length >= 3, "expected reproducibility actions"); +assert.ok(packet.researchGaps.length >= 3, "expected research-gap prompts"); + +const broadClaim = packet.claimReviews.find((review) => review.id === "claim-generalizable-oncology"); +assert.ok(broadClaim, "expected broad generalizability claim review"); +assert.ok( + ["hold-for-transfer-evidence", "quarantine-from-review-packet"].includes(broadClaim.decision), + "broad claim should be held or quarantined until transfer evidence exists" +); +assert.ok( + broadClaim.findings.some((finding) => finding.rule === "population-transfer-gap"), + "broad claim should flag missing population transfer evidence" +); +assert.ok( + broadClaim.findings.some((finding) => finding.rule === "no-external-validation"), + "broad claim should require external validation" +); + +const reproducibleClaim = packet.claimReviews.find((review) => review.id === "claim-reproducible-pipeline"); +assert.ok(reproducibleClaim.score > broadClaim.score, "reproducible claim should score better than broad claim"); +assert.strictEqual(reproducibleClaim.observedScope.externalValidation, true); +assert.strictEqual(reproducibleClaim.observedScope.runnableEvidence, true); + +const emptyClaim = { + id: "claim-empty", + text: "The model works for every lab.", + assertedScope: { + populations: ["adult"], + settings: ["international site"], + instruments: ["bulk-rna-seq"], + environments: ["python-3.11"] + }, + evidenceIds: [], + confidence: "strong" +}; +const emptyReview = evaluateClaim(emptyClaim, project.evidence, project.manuscript); +assert.strictEqual(emptyReview.decision, "quarantine-from-review-packet"); +assert.ok(emptyReview.findings.some((finding) => finding.rule === "missing-evidence")); + +const markdown = renderMarkdownReport(packet); +assert.ok(markdown.includes("## Claim Reviews")); +assert.ok(markdown.includes("## Research Gap Prompts")); + +const svg = renderSvgSummary(packet); +assert.ok(svg.includes(" Date: Fri, 22 May 2026 16:06:02 +0530 Subject: [PATCH 2/2] Add external validity demo video --- .../demo-video.js | 174 ++++++++++++++++++ .../package.json | 5 +- .../reports/demo.webm | Bin 0 -> 21719 bytes 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 external-validity-transfer-assistant/demo-video.js create mode 100644 external-validity-transfer-assistant/reports/demo.webm diff --git a/external-validity-transfer-assistant/demo-video.js b/external-validity-transfer-assistant/demo-video.js new file mode 100644 index 00000000..38293ac6 --- /dev/null +++ b/external-validity-transfer-assistant/demo-video.js @@ -0,0 +1,174 @@ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +const reportDir = path.join(__dirname, "reports"); +const outputPath = path.join(reportDir, "demo.webm"); + +const chromeCandidates = [ + process.env.CHROME_PATH, + "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", + "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", + "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe" +].filter(Boolean); + +function findBrowser() { + const found = chromeCandidates.find((candidate) => fs.existsSync(candidate)); + if (!found) { + throw new Error("Chrome or Edge was not found. Set CHROME_PATH to generate reports/demo.webm."); + } + return found; +} + +function fileUrl(filePath) { + return `file:///${filePath.replace(/\\/g, "/")}`; +} + +const html = String.raw` + + + + External validity transfer assistant demo + + + + +
recording
+ + +`; + +fs.mkdirSync(reportDir, { recursive: true }); + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "external-validity-demo-")); +const htmlPath = path.join(tempDir, "demo.html"); +const profileDir = path.join(tempDir, "profile"); +fs.writeFileSync(htmlPath, html, "utf8"); + +const browserPath = findBrowser(); +const stdout = execFileSync( + browserPath, + [ + "--headless=new", + "--disable-gpu", + "--disable-dev-shm-usage", + "--autoplay-policy=no-user-gesture-required", + "--run-all-compositor-stages-before-draw", + "--virtual-time-budget=7000", + `--user-data-dir=${profileDir}`, + "--dump-dom", + fileUrl(htmlPath) + ], + { encoding: "utf8", maxBuffer: 30 * 1024 * 1024 } +); + +const match = stdout.match(/data:video\/webm;base64,([A-Za-z0-9+/=]+)/); +if (!match) { + throw new Error(`Demo video generation failed. Browser output ended with: ${stdout.slice(-600)}`); +} + +fs.writeFileSync(outputPath, Buffer.from(match[1], "base64")); +console.log(`Generated ${path.relative(process.cwd(), outputPath)}`); diff --git a/external-validity-transfer-assistant/package.json b/external-validity-transfer-assistant/package.json index 5c1126cb..13d41e83 100644 --- a/external-validity-transfer-assistant/package.json +++ b/external-validity-transfer-assistant/package.json @@ -6,8 +6,9 @@ "private": true, "type": "commonjs", "scripts": { - "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js", + "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check demo-video.js && node --check test.js", "test": "node test.js", - "demo": "node demo.js" + "demo": "node demo.js", + "demo:video": "node demo-video.js" } } diff --git a/external-validity-transfer-assistant/reports/demo.webm b/external-validity-transfer-assistant/reports/demo.webm new file mode 100644 index 0000000000000000000000000000000000000000..1dee455767a542f88e7dedc1288c0d41157ad1e5 GIT binary patch literal 21719 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 KKaJ@LqVIpZd|&_o literal 0 HcmV?d00001