Commit a044b73df2312669a22174fcdb5716238068a80e

Authored by graphlemon
1 parent ed477539

graphlemon: auto-commit uncommitted changes for judgment-550c6f093701-main-checkout-drift

frontend/app.js
... ... @@ -2104,6 +2104,7 @@ function mountGraphConsole() {
2104 2104 }
2105 2105 workGraphPanelsEl.innerHTML = panelsHtml;
2106 2106 mountGraphVisualizerTooltips(workGraphPanelsEl);
  2107 + mountGraphVisualizerEdges(workGraphPanelsEl);
2107 2108 mountWorkNodeClickExpand(workGraphPanelsEl, { onNodeClick: onWorkNodeClick });
2108 2109 }
2109 2110  
... ...
frontend/style.css
... ... @@ -136,6 +136,22 @@ table.data-table tr[data-task-id]:hover td { background: #f9fafb; }
136 136 .work-node.status-waiting_for_quota::after { content: "🕐"; position: absolute; top: -8px; right: -8px; font-size: 14px; }
137 137 .work-node.status-blocked, .work-node.status-failed { border-color: var(--status-blocked); background: #fef2f2; }
138 138 .work-node.status-done { border-color: var(--status-done); background: #f0fdf4; }
  139 +
  140 +/* 2026-08-08 (Work Graph self-planning + loop-back): on_failure edges
  141 + (loop_back) drawn as lines over the existing row layout, plus a badge
  142 + for retry_self loops (which have no edge to draw -- see
  143 + mountGraphVisualizerEdges/countSelfLoopAttempts in app.js). The svg
  144 + sits absolutely over .work-graph (already position:relative) and
  145 + paints behind the node boxes since it appears earlier in the DOM (see
  146 + renderGraphVisualizerPanel) -- no z-index juggling needed. */
  147 +.graph-edges-svg { position: absolute; top: 0; left: 0; pointer-events: none; overflow: visible; }
  148 +.edge-on-failure { stroke: #dc2626; stroke-width: 2; stroke-dasharray: 5 3; fill: none; }
  149 +.edge-label { font-size: 10px; fill: #dc2626; text-anchor: middle; paint-order: stroke; stroke: #fff; stroke-width: 3px; }
  150 +.loop-badge {
  151 + display: inline-block; margin-left: 6px; padding: 0 5px; border-radius: 9px;
  152 + background: #fee2e2; color: #dc2626; font-size: 10px; font-weight: 600; cursor: help;
  153 +}
  154 +.edge-sample { display: inline-block; width: 20px; height: 0; border-top: 2px dashed #dc2626; }
139 155 .legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: #6b7280; }
140 156 .legend-item { display: flex; align-items: center; gap: 6px; }
141 157 .legend-swatch { width: 14px; height: 14px; border-radius: 4px; border: 2px solid; }
... ...
frontend/test_graph_console_smoke.js
... ... @@ -121,6 +121,79 @@ new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox);
121 121 assert.ok(graphConsoleHtml.includes("work-node status-running clickable"), "opted-in call adds the clickable class");
122 122 assert.ok(graphConsoleHtml.includes('data-clickable-node-exec-id="exec-1"'), "opted-in call adds the click-target id");
123 123 assert.ok(graphConsoleHtml.includes('<div class="log-viewer-slot">'), "opted-in call reserves a log-viewer-slot");
  124 + assert.ok(graphConsoleHtml.includes('data-node-id="n1"'), "every node carries data-node-id for edge-drawing lookup");
  125 +}
  126 +
  127 +// ---- renderGraphVisualizerPanel: Work Graph self-planning + loop-back
  128 +// (2026-08-08) -- retry_self badge and on_failure edge data. Real line
  129 +// positions require getBoundingClientRect (mountGraphVisualizerEdges),
  130 +// which this bare vm sandbox can't provide -- that part is manual-browser-
  131 +// verified only, same convention this file's own header note already
  132 +// documents for DOM-rendering functions in general. What CAN be
  133 +// mechanically checked here is the HTML-string generation itself: the
  134 +// badge text/count, and the data-on-failure-edges payload the real mount
  135 +// function would read.
  136 +{
  137 + const runWithSelfLoop = {
  138 + org_node: "planner", run_id: "run-2",
  139 + graph_manifest: { nodes: [{ node_id: "benchmark", depends_on: [] }] },
  140 + nodes: [{ node_id: "benchmark", node_exec_id: "exec-2", subagent_type: "planner", status: "reflecting" }],
  141 + loop_back_events: [
  142 + { source_node_id: "benchmark", target_node_id: "benchmark", attempt_number: 1, feedback: ["missing scenario"] },
  143 + { source_node_id: "benchmark", target_node_id: "benchmark", attempt_number: 2, feedback: ["still missing"] },
  144 + ],
  145 + };
  146 + const html = sandbox.renderGraphVisualizerPanel(runWithSelfLoop);
  147 + assert.ok(html.includes('class="loop-badge"'), "a node with self-loop events gets a loop-badge");
  148 + assert.ok(html.includes(">↺ 2<"), "the badge shows the real count of self-loop attempts, not just a presence flag");
  149 + // the attribute is always present (simpler than conditionally omitting
  150 + // it), just empty when there's no on_failure edge to draw.
  151 + assert.ok(html.includes('data-on-failure-edges="[]"'), "retry_self alone has no on_failure edges -- payload is an empty array");
  152 +
  153 + const runWithoutLoop = {
  154 + org_node: "planner", run_id: "run-3",
  155 + graph_manifest: { nodes: [{ node_id: "brd", depends_on: [] }] },
  156 + nodes: [{ node_id: "brd", node_exec_id: "exec-3", subagent_type: "planner", status: "done" }],
  157 + };
  158 + const plainHtml = sandbox.renderGraphVisualizerPanel(runWithoutLoop);
  159 + assert.ok(!plainHtml.includes("loop-badge"), "a run with no loop_back_events at all (older tasks) renders no badge");
  160 +
  161 + const runWithLoopBackEdge = {
  162 + org_node: "planner", run_id: "run-4",
  163 + graph_manifest: {
  164 + nodes: [{ node_id: "sdd", depends_on: [] }, { node_id: "benchmark", depends_on: ["sdd"] }],
  165 + edges: [{ from: "benchmark", to: "sdd", edge_type: "on_failure" }],
  166 + },
  167 + nodes: [
  168 + { node_id: "sdd", node_exec_id: "exec-sdd", subagent_type: "planner", status: "done" },
  169 + { node_id: "benchmark", node_exec_id: "exec-bm", subagent_type: "planner", status: "running" },
  170 + ],
  171 + loop_back_events: [
  172 + { source_node_id: "benchmark", target_node_id: "sdd", attempt_number: 1, feedback: ["x"] },
  173 + ],
  174 + };
  175 + const loopBackHtml = sandbox.renderGraphVisualizerPanel(runWithLoopBackEdge);
  176 + assert.ok(loopBackHtml.includes("data-on-failure-edges"), "a real on_failure edge emits the edges payload for mountGraphVisualizerEdges to read");
  177 + assert.ok(loopBackHtml.includes("&quot;from&quot;:&quot;benchmark&quot;"), "the payload's escaped JSON names the real source node");
  178 + assert.ok(loopBackHtml.includes("&quot;triggerCount&quot;:1"), "the payload counts real triggers from loop_back_events, not just declares the edge");
  179 + assert.ok(loopBackHtml.includes("loop_back(失败回路)"), "the legend explains the on_failure line when one is present");
  180 +
  181 + const noEdgeLegendHtml = sandbox.renderGraphVisualizerPanel(runWithSelfLoop);
  182 + assert.ok(!noEdgeLegendHtml.includes("loop_back(失败回路)"), "no on_failure edge -- legend stays unchanged, no dangling explanation");
  183 +}
  184 +
  185 +// ---- countSelfLoopAttempts ----
  186 +{
  187 + const run = {
  188 + loop_back_events: [
  189 + { source_node_id: "a", target_node_id: "a", attempt_number: 1 },
  190 + { source_node_id: "a", target_node_id: "b", attempt_number: 1 }, // loop_back, not a self-loop -- must not count
  191 + { source_node_id: "a", target_node_id: "a", attempt_number: 2 },
  192 + ],
  193 + };
  194 + assert.strictEqual(sandbox.countSelfLoopAttempts(run, "a"), 2, "counts only true self-loops (source===target===nodeId)");
  195 + assert.strictEqual(sandbox.countSelfLoopAttempts(run, "b"), 0, "a node that was only ever a loop_back TARGET (not source) has zero self-loops");
  196 + assert.strictEqual(sandbox.countSelfLoopAttempts({}, "a"), 0, "missing loop_back_events (older task) is treated as zero, not a crash");
124 197 }
125 198  
126 199 // ---- renderArtifactSummaryList (FR-5) ----
... ...