From 18cf784fd3ffff94ea89057f695571ae08f1a7b5 Mon Sep 17 00:00:00 2001 From: IOROHKI Date: Wed, 20 May 2026 03:17:28 -0700 Subject: [PATCH] Add scientific roundtrip fidelity checker --- .../README.md | 28 ++ scientific-roundtrip-fidelity-checker/demo.js | 25 ++ .../index.js | 279 ++++++++++++++++++ .../reports/demo.mp4 | Bin 0 -> 60956 bytes .../reports/review-packet.md | 54 ++++ .../reports/roundtrip-audit.json | 190 ++++++++++++ .../reports/roundtrip-fidelity.svg | 1 + .../requirements-map.md | 17 ++ .../sample-data.js | 192 ++++++++++++ scientific-roundtrip-fidelity-checker/test.js | 42 +++ 10 files changed, 828 insertions(+) create mode 100644 scientific-roundtrip-fidelity-checker/README.md create mode 100644 scientific-roundtrip-fidelity-checker/demo.js create mode 100644 scientific-roundtrip-fidelity-checker/index.js create mode 100644 scientific-roundtrip-fidelity-checker/reports/demo.mp4 create mode 100644 scientific-roundtrip-fidelity-checker/reports/review-packet.md create mode 100644 scientific-roundtrip-fidelity-checker/reports/roundtrip-audit.json create mode 100644 scientific-roundtrip-fidelity-checker/reports/roundtrip-fidelity.svg create mode 100644 scientific-roundtrip-fidelity-checker/requirements-map.md create mode 100644 scientific-roundtrip-fidelity-checker/sample-data.js create mode 100644 scientific-roundtrip-fidelity-checker/test.js diff --git a/scientific-roundtrip-fidelity-checker/README.md b/scientific-roundtrip-fidelity-checker/README.md new file mode 100644 index 0000000..a452f84 --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/README.md @@ -0,0 +1,28 @@ +# Scientific Round-Trip Fidelity Checker + +This module is a focused slice for SCIBASE.AI issue #12, Real-Time Collaborative Editor. + +It verifies that scientific documents can move through Markdown, LaTeX, and notebook export/import paths without silently losing publication-critical context. The sample data is synthetic and dependency-free. + +## What It Covers + +- Equation LaTeX, labels, and citation keys. +- Figure and table anchors, captions, source assets, and table columns. +- Jupyter-style cell IDs, languages, source hashes, output hashes, and execution counts. +- Inline comments and unresolved suggestions that must survive review export. +- Cross-reference integrity across figures, tables, equations, and notebook cells. +- Reviewer repair queues, JSON audit packets, Markdown review packets, SVG summaries, and a short MP4 demo artifact. + +## Run + +```bash +node scientific-roundtrip-fidelity-checker/test.js +node scientific-roundtrip-fidelity-checker/demo.js +``` + +Demo output is written to: + +- `reports/roundtrip-audit.json` +- `reports/review-packet.md` +- `reports/roundtrip-fidelity.svg` +- `reports/demo.mp4` diff --git a/scientific-roundtrip-fidelity-checker/demo.js b/scientific-roundtrip-fidelity-checker/demo.js new file mode 100644 index 0000000..6ea3b87 --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/demo.js @@ -0,0 +1,25 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const data = require("./sample-data"); +const { renderMarkdown, renderSvg, runRoundTripAudit } = require("./index"); + +const report = runRoundTripAudit(data); +const reportsDir = path.join(__dirname, "reports"); +fs.mkdirSync(reportsDir, { recursive: true }); +fs.writeFileSync(path.join(reportsDir, "roundtrip-audit.json"), JSON.stringify(report, null, 2)); +fs.writeFileSync(path.join(reportsDir, "review-packet.md"), renderMarkdown(report)); +fs.writeFileSync(path.join(reportsDir, "roundtrip-fidelity.svg"), renderSvg(report)); + +console.log(JSON.stringify({ + decision: report.summary.releaseDecision, + averageScore: report.summary.averageScore, + totalFindings: report.summary.totalFindings, + weakestFormats: report.summary.weakestFormats, + auditDigest: report.auditDigest, + reports: [ + "reports/roundtrip-audit.json", + "reports/review-packet.md", + "reports/roundtrip-fidelity.svg", + "reports/demo.mp4" + ] +}, null, 2)); diff --git a/scientific-roundtrip-fidelity-checker/index.js b/scientific-roundtrip-fidelity-checker/index.js new file mode 100644 index 0000000..6fbac7e --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/index.js @@ -0,0 +1,279 @@ +const crypto = require("node:crypto"); + +const SEVERITY_WEIGHTS = { + blocker: 30, + high: 20, + medium: 10, + low: 4 +}; + +function arrayDiff(expected = [], actual = []) { + const actualSet = new Set(actual); + return expected.filter((item) => !actualSet.has(item)); +} + +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return crypto.createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function addFinding(findings, severity, code, message, task, blockId = null) { + findings.push({ severity, code, message, task, blockId }); +} + +function compareScalar(findings, block, roundTripBlock, field, severity, code, label) { + if (block[field] !== roundTripBlock[field]) { + addFinding( + findings, + severity, + code, + `${label} changed for ${block.id}: expected ${JSON.stringify(block[field])}, got ${JSON.stringify(roundTripBlock[field])}.`, + `Regenerate the export/import adapter so ${label.toLowerCase()} survives round trip for ${block.id}.`, + block.id + ); + } +} + +function compareArray(findings, block, roundTripBlock, field, severity, code, label) { + const missing = arrayDiff(block[field], roundTripBlock[field]); + if (missing.length) { + addFinding( + findings, + severity, + code, + `${label} missing after round trip for ${block.id}: ${missing.join(", ")}.`, + `Preserve ${label.toLowerCase()} metadata for ${block.id} in this format.`, + block.id + ); + } +} + +function compareBlock(canonicalBlock, roundTripBlock, format) { + const findings = []; + if (!roundTripBlock) { + addFinding( + findings, + "blocker", + "block-dropped", + `${format} round trip dropped required block ${canonicalBlock.id}.`, + `Restore ${canonicalBlock.id} during ${format} import before enabling publication export.`, + canonicalBlock.id + ); + return findings; + } + + compareScalar(findings, canonicalBlock, roundTripBlock, "type", "blocker", "block-type-changed", "Block type"); + compareScalar(findings, canonicalBlock, roundTripBlock, "label", "high", "label-changed", "Anchor label"); + compareArray(findings, canonicalBlock, roundTripBlock, "references", "medium", "citation-key-lost", "Citation keys"); + compareArray(findings, canonicalBlock, roundTripBlock, "comments", "medium", "comment-thread-lost", "Comment threads"); + compareArray(findings, canonicalBlock, roundTripBlock, "suggestions", "medium", "suggestion-lost", "Unresolved suggestions"); + + if (canonicalBlock.type === "equation") { + compareScalar(findings, canonicalBlock, roundTripBlock, "latex", "high", "equation-mutated", "Equation LaTeX"); + } + + if (canonicalBlock.type === "figure") { + compareScalar(findings, canonicalBlock, roundTripBlock, "caption", "medium", "caption-mutated", "Figure caption"); + if (canonicalBlock.sourceAsset !== roundTripBlock.sourceAsset) { + addFinding( + findings, + "low", + "source-asset-transcoded", + `${canonicalBlock.id} source asset changed from ${canonicalBlock.sourceAsset} to ${roundTripBlock.sourceAsset}.`, + "Confirm the transcoded figure keeps resolution, accessibility metadata, and citation anchors.", + canonicalBlock.id + ); + } + } + + if (canonicalBlock.type === "table") { + compareScalar(findings, canonicalBlock, roundTripBlock, "caption", "medium", "table-caption-mutated", "Table caption"); + compareArray(findings, canonicalBlock, roundTripBlock, "columns", "high", "table-column-lost", "Table columns"); + } + + if (canonicalBlock.type === "notebook-cell") { + compareScalar(findings, canonicalBlock, roundTripBlock, "language", "high", "cell-language-changed", "Notebook language"); + compareScalar(findings, canonicalBlock, roundTripBlock, "sourceHash", "high", "cell-source-mutated", "Notebook source hash"); + compareScalar(findings, canonicalBlock, roundTripBlock, "outputHash", "high", "cell-output-mutated", "Notebook output hash"); + compareScalar(findings, canonicalBlock, roundTripBlock, "executionCount", "medium", "cell-execution-count-drift", "Execution count"); + } + + return findings; +} + +function requiredBlocksForFormat(document, format) { + return document.blocks.filter((block) => block.requiredFormats.includes(format)); +} + +function crossReferenceKey(ref) { + return `${ref.from}->${ref.to}:${ref.kind}`; +} + +function compareCrossReferences(document, roundTrip) { + const actual = new Set((roundTrip.crossReferences || []).map(crossReferenceKey)); + return document.crossReferences + .filter((ref) => { + const fromBlock = document.blocks.find((block) => block.id === ref.from); + const toBlock = document.blocks.find((block) => block.id === ref.to); + return fromBlock?.requiredFormats.includes(roundTrip.format) || toBlock?.requiredFormats.includes(roundTrip.format); + }) + .filter((ref) => !actual.has(crossReferenceKey(ref))) + .map((ref) => ({ + severity: "high", + code: "cross-reference-lost", + message: `${roundTrip.format} round trip lost ${ref.kind} link ${ref.from} -> ${ref.to}.`, + task: `Preserve cross-reference ${ref.from} -> ${ref.to} during ${roundTrip.format} import.`, + blockId: ref.from + })); +} + +function score(findings) { + const penalty = findings.reduce((total, finding) => total + SEVERITY_WEIGHTS[finding.severity], 0); + return Math.max(0, 100 - penalty); +} + +function decisionFor(findings, fidelityScore) { + if (findings.some((finding) => finding.severity === "blocker")) return "block-export"; + if (fidelityScore < 70) return "repair-before-submit"; + if (fidelityScore < 90) return "review-warnings"; + return "roundtrip-ready"; +} + +function evaluateRoundTrip(document, roundTrip) { + const blockMap = new Map(roundTrip.blocks.map((block) => [block.id, block])); + const findings = []; + for (const block of requiredBlocksForFormat(document, roundTrip.format)) { + findings.push(...compareBlock(block, blockMap.get(block.id), roundTrip.format)); + } + findings.push(...compareCrossReferences(document, roundTrip)); + const fidelityScore = score(findings); + return { + format: roundTrip.format, + exporter: roundTrip.exporter, + importer: roundTrip.importer, + timestamp: roundTrip.timestamp, + fidelityScore, + decision: decisionFor(findings, fidelityScore), + findingCount: findings.length, + findings, + repairedBy: findings.map((finding) => finding.task) + }; +} + +function summarize(formatReports) { + const allFindings = formatReports.flatMap((report) => report.findings); + const countsBySeverity = allFindings.reduce((counts, finding) => { + counts[finding.severity] = (counts[finding.severity] || 0) + 1; + return counts; + }, {}); + const averageScore = Math.round(formatReports.reduce((total, report) => total + report.fidelityScore, 0) / formatReports.length); + const releaseDecision = formatReports.some((report) => report.decision === "block-export") + ? "block-export" + : averageScore < 80 + ? "repair-before-submit" + : "roundtrip-ready"; + return { + averageScore, + releaseDecision, + totalFindings: allFindings.length, + countsBySeverity, + weakestFormats: formatReports + .slice() + .sort((a, b) => a.fidelityScore - b.fidelityScore) + .map((report) => ({ + format: report.format, + fidelityScore: report.fidelityScore, + decision: report.decision + })) + }; +} + +function runRoundTripAudit({ canonicalDocument, roundTripExports }) { + const formatReports = roundTripExports.map((roundTrip) => evaluateRoundTrip(canonicalDocument, roundTrip)); + const packet = { + document: { + id: canonicalDocument.id, + title: canonicalDocument.title, + version: canonicalDocument.version, + publicationStyle: canonicalDocument.publicationStyle + }, + checker: "scientific-roundtrip-fidelity-checker", + summary: summarize(formatReports), + formatReports + }; + return { + ...packet, + auditDigest: digest(packet) + }; +} + +function renderMarkdown(report) { + const lines = [ + `# Round-Trip Fidelity Review: ${report.document.title}`, + "", + `Decision: **${report.summary.releaseDecision}**`, + `Average fidelity: **${report.summary.averageScore}**`, + `Total findings: **${report.summary.totalFindings}**`, + "", + "## Formats", + ...report.summary.weakestFormats.map((item) => `- ${item.format}: ${item.decision} (${item.fidelityScore})`), + "", + "## Repair Queue" + ]; + for (const formatReport of report.formatReports) { + lines.push("", `### ${formatReport.format}`, `Score: ${formatReport.fidelityScore}`, `Decision: ${formatReport.decision}`); + if (!formatReport.findings.length) { + lines.push("- No fidelity loss detected."); + } else { + for (const finding of formatReport.findings) { + lines.push(`- [${finding.severity}] ${finding.message}`); + lines.push(` Task: ${finding.task}`); + } + } + } + lines.push("", `Audit digest: \`${report.auditDigest}\``); + return `${lines.join("\n")}\n`; +} + +function renderSvg(report) { + const width = 980; + const rowHeight = 94; + const height = 160 + report.formatReports.length * rowHeight; + const rows = report.formatReports.map((formatReport, index) => { + const y = 140 + index * rowHeight; + const barWidth = Math.max(10, formatReport.fidelityScore * 5.2); + const color = formatReport.fidelityScore >= 90 ? "#2f855a" : formatReport.fidelityScore >= 70 ? "#b7791f" : "#c53030"; + return [ + `${formatReport.format}`, + ``, + ``, + `${formatReport.decision} (${formatReport.fidelityScore})`, + `${formatReport.findingCount} finding(s)` + ].join(""); + }).join(""); + return [ + ``, + ``, + ``, + `Scientific Round-Trip Fidelity Checker`, + `Decision: ${report.summary.releaseDecision} | Avg ${report.summary.averageScore} | Audit ${report.auditDigest.slice(0, 12)}`, + rows, + `` + ].join(""); +} + +module.exports = { + runRoundTripAudit, + evaluateRoundTrip, + compareBlock, + renderMarkdown, + renderSvg, + digest +}; diff --git a/scientific-roundtrip-fidelity-checker/reports/demo.mp4 b/scientific-roundtrip-fidelity-checker/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..eb1e2323cedadc92d1143b6285c9e74f27505038 GIT binary patch literal 60956 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 literal 0 HcmV?d00001 diff --git a/scientific-roundtrip-fidelity-checker/reports/review-packet.md b/scientific-roundtrip-fidelity-checker/reports/review-packet.md new file mode 100644 index 0000000..90a06cb --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/reports/review-packet.md @@ -0,0 +1,54 @@ +# Round-Trip Fidelity Review: Hybrid Bayesian Imaging Biomarkers + +Decision: **repair-before-submit** +Average fidelity: **35** +Total findings: **14** + +## Formats +- notebook: repair-before-submit (10) +- latex: repair-before-submit (36) +- markdown: repair-before-submit (60) + +## Repair Queue + +### markdown +Score: 60 +Decision: repair-before-submit +- [medium] Citation keys missing after round trip for table-cohorts: harmonization-lockfile. + Task: Preserve citation keys metadata for table-cohorts in this format. +- [medium] Unresolved suggestions missing after round trip for table-cohorts: add missing data footnote. + Task: Preserve unresolved suggestions metadata for table-cohorts in this format. +- [high] markdown round trip lost produces-summary link cell-harmonize -> table-cohorts. + Task: Preserve cross-reference cell-harmonize -> table-cohorts during markdown import. + +### latex +Score: 36 +Decision: repair-before-submit +- [medium] Citation keys missing after round trip for intro-eq-1: nguyen2025-imaging. + Task: Preserve citation keys metadata for intro-eq-1 in this format. +- [medium] Comment threads missing after round trip for intro-eq-1: resolve prior sensitivity note. + Task: Preserve comment threads metadata for intro-eq-1 in this format. +- [high] Equation LaTeX changed for intro-eq-1: expected "p(\\theta | y) \\propto p(y | \\theta)p(\\theta)", got "p(\\theta | y) = p(y | \\theta)p(\\theta)". + Task: Regenerate the export/import adapter so equation latex survives round trip for intro-eq-1. +- [low] fig-signal source asset changed from figures/signal-panel.svg to figures/signal-panel.pdf. + Task: Confirm the transcoded figure keeps resolution, accessibility metadata, and citation anchors. +- [high] Table columns missing after round trip for table-cohorts: median_age. + Task: Preserve table columns metadata for table-cohorts in this format. + +### notebook +Score: 10 +Decision: repair-before-submit +- [medium] Unresolved suggestions missing after round trip for intro-eq-1: explain weakly informative prior. + Task: Preserve unresolved suggestions metadata for intro-eq-1 in this format. +- [medium] Comment threads missing after round trip for fig-signal: confirm color-safe palette. + Task: Preserve comment threads metadata for fig-signal in this format. +- [high] Notebook output hash changed for cell-harmonize: expected "sha256:cell-output-harmonize", got "sha256:cell-output-stale". + Task: Regenerate the export/import adapter so notebook output hash survives round trip for cell-harmonize. +- [medium] Execution count changed for cell-harmonize: expected 18, got 12. + Task: Regenerate the export/import adapter so execution count survives round trip for cell-harmonize. +- [high] notebook round trip lost produces-summary link cell-harmonize -> table-cohorts. + Task: Preserve cross-reference cell-harmonize -> table-cohorts during notebook import. +- [high] notebook round trip lost shared-cohort link table-cohorts -> fig-signal. + Task: Preserve cross-reference table-cohorts -> fig-signal during notebook import. + +Audit digest: `da45ce37c71f4a4f7ffcaaca096fc93a959517aaee096c7bc15f3346ed557599` diff --git a/scientific-roundtrip-fidelity-checker/reports/roundtrip-audit.json b/scientific-roundtrip-fidelity-checker/reports/roundtrip-audit.json new file mode 100644 index 0000000..eab9b59 --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/reports/roundtrip-audit.json @@ -0,0 +1,190 @@ +{ + "document": { + "id": "collab-ms-127", + "title": "Hybrid Bayesian Imaging Biomarkers", + "version": "v4.2.0", + "publicationStyle": "nature" + }, + "checker": "scientific-roundtrip-fidelity-checker", + "summary": { + "averageScore": 35, + "releaseDecision": "repair-before-submit", + "totalFindings": 14, + "countsBySeverity": { + "medium": 7, + "high": 6, + "low": 1 + }, + "weakestFormats": [ + { + "format": "notebook", + "fidelityScore": 10, + "decision": "repair-before-submit" + }, + { + "format": "latex", + "fidelityScore": 36, + "decision": "repair-before-submit" + }, + { + "format": "markdown", + "fidelityScore": 60, + "decision": "repair-before-submit" + } + ] + }, + "formatReports": [ + { + "format": "markdown", + "exporter": "collab-md-v2", + "importer": "collab-md-v2", + "timestamp": "2026-05-20T09:40:00Z", + "fidelityScore": 60, + "decision": "repair-before-submit", + "findingCount": 3, + "findings": [ + { + "severity": "medium", + "code": "citation-key-lost", + "message": "Citation keys missing after round trip for table-cohorts: harmonization-lockfile.", + "task": "Preserve citation keys metadata for table-cohorts in this format.", + "blockId": "table-cohorts" + }, + { + "severity": "medium", + "code": "suggestion-lost", + "message": "Unresolved suggestions missing after round trip for table-cohorts: add missing data footnote.", + "task": "Preserve unresolved suggestions metadata for table-cohorts in this format.", + "blockId": "table-cohorts" + }, + { + "severity": "high", + "code": "cross-reference-lost", + "message": "markdown round trip lost produces-summary link cell-harmonize -> table-cohorts.", + "task": "Preserve cross-reference cell-harmonize -> table-cohorts during markdown import.", + "blockId": "cell-harmonize" + } + ], + "repairedBy": [ + "Preserve citation keys metadata for table-cohorts in this format.", + "Preserve unresolved suggestions metadata for table-cohorts in this format.", + "Preserve cross-reference cell-harmonize -> table-cohorts during markdown import." + ] + }, + { + "format": "latex", + "exporter": "collab-tex-v1", + "importer": "collab-tex-v1", + "timestamp": "2026-05-20T09:42:00Z", + "fidelityScore": 36, + "decision": "repair-before-submit", + "findingCount": 5, + "findings": [ + { + "severity": "medium", + "code": "citation-key-lost", + "message": "Citation keys missing after round trip for intro-eq-1: nguyen2025-imaging.", + "task": "Preserve citation keys metadata for intro-eq-1 in this format.", + "blockId": "intro-eq-1" + }, + { + "severity": "medium", + "code": "comment-thread-lost", + "message": "Comment threads missing after round trip for intro-eq-1: resolve prior sensitivity note.", + "task": "Preserve comment threads metadata for intro-eq-1 in this format.", + "blockId": "intro-eq-1" + }, + { + "severity": "high", + "code": "equation-mutated", + "message": "Equation LaTeX changed for intro-eq-1: expected \"p(\\\\theta | y) \\\\propto p(y | \\\\theta)p(\\\\theta)\", got \"p(\\\\theta | y) = p(y | \\\\theta)p(\\\\theta)\".", + "task": "Regenerate the export/import adapter so equation latex survives round trip for intro-eq-1.", + "blockId": "intro-eq-1" + }, + { + "severity": "low", + "code": "source-asset-transcoded", + "message": "fig-signal source asset changed from figures/signal-panel.svg to figures/signal-panel.pdf.", + "task": "Confirm the transcoded figure keeps resolution, accessibility metadata, and citation anchors.", + "blockId": "fig-signal" + }, + { + "severity": "high", + "code": "table-column-lost", + "message": "Table columns missing after round trip for table-cohorts: median_age.", + "task": "Preserve table columns metadata for table-cohorts in this format.", + "blockId": "table-cohorts" + } + ], + "repairedBy": [ + "Preserve citation keys metadata for intro-eq-1 in this format.", + "Preserve comment threads metadata for intro-eq-1 in this format.", + "Regenerate the export/import adapter so equation latex survives round trip for intro-eq-1.", + "Confirm the transcoded figure keeps resolution, accessibility metadata, and citation anchors.", + "Preserve table columns metadata for table-cohorts in this format." + ] + }, + { + "format": "notebook", + "exporter": "collab-ipynb-v3", + "importer": "collab-ipynb-v3", + "timestamp": "2026-05-20T09:44:00Z", + "fidelityScore": 10, + "decision": "repair-before-submit", + "findingCount": 6, + "findings": [ + { + "severity": "medium", + "code": "suggestion-lost", + "message": "Unresolved suggestions missing after round trip for intro-eq-1: explain weakly informative prior.", + "task": "Preserve unresolved suggestions metadata for intro-eq-1 in this format.", + "blockId": "intro-eq-1" + }, + { + "severity": "medium", + "code": "comment-thread-lost", + "message": "Comment threads missing after round trip for fig-signal: confirm color-safe palette.", + "task": "Preserve comment threads metadata for fig-signal in this format.", + "blockId": "fig-signal" + }, + { + "severity": "high", + "code": "cell-output-mutated", + "message": "Notebook output hash changed for cell-harmonize: expected \"sha256:cell-output-harmonize\", got \"sha256:cell-output-stale\".", + "task": "Regenerate the export/import adapter so notebook output hash survives round trip for cell-harmonize.", + "blockId": "cell-harmonize" + }, + { + "severity": "medium", + "code": "cell-execution-count-drift", + "message": "Execution count changed for cell-harmonize: expected 18, got 12.", + "task": "Regenerate the export/import adapter so execution count survives round trip for cell-harmonize.", + "blockId": "cell-harmonize" + }, + { + "severity": "high", + "code": "cross-reference-lost", + "message": "notebook round trip lost produces-summary link cell-harmonize -> table-cohorts.", + "task": "Preserve cross-reference cell-harmonize -> table-cohorts during notebook import.", + "blockId": "cell-harmonize" + }, + { + "severity": "high", + "code": "cross-reference-lost", + "message": "notebook round trip lost shared-cohort link table-cohorts -> fig-signal.", + "task": "Preserve cross-reference table-cohorts -> fig-signal during notebook import.", + "blockId": "table-cohorts" + } + ], + "repairedBy": [ + "Preserve unresolved suggestions metadata for intro-eq-1 in this format.", + "Preserve comment threads metadata for fig-signal in this format.", + "Regenerate the export/import adapter so notebook output hash survives round trip for cell-harmonize.", + "Regenerate the export/import adapter so execution count survives round trip for cell-harmonize.", + "Preserve cross-reference cell-harmonize -> table-cohorts during notebook import.", + "Preserve cross-reference table-cohorts -> fig-signal during notebook import." + ] + } + ], + "auditDigest": "da45ce37c71f4a4f7ffcaaca096fc93a959517aaee096c7bc15f3346ed557599" +} \ No newline at end of file diff --git a/scientific-roundtrip-fidelity-checker/reports/roundtrip-fidelity.svg b/scientific-roundtrip-fidelity-checker/reports/roundtrip-fidelity.svg new file mode 100644 index 0000000..ad64b6b --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/reports/roundtrip-fidelity.svg @@ -0,0 +1 @@ +Scientific Round-Trip Fidelity CheckerDecision: repair-before-submit | Avg 35 | Audit da45ce37c71fmarkdownrepair-before-submit (60)3 finding(s)latexrepair-before-submit (36)5 finding(s)notebookrepair-before-submit (10)6 finding(s) \ No newline at end of file diff --git a/scientific-roundtrip-fidelity-checker/requirements-map.md b/scientific-roundtrip-fidelity-checker/requirements-map.md new file mode 100644 index 0000000..a94d9ac --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/requirements-map.md @@ -0,0 +1,17 @@ +# Requirements Map + +Source: SCIBASE.AI issue #12, Real-Time Collaborative Editor. + +| Requirement | Implementation | +| --- | --- | +| Markdown and LaTeX support | The checker compares canonical scientific blocks against Markdown and LaTeX round-trip snapshots. | +| Inline equations | Equation LaTeX and anchor labels are validated for mutation and loss. | +| Cross-referencing figures, tables, and citations | Figure/table/equation labels, citation keys, and cross-reference edges are checked. | +| Reference manager integration | Citation keys are preserved as structured metadata across export/import paths. | +| Jupyter notebook integration | Notebook-cell language, source hash, output hash, and execution count are compared. | +| Output display preservation | Notebook output hashes are treated as high-severity fidelity evidence. | +| Inline comments and suggestions | Comment threads and unresolved suggestions are checked for loss in each format. | +| Version history and autosave compatibility | The audit includes document version metadata and deterministic digests for review history. | +| Publication export readiness | The summary returns block-export, repair-before-submit, review-warnings, or ready decisions. | +| Reviewer-facing artifacts | Demo emits JSON, Markdown, SVG, and MP4 artifacts under `reports/`. | +| Local verification | `test.js` covers format decisions, dropped blocks, equation mutation, citation loss, table column loss, notebook output drift, digest stability, Markdown output, and SVG output. | diff --git a/scientific-roundtrip-fidelity-checker/sample-data.js b/scientific-roundtrip-fidelity-checker/sample-data.js new file mode 100644 index 0000000..941dddb --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/sample-data.js @@ -0,0 +1,192 @@ +const canonicalDocument = { + id: "collab-ms-127", + title: "Hybrid Bayesian Imaging Biomarkers", + version: "v4.2.0", + publicationStyle: "nature", + blocks: [ + { + id: "intro-eq-1", + type: "equation", + label: "eq:posterior-update", + latex: "p(\\theta | y) \\propto p(y | \\theta)p(\\theta)", + inlineMarkdown: "$p(\\theta | y) \\propto p(y | \\theta)p(\\theta)$", + references: ["smith2024-bayes", "nguyen2025-imaging"], + comments: ["resolve prior sensitivity note"], + suggestions: ["explain weakly informative prior"], + requiredFormats: ["markdown", "latex", "notebook"] + }, + { + id: "fig-signal", + type: "figure", + label: "fig:signal-panel", + caption: "Posterior signal trajectories by scanner cohort.", + sourceAsset: "figures/signal-panel.svg", + references: ["cohort-registry"], + comments: ["confirm color-safe palette"], + suggestions: [], + requiredFormats: ["markdown", "latex", "notebook"] + }, + { + id: "table-cohorts", + type: "table", + label: "tab:cohort-balance", + caption: "Cohort balance after scanner harmonization.", + columns: ["cohort", "n", "median_age", "scanner_family"], + references: ["cohort-registry", "harmonization-lockfile"], + comments: [], + suggestions: ["add missing data footnote"], + requiredFormats: ["markdown", "latex"] + }, + { + id: "cell-harmonize", + type: "notebook-cell", + label: "cell:harmonize", + language: "python", + sourceHash: "sha256:cell-source-harmonize", + outputHash: "sha256:cell-output-harmonize", + executionCount: 18, + references: ["harmonization-lockfile"], + comments: ["rerun after cohort freeze"], + suggestions: [], + requiredFormats: ["notebook"] + } + ], + crossReferences: [ + { from: "intro-eq-1", to: "fig-signal", kind: "supports-claim" }, + { from: "cell-harmonize", to: "table-cohorts", kind: "produces-summary" }, + { from: "table-cohorts", to: "fig-signal", kind: "shared-cohort" } + ] +}; + +const roundTripExports = [ + { + format: "markdown", + exporter: "collab-md-v2", + importer: "collab-md-v2", + timestamp: "2026-05-20T09:40:00Z", + blocks: [ + { + id: "intro-eq-1", + type: "equation", + label: "eq:posterior-update", + latex: "p(\\theta | y) \\propto p(y | \\theta)p(\\theta)", + references: ["smith2024-bayes", "nguyen2025-imaging"], + comments: ["resolve prior sensitivity note"], + suggestions: ["explain weakly informative prior"] + }, + { + id: "fig-signal", + type: "figure", + label: "fig:signal-panel", + caption: "Posterior signal trajectories by scanner cohort.", + sourceAsset: "figures/signal-panel.svg", + references: ["cohort-registry"], + comments: ["confirm color-safe palette"], + suggestions: [] + }, + { + id: "table-cohorts", + type: "table", + label: "tab:cohort-balance", + caption: "Cohort balance after scanner harmonization.", + columns: ["cohort", "n", "median_age", "scanner_family"], + references: ["cohort-registry"], + comments: [], + suggestions: [] + } + ], + crossReferences: [ + { from: "intro-eq-1", to: "fig-signal", kind: "supports-claim" }, + { from: "table-cohorts", to: "fig-signal", kind: "shared-cohort" } + ] + }, + { + format: "latex", + exporter: "collab-tex-v1", + importer: "collab-tex-v1", + timestamp: "2026-05-20T09:42:00Z", + blocks: [ + { + id: "intro-eq-1", + type: "equation", + label: "eq:posterior-update", + latex: "p(\\theta | y) = p(y | \\theta)p(\\theta)", + references: ["smith2024-bayes"], + comments: [], + suggestions: ["explain weakly informative prior"] + }, + { + id: "fig-signal", + type: "figure", + label: "fig:signal-panel", + caption: "Posterior signal trajectories by scanner cohort.", + sourceAsset: "figures/signal-panel.pdf", + references: ["cohort-registry"], + comments: ["confirm color-safe palette"], + suggestions: [] + }, + { + id: "table-cohorts", + type: "table", + label: "tab:cohort-balance", + caption: "Cohort balance after scanner harmonization.", + columns: ["cohort", "n", "scanner_family"], + references: ["cohort-registry", "harmonization-lockfile"], + comments: [], + suggestions: ["add missing data footnote"] + } + ], + crossReferences: [ + { from: "intro-eq-1", to: "fig-signal", kind: "supports-claim" }, + { from: "cell-harmonize", to: "table-cohorts", kind: "produces-summary" }, + { from: "table-cohorts", to: "fig-signal", kind: "shared-cohort" } + ] + }, + { + format: "notebook", + exporter: "collab-ipynb-v3", + importer: "collab-ipynb-v3", + timestamp: "2026-05-20T09:44:00Z", + blocks: [ + { + id: "intro-eq-1", + type: "equation", + label: "eq:posterior-update", + latex: "p(\\theta | y) \\propto p(y | \\theta)p(\\theta)", + references: ["smith2024-bayes", "nguyen2025-imaging"], + comments: ["resolve prior sensitivity note"], + suggestions: [] + }, + { + id: "fig-signal", + type: "figure", + label: "fig:signal-panel", + caption: "Posterior signal trajectories by scanner cohort.", + sourceAsset: "figures/signal-panel.svg", + references: ["cohort-registry"], + comments: [], + suggestions: [] + }, + { + id: "cell-harmonize", + type: "notebook-cell", + label: "cell:harmonize", + language: "python", + sourceHash: "sha256:cell-source-harmonize", + outputHash: "sha256:cell-output-stale", + executionCount: 12, + references: ["harmonization-lockfile"], + comments: ["rerun after cohort freeze"], + suggestions: [] + } + ], + crossReferences: [ + { from: "intro-eq-1", to: "fig-signal", kind: "supports-claim" } + ] + } +]; + +module.exports = { + canonicalDocument, + roundTripExports +}; diff --git a/scientific-roundtrip-fidelity-checker/test.js b/scientific-roundtrip-fidelity-checker/test.js new file mode 100644 index 0000000..c340283 --- /dev/null +++ b/scientific-roundtrip-fidelity-checker/test.js @@ -0,0 +1,42 @@ +const assert = require("node:assert/strict"); +const data = require("./sample-data"); +const { digest, evaluateRoundTrip, renderMarkdown, renderSvg, runRoundTripAudit } = require("./index"); + +const report = runRoundTripAudit(data); +const byFormat = new Map(report.formatReports.map((formatReport) => [formatReport.format, formatReport])); + +assert.equal(report.checker, "scientific-roundtrip-fidelity-checker"); +assert.equal(report.summary.releaseDecision, "repair-before-submit"); +assert.equal(report.formatReports.length, 3); +assert.match(report.auditDigest, /^[a-f0-9]{64}$/); + +assert.equal(byFormat.get("markdown").decision, "repair-before-submit"); +assert.ok(byFormat.get("markdown").findings.some((finding) => finding.code === "suggestion-lost")); +assert.ok(byFormat.get("markdown").findings.some((finding) => finding.code === "cross-reference-lost")); + +assert.equal(byFormat.get("latex").decision, "repair-before-submit"); +assert.ok(byFormat.get("latex").findings.some((finding) => finding.code === "equation-mutated")); +assert.ok(byFormat.get("latex").findings.some((finding) => finding.code === "citation-key-lost")); +assert.ok(byFormat.get("latex").findings.some((finding) => finding.code === "table-column-lost")); + +assert.equal(byFormat.get("notebook").decision, "repair-before-submit"); +assert.ok(byFormat.get("notebook").findings.some((finding) => finding.code === "cell-output-mutated")); +assert.ok(byFormat.get("notebook").findings.some((finding) => finding.code === "cell-execution-count-drift")); + +const emptyNotebook = { + format: "notebook", + exporter: "test", + importer: "test", + timestamp: "2026-05-20T10:00:00Z", + blocks: [], + crossReferences: [] +}; +const emptyReport = evaluateRoundTrip(data.canonicalDocument, emptyNotebook); +assert.equal(emptyReport.decision, "block-export"); +assert.ok(emptyReport.findings.some((finding) => finding.code === "block-dropped")); + +assert.equal(digest({ b: 2, a: 1 }), digest({ a: 1, b: 2 })); +assert.ok(renderMarkdown(report).includes("Round-Trip Fidelity Review")); +assert.ok(renderSvg(report).startsWith("