diff --git a/bayesian-prior-sensitivity-assistant/README.md b/bayesian-prior-sensitivity-assistant/README.md new file mode 100644 index 00000000..d3c9c4af --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/README.md @@ -0,0 +1,35 @@ +# Bayesian Prior Sensitivity Assistant + +Synthetic, dependency-free auto peer-review assistant for SCIBASE issue #16, AI-Powered Research Assistant Suite. + +This module evaluates Bayesian analysis review packets for prior-sensitivity and posterior-robustness risks before an AI assistant promotes manuscript feedback to authors or reviewers. + +## What It Checks + +- priors used without manuscript disclosure +- informative priors without domain justification +- posterior direction flips under alternate priors +- large posterior shifts under prior sensitivity runs +- convergence failures, divergent transitions, and low effective sample size +- missing or weak prior predictive checks +- strong manuscript claims that are not robust across priors + +## Run + +```bash +npm run check +npm test +npm run demo +npm run demo:video +``` + +The demo writes reviewer artifacts under `reports/`: + +- `bayesian-prior-sensitivity-packet.json` +- `bayesian-prior-sensitivity-report.md` +- `summary.svg` +- `demo.avi` + +## Safety + +All records are synthetic. The module does not read local files outside this folder, call external services, execute notebooks, or process real manuscripts. diff --git a/bayesian-prior-sensitivity-assistant/acceptance-notes.md b/bayesian-prior-sensitivity-assistant/acceptance-notes.md new file mode 100644 index 00000000..b8554317 --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/acceptance-notes.md @@ -0,0 +1,19 @@ +# Acceptance Notes + +Reviewer acceptance checklist: + +- The high-risk clinical Bayesian model is held for major revision. +- The borderline materials model is marked for minor revision. +- The ready ecology model passes without findings. +- Strong claims that are not prior-robust are blocked. +- Audit digests are deterministic across repeated runs. +- Demo artifacts are generated locally from synthetic data only. + +Validation commands: + +```bash +npm run check +npm test +npm run demo +npm run demo:video +``` diff --git a/bayesian-prior-sensitivity-assistant/demo.js b/bayesian-prior-sensitivity-assistant/demo.js new file mode 100644 index 00000000..bc065bb1 --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/demo.js @@ -0,0 +1,84 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { assessBayesianPriorSensitivity } = require("./index"); +const { mixedBayesianReviewPacket, readyBayesianReviewPacket } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const packet = { + mixed: assessBayesianPriorSensitivity(mixedBayesianReviewPacket), + ready: assessBayesianPriorSensitivity(readyBayesianReviewPacket) +}; + +fs.writeFileSync(path.join(reportsDir, "bayesian-prior-sensitivity-packet.json"), `${JSON.stringify(packet, null, 2)}\n`); +fs.writeFileSync(path.join(reportsDir, "bayesian-prior-sensitivity-report.md"), renderMarkdown(packet)); +fs.writeFileSync(path.join(reportsDir, "summary.svg"), renderSvg(packet)); + +console.log(`Wrote ${path.relative(process.cwd(), reportsDir)}`); +console.log(`Mixed packet status: ${packet.mixed.status}`); +console.log(`Audit digest: ${packet.mixed.auditDigest}`); + +function renderMarkdown(packet) { + const lines = [ + "# Bayesian Prior Sensitivity Assistant Report", + "", + "Synthetic auto peer-review packet for SCIBASE issue #16.", + "", + "## Summary", + "", + "| Packet | Status | Major | Minor | Ready | Digest |", + "| --- | --- | ---: | ---: | ---: | --- |", + `| mixed | ${packet.mixed.status} | ${packet.mixed.summary.major} | ${packet.mixed.summary.minor} | ${packet.mixed.summary.ready} | ${packet.mixed.auditDigest.slice(0, 12)} |`, + `| ready | ${packet.ready.status} | ${packet.ready.summary.major} | ${packet.ready.summary.minor} | ${packet.ready.summary.ready} | ${packet.ready.auditDigest.slice(0, 12)} |`, + "", + "## Analysis Decisions", + "" + ]; + + for (const analysis of packet.mixed.analyses) { + lines.push(`### ${analysis.analysisId}: ${analysis.decision}`); + for (const blocker of analysis.blockers) { + lines.push(`- blocker ${blocker.code}: ${blocker.message}`); + } + for (const warning of analysis.warnings) { + lines.push(`- warning ${warning.code}: ${warning.message}`); + } + lines.push(""); + } + + lines.push("## Non-Overlap Notes", ""); + lines.push( + "This module focuses specifically on Bayesian prior sensitivity, convergence diagnostics, prior predictive checks, and posterior claim calibration. It does not implement a broad assistant suite, multiple-comparison correction, generic statistical consistency checks, study power feasibility, uncertainty tone review, prompt safety, literature freshness, image integrity, external validity, or protocol trace workflows." + ); + + return `${lines.join("\n")}\n`; +} + +function renderSvg(packet) { + const mixed = packet.mixed.summary; + return ` + + + + Bayesian prior sensitivity assistant + Auto peer-review red flags for posterior robustness + ${bar(56, 154, "Major revision", mixed.major, "#b91c1c")} + ${bar(56, 214, "Minor revision", mixed.minor, "#b7791f")} + ${bar(56, 274, "Ready", mixed.ready, "#15803d")} + Digest ${packet.mixed.auditDigest.slice(0, 16)} + +`; +} + +function bar(x, y, label, count, color) { + const width = Math.max(24, count * 120); + return [ + ` ${label}`, + ` `, + ` `, + ` ${count} analysis item(s)` + ].join("\n"); +} diff --git a/bayesian-prior-sensitivity-assistant/index.js b/bayesian-prior-sensitivity-assistant/index.js new file mode 100644 index 00000000..00e38d15 --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/index.js @@ -0,0 +1,229 @@ +"use strict"; + +const crypto = require("crypto"); + +const DEFAULT_POLICY = { + maxRhat: 1.01, + severeRhat: 1.05, + minEffectiveSampleSize: 400, + severeEffectiveSampleSize: 150, + maxPosteriorShift: 0.2, + minPriorPredictiveCoverage: 0.7, + strongClaimProbability: 0.95 +}; + +function assessBayesianPriorSensitivity(packet, policy = {}) { + const settings = { ...DEFAULT_POLICY, ...policy }; + const generatedAt = new Date(packet.generatedAt || "2026-05-28T00:00:00Z"); + const analyses = (packet.analyses || []).map((analysis) => evaluateAnalysis(analysis, settings)); + const summary = summarize(analyses); + const payload = { + generatedAt: generatedAt.toISOString(), + manuscriptId: packet.manuscriptId || "synthetic-bayesian-manuscript", + status: summary.status, + summary, + analyses + }; + + return { + ...payload, + auditDigest: digest(payload) + }; +} + +function evaluateAnalysis(analysis, settings) { + const blockers = []; + const warnings = []; + const actions = []; + + checkPriorDisclosure(analysis, blockers, warnings, actions); + checkPriorSensitivity(analysis, settings, blockers, warnings, actions); + checkDiagnostics(analysis, settings, blockers, warnings, actions); + checkPriorPredictive(analysis, settings, blockers, warnings, actions); + checkClaims(analysis, settings, blockers, warnings, actions); + + const decision = blockers.length > 0 + ? "major_revision_prior_robustness" + : warnings.length > 0 + ? "minor_revision_prior_robustness" + : "ready_for_peer_review"; + + return { + analysisId: analysis.id, + domain: analysis.domain, + decision, + blockers, + warnings, + actions: [...new Set(actions)] + }; +} + +function checkPriorDisclosure(analysis, blockers, warnings, actions) { + const undisclosed = analysis.priors.filter((prior) => !prior.reported); + const unjustifiedInformative = analysis.priors.filter((prior) => prior.reported && prior.strength === "informative" && !prior.justification); + + if (undisclosed.length > 0) { + blockers.push({ + code: "prior_not_reported", + message: `${undisclosed.length} model prior(s) are used without manuscript disclosure.` + }); + actions.push("Disclose all model priors with parameter values and rationale."); + } + + if (unjustifiedInformative.length > 0) { + warnings.push({ + code: "informative_prior_not_justified", + message: `${unjustifiedInformative.length} informative prior(s) lack domain justification.` + }); + actions.push("Add domain rationale or sensitivity analysis for informative priors."); + } +} + +function checkPriorSensitivity(analysis, settings, blockers, warnings, actions) { + for (const run of analysis.sensitivityRuns) { + const baseline = run.baselineEstimate; + const alternate = run.alternateEstimate; + const shift = Math.abs(alternate - baseline); + const directionFlips = Math.sign(baseline) !== 0 && Math.sign(alternate) !== 0 && Math.sign(baseline) !== Math.sign(alternate); + + if (directionFlips) { + blockers.push({ + code: "posterior_direction_flips_under_prior", + message: `${run.name} changes effect direction from ${round(baseline)} to ${round(alternate)}.` + }); + actions.push("Mark the manuscript claim as prior-sensitive and add a robustness table."); + } else if (shift > settings.maxPosteriorShift) { + warnings.push({ + code: "large_posterior_shift_under_prior", + message: `${run.name} shifts posterior estimate by ${round(shift)}.` + }); + actions.push("Report alternate-prior posterior estimates beside the primary model."); + } + } +} + +function checkDiagnostics(analysis, settings, blockers, warnings, actions) { + const diagnostics = analysis.diagnostics; + + if (diagnostics.divergentTransitions > 0) { + blockers.push({ + code: "divergent_transitions_present", + message: `${diagnostics.divergentTransitions} divergent transition(s) were reported.` + }); + actions.push("Resolve sampler divergences before accepting posterior claims."); + } + + if (diagnostics.maxRhat > settings.severeRhat) { + blockers.push({ + code: "severe_rhat_convergence_failure", + message: `Maximum R-hat is ${round(diagnostics.maxRhat)}.` + }); + actions.push("Rerun or reparameterize the model until convergence diagnostics pass."); + } else if (diagnostics.maxRhat > settings.maxRhat) { + warnings.push({ + code: "rhat_above_review_threshold", + message: `Maximum R-hat is ${round(diagnostics.maxRhat)}.` + }); + actions.push("Include convergence diagnostics and rerun chains if needed."); + } + + if (diagnostics.minEffectiveSampleSize < settings.severeEffectiveSampleSize) { + blockers.push({ + code: "severe_effective_sample_size_shortfall", + message: `Minimum effective sample size is ${diagnostics.minEffectiveSampleSize}.` + }); + actions.push("Increase sampling effort or simplify the model before review."); + } else if (diagnostics.minEffectiveSampleSize < settings.minEffectiveSampleSize) { + warnings.push({ + code: "low_effective_sample_size", + message: `Minimum effective sample size is ${diagnostics.minEffectiveSampleSize}.` + }); + actions.push("Report effective sample sizes and rerun where posterior tails are unstable."); + } +} + +function checkPriorPredictive(analysis, settings, blockers, warnings, actions) { + const check = analysis.priorPredictiveCheck; + + if (!check.reported) { + blockers.push({ + code: "prior_predictive_check_missing", + message: "No prior predictive check is reported for the Bayesian model." + }); + actions.push("Add a prior predictive check before claims are promoted to peer-review text."); + return; + } + + if (check.coverage < settings.minPriorPredictiveCoverage) { + warnings.push({ + code: "weak_prior_predictive_coverage", + message: `Prior predictive coverage is ${Math.round(check.coverage * 100)}%.` + }); + actions.push("Revise priors or explain why prior predictive coverage is low."); + } +} + +function checkClaims(analysis, settings, blockers, warnings, actions) { + for (const claim of analysis.claims) { + const strongLanguage = ["proves", "conclusive", "definitive"].includes(claim.strength); + + if (strongLanguage && !claim.robustAcrossPriors) { + blockers.push({ + code: "strong_claim_not_prior_robust", + message: `${claim.id} uses strong language without prior-robust support.` + }); + actions.push("Downgrade claim language or add prior-robust evidence."); + } else if (strongLanguage && claim.posteriorProbability < settings.strongClaimProbability) { + warnings.push({ + code: "claim_probability_below_strong_language", + message: `${claim.id} has posterior probability ${round(claim.posteriorProbability)} with strong wording.` + }); + actions.push("Calibrate the manuscript claim to the posterior probability."); + } + } +} + +function summarize(analyses) { + const major = analyses.filter((analysis) => analysis.decision === "major_revision_prior_robustness").length; + const minor = analyses.filter((analysis) => analysis.decision === "minor_revision_prior_robustness").length; + const ready = analyses.filter((analysis) => analysis.decision === "ready_for_peer_review").length; + const blockerCount = analyses.reduce((sum, analysis) => sum + analysis.blockers.length, 0); + const warningCount = analyses.reduce((sum, analysis) => sum + analysis.warnings.length, 0); + const status = major > 0 ? "hold_peer_review_claims" : minor > 0 ? "revise_before_review" : "ready"; + + return { + status, + analysisCount: analyses.length, + major, + minor, + ready, + blockerCount, + warningCount + }; +} + +function digest(value) { + return crypto.createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function round(value) { + return Math.round(value * 100) / 100; +} + +module.exports = { + assessBayesianPriorSensitivity, + DEFAULT_POLICY +}; diff --git a/bayesian-prior-sensitivity-assistant/make-demo-video.js b/bayesian-prior-sensitivity-assistant/make-demo-video.js new file mode 100644 index 00000000..9ead76c3 --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/make-demo-video.js @@ -0,0 +1,163 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const width = 320; +const height = 180; +const fps = 2; +const frameCount = 16; +const rowSize = Math.ceil((width * 3) / 4) * 4; +const frameSize = rowSize * height; +const reportsDir = path.join(__dirname, "reports"); +const outputPath = path.join(reportsDir, "demo.avi"); + +fs.mkdirSync(reportsDir, { recursive: true }); + +const frames = []; +for (let i = 0; i < frameCount; i += 1) { + frames.push(makeFrame(i)); +} + +const chunks = []; +const indexes = []; +let offset = 4; +for (const frame of frames) { + const chunk = riffChunk("00db", frame); + chunks.push(chunk); + indexes.push(indexEntry("00db", 0x10, offset, frame.length)); + offset += chunk.length; +} + +const hdrl = listChunk("hdrl", [ + riffChunk("avih", aviHeader()), + listChunk("strl", [ + riffChunk("strh", streamHeader()), + riffChunk("strf", bitmapInfoHeader()) + ]) +]); +const movi = listChunk("movi", chunks); +const idx1 = riffChunk("idx1", Buffer.concat(indexes)); +const riff = riffChunk("RIFF", Buffer.concat([Buffer.from("AVI "), hdrl, movi, idx1])); + +fs.writeFileSync(outputPath, riff); +console.log(`Wrote ${path.relative(process.cwd(), outputPath)} (${riff.length} bytes)`); + +function makeFrame(frameIndex) { + const frame = Buffer.alloc(frameSize, 0xff); + const progress = (frameIndex + 1) / frameCount; + fillRect(frame, 0, 0, width, height, [247, 247, 251]); + fillRect(frame, 18, 18, 284, 144, [255, 255, 255]); + strokeRect(frame, 18, 18, 284, 144, [210, 216, 226]); + fillRect(frame, 38, 50, 240, 18, [232, 237, 244]); + fillRect(frame, 38, 50, Math.round(240 * progress), 18, [185, 28, 28]); + fillRect(frame, 38, 90, 240, 18, [232, 237, 244]); + fillRect(frame, 38, 90, Math.round(120 * progress), 18, [183, 121, 31]); + fillRect(frame, 38, 130, 240, 18, [232, 237, 244]); + fillRect(frame, 38, 130, Math.round(120 * progress), 18, [21, 128, 61]); + drawTicks(frame, 38, 76, frameIndex); + return frame; +} + +function drawTicks(frame, x, y, frameIndex) { + const count = Math.min(5, Math.floor(frameIndex / 3) + 1); + for (let i = 0; i < count; i += 1) { + fillRect(frame, x + i * 18, y, 10, 10, [42, 72, 88]); + } +} + +function fillRect(frame, x, y, w, h, rgb) { + for (let py = y; py < y + h; py += 1) { + if (py < 0 || py >= height) continue; + for (let px = x; px < x + w; px += 1) { + if (px < 0 || px >= width) continue; + setPixel(frame, px, py, rgb); + } + } +} + +function strokeRect(frame, x, y, w, h, rgb) { + fillRect(frame, x, y, w, 1, rgb); + fillRect(frame, x, y + h - 1, w, 1, rgb); + fillRect(frame, x, y, 1, h, rgb); + fillRect(frame, x + w - 1, y, 1, h, rgb); +} + +function setPixel(frame, x, y, rgb) { + const bottomUpY = height - y - 1; + const index = bottomUpY * rowSize + x * 3; + frame[index] = rgb[2]; + frame[index + 1] = rgb[1]; + frame[index + 2] = rgb[0]; +} + +function aviHeader() { + const buffer = Buffer.alloc(56); + buffer.writeUInt32LE(Math.round(1000000 / fps), 0); + buffer.writeUInt32LE(frameSize * fps, 4); + buffer.writeUInt32LE(0, 8); + buffer.writeUInt32LE(0x10, 12); + buffer.writeUInt32LE(frameCount, 16); + buffer.writeUInt32LE(0, 20); + buffer.writeUInt32LE(1, 24); + buffer.writeUInt32LE(frameSize, 28); + buffer.writeUInt32LE(width, 32); + buffer.writeUInt32LE(height, 36); + return buffer; +} + +function streamHeader() { + const buffer = Buffer.alloc(56); + buffer.write("vids", 0, 4, "ascii"); + buffer.write("DIB ", 4, 4, "ascii"); + buffer.writeUInt32LE(0, 8); + buffer.writeUInt32LE(0, 12); + buffer.writeUInt32LE(0, 16); + buffer.writeUInt32LE(1, 20); + buffer.writeUInt32LE(fps, 24); + buffer.writeUInt32LE(0, 28); + buffer.writeUInt32LE(frameCount, 32); + buffer.writeUInt32LE(frameSize, 36); + buffer.writeInt32LE(-1, 40); + buffer.writeUInt32LE(0, 44); + buffer.writeInt16LE(0, 48); + buffer.writeInt16LE(0, 50); + buffer.writeInt16LE(width, 52); + buffer.writeInt16LE(height, 54); + return buffer; +} + +function bitmapInfoHeader() { + const buffer = Buffer.alloc(40); + buffer.writeUInt32LE(40, 0); + buffer.writeInt32LE(width, 4); + buffer.writeInt32LE(height, 8); + buffer.writeUInt16LE(1, 12); + buffer.writeUInt16LE(24, 14); + buffer.writeUInt32LE(0, 16); + buffer.writeUInt32LE(frameSize, 20); + return buffer; +} + +function indexEntry(id, flags, chunkOffset, size) { + const buffer = Buffer.alloc(16); + buffer.write(id, 0, 4, "ascii"); + buffer.writeUInt32LE(flags, 4); + buffer.writeUInt32LE(chunkOffset, 8); + buffer.writeUInt32LE(size, 12); + return buffer; +} + +function riffChunk(id, payload) { + const size = payload.length; + const pad = size % 2 === 1 ? 1 : 0; + const buffer = Buffer.alloc(8 + size + pad); + buffer.write(id, 0, 4, "ascii"); + buffer.writeUInt32LE(size, 4); + payload.copy(buffer, 8); + return buffer; +} + +function listChunk(type, chunks) { + return riffChunk("LIST", Buffer.concat([Buffer.from(type, "ascii"), ...chunks])); +} diff --git a/bayesian-prior-sensitivity-assistant/package.json b/bayesian-prior-sensitivity-assistant/package.json new file mode 100644 index 00000000..45e6a26a --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/package.json @@ -0,0 +1,13 @@ +{ + "name": "bayesian-prior-sensitivity-assistant", + "version": "1.0.0", + "description": "Synthetic Bayesian prior sensitivity review assistant for SCIBASE issue 16.", + "main": "index.js", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js && node --check make-demo-video.js", + "test": "node test.js", + "demo": "node demo.js", + "demo:video": "node make-demo-video.js" + }, + "license": "MIT" +} diff --git a/bayesian-prior-sensitivity-assistant/reports/bayesian-prior-sensitivity-packet.json b/bayesian-prior-sensitivity-assistant/reports/bayesian-prior-sensitivity-packet.json new file mode 100644 index 00000000..94709540 --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/reports/bayesian-prior-sensitivity-packet.json @@ -0,0 +1,138 @@ +{ + "mixed": { + "generatedAt": "2026-05-28T07:30:00.000Z", + "manuscriptId": "synthetic-bayesian-review", + "status": "hold_peer_review_claims", + "summary": { + "status": "hold_peer_review_claims", + "analysisCount": 3, + "major": 1, + "minor": 1, + "ready": 1, + "blockerCount": 7, + "warningCount": 6 + }, + "analyses": [ + { + "analysisId": "clinical-survival-model", + "domain": "clinical trials", + "decision": "major_revision_prior_robustness", + "blockers": [ + { + "code": "prior_not_reported", + "message": "1 model prior(s) are used without manuscript disclosure." + }, + { + "code": "posterior_direction_flips_under_prior", + "message": "skeptical treatment prior changes effect direction from 0.31 to -0.08." + }, + { + "code": "divergent_transitions_present", + "message": "14 divergent transition(s) were reported." + }, + { + "code": "severe_rhat_convergence_failure", + "message": "Maximum R-hat is 1.08." + }, + { + "code": "severe_effective_sample_size_shortfall", + "message": "Minimum effective sample size is 112." + }, + { + "code": "prior_predictive_check_missing", + "message": "No prior predictive check is reported for the Bayesian model." + }, + { + "code": "strong_claim_not_prior_robust", + "message": "claim-treatment-definitive uses strong language without prior-robust support." + } + ], + "warnings": [ + { + "code": "informative_prior_not_justified", + "message": "1 informative prior(s) lack domain justification." + }, + { + "code": "large_posterior_shift_under_prior", + "message": "wider frailty prior shifts posterior estimate by 0.27." + } + ], + "actions": [ + "Disclose all model priors with parameter values and rationale.", + "Add domain rationale or sensitivity analysis for informative priors.", + "Mark the manuscript claim as prior-sensitive and add a robustness table.", + "Report alternate-prior posterior estimates beside the primary model.", + "Resolve sampler divergences before accepting posterior claims.", + "Rerun or reparameterize the model until convergence diagnostics pass.", + "Increase sampling effort or simplify the model before review.", + "Add a prior predictive check before claims are promoted to peer-review text.", + "Downgrade claim language or add prior-robust evidence." + ] + }, + { + "analysisId": "materials-yield-model", + "domain": "materials science", + "decision": "minor_revision_prior_robustness", + "blockers": [], + "warnings": [ + { + "code": "informative_prior_not_justified", + "message": "1 informative prior(s) lack domain justification." + }, + { + "code": "rhat_above_review_threshold", + "message": "Maximum R-hat is 1.02." + }, + { + "code": "low_effective_sample_size", + "message": "Minimum effective sample size is 260." + }, + { + "code": "weak_prior_predictive_coverage", + "message": "Prior predictive coverage is 58%." + } + ], + "actions": [ + "Add domain rationale or sensitivity analysis for informative priors.", + "Include convergence diagnostics and rerun chains if needed.", + "Report effective sample sizes and rerun where posterior tails are unstable.", + "Revise priors or explain why prior predictive coverage is low." + ] + }, + { + "analysisId": "ecology-occupancy-model", + "domain": "ecology", + "decision": "ready_for_peer_review", + "blockers": [], + "warnings": [], + "actions": [] + } + ], + "auditDigest": "0f1f6b29a79b38614e0b5c509f8479266adf55dd81661884b67153e2eb5f9757" + }, + "ready": { + "generatedAt": "2026-05-28T07:30:00.000Z", + "manuscriptId": "synthetic-ready-bayesian-review", + "status": "ready", + "summary": { + "status": "ready", + "analysisCount": 1, + "major": 0, + "minor": 0, + "ready": 1, + "blockerCount": 0, + "warningCount": 0 + }, + "analyses": [ + { + "analysisId": "ecology-occupancy-model", + "domain": "ecology", + "decision": "ready_for_peer_review", + "blockers": [], + "warnings": [], + "actions": [] + } + ], + "auditDigest": "43a9b74dc9b5b4ed480e414f2f5d7b6a7e9d8dae4df9b00cfddabb354aeea190" + } +} diff --git a/bayesian-prior-sensitivity-assistant/reports/bayesian-prior-sensitivity-report.md b/bayesian-prior-sensitivity-assistant/reports/bayesian-prior-sensitivity-report.md new file mode 100644 index 00000000..89cf4706 --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/reports/bayesian-prior-sensitivity-report.md @@ -0,0 +1,35 @@ +# Bayesian Prior Sensitivity Assistant Report + +Synthetic auto peer-review packet for SCIBASE issue #16. + +## Summary + +| Packet | Status | Major | Minor | Ready | Digest | +| --- | --- | ---: | ---: | ---: | --- | +| mixed | hold_peer_review_claims | 1 | 1 | 1 | 0f1f6b29a79b | +| ready | ready | 0 | 0 | 1 | 43a9b74dc9b5 | + +## Analysis Decisions + +### clinical-survival-model: major_revision_prior_robustness +- blocker prior_not_reported: 1 model prior(s) are used without manuscript disclosure. +- blocker posterior_direction_flips_under_prior: skeptical treatment prior changes effect direction from 0.31 to -0.08. +- blocker divergent_transitions_present: 14 divergent transition(s) were reported. +- blocker severe_rhat_convergence_failure: Maximum R-hat is 1.08. +- blocker severe_effective_sample_size_shortfall: Minimum effective sample size is 112. +- blocker prior_predictive_check_missing: No prior predictive check is reported for the Bayesian model. +- blocker strong_claim_not_prior_robust: claim-treatment-definitive uses strong language without prior-robust support. +- warning informative_prior_not_justified: 1 informative prior(s) lack domain justification. +- warning large_posterior_shift_under_prior: wider frailty prior shifts posterior estimate by 0.27. + +### materials-yield-model: minor_revision_prior_robustness +- warning informative_prior_not_justified: 1 informative prior(s) lack domain justification. +- warning rhat_above_review_threshold: Maximum R-hat is 1.02. +- warning low_effective_sample_size: Minimum effective sample size is 260. +- warning weak_prior_predictive_coverage: Prior predictive coverage is 58%. + +### ecology-occupancy-model: ready_for_peer_review + +## Non-Overlap Notes + +This module focuses specifically on Bayesian prior sensitivity, convergence diagnostics, prior predictive checks, and posterior claim calibration. It does not implement a broad assistant suite, multiple-comparison correction, generic statistical consistency checks, study power feasibility, uncertainty tone review, prompt safety, literature freshness, image integrity, external validity, or protocol trace workflows. diff --git a/bayesian-prior-sensitivity-assistant/reports/demo.avi b/bayesian-prior-sensitivity-assistant/reports/demo.avi new file mode 100644 index 00000000..cc3de3b7 Binary files /dev/null and b/bayesian-prior-sensitivity-assistant/reports/demo.avi differ diff --git a/bayesian-prior-sensitivity-assistant/reports/summary.svg b/bayesian-prior-sensitivity-assistant/reports/summary.svg new file mode 100644 index 00000000..b9c0ce1f --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/reports/summary.svg @@ -0,0 +1,20 @@ + + + + + Bayesian prior sensitivity assistant + Auto peer-review red flags for posterior robustness + Major revision + + + 1 analysis item(s) + Minor revision + + + 1 analysis item(s) + Ready + + + 1 analysis item(s) + Digest 0f1f6b29a79b3861 + diff --git a/bayesian-prior-sensitivity-assistant/requirements-map.md b/bayesian-prior-sensitivity-assistant/requirements-map.md new file mode 100644 index 00000000..460e8720 --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/requirements-map.md @@ -0,0 +1,13 @@ +# Requirements Map + +| Issue #16 requirement | Coverage | +| --- | --- | +| Auto peer review reports | Emits structured reviewer findings, severity, remediation actions, and deterministic audit digests. | +| Statistical or methodological red flags | Flags prior sensitivity, convergence, divergent transitions, low effective sample size, and weak prior predictive coverage. | +| Claims vs. evidence alignment | Checks whether strong manuscript claims remain robust under alternate priors and posterior probabilities. | +| Adaptive templates per domain | Includes synthetic clinical, materials science, and ecology review packets with domain labels. | +| Reproducibility confidence support | Records deterministic outcomes and diagnostics that can feed reproducibility review workflows. | + +## Non-Overlap + +This is not a broad AI assistant suite, multiple-comparison control tool, generic statistical consistency checker, study power feasibility assistant, uncertainty tone reviewer, prompt safety guard, literature freshness checker, image integrity tool, external-validity reviewer, or protocol trace workflow. It focuses narrowly on Bayesian prior sensitivity and posterior robustness. diff --git a/bayesian-prior-sensitivity-assistant/sample-data.js b/bayesian-prior-sensitivity-assistant/sample-data.js new file mode 100644 index 00000000..51eaa528 --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/sample-data.js @@ -0,0 +1,143 @@ +"use strict"; + +const mixedBayesianReviewPacket = { + generatedAt: "2026-05-28T07:30:00Z", + manuscriptId: "synthetic-bayesian-review", + analyses: [ + { + id: "clinical-survival-model", + domain: "clinical trials", + priors: [ + { + name: "treatment_log_hazard_ratio", + reported: true, + strength: "informative", + justification: false + }, + { + name: "baseline_hazard_spline", + reported: false, + strength: "weakly-informative", + justification: false + } + ], + sensitivityRuns: [ + { + name: "skeptical treatment prior", + baselineEstimate: 0.31, + alternateEstimate: -0.08 + }, + { + name: "wider frailty prior", + baselineEstimate: 0.31, + alternateEstimate: 0.04 + } + ], + diagnostics: { + maxRhat: 1.08, + minEffectiveSampleSize: 112, + divergentTransitions: 14 + }, + priorPredictiveCheck: { + reported: false, + coverage: 0 + }, + claims: [ + { + id: "claim-treatment-definitive", + posteriorProbability: 0.91, + strength: "definitive", + robustAcrossPriors: false + } + ] + }, + { + id: "materials-yield-model", + domain: "materials science", + priors: [ + { + name: "thermal_gradient_effect", + reported: true, + strength: "informative", + justification: false + } + ], + sensitivityRuns: [ + { + name: "neutral process prior", + baselineEstimate: 0.44, + alternateEstimate: 0.29 + } + ], + diagnostics: { + maxRhat: 1.02, + minEffectiveSampleSize: 260, + divergentTransitions: 0 + }, + priorPredictiveCheck: { + reported: true, + coverage: 0.58 + }, + claims: [ + { + id: "claim-yield-improves", + posteriorProbability: 0.94, + strength: "likely", + robustAcrossPriors: true + } + ] + }, + { + id: "ecology-occupancy-model", + domain: "ecology", + priors: [ + { + name: "occupancy_intercept", + reported: true, + strength: "weakly-informative", + justification: true + } + ], + sensitivityRuns: [ + { + name: "wide occupancy prior", + baselineEstimate: 0.22, + alternateEstimate: 0.18 + }, + { + name: "skeptical occupancy prior", + baselineEstimate: 0.22, + alternateEstimate: 0.2 + } + ], + diagnostics: { + maxRhat: 1, + minEffectiveSampleSize: 920, + divergentTransitions: 0 + }, + priorPredictiveCheck: { + reported: true, + coverage: 0.82 + }, + claims: [ + { + id: "claim-habitat-associated", + posteriorProbability: 0.96, + strength: "likely", + robustAcrossPriors: true + } + ] + } + ] +}; + +const readyBayesianReviewPacket = { + generatedAt: "2026-05-28T07:30:00Z", + manuscriptId: "synthetic-ready-bayesian-review", + analyses: [mixedBayesianReviewPacket.analyses[2]] +}; + +module.exports = { + mixedBayesianReviewPacket, + readyBayesianReviewPacket +}; diff --git a/bayesian-prior-sensitivity-assistant/test.js b/bayesian-prior-sensitivity-assistant/test.js new file mode 100644 index 00000000..1da0814b --- /dev/null +++ b/bayesian-prior-sensitivity-assistant/test.js @@ -0,0 +1,61 @@ +"use strict"; + +const assert = require("assert"); +const { assessBayesianPriorSensitivity } = require("./index"); +const { mixedBayesianReviewPacket, readyBayesianReviewPacket } = require("./sample-data"); + +function testHighRiskBayesianAnalysisIsHeld() { + const result = assessBayesianPriorSensitivity(mixedBayesianReviewPacket); + const clinical = result.analyses.find((analysis) => analysis.analysisId === "clinical-survival-model"); + const codes = clinical.blockers.map((blocker) => blocker.code); + + assert.strictEqual(result.status, "hold_peer_review_claims"); + assert.strictEqual(clinical.decision, "major_revision_prior_robustness"); + assert(codes.includes("prior_not_reported")); + assert(codes.includes("posterior_direction_flips_under_prior")); + assert(codes.includes("divergent_transitions_present")); + assert(codes.includes("severe_rhat_convergence_failure")); + assert(codes.includes("severe_effective_sample_size_shortfall")); + assert(codes.includes("prior_predictive_check_missing")); + assert(codes.includes("strong_claim_not_prior_robust")); +} + +function testModerateBayesianAnalysisNeedsMinorRevision() { + const result = assessBayesianPriorSensitivity(mixedBayesianReviewPacket); + const materials = result.analyses.find((analysis) => analysis.analysisId === "materials-yield-model"); + const codes = materials.warnings.map((warning) => warning.code); + + assert.strictEqual(materials.decision, "minor_revision_prior_robustness"); + assert(codes.includes("informative_prior_not_justified")); + assert(codes.includes("rhat_above_review_threshold")); + assert(codes.includes("low_effective_sample_size")); + assert(codes.includes("weak_prior_predictive_coverage")); +} + +function testReadyBayesianAnalysisPasses() { + const result = assessBayesianPriorSensitivity(readyBayesianReviewPacket); + const ready = result.analyses[0]; + + assert.strictEqual(result.status, "ready"); + assert.strictEqual(ready.decision, "ready_for_peer_review"); + assert.strictEqual(ready.blockers.length, 0); + assert.strictEqual(ready.warnings.length, 0); +} + +function testDigestIsDeterministic() { + const first = assessBayesianPriorSensitivity(mixedBayesianReviewPacket); + const second = assessBayesianPriorSensitivity(mixedBayesianReviewPacket); + + assert.match(first.auditDigest, /^[a-f0-9]{64}$/); + assert.strictEqual(first.auditDigest, second.auditDigest); +} + +function run() { + testHighRiskBayesianAnalysisIsHeld(); + testModerateBayesianAnalysisNeedsMinorRevision(); + testReadyBayesianAnalysisPasses(); + testDigestIsDeterministic(); + console.log("4 tests passed"); +} + +run();