diff --git a/randomization-blinding-integrity-assistant/README.md b/randomization-blinding-integrity-assistant/README.md
new file mode 100644
index 00000000..10dc30eb
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/README.md
@@ -0,0 +1,24 @@
+# Randomization Blinding Integrity Assistant
+
+Self-contained reviewer assistant slice for SCIBASE issue #16.
+
+It evaluates synthetic clinical and preclinical study packets before AI peer-review output is shown. The guard checks randomization sequence evidence, allocation concealment, arm balance, stratification balance, blinding role coverage, arm-label leakage, early unblinding, and post-randomization exclusions.
+
+## Files
+
+- `index.js` - dependency-free evaluator and Markdown reviewer packet builder
+- `sample-data.js` - synthetic study packets
+- `test.js` - Node test coverage for hold, author-response, imbalance, and approved paths
+- `demo.js` - writes JSON, Markdown, and SVG reviewer artifacts under `reports/`
+- `render-video.js` - creates a short MP4 demo artifact
+
+## Validation
+
+```bash
+npm run check
+npm test
+npm run demo
+npm run video
+```
+
+Synthetic data only. No private manuscripts, patient records, credentials, external APIs, model calls, network calls, payment data, or payout details are used.
diff --git a/randomization-blinding-integrity-assistant/demo.js b/randomization-blinding-integrity-assistant/demo.js
new file mode 100644
index 00000000..ff680092
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/demo.js
@@ -0,0 +1,79 @@
+const fs = require('node:fs');
+const path = require('node:path');
+
+const {evaluateRandomizationBlindingIntegrity, buildReviewerPacket} = require('./index');
+const {samplePacket} = require('./sample-data');
+
+const REPORT_DIR = path.join(__dirname, 'reports');
+
+function escapeXml(value) {
+ return String(value)
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"');
+}
+
+function buildSummarySvg(result) {
+ const width = 1280;
+ const height = 720;
+ const findingWidth = Math.max(20, Math.min(760, result.findings.length * 72));
+ const scoreWidth = Math.max(20, Math.min(760, result.readinessScore * 7.6));
+ const actionWidth = Math.max(20, Math.min(760, result.requiredActions.length * 90));
+ const topFindings = result.findings.slice(0, 5);
+
+ return ``;
+}
+
+function main() {
+ fs.mkdirSync(REPORT_DIR, {recursive: true});
+ const result = evaluateRandomizationBlindingIntegrity(samplePacket);
+ const markdown = buildReviewerPacket(result);
+
+ fs.writeFileSync(path.join(REPORT_DIR, 'randomization-blinding-packet.json'), `${JSON.stringify(result, null, 2)}\n`);
+ fs.writeFileSync(path.join(REPORT_DIR, 'randomization-blinding-report.md'), markdown);
+ fs.writeFileSync(path.join(REPORT_DIR, 'summary.svg'), buildSummarySvg(result));
+
+ console.log(JSON.stringify({
+ manuscriptId: result.manuscriptId,
+ decision: result.decision,
+ readinessScore: result.readinessScore,
+ findings: result.findings.length,
+ requiredActions: result.requiredActions.length,
+ auditDigest: result.auditDigest,
+ }, null, 2));
+}
+
+if (require.main === module) {
+ main();
+}
+
+module.exports = {
+ buildSummarySvg,
+};
diff --git a/randomization-blinding-integrity-assistant/index.js b/randomization-blinding-integrity-assistant/index.js
new file mode 100644
index 00000000..98588fc1
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/index.js
@@ -0,0 +1,307 @@
+const crypto = require('node:crypto');
+
+const RANDOMIZATION_FINDINGS = new Set([
+ 'missing-randomization-sequence',
+ 'missing-allocation-concealment',
+ 'arm-size-imbalance',
+ 'stratification-imbalance',
+]);
+
+const BLINDING_FINDINGS = new Set([
+ 'missing-blinding-role',
+ 'arm-label-leakage',
+ 'early-unblinding-event',
+]);
+
+const EXCLUSION_FINDINGS = new Set([
+ 'unblinded-post-randomization-exclusion',
+]);
+
+const BALANCE_FINDINGS = new Set([
+ 'arm-size-imbalance',
+ 'stratification-imbalance',
+]);
+
+function cleanText(value) {
+ return String(value || '').trim();
+}
+
+function asArray(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function isClinicalStudy(study) {
+ const text = `${study.design || ''} ${study.population || ''}`.toLowerCase();
+ return /clinical|trial|patient|participant|human/.test(text) && !/preclinical|animal|mouse|rat/.test(text);
+}
+
+function percentArmImbalance(arms) {
+ const counts = asArray(arms)
+ .map((arm) => Number(arm.enrolled || 0))
+ .filter((count) => Number.isFinite(count) && count > 0);
+ if (counts.length < 2) return 0;
+ const max = Math.max(...counts);
+ const min = Math.min(...counts);
+ return max === 0 ? 0 : ((max - min) / max) * 100;
+}
+
+function percentStratumImbalance(arms, stratumName) {
+ const counts = asArray(arms)
+ .map((arm) => Number(arm.strata?.[stratumName] || 0))
+ .filter((count) => Number.isFinite(count) && count > 0);
+ if (counts.length < 2) return 0;
+ const max = Math.max(...counts);
+ const min = Math.min(...counts);
+ return max === 0 ? 0 : ((max - min) / max) * 100;
+}
+
+function addFinding(findings, finding) {
+ findings.push({
+ severity: finding.severity || 'major',
+ ...finding,
+ });
+}
+
+function evaluateStudy(study) {
+ const findings = [];
+ const randomization = study.randomization || {};
+ const blinding = study.blinding || {};
+
+ if (!randomization.sequenceEvidence) {
+ addFinding(findings, {
+ type: 'missing-randomization-sequence',
+ severity: 'critical',
+ studyId: study.id,
+ message: 'Randomization sequence evidence is missing from the reviewer packet.',
+ action: 'attach_randomization_sequence',
+ });
+ }
+
+ if (!randomization.allocationConcealment) {
+ addFinding(findings, {
+ type: 'missing-allocation-concealment',
+ severity: 'critical',
+ studyId: study.id,
+ message: 'Allocation concealment is not documented before outcome review.',
+ action: 'document_allocation_concealment',
+ });
+ }
+
+ const maxArmImbalancePercent = Number(randomization.maxArmImbalancePercent || 25);
+ const armImbalance = percentArmImbalance(study.arms);
+ if (armImbalance > maxArmImbalancePercent) {
+ addFinding(findings, {
+ type: 'arm-size-imbalance',
+ severity: 'major',
+ studyId: study.id,
+ metric: Number(armImbalance.toFixed(1)),
+ threshold: maxArmImbalancePercent,
+ message: `Arm sizes differ by ${armImbalance.toFixed(1)}%, above the configured ${maxArmImbalancePercent}% threshold.`,
+ action: 'explain_randomization_imbalance',
+ });
+ }
+
+ for (const factor of asArray(randomization.stratificationFactors)) {
+ const imbalance = percentStratumImbalance(study.arms, factor);
+ if (imbalance > 50) {
+ addFinding(findings, {
+ type: 'stratification-imbalance',
+ severity: 'major',
+ studyId: study.id,
+ stratum: factor,
+ metric: Number(imbalance.toFixed(1)),
+ threshold: 50,
+ message: `Stratum ${factor} differs by ${imbalance.toFixed(1)}% between arms.`,
+ action: 'review_stratification_balance',
+ });
+ }
+ }
+
+ const requiredRoles = isClinicalStudy(study)
+ ? ['participant', 'careProvider', 'outcomeAssessor', 'analyst']
+ : ['outcomeAssessor', 'analyst'];
+ const missingRoles = requiredRoles.filter((role) => blinding[role] !== true);
+ if (missingRoles.length > 0) {
+ addFinding(findings, {
+ type: 'missing-blinding-role',
+ severity: 'major',
+ studyId: study.id,
+ roles: missingRoles,
+ message: `Blinding is missing for ${missingRoles.join(', ')}.`,
+ action: 'document_blinding_roles',
+ });
+ }
+
+ const exposures = asArray(blinding.armLabelExposure);
+ if (exposures.length > 0) {
+ addFinding(findings, {
+ type: 'arm-label-leakage',
+ severity: 'major',
+ studyId: study.id,
+ artifacts: exposures.map((item) => item.artifact || 'unknown artifact'),
+ message: 'Arm labels are visible in analysis or reviewer artifacts before endpoint lock.',
+ action: 'mask_arm_labels_before_analysis',
+ });
+ }
+
+ for (const event of asArray(blinding.unblindingEvents)) {
+ if (event.beforePrimaryEndpointLock) {
+ addFinding(findings, {
+ type: 'early-unblinding-event',
+ severity: 'major',
+ studyId: study.id,
+ role: event.role || 'unknown role',
+ occurredAt: event.occurredAt || 'unknown date',
+ message: 'Unblinding occurred before primary endpoint lock.',
+ action: 'explain_unblinding_event',
+ });
+ }
+ }
+
+ for (const exclusion of asArray(study.exclusions)) {
+ if (exclusion.afterRandomization && !exclusion.blindedDecision) {
+ addFinding(findings, {
+ type: 'unblinded-post-randomization-exclusion',
+ severity: 'major',
+ studyId: study.id,
+ participantId: exclusion.participantId || 'unknown participant',
+ reason: exclusion.reason || 'not provided',
+ message: 'Post-randomization exclusion lacks blinded decision evidence.',
+ action: 'justify_unblinded_exclusion',
+ });
+ }
+ }
+
+ return findings;
+}
+
+function summarize(findings, studyCount) {
+ return {
+ studyCount,
+ randomizationIssues: findings.filter((finding) => RANDOMIZATION_FINDINGS.has(finding.type)).length,
+ blindingIssues: findings.filter((finding) => BLINDING_FINDINGS.has(finding.type)).length,
+ exclusionIssues: findings.filter((finding) => EXCLUSION_FINDINGS.has(finding.type)).length,
+ balanceIssues: findings.filter((finding) => BALANCE_FINDINGS.has(finding.type)).length,
+ };
+}
+
+function calculateReadinessScore(findings) {
+ const score = findings.reduce((total, finding) => {
+ if (finding.severity === 'critical') return total - 25;
+ if (finding.severity === 'major') return total - 15;
+ return total - 8;
+ }, 100);
+ return Math.max(0, score);
+}
+
+function chooseDecision(findings) {
+ if (findings.some((finding) => finding.severity === 'critical')) {
+ return 'hold-for-review';
+ }
+ if (findings.length > 0) {
+ return 'needs-author-response';
+ }
+ return 'approved';
+}
+
+function buildRequiredActions(findings) {
+ const seen = new Set();
+ return findings
+ .map((finding) => ({
+ type: finding.action,
+ studyId: finding.studyId,
+ findingType: finding.type,
+ message: actionMessage(finding),
+ }))
+ .filter((action) => {
+ const key = `${action.type}:${action.studyId}:${action.findingType}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+}
+
+function actionMessage(finding) {
+ const messages = {
+ attach_randomization_sequence: 'Attach sequence generation evidence or randomization service export.',
+ document_allocation_concealment: 'Document who concealed allocation and when assignments became visible.',
+ explain_randomization_imbalance: 'Explain observed arm imbalance and whether it affects the primary endpoint.',
+ review_stratification_balance: 'Review stratification balance and update the analysis caveat.',
+ document_blinding_roles: 'Document blinded roles or explain why a role could not be blinded.',
+ mask_arm_labels_before_analysis: 'Mask labels in notebooks, tables, and reviewer packets before endpoint lock.',
+ explain_unblinding_event: 'Explain unblinding timing, affected roles, and sensitivity analysis impact.',
+ justify_unblinded_exclusion: 'Justify post-randomization exclusions with blinded adjudication evidence.',
+ };
+ return messages[finding.action] || finding.message;
+}
+
+function buildDigest(input) {
+ return crypto.createHash('sha256').update(JSON.stringify(input)).digest('hex').slice(0, 16);
+}
+
+function evaluateRandomizationBlindingIntegrity(packet) {
+ const studies = asArray(packet.studies);
+ const findings = studies.flatMap(evaluateStudy);
+ const summary = summarize(findings, studies.length);
+ const decision = chooseDecision(findings);
+ const readinessScore = decision === 'approved' ? 100 : calculateReadinessScore(findings);
+
+ return {
+ manuscriptId: cleanText(packet.manuscriptId) || 'unknown-manuscript',
+ generatedAt: packet.generatedAt || new Date().toISOString(),
+ decision,
+ readinessScore,
+ summary,
+ findings,
+ requiredActions: buildRequiredActions(findings),
+ auditDigest: buildDigest({studies, findings, summary, decision}),
+ };
+}
+
+function buildReviewerPacket(result) {
+ const lines = [
+ '# Randomization Blinding Integrity Assistant Report',
+ '',
+ `Manuscript: ${result.manuscriptId}`,
+ `Generated: ${result.generatedAt}`,
+ `Decision: ${result.decision}`,
+ `Readiness score: ${result.readinessScore}`,
+ `Findings: ${result.findings.length}`,
+ `Audit digest: ${result.auditDigest}`,
+ '',
+ '## Summary',
+ '',
+ `- Studies reviewed: ${result.summary.studyCount}`,
+ `- Randomization issues: ${result.summary.randomizationIssues}`,
+ `- Blinding issues: ${result.summary.blindingIssues}`,
+ `- Exclusion issues: ${result.summary.exclusionIssues}`,
+ `- Balance issues: ${result.summary.balanceIssues}`,
+ '',
+ '## Findings',
+ '',
+ ];
+
+ if (result.findings.length === 0) {
+ lines.push('- No randomization or blinding integrity findings.');
+ } else {
+ for (const finding of result.findings) {
+ lines.push(`- ${finding.severity.toUpperCase()} ${finding.type} in ${finding.studyId}: ${finding.message}`);
+ }
+ }
+
+ lines.push('', '## Required Actions', '');
+ if (result.requiredActions.length === 0) {
+ lines.push('- No author action required.');
+ } else {
+ for (const action of result.requiredActions) {
+ lines.push(`- ${action.type} (${action.studyId}): ${action.message}`);
+ }
+ }
+
+ return `${lines.join('\n')}\n`;
+}
+
+module.exports = {
+ evaluateRandomizationBlindingIntegrity,
+ buildReviewerPacket,
+};
diff --git a/randomization-blinding-integrity-assistant/package.json b/randomization-blinding-integrity-assistant/package.json
new file mode 100644
index 00000000..9d9de7e3
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "randomization-blinding-integrity-assistant",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Dependency-free randomization and blinding integrity assistant for SCIBASE issue #16.",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check render-video.js && node --check test.js",
+ "test": "node --test test.js",
+ "demo": "node demo.js",
+ "video": "node render-video.js"
+ }
+}
diff --git a/randomization-blinding-integrity-assistant/render-video.js b/randomization-blinding-integrity-assistant/render-video.js
new file mode 100644
index 00000000..c69c4e16
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/render-video.js
@@ -0,0 +1,60 @@
+const fs = require('node:fs');
+const path = require('node:path');
+const {spawnSync} = require('node:child_process');
+
+const {evaluateRandomizationBlindingIntegrity} = require('./index');
+const {samplePacket} = require('./sample-data');
+
+const REPORT_DIR = path.join(__dirname, 'reports');
+
+function resolveFfmpeg() {
+ if (process.env.FFMPEG_PATH) return process.env.FFMPEG_PATH;
+ const candidate = path.resolve(__dirname, '..', '..', '..', 'node_modules', 'ffmpeg-static', 'ffmpeg.exe');
+ if (fs.existsSync(candidate)) return candidate;
+ return 'ffmpeg';
+}
+
+function main() {
+ fs.mkdirSync(REPORT_DIR, {recursive: true});
+ const result = evaluateRandomizationBlindingIntegrity(samplePacket);
+ const outPath = path.join(REPORT_DIR, 'demo.mp4');
+ const ffmpeg = resolveFfmpeg();
+ const scoreWidth = Math.max(24, Math.min(820, Math.round(result.readinessScore * 8.2)));
+ const findingWidth = Math.max(24, Math.min(820, result.findings.length * 86));
+ const actionWidth = Math.max(24, Math.min(820, result.requiredActions.length * 92));
+ const filters = [
+ 'drawbox=x=52:y=52:w=1176:h=616:color=white@0.13:t=fill',
+ 'drawbox=x=76:y=76:w=1128:h=568:color=white@0.08:t=fill',
+ 'drawbox=x=110:y=168:w=820:h=44:color=white@0.28:t=fill',
+ `drawbox=x=110:y=168:w=${scoreWidth}:h=44:color=0x2e7d32@1:t=fill`,
+ 'drawbox=x=110:y=286:w=820:h=44:color=white@0.28:t=fill',
+ `drawbox=x=110:y=286:w=${findingWidth}:h=44:color=0xc2410c@1:t=fill`,
+ 'drawbox=x=110:y=404:w=820:h=44:color=white@0.28:t=fill',
+ `drawbox=x=110:y=404:w=${actionWidth}:h=44:color=0x1565c0@1:t=fill`,
+ 'drawbox=x=984:y=168:w=140:h=44:color=0x2e7d32@1:t=fill',
+ 'drawbox=x=984:y=286:w=140:h=44:color=0xc2410c@1:t=fill',
+ 'drawbox=x=984:y=404:w=140:h=44:color=0x1565c0@1:t=fill',
+ 'drawbox=x=110:y=548:w=520:h=38:color=white@0.22:t=fill',
+ 'drawbox=x=110:y=548:w=420:h=38:color=0xf9ab00@1:t=fill',
+ ].join(',');
+
+ const args = [
+ '-y',
+ '-f', 'lavfi',
+ '-i', 'color=c=0x17324d:s=1280x720:d=4:r=25',
+ '-vf', filters,
+ '-c:v', 'libx264',
+ '-pix_fmt', 'yuv420p',
+ '-movflags', '+faststart',
+ outPath,
+ ];
+ const resultProcess = spawnSync(ffmpeg, args, {stdio: 'inherit'});
+ if (resultProcess.status !== 0) {
+ throw new Error(`ffmpeg exited with ${resultProcess.status}`);
+ }
+ console.log(outPath);
+}
+
+if (require.main === module) {
+ main();
+}
diff --git a/randomization-blinding-integrity-assistant/reports/demo.mp4 b/randomization-blinding-integrity-assistant/reports/demo.mp4
new file mode 100644
index 00000000..312ccda7
Binary files /dev/null and b/randomization-blinding-integrity-assistant/reports/demo.mp4 differ
diff --git a/randomization-blinding-integrity-assistant/reports/randomization-blinding-packet.json b/randomization-blinding-integrity-assistant/reports/randomization-blinding-packet.json
new file mode 100644
index 00000000..1036a33e
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/reports/randomization-blinding-packet.json
@@ -0,0 +1,106 @@
+{
+ "manuscriptId": "ms-randomized-review-demo",
+ "generatedAt": "2026-05-23T06:00:00Z",
+ "decision": "hold-for-review",
+ "readinessScore": 0,
+ "summary": {
+ "studyCount": 3,
+ "randomizationIssues": 2,
+ "blindingIssues": 3,
+ "exclusionIssues": 1,
+ "balanceIssues": 0
+ },
+ "findings": [
+ {
+ "severity": "critical",
+ "type": "missing-randomization-sequence",
+ "studyId": "trial-microbiome-01",
+ "message": "Randomization sequence evidence is missing from the reviewer packet.",
+ "action": "attach_randomization_sequence"
+ },
+ {
+ "severity": "critical",
+ "type": "missing-allocation-concealment",
+ "studyId": "trial-microbiome-01",
+ "message": "Allocation concealment is not documented before outcome review.",
+ "action": "document_allocation_concealment"
+ },
+ {
+ "severity": "major",
+ "type": "missing-blinding-role",
+ "studyId": "animal-stroke-02",
+ "roles": [
+ "analyst"
+ ],
+ "message": "Blinding is missing for analyst.",
+ "action": "document_blinding_roles"
+ },
+ {
+ "severity": "major",
+ "type": "arm-label-leakage",
+ "studyId": "animal-stroke-02",
+ "artifacts": [
+ "analysis-notebook.ipynb"
+ ],
+ "message": "Arm labels are visible in analysis or reviewer artifacts before endpoint lock.",
+ "action": "mask_arm_labels_before_analysis"
+ },
+ {
+ "severity": "major",
+ "type": "early-unblinding-event",
+ "studyId": "animal-stroke-02",
+ "role": "analyst",
+ "occurredAt": "2026-04-11",
+ "message": "Unblinding occurred before primary endpoint lock.",
+ "action": "explain_unblinding_event"
+ },
+ {
+ "severity": "major",
+ "type": "unblinded-post-randomization-exclusion",
+ "studyId": "animal-stroke-02",
+ "participantId": "rat-17",
+ "reason": "outlier response",
+ "message": "Post-randomization exclusion lacks blinded decision evidence.",
+ "action": "justify_unblinded_exclusion"
+ }
+ ],
+ "requiredActions": [
+ {
+ "type": "attach_randomization_sequence",
+ "studyId": "trial-microbiome-01",
+ "findingType": "missing-randomization-sequence",
+ "message": "Attach sequence generation evidence or randomization service export."
+ },
+ {
+ "type": "document_allocation_concealment",
+ "studyId": "trial-microbiome-01",
+ "findingType": "missing-allocation-concealment",
+ "message": "Document who concealed allocation and when assignments became visible."
+ },
+ {
+ "type": "document_blinding_roles",
+ "studyId": "animal-stroke-02",
+ "findingType": "missing-blinding-role",
+ "message": "Document blinded roles or explain why a role could not be blinded."
+ },
+ {
+ "type": "mask_arm_labels_before_analysis",
+ "studyId": "animal-stroke-02",
+ "findingType": "arm-label-leakage",
+ "message": "Mask labels in notebooks, tables, and reviewer packets before endpoint lock."
+ },
+ {
+ "type": "explain_unblinding_event",
+ "studyId": "animal-stroke-02",
+ "findingType": "early-unblinding-event",
+ "message": "Explain unblinding timing, affected roles, and sensitivity analysis impact."
+ },
+ {
+ "type": "justify_unblinded_exclusion",
+ "studyId": "animal-stroke-02",
+ "findingType": "unblinded-post-randomization-exclusion",
+ "message": "Justify post-randomization exclusions with blinded adjudication evidence."
+ }
+ ],
+ "auditDigest": "d8ee0413885e0ff3"
+}
diff --git a/randomization-blinding-integrity-assistant/reports/randomization-blinding-report.md b/randomization-blinding-integrity-assistant/reports/randomization-blinding-report.md
new file mode 100644
index 00000000..45ec1ac5
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/reports/randomization-blinding-report.md
@@ -0,0 +1,34 @@
+# Randomization Blinding Integrity Assistant Report
+
+Manuscript: ms-randomized-review-demo
+Generated: 2026-05-23T06:00:00Z
+Decision: hold-for-review
+Readiness score: 0
+Findings: 6
+Audit digest: d8ee0413885e0ff3
+
+## Summary
+
+- Studies reviewed: 3
+- Randomization issues: 2
+- Blinding issues: 3
+- Exclusion issues: 1
+- Balance issues: 0
+
+## Findings
+
+- CRITICAL missing-randomization-sequence in trial-microbiome-01: Randomization sequence evidence is missing from the reviewer packet.
+- CRITICAL missing-allocation-concealment in trial-microbiome-01: Allocation concealment is not documented before outcome review.
+- MAJOR missing-blinding-role in animal-stroke-02: Blinding is missing for analyst.
+- MAJOR arm-label-leakage in animal-stroke-02: Arm labels are visible in analysis or reviewer artifacts before endpoint lock.
+- MAJOR early-unblinding-event in animal-stroke-02: Unblinding occurred before primary endpoint lock.
+- MAJOR unblinded-post-randomization-exclusion in animal-stroke-02: Post-randomization exclusion lacks blinded decision evidence.
+
+## Required Actions
+
+- attach_randomization_sequence (trial-microbiome-01): Attach sequence generation evidence or randomization service export.
+- document_allocation_concealment (trial-microbiome-01): Document who concealed allocation and when assignments became visible.
+- document_blinding_roles (animal-stroke-02): Document blinded roles or explain why a role could not be blinded.
+- mask_arm_labels_before_analysis (animal-stroke-02): Mask labels in notebooks, tables, and reviewer packets before endpoint lock.
+- explain_unblinding_event (animal-stroke-02): Explain unblinding timing, affected roles, and sensitivity analysis impact.
+- justify_unblinded_exclusion (animal-stroke-02): Justify post-randomization exclusions with blinded adjudication evidence.
diff --git a/randomization-blinding-integrity-assistant/reports/summary.svg b/randomization-blinding-integrity-assistant/reports/summary.svg
new file mode 100644
index 00000000..d5caf53e
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/reports/summary.svg
@@ -0,0 +1,31 @@
+
\ No newline at end of file
diff --git a/randomization-blinding-integrity-assistant/sample-data.js b/randomization-blinding-integrity-assistant/sample-data.js
new file mode 100644
index 00000000..fe8060cf
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/sample-data.js
@@ -0,0 +1,93 @@
+const samplePacket = {
+ manuscriptId: 'ms-randomized-review-demo',
+ generatedAt: '2026-05-23T06:00:00Z',
+ studies: [
+ {
+ id: 'trial-microbiome-01',
+ design: 'parallel clinical trial',
+ population: 'adult participants',
+ arms: [
+ {id: 'control', enrolled: 42, strata: {siteA: 20, siteB: 22}},
+ {id: 'probiotic', enrolled: 41, strata: {siteA: 21, siteB: 20}},
+ ],
+ randomization: {
+ sequenceEvidence: false,
+ method: '',
+ allocationConcealment: false,
+ stratificationFactors: ['siteA', 'siteB'],
+ maxArmImbalancePercent: 15,
+ },
+ blinding: {
+ participant: true,
+ careProvider: true,
+ outcomeAssessor: true,
+ analyst: true,
+ armLabelExposure: [],
+ unblindingEvents: [],
+ },
+ exclusions: [],
+ },
+ {
+ id: 'animal-stroke-02',
+ design: 'preclinical animal experiment',
+ population: 'rat ischemia model',
+ arms: [
+ {id: 'sham', enrolled: 18, strata: {male: 9, female: 9}},
+ {id: 'treatment', enrolled: 18, strata: {male: 10, female: 8}},
+ ],
+ randomization: {
+ sequenceEvidence: true,
+ method: 'blocked randomization',
+ allocationConcealment: true,
+ stratificationFactors: ['male', 'female'],
+ maxArmImbalancePercent: 20,
+ },
+ blinding: {
+ participant: false,
+ careProvider: false,
+ outcomeAssessor: true,
+ analyst: false,
+ armLabelExposure: [
+ {artifact: 'analysis-notebook.ipynb', label: 'treatment arm names visible before primary analysis'},
+ ],
+ unblindingEvents: [
+ {role: 'analyst', occurredAt: '2026-04-11', beforePrimaryEndpointLock: true},
+ ],
+ },
+ exclusions: [
+ {participantId: 'rat-17', afterRandomization: true, reason: 'outlier response', blindedDecision: false},
+ ],
+ },
+ {
+ id: 'trial-oncology-04',
+ design: 'double-blind randomized trial',
+ population: 'human oncology participants',
+ arms: [
+ {id: 'placebo', enrolled: 120, strata: {siteA: 60, siteB: 60}},
+ {id: 'therapy', enrolled: 118, strata: {siteA: 59, siteB: 59}},
+ ],
+ randomization: {
+ sequenceEvidence: true,
+ method: 'centralized permuted blocks',
+ allocationConcealment: true,
+ stratificationFactors: ['siteA', 'siteB'],
+ maxArmImbalancePercent: 10,
+ },
+ blinding: {
+ participant: true,
+ careProvider: true,
+ outcomeAssessor: true,
+ analyst: true,
+ armLabelExposure: [],
+ unblindingEvents: [],
+ },
+ exclusions: [
+ {participantId: 'p-099', afterRandomization: true, reason: 'withdrew consent', blindedDecision: true},
+ ],
+ },
+ ],
+};
+
+module.exports = {
+ samplePacket,
+};
diff --git a/randomization-blinding-integrity-assistant/test.js b/randomization-blinding-integrity-assistant/test.js
new file mode 100644
index 00000000..90cb93a1
--- /dev/null
+++ b/randomization-blinding-integrity-assistant/test.js
@@ -0,0 +1,179 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+
+const {
+ evaluateRandomizationBlindingIntegrity,
+ buildReviewerPacket,
+} = require('./index');
+
+test('holds studies that lack sequence evidence and allocation concealment', () => {
+ const result = evaluateRandomizationBlindingIntegrity({
+ manuscriptId: 'ms-hidden-allocation-risk',
+ generatedAt: '2026-05-23T06:00:00Z',
+ studies: [
+ {
+ id: 'trial-microbiome-01',
+ design: 'parallel clinical trial',
+ arms: [
+ {id: 'control', enrolled: 42},
+ {id: 'probiotic', enrolled: 41},
+ ],
+ randomization: {
+ sequenceEvidence: false,
+ method: '',
+ allocationConcealment: false,
+ stratificationFactors: ['site'],
+ },
+ blinding: {
+ participant: true,
+ careProvider: true,
+ outcomeAssessor: true,
+ analyst: true,
+ armLabelExposure: [],
+ unblindingEvents: [],
+ },
+ exclusions: [],
+ },
+ ],
+ });
+
+ assert.equal(result.decision, 'hold-for-review');
+ assert.equal(result.summary.studyCount, 1);
+ assert.equal(result.summary.randomizationIssues, 2);
+ assert.deepEqual(
+ result.findings.map((finding) => finding.type),
+ ['missing-randomization-sequence', 'missing-allocation-concealment']
+ );
+ assert.equal(result.requiredActions[0].type, 'attach_randomization_sequence');
+});
+
+test('requires author response for unblinding, arm-label leakage, and post-randomization exclusions', () => {
+ const result = evaluateRandomizationBlindingIntegrity({
+ manuscriptId: 'ms-unblinded-analysis',
+ generatedAt: '2026-05-23T06:00:00Z',
+ studies: [
+ {
+ id: 'animal-stroke-02',
+ design: 'preclinical animal experiment',
+ arms: [
+ {id: 'sham', enrolled: 18},
+ {id: 'treatment', enrolled: 18},
+ ],
+ randomization: {
+ sequenceEvidence: true,
+ method: 'blocked randomization',
+ allocationConcealment: true,
+ stratificationFactors: ['sex', 'baseline-score'],
+ },
+ blinding: {
+ participant: false,
+ careProvider: false,
+ outcomeAssessor: true,
+ analyst: false,
+ armLabelExposure: [
+ {artifact: 'analysis-notebook.ipynb', label: 'treatment arm names visible before primary analysis'},
+ ],
+ unblindingEvents: [
+ {role: 'analyst', occurredAt: '2026-04-11', beforePrimaryEndpointLock: true},
+ ],
+ },
+ exclusions: [
+ {participantId: 'rat-17', afterRandomization: true, reason: 'outlier response', blindedDecision: false},
+ ],
+ },
+ ],
+ });
+
+ assert.equal(result.decision, 'needs-author-response');
+ assert.equal(result.summary.blindingIssues, 3);
+ assert.equal(result.summary.exclusionIssues, 1);
+ assert.deepEqual(
+ result.findings.map((finding) => finding.type),
+ ['missing-blinding-role', 'arm-label-leakage', 'early-unblinding-event', 'unblinded-post-randomization-exclusion']
+ );
+ assert.equal(result.requiredActions.at(-1).type, 'justify_unblinded_exclusion');
+});
+
+test('flags stratification imbalance without blocking complete blinded packets', () => {
+ const result = evaluateRandomizationBlindingIntegrity({
+ manuscriptId: 'ms-imbalance-review',
+ generatedAt: '2026-05-23T06:00:00Z',
+ studies: [
+ {
+ id: 'trial-cardiology-03',
+ design: 'multicenter randomized trial',
+ arms: [
+ {id: 'standard-care', enrolled: 33, strata: {siteA: 30, siteB: 3}},
+ {id: 'new-device', enrolled: 45, strata: {siteA: 20, siteB: 25}},
+ ],
+ randomization: {
+ sequenceEvidence: true,
+ method: 'permuted blocks',
+ allocationConcealment: true,
+ stratificationFactors: ['site'],
+ maxArmImbalancePercent: 20,
+ },
+ blinding: {
+ participant: true,
+ careProvider: true,
+ outcomeAssessor: true,
+ analyst: true,
+ armLabelExposure: [],
+ unblindingEvents: [],
+ },
+ exclusions: [],
+ },
+ ],
+ });
+
+ assert.equal(result.decision, 'needs-author-response');
+ assert.equal(result.summary.balanceIssues, 1);
+ assert.equal(result.findings[0].type, 'arm-size-imbalance');
+ assert.equal(result.requiredActions[0].type, 'explain_randomization_imbalance');
+});
+
+test('approves complete integrity packets and builds deterministic reviewer report', () => {
+ const result = evaluateRandomizationBlindingIntegrity({
+ manuscriptId: 'ms-ready-randomized-study',
+ generatedAt: '2026-05-23T06:00:00Z',
+ studies: [
+ {
+ id: 'trial-oncology-04',
+ design: 'double-blind randomized trial',
+ arms: [
+ {id: 'placebo', enrolled: 120, strata: {siteA: 60, siteB: 60}},
+ {id: 'therapy', enrolled: 118, strata: {siteA: 59, siteB: 59}},
+ ],
+ randomization: {
+ sequenceEvidence: true,
+ method: 'centralized permuted blocks',
+ allocationConcealment: true,
+ stratificationFactors: ['site', 'disease-stage'],
+ maxArmImbalancePercent: 10,
+ },
+ blinding: {
+ participant: true,
+ careProvider: true,
+ outcomeAssessor: true,
+ analyst: true,
+ armLabelExposure: [],
+ unblindingEvents: [],
+ },
+ exclusions: [
+ {participantId: 'p-099', afterRandomization: true, reason: 'withdrew consent', blindedDecision: true},
+ ],
+ },
+ ],
+ });
+
+ assert.equal(result.decision, 'approved');
+ assert.equal(result.readinessScore, 100);
+ assert.equal(result.findings.length, 0);
+
+ const packet = buildReviewerPacket(result);
+ assert.match(packet, /# Randomization Blinding Integrity Assistant Report/);
+ assert.match(packet, /Manuscript: ms-ready-randomized-study/);
+ assert.match(packet, /Decision: approved/);
+ assert.match(packet, /Readiness score: 100/);
+ assert.match(packet, /Findings: 0/);
+});