Loading tools/interoperability_visualizer/README.md +2 −1 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ Output paths and non-HTML output names are rejected. The complete `output/` dire - Select any concrete interoperability test permutation. - Switch between the full-size **Preview** graph and **Expected output** workspace. - Brokers use a left-to-right hierarchy rooted at `b1`; each registration hop advances one level. - Hover over a broker to inspect every entity created in that broker during setup. - Hover over a registration arc to inspect its effective creation payload. - Browse assertion-derived expected responses by broker or in execution order. Loading @@ -51,7 +52,7 @@ Output paths and non-HTML output names are rejected. The complete `output/` dire - Expand a response step to see its prominent HTTP status, request, assertions, and expected JSON inline. - Click a broker or registration to pin it; press Escape or use the close button to unpin it. - Drag brokers to rearrange the architecture. - Drag empty graph space to pan, use the mouse wheel to zoom, and use **Reset view** to restore the viewport. - Drag empty graph space to pan, use the mouse wheel to zoom, and use **Reset view** to restore the hierarchy and viewport. Registration arrows start at the broker where the registration is created and point to the broker referenced by its endpoint. Effective payloads resolve entity IDs and contexts while keeping runtime broker URLs and generated Loading tools/interoperability_visualizer/tests/test_visualizer.py +4 −0 Original line number Diff line number Diff line Loading @@ -369,6 +369,10 @@ class GeneratorTest(unittest.TestCase): "Assertions and expected output after Step", "Robot operation", "correlated expected HTTP response", "level-guide", "initialPositions(test)", "levels.set(target, nextLevel)", "renderLevels()", ): self.assertIn(value, template) Loading tools/interoperability_visualizer/viewer_template.html +112 −16 Original line number Diff line number Diff line Loading @@ -859,6 +859,22 @@ outline: none; } .level-guide { stroke: #d7e0e8; stroke-width: 1; stroke-dasharray: 5 7; vector-effect: non-scaling-stroke; } .level-label { fill: #74869a; font-size: 11px; font-weight: 750; letter-spacing: 0.06em; text-anchor: middle; text-transform: uppercase; } .node-circle { fill: #fff; stroke: #183957; Loading Loading @@ -1072,6 +1088,7 @@ <desc id="graph-description">Brokers are nodes and context source registrations are directed arcs.</desc> <defs id="markers"></defs> <g id="viewport"> <g id="levels"></g> <g id="edges"></g> <g id="nodes"></g> </g> Loading Loading @@ -1129,6 +1146,7 @@ const responsesPanel = document.querySelector(".responses-panel"); const svg = document.getElementById("graph"); const viewport = document.getElementById("viewport"); const levelsLayer = document.getElementById("levels"); const nodesLayer = document.getElementById("nodes"); const edgesLayer = document.getElementById("edges"); const markers = document.getElementById("markers"); Loading @@ -1146,6 +1164,7 @@ const outputViewButton = document.getElementById("output-view"); const positions = new Map(); const nodeElements = new Map(); let layoutColumns = []; let currentTest = null; let pinned = null; let dragState = null; Loading Loading @@ -1224,26 +1243,93 @@ } } function initialPositions(brokers) { function brokerNumber(id) { const match = /^b(\d+)$/.exec(id); return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER; } function initialPositions(test) { positions.clear(); const center = { x: 450, y: 305 }; if (brokers.length === 1) { positions.set(brokers[0].id, center); const brokerIds = test.brokers .map((broker) => broker.id) .sort((left, right) => brokerNumber(left) - brokerNumber(right) || left.localeCompare(right)); if (!brokerIds.length) { layoutColumns = []; return; } if (brokers.length === 2) { positions.set(brokers[0].id, { x: 275, y: 305 }); positions.set(brokers[1].id, { x: 625, y: 305 }); return; const root = brokerIds.includes("b1") ? "b1" : brokerIds[0]; const adjacency = new Map(brokerIds.map((id) => [id, []])); for (const registration of test.registrations) { if (!adjacency.has(registration.source)) adjacency.set(registration.source, []); adjacency.get(registration.source).push(registration.target); } for (const targets of adjacency.values()) { targets.sort((left, right) => brokerNumber(left) - brokerNumber(right) || left.localeCompare(right)); } const levels = new Map([[root, 0]]); const queue = [root]; for (let index = 0; index < queue.length; index += 1) { const source = queue[index]; const nextLevel = levels.get(source) + 1; for (const target of adjacency.get(source) || []) { if (!levels.has(target)) { levels.set(target, nextLevel); queue.push(target); } const radius = brokers.length > 4 ? 235 : 205; brokers.forEach((broker, index) => { const angle = -Math.PI / 2 + index * Math.PI * 2 / brokers.length; positions.set(broker.id, { x: center.x + Math.cos(angle) * radius, y: center.y + Math.sin(angle) * radius, } } const disconnectedLevel = Math.max(...levels.values()) + 1; for (const brokerId of brokerIds) { if (!levels.has(brokerId)) levels.set(brokerId, disconnectedLevel); } const byLevel = new Map(); for (const brokerId of brokerIds) { const level = levels.get(brokerId); if (!byLevel.has(level)) byLevel.set(level, []); byLevel.get(level).push(brokerId); } const orderedLevels = [...byLevel.keys()].sort((left, right) => left - right); const maxLevel = Math.max(...orderedLevels); const left = 135; const right = 765; const top = 120; const bottom = 520; layoutColumns = orderedLevels.map((level) => { const x = maxLevel === 0 ? left : left + (right - left) * level / maxLevel; const ids = byLevel.get(level); ids.forEach((id, index) => { const y = ids.length === 1 ? (top + bottom) / 2 : top + (bottom - top) * index / (ids.length - 1); positions.set(id, { x, y }); }); return { level, x }; }); } function renderLevels() { levelsLayer.replaceChildren(); for (const column of layoutColumns) { levelsLayer.append(svgElement("line", { class: "level-guide", x1: column.x, y1: "62", x2: column.x, y2: "575", })); const label = svgElement("text", { class: "level-label", x: column.x, y: "42", }); label.textContent = `Level ${column.level}`; levelsLayer.append(label); } } function renderNodes(test) { Loading Loading @@ -1796,6 +1882,15 @@ applyView(); } function resetLayout() { if (!currentTest) return; initialPositions(currentTest); renderLevels(); renderNodes(currentTest); renderEdges(currentTest); resetView(); } svg.addEventListener("pointerdown", (event) => { if (event.button !== 0 || event.target !== svg) return; const point = clientToSvg(event); Loading Loading @@ -1862,9 +1957,10 @@ if (!currentTest) return; pinned = null; detailsPanel.classList.remove("pinned"); initialPositions(currentTest.brokers); initialPositions(currentTest); resetView(); renderSummary(currentTest); renderLevels(); renderNodes(currentTest); renderEdges(currentTest); renderResponses(currentTest); Loading @@ -1874,7 +1970,7 @@ select.addEventListener("change", () => selectTest(Number(select.value))); detailsClose.addEventListener("click", unpin); resetViewButton.addEventListener("click", resetView); resetViewButton.addEventListener("click", resetLayout); brokerViewButton.addEventListener("click", () => setResponseView("broker")); timelineViewButton.addEventListener("click", () => setResponseView("timeline")); previewViewButton.addEventListener("click", () => setSurfaceView("architecture")); Loading Loading
tools/interoperability_visualizer/README.md +2 −1 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ Output paths and non-HTML output names are rejected. The complete `output/` dire - Select any concrete interoperability test permutation. - Switch between the full-size **Preview** graph and **Expected output** workspace. - Brokers use a left-to-right hierarchy rooted at `b1`; each registration hop advances one level. - Hover over a broker to inspect every entity created in that broker during setup. - Hover over a registration arc to inspect its effective creation payload. - Browse assertion-derived expected responses by broker or in execution order. Loading @@ -51,7 +52,7 @@ Output paths and non-HTML output names are rejected. The complete `output/` dire - Expand a response step to see its prominent HTTP status, request, assertions, and expected JSON inline. - Click a broker or registration to pin it; press Escape or use the close button to unpin it. - Drag brokers to rearrange the architecture. - Drag empty graph space to pan, use the mouse wheel to zoom, and use **Reset view** to restore the viewport. - Drag empty graph space to pan, use the mouse wheel to zoom, and use **Reset view** to restore the hierarchy and viewport. Registration arrows start at the broker where the registration is created and point to the broker referenced by its endpoint. Effective payloads resolve entity IDs and contexts while keeping runtime broker URLs and generated Loading
tools/interoperability_visualizer/tests/test_visualizer.py +4 −0 Original line number Diff line number Diff line Loading @@ -369,6 +369,10 @@ class GeneratorTest(unittest.TestCase): "Assertions and expected output after Step", "Robot operation", "correlated expected HTTP response", "level-guide", "initialPositions(test)", "levels.set(target, nextLevel)", "renderLevels()", ): self.assertIn(value, template) Loading
tools/interoperability_visualizer/viewer_template.html +112 −16 Original line number Diff line number Diff line Loading @@ -859,6 +859,22 @@ outline: none; } .level-guide { stroke: #d7e0e8; stroke-width: 1; stroke-dasharray: 5 7; vector-effect: non-scaling-stroke; } .level-label { fill: #74869a; font-size: 11px; font-weight: 750; letter-spacing: 0.06em; text-anchor: middle; text-transform: uppercase; } .node-circle { fill: #fff; stroke: #183957; Loading Loading @@ -1072,6 +1088,7 @@ <desc id="graph-description">Brokers are nodes and context source registrations are directed arcs.</desc> <defs id="markers"></defs> <g id="viewport"> <g id="levels"></g> <g id="edges"></g> <g id="nodes"></g> </g> Loading Loading @@ -1129,6 +1146,7 @@ const responsesPanel = document.querySelector(".responses-panel"); const svg = document.getElementById("graph"); const viewport = document.getElementById("viewport"); const levelsLayer = document.getElementById("levels"); const nodesLayer = document.getElementById("nodes"); const edgesLayer = document.getElementById("edges"); const markers = document.getElementById("markers"); Loading @@ -1146,6 +1164,7 @@ const outputViewButton = document.getElementById("output-view"); const positions = new Map(); const nodeElements = new Map(); let layoutColumns = []; let currentTest = null; let pinned = null; let dragState = null; Loading Loading @@ -1224,26 +1243,93 @@ } } function initialPositions(brokers) { function brokerNumber(id) { const match = /^b(\d+)$/.exec(id); return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER; } function initialPositions(test) { positions.clear(); const center = { x: 450, y: 305 }; if (brokers.length === 1) { positions.set(brokers[0].id, center); const brokerIds = test.brokers .map((broker) => broker.id) .sort((left, right) => brokerNumber(left) - brokerNumber(right) || left.localeCompare(right)); if (!brokerIds.length) { layoutColumns = []; return; } if (brokers.length === 2) { positions.set(brokers[0].id, { x: 275, y: 305 }); positions.set(brokers[1].id, { x: 625, y: 305 }); return; const root = brokerIds.includes("b1") ? "b1" : brokerIds[0]; const adjacency = new Map(brokerIds.map((id) => [id, []])); for (const registration of test.registrations) { if (!adjacency.has(registration.source)) adjacency.set(registration.source, []); adjacency.get(registration.source).push(registration.target); } for (const targets of adjacency.values()) { targets.sort((left, right) => brokerNumber(left) - brokerNumber(right) || left.localeCompare(right)); } const levels = new Map([[root, 0]]); const queue = [root]; for (let index = 0; index < queue.length; index += 1) { const source = queue[index]; const nextLevel = levels.get(source) + 1; for (const target of adjacency.get(source) || []) { if (!levels.has(target)) { levels.set(target, nextLevel); queue.push(target); } const radius = brokers.length > 4 ? 235 : 205; brokers.forEach((broker, index) => { const angle = -Math.PI / 2 + index * Math.PI * 2 / brokers.length; positions.set(broker.id, { x: center.x + Math.cos(angle) * radius, y: center.y + Math.sin(angle) * radius, } } const disconnectedLevel = Math.max(...levels.values()) + 1; for (const brokerId of brokerIds) { if (!levels.has(brokerId)) levels.set(brokerId, disconnectedLevel); } const byLevel = new Map(); for (const brokerId of brokerIds) { const level = levels.get(brokerId); if (!byLevel.has(level)) byLevel.set(level, []); byLevel.get(level).push(brokerId); } const orderedLevels = [...byLevel.keys()].sort((left, right) => left - right); const maxLevel = Math.max(...orderedLevels); const left = 135; const right = 765; const top = 120; const bottom = 520; layoutColumns = orderedLevels.map((level) => { const x = maxLevel === 0 ? left : left + (right - left) * level / maxLevel; const ids = byLevel.get(level); ids.forEach((id, index) => { const y = ids.length === 1 ? (top + bottom) / 2 : top + (bottom - top) * index / (ids.length - 1); positions.set(id, { x, y }); }); return { level, x }; }); } function renderLevels() { levelsLayer.replaceChildren(); for (const column of layoutColumns) { levelsLayer.append(svgElement("line", { class: "level-guide", x1: column.x, y1: "62", x2: column.x, y2: "575", })); const label = svgElement("text", { class: "level-label", x: column.x, y: "42", }); label.textContent = `Level ${column.level}`; levelsLayer.append(label); } } function renderNodes(test) { Loading Loading @@ -1796,6 +1882,15 @@ applyView(); } function resetLayout() { if (!currentTest) return; initialPositions(currentTest); renderLevels(); renderNodes(currentTest); renderEdges(currentTest); resetView(); } svg.addEventListener("pointerdown", (event) => { if (event.button !== 0 || event.target !== svg) return; const point = clientToSvg(event); Loading Loading @@ -1862,9 +1957,10 @@ if (!currentTest) return; pinned = null; detailsPanel.classList.remove("pinned"); initialPositions(currentTest.brokers); initialPositions(currentTest); resetView(); renderSummary(currentTest); renderLevels(); renderNodes(currentTest); renderEdges(currentTest); renderResponses(currentTest); Loading @@ -1874,7 +1970,7 @@ select.addEventListener("change", () => selectTest(Number(select.value))); detailsClose.addEventListener("click", unpin); resetViewButton.addEventListener("click", resetView); resetViewButton.addEventListener("click", resetLayout); brokerViewButton.addEventListener("click", () => setResponseView("broker")); timelineViewButton.addEventListener("click", () => setResponseView("timeline")); previewViewButton.addEventListener("click", () => setSurfaceView("architecture")); Loading