Commit 8c2f05f7 authored by Marco Cavalli's avatar Marco Cavalli
Browse files

feat: implement left to right graph propagation

parent 1e36e53f
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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.
@@ -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
+4 −0
Original line number Diff line number Diff line
@@ -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)

+112 −16
Original line number Diff line number Diff line
@@ -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;
@@ -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>
@@ -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");
@@ -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;
@@ -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) {
@@ -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);
@@ -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);
@@ -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"));