Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions bayesian-prior-sensitivity-assistant/README.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions bayesian-prior-sensitivity-assistant/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -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
```
84 changes: 84 additions & 0 deletions bayesian-prior-sensitivity-assistant/demo.js
Original file line number Diff line number Diff line change
@@ -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 `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="340" viewBox="0 0 760 340" role="img" aria-label="Bayesian prior sensitivity summary">
<rect width="760" height="340" fill="#f7f7fb"/>
<rect x="32" y="32" width="696" height="276" rx="8" fill="#ffffff" stroke="#d7dce5"/>
<text x="56" y="76" font-family="Arial, sans-serif" font-size="26" font-weight="700" fill="#172033">Bayesian prior sensitivity assistant</text>
<text x="56" y="106" font-family="Arial, sans-serif" font-size="14" fill="#526173">Auto peer-review red flags for posterior robustness</text>
${bar(56, 154, "Major revision", mixed.major, "#b91c1c")}
${bar(56, 214, "Minor revision", mixed.minor, "#b7791f")}
${bar(56, 274, "Ready", mixed.ready, "#15803d")}
<text x="548" y="294" font-family="Arial, sans-serif" font-size="12" fill="#526173">Digest ${packet.mixed.auditDigest.slice(0, 16)}</text>
</svg>
`;
}

function bar(x, y, label, count, color) {
const width = Math.max(24, count * 120);
return [
` <text x="${x}" y="${y - 14}" font-family="Arial, sans-serif" font-size="15" font-weight="700" fill="#172033">${label}</text>`,
` <rect x="${x}" y="${y}" width="360" height="24" rx="4" fill="#e8edf4"/>`,
` <rect x="${x}" y="${y}" width="${width}" height="24" rx="4" fill="${color}"/>`,
` <text x="${x + 382}" y="${y + 17}" font-family="Arial, sans-serif" font-size="13" fill="#172033">${count} analysis item(s)</text>`
].join("\n");
}
229 changes: 229 additions & 0 deletions bayesian-prior-sensitivity-assistant/index.js
Original file line number Diff line number Diff line change
@@ -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
};
Loading