// Native Workflow v2 owns node geometry, mainPath and routing. The portal adds // multiline typography: upstream's three single-line labels are too small for // a five-stage reading view. Do not change the pinned vendor or graph semantics. const escape = text => text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"'); const wrap = (text, limit = 21) => { const lines = []; for (const word of text.split(/\s+/)) { if (!lines.length || `${lines.at(-1)} ${word}`.length > limit) lines.push(word); else lines[lines.length - 1] += ` ${word}`; } return lines; }; export function roadmapViewer(html, spec) { if (spec.diagram_type !== 'workflow' || spec.schema_version !== 2 || spec.nodes.length !== 5) throw new Error('Roadmap workflow contract drift'); const boxes = []; for (const [index, node] of spec.nodes.entries()) { const start = html.indexOf(`', next) + 4; if (start < 0 || next < start || end < next) throw new Error(`Roadmap node drift: ${node.id}`); const original = html.slice(start, end); const rect = / { const lines = wrap(value, size === 30 ? 18 : 21); const maxLines = top === 160 ? 3 : 2; if (lines.length > maxLines || lines.some(line => line.length > 21)) throw new Error(`Roadmap copy exceeds card capacity: ${node.id}`); return `${lines.map((line,i) => `${escape(line)}`).join('')}`; }; if (!node.sublabel || node.tag || /Output:|Exit:|\n/.test(node.sublabel)) throw new Error(`Roadmap content contract drift: ${node.id}`); const content = text(String(index+1), 40, 26, 't-primary', 'data-stage-number="" font-weight="700"') + text(node.label, 88, 30, 't-primary', 'data-node-label="" font-weight="700"') + text(node.sublabel, 160, 26, 't-muted'); // Preserve native node title/metadata and rectangles; replace sigil/text only. // Sigils are nested SVG groups. Anchor on the two native node rectangles. const rectangles = [...original.matchAll(/]*\/>/g)]; if (rectangles.length !== 2) throw new Error('Roadmap shape contract drift'); const prefixEnd = rectangles[1].index + rectangles[1][0].length; const replacement = original.slice(0,prefixEnd).replaceAll('rx="6"', 'rx="0"') + content + ''; html = html.slice(0,start) + replacement + html.slice(end); } const left = Math.min(...boxes.map(b => b.x)) - 12; const top = Math.min(...boxes.map(b => b.y)) - 12; const width = Math.max(...boxes.map(b => b.x+b.w)) - left + 12; const height = Math.max(...boxes.map(b => b.y+b.h)) - top + 12; // One editorial lane; its title/frame would repeat the page's framing. html = html.replace(/]*data-composition-frame-kind="lane"[^>]*\/>\s*]*>[\s\S]*?<\/text>/g, ''); const roots = [...html.matchAll(/]*role="img"[^>]*>/g)]; if (roots.length !== 1) throw new Error('Roadmap SVG contract drift'); const root = roots[0][0].replace(/viewBox="[^"]*"/, `viewBox="${left} ${top} ${width} ${height}"`); const styles = ``; html = html.replace(roots[0][0], root + styles); return html; }