From e4911cc025778ded1bc0f5b4909cdc40dcce4b23 Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 23 May 2026 02:45:38 +0200 Subject: [PATCH] Add collaborative LaTeX macro safety guard --- README.md | 2 + .../README.md | 48 ++ .../demo.js | 27 ++ .../index.js | 447 +++++++++++++++++ .../package.json | 13 + .../reports/demo.json | 451 ++++++++++++++++++ .../reports/demo.md | 40 ++ .../reports/demo.mp4 | Bin 0 -> 34320 bytes .../reports/demo.svg | 43 ++ .../sample-data.js | 135 ++++++ .../scripts/render-demo-video.js | 85 ++++ .../test.js | 74 +++ 12 files changed, 1365 insertions(+) create mode 100644 collaborative-latex-macro-safety-guard/README.md create mode 100644 collaborative-latex-macro-safety-guard/demo.js create mode 100644 collaborative-latex-macro-safety-guard/index.js create mode 100644 collaborative-latex-macro-safety-guard/package.json create mode 100644 collaborative-latex-macro-safety-guard/reports/demo.json create mode 100644 collaborative-latex-macro-safety-guard/reports/demo.md create mode 100644 collaborative-latex-macro-safety-guard/reports/demo.mp4 create mode 100644 collaborative-latex-macro-safety-guard/reports/demo.svg create mode 100644 collaborative-latex-macro-safety-guard/sample-data.js create mode 100644 collaborative-latex-macro-safety-guard/scripts/render-demo-video.js create mode 100644 collaborative-latex-macro-safety-guard/test.js diff --git a/README.md b/README.md index d338cf68..13689437 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # deepevents.ai deepevents.ai main codebase + +- [Collaborative LaTeX macro safety guard](collaborative-latex-macro-safety-guard/README.md) diff --git a/collaborative-latex-macro-safety-guard/README.md b/collaborative-latex-macro-safety-guard/README.md new file mode 100644 index 00000000..44c63258 --- /dev/null +++ b/collaborative-latex-macro-safety-guard/README.md @@ -0,0 +1,48 @@ +# Collaborative LaTeX Macro Safety Guard + +This module is a self-contained real-time collaborative research editor slice for SCIBASE.AI issue #12. It validates synchronized Markdown and LaTeX editor packets before live preview or named-version export. + +The guard uses synthetic data only. It does not connect to live users, private manuscripts, browser sessions, credentials, or external services. + +## Scope + +- Detect conflicting collaborator macro definitions before broadcast. +- Block unsafe macro commands such as raw HTML helpers, external links, and image includes unless a trusted render packet is explicitly reviewed. +- Detect recursive macro expansion cycles that can hang live preview or export rendering. +- Block raw active HTML and unapproved external render resources inside Markdown/LaTeX blocks. +- Hold locked sections when unapproved or external-resource macro changes affect the render packet. +- Emit deterministic remediation actions for live preview, trusted KaTeX rendering, and named-version export gates. + +## Requirement Mapping + +| Issue #12 area | Implementation | +| --- | --- | +| Rich scientific formatting | Macro, equation, Markdown, and external render-resource checks | +| Real-time collaboration | Multi-user macro conflict and shared render packet validation | +| Locking/unlock modes | Locked section render holds for risky macro packets | +| Version history and autosave | Named-version export decisions and reviewer remediation packets | + +## Files + +- `index.js` - dependency-free evaluator plus Markdown/SVG report renderers. +- `sample-data.js` - synthetic ready, blocked, and needs-review editor packets. +- `test.js` - Node assertion coverage for guard decisions and report renderers. +- `demo.js` - writes deterministic JSON, Markdown, and SVG reviewer artifacts. +- `scripts/render-demo-video.js` - optional ffmpeg-based MP4 renderer. +- `reports/` - generated reviewer artifacts. + +## Validation + +```bash +npm run check +npm test +npm run demo +``` + +Optional video render when ffmpeg is available: + +```bash +npm run demo:video +``` + +The included demo artifacts are deterministic and based only on the synthetic fixtures in `sample-data.js`. diff --git a/collaborative-latex-macro-safety-guard/demo.js b/collaborative-latex-macro-safety-guard/demo.js new file mode 100644 index 00000000..d6b4fda9 --- /dev/null +++ b/collaborative-latex-macro-safety-guard/demo.js @@ -0,0 +1,27 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { + createGuardReport, + renderMarkdown, + renderSvg, +} = require("./index"); +const { samplePackets } = require("./sample-data"); + +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); + +const report = createGuardReport(samplePackets); +const jsonPath = path.join(reportsDir, "demo.json"); +const markdownPath = path.join(reportsDir, "demo.md"); +const svgPath = path.join(reportsDir, "demo.svg"); + +fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`); +fs.writeFileSync(markdownPath, renderMarkdown(report)); +fs.writeFileSync(svgPath, renderSvg(report)); + +console.log(`Wrote ${path.relative(process.cwd(), jsonPath)}`); +console.log(`Wrote ${path.relative(process.cwd(), markdownPath)}`); +console.log(`Wrote ${path.relative(process.cwd(), svgPath)}`); +console.log( + `Ready=${report.totals.ready} NeedsReview=${report.totals.needsReview} Blocked=${report.totals.blocked}`, +); diff --git a/collaborative-latex-macro-safety-guard/index.js b/collaborative-latex-macro-safety-guard/index.js new file mode 100644 index 00000000..4f922c57 --- /dev/null +++ b/collaborative-latex-macro-safety-guard/index.js @@ -0,0 +1,447 @@ +const DEFAULT_POLICY = Object.freeze({ + generatedAt: "2026-05-22T00:00:00.000Z", + allowedExternalHosts: ["doi.org", "api.datacite.org"], + unsafeMacroCommands: [ + "\\href", + "\\includegraphics", + "\\htmlClass", + "\\htmlId", + "\\htmlStyle", + "\\url", + ], + rawHtmlPattern: /<\s*(script|iframe|object|embed|link|meta|style)\b/i, + externalUrlPattern: /https?:\/\/([^)\s"']+)/gi, + maxExpansionDepth: 8, +}); + +const REQUIREMENT_MAP = Object.freeze({ + scientific_formatting: + "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + realtime_collaboration: + "Real-Time Collaboration: multi-user edits with live render safety", + locking: + "Real-Time Collaboration: controlled sections and lock/unlock modes", + version_history: + "Version History & Autosave: safe named-version export", +}); + +function evaluateMacroPacket(packet, policy = DEFAULT_POLICY) { + assertPacket(packet); + + const checks = []; + const issues = []; + const addCheck = (code, passed, detail) => { + checks.push({ code, passed, detail }); + }; + const addIssue = ( + severity, + code, + message, + requirement, + remediation, + target, + ) => { + issues.push({ + severity, + code, + message, + requirement, + remediation, + target, + }); + }; + + const macroGroups = groupMacros(packet.macros); + checkMacroConflicts(macroGroups, addCheck, addIssue); + checkUnsafeMacroCommands(packet, policy, addCheck, addIssue); + checkMacroRecursion(packet, policy, addCheck, addIssue); + checkBlockContent(packet, policy, addCheck, addIssue); + checkLockedSection(packet, addCheck, addIssue); + + const blockingIssues = issues.filter((issue) => issue.severity === "error"); + const warnings = issues.filter((issue) => issue.severity === "warning"); + const status = + blockingIssues.length > 0 + ? "blocked" + : warnings.length > 0 + ? "needs_review" + : "ready"; + + return { + generatedAt: policy.generatedAt, + packetId: packet.packetId, + documentId: packet.documentId, + namedVersion: packet.namedVersion, + sectionId: packet.sectionId, + status, + summary: { + macros: packet.macros.length, + blocks: packet.blocks.length, + checks: checks.length, + passedChecks: checks.filter((check) => check.passed).length, + blockingIssues: blockingIssues.length, + warnings: warnings.length, + }, + checks, + issues: issues.sort(compareIssues), + renderDecision: buildRenderDecision(status, issues), + requirementCoverage: REQUIREMENT_MAP, + }; +} + +function createGuardReport(packets, policy = DEFAULT_POLICY) { + const evaluations = packets + .map((packet) => evaluateMacroPacket(packet, policy)) + .sort((a, b) => a.packetId.localeCompare(b.packetId)); + + return { + generatedAt: policy.generatedAt, + guard: "collaborative-latex-macro-safety-guard", + issue: "SCIBASE-AI/SCIBASE.AI#12", + totals: { + packets: evaluations.length, + ready: evaluations.filter((item) => item.status === "ready").length, + needsReview: evaluations.filter((item) => item.status === "needs_review") + .length, + blocked: evaluations.filter((item) => item.status === "blocked").length, + blockingIssues: evaluations.reduce( + (sum, item) => sum + item.summary.blockingIssues, + 0, + ), + warnings: evaluations.reduce((sum, item) => sum + item.summary.warnings, 0), + }, + evaluations, + }; +} + +function renderMarkdown(report) { + const rows = report.evaluations + .map( + (item) => + `| ${item.packetId} | ${item.namedVersion} | ${item.status} | ${item.summary.blockingIssues} | ${item.summary.warnings} |`, + ) + .join("\n"); + const issueRows = + report.evaluations + .flatMap((item) => + item.issues.map( + (issue) => + `- ${item.packetId} ${issue.code} (${issue.target}): ${issue.remediation}`, + ), + ) + .join("\n") || "- No remediation required."; + + return `# Collaborative LaTeX Macro Safety Guard + +Synthetic reviewer packet for ${report.issue}. + +## Summary + +| Packet | Named version | Status | Blocking issues | Warnings | +| --- | --- | --- | ---: | ---: | +${rows} + +## Gate Totals + +- Ready packets: ${report.totals.ready} +- Needs review: ${report.totals.needsReview} +- Blocked packets: ${report.totals.blocked} +- Blocking issues: ${report.totals.blockingIssues} +- Warnings: ${report.totals.warnings} + +## Remediation Queue + +${issueRows} + +## Requirement Mapping + +- Macro and equation safety: ${REQUIREMENT_MAP.scientific_formatting} +- Multi-user macro edit checks: ${REQUIREMENT_MAP.realtime_collaboration} +- Locked section render holds: ${REQUIREMENT_MAP.locking} +- Named-version export gate: ${REQUIREMENT_MAP.version_history} +`; +} + +function renderSvg(report) { + const width = 960; + const height = 540; + const cardWidth = 280; + const colors = { + ready: "#157f57", + needs_review: "#b7791f", + blocked: "#b42318", + }; + const cards = report.evaluations + .map((item, index) => { + const x = 40 + index * (cardWidth + 20); + const color = colors[item.status]; + return ` + + + ${escapeXml(item.packetId)} + ${escapeXml(item.sectionId)} + ${escapeXml(item.status)} + Checks: ${item.summary.passedChecks}/${item.summary.checks} + Blocks: ${item.summary.blockingIssues} +`; + }) + .join("\n"); + + return ` +Collaborative LaTeX macro safety guard +Deterministic status report for real-time editor macro render packets. + +Collaborative LaTeX Macro Safety Guard +Blocks unsafe macros, raw HTML, recursive edits, and locked-section render risks. + + + Ready ${report.totals.ready} + + Needs review ${report.totals.needsReview} + + Blocked ${report.totals.blocked} + +${cards} +Issue mapping: real-time collaborative editor, Markdown/LaTeX rendering, live collaboration, versioned export. + +`; +} + +function checkMacroConflicts(macroGroups, addCheck, addIssue) { + for (const [name, macros] of [...macroGroups.entries()].sort()) { + const bodies = new Set(macros.map((macro) => macro.body)); + const passed = bodies.size === 1; + addCheck("MACRO_CONFLICT", passed, `${name}; definitions=${bodies.size}`); + if (!passed) { + addIssue( + "error", + "MACRO_DEFINITION_CONFLICT", + "Collaborators submitted conflicting bodies for the same macro.", + REQUIREMENT_MAP.realtime_collaboration, + "Resolve the macro conflict before broadcasting the shared render state.", + name, + ); + } + } +} + +function checkUnsafeMacroCommands(packet, policy, addCheck, addIssue) { + for (const macro of packet.macros) { + const used = policy.unsafeMacroCommands.filter((command) => + macro.body.includes(command), + ); + const trusted = packet.trustedRenderMode && Boolean(macro.trustJustification); + const passed = used.length === 0 || trusted; + addCheck( + "MACRO_TRUST_COMMANDS", + passed, + `${macro.name}; commands=${used.join(",") || "none"}`, + ); + if (used.length > 0) { + addIssue( + trusted ? "warning" : "error", + trusted ? "TRUSTED_MACRO_REVIEW_REQUIRED" : "UNSAFE_MACRO_COMMAND", + "Macro body uses a command that can escape plain equation rendering.", + REQUIREMENT_MAP.scientific_formatting, + trusted + ? "Keep the trusted-render justification with reviewer signoff." + : "Remove the unsafe command or move it behind an approved trusted-render packet.", + macro.name, + ); + } + + if (!macro.approved) { + addIssue( + "error", + "UNAPPROVED_MACRO_EDIT", + "Macro edit has not been approved for the shared named version.", + REQUIREMENT_MAP.version_history, + "Require approval before autosave promotes this macro into the named version.", + macro.name, + ); + } + } +} + +function checkMacroRecursion(packet, policy, addCheck, addIssue) { + const macroMap = new Map(packet.macros.map((macro) => [macro.name, macro])); + for (const macro of packet.macros) { + const cycle = findMacroCycle(macro, macroMap, policy.maxExpansionDepth); + const passed = cycle.length === 0; + addCheck("MACRO_RECURSION", passed, `${macro.name}; cycle=${cycle.join(" -> ") || "none"}`); + if (!passed) { + addIssue( + "error", + "RECURSIVE_MACRO_EXPANSION", + "Macro expansion forms a recursive cycle.", + REQUIREMENT_MAP.scientific_formatting, + "Break the macro cycle before live preview or export rendering.", + macro.name, + ); + } + } +} + +function checkBlockContent(packet, policy, addCheck, addIssue) { + for (const block of packet.blocks) { + const rawHtml = policy.rawHtmlPattern.test(block.text); + addCheck("RAW_HTML_BLOCK", !rawHtml, `${block.blockId}; rawHtml=${rawHtml}`); + if (rawHtml) { + addIssue( + "error", + "RAW_HTML_IN_COLLAB_BLOCK", + "Collaborative block contains raw active HTML.", + REQUIREMENT_MAP.scientific_formatting, + "Strip active HTML before live preview or named-version export.", + block.blockId, + ); + } + + const externalHosts = findExternalHosts(block.text, policy); + const disallowed = externalHosts.filter( + (host) => !policy.allowedExternalHosts.includes(host), + ); + addCheck( + "EXTERNAL_RENDER_RESOURCE", + disallowed.length === 0, + `${block.blockId}; hosts=${externalHosts.join(",") || "none"}`, + ); + if (disallowed.length > 0) { + addIssue( + "error", + "EXTERNAL_RENDER_RESOURCE_BLOCKED", + "Collaborative render block references an unapproved external resource.", + REQUIREMENT_MAP.scientific_formatting, + "Replace external resources with repository-managed figure or citation assets.", + block.blockId, + ); + } + } +} + +function checkLockedSection(packet, addCheck, addIssue) { + const lockedBlocks = packet.blocks.filter((block) => block.locked); + const hasRenderRisk = packet.macros.some( + (macro) => !macro.approved || macro.body.includes("\\href"), + ); + const passed = lockedBlocks.length === 0 || !hasRenderRisk; + addCheck( + "LOCKED_SECTION_RENDER_HOLD", + passed, + `lockedBlocks=${lockedBlocks.length}; renderRisk=${hasRenderRisk}`, + ); + if (!passed) { + addIssue( + "error", + "LOCKED_SECTION_RENDER_HOLD", + "Locked section contains unapproved or external-resource macro changes.", + REQUIREMENT_MAP.locking, + "Hold render/export until the section owner approves the macro packet.", + packet.sectionId, + ); + } +} + +function buildRenderDecision(status, issues) { + const blocking = issues.filter((issue) => issue.severity === "error"); + const warnings = issues.filter((issue) => issue.severity === "warning"); + return { + allowLivePreview: blocking.length === 0, + allowNamedVersionExport: blocking.length === 0, + allowTrustedKatexRender: blocking.length === 0 && warnings.length === 0, + requiresReviewerSignoff: warnings.length > 0, + remediationChecklist: issues.map((issue) => ({ + code: issue.code, + target: issue.target, + action: issue.remediation, + })), + status, + }; +} + +function groupMacros(macros) { + const groups = new Map(); + for (const macro of macros) { + if (!groups.has(macro.name)) { + groups.set(macro.name, []); + } + groups.get(macro.name).push(macro); + } + return groups; +} + +function findMacroCycle(startMacro, macroMap, maxDepth) { + const visited = []; + let current = startMacro; + for (let depth = 0; depth <= maxDepth; depth += 1) { + visited.push(current.name); + const nextName = [...macroMap.keys()].find((name) => + current.body.includes(name), + ); + if (!nextName) { + return []; + } + if (visited.includes(nextName)) { + return [...visited, nextName]; + } + current = macroMap.get(nextName); + } + return visited; +} + +function findExternalHosts(text, policy) { + const hosts = []; + let match = policy.externalUrlPattern.exec(text); + while (match) { + hosts.push(match[1].toLowerCase()); + match = policy.externalUrlPattern.exec(text); + } + policy.externalUrlPattern.lastIndex = 0; + return [...new Set(hosts)].sort(); +} + +function assertPacket(packet) { + if (!packet || typeof packet !== "object") { + throw new TypeError("packet must be an object"); + } + for (const field of ["packetId", "documentId", "namedVersion", "sectionId"]) { + if (!packet[field]) { + throw new TypeError(`packet.${field} is required`); + } + } + if (!Array.isArray(packet.macros)) { + throw new TypeError("packet.macros must be an array"); + } + if (!Array.isArray(packet.blocks)) { + throw new TypeError("packet.blocks must be an array"); + } +} + +function compareIssues(a, b) { + return ( + severityRank(a.severity) - severityRank(b.severity) || + a.code.localeCompare(b.code) || + a.target.localeCompare(b.target) + ); +} + +function severityRank(severity) { + return severity === "error" ? 0 : 1; +} + +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +module.exports = { + DEFAULT_POLICY, + REQUIREMENT_MAP, + createGuardReport, + evaluateMacroPacket, + renderMarkdown, + renderSvg, +}; diff --git a/collaborative-latex-macro-safety-guard/package.json b/collaborative-latex-macro-safety-guard/package.json new file mode 100644 index 00000000..85bdf58e --- /dev/null +++ b/collaborative-latex-macro-safety-guard/package.json @@ -0,0 +1,13 @@ +{ + "name": "collaborative-latex-macro-safety-guard", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "description": "Synthetic collaborative LaTeX macro safety guard for SCIBASE real-time editor packets.", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check test.js && node --check scripts/render-demo-video.js", + "test": "node test.js", + "demo": "node demo.js", + "demo:video": "node scripts/render-demo-video.js" + } +} diff --git a/collaborative-latex-macro-safety-guard/reports/demo.json b/collaborative-latex-macro-safety-guard/reports/demo.json new file mode 100644 index 00000000..92bc4a82 --- /dev/null +++ b/collaborative-latex-macro-safety-guard/reports/demo.json @@ -0,0 +1,451 @@ +{ + "generatedAt": "2026-05-22T00:00:00.000Z", + "guard": "collaborative-latex-macro-safety-guard", + "issue": "SCIBASE-AI/SCIBASE.AI#12", + "totals": { + "packets": 3, + "ready": 1, + "needsReview": 1, + "blocked": 1, + "blockingIssues": 10, + "warnings": 1 + }, + "evaluations": [ + { + "generatedAt": "2026-05-22T00:00:00.000Z", + "packetId": "packet-safe-methods-v4", + "documentId": "shared-manuscript/neuro-methods", + "namedVersion": "Manuscript v4 Methods", + "sectionId": "methods-statistical-model", + "status": "ready", + "summary": { + "macros": 2, + "blocks": 2, + "checks": 11, + "passedChecks": 11, + "blockingIssues": 0, + "warnings": 0 + }, + "checks": [ + { + "code": "MACRO_CONFLICT", + "passed": true, + "detail": "\\ci; definitions=1" + }, + { + "code": "MACRO_CONFLICT", + "passed": true, + "detail": "\\mean; definitions=1" + }, + { + "code": "MACRO_TRUST_COMMANDS", + "passed": true, + "detail": "\\mean; commands=none" + }, + { + "code": "MACRO_TRUST_COMMANDS", + "passed": true, + "detail": "\\ci; commands=none" + }, + { + "code": "MACRO_RECURSION", + "passed": true, + "detail": "\\mean; cycle=none" + }, + { + "code": "MACRO_RECURSION", + "passed": true, + "detail": "\\ci; cycle=none" + }, + { + "code": "RAW_HTML_BLOCK", + "passed": true, + "detail": "eq-primary-outcome; rawHtml=false" + }, + { + "code": "EXTERNAL_RENDER_RESOURCE", + "passed": true, + "detail": "eq-primary-outcome; hosts=none" + }, + { + "code": "RAW_HTML_BLOCK", + "passed": true, + "detail": "md-summary; rawHtml=false" + }, + { + "code": "EXTERNAL_RENDER_RESOURCE", + "passed": true, + "detail": "md-summary; hosts=none" + }, + { + "code": "LOCKED_SECTION_RENDER_HOLD", + "passed": true, + "detail": "lockedBlocks=0; renderRisk=false" + } + ], + "issues": [], + "renderDecision": { + "allowLivePreview": true, + "allowNamedVersionExport": true, + "allowTrustedKatexRender": true, + "requiresReviewerSignoff": false, + "remediationChecklist": [], + "status": "ready" + }, + "requirementCoverage": { + "scientific_formatting": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "realtime_collaboration": "Real-Time Collaboration: multi-user edits with live render safety", + "locking": "Real-Time Collaboration: controlled sections and lock/unlock modes", + "version_history": "Version History & Autosave: safe named-version export" + } + }, + { + "generatedAt": "2026-05-22T00:00:00.000Z", + "packetId": "packet-trusted-supplement-v2", + "documentId": "shared-manuscript/supplement", + "namedVersion": "Supplement Draft", + "sectionId": "appendix-derivations", + "status": "needs_review", + "summary": { + "macros": 1, + "blocks": 1, + "checks": 6, + "passedChecks": 6, + "blockingIssues": 0, + "warnings": 1 + }, + "checks": [ + { + "code": "MACRO_CONFLICT", + "passed": true, + "detail": "\\annotate; definitions=1" + }, + { + "code": "MACRO_TRUST_COMMANDS", + "passed": true, + "detail": "\\annotate; commands=\\htmlId" + }, + { + "code": "MACRO_RECURSION", + "passed": true, + "detail": "\\annotate; cycle=none" + }, + { + "code": "RAW_HTML_BLOCK", + "passed": true, + "detail": "appendix-equation; rawHtml=false" + }, + { + "code": "EXTERNAL_RENDER_RESOURCE", + "passed": true, + "detail": "appendix-equation; hosts=none" + }, + { + "code": "LOCKED_SECTION_RENDER_HOLD", + "passed": true, + "detail": "lockedBlocks=0; renderRisk=false" + } + ], + "issues": [ + { + "severity": "warning", + "code": "TRUSTED_MACRO_REVIEW_REQUIRED", + "message": "Macro body uses a command that can escape plain equation rendering.", + "requirement": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "remediation": "Keep the trusted-render justification with reviewer signoff.", + "target": "\\annotate" + } + ], + "renderDecision": { + "allowLivePreview": true, + "allowNamedVersionExport": true, + "allowTrustedKatexRender": false, + "requiresReviewerSignoff": true, + "remediationChecklist": [ + { + "code": "TRUSTED_MACRO_REVIEW_REQUIRED", + "target": "\\annotate", + "action": "Keep the trusted-render justification with reviewer signoff." + } + ], + "status": "needs_review" + }, + "requirementCoverage": { + "scientific_formatting": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "realtime_collaboration": "Real-Time Collaboration: multi-user edits with live render safety", + "locking": "Real-Time Collaboration: controlled sections and lock/unlock modes", + "version_history": "Version History & Autosave: safe named-version export" + } + }, + { + "generatedAt": "2026-05-22T00:00:00.000Z", + "packetId": "packet-unsafe-results-v8", + "documentId": "shared-manuscript/results-live", + "namedVersion": "Final Submission", + "sectionId": "results-figures", + "status": "blocked", + "summary": { + "macros": 5, + "blocks": 3, + "checks": 21, + "passedChecks": 12, + "blockingIssues": 10, + "warnings": 0 + }, + "checks": [ + { + "code": "MACRO_CONFLICT", + "passed": true, + "detail": "\\dataset; definitions=1" + }, + { + "code": "MACRO_CONFLICT", + "passed": false, + "detail": "\\hazard; definitions=2" + }, + { + "code": "MACRO_CONFLICT", + "passed": true, + "detail": "\\loopA; definitions=1" + }, + { + "code": "MACRO_CONFLICT", + "passed": true, + "detail": "\\loopB; definitions=1" + }, + { + "code": "MACRO_TRUST_COMMANDS", + "passed": false, + "detail": "\\dataset; commands=\\href" + }, + { + "code": "MACRO_TRUST_COMMANDS", + "passed": true, + "detail": "\\loopA; commands=none" + }, + { + "code": "MACRO_TRUST_COMMANDS", + "passed": true, + "detail": "\\loopB; commands=none" + }, + { + "code": "MACRO_TRUST_COMMANDS", + "passed": false, + "detail": "\\hazard; commands=\\htmlClass" + }, + { + "code": "MACRO_TRUST_COMMANDS", + "passed": true, + "detail": "\\hazard; commands=none" + }, + { + "code": "MACRO_RECURSION", + "passed": true, + "detail": "\\dataset; cycle=none" + }, + { + "code": "MACRO_RECURSION", + "passed": false, + "detail": "\\loopA; cycle=\\loopA -> \\loopB -> \\loopA" + }, + { + "code": "MACRO_RECURSION", + "passed": false, + "detail": "\\loopB; cycle=\\loopB -> \\loopA -> \\loopB" + }, + { + "code": "MACRO_RECURSION", + "passed": true, + "detail": "\\hazard; cycle=none" + }, + { + "code": "MACRO_RECURSION", + "passed": true, + "detail": "\\hazard; cycle=none" + }, + { + "code": "RAW_HTML_BLOCK", + "passed": true, + "detail": "eq-hazard-model; rawHtml=false" + }, + { + "code": "EXTERNAL_RENDER_RESOURCE", + "passed": true, + "detail": "eq-hazard-model; hosts=none" + }, + { + "code": "RAW_HTML_BLOCK", + "passed": true, + "detail": "figure-caption; rawHtml=false" + }, + { + "code": "EXTERNAL_RENDER_RESOURCE", + "passed": false, + "detail": "figure-caption; hosts=private.example.invalid/figure.png" + }, + { + "code": "RAW_HTML_BLOCK", + "passed": false, + "detail": "unsafe-inline-html; rawHtml=true" + }, + { + "code": "EXTERNAL_RENDER_RESOURCE", + "passed": false, + "detail": "unsafe-inline-html; hosts=private.example.invalid/embed" + }, + { + "code": "LOCKED_SECTION_RENDER_HOLD", + "passed": false, + "detail": "lockedBlocks=2; renderRisk=true" + } + ], + "issues": [ + { + "severity": "error", + "code": "EXTERNAL_RENDER_RESOURCE_BLOCKED", + "message": "Collaborative render block references an unapproved external resource.", + "requirement": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "remediation": "Replace external resources with repository-managed figure or citation assets.", + "target": "figure-caption" + }, + { + "severity": "error", + "code": "EXTERNAL_RENDER_RESOURCE_BLOCKED", + "message": "Collaborative render block references an unapproved external resource.", + "requirement": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "remediation": "Replace external resources with repository-managed figure or citation assets.", + "target": "unsafe-inline-html" + }, + { + "severity": "error", + "code": "LOCKED_SECTION_RENDER_HOLD", + "message": "Locked section contains unapproved or external-resource macro changes.", + "requirement": "Real-Time Collaboration: controlled sections and lock/unlock modes", + "remediation": "Hold render/export until the section owner approves the macro packet.", + "target": "results-figures" + }, + { + "severity": "error", + "code": "MACRO_DEFINITION_CONFLICT", + "message": "Collaborators submitted conflicting bodies for the same macro.", + "requirement": "Real-Time Collaboration: multi-user edits with live render safety", + "remediation": "Resolve the macro conflict before broadcasting the shared render state.", + "target": "\\hazard" + }, + { + "severity": "error", + "code": "RAW_HTML_IN_COLLAB_BLOCK", + "message": "Collaborative block contains raw active HTML.", + "requirement": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "remediation": "Strip active HTML before live preview or named-version export.", + "target": "unsafe-inline-html" + }, + { + "severity": "error", + "code": "RECURSIVE_MACRO_EXPANSION", + "message": "Macro expansion forms a recursive cycle.", + "requirement": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "remediation": "Break the macro cycle before live preview or export rendering.", + "target": "\\loopA" + }, + { + "severity": "error", + "code": "RECURSIVE_MACRO_EXPANSION", + "message": "Macro expansion forms a recursive cycle.", + "requirement": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "remediation": "Break the macro cycle before live preview or export rendering.", + "target": "\\loopB" + }, + { + "severity": "error", + "code": "UNAPPROVED_MACRO_EDIT", + "message": "Macro edit has not been approved for the shared named version.", + "requirement": "Version History & Autosave: safe named-version export", + "remediation": "Require approval before autosave promotes this macro into the named version.", + "target": "\\dataset" + }, + { + "severity": "error", + "code": "UNSAFE_MACRO_COMMAND", + "message": "Macro body uses a command that can escape plain equation rendering.", + "requirement": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "remediation": "Remove the unsafe command or move it behind an approved trusted-render packet.", + "target": "\\dataset" + }, + { + "severity": "error", + "code": "UNSAFE_MACRO_COMMAND", + "message": "Macro body uses a command that can escape plain equation rendering.", + "requirement": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "remediation": "Remove the unsafe command or move it behind an approved trusted-render packet.", + "target": "\\hazard" + } + ], + "renderDecision": { + "allowLivePreview": false, + "allowNamedVersionExport": false, + "allowTrustedKatexRender": false, + "requiresReviewerSignoff": false, + "remediationChecklist": [ + { + "code": "EXTERNAL_RENDER_RESOURCE_BLOCKED", + "target": "figure-caption", + "action": "Replace external resources with repository-managed figure or citation assets." + }, + { + "code": "EXTERNAL_RENDER_RESOURCE_BLOCKED", + "target": "unsafe-inline-html", + "action": "Replace external resources with repository-managed figure or citation assets." + }, + { + "code": "LOCKED_SECTION_RENDER_HOLD", + "target": "results-figures", + "action": "Hold render/export until the section owner approves the macro packet." + }, + { + "code": "MACRO_DEFINITION_CONFLICT", + "target": "\\hazard", + "action": "Resolve the macro conflict before broadcasting the shared render state." + }, + { + "code": "RAW_HTML_IN_COLLAB_BLOCK", + "target": "unsafe-inline-html", + "action": "Strip active HTML before live preview or named-version export." + }, + { + "code": "RECURSIVE_MACRO_EXPANSION", + "target": "\\loopA", + "action": "Break the macro cycle before live preview or export rendering." + }, + { + "code": "RECURSIVE_MACRO_EXPANSION", + "target": "\\loopB", + "action": "Break the macro cycle before live preview or export rendering." + }, + { + "code": "UNAPPROVED_MACRO_EDIT", + "target": "\\dataset", + "action": "Require approval before autosave promotes this macro into the named version." + }, + { + "code": "UNSAFE_MACRO_COMMAND", + "target": "\\dataset", + "action": "Remove the unsafe command or move it behind an approved trusted-render packet." + }, + { + "code": "UNSAFE_MACRO_COMMAND", + "target": "\\hazard", + "action": "Remove the unsafe command or move it behind an approved trusted-render packet." + } + ], + "status": "blocked" + }, + "requirementCoverage": { + "scientific_formatting": "Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references", + "realtime_collaboration": "Real-Time Collaboration: multi-user edits with live render safety", + "locking": "Real-Time Collaboration: controlled sections and lock/unlock modes", + "version_history": "Version History & Autosave: safe named-version export" + } + } + ] +} diff --git a/collaborative-latex-macro-safety-guard/reports/demo.md b/collaborative-latex-macro-safety-guard/reports/demo.md new file mode 100644 index 00000000..543689b8 --- /dev/null +++ b/collaborative-latex-macro-safety-guard/reports/demo.md @@ -0,0 +1,40 @@ +# Collaborative LaTeX Macro Safety Guard + +Synthetic reviewer packet for SCIBASE-AI/SCIBASE.AI#12. + +## Summary + +| Packet | Named version | Status | Blocking issues | Warnings | +| --- | --- | --- | ---: | ---: | +| packet-safe-methods-v4 | Manuscript v4 Methods | ready | 0 | 0 | +| packet-trusted-supplement-v2 | Supplement Draft | needs_review | 0 | 1 | +| packet-unsafe-results-v8 | Final Submission | blocked | 10 | 0 | + +## Gate Totals + +- Ready packets: 1 +- Needs review: 1 +- Blocked packets: 1 +- Blocking issues: 10 +- Warnings: 1 + +## Remediation Queue + +- packet-trusted-supplement-v2 TRUSTED_MACRO_REVIEW_REQUIRED (\annotate): Keep the trusted-render justification with reviewer signoff. +- packet-unsafe-results-v8 EXTERNAL_RENDER_RESOURCE_BLOCKED (figure-caption): Replace external resources with repository-managed figure or citation assets. +- packet-unsafe-results-v8 EXTERNAL_RENDER_RESOURCE_BLOCKED (unsafe-inline-html): Replace external resources with repository-managed figure or citation assets. +- packet-unsafe-results-v8 LOCKED_SECTION_RENDER_HOLD (results-figures): Hold render/export until the section owner approves the macro packet. +- packet-unsafe-results-v8 MACRO_DEFINITION_CONFLICT (\hazard): Resolve the macro conflict before broadcasting the shared render state. +- packet-unsafe-results-v8 RAW_HTML_IN_COLLAB_BLOCK (unsafe-inline-html): Strip active HTML before live preview or named-version export. +- packet-unsafe-results-v8 RECURSIVE_MACRO_EXPANSION (\loopA): Break the macro cycle before live preview or export rendering. +- packet-unsafe-results-v8 RECURSIVE_MACRO_EXPANSION (\loopB): Break the macro cycle before live preview or export rendering. +- packet-unsafe-results-v8 UNAPPROVED_MACRO_EDIT (\dataset): Require approval before autosave promotes this macro into the named version. +- packet-unsafe-results-v8 UNSAFE_MACRO_COMMAND (\dataset): Remove the unsafe command or move it behind an approved trusted-render packet. +- packet-unsafe-results-v8 UNSAFE_MACRO_COMMAND (\hazard): Remove the unsafe command or move it behind an approved trusted-render packet. + +## Requirement Mapping + +- Macro and equation safety: Rich Scientific Formatting: Markdown, LaTeX, equations, and cross references +- Multi-user macro edit checks: Real-Time Collaboration: multi-user edits with live render safety +- Locked section render holds: Real-Time Collaboration: controlled sections and lock/unlock modes +- Named-version export gate: Version History & Autosave: safe named-version export diff --git a/collaborative-latex-macro-safety-guard/reports/demo.mp4 b/collaborative-latex-macro-safety-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..a60a5e91591e6b57ca245c9687b01c543ccc2050 GIT binary patch literal 34320 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 literal 0 HcmV?d00001 diff --git a/collaborative-latex-macro-safety-guard/reports/demo.svg b/collaborative-latex-macro-safety-guard/reports/demo.svg new file mode 100644 index 00000000..969d7c49 --- /dev/null +++ b/collaborative-latex-macro-safety-guard/reports/demo.svg @@ -0,0 +1,43 @@ + +Collaborative LaTeX macro safety guard +Deterministic status report for real-time editor macro render packets. + +Collaborative LaTeX Macro Safety Guard +Blocks unsafe macros, raw HTML, recursive edits, and locked-section render risks. + + + Ready 1 + + Needs review 1 + + Blocked 1 + + + + + packet-safe-methods-v4 + methods-statistical-model + ready + Checks: 11/11 + Blocks: 0 + + + + + packet-trusted-supplement-v2 + appendix-derivations + needs_review + Checks: 6/6 + Blocks: 0 + + + + + packet-unsafe-results-v8 + results-figures + blocked + Checks: 12/21 + Blocks: 10 + +Issue mapping: real-time collaborative editor, Markdown/LaTeX rendering, live collaboration, versioned export. + diff --git a/collaborative-latex-macro-safety-guard/sample-data.js b/collaborative-latex-macro-safety-guard/sample-data.js new file mode 100644 index 00000000..69c385a6 --- /dev/null +++ b/collaborative-latex-macro-safety-guard/sample-data.js @@ -0,0 +1,135 @@ +const safePacket = { + packetId: "packet-safe-methods-v4", + documentId: "shared-manuscript/neuro-methods", + namedVersion: "Manuscript v4 Methods", + sectionId: "methods-statistical-model", + collaborators: ["orcid:0000-0002-1825-0097", "orcid:0000-0003-1555-4212"], + trustedRenderMode: false, + macros: [ + { + name: "\\mean", + body: "\\operatorname{mean}", + author: "orcid:0000-0002-1825-0097", + scope: "section", + approved: true, + }, + { + name: "\\ci", + body: "\\mathrm{CI}", + author: "orcid:0000-0003-1555-4212", + scope: "document", + approved: true, + }, + ], + blocks: [ + { + blockId: "eq-primary-outcome", + type: "latex", + text: "The primary endpoint is $\\mean(X) \\pm 95\\%\\ \\ci$.", + locked: false, + }, + { + blockId: "md-summary", + type: "markdown", + text: "The same model is rendered in the methods preview.", + locked: false, + }, + ], +}; + +const blockedPacket = { + packetId: "packet-unsafe-results-v8", + documentId: "shared-manuscript/results-live", + namedVersion: "Final Submission", + sectionId: "results-figures", + collaborators: ["orcid:0000-0001-1111-2222", "orcid:0000-0001-3333-4444"], + trustedRenderMode: false, + macros: [ + { + name: "\\dataset", + body: "\\href{https://private.example.invalid/raw.csv}{dataset}", + author: "orcid:0000-0001-1111-2222", + scope: "document", + approved: false, + }, + { + name: "\\loopA", + body: "\\loopB", + author: "orcid:0000-0001-3333-4444", + scope: "section", + approved: true, + }, + { + name: "\\loopB", + body: "\\loopA", + author: "orcid:0000-0001-3333-4444", + scope: "section", + approved: true, + }, + { + name: "\\hazard", + body: "\\htmlClass{warning}{hazard}", + author: "orcid:0000-0001-1111-2222", + scope: "section", + approved: true, + }, + { + name: "\\hazard", + body: "\\textbf{hazard}", + author: "orcid:0000-0001-3333-4444", + scope: "section", + approved: true, + }, + ], + blocks: [ + { + blockId: "eq-hazard-model", + type: "latex", + text: "$\\hazard = \\loopA + \\dataset$", + locked: true, + }, + { + blockId: "figure-caption", + type: "markdown", + text: "![private cohort](https://private.example.invalid/figure.png)", + locked: true, + }, + { + blockId: "unsafe-inline-html", + type: "markdown", + text: "", + locked: false, + }, + ], +}; + +const reviewPacket = { + packetId: "packet-trusted-supplement-v2", + documentId: "shared-manuscript/supplement", + namedVersion: "Supplement Draft", + sectionId: "appendix-derivations", + collaborators: ["orcid:0000-0002-9999-7777"], + trustedRenderMode: true, + macros: [ + { + name: "\\annotate", + body: "\\htmlId{derivation-note}{note}", + author: "orcid:0000-0002-9999-7777", + scope: "section", + approved: true, + trustJustification: "Maintainer-approved KaTeX trust macro for internal appendix anchor.", + }, + ], + blocks: [ + { + blockId: "appendix-equation", + type: "latex", + text: "$\\annotate = \\sum_i x_i$", + locked: false, + }, + ], +}; + +module.exports = { + samplePackets: [safePacket, blockedPacket, reviewPacket], +}; diff --git a/collaborative-latex-macro-safety-guard/scripts/render-demo-video.js b/collaborative-latex-macro-safety-guard/scripts/render-demo-video.js new file mode 100644 index 00000000..8325dacf --- /dev/null +++ b/collaborative-latex-macro-safety-guard/scripts/render-demo-video.js @@ -0,0 +1,85 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const reportsDir = path.join(__dirname, "..", "reports"); +const reportPath = path.join(reportsDir, "demo.json"); +const outputPath = path.join(reportsDir, "demo.mp4"); + +if (!fs.existsSync(reportPath)) { + throw new Error("Run npm run demo before rendering the video."); +} + +const report = JSON.parse(fs.readFileSync(reportPath, "utf8")); +const ffmpeg = process.env.FFMPEG_PATH || "ffmpeg"; +const title = "SCIBASE LaTeX macro safety guard"; +const subtitle = `ready ${report.totals.ready} review ${report.totals.needsReview} blocked ${report.totals.blocked}`; +const detail = `blocking issues ${report.totals.blockingIssues} warnings ${report.totals.warnings}`; +const fontArg = getFontArg(); +const filters = [ + "drawbox=x=0:y=0:w=iw:h=ih:color=0x111827@1:t=fill", + "drawbox=x=72:y=86:w=1136:h=548:color=0xffffff@0.94:t=fill", + "drawbox=x=72:y=86:w=1136:h=18:color=0xb7791f@1:t=fill", + drawText(title, 112, 160, 42, "0x111827", fontArg), + drawText("Real-time collaborative research editor", 112, 220, 24, "0x374151", fontArg), + drawText(subtitle, 112, 305, 34, "0x111827", fontArg), + drawText(detail, 112, 365, 26, "0xb42318", fontArg), + drawText( + "Validates synced Markdown and LaTeX packets before live preview or named-version export.", + 112, + 450, + 22, + "0x374151", + fontArg, + ), + drawText("Synthetic data only. No credentials, users, or external services.", 112, 500, 22, "0x374151", fontArg), +].join(","); + +const result = spawnSync( + ffmpeg, + [ + "-y", + "-f", + "lavfi", + "-i", + "color=c=0x111827:s=1280x720:d=4:r=25", + "-vf", + filters, + "-pix_fmt", + "yuv420p", + outputPath, + ], + { stdio: "inherit" }, +); + +if (result.status !== 0) { + throw new Error(`ffmpeg exited with status ${result.status}`); +} + +console.log(`Wrote ${path.relative(process.cwd(), outputPath)}`); + +function drawText(text, x, y, size, color, fontArg) { + return `drawtext=${fontArg}:text='${escapeDrawtext(text)}':x=${x}:y=${y}:fontsize=${size}:fontcolor=${color}`; +} + +function escapeDrawtext(value) { + return String(value) + .replaceAll("\\", "\\\\") + .replaceAll(":", "\\:") + .replaceAll("'", "\\'"); +} + +function getFontArg() { + const candidates = [ + "C:/Windows/Fonts/arial.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/System/Library/Fonts/Supplemental/Arial.ttf", + ]; + const font = candidates.find((candidate) => fs.existsSync(candidate)); + if (!font) { + throw new Error( + `No supported TrueType font file found. Checked: ${candidates.join(", ")}`, + ); + } + return `fontfile='${font.replaceAll(":", "\\:")}'`; +} diff --git a/collaborative-latex-macro-safety-guard/test.js b/collaborative-latex-macro-safety-guard/test.js new file mode 100644 index 00000000..4ef742e8 --- /dev/null +++ b/collaborative-latex-macro-safety-guard/test.js @@ -0,0 +1,74 @@ +const assert = require("node:assert/strict"); +const { + createGuardReport, + evaluateMacroPacket, + renderMarkdown, + renderSvg, +} = require("./index"); +const { samplePackets } = require("./sample-data"); + +const ready = evaluateMacroPacket(samplePackets[0]); +assert.equal(ready.status, "ready"); +assert.equal(ready.renderDecision.allowLivePreview, true); +assert.equal(ready.renderDecision.allowNamedVersionExport, true); + +const blocked = evaluateMacroPacket(samplePackets[1]); +assert.equal(blocked.status, "blocked"); +assert.equal(blocked.renderDecision.allowLivePreview, false); +assert.ok( + blocked.issues.some((issue) => issue.code === "UNSAFE_MACRO_COMMAND"), +); +assert.ok( + blocked.issues.some((issue) => issue.code === "RECURSIVE_MACRO_EXPANSION"), +); +assert.ok( + blocked.issues.some((issue) => issue.code === "MACRO_DEFINITION_CONFLICT"), +); +assert.ok( + blocked.issues.some((issue) => issue.code === "RAW_HTML_IN_COLLAB_BLOCK"), +); +assert.ok( + blocked.issues.some( + (issue) => issue.code === "EXTERNAL_RENDER_RESOURCE_BLOCKED", + ), +); +assert.ok( + blocked.issues.some((issue) => issue.code === "LOCKED_SECTION_RENDER_HOLD"), +); + +const review = evaluateMacroPacket(samplePackets[2]); +assert.equal(review.status, "needs_review"); +assert.equal(review.renderDecision.allowLivePreview, true); +assert.equal(review.renderDecision.allowTrustedKatexRender, false); +assert.ok( + review.issues.some( + (issue) => issue.code === "TRUSTED_MACRO_REVIEW_REQUIRED", + ), +); + +const report = createGuardReport(samplePackets); +assert.deepEqual(report.totals, { + packets: 3, + ready: 1, + needsReview: 1, + blocked: 1, + blockingIssues: 10, + warnings: 1, +}); + +const markdown = renderMarkdown(report); +assert.match(markdown, /Collaborative LaTeX Macro Safety Guard/); +assert.match(markdown, /RECURSIVE_MACRO_EXPANSION/); +assert.match(markdown, /Named-version export gate/); + +const svg = renderSvg(report); +assert.match(svg, / evaluateMacroPacket({ packetId: "missing-document" }), + /packet.documentId is required/, +); + +console.log("collaborative-latex-macro-safety-guard tests passed");