@tea-agent/loop-agent 0.26.0 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +1056 -1020
  2. package/README.md +8 -3
  3. package/bin/loop-agent.js +21 -21
  4. package/dist/application/dag/generate-task-dag.js +33 -0
  5. package/dist/cli/command-definitions.js +25 -10
  6. package/dist/cli/help.js +4 -3
  7. package/dist/cli/program.js +43 -17
  8. package/dist/commands/cursor-prompt.js +6 -6
  9. package/dist/commands/import-prd.js +7 -2
  10. package/dist/commands/init.js +7 -5
  11. package/dist/commands/loop-benchmark.js +11 -11
  12. package/dist/commands/pi-reuse-benchmark.js +16 -16
  13. package/dist/commands/task-source-prepare.js +474 -0
  14. package/dist/executors/dag-pi-executor.js +40 -5
  15. package/dist/executors/shell-executor.js +111 -0
  16. package/dist/executors/shell-presets.js +12 -4
  17. package/dist/executors/shell-write-guard.js +161 -25
  18. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  19. package/dist/task/config-types.js +6 -0
  20. package/dist/task/contract/constants.js +1 -0
  21. package/dist/task/contract/project.js +8 -0
  22. package/dist/task/contract/schema.js +1 -0
  23. package/dist/task/frontend-preflight.js +131 -0
  24. package/dist/task/runtime.js +2 -4
  25. package/dist/task/source-prepare/build-draft.js +224 -0
  26. package/dist/task/source-prepare/completeness.js +195 -0
  27. package/dist/task/source-prepare/index.js +7 -0
  28. package/dist/task/source-prepare/parse-intent.js +373 -0
  29. package/dist/task/source-prepare/path-policy.js +197 -0
  30. package/dist/task/source-prepare/prepare.js +506 -0
  31. package/dist/task/source-prepare/reference-integrity.js +274 -0
  32. package/dist/task/source-prepare/types.js +7 -0
  33. package/dist/worker/observability/read-model.js +134 -0
  34. package/dist/worker/observe/static/copy.js +67 -67
  35. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  36. package/dist/worker/observe/static/dag-layout.js +83 -83
  37. package/dist/worker/observe/static/dom.js +220 -220
  38. package/dist/worker/observe/static/relations.js +133 -133
  39. package/dist/worker/observe/static/router.js +93 -93
  40. package/dist/worker/observe/static/run-processing.js +148 -148
  41. package/dist/worker/observe/static/state.js +61 -0
  42. package/dist/worker/observe/static/styles.css +8 -0
  43. package/dist/worker/observe/static/views/batch.js +227 -227
  44. package/dist/worker/observe/static/views/dag-graph.js +248 -172
  45. package/dist/worker/observe/static/views/dag-inspector.js +374 -157
  46. package/dist/worker/observe/static/views/dag.js +4 -11
  47. package/dist/worker/observe/static/views/failures.js +143 -143
  48. package/dist/worker/observe/static/views/feature.js +492 -492
  49. package/dist/worker/observe/static/views/run.js +453 -453
  50. package/dist/worker/observe/static/views/shell.js +7 -7
  51. package/dist/worker/observe/static/views/timeline.js +163 -163
  52. package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
  53. package/dist/workflows/dag/canvas-observer.js +275 -275
  54. package/dist/workflows/dag/convergence/controller.js +110 -21
  55. package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
  56. package/dist/workflows/dag/frontend-review-context.js +7 -1
  57. package/dist/workflows/dag/frontend-verification-trace.js +14 -3
  58. package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
  59. package/dist/workflows/dag/init-hybrid.js +96 -34
  60. package/dist/workflows/dag/output-protocol.js +180 -7
  61. package/dist/workflows/dag/runner.js +141 -52
  62. package/dist/workflows/dag/types.js +4 -0
  63. package/dist/workflows/dag/validate.js +3 -2
  64. package/docs/skills/README.md +7 -7
  65. package/docs/templates/adr.md +60 -60
  66. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  67. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  68. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  69. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  70. package/docs/templates/agent-dag-report.schema.json +473 -473
  71. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  72. package/docs/templates/backend-test-dag.json +100 -8
  73. package/docs/templates/backend-test-result.schema.json +99 -99
  74. package/docs/templates/feature-spec.md +53 -53
  75. package/docs/templates/frontend-design-contract.md +42 -42
  76. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  77. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  78. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  79. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  80. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  81. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  82. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  83. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  84. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  85. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  86. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  87. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  88. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  89. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  90. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  91. package/docs/templates/frontend-eval/metrics.md +138 -138
  92. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  93. package/docs/templates/frontend-task-constraints.md +35 -35
  94. package/docs/templates/frontend-task-requirement.md +70 -70
  95. package/docs/templates/init-evolution-review.md +35 -35
  96. package/docs/templates/init-managed-agents.md +10 -5
  97. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  98. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  99. package/docs/templates/knowledge-sync-dag.json +178 -178
  100. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  101. package/docs/templates/product-line/closeout.yaml +9 -9
  102. package/docs/templates/product-line/design.md +13 -13
  103. package/docs/templates/product-line/links.md +10 -10
  104. package/docs/templates/product-line/requirement.md +17 -17
  105. package/docs/templates/product-line/test-plan.md +7 -7
  106. package/docs/templates/production-readiness-checklist.md +57 -57
  107. package/docs/templates/project-start-checklist.md +9 -9
  108. package/docs/templates/qa-report.md +48 -48
  109. package/docs/templates/sprint-contract.md +29 -29
  110. package/docs/templates/worker-dogfood-evidence.md +80 -80
  111. package/docs/templates/worker-dogfood-setup.md +68 -68
  112. package/package.json +1 -1
  113. package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
  114. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  115. package/scripts/kb-graph-materialize.mjs +105 -105
  116. package/scripts/kb-graph-promote.mjs +164 -164
  117. package/scripts/kb-query.mjs +554 -554
  118. package/skills/ai-engineering-context/SKILL.md +48 -48
  119. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  120. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  121. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  122. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  123. package/skills/analyze-product-dependencies/references/example.md +76 -76
  124. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  125. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  126. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  127. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  128. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  129. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  130. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  131. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  132. package/skills/analyze-product-requirements/SKILL.md +90 -90
  133. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  134. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  135. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  136. package/skills/analyze-product-requirements/references/example.md +86 -86
  137. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  138. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  139. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  140. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  141. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  142. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  143. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  144. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  145. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  146. package/skills/browser-tools/browser-content.js +103 -103
  147. package/skills/browser-tools/browser-cookies.js +35 -35
  148. package/skills/browser-tools/browser-eval.js +53 -53
  149. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  150. package/skills/browser-tools/browser-nav.js +44 -44
  151. package/skills/browser-tools/browser-pick.js +162 -162
  152. package/skills/browser-tools/browser-screenshot.js +34 -34
  153. package/skills/browser-tools/browser-start.js +86 -86
  154. package/skills/browser-tools/package-lock.json +2556 -2556
  155. package/skills/browser-tools/package.json +19 -19
  156. package/skills/code-review-core/SKILL.md +20 -20
  157. package/skills/codebase-scout/SKILL.md +19 -19
  158. package/skills/grill-me/SKILL.md +10 -10
  159. package/skills/loop-agent/SKILL.md +5 -2
  160. package/skills/loop-agent/references/README.md +67 -67
  161. package/skills/loop-agent/references/command-reference.md +17 -15
  162. package/skills/loop-agent/references/docs-converge.md +126 -126
  163. package/skills/loop-agent/references/harness-policy.md +3 -4
  164. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  165. package/skills/loop-agent/references/learned/README.md +21 -21
  166. package/skills/loop-agent/references/long-running-loop.md +57 -57
  167. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  168. package/skills/loop-agent/references/pi-prompt.md +23 -23
  169. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  170. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  171. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  172. package/skills/loop-agent/references/task-workflow.md +7 -5
  173. package/skills/playwright-cli/SKILL.md +420 -420
  174. package/skills/playwright-cli/references/element-attributes.md +23 -23
  175. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  176. package/skills/playwright-cli/references/request-mocking.md +87 -87
  177. package/skills/playwright-cli/references/running-code.md +241 -241
  178. package/skills/playwright-cli/references/session-management.md +225 -225
  179. package/skills/playwright-cli/references/storage-state.md +275 -275
  180. package/skills/playwright-cli/references/test-generation.md +433 -433
  181. package/skills/playwright-cli/references/tracing.md +139 -139
  182. package/skills/playwright-cli/references/video-recording.md +143 -143
  183. package/skills/requesting-code-review/SKILL.md +101 -101
  184. package/skills/requesting-code-review/code-reviewer.md +168 -168
  185. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  186. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  187. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  188. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  189. package/skills/systematic-debugging/find-polluter.sh +63 -63
  190. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  191. package/skills/systematic-debugging/test-academic.md +14 -14
  192. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  193. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  194. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  195. package/skills/using-git-worktrees/SKILL.md +215 -215
  196. package/skills/verification-before-completion/SKILL.md +154 -154
  197. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,172 +1,248 @@
1
- /** DAG dependency graph rendering (Observe UI R5). */
2
- import { layoutDag } from "../dag-layout.js";
3
- import { el } from "../dom.js";
4
- import {
5
- statusLabel,
6
- statusSymbol,
7
- formatMs,
8
- truncateText,
9
- } from "../format.js";
10
- import {
11
- uiState,
12
- dagGraphViewportStates,
13
- updateDagGraphViewportState,
14
- } from "../state.js";
15
- import { selectDagNode } from "./dag-inspector.js";
16
-
17
- export function svgEl(tag, attrs = {}) {
18
- const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
19
- for (const [key, value] of Object.entries(attrs))
20
- node.setAttribute(key, String(value));
21
- return node;
22
- }
23
-
24
- export function renderDagGraph(dag, dagRunId, existingWrap = null) {
25
- const nodes = dag.nodes ?? [];
26
- const edges = dag.edges ?? [];
27
- const layout = layoutDag(nodes, edges, dag.ranks ?? []);
28
- const wrap = existingWrap ?? el("div", "dag-graph-wrap");
29
- if (existingWrap) {
30
- // reuse-dag-graph-wrap: replace children while keeping wrap identity.
31
- while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
32
- }
33
- const toolbar = el("div", "dag-graph-toolbar");
34
- toolbar.appendChild(
35
- el(
36
- "span",
37
- "dag-graph-summary",
38
- `${nodes.length} 节点 · ${layout.edges.length} 条依赖`,
39
- ),
40
- );
41
- const reset = el("button", "dag-graph-reset", "重置视图");
42
- toolbar.appendChild(reset);
43
- wrap.appendChild(toolbar);
44
- if (edges.length === 0)
45
- wrap.appendChild(
46
- el(
47
- "p",
48
- "dag-graph-fallback",
49
- "该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。",
50
- ),
51
- );
52
- const viewport = el("div", "dag-graph-viewport");
53
- viewport.dataset.dagRunId = dagRunId;
54
- const svg = svgEl("svg", {
55
- class: "dag-graph",
56
- viewBox: `0 0 ${layout.width} ${layout.height}`,
57
- width: layout.width,
58
- height: layout.height,
59
- role: "img",
60
- "aria-label": `DAG 依赖图,共 ${nodes.length} 个节点`,
61
- });
62
- const defs = svgEl("defs");
63
- const marker = svgEl("marker", {
64
- id: "dag-arrow",
65
- viewBox: "0 0 10 10",
66
- refX: 9,
67
- refY: 5,
68
- markerWidth: 7,
69
- markerHeight: 7,
70
- orient: "auto-start-reverse",
71
- });
72
- marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
73
- defs.appendChild(marker);
74
- svg.appendChild(defs);
75
- const related = new Set([uiState.selectedDagNodeId]);
76
- for (const edge of layout.edges)
77
- if (
78
- edge.from === uiState.selectedDagNodeId ||
79
- edge.to === uiState.selectedDagNodeId
80
- ) {
81
- related.add(edge.from);
82
- related.add(edge.to);
83
- }
84
- for (const edge of layout.edges) {
85
- const path = svgEl("path", {
86
- d: edge.path,
87
- class: `dag-edge${uiState.selectedDagNodeId && (!related.has(edge.from) || !related.has(edge.to)) ? " dag-dimmed" : ""}`,
88
- "marker-end": "url(#dag-arrow)",
89
- });
90
- path.appendChild(svgEl("title"));
91
- path.firstChild.textContent = `${edge.from} → ${edge.to}`;
92
- svg.appendChild(path);
93
- }
94
- const byId = Object.fromEntries(nodes.map((node) => [node.nodeId, node]));
95
- for (const position of layout.nodes) {
96
- const node = byId[position.nodeId] ?? { nodeId: position.nodeId };
97
- const status = (node.status ?? "unknown").toLowerCase();
98
- const group = svgEl("g", {
99
- transform: `translate(${position.x} ${position.y})`,
100
- class: `dag-graph-node status-${status}${node.nodeId === uiState.selectedDagNodeId ? " selected" : ""}${uiState.selectedDagNodeId && !related.has(node.nodeId) ? " dag-dimmed" : ""}`,
101
- tabindex: 0,
102
- role: "button",
103
- "aria-label": `${node.nodeId},状态 ${status}`,
104
- });
105
- group.appendChild(
106
- svgEl("rect", { width: position.width, height: position.height, rx: 10 }),
107
- );
108
- const title = svgEl("title");
109
- title.textContent = `${node.nodeId}\n${node.executor ?? "未知执行器"}\n${status}`;
110
- group.appendChild(title);
111
- const label = svgEl("text", { x: 14, y: 26, class: "dag-node-label" });
112
- label.textContent = truncateText(node.label ?? node.nodeId, 26);
113
- group.appendChild(label);
114
- const executor = svgEl("text", { x: 14, y: 50, class: "dag-node-meta" });
115
- executor.textContent = `◈ ${node.executor ?? "unknown"}`;
116
- group.appendChild(executor);
117
- const state = svgEl("text", { x: 14, y: 74, class: "dag-node-status" });
118
- state.textContent = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`;
119
- group.appendChild(state);
120
- const activate = () => selectDagNode(dagRunId, node.nodeId);
121
- group.addEventListener("click", activate);
122
- group.addEventListener("keydown", (event) => {
123
- if (event.key === "Enter" || event.key === " ") {
124
- event.preventDefault();
125
- activate();
126
- }
127
- });
128
- svg.appendChild(group);
129
- }
130
- viewport.appendChild(svg);
131
- wrap.appendChild(viewport);
132
- const graphViewportState =
133
- dagGraphViewportStates.get(dagRunId) ??
134
- (uiState.dagGraphViewportState?.dagRunId === dagRunId
135
- ? uiState.dagGraphViewportState
136
- : null);
137
- viewport.addEventListener(
138
- "scroll",
139
- () => {
140
- uiState.dagGraphViewportState = updateDagGraphViewportState(
141
- uiState.dagGraphViewportState,
142
- dagRunId,
143
- viewport.scrollLeft,
144
- viewport.scrollTop,
145
- );
146
- dagGraphViewportStates.set(dagRunId, uiState.dagGraphViewportState);
147
- },
148
- { passive: true },
149
- );
150
- if (graphViewportState) {
151
- const restoreViewport = () => {
152
- viewport.scrollLeft = graphViewportState.scrollLeft;
153
- viewport.scrollTop = graphViewportState.scrollTop;
154
- };
155
- if (typeof requestAnimationFrame === "function") {
156
- requestAnimationFrame(restoreViewport);
157
- } else {
158
- restoreViewport();
159
- }
160
- }
161
- reset.addEventListener("click", () => {
162
- uiState.dagGraphViewportState = updateDagGraphViewportState(
163
- uiState.dagGraphViewportState,
164
- dagRunId,
165
- 0,
166
- 0,
167
- );
168
- dagGraphViewportStates.set(dagRunId, uiState.dagGraphViewportState);
169
- viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
170
- });
171
- return wrap;
172
- }
1
+ /** DAG dependency graph rendering (Observe UI R5). */
2
+ import { layoutDag } from "../dag-layout.js";
3
+ import { el } from "../dom.js";
4
+ import {
5
+ statusLabel,
6
+ statusSymbol,
7
+ formatMs,
8
+ truncateText,
9
+ } from "../format.js";
10
+ import {
11
+ uiState,
12
+ dagGraphViewportStates,
13
+ updateDagGraphViewportState,
14
+ } from "../state.js";
15
+ import { selectDagNode } from "./dag-inspector.js";
16
+
17
+ export function svgEl(tag, attrs = {}) {
18
+ const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
19
+ for (const [key, value] of Object.entries(attrs))
20
+ node.setAttribute(key, String(value));
21
+ return node;
22
+ }
23
+
24
+ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
25
+ const nodes = dag.nodes ?? [];
26
+ const edges = dag.edges ?? [];
27
+ const layout = layoutDag(nodes, edges, dag.ranks ?? []);
28
+ const wrap = existingWrap ?? el("div", "dag-graph-wrap");
29
+ // Preserve .dag-graph-viewport DOM identity across poll refreshes so the
30
+ // user's active scroll/pointer target is not destroyed mid-gesture.
31
+ let viewport =
32
+ existingWrap?.querySelector?.(":scope > .dag-graph-viewport") ??
33
+ existingWrap?.querySelector?.(".dag-graph-viewport") ??
34
+ null;
35
+ if (existingWrap) {
36
+ // Remove only non-interactive siblings. Keeping the viewport connected is
37
+ // important: detaching and re-inserting the same node still cancels a native
38
+ // scrollbar drag/pointer target even though object identity is unchanged.
39
+ for (const child of [...wrap.children]) {
40
+ if (child !== viewport) wrap.removeChild(child);
41
+ }
42
+ if (viewport) {
43
+ while (viewport.firstChild) viewport.removeChild(viewport.firstChild);
44
+ }
45
+ }
46
+ const toolbar = el("div", "dag-graph-toolbar");
47
+ let summaryText = `${nodes.length} 节点 · ${layout.edges.length} 条依赖`;
48
+ const convergence = dag.convergence;
49
+ if (convergence?.enabled) {
50
+ summaryText += ` · pass ${convergence.currentPass}/${convergence.maxPasses}`;
51
+ if (convergence.terminalReason) {
52
+ summaryText += ` · ${convergence.terminalReason}`;
53
+ }
54
+ }
55
+ toolbar.appendChild(el("span", "dag-graph-summary", summaryText));
56
+ const reset = el("button", "dag-graph-reset", "重置视图");
57
+ toolbar.appendChild(reset);
58
+ const appendBeforeViewport = (child) => {
59
+ if (viewport?.parentNode === wrap) wrap.insertBefore(child, viewport);
60
+ else wrap.appendChild(child);
61
+ };
62
+ appendBeforeViewport(toolbar);
63
+ if (edges.length === 0) {
64
+ appendBeforeViewport(
65
+ el(
66
+ "p",
67
+ "dag-graph-fallback",
68
+ "该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。",
69
+ ),
70
+ );
71
+ }
72
+ if (!viewport) {
73
+ viewport = el("div", "dag-graph-viewport");
74
+ }
75
+ viewport.dataset.dagRunId = dagRunId;
76
+ const svg = svgEl("svg", {
77
+ class: "dag-graph",
78
+ viewBox: `0 0 ${layout.width} ${layout.height}`,
79
+ width: layout.width,
80
+ height: layout.height,
81
+ role: "img",
82
+ "aria-label": `DAG 依赖图,共 ${nodes.length} 个节点`,
83
+ });
84
+ const defs = svgEl("defs");
85
+ const marker = svgEl("marker", {
86
+ id: "dag-arrow",
87
+ viewBox: "0 0 10 10",
88
+ refX: 9,
89
+ refY: 5,
90
+ markerWidth: 7,
91
+ markerHeight: 7,
92
+ orient: "auto-start-reverse",
93
+ });
94
+ marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
95
+ defs.appendChild(marker);
96
+ const controlMarker = svgEl("marker", {
97
+ id: "dag-arrow-control",
98
+ viewBox: "0 0 10 10",
99
+ refX: 9,
100
+ refY: 5,
101
+ markerWidth: 7,
102
+ markerHeight: 7,
103
+ orient: "auto-start-reverse",
104
+ });
105
+ controlMarker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
106
+ defs.appendChild(controlMarker);
107
+ svg.appendChild(defs);
108
+ const related = new Set([uiState.selectedDagNodeId]);
109
+ for (const edge of layout.edges)
110
+ if (
111
+ edge.from === uiState.selectedDagNodeId ||
112
+ edge.to === uiState.selectedDagNodeId
113
+ ) {
114
+ related.add(edge.from);
115
+ related.add(edge.to);
116
+ }
117
+ for (const edge of layout.edges) {
118
+ const path = svgEl("path", {
119
+ d: edge.path,
120
+ class: `dag-edge${uiState.selectedDagNodeId && (!related.has(edge.from) || !related.has(edge.to)) ? " dag-dimmed" : ""}`,
121
+ "marker-end": "url(#dag-arrow)",
122
+ });
123
+ path.appendChild(svgEl("title"));
124
+ path.firstChild.textContent = `${edge.from} → ${edge.to}`;
125
+ svg.appendChild(path);
126
+ }
127
+ // 控制回环:仅由 read model 派生(dag.controlEdges),不进 layout、不参与拓扑。
128
+ // 路径从 review-gate-shell 左侧绕回 repair-pi 右侧,避开节点矩形。
129
+ const layoutByNode = new Map(layout.nodes.map((n) => [n.nodeId, n]));
130
+ for (const ctrl of dag.controlEdges ?? []) {
131
+ const from = layoutByNode.get(ctrl.from);
132
+ const to = layoutByNode.get(ctrl.to);
133
+ if (!from || !to) continue;
134
+ const x1 = from.x;
135
+ const y1 = from.y + from.height / 2;
136
+ const x2 = to.x + to.width;
137
+ const y2 = to.y + to.height / 2;
138
+ const lift = Math.max(48, (from.x - to.x) / 2);
139
+ const rise = Math.max(40, Math.abs(from.y - to.y) / 2 + 16);
140
+ const path = svgEl("path", {
141
+ d: `M ${x1} ${y1} C ${x1 - lift} ${y1 - rise}, ${x2 + lift} ${y2 - rise}, ${x2} ${y2}`,
142
+ class: "dag-edge dag-edge-control",
143
+ "marker-end": "url(#dag-arrow-control)",
144
+ });
145
+ path.appendChild(svgEl("title"));
146
+ path.firstChild.textContent = `${ctrl.from} → ${ctrl.to} (control loop)`;
147
+ svg.appendChild(path);
148
+ }
149
+ const byId = Object.fromEntries(nodes.map((node) => [node.nodeId, node]));
150
+ for (const position of layout.nodes) {
151
+ const node = byId[position.nodeId] ?? { nodeId: position.nodeId };
152
+ const status = (node.status ?? "unknown").toLowerCase();
153
+ const group = svgEl("g", {
154
+ transform: `translate(${position.x} ${position.y})`,
155
+ class: `dag-graph-node status-${status}${node.nodeId === uiState.selectedDagNodeId ? " selected" : ""}${uiState.selectedDagNodeId && !related.has(node.nodeId) ? " dag-dimmed" : ""}`,
156
+ tabindex: 0,
157
+ role: "button",
158
+ "aria-label": `${node.nodeId},状态 ${status}`,
159
+ });
160
+ group.appendChild(
161
+ svgEl("rect", { width: position.width, height: position.height, rx: 10 }),
162
+ );
163
+ const title = svgEl("title");
164
+ title.textContent = `${node.nodeId}\n${node.executor ?? "未知执行器"}\n${status}`;
165
+ group.appendChild(title);
166
+ const label = svgEl("text", { x: 14, y: 26, class: "dag-node-label" });
167
+ label.textContent = truncateText(node.label ?? node.nodeId, 26);
168
+ group.appendChild(label);
169
+ const executor = svgEl("text", { x: 14, y: 50, class: "dag-node-meta" });
170
+ executor.textContent = `◈ ${node.executor ?? "unknown"}`;
171
+ group.appendChild(executor);
172
+ const state = svgEl("text", { x: 14, y: 74, class: "dag-node-status" });
173
+ let stateText = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`;
174
+ if (node.passes?.length) {
175
+ stateText += ` · P${node.passes.join(",")}`;
176
+ }
177
+ if (node.attemptCount > 0) {
178
+ stateText += ` · attempt ${node.attemptCount}`;
179
+ }
180
+ state.textContent = stateText;
181
+ group.appendChild(state);
182
+ const activate = () => selectDagNode(dagRunId, node.nodeId);
183
+ group.addEventListener("click", activate);
184
+ group.addEventListener("keydown", (event) => {
185
+ if (event.key === "Enter" || event.key === " ") {
186
+ event.preventDefault();
187
+ activate();
188
+ }
189
+ });
190
+ svg.appendChild(group);
191
+ }
192
+ viewport.appendChild(svg);
193
+ if (viewport.parentNode !== wrap) wrap.appendChild(viewport);
194
+ const graphViewportState =
195
+ dagGraphViewportStates.get(dagRunId) ??
196
+ (uiState.dagGraphViewportState?.dagRunId === dagRunId
197
+ ? uiState.dagGraphViewportState
198
+ : null);
199
+ // Bind scroll capture once per viewport instance so reuse does not stack listeners.
200
+ if (viewport.dataset.scrollBound !== "1") {
201
+ viewport.dataset.scrollBound = "1";
202
+ viewport.addEventListener(
203
+ "scroll",
204
+ () => {
205
+ const id = viewport.dataset.dagRunId;
206
+ if (!id) return;
207
+ uiState.dagGraphViewportState = updateDagGraphViewportState(
208
+ uiState.dagGraphViewportState,
209
+ id,
210
+ viewport.scrollLeft,
211
+ viewport.scrollTop,
212
+ );
213
+ dagGraphViewportStates.set(id, uiState.dagGraphViewportState);
214
+ },
215
+ { passive: true },
216
+ );
217
+ }
218
+ if (graphViewportState) {
219
+ const restoreViewport = () => {
220
+ if (viewport.dataset.dagRunId !== dagRunId) return;
221
+ viewport.scrollLeft = graphViewportState.scrollLeft;
222
+ viewport.scrollTop = graphViewportState.scrollTop;
223
+ };
224
+ // Apply immediately so identity reuse keeps position even before paint,
225
+ // then again on rAF after SVG layout settles.
226
+ restoreViewport();
227
+ if (typeof requestAnimationFrame === "function") {
228
+ requestAnimationFrame(restoreViewport);
229
+ }
230
+ }
231
+ // Toolbar is rebuilt each paint; bind reset to the current viewport/dagRunId.
232
+ reset.addEventListener("click", () => {
233
+ uiState.dagGraphViewportState = updateDagGraphViewportState(
234
+ uiState.dagGraphViewportState,
235
+ dagRunId,
236
+ 0,
237
+ 0,
238
+ );
239
+ dagGraphViewportStates.set(dagRunId, uiState.dagGraphViewportState);
240
+ if (typeof viewport.scrollTo === "function") {
241
+ viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
242
+ } else {
243
+ viewport.scrollLeft = 0;
244
+ viewport.scrollTop = 0;
245
+ }
246
+ });
247
+ return wrap;
248
+ }