From 9d61fd238838ddabf1f745184841e309e8fc51d6 Mon Sep 17 00:00:00 2001 From: davidrsdiaz Date: Thu, 28 May 2026 04:40:20 -0500 Subject: [PATCH] Add collaborative find replace safety guard --- .../find-replace-safety-guard/README.md | 21 ++ .../find-replace-safety-guard/demo.js | 15 ++ .../find-replace-safety-guard/index.js | 248 ++++++++++++++++++ .../make-demo-video.js | 157 +++++++++++ .../reports/demo.mp4 | Bin 0 -> 43296 bytes .../reports/find-replace-review.json | 141 ++++++++++ .../reports/find-replace-review.md | 25 ++ .../reports/find-replace-summary.svg | 20 ++ .../find-replace-safety-guard/sample-data.js | 102 +++++++ .../find-replace-safety-guard/test.js | 33 +++ 10 files changed, 762 insertions(+) create mode 100644 collaborative-research-editor/find-replace-safety-guard/README.md create mode 100644 collaborative-research-editor/find-replace-safety-guard/demo.js create mode 100644 collaborative-research-editor/find-replace-safety-guard/index.js create mode 100644 collaborative-research-editor/find-replace-safety-guard/make-demo-video.js create mode 100644 collaborative-research-editor/find-replace-safety-guard/reports/demo.mp4 create mode 100644 collaborative-research-editor/find-replace-safety-guard/reports/find-replace-review.json create mode 100644 collaborative-research-editor/find-replace-safety-guard/reports/find-replace-review.md create mode 100644 collaborative-research-editor/find-replace-safety-guard/reports/find-replace-summary.svg create mode 100644 collaborative-research-editor/find-replace-safety-guard/sample-data.js create mode 100644 collaborative-research-editor/find-replace-safety-guard/test.js diff --git a/collaborative-research-editor/find-replace-safety-guard/README.md b/collaborative-research-editor/find-replace-safety-guard/README.md new file mode 100644 index 00000000..f0301d6e --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/README.md @@ -0,0 +1,21 @@ +# Collaborative Find/Replace Safety Guard + +Focused slice for SCIBASE issue #12: a real-time collaborative editor guard that previews global find/replace operations before they mutate shared manuscript state. + +## What it checks + +- Protected section locks and final-review freeze state. +- Citation keys, cross-reference anchors, and LaTeX command names. +- Notebook/code cells where semantic replacements need explicit review. +- Inline comment quote anchors that would no longer resolve after replacement. +- WYSIWYG/source-mode replacement scope so batch edits remain reviewable. + +The module is deterministic and dependency-free. It uses synthetic document packets only; it does not connect to live editors, private projects, external services, or credentials. + +## Validation + +```bash +node collaborative-research-editor/find-replace-safety-guard/test.js +node collaborative-research-editor/find-replace-safety-guard/demo.js +node collaborative-research-editor/find-replace-safety-guard/make-demo-video.js +``` diff --git a/collaborative-research-editor/find-replace-safety-guard/demo.js b/collaborative-research-editor/find-replace-safety-guard/demo.js new file mode 100644 index 00000000..2fd621f2 --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/demo.js @@ -0,0 +1,15 @@ +const fs = require("fs"); +const path = require("path"); +const { replacementPreview, renderMarkdownReport, renderSvgSummary } = require("./index"); +const { riskyDocument, riskyOperation } = require("./sample-data"); + +const outputDir = path.join(__dirname, "reports"); +fs.mkdirSync(outputDir, { recursive: true }); + +const result = replacementPreview(riskyDocument, riskyOperation); +fs.writeFileSync(path.join(outputDir, "find-replace-review.json"), `${JSON.stringify(result, null, 2)}\n`); +fs.writeFileSync(path.join(outputDir, "find-replace-review.md"), renderMarkdownReport(result)); +fs.writeFileSync(path.join(outputDir, "find-replace-summary.svg"), renderSvgSummary(result)); + +console.log(`decision=${result.decision} riskScore=${result.riskScore} findings=${result.findings.length}`); +console.log(`reports=${outputDir}`); diff --git a/collaborative-research-editor/find-replace-safety-guard/index.js b/collaborative-research-editor/find-replace-safety-guard/index.js new file mode 100644 index 00000000..527afeeb --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/index.js @@ -0,0 +1,248 @@ +const crypto = require("crypto"); + +const SEVERITY_WEIGHT = { + blocker: 30, + high: 18, + medium: 9, + low: 4, +}; + +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").slice(0, 16); +} + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function buildMatcher(operation) { + const flags = operation.caseSensitive ? "g" : "gi"; + const source = operation.wholeWord ? `\\b${escapeRegExp(operation.find)}\\b` : escapeRegExp(operation.find); + return new RegExp(source, flags); +} + +function blockText(block) { + return [block.content, block.key, block.anchor, block.caption].filter(Boolean).join("\n"); +} + +function countMatches(text, matcher) { + const matches = String(text).match(matcher); + return matches ? matches.length : 0; +} + +function classifyBlockRisk(block, section, operation, matcher) { + const findings = []; + const text = blockText(block); + const matches = countMatches(text, matcher); + if (!matches) { + return findings; + } + + const identity = `${section.id}/${block.id}`; + if (section.locked || section.freeze === "final-review") { + findings.push({ + severity: "blocker", + code: "LOCKED_SECTION_MATCH", + title: "Batch replacement touches a locked or final-review section", + evidence: `${identity} has ${matches} match(es) while section state is ${section.locked ? "locked" : section.freeze}.`, + action: "Exclude the section, unlock with an audit reason, or convert matches to explicit suggestions.", + }); + } + + if (block.type === "citation" || /@[A-Za-z0-9:_-]+/.test(text)) { + findings.push({ + severity: "high", + code: "CITATION_KEY_MUTATION", + title: "Replacement would alter citation keys or reference anchors", + evidence: `${identity} contains citation/reference text matching "${operation.find}".`, + action: "Route citation-key changes through the reference manager merge workflow instead of direct replacement.", + }); + } + + if (block.type === "latex" && /\\[A-Za-z]+/.test(text)) { + findings.push({ + severity: "high", + code: "LATEX_COMMAND_MUTATION", + title: "Replacement would alter LaTeX command or equation source", + evidence: `${identity} is a LaTeX block with ${matches} match(es).`, + action: "Preview equation output and require formula-owner approval before applying source-mode replacements.", + }); + } + + if (block.type === "code" || block.type === "notebook") { + findings.push({ + severity: "high", + code: "CODE_CELL_MUTATION", + title: "Replacement would mutate notebook or code cell source", + evidence: `${identity} is ${block.type} source with ${matches} match(es).`, + action: "Create a code-review task and rerun the notebook before sharing updated outputs.", + }); + } + + if (block.type === "cross-reference" || /fig:|tbl:|eq:/i.test(text)) { + findings.push({ + severity: "medium", + code: "CROSS_REFERENCE_TOUCH", + title: "Replacement touches cross-reference anchors", + evidence: `${identity} contains figure, table, or equation anchors.`, + action: "Regenerate cross-reference maps and hold export until anchors resolve.", + }); + } + + return findings; +} + +function replacementPreview(document, operation) { + const matcher = buildMatcher(operation); + const scope = new Set(operation.scopeSections || []); + const changedBlocks = []; + const findings = []; + + document.sections.forEach((section) => { + if (scope.size && !scope.has(section.id)) { + return; + } + section.blocks.forEach((block) => { + const text = blockText(block); + const matches = countMatches(text, matcher); + if (!matches) { + return; + } + const before = text; + const after = text.replace(matcher, operation.replace); + changedBlocks.push({ + sectionId: section.id, + blockId: block.id, + type: block.type, + matches, + beforeDigest: digest(before), + afterDigest: digest(after), + }); + findings.push(...classifyBlockRisk(block, section, operation, matcher)); + }); + }); + + (document.comments || []).forEach((comment) => { + const changed = changedBlocks.some((block) => block.blockId === comment.blockId); + if (changed && comment.quote && countMatches(comment.quote, matcher)) { + findings.push({ + severity: "medium", + code: "COMMENT_ANCHOR_DRIFT", + title: "Replacement would invalidate an inline comment quote anchor", + evidence: `Comment ${comment.id} anchors to text that would be replaced in block ${comment.blockId}.`, + action: "Convert the edit to a suggestion and re-anchor or resolve the comment before applying.", + }); + } + }); + + if (operation.mode === "wysiwyg" && findings.some((finding) => /LATEX|CODE|CITATION/.test(finding.code))) { + findings.push({ + severity: "medium", + code: "WYSIWYG_SCOPE_MISMATCH", + title: "WYSIWYG replacement crosses source-only content", + evidence: "The operation starts in WYSIWYG mode but reaches LaTeX, citation, or code source blocks.", + action: "Split the operation by block type so source-mode changes receive explicit review.", + }); + } + + const riskScore = Math.min(100, findings.reduce((sum, finding) => sum + SEVERITY_WEIGHT[finding.severity], 0)); + const blockers = findings.filter((finding) => finding.severity === "blocker").length; + const high = findings.filter((finding) => finding.severity === "high").length; + const decision = blockers || high ? "hold-batch-edit" : findings.length ? "suggestion-preview-required" : "safe-to-apply"; + const packet = { + documentId: document.id, + operation: { + find: operation.find, + replace: operation.replace, + mode: operation.mode, + wholeWord: Boolean(operation.wholeWord), + scopeSections: operation.scopeSections || [], + }, + decision, + riskScore, + changedBlocks, + findings, + actions: findings.map((finding) => ({ code: finding.code, action: finding.action })), + generatedFrom: "synthetic-data-only", + }; + + return { + ...packet, + auditDigest: digest(packet), + }; +} + +function renderMarkdownReport(result) { + const lines = [ + `# Collaborative Find/Replace Safety Review`, + "", + `Decision: ${result.decision}`, + `Risk score: ${result.riskScore}`, + `Changed blocks: ${result.changedBlocks.length}`, + `Audit digest: ${result.auditDigest}`, + "", + "## Findings", + ]; + if (!result.findings.length) { + lines.push("- No blocking find/replace risks."); + } else { + result.findings.forEach((finding) => { + lines.push(`- [${finding.severity}] ${finding.title}: ${finding.evidence}`); + lines.push(` Action: ${finding.action}`); + }); + } + lines.push("", "## Safety", "- Synthetic document packet only; no live editor or private manuscript data."); + return `${lines.join("\n")}\n`; +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function renderSvgSummary(result) { + const color = result.decision === "hold-batch-edit" ? "#b42318" : result.decision === "safe-to-apply" ? "#067647" : "#b54708"; + const rows = result.findings + .slice(0, 5) + .map((finding, index) => { + const y = 215 + index * 54; + return `${escapeXml(finding.severity.toUpperCase())}: ${escapeXml(finding.title).slice(0, 72)} +${escapeXml(finding.code)}`; + }) + .join("\n"); + return ` + + +Collaborative Find/Replace Safety Guard +Preview batch edits before shared manuscript mutation + +${escapeXml(result.decision.toUpperCase())} +Risk score: ${result.riskScore} +${rows} +Audit digest: ${escapeXml(result.auditDigest)} | Synthetic data only | No external services +`; +} + +module.exports = { + replacementPreview, + renderMarkdownReport, + renderSvgSummary, + stableStringify, + digest, +}; diff --git a/collaborative-research-editor/find-replace-safety-guard/make-demo-video.js b/collaborative-research-editor/find-replace-safety-guard/make-demo-video.js new file mode 100644 index 00000000..89244b77 --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/make-demo-video.js @@ -0,0 +1,157 @@ +const fs = require("fs"); +const path = require("path"); +const { execFileSync } = require("child_process"); +const { replacementPreview } = require("./index"); +const { riskyDocument, riskyOperation } = require("./sample-data"); + +const WIDTH = 1280; +const HEIGHT = 720; +const outputDir = path.join(__dirname, "reports"); +const frameDir = path.join(outputDir, "ppm-frames"); +fs.mkdirSync(frameDir, { recursive: true }); + +const GLYPHS = { + A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"], + B: ["11110", "10001", "10001", "11110", "10001", "10001", "11110"], + C: ["01111", "10000", "10000", "10000", "10000", "10000", "01111"], + D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"], + E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"], + F: ["11111", "10000", "10000", "11110", "10000", "10000", "10000"], + G: ["01111", "10000", "10000", "10111", "10001", "10001", "01110"], + H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"], + I: ["11111", "00100", "00100", "00100", "00100", "00100", "11111"], + J: ["00111", "00010", "00010", "00010", "10010", "10010", "01100"], + K: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"], + L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"], + M: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"], + N: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"], + O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"], + P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"], + Q: ["01110", "10001", "10001", "10001", "10101", "10010", "01101"], + R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"], + S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"], + T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"], + U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"], + V: ["10001", "10001", "10001", "10001", "10001", "01010", "00100"], + W: ["10001", "10001", "10001", "10101", "10101", "10101", "01010"], + X: ["10001", "10001", "01010", "00100", "01010", "10001", "10001"], + Y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"], + Z: ["11111", "00001", "00010", "00100", "01000", "10000", "11111"], + "0": ["01110", "10001", "10011", "10101", "11001", "10001", "01110"], + "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"], + "2": ["01110", "10001", "00001", "00010", "00100", "01000", "11111"], + "3": ["11110", "00001", "00001", "01110", "00001", "00001", "11110"], + "4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"], + "5": ["11111", "10000", "10000", "11110", "00001", "00001", "11110"], + "6": ["01110", "10000", "10000", "11110", "10001", "10001", "01110"], + "7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"], + "8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"], + "9": ["01110", "10001", "10001", "01111", "00001", "00001", "01110"], + "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"], + "#": ["01010", "11111", "01010", "01010", "11111", "01010", "01010"], + " ": ["00000", "00000", "00000", "00000", "00000", "00000", "00000"], +}; + +function rgb(hex) { + const clean = hex.replace("#", ""); + return [Number.parseInt(clean.slice(0, 2), 16), Number.parseInt(clean.slice(2, 4), 16), Number.parseInt(clean.slice(4, 6), 16)]; +} + +function canvas(color) { + const data = Buffer.alloc(WIDTH * HEIGHT * 3); + const [r, g, b] = rgb(color); + for (let offset = 0; offset < data.length; offset += 3) { + data[offset] = r; + data[offset + 1] = g; + data[offset + 2] = b; + } + return data; +} + +function rect(data, x, y, width, height, color) { + const [r, g, b] = rgb(color); + for (let row = Math.max(0, y); row < Math.min(HEIGHT, y + height); row += 1) { + for (let col = Math.max(0, x); col < Math.min(WIDTH, x + width); col += 1) { + const offset = (row * WIDTH + col) * 3; + data[offset] = r; + data[offset + 1] = g; + data[offset + 2] = b; + } + } +} + +function text(data, value, x, y, scale, color) { + const [r, g, b] = rgb(color); + let cursor = x; + String(value).toUpperCase().split("").forEach((char) => { + const glyph = GLYPHS[char] || GLYPHS[" "]; + glyph.forEach((line, gy) => { + line.split("").forEach((pixel, gx) => { + if (pixel !== "1") return; + for (let sy = 0; sy < scale; sy += 1) { + for (let sx = 0; sx < scale; sx += 1) { + const px = cursor + gx * scale + sx; + const py = y + gy * scale + sy; + if (px < 0 || px >= WIDTH || py < 0 || py >= HEIGHT) continue; + const offset = (py * WIDTH + px) * 3; + data[offset] = r; + data[offset + 1] = g; + data[offset + 2] = b; + } + } + }); + }); + cursor += 6 * scale; + }); +} + +function writePpm(filePath, data) { + fs.writeFileSync(filePath, Buffer.concat([Buffer.from(`P6\n${WIDTH} ${HEIGHT}\n255\n`), data])); +} + +function renderFrame(filePath, subtitle, lines, result) { + const data = canvas("#f7f8fa"); + rect(data, 36, 36, 1208, 648, "#ffffff"); + rect(data, 36, 36, 1208, 3, "#d0d5dd"); + rect(data, 36, 681, 1208, 3, "#d0d5dd"); + rect(data, 36, 36, 3, 648, "#d0d5dd"); + rect(data, 1241, 36, 3, 648, "#d0d5dd"); + text(data, "COLLAB FIND REPLACE SAFETY GUARD", 56, 74, 5, "#101828"); + text(data, subtitle, 56, 126, 3, "#475467"); + rect(data, 56, 166, 390, 54, "#b42318"); + text(data, "HOLD BATCH EDIT", 76, 184, 4, "#ffffff"); + text(data, `RISK SCORE ${result.riskScore}`, 476, 184, 4, "#101828"); + lines.forEach((line, index) => text(data, line, 56, 260 + index * 62, 4, "#111827")); + text(data, "JSON MARKDOWN SVG MP4 REVIEWER ARTIFACTS", 56, 592, 3, "#344054"); + text(data, `AUDIT DIGEST ${result.auditDigest.toUpperCase()}`, 56, 638, 3, "#667085"); + writePpm(filePath, data); +} + +const result = replacementPreview(riskyDocument, riskyOperation); +const frames = [ + ["SCIBASE ISSUE #12 BATCH EDIT PREVIEW", ["BLOCKER LOCKED SECTION MATCH", "HIGH CITATION KEY MUTATION", "HIGH LATEX COMMAND MUTATION"]], + ["SOURCE AND WYSIWYG SCOPE PROTECTION", ["HIGH CODE CELL MUTATION", "MEDIUM WYSIWYG SCOPE MISMATCH", "MEDIUM CROSS REFERENCE TOUCH"]], + ["COMMENT ANCHORS STAY REVIEWABLE", ["MEDIUM COMMENT ANCHOR DRIFT", "ACTION CONVERT TO SUGGESTION", "NO LIVE EDITOR OR PRIVATE DATA"]], + ["DETERMINISTIC REVIEW PACKET", ["SYNTHETIC DOCUMENT ONLY", "PREVIEW BEFORE MUTATION", "READY FOR MAINTAINER REVIEW"]], +]; + +frames.forEach(([subtitle, lines], index) => { + renderFrame(path.join(frameDir, `frame-${String(index + 1).padStart(3, "0")}.ppm`), subtitle, lines, result); +}); + +const outputPath = path.join(outputDir, "demo.mp4"); +execFileSync("ffmpeg", [ + "-y", + "-framerate", + "1", + "-i", + path.join(frameDir, "frame-%03d.ppm"), + "-vf", + "fps=12,format=yuv420p", + "-movflags", + "+faststart", + outputPath, +], { stdio: "inherit" }); + +fs.rmSync(frameDir, { recursive: true, force: true }); +console.log(`demo video=${outputPath}`); diff --git a/collaborative-research-editor/find-replace-safety-guard/reports/demo.mp4 b/collaborative-research-editor/find-replace-safety-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..dcd94d59e398e153d7d93b6de943551cb5187278 GIT binary patch literal 43296 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≧*M(~}t>)FjZJFTM3A@R9etFoXgiOmhdrj-e78Avpep3_p-^eEjMF}ip zH#A=$wZqfxAkAh@HGq}{bvl|l4pq->uZRI{=38Lgoj)3NHXI7r$lL|vI~PyGTn9;b z2iMk7{CwBMdIgfP_9j*2nR5QF9h_{@i(As2d3^tV8Y)V0fh>|-0F{AK=PAcww_EGp zE{4{fiCe{cM#2QQHA@d$gb33N9#1A6?dl0tDKdMs!tC798XiIQ^*aqD=ko>XGc6*q z>CLg1SD6zpNBuHT>VUSm@=Ctn4@^kHMj>Wj)ZuM zV1|^mGx>&EK2g{u?)Xp(0E5uy@#cn#ug+Z5YtwbPE8ZaB4&}KrL-mK0s78-fy(4Z%lh2Do;-zfSooV{IVlz#8)?^@@dMK zsjwqtq>R%$(0%va`86$6;jDuaxdAHYH1BVi!vICqSRd!Km~NJ`onSne!Fw=CG24Y_NPm!e#($8Rj_JCUhLYDPZn3vp;*ESZ0!3Av)r zJxM2)9S8Gm(g9vb#ZyEarN0-zZR40+0p{dvh?sTxIHADtxl%d-w%<)Fo{swqX$OSQ zd0`}T^@-8OUD$TX;5|*!OY=WU;EX5B@H68WA?5wCoaWN#lVqLa)4nn%TSLWdUu$6^ z4fBY;PMfZjU8Gk3rC0Lm9K{@`9Gs^r*6#PZOSaUMgLG50mx<$lE@uUt@k=@=aP6Pl z&cVW*lTc7NHJu00n(6J|r&$3W$FNl|+H669)zkh|o=@->?D)%%^ zzYB4Ad}w?RH))!@y_DFGjjrz{gDs076+KL7Xvv-=nslRhULC%L8H~s8*wCl-!=5nM za2{NE+n3B%QP=dK_6z-8Brhp{4T7}w7J0NMuyY4 z;{H3TnFZnPnFBI=Cd?Le*@7OV1XPfQs~6y~r8xpkO%*JOl$J1_p{x8noL?rL4JNaCkROZ!~5 zBcnvahJ#PC@&#B@{FRRf)%z~iakYP|y2IQkBM)is!ehs%n|9(725E)om$q92E(G)B z44u=D09Vk^K$b_Ik?m7H_g^D@zNrA8YSGu4e8KZ3&w2HTidsB{fVG%?67N(uOa;;B1WT z3Ij6l77n@^g3{7$iBsc0En98m|NN#K_w{R=SL?{Y=zBn#X^uwFLd)PLLgxm+w`|+3 zyG3`etR%v(V4qMg+>)ARk5et?Pbv;rP=DOmY%lWXY!=EYTwJ6J5ydgM{qY{th-C;N zZZ9MB^(mY*4QIdI)5qoidIs-VDr@p=AM2HH@?&Ov+|ygLUBZB}HcuMiyo5r$TJk0e zra-C2gkgvDuu54ak-rk^6mrTHNL3YJhojFe7l(ifC?@hi{F z)Uphz~@jD>3&p(txe#Q$X$YT143*1a{(hpXzwU3bNl$g+>CH7R! zKVFmAvP4*K54m8kmgADrp1FQB}#umT5sWIxD5fRw_HFD`zY+2eScJ{*EGVZ~^1ZLEbfT_ezO*gN2wCKqXs@avZ?Af|UUJsB*OG~nh`X{lUEn43 zUrP^T$UQlDqc@Yh-i};`2r|5_-7|LEsZ_Rq@kk)~a;QN;|7b7o#9}zgc}X2g-{y$W zMI%)L7gU|iA`oCJPuNlu(dTyuobjQZu}yf}J;IKbg#S~@-HW7yh9>YN(N4jIW_lLM zy32C!e;S>KkQqTTKo&&Mu}ya$oXB4f{t~A&M#?H?_M1hA4TW$^_3Ad!4^!!FM&c zQnI=tz0g@vpYFArh7HA`Is0Ou6&)DL{kt%mmad|;=ckYmR}ILVO+{SS&0aNk=yE4< z#DJgMvct~iCr6`ro)k!b6oe#KqDgzGu4FKVBB$!VJ&Oet*eVZcBheR7L4%om&2gck zS793l(r4e)7&=BT*Do&~CK4`^F%OqvhPm5-##H za23@zwuB8$p#b`iCTbEdnMd6IT^OTVjD*}!k(l;N~*(k;7gREy*g(i=>$jgix460|KnUMxNq z=LQ8e3|R$@<@$1g%G(UtEhbTyYkm+eYnsVOO!bc1ZPj?GBq*xhVK3CfKd^lm2(S(V zvv%4GcC3)vZUKP;q9iCi?Erl6I!q)9^*AI1RzF|8?ME#`eAKxn#+QvjvqbIJ{|pwP`w;o--DD<- zvSlW=NrL&$>9~!7ACwYsF#c#v0;E=@Hn(-0JC_z_0cc395!c0#rF6|(azkw z3*t5Amm7GTz4FU3-y|KvS~@lV=Mg%M05+;AdYY~`NWX-ncJ!%1M16exi<(t+t6e+Z z(N)gx6|=qzKqQ#Chr&=g&Va@v1g@1~nZwWM+Lo8-ID{zV!;mjt}Et35lOxQb)H zz~!9dO|5~zOE;eJb=Y>pBKKy*Bk*JoxDu02^CM6lx^+9+2bqT62G^$avjDCR8~>V% zVt>ZlKBT)1$MVHxR?AcMT0ZMtvQ+R^%E5$U$|TBZqPGH6)jO*071r=*Q292(!9_8< zRs!lJ66@m+avU@jV>F{Zd>^_Hc*n2r?sY`^+kb>@t|*&y_b=k4eTHz5OY*Ub7S4>L z7(2;eW|LM1-odz_FT0B!B5j3R*+PrHtduE1G|ovDWN!#pC!SH=^vH@6Uc3p6u~1z- zYvAj0JN&G|Fib_tA@&v8nu}=S=-bl^*jVSrD|o^M5|%}FSaMQ`Z-&Q;VT=*MOLQf| zB~VkFifx9!P!5&l-OSv^WeD6b|9)eGbIGCMy-@mmDD|sFO8b2H60Z8LTC!=>W9Dm{ zx|B$j4Rliq=h#c~5TbG1pn0D~4XVSkU-6%?yI+Sii&*lVri&O~-{G$PDXJeZdim--AytkS= zKRUhNcpemO2#FT8zCzT`?dZe`>p!7R#m0YDXVI%Rk@X3ks4cmWTLOFQq+FpXf^u8@ zbK(jn@@!4L*2LgEAx3adUWIi@;_&`$S<=)?rE7Hrm3*ohiAVqsI0WmJz7#zQ@JBEg z-_@hBd5q`u8hohx2IKle&-D(BmM*>8-zmE!v{DaiA=InkqLVLm9$^jmc- zqBG=+^zqfEj?D8ZmA36ej(P%+r+sn9)-}i}_HFme&e(Q1C+Rs>R*{q=-$Y9p5yX13 zXR9{&GL5%1GRrIP^?trO)H-rgyOu&X!B_Gpo>7-&A5Kq1IReS5J5yd0g9jxy_P!K# z1xyz!6d89XBVuqMunzcY_|qt`MfrOGx=y!6iw{yo%;ETYKtN^G1H7(wB!g-ev-`oM z$ts5S!*0Du2QbUOi@#N3T2}kl8(OjBIFdzSw2^&FMsfV>g28^=Qx7V3D(hx&H-CF? zTgqdk?XuFf1-;=y>$i2gj)83Lx_G$vAvkogoLKKjZa+O|!%k%(n+hyTvFt-jK}B2- z>AbQOZ#?vrFNJyr^}ZW%fehj6TErw9hDYoNJJK=g#0~ZFEcTMi@unX7?wfDHieThV zgO+78q*#D z1pf+@yoVv{K^*<|o~-YAkX!k^21=d8uTnF&Es0JXD>!+0<*!de=SvJs^Mhs>A^drZ zShh-a(NEdQ67X5Eu((P{W-nm|N5whf0p{R<+g$jfWNM$-k|rXdES5LZ$)z}d>*Vzq zboEDkuOq8(!yLtb2ZJb{#pjS(6$a#-2)17^ME9pgJV zbUy%n?F(a>(zOKQ|J)?>4d)HA)-yizB!E?&a< z1_skHY_Fz--p_dV?9g`L>DG~giG|goKw{5|CQ|ms(x+3RRjbrklDVj{Wa8s^%BfKo247fS+Ir5B+vLG?HcO zxtee4a$_}$#wGDR-RW|D`1gx1J{v5;IAQo&9DXC{S>3YcYSZ$EHEYHag^HN!-hkBC zvzmg>MVq!SZGN`m>feu1ldv27JKOa|j>iSRHF$~&7Ry=A8(d;F_h)0BfIqVxDe;j* zB@Q`0=)Sp-_eQ_c49Xu&*iNH25eN2UO&u@AC zPdb2Gd|=E$rrC?qSh*u7ql8EARlwZ}`%Xl9aI zV&-*W^F1%()n?N(SO<5c6E7?I7i+5y?9i3Jw9kBpJR{S3y;Tp-W%-iXI4Fga5K2h^ zn|IP$s_SA@e-S#F_AqS(#H8{@g~NYQaC~_fp|E8gi2LSI+Da-AOf4*?f#xG&-|CBL zd(zbt$ye#{IW|y2mbrAfQR9o%*H=i&raNW}cZ)5IKS(_6IhBR`UJ)4^K3Gd{Pv9a# z?fVaNr^9|%)cGbWL?Sx3M=X%&sh2evenkTk0VN#P6gsG=_Jaeb;~a%%yWV%k6x|Ox z=9BsP6iY1KY7P(mad)Es##P%$-kHy^xd$VcE`y(_b`Q+tW}8`Qx;8t*dvzodW$U%gJ$O_)tha80 zTW`4b5bZB7* z93{eH-ed*zr6kGN`yKiDJH~x=+9CLywhND20pP3@)Ycey+f#kV4CXtA$6V&LADVOD zgqaQJ_lV~1t4%Fwz2_RVoUA({4b9JVX;M7jF=4(K{cM6b(3md|+57g@GtNeSf!;&p z-EwcH9fSSarIBXKgNiMUK*!8r=a8CVHJ?y&`qF^-Xf`_m#Kblmi&qT9N$ht!FOjfg;opYj{-v`&-Mvn{R^u5lh6C@s~Pc@)TVzoyCFl&XNaSb%iMrC zxKC~Q)WQ_J*LG{IxbpV$Kcl*90BP>qswy72m-a7ckEk+XMy68=-Vb9-#nSX+%hT}< zE%X;KU**S@nNk&*E_({Z0RH2A<_oJorRK*+3y_34+-*<>WckTU`~%ObKPU7A5lOb& zQW^;9h+-ZByFJt#{}AF){PS)X8+(~ZAUgK()>?&O8lf;4>U}LReB|fTk0NGwvrj@= zkL|aby#eBQ1qg*$-(pg_yw{>`hp6TC(t8=Y${U5$Wi zU2HX@YO|#wQcrdF4t;dDOjls*R}?(W=!}1HsThrP5oY50j?S4xWlafwlcsyuMkPhu zNKG6x#_)=B>`~JpX@GZb`Ez|9hK({LQ&Oz^vpZcKaJ^N49~0$k^rut7?{;#^J2I_o zxMyVq+bA$S>SI`GCdhr3&*}&j7NTwn*MsnwBOq~4^#4NT5y^fr{ z*-=zI)xcRCIk1wYH7cxPvZB`h?Q=#0`xV0HZkGaa`fHgB?}A&ZKfhuW^3;$tsM+@o zjQGk+tgj_e-!sW(kKX_ITE4S8pdvkbf90LW=UZn!?Hj>On-3Fq%p~(Mw`HkqW`v4U zE&;ONqBRBqJ}EhqLlBMbv`1Y3NF-T0BS6{nuP)l)A^hH92LAPl7G8AG8WVTr`?^Vj z6`#edYeXw@of?G3`JF>8%8vc*rB;toqVWU{g#QP54ZnZmyb9T2 zj(VJb<8t%B*(s{=dHWqXhO{eXZ=C3;%stlIIO1*DX<|E&>()GgplOp93MtRGH4~h@ z%yy%z?%@yBVEKS^_W7&*c!q^u-J}k z$~~Yy^)M9ZqrCd7E7;5q$)7vkK$GrqYZtT62B3f^KL=TY1u1G{=p5sgxL{_$AQSyq zU$5Is=iP`Vc%={!tNIY^S}8gps=Cxa%bG*W)nDZ{8 z7!179|4X#N2$Er6C>V8khfkTKa#*lM%^b zI##!L;J;cMEKYX<%+SU~VT%S5$OJ=QJw$NGPE-T^uZ#juTFP1~?!v)v+Qs2>f0MV@ zYmz682OBANe#Tzmd+3WgJAof!0>Cug!CpSzi^Sx6?$f-x9XzYHy!}W%#5HD=*GQX) zA&=Z=kgsCkML<5sEO)I@vpWMJ#tfxrwwgA&^B~;cPHWe z$oF(DC9NbL7WDb)eGQHg=v%e)rpY93F-#LH0Ssw|6`u2gBF? z-0;!?&IazcY5(^~U|zf+fF)M;&y7EO@RO>mJdBFrJ!y3=A6+&Cgh=rO;txSE-BYxx z22pDabWS2(zsad!-!6lcqlyDXq*{anL-Prs3M|enWg3fim;J^XAKR_eSxC04!seI@ z5{+=9A}pcEgsKH_`?sM=cS|OMfj7;$=`Eg0W%#;e=mL{KE19Cg%j`iXdz8fKjWN|> z^fc2b-lu#pns$5mi{E+8G+=R;i>mi@3$)mUPscRq2D}eSt}{GHgF5Y6cD^~;76wf) zx%Jb_YkEqJTy`jp`XOHnk1WZS(7wzL#Hc0}PHGgWA4`+R2=)#o`cjBJHj(bKQr=lu zv7}U^kg6b)?!iVah@xw)G9&(&&p}IGzzDu!Xr1Be8-0}CV_{BYHK(%MRw$_#8l;RL z@*KIZFVRjh!17ePG)(OkU(R6AvG1Q=O&C0ms-Y)IYq+nHa!CbiMCF96GNx(SVzehnUNk3Fqi(^9TVBDQnMqcGa(31`2?n#< zU!wMY!Cq1#fh+fhdXp#N(WVZuGtG=l8q!Axhrum7hFM0B_1zOzzO=1&?l~@i`i>o2 zrCeaaOHH`^__xUD!-(hKeY6K`WKy39#V#k6m`~EEe~k%Y(|U*eD2YVzkyOY1;?z)i zFF$eLD!w3-@}`FLVxDk}RNWw}92+6`by=v7u;GseAR<@D=_ZE67&}U&Aq~>}0oUaV z$?M0k>mfKaXqd^W(9EgHqZvteEzkt-bCR`U`cW65<=Wv)#Ew9;PImRV8s(bo^!Umm z?kb*L6epSp<9L~LPhe4@xC~VcypMCT|7C)HRfERr>0$fD6MXD~8^1u~y~TC@f^^qn z7)hP)#6FPVCtJfAk5jeqfDKh*mlc#H==~3H(a2nDEFaIf6O|YRp3b3Pr`9S*)cFDI zDH&G{Kz#~r*ZSY5elJe@#GGdt-+$6?Jeb8s?x z6(c=n^9gdU#HHEuZn!}(;~1eimL1;>SQ?61Q>gl&T?kCzeIqWe@tIVRHvw$POmXu+ zYlO*7no&_>1!!$!Tz#cIp?9_eUt(M?I!7IBCMy|l$*|Uw*4;Q+05p2JG0hLWNst$9 zmFK=PuimXv53J2^DZGegg40ZVZyxS*F)tK9|EP-}^qII?W(j zIMih3;AJb*_&`#OrAnvD2`7{8pXTDuMl`t!H((sxQ`i zJ{zV(IA3>{i@F79&337wnMt>N|gUimq@2Kn8^#a$^fjT8ock>s5o99$3; z+XI3#A@}KMsi1Jv9W3B>ZmSl{-ntAP=b5Nbf!@wbub(z|i_pOQ zvIH@gfc#cRPer2-uF@{ddqEgA!oY$fHhoGi$nBE;^M!9qeBw?g)C3;%#-(!OkYYjk<$kCntz`uJaQbW z9-!l6%{1i!;Z3D*A@Btg+9OV)c|qvrHw!xN3P!Xu-V%SN`o3sQ(LApF^U$u+JJpm- zTtrj1{?chBSi#dLgCXxnI@d~IS6{!o@-E^679`Oo-w$3bahAR!sUVLHAnY}yuhn(p z`u(|KlsRWs;j!zc{;L?0hWQa~Cyav|AEwH!M)aTXnImnA8IP7M?ReoTAiiBkDFt$x zw;nf^pi5MZGXcAIl@?qVMOA*C#Y0D;-ISa9-I@P}tMOgOe9yckg|e5ySHq4mVOf#t zxycT1NpH~C%!``o6~ofI05USVLg35~*Hk`d&~FBoX}lD^xdn#d>-z=zP$0n`;dI|g*vyJWrQgGUmv4C}FLssmLU&1~qd*7Z8%emzAt5A1r zU)>22r86J=fO$o~Q!<|y{iJU6;sa^A9+P{&(pzw&Ztp9(OB`7K$rug3wT_!47TeptvhNshRL8I>N7e61G!p_}Qq?0rVv+p+R;lX>9@4)8fKlFiE z^1D5DXdk1QdVfyy8;XrJo14%Ctb?h;b{SGR*p19##I!3gy#Hn$61(9&hsc zb+d;J&ha-GFSa@p1$P!=dM^WvaQo^QCpg#gr~nku@AhtdvJwk9`AankBROuEe9`vv)UH~Rf;NJKL zI8%@B_rEGSCNA-82$Y<2mO+iY{e9}@z}=lPVT_MzjeTF)1^3t+bd4qxDTIATCIUB) zrbwFX*VV}E$UW6HANcl}CQdf?Eplpw^=2bJJT!%1>G$nH#hsmVgrCro;-*!Ssy5^JD#O`)Bme%!Awe?kKz z?m})Kej>~GxN4)ih1bSzk^G8;$KLkW5J#akyeZF*Fobm`CehG~b`%yGGw{j>J3LP> zThaAC1-CX=SX{bjXX~ElsD)}P{LnvT*_yF3TP`dKxSV+K$S+fGnlHYVzRq0ywl(}1 zv(uw41)rbJ8#9Sg^=PdCS>E5@ApUp(NX%uN?0tNma>r6C4me^eZ<(L(s^an1p? zex+&GdVF4Z=WF_7J-K9Y%_wr_Uo(Vu8->Ka@NQxE{6!Bg&1{O31vXKE^Mq6rWP)5Q zqDSaYTfvO^rH8O^!5MV94h->1%bSP5$1jsG&ugipG8TCVQCjV#$DqXumWN^>?E(%W`qb9AXle1SDL5_(tU?#jfA9- zUY=gTeqYxtRA~`*;|{ZJN>;A@UHuK^R_FG4=giX6=%CJzYNy7T@!~WnF$Gh#*DgF+ zAv2hWE9qPKx@sFW>blmi+*v%2M2!?{M=12qq+R|>#E#}w1vM#y6_{ETsb(EN%)Ehr z0S2ci?tQJM+8uK4XW2Y=fbnwN$oo(ovT2Mqjk)A;LnI-hUGsJ=7sUW zA|oj+f7^jF89Zl+P7(NfZ4*(S+6!r8rwqn^O15xESRJk`YMm_Ng8sN?J6lnVo+pk4 zO26z!gVoxgutK=~7CYe63@71`cKemcmF0)^Y?(@USyZwxu`7h|R_dSx2NfLRcM}BO zXN7E#vNrv-eQ;8;!NBHDAar!eiX>Aow{-&Bw!JqOc%54zdmjs#@3OuAYZg%X3a+er zd0U*GmgXA#e4I1v)W8>+ubM4{5q~Lz#SY5+`N^1Q%$t$hY_PjDlf?;) zO__P-VE^py7*d6>2jPuSK1+%O&n0)TL!^9(Dj$^pMcld~11Z$A&1){QdJPG?ZX7VK zX*u~lkeQC1E?(-fz9x-5)Q!N5<)9W~G4Gc3471%=2kD)g9(?&NU5?uXvoxoqe|lOxRmkK~f-m9_ z6po%`X|K3k583U0{bStc$R*r-R2WOqy)Qe55gqWCPa>=e0nR2IrC=2h7@C|O*cANzNj583algl30q9SMzgJAdLN zCVs2wPw?$hAhgB(;^!`mi*o72DQP!8rzx0J9rinAE+cjh#H7p?U7qpH5cgpWGkA29 z-5heA{zPN>msn>$7u35Y$9~Mdjw~t@2h=@C&gQ8*_%KXk-q|q3CJnME@$9G~0)kqK z_V8H=(F>0T!mt9rC>cMeRMDYO!*zP=QwnhT#RtYbCMYheP@m8I5)Auinl{JWQ54oU zy{_bA6s0rOMJC)-$4eXEn>iq|(i2*3?C|29Xc6k(OP`;_yPa8(`j{YQB&s3IC4gnh zZG=b*sf%a=xo?EpYLd2|-w9+m3rjp3jotUQU9l`Ymti;xc%9$U0?n;5NPI83-=iWgK z^04FDAtE2Y#j43!$#NM_F<1gJ$dl_Ti5p3T5xK_&u&vd)e><${TsKtBfoOn9L#0PH zz{7aTfI{p;{Lq-|IjK z>URn5T-WCvXZFl@rI^_F1g)M9L}2EkSYI6lTQbm+rd1N&yr7(tcr5N$Z!Rccq+@#k0FklVKvB7}7IO8oU2;yt03B5yppf~iZ zS+DMu{EcA}x5)$FZo%~7h5$>$+6+zZFQ@)AaPG!BS+7G39~Oe^*zGQQ;XIC}oa&bI z$1FPg&*u{MNpfpNOZO~qMcz+g}-!@xM1oSMXP-a6OgX3EMtsyy=0l5oq zA6;)-x2JDa^8!0&B;%)-8aywaXS76qDqhAuLF$&}f&;pyiRA6{nJ zDQYyf8A{@)mdv>ux2ouj5{%B~zte^jIr5~Q0sKhjsF);AzK9y^R`oll!886DVTY=I zlXl9cV5GDy-#yx)q!_bXaDb>}eFwuTjSkq9w<%t7{6#}4317)z)qtqv3ifk_>0$hX7D%sz%n9*LiFLC3=>Ckf> zTkzV7sbJ)@7zy{7Q?IGfg(P?YaNFT_g_&$s`)9G8!D0Q|NLAbvHW)RGbD-vR%+6k7 zek&n)Y;l^><4%XcLl;8-58a#s%{d%x5;_H$p9(DH)X!|n4+;r`%DEl+9X!hqPmZ9c z0r3vDzOGV(#N_m`y|1Qn)wng$$_+8PC{G2Sk3SxRc6!2!#1m|$AHUg`uN0?us6s|$ ziXi1(HPn8M8dT|)4R(Z*S_loD_bNsSWp-%QYQ)v&*C=UupIu}!zE)m8Z(s*p+|qo~G8WxE%1KTd=IJ?PqO1J{ zCt`HR#hh`-q;(=D)zdQ{^3iHvHlU~DRdmP`*;fuaIoJ@Bj@}8tFLezBD;dxkAA~G5 z$tS3VyZfX|!&oDSR!+2<8SFx{o3oaQFBv0Uiv`}VKOZkd+x1`-k}a8*7g$Qa{lfCB z=pJvrTGfX-AalqYxtx%Y99OnCe`OcUYP))kDJd8hKZJB$e7{dP_|u1z3`@lS!GVEz zrvV?(A$|*^vwy!v$m~Rc4nDVN$0n%wCY`-7EjCUv*R!W5pQqV79g^XqIy!ZI`R<^; zYWj}NQ0u{y-M_Ga_K3q;2-^}j=sm+G^($WH3sCGn36PUoM3Cf^Kt372i0#eVkY{Lw6LVxEh!W=~6`TR)#@`QiH>TpUiq zcB7Z=vCdp%~N8fSvw-^-Ikck~7ub4_31W=vxOG%wWQvm@v7#P7aOGq5F zLvm0vFwBN=I6neTQ1vdu*%Nd}>O5NYwo1B{CqX&o<;+4vCG2~>eO~pMF{(7bN|T(geEpSRf3ku-3y0j=n$xC_1beoWz>k>V84w=f`y}yYB-7T<#!+!b z_4ymJ+146(pf9eqNqk!nz^3biVKWN;*Qakv;yROAjTY;sg8Xxj*Ior(8omdmmo}=x zOP9L4ep`bSXO7ho_C3}4#5^LEf3YjsbSWlB62juIA0dc!o3UAa88#E?jESxjp`(l2 zXh!hPom8KKX!7U7W_t#5^F#B%EdEY9y~GFMO2GRC*!Ux}EG+{;_78-Lat_4Qf5?I< zk%I5Cia!01zQWvttuQg)x`6iPYUm5?rLa=*wiHHz#Qe1LZxf+An){`$Y5bt~$?^p! zvyb8FbIna{VbbnG&A4_{`|wqV+^C0#Uda&I@Zou-ntjA$otCLAHm2>h&nDv|cZJc? zua~DI{jw?*_O?N3x~o1PzJpC7Vg75oefAaQK~ezcd4i#8MDWQc;La;GuK#ozT(P!J zwADCm7j0bf&Dh(ijuHdqyp~?&a3nCB)%ZOLvJlr zjl?|OBO5aCwrrRl-N8}n0=S?PY@zA>YS@-g)iwXUXQ--Mf#Y$Z97@(=ItB+RDNlCl zFI2nN1~MbspA-@3;cBGN?z#++p$Etb2jO3M2hWg#CY|}y>eu_0*~N=S6GDE6Ta3Fb z;12JVgapVq(?TpIL7&Bjcx^t@t6xw3f&03)jj0BiyVEv`GSHzx) zxoo#1(1{6yM;oD71HOGpI-uSSm~>~9IiAew89?QruQZpkVo2_qb4|9X^hs+Lb4~%b zxh__!6_@ahipbJfL&TOK!;l0!@h6id$!nja73YiUm`^~f`^$ucPwC8%oroc5uSQ<`cQl$WCACohz)%CWbdMfzq z!4h4vP9N0KQJd`265J-$|5ej(JyxlT)fmPTwG*^@hUsBwQYc;jFUK#ET(&cWc(XzXRi z-&#tz7>ppt7LpPoya|{^qxa{b+JZs*jgK0~F?r&-5|b#)~TlJ3ibJFh&*B znJyZw0z?vs9M0hcYCZO~5P^@z-RF>PvhG(;1f%Pb4m}nw8C~FCiG|?=>Y5B*$6)w8 z@N+vk!Hk&htTSVB9IwfKnKXuY=CY~&lHF;%ZQDP-uGKW32K`%-FB*dFZ;&ObmuNew z!?40|byosdAlPbBOUzJ-Zn|Y|07`L#Yvt{oAIhL#D$Qx`+^_Vt60fkkAfxAMKQ^<` zWdL(n)EwzZO(V15{i##YnVMCm;}`rp$?yXsom1&7r97MLP+)H-ymX;>>c^^7j=l`R z!EpHoLW*hPVE72FZNQTv!CfVh8<7@lE$mfL9joN`bHTIW;&~pLr7`W4 zm}`sm^|bWiY<+;_gzC_|yEw6j_W8mLPSA(i1p!tqqH?bg=^On8gP5|3l+ON)k+4HA z<0o|AT&lizDXirngKfxiI5=Bp@*kK^Gp8F*i{D7rNh7x6JN(+gQ7YN1%gKqwp3p*X8Z5J7W}iSK&D; zAKCZ^Uob6{@-AF_Alis|CjCbT!6EFot)GM=_(%*_q{Q5vFnq&89!KNC7K@C`rSzsT z3dH@J|GfRJrJO#Ru$oC!QQ9)^2={C$!0iwnwZVvVE2nN@X&bNx9`Z|MapKpI-9%X3 zBPbSg$D^czzQ~*3rApk2U_OUt`ZOoal=-9n>(X^CLlnORFd>x2ND&`qO@@P_&BKbk z!W`s;SZIte?j~u|flc{@{bZW*=aKTX;k1kJ25VoFZ6U}b9)oj9nw`8AWlGI`BNs#n zC^@?{IWxg6s2VOMzyTe?r<{>OZI z6~Ds?uSa$v3tbRyt>P1dPmGUeugUE*FT(g26Cma8POX>BoFcnju7QS3zq*+cPKVE%} z37-@nvV$q?)YJ=%P^=~FebF1~)tycc?e@+8rKGIL<)g*PpuyV%fh{V9x)3*)TVHh5HBkNyooaX7Uaif$)t zm*AL;$Q5ae>nGi=`h=KG5m?c&o!ZOGyQB6kjuq7CKis;jC75%8o`g@6sh);npznSK z9dthqwjgQRa4(x5-zgTSV}o5o@QzK>iqt+gtQS`OU9w_Twf06I^!_$ zG5%J@2QvFpUnPqlyA0x6k}*yYZh2%YQ07wA&*x|=NF%xGJiv%w7FbjN02LX}VOAl12Y=^m zgWZpal3evIgTZcE$W+yytKZ;*L&d4qp=7%lB|9Dln6j@lrfD5RjNoPOZ)cgDt0)rP z^cIfAuIX(zaK4rXIqKQ{*;1e;srHiAO1x!;#4d%2ctUzb3H?lf$-vwQPbC$*Ta@u# zw%MvrEuLxx>#CL?@04-?~6KpBj_9pD=yP`Zx=Ux;GVlIJ^HToj^hZl5HL z8Sy%&yiSVA<;L_%wuu;}1-*#pK9o(o+pKax3eGV!NNCSI`%gf0h;O%E*oK0ZnuT$h z8Mw%y0zYgwQOBLga;?)Fr2F`U`)z36-x^2P{E8NY;BgZeKsA2gBC2z2qNLWVaENBhIFceJ`8<`eI}Fg(V|4W|LwAd15`D62vI@=x!rVPu&h z7uSiU^a5wm7=7s8b#Yxv+uK=6TR7;;z&QZq3?VX~*610JBs8oZGmgwOR+KQ`N#qy06cH)bGDp&o0P3LFEg9-q`}c#6<0HI%PA&_d@w*MvdxCBxq_vKySHrI;75a$=}9s?xA$=SrD zaO?g~S)Y-u>4t<+X;l~FFYxug8-k3j(;q&xCwoa&q%bd`@=71nkAMhdx&N0bR~(-Z zes+SE7vo2ckh}R=VRxhO?dS;e7bnp~Po$S@8K6X$E|5bIT1Yp9nIL1H1tJdUfZR=^=|4!(syhO<$!&`kXkJBS_QjB&s;iuXYBB506@+KG5Irf6K6c-p{1q61WM5-0yL5hTmM02Yk*ePu;lH zfJCeEf4TZ^!4H@OXR}#tZV&Hrm7mexv0OfGHpz!@MHszS>^R7eymSbSR}e8s36sek zlIWQKVt-~^)H9X};#h$5P(i6DGD2{ie05&>qh6EeR3Tmb@Fa!|Q`+#5luL{|DXy&mkl>dkTdX zIQr+ZJK6dV?2(?c)-5N`xY-*s#*?pA$^Qo`{qDLiFkeQ~PzK1pi4(`HMIsM0a~lcMnI_?%*Xg4Fz>K&5`cn5}I) z)37l3t@JS89gdNLWox~lna!LRg||~;f_nB4@|3x?IM9P_|3}sxz3ewh<;1hLhEvzA z%1E4pk0DwU?sP_ym7T1LI+5>wEWXfP9tE!h1Fw7Fj62ld6r&PlBL^10)`F}UEel1^ zO~6Ilt$I>@z|m>&W$$&&L%aGGoYj?A;I9evF-6gX>rVA0$Jf5D`f#rzP$n$XbGwYZsXYGir~nw&Wi~<)g$|0W zXU=Z#Wzc^8tMeolIeVt>Z#hU`o zr3t%SdbIV8Da0;YgMCzHaZfc;9HR5EPqYj5vXrcFR)^~i1vfoiuUQ&NaAR4<@P|Sf zt~|}(XnKC(1RJU?;_9YdLhEkuB(LFU)s=i%Wa6aUv!=G74T|cBlGYm76NfRg&7w9^ zLhNuoop18iwj0LJYH3vqgJ1V~{2J=EV}8x%HJrT7B`y7!=y^WI%0*YFW!p3f+eHe% zQdxi+E}U!scQ*m+bP-7I&unYR&-?-+ojItcK9oB|*YyGuomoXw0}$sB?VpNeOk%p6 z^`Du7YtW<+6U{w3k;euwgTo+ak!GFYUsu{rIaPVbF$JnYC%XffkZ_SEt7*XcaJ zfaFBZL zwjw+W!?|Tc`>u&<)s7=}-wgHfb8H2N!Ec1tf=)2g{1Qm-y}sX zpKSLXYVIPmP*(rqonsXo>?Cx+?O}XcIMekH8~UBlYkN0at(qP(p4HL~JzqX1kB+`O ziuv<*!0PaJ+cwFM>7M;i!WrEND^~stKlM>k7x5T}FOdzQUPj0HKi$(blQx0)U5KFN z@w=a6)A_Tt;w!QELT`>1=B!#)Eq4s8 z4VY)0b=o_maFMUUYQ)wQ&jx+Hw(g#L5n zo3%$a*Od<6A#&%kqZQIDmS`9%1u!2lO&^mcF)>;}+iD&5S=9!Q-ot2cM0o}lprnY9o0 zJa69jsmA+Dh`MB@mNI7Uzq557YB1oXWZW& zJLNJZtY^ZN(bgM1o(n+cLOCJTUNE9npv)q=4*B#odFKod!e<_@34RX=Ah~Tk=))-r z8^`-cFq3;Qb=8P`N-I7}BS+WH+CafB@>19r+=F^EFgI+&%X(K61}u$s#C=a@%qhN z1GFZ}z{b)T^CQ_QY4jTNi&71;AY#vqI0%g(FVD+-Y3XwtxlHON_W?zxga89a;9Bq4 zaJ8tjBi*%K3wkY+YzMRKGj5S&FeeI`?C4sF*Zz?f^>BRM!dV4yOCzUGG;W*`9gAO>TahQ z?ZOQIoW$`}quX1Q4sg6nW}A#kKuGlJLWdUTFbM(>zopgz5kk4b9%0`S{j^fYZ0Slg z4AnorAlhNuD<^yA1tIoZ_?$=9TC`0L7d--KB;wCrB(w+ z1b%_QMnpZ}9aI?Rlmh8}{nZF^V;}8%*!~{XS>Xo$lY#*Cn8%29&6;%Ctvpx6Mzr4j z@CjWg2RRfXjQ{}9mA%n0IYsP2S@?s8e9EC z0|MAhB`Cuk!rpj&SO&n9z|XOqI-F5lpd9ZH4?h0aRia$o|HnN`h^N6RaNHY=8ACIH zp!YV=iy=_cx3d(XObe-UA&U%INRqGr+_`|a!pebU80tVkp92W>OKbvDVB-swbi>2? zm2ozKzI1<%pZ^=~*x-33{hx0AyXV4ntz6=d8tW&lyokhz#;k0a%$Qx7H}R`M?ojoGA|o>zKNgJ1uQC*Fjf|&nHdQHV)zCoB9t2@ zhD6Kp-2y-W9Te$ZPNzRl0qi}TH5c(Ry#?M_lr!5sqg8rqs}I zC0-8#0=!bEeM%bxQMbOCtkgfg+!R5}TuMjmtr$@#Iu6wXA#%x{-fc{NgI*bQe|8j- zniv*%|JBp=<*;q7DPGo|&W@knuQr@scBTIC01zwdqkq@?OjDHpP?P*F9KJDTpr2+N z$_^P!xx5;^l(e?Tg`V}Re!*9_Wp{nSnO1g`zsK~*q#2*Zv+Plpn_!_ANDQZp;>CS?sRX7AL!*IM- z8ElaROM=|C*X-33=g)=SgY+qs$AlJ^6iXLB?+oN->79Bd}pf)&F#+q8b!&Y+$m3N@RQ- zXb*!1ZGuw8Ksr?9ERY?DD8dZ*QjUG^lPUCzQy(~{wZVjEu4Lq}IRexA!R4d*y}LjC zNS0MJVP!76f!NHkB&MvbY`t=Pw0R{OWsG;<#=(RhJ1#Xsi}8$^LEnI}d`eoHpRGXA z3?)@-(UCW8;Tc_+*5|l<9&_h8^1migBkDdhMOf)c?RRs98w`AR9HOmUa`-mV9Q$s$ zjrqBcSoQ@S;TA5)o+$lPe&>ZyU#BA1o$$kY(s!>xeUX{y#KkrSQZ8V$!e`rbqt8C!5V7jqUV6da+I&>-;l@x$rtX6wA(A{z4Oc|vE@ z7rhYEpm%2*3oYBhdNUdbYu7G1XzByZI^q#>)ig}FVh#ohschX|Xn38;7u=7a&znU> z0>{KU4IJ}`YmKfCe~*EQ8t!~U%AT-Iu;d;AhkTw`M2>fmkA2 zUT9RmhN1MZtWxdcCS19fOpQ`tqwlb)Kq?P5F^!GTJRbn?Ysat5Gm)N}C4<_@?_e)58|W)e>7xEX%= zTni@NDg@-gzcrFpcTWX0MybLxCR+1+izqA*p@ai}x2@;L0SJMee(4stnC@P64MTNP z4|B>N^@{A~YyMVBx4aGSi>5rmp`U(HEl%C}BI1NoP6Mr59#bqtS z_bH)q3QJ&^cT~j1t^<-g7V66-Oj3EOeB(iQ)0-_%e`Cs!g`Bn1`|y(RAqC(uJ=7tT zx4^^OqXHYl9zkrCZCgi%&zme7$$zNWB@F{+udOEaz?JtK9AiUs@ZZsYH(Z)nLW4A( z1H;{TwRlt~YckY=f&am2s4`%y$>s7|c)kve#U}96Efr*+;j{Q1W&wrM;tbXB`?Pm; zz~~CWNKI3s*k?{|U*V$8sU{foFl0^u);k8j9wDHU30Ui=YUu^kj^K-BvSJx#<^5-2 zoPBJ>JssfQ2`NaGw>?n@e7xJmWCM$%k<9c6i)2yr?A-R&eAE?bU%F!pU!l8= z`Co#)6G5`~6?vcTdu_I+9}ENcpK78lnBBd>e)Y@Ew6IKFWTA0h?F}PYJSmsmF>+7- znom1*6~V#iVsGsA+QxG=Rbx?CcP=- z=Ptf4xX4OHZ1}X3!BPGkrul&bI1Kzr$UPI^T?>aOD%@pJ;@L#uV9nH`te(Kkew}37^SXytEBD_rfE8*92 z1*HTy{HKW)yyWt2dLsOsBueg;jYMY)%UnCKOVn37Qo|sfk5#v`21RX1H91soa}2P| z9@h|>f_`@_*gV2K_CmTFFiKk!6Sru5?u1s|g7VZQKc7qWSsxOC^a~L7hMo4^$X1g` zglFF)yorP-$UgVvb9$)PfeR}IRpE@*pm5UF-03?xpHPradA{F&Yy)tyW&{MJL-rmz z5L(BrBzn_AppiGTMP4gAb(#^yA)$<2W^)OaO{gw>041HS97HEAN6?3Q__iOFCliH4 zc+Ot=<;p9xxVbHJ{P+9z2Uz@JtZ}9Ju?q^SL8q>?V@VmI=eQ+vlHlSVmALEfy515* zmUPJGP}$i$g}ORM=gf(sZ^gKh<~*S8sS=np6Ya=tp&lZT5(P?YVwVV3SvmK3bnE#L^~I zn*3Vdu=g0@D5`%~nn^qNzMuWtOf(Sz?$sZO9tyRuu@^{BXD@3J+SGo0)m3KI48TwTKCT%GJBl5a@_Tu5`@&mSxK|BJ~kAnX^Rh#xdMOu)UiH=lII>h4W`>zV4g{R`BMi(qd(o z&hn|B;asB%TQrZ{D5+m~NcTwTL*Qu_v-@_jZIC8FSkkDx@r+CRSU4}7$N;-X$Qtnm zMOdQiMY;n2ddsoktQ$>UXr@@1U#FHIB+w&|S$chEJop5S)+ov5#=PW(HQMVSue%Q6 zP$`Tl!!EX0QX}vJTRUiEM+Yb#ob9vIv8HkK>`f-~n8BQvL0aFhFUlMJ-WtaZPy7}N zE=#7Qhnt`T$E8*n*?W8b9tjUWni<|@${XUP6V;zVW6if}NJa+gDx`P}6Z&W!v3Bv* zsi6YKUY|ja*=QHJkpE!WHSR>TI`u$@Ey9fk#s%DO$KpJK!j>8U_(UEdgQlb<#K1FV z-J2~T%7}VqP$s#JwL^c4vZDy|wflOcK-7gOAg*(enFf8-=X1G6(7r(W;8@_EVa~-K zhG5@CYPKf8a{maB>|GB>XfWkLwK-Bw!7J7s=Tc$YihHUhfnul}={iN6rr=yI+%gvK zyH$mP8Bc039{fBP`&9zH^J>t2SBPtAd66xzYPTMgqXhA{GnKbys>~52juEIPU~~!< zJc^sUrJo&cr?iX=cOsUmd5#Xb!UhJ|fFlGcGOG=`xWdmH%pJp_$3J2P4glbK4FV9C z4RS8n_FgDS18YkT2pBdb>#Yh=jbAt%Jq=tQHCxqzYjhVe>jkNhZQ_3x01valKkXzV zWi6{p7N4j|**}|5u2dk}q5O$(kHE#8vUSCUsISTRK>$li6zu)jsauaBXNZ_i(-SQZ zDSx5zv$=J4A-hxCSys~W_?o$!eLb{8x}3It?<=L;iNw|D-eI#^zL`P@^h{rx_^Sga zFrcd6!SSA+%K;Yau}zg)i#vPiB0cs3^6F3%m1$UF5nKg^W@s$&mrhl9Q%P#KZ~CYZrPt-;D6t6n~s!P-y&e`)DAW`(wK z;=ZZ-dcA6B{W!M!62hsED6L~HKe+$Wxl1`r@ry=tetoVu{o0DjkeS5lI;KZLT|O*(2f=YatGNb|Nq>x54z68KEDqPGc-0@BH)-O+9Q=8WTD)BVXTt#f z@npNWxkqzPEWZ9Q*{!GcGCilR#h($n0=2Y(j>Lwk4{qvVeYP1Do|p|;V-u}3^*WB5 zOVdP~;?#s(PGumm^pVG3g>aUONDWWOns&?ww~HlwFL7osr|`$I%duD_CyG|mxBVD~ zP4vRo=FJVU31wt{zwZ0-$=owq&QlWy7itYSl~e%KJQeeD#lDdWe#yk%w@u|s#o3lnU%+k{!ag}H zJ3<%0S4YDld4EQL&M&t*q4=0aCGovZE8=DFqq24IlWcdL!a&G%g?u*2Cvzye)f8+z z@0QdsM38PDTa;w=uduU}L&y}54V=M5x39Zvd{XvUeyf_4=`*t(QPhm&zMu0{EAd?f z7`HT4X5Kpq4wJ6KoqR7j&X#XrOEnD98dc!&sL~)FD5Bm*Rkt288s&}op|zzceGw*K zTP8$_R_``htkkdOsfKd~pI(P#7buntyFv!{5RRSaDRp6!j?J0tpBOto&+mIHc+1cZ zX%s<1EG`d?^oPB)05ZYo%J;4a}LL6Ps$k|b5mT7 zA=w;(1TqaF@KBx=>K4y-I_ZJLi-^xBhPE+N}ianCa;i)RUk9J?d=t+ z?wlPYF)ipRjC`f1A;gf-l$;%l_s)+aEW_i?XYH)ndt68V&<2)?@3}eT=};flhX$&w zt~Gki?G(%_b|{{zplDNhQ1J}q8sC7iSi$xO@8Z@#V(p8-@D?|^+`=X4+=sUg{ui%Z z_qNJ|_rPFqTgd~nWcT-7`Lrw;O5=_Ht;LT2 zrfjgk+Ub*^6oGoqe`>tkjJWBSjr>HF!m1+4_y(N_BKir2c&0l(sfYwLoC|Y$XIQbp zL2Q6R&+zgQDk_C1KVY=Mg6SHr{$9Dz%aY5FW6`vpl-uBCxCMShmj5T-!w+rM(L9<0 zAA-tZvi$M#w$7GfTr=Rcd=n>iWysACy;<0)|UqJ-P!AX8R&Jk5;u5 zw= z)N>y?v+D?zF#0+eOfpQ_+PneWR}6?&yrCq|%kbas1r!y1tJBw{*p z_7?yXpUz+TR_HZ9zYI8r+DtD(HQGqp<@eE!iab@En_ep;F5s;O5vSk3pm$hFWy~%x zA~g-5#gxj7=KN+*Z%VY_ZP)omUl3=sjBh(_&eEyLgJA;=E&>N6{`$IIi!hO>Q_NlbE6M@FgmTBl(Egi>^GB`uk=^244p@8MZEfMB|8!BwM8cgb z+1Vb_i=XP}OalO%Qrnqg`sUWgr1mq~QEmk3zBh$sbOZ1ylSda+&r#_1DhyGWpBVFg zL^9f>GyP=@@TJ1h%mZo`|BLus(!!;R2K|7JsQ>`Lh3GVmmIFg5^tQNQ^peN`;eE;g5Qz09 zE!noQcbBPaD~f-l9RT`!dJq2i+wBfe?%Dq&`7kQBL2_6cm7er&m?GTRnW2OcGV@WP z15m7AIyxGnxyib1utTP);x7t_vjJfF+p5&mAmUqJ_+i@Gj@&_Dx+*ZZ9A6xyM)IU3 zRC)qkI5fURnMctXI!CL%d%$=cQc#zwH`H}7H~=|j<{EQ$+k@W@%nf`199H0ylT%a` zqe^Z>3wIKB3!z65+-H87ImcQlx#POrVSF}~9-B*!8~q2USgOPtt>R5dZ4E_2JsIYT z5dQ<_u&|=?YXFeA>+QZiqC?Stu2JVdOeXyIcAqsunb-dh6^T%HYzyGLJ}R@EssDPr z9jLq0PutpxST|~Yc=&rKzn%czJ;Y1E_D&5tEU2Gbm+(uydnXV*K9-ZHk~FakVXG*DNXP#BZV>yVnS)@+7*(3p_XUc?>SJ+k*nX4{QoNi1Gb5DN0Wsb$! zH2hG<6~K@)i{HoL13l0ZYU1Xvg55cE1h=n}I-v`cu*AZxilEb4o&h*PBm` z|AeHs`Gy@EokE)4nq(krKtY0-jnGf`kF#D#H#H;+iB`61#E1at^r!6E`H2VZ7UrcC zK?hh<@B*ieWiBrQ4Pmhuzn6s-+LOb9 z{`*J4fsiz(=tkF(M?~a^x2ef+zGwaa?s)e9sOwsxJox|7?W%!9|MB0XK&SgTh2%^J zz*w0ql+%+0B|(6wM87MzID0_eSF|k3SU1fr|W~%kq!pr zHL5-29|8#-d2kaX5|9mF9O97)QEPciNuex<-D@o|;0VaFe#;&=%b9i+Xxq@}q#oCg z(_)Bm6&4WN$H`@lqjv;YiVW{hMWW*gNolS~C#QKTG}}-(?8^-brm;40Kr zmn^h6Q7Ja%k}IVctOgLdu8Hiw^8m<*e@WoCJ3y>Z7VH0~N7U3`4#P48yxr_I`Snu$ z#A1>j=qqQ+N7FkUk@<{WGF2ShNEt{~ctXlGS&#or$v>oSlFtOI*WCwtsbxMpCVOcf z{}UW6pUYatvWF7=T<#o;3sd>D4=vHZUxBkqM-1842`B==dD#9Brf*n;-$wvGp*-dP zS>uSVPln%r06KFD^mdzJ)lFW&75`a_Uq699EVA>x$MCn^_YeL0UlJbp?LY=7&+-3O zKmct(006i!-2|mQS;A1&;kL$;H&rv3@z`=iVCGM`oe2Nq3ZEZFNR~}-;1%hVs>T0L)g9)%YjI<*_8&#atFHl|eFNJOq8sZBN7|+5 zp?lHO@}?T8^pcN@p5Gznw1|rP2?UM`v)I4pF3P9kDOv>m{Ygkei59Fy8;z zsC;k1^p{}upS97_r%I~ur@-rCMmO>8#w3n5j&^<&SJmqfix*9(&Ihgz<(;i?^} zJxv>6YII`Ui|38eL@2p}@KAT}E#!MGcy8pY7=n|+`yH!1Tt@_{z87fNIYaoH54w!z zWSClDd>))^Yl_&Cgmpz&6mM3UGqT_tt)s|IJts$@Z*x<8Q1Q}QfYhLz)Akfg&vCr2 zXinp;l0`RPeWFaQ-*ua^ODWq~1L+ov*b~CLO~1o=sERt1NoHq!vbTHHByD(!$BC)O z?C-ARsYkU=?&|*N-*q%f(z8-6K2@iZhzcRo@Ri9~Qj$D({ju1YlNAUY{Uq0EvLwpF z^%-jug9bhR8jThWywD+X;O!mmZl=%pucBTsf?vQSknfrbSjK!tkBi<#$SG3%!_=b8 z$M47B-184YRGqG^!USmISiW}nM)Z(sXVRl!xvrC<^MdHLd*f>zJ;}U5suF|8w~jo& z+8#gCh!`ypp9zj^hFEcyy&;Tu40-SlZy$FckLkNXekW8ZpGibRgj7@#dn(G{>$0X^ zoQ5CV5^5+S3ifKlxCH~Q!sQ2(zUzfNi4p8r0*nk%jgbN{qwHK?S&M)3N5cYaG`Krd z^LiC=n7Y-!Kw+7pM^~%;7rGaf%0Z)m$=pn(vI2%wM54V`>rl9DiCS0%Pg)>TpSg0` z)e(IE=~;l68u@QDDNyp@qrnyGllOdmpGBBo^qSv0+gjvWEO480a&UjRc1)&`+do7G z`|ImvBxtbTnc&&TU?F;(OhX2Bax(f=AXIq3+XuWrW-$w9pqx-IzGBbAs(nDOC7{v$ zNDw)ABeFd%D4I8k{K$KoUb~Fp#5@(1d}FPF@F{YaR<}^r&plwpD;JPSz&MF_tA1Rq z2?W^v=oNDoRj1rbq5ZkhNq`jT5AB%Cx7dKGtMf&VP0@bqCpgBR#id9Wb9b8n%RN!3avV_%5F??=a14U_94_BDzez>STR+i$wUKVMM;s$avURnl(zc#5TR~Kvj`U za-Z~BM{ot&?)!6gIvZ)^B%;{2*5LV%^(?cHA$XX8z1Gjz{$honGe$Kc0&y*hng9BO zYQlK1Mp|eFSMrC@kHA@eWm@`a82$njqMIDZRn)B4luf#TSaXT@NiTbRfA0o&)o&~u z=oQS0i4R!hSxn@@5i!QK7;M((ee@Lw@Y&dbtwWOa6Z1`NCTDM4^VW5*f2HVwVf{pt zGv{xfoZ8+($O&<9p^8~jS1{{?+^XaR`eEx-Z5k_ShUoYA+mvulK7G)4ASD5b+si5h zZko|B>iV3~!Fby{ft~m3_~`mau?jx=zybXSJeJz-lDq7TL;ZaEcp{)`MD|znPrupT5!!pgSY&2AWhM>9;B0blGHbvuWBpmetVV>q|0e>xeq_;dP6)OR(>f#)4-1Wt3eV^_K? zqebqy{5ZSv#5mbK0-i;6SdRLQa?DV+4-Lz`b3FfY7zMuR=G6OAH_tjR@ZBGLkXi!Z ztfMX$1h;)V*SKsR`czx2-u1O}J!K)h_oI14%_a6J#ns5NjF%bONK6HSfW63LCT_V& zNU#i-IZUDJ{w7Vu%ks5I?UaFz_$#x@TgPD^%G|6BT-_vAthdriJXTmvoeu zWYrEG02A%EB)U;JgorVm8d6Q-8&n>f<#)N z_#<0W@q>jpV196WcJn&^ufom(D6TAM)HArdy95ue!QI{6-3BLvyE}p41PKlycyPA_ zLU6ZW!QBII$nM+S_t#(bs&1WgYvy(z>$x-Yo$kK)4>6DOX80Cx4^;HYSv;>~eI`X= zK3-{-VzFF@Lmt8o2CfWl=)ODHQfjg_CI8YRGn!Z=!lnRh{Z&`>MT0sB-g*k0pr`i< z5n!kkG4k;wP&e%ky2ppdZ#wj`3rhOkV=8_)nP&P2+|p&!&U|C@?^&4smijTrl!y)Y zx5k~mVMg+ZF<)ya03gtWp@mr1NO&#_A3GK1xxna*_xo#HeASN+&~;XOy`}|e)=UU% zbNX1g@+*ZMw3lZCbGL0|R1G4{-V0I`?A|ONU5n_PE|(=8Gs|()1>bv%*DPQTpVyN3 z7A5{FwU)!YhWr8qZfV=AE*?yC?%#WV7hd@N@V?&PhQrn%`Mzfrr;jZ+)+^@5>=RjR zQ`!WHUdzL(ksJ?yeth0ySIqqn{ltyu`W%KfIV=-tiv!gHA9`Lf30-d(c!3?x`rRcr z@3|SrLm#7Cg>jn0BwTA(2C6Jz0MGLr)6bq@cLZj%q`u+A_C}$8-eX7e`W#>Z{~NrI znRT5!G574yeQ#i<4@8QY-vlyw#HM~A6gDA-D?FZ@pKdMQibY-DL!t_?v#-pb5H>IU zo#xEdQbQp_hPDvE5t<)nE(Vj&kff~BA+mkdhK49lD=(7)Ra9>WvOHq+Gy5>Of>es z@K41S$+CwJ?UrR6f|9njJ6xAkb!B6}DivDZuX{ubL{<&r54^>f-Kv2jBAP@}Ph&}B z3yQ~!zAJKEF0tfz-D6=yfxJkCohg9nSC0`!xA_|bIbMu1PD2;5IM1kHHCPdEYp77| zy*uZEUhhLmp%>qj-Jll98nl?i2+95eMn!xI6%L}g0lmhNEUA4$cc(V-)~{_z7g#m7 z&utVRwvu#jHv8atO-Hed z-^o{4<&O2KSYFebhU1%vAgxfEhlJnO6~4*KIAOV_0DveEr`Hl33XTo9YG{OF_zuU?(5 zrsrb0u&ea~p`a7wjYU8YNU^>V<=7|j89TsT8o|2w}uW2{v8MDR*y`dIJMs)(>tVftTr+m-b_9oPg~mLir^6Z zj~qWc*m=C!$HUbPuTdi^SgA1?^3Bj>2ez#Ic}**5z&}%Au%M!Fl&hH%i`jmGXZDl4 zLHDA@GE9qe%JJWCzplnzpSrp;)A)Ol!7sh1UkQ;vtC*aUVuTms8IV;}ZJkFN(4}0x z($BfA&%t*_o8W?cNheU6IxsV*HXLQHyIyj$S1|ubTEc}X-qR&buS#$U%CM0VCW#!v zphuX!2P1xMXqG2K zk?q_MF;kW~+b|0*o4?vbMsRyK3p&j^4MEAaK7crlLH4Vb4?tO0$~d ziAa9=)kd)%k5+hw8Aiuir&p7GdoP4_U__+vn;d3F zxZ{9YxbxmILh>$QLOHke#JTz!2Tucc1{Oa;NH=mxN@S;n%sn8xifr;RFavRt9u_*} z18|pP4x=0`+XEiSKNxmf6tIDsZOVWCi>%m-+sPq^^ETg8iI3=b~f zP1SSi*Ag@EAV(T7b?`yse*8^3Ky{{IijDd4s0Q5%!Hw24@OnizQU-=T&I{k}ja+H{ zQ3=cW6oH;;w-_Tq5?2YZ7B}*7ta13gBG0Xp<*_E^%_6XkeqXv4aM3vBkdYLcZ z55t9KaLBLxLjUp{O3mhll1Ajg6K1V9&#*$I9DRcubPR}2ZoEgVktHPQcyOlp*u`*$ z<3a`dI@mu9$UT?pl}kCA?r`&U-~Di6?T#zUYuGf<#%qaj-%o-@y}wbnuDwJ)uPOwG z-&sX3G`4cK2K|MUUdp={tQ9VnpZ*^%R>`Yx^m_f(55L1^HBAkMTL^OM6bVuh-Bn49 z6a}I^O3y*tLD!PBtGViF;vkmf;9N1q*@nOL1ep5zrA&i#1Omhw|KV$NkX;FA0A)3b zuUh>;s{c1bKHl9@Eo0BemzN>O)k^_S3K0td<~RSt(Oj0|;;{Ybl7|s{KD=DPqsz?Y zcr*HavBb>u86Wu(07qXw9KXf_h2%JWHELgUc_z=pY?1OiG!{qyZNKxOkun0qK&%K1 zWzsKYMw3W6Qxugr1G1Gs?9Y*Hui10qKcH!D3UQrFgGo#w>VZZtZ8-KoHUO-|vfKa0 za+J?c6=&iYi~0T6x>IIUmDEni4}8Fun3tGA8;T$uMVOrWV+5ezt&uyIa- ze=_DlodoW5Z6K}%;0ir=4lx38c-hWM3*-10 zyA#UWaQc7qQE(DP<8>UBoXpmn(vRpKX`U_s*R-B`_tv zV-G`_j=L~rce@S#`ioR2nU_3s>rZn$wE0?oah5ddR=+=BAN%RKcPMV#(W9V|ZuGUZ zk1`a8eK=+jZ~unhn&+5AOs5jZ?{fD;!%TH1Y_k?D(bI?TU_Y8nixEWQTMf6QYB4}j zhkw{D2o8%hDB>>R)eMI?z^;)2K0%NQ+7}|~E|$Ii zKaC9lBYM9LxZ-RX7Lu-|cLN6oZ?9hnE$p-@$%vvb1}USDRH~aPvV~7YU#*IqX3+Nc zZnK-qih1{=S4e?EhhMohFjXf@57@Z!GQ{hL;#Xdq_%-YFmrsQ(EwBKucscRDfh949fOI!n9}8MYJWY)H3y-Kg!>{UN`**}jp8u4PHv`J5eOCut{4JtwpG1v8?g z{&#Q~rM%rrp$OkSPUV8snc3EuBt8^O(G`Q6P6%{J@yH?_?qqwg|H=N8WrPcj@vJUm zo`iZUmNTkJU3r+&R@vKo7Dtk;D0if`AtJ(Ud%q38@pq5*O+1jD)gT9B6&Yo!{|W0&mh^nd%~J)F6r08cz=x4+^io1y zO)rp+z+ay^Ha7LPY+Fz|QSW1#+|v%tq>NmWHGD2E$WJBJnmU?8S12V!bFRAmhraCj zGb9sOi>}8rF>^G3?MaXKD}A&JuChftd=%3H`MNjZ6^NB`+U-p@^+ytQ-yb@KH+nRo zvO+|1Ciy99FmPc!_Efw^MwGBq`&1q;gP%2InYg)}5^s@qgf5UPx0ik>%G|I%#Q-g? z#ASANMQ-TkGdx!=6XzR5UL^(JuWfY-V`JtWb;^I3{{2p8;ZA0GZi*c>7RLaMDeL3> zR2%A{$8VZ9C8xPql)Rn#pAyIJiEYO6Ky+`k*v`q!?a*0nm**SiN9MY%&v3}l@X-R= zh}p>b@S@BPBGwLt1MVcCl-C6wPTmPf6gYRWOYnz3E{>#@(Y$$|1g~@SKL55>)eB_H z-IJLWqwLkmI{lSmn(0KRW>~g({g^(52&2bwsrS~N&uM%v-xmDqlc9qnUikw-dYvOd zseD6-caaq*?lE)hD%lQ-O=Pjg-b9q;qC18t?1uEW2x{wQ7`{_NXA2KrA0TdbWL+Q+ zGjjh=$E>*QjH&a7x%4C^qXhllS{>}ucA>D#oR}Eh@euWll3LB7F4jhiL zjrRFRpe@~@^{%t#kBI(5Wp5-YaE3^DUp|J#&q?GxuycMEKbSf%K+PtJkc&NuJw!7$ zYN%i+2pjz>S}gHdSy0(uj_?^&D*07VK^23w*PLqkq|tf6L(p_Xk+PR|Ug!M>^mD;F z_1h6?No<@k+EwPLX7(tBHY?5M)y+(qCiDpa6aiNuLvX_$kUbXN{p-o|5Gqns0aN1W z*dsi17@di4S1Y9_8tm~~%yB4FY0^G?@crBr;e?p!`)~UlFsXb6&>*+wGgwnnuh_!MHR!yK6 z|JJFXM}0dU`ZYr0Z63F2-DI(XNue**()4Gf+sKsrwyc2OGOzvh;;yVApmEN}9of@)|MGIVW32l2$AwX@htAQG?k`Fg?a%K9 z2EVT2w2w4Lf?pfCY@j6ij5pPK-^8qsaC(1v$0o-2Y#5&j0|-;|D5Ctm790F_VE%dO z764tIe$|rehg@)75}J9DWlWX8#UWjx=}gS0E0Wf~u6Gd{81c3Q30?g{R7H#S88?+) z8`&zmB87r&AjHGfXwmmGc9rLGP`6PKo7MHO&ACWD;GJ#1yuT0l>q#-|PVTul0eJz6JnMg@g9axVP< zkU=xmz1SaEDm`RTG2(WVX# zJG(ex2l3@r`aqni#X6Wtk{~lutq0BP;%^DQYZgB*CnK}SF@mS)SH$&05eU#>*Y%}-@QxQankmsZ)o)*?=QO&lqNYL9#p@9gBCKJ#hnER_m% zu(l=s6rN|LGC&8D;nH<5G$sLo)l`$3UCS71^kk7*pf4?v{kg``0eAn%_;Bcgz}v%Y zb!jWIYX}>)Hjq9(0}^i%D-peAS8|j5&a~>*=BaQezPwq`KYLYLn|RImG!XRqV%W{l zxBleV$tRi-8M7mDlcJ_0BDo3b=hc*^4I}Ys1otCy=h2fOyzwG|Yc}SVW$yeSqu^``%3eS%-{_H9X;Kal0~B{Pvtud-<6Qke)ahx zEW3R!@o5lk+Yy+MbmMqrT928!!Brr_vSYnhT1g6X7I}E&dEf-Dr{ntA(CTu$jm6a$ z`LohhXf=L2(H!G;gDo7=DWaK3K`o-UycjV`W|iHR1wSFb)+FeR0DX@NOYhVXd#Ul& zcuzats@AQK4l0de!yY2GtP0@VCAbY1H3pduK7LbRnV|8)gosb3>(eEF5b*fQy$u= z&%fM!bbnzU+26o3>tB;Hips#?DQFS_VX44ScInhQjTP&hXE<#^cPjg2hgb{j;3FZ> zI6npF@CcgIJd<$lE%ekk7P>B=Op~K3M*Dv1xYjSBYU|p_9xjC1ez@hyIz$gv4%FMR zUIDoXWmUZUUUB2uq4rC8Vz4lK;+VQ%QX(}2_Gfj%i{mwRvj`Q5Vs z%I7yhzZ1P4u-HWv1on+*8}yY9$ZKbIbIN|;FSJTPb!>3sAD{(Sabm9abn@$T$!5PTd{#$zR;o8I#$j5;Vc?yaZ*%j`vLuaAD4gq1;B7?? z{?rkgFO?YLgpr;%J1!2Y{FJh@y%FIPEFqwU-BIo8*%mBF^CSdC$8#L zz%Sr5#FEcY8l<2o6KEx6JY8VID|a&Ivk_iVZ6zf#f<3RkfaEV7P)Ildm7!GZOU}y? z_>_>_)V?zDOD?u0=zQBnkYeaO+>e{N7~u@*_F42J{73ay-DIsRykH`$Dq(t><4%w^ zH;#Wx6%;4SzIxDj%6Yo#Qb?JjP%NX79d?16f`FH(zoE;*lL5A1X!_PS&iIkeM`iD7 z5tx8h|6__9`kGD5ixs^xFRkz8FuaPT4&E$4!k=T}Mi``DPIRaJEbl^S>RW`8NV;?) zcFr#l)R0&5@WmIes?IvZj9X?*mF0etlv^4D59)viCOZ{}*xldEN0B46XhT(KPp{41 zOh|^CYMTzg6a}`&it2wanE|D0N5@)b15^2bQ3RHjf5*2J$sx0d8_XG^`6-sxN}ncFz26_GT!aP5l) z0l)C-^dSz#a)sUG^wZYhyq&BfPX@Lv;yppHu=g&`Ge46c|Hy$ukpJ;9ko*(>B?&dy zvJ@uSg-U0$EX1LsnOyL_008zIR?(W`fFyH@cV9|<_!YJ5{=V3CYfS@ytAQq) zOL29Sl6(OJ(DwxWW@Nx)uytKr}ly+KlEQVIugM zps%kHA?TTNw{QK+>B6R%Or1lwn%K?E;C*z`93)XYc=QlOakkea_jT#7i+-8Xl}#?w ztDwMAL^b1Q1A%W%t#j1-k$TJPH621xt>JUf;N!h+0@=w=s{1zF=TaG!nC(}mFfDGJ zqRXYAPDBI7R}ADmrX=5^>*eYys1X&D^GWy zO%M-PmBDP@uGUM1;aIjPWRO*X2Ev`M6)Fi0ihB06%AD94>}(F?q-!=`K!UBly8@5b z>j@1je3!CTa2}iTCDu8urEjKkZtnRwMjjxo|;NZ718PIoa(hd`8O zr4%oNpw~gxGYF>9=J{hl08DA_a3M%VcEjE_vs&@S3&~P37&a)4x8glf_t@XDHhDgLL`pS z*6Z1$dUMj21$#n*LoPuOb|`5ADW`)W+FAdbKI3nKh`FSwJ54ci!HT#2vw;4OYG|lG z-mFM31dj3Flo^4~Pvz-Q`U|#6y^_MPqIy(%6SnF7xp3c4wy{C(w(3 ziToQYV1cC06vPb~G474<60fNfik&IG>E9xRS&-(eL?I%O5uNne*oC&?UP3#8%R8wQV3?eED1+9A!TyvfWqy>m7I*OO-86wLH%xC!r LKK}nZC-#2;XvnlE literal 0 HcmV?d00001 diff --git a/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-review.json b/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-review.json new file mode 100644 index 00000000..77d8051c --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-review.json @@ -0,0 +1,141 @@ +{ + "documentId": "collab-doc-12-find-replace", + "operation": { + "find": "alpha", + "replace": "omega", + "mode": "wysiwyg", + "wholeWord": false, + "scopeSections": [ + "methods", + "references", + "final-figures" + ] + }, + "decision": "hold-batch-edit", + "riskScore": 100, + "changedBlocks": [ + { + "sectionId": "methods", + "blockId": "methods-p1", + "type": "paragraph", + "matches": 2, + "beforeDigest": "a7c02b6adacc531c", + "afterDigest": "b604f6421a609783" + }, + { + "sectionId": "methods", + "blockId": "methods-eq1", + "type": "latex", + "matches": 2, + "beforeDigest": "f32017986cd1ba0f", + "afterDigest": "89c44468bc1034ed" + }, + { + "sectionId": "methods", + "blockId": "methods-code1", + "type": "notebook", + "matches": 1, + "beforeDigest": "8391629e63709c74", + "afterDigest": "c8645a8d6004fe0e" + }, + { + "sectionId": "references", + "blockId": "ref-key", + "type": "citation", + "matches": 2, + "beforeDigest": "473fe626bbfe0c64", + "afterDigest": "94126d50ed228c79" + }, + { + "sectionId": "final-figures", + "blockId": "fig1-caption", + "type": "cross-reference", + "matches": 2, + "beforeDigest": "0853df185fc30260", + "afterDigest": "40b70db10eaf56eb" + } + ], + "findings": [ + { + "severity": "high", + "code": "LATEX_COMMAND_MUTATION", + "title": "Replacement would alter LaTeX command or equation source", + "evidence": "methods/methods-eq1 is a LaTeX block with 2 match(es).", + "action": "Preview equation output and require formula-owner approval before applying source-mode replacements." + }, + { + "severity": "high", + "code": "CODE_CELL_MUTATION", + "title": "Replacement would mutate notebook or code cell source", + "evidence": "methods/methods-code1 is notebook source with 1 match(es).", + "action": "Create a code-review task and rerun the notebook before sharing updated outputs." + }, + { + "severity": "high", + "code": "CITATION_KEY_MUTATION", + "title": "Replacement would alter citation keys or reference anchors", + "evidence": "references/ref-key contains citation/reference text matching \"alpha\".", + "action": "Route citation-key changes through the reference manager merge workflow instead of direct replacement." + }, + { + "severity": "blocker", + "code": "LOCKED_SECTION_MATCH", + "title": "Batch replacement touches a locked or final-review section", + "evidence": "final-figures/fig1-caption has 2 match(es) while section state is locked.", + "action": "Exclude the section, unlock with an audit reason, or convert matches to explicit suggestions." + }, + { + "severity": "medium", + "code": "CROSS_REFERENCE_TOUCH", + "title": "Replacement touches cross-reference anchors", + "evidence": "final-figures/fig1-caption contains figure, table, or equation anchors.", + "action": "Regenerate cross-reference maps and hold export until anchors resolve." + }, + { + "severity": "medium", + "code": "COMMENT_ANCHOR_DRIFT", + "title": "Replacement would invalidate an inline comment quote anchor", + "evidence": "Comment cmt-17 anchors to text that would be replaced in block methods-p1.", + "action": "Convert the edit to a suggestion and re-anchor or resolve the comment before applying." + }, + { + "severity": "medium", + "code": "WYSIWYG_SCOPE_MISMATCH", + "title": "WYSIWYG replacement crosses source-only content", + "evidence": "The operation starts in WYSIWYG mode but reaches LaTeX, citation, or code source blocks.", + "action": "Split the operation by block type so source-mode changes receive explicit review." + } + ], + "actions": [ + { + "code": "LATEX_COMMAND_MUTATION", + "action": "Preview equation output and require formula-owner approval before applying source-mode replacements." + }, + { + "code": "CODE_CELL_MUTATION", + "action": "Create a code-review task and rerun the notebook before sharing updated outputs." + }, + { + "code": "CITATION_KEY_MUTATION", + "action": "Route citation-key changes through the reference manager merge workflow instead of direct replacement." + }, + { + "code": "LOCKED_SECTION_MATCH", + "action": "Exclude the section, unlock with an audit reason, or convert matches to explicit suggestions." + }, + { + "code": "CROSS_REFERENCE_TOUCH", + "action": "Regenerate cross-reference maps and hold export until anchors resolve." + }, + { + "code": "COMMENT_ANCHOR_DRIFT", + "action": "Convert the edit to a suggestion and re-anchor or resolve the comment before applying." + }, + { + "code": "WYSIWYG_SCOPE_MISMATCH", + "action": "Split the operation by block type so source-mode changes receive explicit review." + } + ], + "generatedFrom": "synthetic-data-only", + "auditDigest": "855bf874ddecf9aa" +} diff --git a/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-review.md b/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-review.md new file mode 100644 index 00000000..044961f1 --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-review.md @@ -0,0 +1,25 @@ +# Collaborative Find/Replace Safety Review + +Decision: hold-batch-edit +Risk score: 100 +Changed blocks: 5 +Audit digest: 855bf874ddecf9aa + +## Findings +- [high] Replacement would alter LaTeX command or equation source: methods/methods-eq1 is a LaTeX block with 2 match(es). + Action: Preview equation output and require formula-owner approval before applying source-mode replacements. +- [high] Replacement would mutate notebook or code cell source: methods/methods-code1 is notebook source with 1 match(es). + Action: Create a code-review task and rerun the notebook before sharing updated outputs. +- [high] Replacement would alter citation keys or reference anchors: references/ref-key contains citation/reference text matching "alpha". + Action: Route citation-key changes through the reference manager merge workflow instead of direct replacement. +- [blocker] Batch replacement touches a locked or final-review section: final-figures/fig1-caption has 2 match(es) while section state is locked. + Action: Exclude the section, unlock with an audit reason, or convert matches to explicit suggestions. +- [medium] Replacement touches cross-reference anchors: final-figures/fig1-caption contains figure, table, or equation anchors. + Action: Regenerate cross-reference maps and hold export until anchors resolve. +- [medium] Replacement would invalidate an inline comment quote anchor: Comment cmt-17 anchors to text that would be replaced in block methods-p1. + Action: Convert the edit to a suggestion and re-anchor or resolve the comment before applying. +- [medium] WYSIWYG replacement crosses source-only content: The operation starts in WYSIWYG mode but reaches LaTeX, citation, or code source blocks. + Action: Split the operation by block type so source-mode changes receive explicit review. + +## Safety +- Synthetic document packet only; no live editor or private manuscript data. diff --git a/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-summary.svg b/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-summary.svg new file mode 100644 index 00000000..8e6b6b4f --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/reports/find-replace-summary.svg @@ -0,0 +1,20 @@ + + + +Collaborative Find/Replace Safety Guard +Preview batch edits before shared manuscript mutation + +HOLD-BATCH-EDIT +Risk score: 100 +HIGH: Replacement would alter LaTeX command or equation source +LATEX_COMMAND_MUTATION +HIGH: Replacement would mutate notebook or code cell source +CODE_CELL_MUTATION +HIGH: Replacement would alter citation keys or reference anchors +CITATION_KEY_MUTATION +BLOCKER: Batch replacement touches a locked or final-review section +LOCKED_SECTION_MATCH +MEDIUM: Replacement touches cross-reference anchors +CROSS_REFERENCE_TOUCH +Audit digest: 855bf874ddecf9aa | Synthetic data only | No external services + \ No newline at end of file diff --git a/collaborative-research-editor/find-replace-safety-guard/sample-data.js b/collaborative-research-editor/find-replace-safety-guard/sample-data.js new file mode 100644 index 00000000..48197fad --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/sample-data.js @@ -0,0 +1,102 @@ +const riskyDocument = { + id: "collab-doc-12-find-replace", + sections: [ + { + id: "methods", + locked: false, + freeze: "draft", + blocks: [ + { + id: "methods-p1", + type: "paragraph", + content: "The alpha assay was selected for the pilot manuscript because alpha response is reproducible.", + }, + { + id: "methods-eq1", + type: "latex", + content: "\\alpha = 0.05; \\frac{signal_{alpha}}{baseline}", + }, + { + id: "methods-code1", + type: "notebook", + content: "model <- lm(response ~ alpha + baseline, data = notebook_frame)", + }, + ], + }, + { + id: "references", + locked: false, + freeze: "draft", + blocks: [ + { + id: "ref-key", + type: "citation", + key: "@alpha2024", + content: "Alpha response benchmark, DOI 10.0000/example.", + }, + ], + }, + { + id: "final-figures", + locked: true, + freeze: "final-review", + blocks: [ + { + id: "fig1-caption", + type: "cross-reference", + anchor: "fig:alpha-response", + caption: "Figure 1. Alpha response across cohorts.", + }, + ], + }, + ], + comments: [ + { + id: "cmt-17", + blockId: "methods-p1", + quote: "alpha response is reproducible", + }, + ], +}; + +const safeDocument = { + id: "collab-doc-12-safe-preview", + sections: [ + { + id: "intro", + locked: false, + freeze: "draft", + blocks: [ + { + id: "intro-p1", + type: "paragraph", + content: "This pilot manuscript uses an exploratory workflow for reader feedback.", + }, + ], + }, + ], + comments: [], +}; + +const riskyOperation = { + find: "alpha", + replace: "omega", + mode: "wysiwyg", + wholeWord: false, + scopeSections: ["methods", "references", "final-figures"], +}; + +const safeOperation = { + find: "pilot", + replace: "feasibility", + mode: "wysiwyg", + wholeWord: true, + scopeSections: ["intro"], +}; + +module.exports = { + riskyDocument, + safeDocument, + riskyOperation, + safeOperation, +}; diff --git a/collaborative-research-editor/find-replace-safety-guard/test.js b/collaborative-research-editor/find-replace-safety-guard/test.js new file mode 100644 index 00000000..e192270e --- /dev/null +++ b/collaborative-research-editor/find-replace-safety-guard/test.js @@ -0,0 +1,33 @@ +const assert = require("assert"); +const { replacementPreview, renderMarkdownReport, renderSvgSummary, digest } = require("./index"); +const { riskyDocument, safeDocument, riskyOperation, safeOperation } = require("./sample-data"); + +const risky = replacementPreview(riskyDocument, riskyOperation); +assert.strictEqual(risky.decision, "hold-batch-edit"); +assert.ok(risky.riskScore >= 90, "risky batch edit should produce a strong hold score"); +assert.ok(risky.findings.some((finding) => finding.code === "LOCKED_SECTION_MATCH")); +assert.ok(risky.findings.some((finding) => finding.code === "LATEX_COMMAND_MUTATION")); +assert.ok(risky.findings.some((finding) => finding.code === "CODE_CELL_MUTATION")); +assert.ok(risky.findings.some((finding) => finding.code === "CITATION_KEY_MUTATION")); +assert.ok(risky.findings.some((finding) => finding.code === "COMMENT_ANCHOR_DRIFT")); +assert.ok(risky.findings.some((finding) => finding.code === "WYSIWYG_SCOPE_MISMATCH")); + +const repeat = replacementPreview(riskyDocument, riskyOperation); +assert.strictEqual(risky.auditDigest, repeat.auditDigest, "audit digest must be deterministic"); + +const safe = replacementPreview(safeDocument, safeOperation); +assert.strictEqual(safe.decision, "safe-to-apply"); +assert.strictEqual(safe.findings.length, 0); +assert.strictEqual(safe.changedBlocks.length, 1); + +const markdown = renderMarkdownReport(risky); +assert.ok(markdown.includes("Collaborative Find/Replace Safety Review")); +assert.ok(markdown.includes("Synthetic document packet only")); + +const svg = renderSvgSummary(risky); +assert.ok(svg.includes("