diff --git a/lay-summary-safety-guard/README.md b/lay-summary-safety-guard/README.md
new file mode 100644
index 00000000..e831a899
--- /dev/null
+++ b/lay-summary-safety-guard/README.md
@@ -0,0 +1,41 @@
+# Layperson Summary Safety Guard
+
+Self-contained reviewer slice for SCIBASE issue #13, AI-Assisted Research Tools.
+
+This guard focuses on the paper summarizer's layperson explanation mode. It checks whether a generated public-facing summary is safe to share with patients, funders, collaborators, or non-specialist readers before publication or one-click export.
+
+It audits:
+
+- unsupported public benefit claims
+- over-strong causal language
+- medical or treatment-advice wording
+- missing uncertainty and limitations
+- unexplained jargon
+- reading-level drift above the intended audience
+- sentence-to-evidence overlap
+
+The module uses synthetic data only and does not call LLMs, citation services, medical systems, external APIs, or credentialed services.
+
+## Files
+
+- `index.js` - lay summary audit engine and report renderers
+- `sample-data.js` - unsafe and safe synthetic summary packets
+- `test.js` - deterministic regression tests
+- `demo.js` - generates JSON, Markdown, and SVG reviewer artifacts
+- `render-video.js` - renders the required short MP4 demo
+- `reports/` - generated reviewer packet
+
+## Validation
+
+```bash
+node lay-summary-safety-guard/test.js
+node lay-summary-safety-guard/demo.js
+node lay-summary-safety-guard/render-video.js
+node --check lay-summary-safety-guard/index.js
+node --check lay-summary-safety-guard/sample-data.js
+node --check lay-summary-safety-guard/test.js
+node --check lay-summary-safety-guard/demo.js
+node --check lay-summary-safety-guard/render-video.js
+git diff --check
+ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt,width,height -show_entries format=duration,size -of default=nw=1 lay-summary-safety-guard/reports/demo.mp4
+```
diff --git a/lay-summary-safety-guard/demo.js b/lay-summary-safety-guard/demo.js
new file mode 100644
index 00000000..96503ab5
--- /dev/null
+++ b/lay-summary-safety-guard/demo.js
@@ -0,0 +1,24 @@
+const path = require('path');
+const { evaluateSummary, writeReports } = require('./index');
+const { unsafePacket } = require('./sample-data');
+
+function runDemo(outputDir = path.join(__dirname, 'reports')) {
+ const audit = evaluateSummary(unsafePacket);
+ const reports = writeReports(audit, outputDir);
+ console.log(`Decision: ${audit.decision}`);
+ console.log(`Sentences checked: ${audit.summary.sentences}`);
+ console.log(`Reading grade: ${audit.summary.readingGrade}`);
+ console.log(`Blockers: ${audit.summary.blockers}`);
+ console.log(`Holds: ${audit.summary.holds}`);
+ console.log('Reports written:');
+ console.log(`- ${reports.jsonPath}`);
+ console.log(`- ${reports.markdownPath}`);
+ console.log(`- ${reports.svgPath}`);
+ return { audit, reports };
+}
+
+if (require.main === module) {
+ runDemo();
+}
+
+module.exports = { runDemo };
diff --git a/lay-summary-safety-guard/index.js b/lay-summary-safety-guard/index.js
new file mode 100644
index 00000000..e43cd7fc
--- /dev/null
+++ b/lay-summary-safety-guard/index.js
@@ -0,0 +1,294 @@
+const fs = require('fs');
+const path = require('path');
+
+const MEDICAL_ADVICE_RE = /\b(ask your doctor to|stop taking|cure|guaranteed|safe for everyone|replace treatment|take this|prescribe)\b/i;
+const CAUSAL_OVERSTATEMENT_RE = /\b(proves|will prevent|will cure|guarantees|eliminates|causes everyone|definitely|certainty)\b/i;
+const BENEFIT_RE = /\b(cure|prevent|prevents|reduces|improves|saves|protects|treats|benefit|works)\b/i;
+const UNCERTAINTY_RE = /\b(may|might|could|suggests|associated|limited|uncertain|early|small|preliminary|not yet known)\b/i;
+
+function tokenize(text) {
+ return String(text || '')
+ .toLowerCase()
+ .replace(/[^a-z0-9\s-]/g, ' ')
+ .split(/\s+/)
+ .filter(Boolean);
+}
+
+function splitSentences(text) {
+ return String(text || '')
+ .split(/(?<=[.!?])\s+/)
+ .map((sentence) => sentence.trim())
+ .filter(Boolean);
+}
+
+function countSyllables(word) {
+ const cleaned = String(word || '').toLowerCase().replace(/[^a-z]/g, '');
+ if (!cleaned) return 0;
+ const matches = cleaned.replace(/e$/, '').match(/[aeiouy]+/g);
+ return Math.max(1, matches ? matches.length : 1);
+}
+
+function fleschKincaidGrade(text) {
+ const sentences = Math.max(1, splitSentences(text).length);
+ const words = tokenize(text);
+ const wordCount = Math.max(1, words.length);
+ const syllables = words.reduce((total, word) => total + countSyllables(word), 0);
+ return Number((0.39 * (wordCount / sentences) + 11.8 * (syllables / wordCount) - 15.59).toFixed(1));
+}
+
+function evidenceCoverage(sentence, evidenceItems) {
+ const words = new Set(tokenize(sentence).filter((word) => word.length > 3));
+ let best = { id: null, score: 0, matchedTerms: [] };
+
+ for (const item of evidenceItems) {
+ const terms = new Set([...(item.terms || []), ...tokenize(item.text)].filter((term) => term.length > 3));
+ const matchedTerms = [...words].filter((word) => terms.has(word));
+ const score = terms.size === 0 ? 0 : matchedTerms.length / Math.min(words.size || 1, terms.size);
+ if (score > best.score) {
+ best = { id: item.id, score: Number(score.toFixed(2)), matchedTerms };
+ }
+ }
+
+ return best;
+}
+
+function addFinding(findings, severity, code, message, evidence = {}) {
+ findings.push({ severity, code, message, evidence });
+}
+
+function evaluateSummary(packet) {
+ const evidenceItems = packet.sourceEvidence || [];
+ const findings = [];
+ const sentenceReports = [];
+ const sentences = splitSentences(packet.summary);
+ const grade = fleschKincaidGrade(packet.summary);
+ const targetGrade = packet.audience?.maxGradeLevel || 8;
+
+ if (grade > targetGrade) {
+ addFinding(
+ findings,
+ 'hold',
+ 'reading-level-too-high',
+ `Lay summary reads at grade ${grade}, above the target grade ${targetGrade}.`,
+ { grade, targetGrade },
+ );
+ }
+
+ for (const jargon of packet.audience?.jargonTerms || []) {
+ const regex = new RegExp(`\\b${escapeRegExp(jargon)}\\b`, 'i');
+ if (regex.test(packet.summary)) {
+ addFinding(findings, 'hold', 'jargon-not-explained', `Jargon term needs plain-language explanation: ${jargon}`, {
+ term: jargon,
+ });
+ }
+ }
+
+ for (const sentence of sentences) {
+ const coverage = evidenceCoverage(sentence, evidenceItems);
+ const sentenceFindings = [];
+
+ if (BENEFIT_RE.test(sentence) && coverage.score < 0.2) {
+ sentenceFindings.push('benefit-claim-weakly-supported');
+ addFinding(
+ findings,
+ 'blocker',
+ 'benefit-claim-weakly-supported',
+ 'A public-facing benefit claim lacks enough source-evidence overlap.',
+ { sentence, bestEvidence: coverage },
+ );
+ }
+
+ if (CAUSAL_OVERSTATEMENT_RE.test(sentence)) {
+ sentenceFindings.push('causal-overstatement');
+ addFinding(
+ findings,
+ 'blocker',
+ 'causal-overstatement',
+ 'Lay summary uses stronger causal wording than the evidence supports.',
+ { sentence },
+ );
+ }
+
+ if (MEDICAL_ADVICE_RE.test(sentence)) {
+ sentenceFindings.push('medical-advice-risk');
+ addFinding(
+ findings,
+ 'blocker',
+ 'medical-advice-risk',
+ 'Lay summary risks sounding like clinical or medical advice.',
+ { sentence },
+ );
+ }
+
+ if (BENEFIT_RE.test(sentence) && !UNCERTAINTY_RE.test(sentence)) {
+ sentenceFindings.push('uncertainty-missing');
+ addFinding(
+ findings,
+ 'hold',
+ 'uncertainty-missing',
+ 'Benefit-oriented sentence should preserve uncertainty for a public audience.',
+ { sentence },
+ );
+ }
+
+ sentenceReports.push({ sentence, bestEvidence: coverage, findings: sentenceFindings });
+ }
+
+ for (const required of packet.requiredLimitations || []) {
+ const terms = tokenize(required);
+ const summaryWords = new Set(tokenize(packet.summary));
+ const matched = terms.filter((term) => summaryWords.has(term));
+ if (matched.length < Math.min(2, terms.length)) {
+ addFinding(findings, 'hold', 'required-limitation-missing', `Required limitation is missing: ${required}`, {
+ limitation: required,
+ });
+ }
+ }
+
+ const blockers = findings.filter((finding) => finding.severity === 'blocker');
+ const holds = findings.filter((finding) => finding.severity === 'hold');
+ const decision = blockers.length ? 'block-public-share' : holds.length ? 'hold-for-rewrite' : 'approve-lay-summary';
+
+ return {
+ title: packet.title,
+ audience: packet.audience,
+ decision,
+ shareable: decision === 'approve-lay-summary',
+ summary: {
+ sentences: sentences.length,
+ readingGrade: grade,
+ blockers: blockers.length,
+ holds: holds.length,
+ evidenceItems: evidenceItems.length,
+ },
+ findings,
+ sentenceReports,
+ rewriteActions: buildRewriteActions(findings),
+ };
+}
+
+function buildRewriteActions(findings) {
+ const actions = [];
+ const has = (code) => findings.some((finding) => finding.code === code);
+
+ if (has('benefit-claim-weakly-supported')) {
+ actions.push('Remove or narrow unsupported benefit claims and attach a specific evidence span.');
+ }
+ if (has('causal-overstatement')) {
+ actions.push('Replace causal verbs such as "proves" or "will prevent" with cautious language such as "suggests" or "may".');
+ }
+ if (has('medical-advice-risk')) {
+ actions.push('Remove treatment instructions and add a human-clinician disclaimer when health content is present.');
+ }
+ if (has('uncertainty-missing')) {
+ actions.push('Add uncertainty, study-size, and population context to benefit-oriented sentences.');
+ }
+ if (has('reading-level-too-high') || has('jargon-not-explained')) {
+ actions.push('Rewrite with shorter sentences and define scientific terms in everyday language.');
+ }
+ if (has('required-limitation-missing')) {
+ actions.push('Add the missing study limitations before the summary is shared with non-specialists.');
+ }
+
+ return actions;
+}
+
+function renderMarkdown(audit) {
+ const lines = [
+ '# Layperson Summary Safety Guard',
+ '',
+ `Title: ${audit.title}`,
+ `Decision: ${audit.decision}`,
+ `Audience: ${audit.audience?.name || 'public'}`,
+ '',
+ '## Summary',
+ '',
+ `- Sentences checked: ${audit.summary.sentences}`,
+ `- Reading grade: ${audit.summary.readingGrade}`,
+ `- Evidence items: ${audit.summary.evidenceItems}`,
+ `- Blockers: ${audit.summary.blockers}`,
+ `- Holds: ${audit.summary.holds}`,
+ '',
+ '## Findings',
+ '',
+ ];
+
+ if (audit.findings.length === 0) {
+ lines.push('No findings. The lay summary is ready for public sharing.');
+ } else {
+ for (const finding of audit.findings) {
+ lines.push(`- ${finding.severity.toUpperCase()} ${finding.code}: ${finding.message}`);
+ }
+ }
+
+ lines.push('', '## Rewrite Actions', '');
+ for (const action of audit.rewriteActions) {
+ lines.push(`- ${action}`);
+ }
+
+ lines.push('', '## Sentence Evidence Map', '');
+ for (const report of audit.sentenceReports) {
+ const evidence = report.bestEvidence.id || 'none';
+ lines.push(`- ${report.findings.length ? 'REWRITE' : 'OK'} evidence=${evidence} score=${report.bestEvidence.score}: ${report.sentence}`);
+ }
+
+ return `${lines.join('\n')}\n`;
+}
+
+function renderSvg(audit) {
+ const statusColor = audit.decision === 'approve-lay-summary' ? '#16a34a' : audit.decision === 'hold-for-rewrite' ? '#ca8a04' : '#dc2626';
+ const rows = audit.findings.slice(0, 7).map((finding, index) => {
+ const y = 188 + index * 36;
+ const color = finding.severity === 'blocker' ? '#dc2626' : '#ca8a04';
+ return `${finding.severity.toUpperCase()}${escapeXml(finding.code)}`;
+ }).join('\n');
+
+ return `
+
+`;
+}
+
+function writeReports(audit, outputDir = path.join(__dirname, 'reports')) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ const jsonPath = path.join(outputDir, 'summary-safety-audit.json');
+ const markdownPath = path.join(outputDir, 'rewrite-packet.md');
+ const svgPath = path.join(outputDir, 'summary.svg');
+ fs.writeFileSync(jsonPath, `${JSON.stringify(audit, null, 2)}\n`);
+ fs.writeFileSync(markdownPath, renderMarkdown(audit));
+ fs.writeFileSync(svgPath, renderSvg(audit));
+ return { jsonPath, markdownPath, svgPath };
+}
+
+function escapeRegExp(value) {
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+function escapeXml(value) {
+ return String(value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+module.exports = {
+ tokenize,
+ splitSentences,
+ fleschKincaidGrade,
+ evidenceCoverage,
+ evaluateSummary,
+ renderMarkdown,
+ renderSvg,
+ writeReports,
+};
diff --git a/lay-summary-safety-guard/render-video.js b/lay-summary-safety-guard/render-video.js
new file mode 100644
index 00000000..200e49f5
--- /dev/null
+++ b/lay-summary-safety-guard/render-video.js
@@ -0,0 +1,130 @@
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { spawnSync } = require('child_process');
+const { runDemo } = require('./demo');
+
+const FONT = {
+ A: ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
+ B: ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],
+ C: ['01111', '10000', '10000', '10000', '10000', '10000', '01111'],
+ D: ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],
+ E: ['11111', '10000', '10000', '11110', '10000', '10000', '11111'],
+ F: ['11111', '10000', '10000', '11110', '10000', '10000', '10000'],
+ G: ['01111', '10000', '10000', '10111', '10001', '10001', '01111'],
+ H: ['10001', '10001', '10001', '11111', '10001', '10001', '10001'],
+ I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'],
+ J: ['00111', '00010', '00010', '00010', '10010', '10010', '01100'],
+ K: ['10001', '10010', '10100', '11000', '10100', '10010', '10001'],
+ L: ['10000', '10000', '10000', '10000', '10000', '10000', '11111'],
+ M: ['10001', '11011', '10101', '10101', '10001', '10001', '10001'],
+ N: ['10001', '11001', '10101', '10011', '10001', '10001', '10001'],
+ O: ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
+ P: ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
+ Q: ['01110', '10001', '10001', '10001', '10101', '10010', '01101'],
+ R: ['11110', '10001', '10001', '11110', '10100', '10010', '10001'],
+ S: ['01111', '10000', '10000', '01110', '00001', '00001', '11110'],
+ T: ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
+ U: ['10001', '10001', '10001', '10001', '10001', '10001', '01110'],
+ V: ['10001', '10001', '10001', '10001', '10001', '01010', '00100'],
+ W: ['10001', '10001', '10001', '10101', '10101', '10101', '01010'],
+ X: ['10001', '10001', '01010', '00100', '01010', '10001', '10001'],
+ Y: ['10001', '10001', '01010', '00100', '00100', '00100', '00100'],
+ Z: ['11111', '00001', '00010', '00100', '01000', '10000', '11111'],
+ 0: ['01110', '10001', '10011', '10101', '11001', '10001', '01110'],
+ 1: ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
+ 2: ['01110', '10001', '00001', '00010', '00100', '01000', '11111'],
+ 3: ['11110', '00001', '00001', '01110', '00001', '00001', '11110'],
+ 4: ['00010', '00110', '01010', '10010', '11111', '00010', '00010'],
+ 5: ['11111', '10000', '10000', '11110', '00001', '00001', '11110'],
+ 6: ['01111', '10000', '10000', '11110', '10001', '10001', '01110'],
+ 7: ['11111', '00001', '00010', '00100', '01000', '01000', '01000'],
+ 8: ['01110', '10001', '10001', '01110', '10001', '10001', '01110'],
+ 9: ['01110', '10001', '10001', '01111', '00001', '00001', '11110'],
+ '-': ['00000', '00000', '00000', '11111', '00000', '00000', '00000'],
+ ':': ['00000', '00100', '00100', '00000', '00100', '00100', '00000'],
+ ' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'],
+};
+
+function rgb(hex) {
+ const value = hex.replace('#', '');
+ return [parseInt(value.slice(0, 2), 16), parseInt(value.slice(2, 4), 16), parseInt(value.slice(4, 6), 16)];
+}
+
+function fillRect(buffer, width, height, x, y, rectWidth, rectHeight, color) {
+ const [r, g, b] = color;
+ for (let py = Math.max(0, y); py < Math.min(height, y + rectHeight); py += 1) {
+ for (let px = Math.max(0, x); px < Math.min(width, x + rectWidth); px += 1) {
+ const offset = (py * width + px) * 3;
+ buffer[offset] = r;
+ buffer[offset + 1] = g;
+ buffer[offset + 2] = b;
+ }
+ }
+}
+
+function drawText(buffer, width, height, text, x, y, scale, color) {
+ let cursor = x;
+ for (const raw of String(text).toUpperCase()) {
+ const glyph = FONT[raw] || FONT[' '];
+ for (let row = 0; row < glyph.length; row += 1) {
+ for (let col = 0; col < glyph[row].length; col += 1) {
+ if (glyph[row][col] === '1') {
+ fillRect(buffer, width, height, cursor + col * scale, y + row * scale, scale, scale, color);
+ }
+ }
+ }
+ cursor += 6 * scale;
+ }
+}
+
+function writePpm(filePath, buffer, width, height) {
+ fs.writeFileSync(filePath, Buffer.concat([Buffer.from(`P6\n${width} ${height}\n255\n`), buffer]));
+}
+
+function renderFrame(filePath, audit, frameIndex, totalFrames) {
+ const width = 960;
+ const height = 540;
+ const buffer = Buffer.alloc(width * height * 3);
+ const navy = rgb('#0f172a');
+ const paper = rgb('#f8fafc');
+ const slate = rgb('#334155');
+ const red = rgb('#dc2626');
+ const amber = rgb('#ca8a04');
+ const green = rgb('#16a34a');
+ fillRect(buffer, width, height, 0, 0, width, height, navy);
+ fillRect(buffer, width, height, 46, 42, 868, 456, paper);
+ fillRect(buffer, width, height, 46, 42, 868, 78, rgb('#e2e8f0'));
+ fillRect(buffer, width, height, 76, 390, 808, 22, rgb('#cbd5e1'));
+ fillRect(buffer, width, height, 76, 390, Math.floor(808 * frameIndex / Math.max(1, totalFrames - 1)), 22, red);
+ drawText(buffer, width, height, 'LAY SUMMARY SAFETY GUARD', 76, 72, 5, navy);
+ drawText(buffer, width, height, `DECISION ${audit.decision.replace(/-/g, ' ')}`, 76, 146, 4, red);
+ drawText(buffer, width, height, `${audit.summary.blockers} BLOCKERS ${audit.summary.holds} HOLDS`, 76, 206, 4, slate);
+ drawText(buffer, width, height, `GRADE ${audit.summary.readingGrade}`, 76, 256, 4, amber);
+ drawText(buffer, width, height, 'EVIDENCE JARGON UNCERTAINTY', 76, 318, 3, green);
+ drawText(buffer, width, height, 'JSON MD SVG MP4 GENERATED', 76, 440, 3, slate);
+ writePpm(filePath, buffer, width, height);
+}
+
+function renderVideo() {
+ const outputDir = path.join(__dirname, 'reports');
+ fs.mkdirSync(outputDir, { recursive: true });
+ const { audit } = runDemo(outputDir);
+ const outputPath = path.join(outputDir, 'demo.mp4');
+ const framesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lay-summary-video-'));
+ const frameCount = 120;
+ for (let i = 0; i < frameCount; i += 1) {
+ renderFrame(path.join(framesDir, `frame-${String(i).padStart(3, '0')}.ppm`), audit, i, frameCount);
+ }
+ const result = spawnSync('ffmpeg', ['-y', '-framerate', '30', '-i', path.join(framesDir, 'frame-%03d.ppm'), '-an', '-pix_fmt', 'yuv420p', outputPath], { encoding: 'utf8' });
+ fs.rmSync(framesDir, { recursive: true, force: true });
+ if (result.status !== 0) throw new Error(`ffmpeg failed:\n${result.stderr}`);
+ console.log(`Video written: ${outputPath}`);
+ return outputPath;
+}
+
+if (require.main === module) {
+ renderVideo();
+}
+
+module.exports = { renderVideo };
diff --git a/lay-summary-safety-guard/reports/demo.mp4 b/lay-summary-safety-guard/reports/demo.mp4
new file mode 100644
index 00000000..23a7a34f
Binary files /dev/null and b/lay-summary-safety-guard/reports/demo.mp4 differ
diff --git a/lay-summary-safety-guard/reports/rewrite-packet.md b/lay-summary-safety-guard/reports/rewrite-packet.md
new file mode 100644
index 00000000..3a50cef8
--- /dev/null
+++ b/lay-summary-safety-guard/reports/rewrite-packet.md
@@ -0,0 +1,44 @@
+# Layperson Summary Safety Guard
+
+Title: Early biomarker screen for inflammatory flare risk
+Decision: block-public-share
+Audience: patient and funder lay audience
+
+## Summary
+
+- Sentences checked: 3
+- Reading grade: 13.8
+- Evidence items: 3
+- Blockers: 4
+- Holds: 9
+
+## Findings
+
+- HOLD reading-level-too-high: Lay summary reads at grade 13.8, above the target grade 8.
+- HOLD jargon-not-explained: Jargon term needs plain-language explanation: cytokine
+- HOLD jargon-not-explained: Jargon term needs plain-language explanation: proteomic
+- HOLD jargon-not-explained: Jargon term needs plain-language explanation: longitudinal
+- BLOCKER benefit-claim-weakly-supported: A public-facing benefit claim lacks enough source-evidence overlap.
+- BLOCKER causal-overstatement: Lay summary uses stronger causal wording than the evidence supports.
+- HOLD uncertainty-missing: Benefit-oriented sentence should preserve uncertainty for a public audience.
+- BLOCKER medical-advice-risk: Lay summary risks sounding like clinical or medical advice.
+- HOLD uncertainty-missing: Benefit-oriented sentence should preserve uncertainty for a public audience.
+- BLOCKER causal-overstatement: Lay summary uses stronger causal wording than the evidence supports.
+- HOLD required-limitation-missing: Required limitation is missing: small observational cohort
+- HOLD required-limitation-missing: Required limitation is missing: external validation pending
+- HOLD required-limitation-missing: Required limitation is missing: not a treatment recommendation
+
+## Rewrite Actions
+
+- Remove or narrow unsupported benefit claims and attach a specific evidence span.
+- Replace causal verbs such as "proves" or "will prevent" with cautious language such as "suggests" or "may".
+- Remove treatment instructions and add a human-clinician disclaimer when health content is present.
+- Add uncertainty, study-size, and population context to benefit-oriented sentences.
+- Rewrite with shorter sentences and define scientific terms in everyday language.
+- Add the missing study limitations before the summary is shared with non-specialists.
+
+## Sentence Evidence Map
+
+- REWRITE evidence=E3 score=0.11: This proteomic cytokine breakthrough proves doctors can prevent inflammatory flares before they happen.
+- REWRITE evidence=E2 score=0.33: Patients should ask their doctor to replace treatment with this simple blood test because it works for everyone.
+- REWRITE evidence=E3 score=0.11: The longitudinal model identifies immune dysregulation and delivers actionable clinical certainty.
diff --git a/lay-summary-safety-guard/reports/summary-safety-audit.json b/lay-summary-safety-guard/reports/summary-safety-audit.json
new file mode 100644
index 00000000..e01db58f
--- /dev/null
+++ b/lay-summary-safety-guard/reports/summary-safety-audit.json
@@ -0,0 +1,189 @@
+{
+ "title": "Early biomarker screen for inflammatory flare risk",
+ "audience": {
+ "name": "patient and funder lay audience",
+ "maxGradeLevel": 8,
+ "jargonTerms": [
+ "cytokine",
+ "proteomic",
+ "longitudinal"
+ ]
+ },
+ "decision": "block-public-share",
+ "shareable": false,
+ "summary": {
+ "sentences": 3,
+ "readingGrade": 13.8,
+ "blockers": 4,
+ "holds": 9,
+ "evidenceItems": 3
+ },
+ "findings": [
+ {
+ "severity": "hold",
+ "code": "reading-level-too-high",
+ "message": "Lay summary reads at grade 13.8, above the target grade 8.",
+ "evidence": {
+ "grade": 13.8,
+ "targetGrade": 8
+ }
+ },
+ {
+ "severity": "hold",
+ "code": "jargon-not-explained",
+ "message": "Jargon term needs plain-language explanation: cytokine",
+ "evidence": {
+ "term": "cytokine"
+ }
+ },
+ {
+ "severity": "hold",
+ "code": "jargon-not-explained",
+ "message": "Jargon term needs plain-language explanation: proteomic",
+ "evidence": {
+ "term": "proteomic"
+ }
+ },
+ {
+ "severity": "hold",
+ "code": "jargon-not-explained",
+ "message": "Jargon term needs plain-language explanation: longitudinal",
+ "evidence": {
+ "term": "longitudinal"
+ }
+ },
+ {
+ "severity": "blocker",
+ "code": "benefit-claim-weakly-supported",
+ "message": "A public-facing benefit claim lacks enough source-evidence overlap.",
+ "evidence": {
+ "sentence": "This proteomic cytokine breakthrough proves doctors can prevent inflammatory flares before they happen.",
+ "bestEvidence": {
+ "id": "E3",
+ "score": 0.11,
+ "matchedTerms": [
+ "before"
+ ]
+ }
+ }
+ },
+ {
+ "severity": "blocker",
+ "code": "causal-overstatement",
+ "message": "Lay summary uses stronger causal wording than the evidence supports.",
+ "evidence": {
+ "sentence": "This proteomic cytokine breakthrough proves doctors can prevent inflammatory flares before they happen."
+ }
+ },
+ {
+ "severity": "hold",
+ "code": "uncertainty-missing",
+ "message": "Benefit-oriented sentence should preserve uncertainty for a public audience.",
+ "evidence": {
+ "sentence": "This proteomic cytokine breakthrough proves doctors can prevent inflammatory flares before they happen."
+ }
+ },
+ {
+ "severity": "blocker",
+ "code": "medical-advice-risk",
+ "message": "Lay summary risks sounding like clinical or medical advice.",
+ "evidence": {
+ "sentence": "Patients should ask their doctor to replace treatment with this simple blood test because it works for everyone."
+ }
+ },
+ {
+ "severity": "hold",
+ "code": "uncertainty-missing",
+ "message": "Benefit-oriented sentence should preserve uncertainty for a public audience.",
+ "evidence": {
+ "sentence": "Patients should ask their doctor to replace treatment with this simple blood test because it works for everyone."
+ }
+ },
+ {
+ "severity": "blocker",
+ "code": "causal-overstatement",
+ "message": "Lay summary uses stronger causal wording than the evidence supports.",
+ "evidence": {
+ "sentence": "The longitudinal model identifies immune dysregulation and delivers actionable clinical certainty."
+ }
+ },
+ {
+ "severity": "hold",
+ "code": "required-limitation-missing",
+ "message": "Required limitation is missing: small observational cohort",
+ "evidence": {
+ "limitation": "small observational cohort"
+ }
+ },
+ {
+ "severity": "hold",
+ "code": "required-limitation-missing",
+ "message": "Required limitation is missing: external validation pending",
+ "evidence": {
+ "limitation": "external validation pending"
+ }
+ },
+ {
+ "severity": "hold",
+ "code": "required-limitation-missing",
+ "message": "Required limitation is missing: not a treatment recommendation",
+ "evidence": {
+ "limitation": "not a treatment recommendation"
+ }
+ }
+ ],
+ "sentenceReports": [
+ {
+ "sentence": "This proteomic cytokine breakthrough proves doctors can prevent inflammatory flares before they happen.",
+ "bestEvidence": {
+ "id": "E3",
+ "score": 0.11,
+ "matchedTerms": [
+ "before"
+ ]
+ },
+ "findings": [
+ "benefit-claim-weakly-supported",
+ "causal-overstatement",
+ "uncertainty-missing"
+ ]
+ },
+ {
+ "sentence": "Patients should ask their doctor to replace treatment with this simple blood test because it works for everyone.",
+ "bestEvidence": {
+ "id": "E2",
+ "score": 0.33,
+ "matchedTerms": [
+ "patients",
+ "treatment",
+ "test"
+ ]
+ },
+ "findings": [
+ "medical-advice-risk",
+ "uncertainty-missing"
+ ]
+ },
+ {
+ "sentence": "The longitudinal model identifies immune dysregulation and delivers actionable clinical certainty.",
+ "bestEvidence": {
+ "id": "E3",
+ "score": 0.11,
+ "matchedTerms": [
+ "clinical"
+ ]
+ },
+ "findings": [
+ "causal-overstatement"
+ ]
+ }
+ ],
+ "rewriteActions": [
+ "Remove or narrow unsupported benefit claims and attach a specific evidence span.",
+ "Replace causal verbs such as \"proves\" or \"will prevent\" with cautious language such as \"suggests\" or \"may\".",
+ "Remove treatment instructions and add a human-clinician disclaimer when health content is present.",
+ "Add uncertainty, study-size, and population context to benefit-oriented sentences.",
+ "Rewrite with shorter sentences and define scientific terms in everyday language.",
+ "Add the missing study limitations before the summary is shared with non-specialists."
+ ]
+}
diff --git a/lay-summary-safety-guard/reports/summary.svg b/lay-summary-safety-guard/reports/summary.svg
new file mode 100644
index 00000000..0adf0311
--- /dev/null
+++ b/lay-summary-safety-guard/reports/summary.svg
@@ -0,0 +1,19 @@
+
+
diff --git a/lay-summary-safety-guard/sample-data.js b/lay-summary-safety-guard/sample-data.js
new file mode 100644
index 00000000..5cc63a79
--- /dev/null
+++ b/lay-summary-safety-guard/sample-data.js
@@ -0,0 +1,47 @@
+const unsafePacket = {
+ title: 'Early biomarker screen for inflammatory flare risk',
+ audience: {
+ name: 'patient and funder lay audience',
+ maxGradeLevel: 8,
+ jargonTerms: ['cytokine', 'proteomic', 'longitudinal'],
+ },
+ sourceEvidence: [
+ {
+ id: 'E1',
+ text: 'In a small observational cohort of 42 adults, cytokine panel changes were associated with later flare risk.',
+ terms: ['small', 'observational', 'cohort', 'cytokine', 'associated', 'flare', 'risk'],
+ },
+ {
+ id: 'E2',
+ text: 'The study did not test treatment decisions and did not include children or pregnant patients.',
+ terms: ['not', 'treatment', 'children', 'pregnant', 'patients', 'limitations'],
+ },
+ {
+ id: 'E3',
+ text: 'External validation is pending, and the authors recommend cautious interpretation before clinical use.',
+ terms: ['validation', 'pending', 'cautious', 'interpretation', 'clinical'],
+ },
+ ],
+ requiredLimitations: [
+ 'small observational cohort',
+ 'external validation pending',
+ 'not a treatment recommendation',
+ ],
+ summary:
+ 'This proteomic cytokine breakthrough proves doctors can prevent inflammatory flares before they happen. Patients should ask their doctor to replace treatment with this simple blood test because it works for everyone. The longitudinal model identifies immune dysregulation and delivers actionable clinical certainty.',
+};
+
+const safePacket = {
+ title: 'Early biomarker screen for inflammatory flare risk',
+ audience: {
+ name: 'patient and funder lay audience',
+ maxGradeLevel: 8,
+ jargonTerms: ['cytokine', 'proteomic', 'longitudinal'],
+ },
+ sourceEvidence: unsafePacket.sourceEvidence,
+ requiredLimitations: unsafePacket.requiredLimitations,
+ summary:
+ 'A small observational study found blood changes that may be linked with later flare risk. Outside validation is still pending. This is not a treatment recommendation, and patients should not change care based on this result alone.',
+};
+
+module.exports = { unsafePacket, safePacket };
diff --git a/lay-summary-safety-guard/test.js b/lay-summary-safety-guard/test.js
new file mode 100644
index 00000000..a1819e82
--- /dev/null
+++ b/lay-summary-safety-guard/test.js
@@ -0,0 +1,62 @@
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const {
+ evaluateSummary,
+ evidenceCoverage,
+ fleschKincaidGrade,
+ renderMarkdown,
+ renderSvg,
+ splitSentences,
+ tokenize,
+ writeReports,
+} = require('./index');
+const { safePacket, unsafePacket } = require('./sample-data');
+
+function runTests() {
+ assert.deepStrictEqual(tokenize('Blood-signal changes!'), ['blood-signal', 'changes']);
+ assert.strictEqual(splitSentences('One. Two? Three!').length, 3);
+ assert(fleschKincaidGrade('This is a short plain sentence.') < 8);
+
+ const coverage = evidenceCoverage('The cohort had cytokine flare risk signals.', unsafePacket.sourceEvidence);
+ assert.strictEqual(coverage.id, 'E1');
+ assert(coverage.score > 0.2);
+
+ const unsafeAudit = evaluateSummary(unsafePacket);
+ assert.strictEqual(unsafeAudit.decision, 'block-public-share');
+ assert.strictEqual(unsafeAudit.shareable, false);
+ const unsafeCodes = new Set(unsafeAudit.findings.map((finding) => finding.code));
+ assert(unsafeCodes.has('causal-overstatement'));
+ assert(unsafeCodes.has('medical-advice-risk'));
+ assert(unsafeCodes.has('uncertainty-missing'));
+ assert(unsafeCodes.has('jargon-not-explained'));
+ assert(unsafeCodes.has('required-limitation-missing'));
+
+ const safeAudit = evaluateSummary(safePacket);
+ assert.strictEqual(safeAudit.decision, 'approve-lay-summary');
+ assert.strictEqual(safeAudit.findings.length, 0);
+
+ const markdown = renderMarkdown(unsafeAudit);
+ assert(markdown.includes('Layperson Summary Safety Guard'));
+ assert(markdown.includes('block-public-share'));
+
+ const svg = renderSvg(unsafeAudit);
+ assert(svg.includes('