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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions editor-mode-toggle-fidelity-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Editor Mode Toggle Fidelity Guard

This module is a focused real-time collaborative editor slice for issue #12. It
checks whether switching between WYSIWYG and Markdown modes loses scientific
semantics before shared edits are accepted.

The guard uses synthetic data only and has no external service dependencies.

## Checks

- Equation token loss during mode switching.
- Citation key loss or mutation.
- Notebook-cell link drift.
- Comment anchor loss.
- Tracked suggestion loss.
- Locked-section edits produced by a mode conversion.

## Local Verification

```bash
node editor-mode-toggle-fidelity-guard/test.js
node editor-mode-toggle-fidelity-guard/demo.js
```

Demo artifacts are written to `editor-mode-toggle-fidelity-guard/reports/`.

## Issue #12 Mapping

This maps to the editor's WYSIWYG toggle, Markdown/LaTeX support, notebook
integration, comments, suggestions, section locks, and collaborative edit
acceptance. It is separate from broad round-trip export, accessibility parity,
LaTeX macro safety, reference merge/formatting, equation or figure anchors,
clipboard import, find/replace, chat mention scope, and journal-style guards.
16 changes: 16 additions & 0 deletions editor-mode-toggle-fidelity-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const fs = require("fs");
const path = require("path");
const { evaluateModeToggle, toMarkdown, toSvg } = require("./modeToggleGuard");
const sampleSnapshot = require("./sampleSnapshot");

const result = evaluateModeToggle(sampleSnapshot);
const reportDir = path.join(__dirname, "reports");

fs.mkdirSync(reportDir, { recursive: true });
fs.writeFileSync(path.join(reportDir, "mode-toggle-packet.json"), `${JSON.stringify(result, null, 2)}\n`);
fs.writeFileSync(path.join(reportDir, "mode-toggle-report.md"), toMarkdown(result));
fs.writeFileSync(path.join(reportDir, "summary.svg"), toSvg(result));

console.log(`decision=${result.decision}`);
console.log(`findings=${result.findings.length}`);
console.log(`reports=${reportDir}`);
153 changes: 153 additions & 0 deletions editor-mode-toggle-fidelity-guard/modeToggleGuard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
function difference(before = [], after = []) {
const afterSet = new Set(after);
return before.filter((item) => !afterSet.has(item));
}

function addFinding(findings, severity, id, message, action, evidence = {}) {
findings.push({ severity, id, message, action, evidence });
}

function evaluateModeToggle(snapshot) {
const findings = [];
const beforeBlocks = new Map((snapshot.before || []).map((block) => [block.id, block]));
const afterBlocks = new Map((snapshot.after || []).map((block) => [block.id, block]));

for (const [id, before] of beforeBlocks.entries()) {
const after = afterBlocks.get(id);
if (!after) {
addFinding(
findings,
"block",
`missing-block:${id}`,
`Block ${id} disappeared during ${snapshot.fromMode} to ${snapshot.toMode} conversion.`,
"Reject the converted edit and restore the block from the pre-toggle snapshot.",
{ blockId: id }
);
continue;
}

const lostEquations = difference(before.equations, after.equations);
if (lostEquations.length > 0) {
addFinding(
findings,
"block",
`equation-loss:${id}`,
`Block ${id} lost equation tokens during mode toggle.`,
"Keep the pre-toggle equation tokens or require manual equation review.",
{ blockId: id, lostEquations }
);
}

const lostCitations = difference(before.citations, after.citations);
if (lostCitations.length > 0) {
addFinding(
findings,
"hold",
`citation-loss:${id}`,
`Block ${id} lost citation keys during mode toggle.`,
"Rebind citation keys before accepting the converted edit.",
{ blockId: id, lostCitations }
);
}

const lostNotebookLinks = difference(before.notebookLinks, after.notebookLinks);
if (lostNotebookLinks.length > 0) {
addFinding(
findings,
"hold",
`notebook-link-loss:${id}`,
`Block ${id} lost notebook-cell links during mode toggle.`,
"Restore notebook-cell anchors before shared edit acceptance.",
{ blockId: id, lostNotebookLinks }
);
}

const lostCommentAnchors = difference(before.commentAnchors, after.commentAnchors);
if (lostCommentAnchors.length > 0) {
addFinding(
findings,
"hold",
`comment-anchor-loss:${id}`,
`Block ${id} lost comment anchors during mode toggle.`,
"Re-anchor comments or defer accepting the converted block.",
{ blockId: id, lostCommentAnchors }
);
}

const lostSuggestions = difference(before.suggestionIds, after.suggestionIds);
if (lostSuggestions.length > 0) {
addFinding(
findings,
"block",
`suggestion-loss:${id}`,
`Block ${id} lost tracked suggestions during mode toggle.`,
"Reject the conversion or preserve suggestion provenance.",
{ blockId: id, lostSuggestions }
);
}

if (before.locked && before.contentHash !== after.contentHash && !after.lockOwnerApprovalId) {
addFinding(
findings,
"block",
`locked-block-mutation:${id}`,
`Locked block ${id} changed during mode toggle without owner approval.`,
"Require lock-owner approval before accepting the converted edit.",
{ blockId: id, beforeHash: before.contentHash, afterHash: after.contentHash }
);
}
}

const blockers = findings.filter((finding) => finding.severity === "block").length;
const holds = findings.filter((finding) => finding.severity === "hold").length;

return {
documentId: snapshot.documentId,
fromMode: snapshot.fromMode,
toMode: snapshot.toMode,
decision: blockers > 0 ? "block-mode-toggle-edit" : holds > 0 ? "hold-for-editor-review" : "ready-to-accept-toggle",
summary: {
blocks: beforeBlocks.size,
blockers,
holds,
findings: findings.length
},
findings
};
}

function toMarkdown(result) {
const rows = result.findings.map((finding) => `| ${finding.severity} | ${finding.message} | ${finding.action} |`);
return [
"# Editor Mode Toggle Fidelity Guard Report",
"",
`Document: ${result.documentId}`,
`Mode switch: ${result.fromMode} -> ${result.toMode}`,
`Decision: ${result.decision}`,
"",
"| Severity | Finding | Action |",
"| --- | --- | --- |",
...(rows.length ? rows : ["| ok | No mode-toggle fidelity issues found. | Accept the converted edit. |"]),
""
].join("\n");
}

function toSvg(result) {
const color = result.decision === "ready-to-accept-toggle" ? "#0f7b45" : result.decision === "hold-for-editor-review" ? "#9a6700" : "#b42318";
return [
`<svg xmlns="http://www.w3.org/2000/svg" width="760" height="220" viewBox="0 0 760 220" role="img" aria-label="Mode toggle fidelity guard summary">`,
`<rect width="760" height="220" fill="#f8fafc"/>`,
`<rect x="30" y="28" width="700" height="164" rx="8" fill="#ffffff" stroke="#d0d7de"/>`,
`<text x="54" y="72" font-family="Arial, sans-serif" font-size="24" font-weight="700" fill="#111827">Mode Toggle Fidelity Guard</text>`,
`<text x="54" y="112" font-family="Arial, sans-serif" font-size="18" fill="${color}">Decision: ${result.decision}</text>`,
`<text x="54" y="146" font-family="Arial, sans-serif" font-size="16" fill="#374151">Blockers: ${result.summary.blockers} | Holds: ${result.summary.holds} | Blocks: ${result.summary.blocks}</text>`,
`<text x="54" y="174" font-family="Arial, sans-serif" font-size="14" fill="#6b7280">${result.fromMode} to ${result.toMode} / ${result.documentId}</text>`,
`</svg>`
].join("\n");
}

module.exports = {
evaluateModeToggle,
toMarkdown,
toSvg
};
Binary file not shown.
85 changes: 85 additions & 0 deletions editor-mode-toggle-fidelity-guard/reports/mode-toggle-packet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"documentId": "manuscript-editor-309",
"fromMode": "wysiwyg",
"toMode": "markdown",
"decision": "block-mode-toggle-edit",
"summary": {
"blocks": 2,
"blockers": 3,
"holds": 3,
"findings": 6
},
"findings": [
{
"severity": "block",
"id": "equation-loss:methods-p2",
"message": "Block methods-p2 lost equation tokens during mode toggle.",
"action": "Keep the pre-toggle equation tokens or require manual equation review.",
"evidence": {
"blockId": "methods-p2",
"lostEquations": [
"eq-growth-rate"
]
}
},
{
"severity": "hold",
"id": "citation-loss:methods-p2",
"message": "Block methods-p2 lost citation keys during mode toggle.",
"action": "Rebind citation keys before accepting the converted edit.",
"evidence": {
"blockId": "methods-p2",
"lostCitations": [
"patel2025model"
]
}
},
{
"severity": "hold",
"id": "notebook-link-loss:methods-p2",
"message": "Block methods-p2 lost notebook-cell links during mode toggle.",
"action": "Restore notebook-cell anchors before shared edit acceptance.",
"evidence": {
"blockId": "methods-p2",
"lostNotebookLinks": [
"cell-fit-12"
]
}
},
{
"severity": "hold",
"id": "comment-anchor-loss:methods-p2",
"message": "Block methods-p2 lost comment anchors during mode toggle.",
"action": "Re-anchor comments or defer accepting the converted block.",
"evidence": {
"blockId": "methods-p2",
"lostCommentAnchors": [
"cmt-44"
]
}
},
{
"severity": "block",
"id": "suggestion-loss:methods-p2",
"message": "Block methods-p2 lost tracked suggestions during mode toggle.",
"action": "Reject the conversion or preserve suggestion provenance.",
"evidence": {
"blockId": "methods-p2",
"lostSuggestions": [
"sug-91"
]
}
},
{
"severity": "block",
"id": "locked-block-mutation:methods-p2",
"message": "Locked block methods-p2 changed during mode toggle without owner approval.",
"action": "Require lock-owner approval before accepting the converted edit.",
"evidence": {
"blockId": "methods-p2",
"beforeHash": "hash-before-methods",
"afterHash": "hash-after-methods"
}
}
]
}
14 changes: 14 additions & 0 deletions editor-mode-toggle-fidelity-guard/reports/mode-toggle-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Editor Mode Toggle Fidelity Guard Report

Document: manuscript-editor-309
Mode switch: wysiwyg -> markdown
Decision: block-mode-toggle-edit

| Severity | Finding | Action |
| --- | --- | --- |
| block | Block methods-p2 lost equation tokens during mode toggle. | Keep the pre-toggle equation tokens or require manual equation review. |
| hold | Block methods-p2 lost citation keys during mode toggle. | Rebind citation keys before accepting the converted edit. |
| hold | Block methods-p2 lost notebook-cell links during mode toggle. | Restore notebook-cell anchors before shared edit acceptance. |
| hold | Block methods-p2 lost comment anchors during mode toggle. | Re-anchor comments or defer accepting the converted block. |
| block | Block methods-p2 lost tracked suggestions during mode toggle. | Reject the conversion or preserve suggestion provenance. |
| block | Locked block methods-p2 changed during mode toggle without owner approval. | Require lock-owner approval before accepting the converted edit. |
8 changes: 8 additions & 0 deletions editor-mode-toggle-fidelity-guard/reports/summary.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions editor-mode-toggle-fidelity-guard/sampleSnapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module.exports = {
documentId: "manuscript-editor-309",
fromMode: "wysiwyg",
toMode: "markdown",
before: [
{
id: "methods-p2",
locked: true,
contentHash: "hash-before-methods",
equations: ["eq-growth-rate"],
citations: ["lee2026assay", "patel2025model"],
notebookLinks: ["cell-fit-12"],
commentAnchors: ["cmt-44"],
suggestionIds: ["sug-91"]
},
{
id: "results-p1",
locked: false,
contentHash: "hash-results",
equations: [],
citations: ["nguyen2024dataset"],
notebookLinks: ["cell-plot-3"],
commentAnchors: [],
suggestionIds: []
}
],
after: [
{
id: "methods-p2",
locked: true,
contentHash: "hash-after-methods",
equations: [],
citations: ["lee2026assay"],
notebookLinks: [],
commentAnchors: [],
suggestionIds: [],
lockOwnerApprovalId: ""
},
{
id: "results-p1",
locked: false,
contentHash: "hash-results",
equations: [],
citations: ["nguyen2024dataset"],
notebookLinks: ["cell-plot-3"],
commentAnchors: [],
suggestionIds: []
}
]
};
Loading