dev-loops 0.1.0
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.
- package/.pi/dev-loop/defaults.yaml +477 -0
- package/AGENTS.md +25 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/agents/dev-loop.agent.md +82 -0
- package/agents/developer.agent.md +37 -0
- package/agents/docs.agent.md +33 -0
- package/agents/fixer.agent.md +53 -0
- package/agents/quality.agent.md +28 -0
- package/agents/refiner.agent.md +87 -0
- package/agents/review.agent.md +64 -0
- package/cli/index.mjs +424 -0
- package/extension/README.md +233 -0
- package/extension/checks.ts +94 -0
- package/extension/index.ts +131 -0
- package/extension/post-merge-update.ts +512 -0
- package/extension/presentation.ts +107 -0
- package/lib/dev-loops-core.mjs +284 -0
- package/package.json +103 -0
- package/scripts/README.md +1007 -0
- package/scripts/_cli-primitives.mjs +10 -0
- package/scripts/_core-helpers.mjs +30 -0
- package/scripts/docs/validate-links.mjs +567 -0
- package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
- package/scripts/github/_review-thread-mutations.mjs +214 -0
- package/scripts/github/capture-review-threads.mjs +180 -0
- package/scripts/github/create-draft-pr.mjs +108 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
- package/scripts/github/detect-linked-issue-pr.mjs +331 -0
- package/scripts/github/manage-sub-issues.mjs +394 -0
- package/scripts/github/probe-copilot-review.mjs +323 -0
- package/scripts/github/ready-for-review.mjs +93 -0
- package/scripts/github/reconcile-draft-gate.mjs +328 -0
- package/scripts/github/reply-resolve-review-thread.mjs +42 -0
- package/scripts/github/reply-resolve-review-threads.mjs +329 -0
- package/scripts/github/request-copilot-review.mjs +551 -0
- package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
- package/scripts/github/stage-reviewer-draft.mjs +191 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
- package/scripts/github/verify-fresh-review-context.mjs +125 -0
- package/scripts/github/write-gate-findings-log.mjs +212 -0
- package/scripts/loop/_checkpoint-io.mjs +55 -0
- package/scripts/loop/_checkpoint-paths.mjs +28 -0
- package/scripts/loop/_handoff-contract.mjs +230 -0
- package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
- package/scripts/loop/_loop-evidence.mjs +32 -0
- package/scripts/loop/_pr-runner-coordination.mjs +611 -0
- package/scripts/loop/_stale-runner-detection.mjs +145 -0
- package/scripts/loop/_steering-state-file.mjs +134 -0
- package/scripts/loop/build-handoff-envelope.mjs +181 -0
- package/scripts/loop/checkpoint-contract.mjs +49 -0
- package/scripts/loop/conductor-monitor.mjs +1850 -0
- package/scripts/loop/conductor.mjs +214 -0
- package/scripts/loop/copilot-pr-handoff.mjs +493 -0
- package/scripts/loop/debt-remediate.mjs +304 -0
- package/scripts/loop/detect-change-scope.mjs +102 -0
- package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
- package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
- package/scripts/loop/detect-internal-only-pr.mjs +270 -0
- package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
- package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
- package/scripts/loop/detect-stale-runner.mjs +250 -0
- package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
- package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
- package/scripts/loop/info.mjs +267 -0
- package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
- package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
- package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
- package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
- package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
- package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
- package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
- package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
- package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
- package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
- package/scripts/loop/inspect-run-viewer.mjs +82 -0
- package/scripts/loop/inspect-run.mjs +382 -0
- package/scripts/loop/outer-loop.mjs +419 -0
- package/scripts/loop/pr-runner-coordination.mjs +143 -0
- package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
- package/scripts/loop/pre-flight-gate.mjs +236 -0
- package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
- package/scripts/loop/pre-push-main-guard.mjs +103 -0
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
- package/scripts/loop/print-gates.mjs +42 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
- package/scripts/loop/run-conductor-cycle.mjs +322 -0
- package/scripts/loop/run-queue.mjs +124 -0
- package/scripts/loop/run-refinement-audit.mjs +513 -0
- package/scripts/loop/run-watch-cycle.mjs +358 -0
- package/scripts/loop/steer-loop.mjs +841 -0
- package/scripts/loop/ui-designer-review-contract.mjs +76 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
- package/scripts/projects/add-queue-item.mjs +528 -0
- package/scripts/projects/ensure-queue-board.mjs +837 -0
- package/scripts/projects/list-queue-items.mjs +489 -0
- package/scripts/projects/move-queue-item.mjs +549 -0
- package/scripts/projects/reorder-queue-item.mjs +518 -0
- package/scripts/refine/_refine-helpers.mjs +258 -0
- package/scripts/refine/prose-linkage-detector.mjs +92 -0
- package/scripts/refine/refinement-completeness-checker.mjs +88 -0
- package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
- package/scripts/refine/tree-integrity-validator.mjs +211 -0
- package/scripts/refine/verify.mjs +178 -0
- package/scripts/repo-wiki-local.mjs +156 -0
- package/scripts/repo-wiki.mjs +119 -0
- package/skills/copilot-pr-followup/SKILL.md +380 -0
- package/skills/dev-loop/SKILL.md +141 -0
- package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
- package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
- package/skills/dev-loop/scripts/init-phase.mjs +71 -0
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
- package/skills/dev-loop/scripts/phase-files.mjs +29 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
- package/skills/dev-loop/scripts/render-template.mjs +82 -0
- package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
- package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
- package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
- package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
- package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
- package/skills/dev-loop/templates/dev-mode-review.md +17 -0
- package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
- package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
- package/skills/dev-loop/templates/phase-doc.md +27 -0
- package/skills/dev-loop/templates/phase-summary.md +13 -0
- package/skills/dev-loop/templates/phase-variant.md +15 -0
- package/skills/dev-loop/templates/retrospective.md +11 -0
- package/skills/dev-loop/templates/review.md +32 -0
- package/skills/dev-loop/templates/ui-vision-review.md +55 -0
- package/skills/docs/acceptance-criteria-verification.md +21 -0
- package/skills/docs/anti-patterns.md +21 -0
- package/skills/docs/artifact-authority-contract.md +119 -0
- package/skills/docs/confirmation-rules.md +28 -0
- package/skills/docs/copilot-ci-status-contract.md +52 -0
- package/skills/docs/copilot-loop-operations.md +233 -0
- package/skills/docs/debt-remediation-contract.md +107 -0
- package/skills/docs/entrypoint-strategies.md +115 -0
- package/skills/docs/epic-tree-refinement-procedure.md +234 -0
- package/skills/docs/issue-intake-procedure.md +235 -0
- package/skills/docs/main-agent-contract.md +72 -0
- package/skills/docs/merge-preconditions.md +29 -0
- package/skills/docs/pr-lifecycle-contract.md +209 -0
- package/skills/docs/public-dev-loop-contract.md +497 -0
- package/skills/docs/retrospective-checkpoint-contract.md +159 -0
- package/skills/docs/stop-conditions.md +29 -0
- package/skills/docs/structural-quality.md +42 -0
- package/skills/docs/tracker-first-loop-state.md +281 -0
- package/skills/docs/validation-policy.md +27 -0
- package/skills/docs/workflow-handoff-contract.md +135 -0
- package/skills/final-approval/SKILL.md +19 -0
- package/skills/local-implementation/SKILL.md +640 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MERMAID_BROWSER_ASSET_PATH,
|
|
5
|
+
MERMAID_BROWSER_ASSET_ROUTE,
|
|
6
|
+
} from "./constants.mjs";
|
|
7
|
+
import {
|
|
8
|
+
STATE as COPILOT_STATE,
|
|
9
|
+
TRANSITIONS as COPILOT_TRANSITIONS,
|
|
10
|
+
} from "@dev-loops/core/loop/copilot-loop-state";
|
|
11
|
+
import {
|
|
12
|
+
OUTER_GRAPH,
|
|
13
|
+
OUTER_STATE,
|
|
14
|
+
OUTER_TERMINAL_STATES,
|
|
15
|
+
OUTER_TRANSITIONS,
|
|
16
|
+
} from "@dev-loops/core/loop/conductor-routing";
|
|
17
|
+
import {
|
|
18
|
+
REVIEWER_STATE,
|
|
19
|
+
REVIEWER_TRANSITIONS,
|
|
20
|
+
} from "@dev-loops/core/loop/reviewer-loop-state";
|
|
21
|
+
import {
|
|
22
|
+
LIFECYCLE_GRAPH,
|
|
23
|
+
LIFECYCLE_STATE,
|
|
24
|
+
LIFECYCLE_TERMINAL_STATES,
|
|
25
|
+
LIFECYCLE_TRANSITIONS,
|
|
26
|
+
} from "@dev-loops/core/loop/lifecycle-state";
|
|
27
|
+
import {
|
|
28
|
+
escapeHtml,
|
|
29
|
+
formatStateToken,
|
|
30
|
+
renderSnapshotStateLabel,
|
|
31
|
+
} from "./shared.mjs";
|
|
32
|
+
|
|
33
|
+
let mermaidBrowserScriptPromise = null;
|
|
34
|
+
|
|
35
|
+
function normalizeTransitions(transitions) {
|
|
36
|
+
if (!Array.isArray(transitions)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const normalizedTransitions = [];
|
|
41
|
+
const seenTransitions = new Set();
|
|
42
|
+
|
|
43
|
+
for (const transition of transitions) {
|
|
44
|
+
if (typeof transition !== "string") {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const normalizedTransition = transition.trim();
|
|
49
|
+
if (normalizedTransition.length === 0 || seenTransitions.has(normalizedTransition)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
seenTransitions.add(normalizedTransition);
|
|
54
|
+
normalizedTransitions.push(normalizedTransition);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return normalizedTransitions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const COPILOT_TERMINAL_STATES = new Set(
|
|
61
|
+
Object.entries(COPILOT_TRANSITIONS)
|
|
62
|
+
.filter(([, nextStates]) => Array.isArray(nextStates) && nextStates.length === 0)
|
|
63
|
+
.map(([state]) => state),
|
|
64
|
+
);
|
|
65
|
+
const OUTER_TERMINAL_STATE_SET = new Set(OUTER_TERMINAL_STATES);
|
|
66
|
+
const REVIEWER_TERMINAL_STATES = new Set(
|
|
67
|
+
Object.entries(REVIEWER_TRANSITIONS)
|
|
68
|
+
.filter(([, nextStates]) => Array.isArray(nextStates) && nextStates.length === 0)
|
|
69
|
+
.map(([state]) => state),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const LIFECYCLE_TERMINAL_STATE_SET = new Set(LIFECYCLE_TERMINAL_STATES);
|
|
73
|
+
|
|
74
|
+
function normalizeCurrentStateInfo(currentState, { knownStates = null, terminalStates = null } = {}) {
|
|
75
|
+
if (typeof currentState === "string" && currentState.length > 0) {
|
|
76
|
+
const normalized = currentState.trim();
|
|
77
|
+
|
|
78
|
+
if (normalized.toLowerCase() === "unknown") {
|
|
79
|
+
return { label: "current state unavailable", available: false, terminal: false };
|
|
80
|
+
}
|
|
81
|
+
if (knownStates instanceof Set && !knownStates.has(normalized)) {
|
|
82
|
+
return { label: "current state unavailable", available: false, terminal: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
label: normalized,
|
|
87
|
+
available: true,
|
|
88
|
+
terminal: terminalStates instanceof Set ? terminalStates.has(normalized) : false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { label: "current state unavailable", available: false, terminal: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function summarizeTransitionAvailability(transitions) {
|
|
96
|
+
const normalizedTransitions = normalizeTransitions(transitions);
|
|
97
|
+
const unavailable = normalizedTransitions === null;
|
|
98
|
+
const empty = !unavailable && normalizedTransitions.length === 0;
|
|
99
|
+
const summary = unavailable
|
|
100
|
+
? "transition data unavailable in this snapshot"
|
|
101
|
+
: empty
|
|
102
|
+
? "no allowed transitions"
|
|
103
|
+
: normalizedTransitions.join(", ");
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
unavailable,
|
|
107
|
+
empty,
|
|
108
|
+
summary,
|
|
109
|
+
normalizedTransitions: unavailable ? [] : normalizedTransitions,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function loadMermaidBrowserScript({ readFileImpl = readFile } = {}) {
|
|
114
|
+
if (mermaidBrowserScriptPromise === null) {
|
|
115
|
+
mermaidBrowserScriptPromise = Promise.resolve()
|
|
116
|
+
.then(() => readFileImpl(MERMAID_BROWSER_ASSET_PATH, "utf8"))
|
|
117
|
+
.catch((error) => {
|
|
118
|
+
mermaidBrowserScriptPromise = null;
|
|
119
|
+
throw error;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return mermaidBrowserScriptPromise;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resetMermaidBrowserScriptCache() {
|
|
126
|
+
mermaidBrowserScriptPromise = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function escapeMermaidLabel(value) {
|
|
130
|
+
return String(value)
|
|
131
|
+
.replaceAll("\\", "\\\\")
|
|
132
|
+
.replaceAll('"', '\\"');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderMermaidNode(id, label, shape = "box") {
|
|
136
|
+
const escapedLabel = escapeMermaidLabel(label);
|
|
137
|
+
|
|
138
|
+
if (shape === "pill") {
|
|
139
|
+
return `${id}(["${escapedLabel}"])`;
|
|
140
|
+
}
|
|
141
|
+
if (shape === "circle") {
|
|
142
|
+
return `${id}(("${escapedLabel}"))`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return `${id}["${escapedLabel}"]`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderMermaidNodeId(laneKey, state) {
|
|
149
|
+
return `${laneKey}_${String(state).replaceAll(/[^a-zA-Z0-9_]+/g, "_")}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function humanizeGraphStateLabel(state) {
|
|
153
|
+
return String(state).replaceAll("_", " ");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildFullStateMachineLane({ laneKey, title, states, transitionTable, currentState, transitions, startStates = [], startLabel = "Start", endLabel = "End", terminalStates = null, displayLabelForState = (state) => state, suppressSaturatedNextHighlights = false }) {
|
|
157
|
+
const knownStates = new Set(states);
|
|
158
|
+
const resolvedTerminalStates = terminalStates instanceof Set
|
|
159
|
+
? terminalStates
|
|
160
|
+
: new Set(states.filter((state) => Array.isArray(transitionTable[state]) && transitionTable[state].length === 0));
|
|
161
|
+
const currentInfo = normalizeCurrentStateInfo(currentState, { knownStates, terminalStates: resolvedTerminalStates });
|
|
162
|
+
const transitionInfo = summarizeTransitionAvailability(transitions);
|
|
163
|
+
const authoritativeCurrentNextStates = currentInfo.available
|
|
164
|
+
? new Set(Array.isArray(transitionTable[currentInfo.label]) ? transitionTable[currentInfo.label] : [])
|
|
165
|
+
: new Set();
|
|
166
|
+
const highlightedNextStates = new Set(
|
|
167
|
+
transitionInfo.unavailable || !currentInfo.available
|
|
168
|
+
? []
|
|
169
|
+
: transitionInfo.normalizedTransitions.filter((state) => authoritativeCurrentNextStates.has(state)),
|
|
170
|
+
);
|
|
171
|
+
const broadNextSet = suppressSaturatedNextHighlights
|
|
172
|
+
&& currentInfo.available
|
|
173
|
+
&& highlightedNextStates.size === knownStates.size;
|
|
174
|
+
const effectiveHighlightedNextStates = broadNextSet ? new Set() : highlightedNextStates;
|
|
175
|
+
const classIds = {
|
|
176
|
+
cue: [],
|
|
177
|
+
current: [],
|
|
178
|
+
currentTerminal: [],
|
|
179
|
+
next: [],
|
|
180
|
+
nextTerminal: [],
|
|
181
|
+
terminal: [],
|
|
182
|
+
inactive: [],
|
|
183
|
+
unavailable: [],
|
|
184
|
+
note: [],
|
|
185
|
+
};
|
|
186
|
+
const lines = [
|
|
187
|
+
` subgraph ${laneKey}["${escapeMermaidLabel(title)}"]`,
|
|
188
|
+
" direction LR",
|
|
189
|
+
];
|
|
190
|
+
const startId = `${laneKey}_start`;
|
|
191
|
+
const endId = `${laneKey}_end`;
|
|
192
|
+
|
|
193
|
+
if (startStates.length > 0) {
|
|
194
|
+
lines.push(` ${renderMermaidNode(startId, startLabel, "pill")}`);
|
|
195
|
+
classIds.cue.push(startId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const state of states) {
|
|
199
|
+
const nodeId = renderMermaidNodeId(laneKey, state);
|
|
200
|
+
const terminal = resolvedTerminalStates.has(state);
|
|
201
|
+
lines.push(` ${renderMermaidNode(nodeId, displayLabelForState(state))}`);
|
|
202
|
+
|
|
203
|
+
if (currentInfo.available && currentInfo.label === state) {
|
|
204
|
+
classIds[terminal ? "currentTerminal" : "current"].push(nodeId);
|
|
205
|
+
} else if (effectiveHighlightedNextStates.has(state)) {
|
|
206
|
+
classIds[terminal ? "nextTerminal" : "next"].push(nodeId);
|
|
207
|
+
} else if (terminal) {
|
|
208
|
+
classIds.terminal.push(nodeId);
|
|
209
|
+
} else {
|
|
210
|
+
classIds.inactive.push(nodeId);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let endVisible = false;
|
|
215
|
+
const ensureEndNode = () => {
|
|
216
|
+
if (!endVisible) {
|
|
217
|
+
lines.push(` ${renderMermaidNode(endId, endLabel, "circle")}`);
|
|
218
|
+
classIds.cue.push(endId);
|
|
219
|
+
endVisible = true;
|
|
220
|
+
}
|
|
221
|
+
return endId;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
for (const startState of startStates) {
|
|
225
|
+
if (knownStates.has(startState)) {
|
|
226
|
+
lines.push(` ${startId} --> ${renderMermaidNodeId(laneKey, startState)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const state of states) {
|
|
231
|
+
const fromId = renderMermaidNodeId(laneKey, state);
|
|
232
|
+
const nextStates = Array.isArray(transitionTable[state]) ? transitionTable[state] : [];
|
|
233
|
+
|
|
234
|
+
if (nextStates.length === 0) {
|
|
235
|
+
lines.push(` ${fromId} --> ${ensureEndNode()}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const nextState of nextStates) {
|
|
240
|
+
if (knownStates.has(nextState)) {
|
|
241
|
+
lines.push(` ${fromId} --> ${renderMermaidNodeId(laneKey, nextState)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let currentId = startStates.length > 0 ? startId : renderMermaidNodeId(laneKey, states[0]);
|
|
247
|
+
if (!currentInfo.available) {
|
|
248
|
+
const unavailableId = `${laneKey}_current_unavailable`;
|
|
249
|
+
lines.push(` ${renderMermaidNode(unavailableId, currentInfo.label)}`);
|
|
250
|
+
classIds.unavailable.push(unavailableId);
|
|
251
|
+
currentId = unavailableId;
|
|
252
|
+
} else {
|
|
253
|
+
currentId = renderMermaidNodeId(laneKey, currentInfo.label);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (transitionInfo.unavailable) {
|
|
257
|
+
const noteId = `${laneKey}_transitions_unavailable`;
|
|
258
|
+
lines.push(` ${renderMermaidNode(noteId, "snapshot next transitions unavailable")}`);
|
|
259
|
+
lines.push(` ${currentId} -.-> ${noteId}`);
|
|
260
|
+
classIds.note.push(noteId);
|
|
261
|
+
} else if (broadNextSet) {
|
|
262
|
+
const noteId = `${laneKey}_broad_next_set`;
|
|
263
|
+
lines.push(` ${renderMermaidNode(noteId, "next evaluation may resolve to any shown state")}`);
|
|
264
|
+
lines.push(` ${currentId} -.-> ${noteId}`);
|
|
265
|
+
classIds.note.push(noteId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
lines.push(" end");
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
title,
|
|
272
|
+
currentLabel: currentInfo.label,
|
|
273
|
+
transitionInfo,
|
|
274
|
+
currentId,
|
|
275
|
+
lines,
|
|
276
|
+
classIds,
|
|
277
|
+
summary: `${currentInfo.label}; full authoritative state machine shown`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function buildInspectionMermaidGraph(snapshot) {
|
|
282
|
+
if (snapshot === null || snapshot === undefined || renderSnapshotStateLabel(snapshot) === "unavailable") {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const lanes = [
|
|
287
|
+
buildFullStateMachineLane({
|
|
288
|
+
laneKey: "outer_loop_family",
|
|
289
|
+
title: "outer-loop family",
|
|
290
|
+
states: Object.values(OUTER_STATE),
|
|
291
|
+
transitionTable: OUTER_TRANSITIONS,
|
|
292
|
+
currentState: snapshot.outerState,
|
|
293
|
+
transitions: snapshot.allowedTransitions,
|
|
294
|
+
startStates: OUTER_GRAPH.entryStates,
|
|
295
|
+
startLabel: OUTER_GRAPH.start.label,
|
|
296
|
+
endLabel: OUTER_GRAPH.end.label,
|
|
297
|
+
terminalStates: OUTER_TERMINAL_STATE_SET,
|
|
298
|
+
displayLabelForState: humanizeGraphStateLabel,
|
|
299
|
+
suppressSaturatedNextHighlights: true,
|
|
300
|
+
}),
|
|
301
|
+
buildFullStateMachineLane({
|
|
302
|
+
laneKey: "copilot_layer",
|
|
303
|
+
title: "copilot layer",
|
|
304
|
+
states: Object.values(COPILOT_STATE),
|
|
305
|
+
transitionTable: COPILOT_TRANSITIONS,
|
|
306
|
+
currentState: snapshot.layers?.copilot?.currentState,
|
|
307
|
+
transitions: snapshot.layers?.copilot?.allowedTransitions,
|
|
308
|
+
startStates: [COPILOT_STATE.PR_DRAFT],
|
|
309
|
+
terminalStates: COPILOT_TERMINAL_STATES,
|
|
310
|
+
}),
|
|
311
|
+
buildFullStateMachineLane({
|
|
312
|
+
laneKey: "reviewer_layer",
|
|
313
|
+
title: "reviewer layer",
|
|
314
|
+
states: Object.values(REVIEWER_STATE),
|
|
315
|
+
transitionTable: REVIEWER_TRANSITIONS,
|
|
316
|
+
currentState: snapshot.layers?.reviewer?.currentState,
|
|
317
|
+
transitions: snapshot.layers?.reviewer?.allowedTransitions,
|
|
318
|
+
startStates: [REVIEWER_STATE.WAITING_FOR_REVIEW_REQUEST],
|
|
319
|
+
terminalStates: REVIEWER_TERMINAL_STATES,
|
|
320
|
+
}),
|
|
321
|
+
buildFullStateMachineLane({
|
|
322
|
+
laneKey: "lifecycle_layer",
|
|
323
|
+
title: "lifecycle",
|
|
324
|
+
states: Object.values(LIFECYCLE_STATE),
|
|
325
|
+
transitionTable: LIFECYCLE_TRANSITIONS,
|
|
326
|
+
currentState: snapshot.lifecyclePhase,
|
|
327
|
+
transitions: snapshot.lifecycleAllowedTransitions,
|
|
328
|
+
startStates: LIFECYCLE_GRAPH.entryStates,
|
|
329
|
+
startLabel: LIFECYCLE_GRAPH.start.label,
|
|
330
|
+
endLabel: LIFECYCLE_GRAPH.end.label,
|
|
331
|
+
terminalStates: LIFECYCLE_TERMINAL_STATE_SET,
|
|
332
|
+
displayLabelForState: humanizeGraphStateLabel,
|
|
333
|
+
}),
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const classIds = {
|
|
337
|
+
cue: [],
|
|
338
|
+
current: [],
|
|
339
|
+
currentTerminal: [],
|
|
340
|
+
next: [],
|
|
341
|
+
nextTerminal: [],
|
|
342
|
+
terminal: [],
|
|
343
|
+
inactive: [],
|
|
344
|
+
unavailable: [],
|
|
345
|
+
note: [],
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
for (const lane of lanes) {
|
|
349
|
+
for (const [className, ids] of Object.entries(lane.classIds)) {
|
|
350
|
+
classIds[className].push(...ids);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const orderedLaneLines = [
|
|
355
|
+
' subgraph lane_stack[" "]',
|
|
356
|
+
' direction TB',
|
|
357
|
+
...lanes.flatMap((lane) => lane.lines.map((line) => ` ${line.trimStart()}`)),
|
|
358
|
+
' end',
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
const lines = [
|
|
362
|
+
"flowchart TB",
|
|
363
|
+
" classDef cue fill:#f5f7f9,stroke:#78909c,stroke-width:1.5px,color:#355061,font-weight:bold;",
|
|
364
|
+
" classDef current fill:#e3f2fd,stroke:#1565c0,stroke-width:4px,color:#12344d,font-weight:bold;",
|
|
365
|
+
" classDef currentTerminal fill:#d9f2df,stroke:#1565c0,stroke-width:4px,color:#12344d,font-weight:800;",
|
|
366
|
+
" classDef next fill:#f3f4ff,stroke:#5c6bc0,stroke-width:2px,color:#233242,font-weight:700;",
|
|
367
|
+
" classDef nextTerminal fill:#eef7ef,stroke:#5c6bc0,stroke-width:3px,color:#1b5e20,font-weight:700;",
|
|
368
|
+
" classDef terminal fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px,color:#1b5e20,font-weight:bold;",
|
|
369
|
+
" classDef inactive fill:#ffffff,stroke:#b0bec5,stroke-width:1.5px,color:#607d8b;",
|
|
370
|
+
" classDef unavailable fill:#f8fafc,stroke:#90a4ae,stroke-width:2px,color:#546e7a,stroke-dasharray: 6 4;",
|
|
371
|
+
" classDef note fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#7f4b00;",
|
|
372
|
+
...orderedLaneLines,
|
|
373
|
+
' outer_loop_family_start ~~~ copilot_layer_start',
|
|
374
|
+
' copilot_layer_start ~~~ reviewer_layer_start',
|
|
375
|
+
' reviewer_layer_start ~~~ lifecycle_layer_start',
|
|
376
|
+
' outer_loop_family_start ~~~ lifecycle_layer_start',
|
|
377
|
+
` ${lanes[0].currentId} -. "layer view" .-> ${lanes[1].currentId}`,
|
|
378
|
+
` ${lanes[1].currentId} -. "layer view" .-> ${lanes[2].currentId}`,
|
|
379
|
+
` ${lanes[2].currentId} -. "layer view" .-> ${lanes[3].currentId}`,
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
for (const [className, ids] of Object.entries(classIds)) {
|
|
383
|
+
if (ids.length > 0) {
|
|
384
|
+
lines.push(` class ${ids.join(",")} ${className};`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const outerState = formatStateToken(snapshot.outerState, "unknown");
|
|
389
|
+
const outerAction = formatStateToken(snapshot.outerAction, "unknown");
|
|
390
|
+
let focusIds = lanes.map((lane) => lane.currentId);
|
|
391
|
+
const lifecycleAvailable = snapshot.lifecyclePhase != null
|
|
392
|
+
&& snapshot.lifecyclePhase !== "unknown"
|
|
393
|
+
&& !String(snapshot.lifecyclePhase).toLowerCase().includes("unavailable");
|
|
394
|
+
if (lifecycleAvailable) {
|
|
395
|
+
focusIds = [lanes[3].currentId];
|
|
396
|
+
} else if (outerState === OUTER_STATE.HANDOFF_TO_COPILOT_LOOP || outerAction === "reenter_copilot_loop") {
|
|
397
|
+
focusIds = [lanes[1].currentId];
|
|
398
|
+
} else if (outerState === OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP || outerAction === "reenter_reviewer_loop") {
|
|
399
|
+
focusIds = [lanes[2].currentId];
|
|
400
|
+
} else {
|
|
401
|
+
const copilotFocusId = lanes[1].currentId;
|
|
402
|
+
const copilotAvailable = !copilotFocusId.includes("unavailable");
|
|
403
|
+
focusIds = copilotAvailable ? [copilotFocusId] : lanes.map((lane) => lane.currentId);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
definition: lines.join("\n"),
|
|
408
|
+
focusIds,
|
|
409
|
+
lanes: lanes.map((lane) => ({
|
|
410
|
+
title: lane.title,
|
|
411
|
+
currentLabel: lane.currentLabel,
|
|
412
|
+
transitionInfo: lane.transitionInfo,
|
|
413
|
+
summary: lane.summary,
|
|
414
|
+
})),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderStateGraphLegend() {
|
|
419
|
+
return `<div class="state-graph-cues" aria-label="State graph cues">
|
|
420
|
+
<span class="state-graph-cue"><span class="state-graph-cue-chip state-graph-cue-chip-start">Start</span> lane entry</span>
|
|
421
|
+
<span class="state-graph-cue"><span class="state-graph-cue-chip state-graph-cue-chip-current">Current</span> snapshot-derived current state</span>
|
|
422
|
+
<span class="state-graph-cue"><span class="state-graph-cue-chip state-graph-cue-chip-next">Next</span> immediate allowed next state</span>
|
|
423
|
+
<span class="state-graph-cue"><span class="state-graph-cue-chip state-graph-cue-chip-end">End</span> terminal / no-transition outcome</span>
|
|
424
|
+
<span class="state-graph-cue"><span class="state-graph-cue-chip state-graph-cue-chip-loop">🔁</span> manual re-inspection cue</span>
|
|
425
|
+
</div>`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function renderStateGraphHelp() {
|
|
429
|
+
return `<ul class="state-graph-help">
|
|
430
|
+
<li><strong>Current:</strong> emphasized nodes show the snapshot-derived current state for each lane when that state is actually known.</li>
|
|
431
|
+
<li><strong>Next:</strong> purple nodes mark immediate allowed next states from the snapshot. Dimmed nodes are still part of the authoritative state machine; they are simply inactive right now.</li>
|
|
432
|
+
<li><strong>Start / End:</strong> entry and exit nodes make lane boundaries easier to scan for the full authoritative graph.</li>
|
|
433
|
+
<li><strong>Orchestrator:</strong> the outer lane comes from the shared authoritative outer-loop graph contract; outerAction remains visible only as a compatibility projection.</li>
|
|
434
|
+
<li><strong>Lifecycle:</strong> the lifecycle lane shows the sequential dev-loop phases from issue intake to merge; current phase is highlighted from the inspection snapshot.</li>
|
|
435
|
+
<li><strong>🔁 Loop cue:</strong> this viewer is revisited by manual reload, so the same current state can recur across inspections until evidence changes.</li>
|
|
436
|
+
</ul>`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function renderStateGraphSummaries(graph) {
|
|
440
|
+
return `<ul class="state-graph-summaries">
|
|
441
|
+
${graph.lanes.map((lane) => `<li class="state-graph-summary"><strong>${escapeHtml(lane.title)}:</strong> current <code>${escapeHtml(lane.currentLabel)}</code>; ${escapeHtml(lane.summary ?? lane.transitionInfo.summary)}; ${escapeHtml(lane.transitionInfo.summary)}</li>`).join("")}
|
|
442
|
+
</ul>`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function renderStateGraphDetails(graph) {
|
|
446
|
+
return `<details class="state-graph-details">
|
|
447
|
+
<summary>Graph guide and lane details</summary>
|
|
448
|
+
${renderStateGraphHelp()}
|
|
449
|
+
${renderStateGraphSummaries(graph)}
|
|
450
|
+
</details>`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function renderMermaidBootScript() {
|
|
454
|
+
return `<script src="${MERMAID_BROWSER_ASSET_ROUTE}"></script>
|
|
455
|
+
<script>
|
|
456
|
+
(() => {
|
|
457
|
+
const frames = Array.from(document.querySelectorAll(".state-graph-frame"));
|
|
458
|
+
const graphs = Array.from(document.querySelectorAll(".mermaid-state-graph"));
|
|
459
|
+
const renderedGraphs = new WeakSet();
|
|
460
|
+
const clampScale = (value) => Math.max(0.5, Math.min(5, value));
|
|
461
|
+
const updateFrameScale = (frame, requestedScale) => {
|
|
462
|
+
const scale = clampScale(requestedScale);
|
|
463
|
+
frame.dataset.graphScale = String(scale);
|
|
464
|
+
const zoomValue = frame.querySelector("[data-graph-zoom-value]");
|
|
465
|
+
if (zoomValue) {
|
|
466
|
+
zoomValue.textContent = String(Math.round(scale * 100)) + "%";
|
|
467
|
+
}
|
|
468
|
+
const graphViewport = frame.querySelector(".mermaid-state-graph");
|
|
469
|
+
const svg = graphViewport ? graphViewport.querySelector("svg") : null;
|
|
470
|
+
if (svg && graphViewport) {
|
|
471
|
+
let wrapper = graphViewport.querySelector(":scope > .mermaid-zoom-inner");
|
|
472
|
+
if (!wrapper) {
|
|
473
|
+
const svgRect = svg.getBoundingClientRect();
|
|
474
|
+
wrapper = document.createElement("div");
|
|
475
|
+
wrapper.className = "mermaid-zoom-inner";
|
|
476
|
+
wrapper.style.transformOrigin = "0 0";
|
|
477
|
+
graphViewport.insertBefore(wrapper, svg);
|
|
478
|
+
wrapper.appendChild(svg);
|
|
479
|
+
svg.style.display = "block";
|
|
480
|
+
svg.style.maxWidth = "none";
|
|
481
|
+
if (svgRect.width > 0) {
|
|
482
|
+
frame.dataset.graphNaturalWidth = String(svgRect.width);
|
|
483
|
+
svg.style.width = svgRect.width + "px";
|
|
484
|
+
}
|
|
485
|
+
if (svgRect.height > 0) {
|
|
486
|
+
frame.dataset.graphNaturalHeight = String(svgRect.height);
|
|
487
|
+
svg.style.height = svgRect.height + "px";
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const naturalWidth = Number(frame.dataset.graphNaturalWidth) || graphViewport.getBoundingClientRect().width;
|
|
491
|
+
const naturalHeight = Number(frame.dataset.graphNaturalHeight) || 256;
|
|
492
|
+
wrapper.style.width = Math.round(naturalWidth * scale) + "px";
|
|
493
|
+
wrapper.style.height = Math.round(naturalHeight * scale) + "px";
|
|
494
|
+
svg.style.transform = "scale(" + scale + ")";
|
|
495
|
+
svg.style.transformOrigin = "0 0";
|
|
496
|
+
void graphViewport.offsetWidth;
|
|
497
|
+
}
|
|
498
|
+
return scale;
|
|
499
|
+
};
|
|
500
|
+
const settleGraphViewport = (delayMs = 180) => new Promise((resolve) => {
|
|
501
|
+
let done = false;
|
|
502
|
+
const finish = () => {
|
|
503
|
+
if (done) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
done = true;
|
|
507
|
+
resolve();
|
|
508
|
+
};
|
|
509
|
+
requestAnimationFrame(() => {
|
|
510
|
+
requestAnimationFrame(finish);
|
|
511
|
+
});
|
|
512
|
+
window.setTimeout(finish, delayMs);
|
|
513
|
+
});
|
|
514
|
+
const zoomGraphViewport = (frame, graphViewport, requestedScale, focusPoint = null) => {
|
|
515
|
+
const previousScale = Number(frame.dataset.graphScale || 1);
|
|
516
|
+
const nextScale = updateFrameScale(frame, requestedScale);
|
|
517
|
+
void frame.offsetWidth;
|
|
518
|
+
if (!focusPoint) {
|
|
519
|
+
return settleGraphViewport();
|
|
520
|
+
}
|
|
521
|
+
const scaleRatio = nextScale / previousScale;
|
|
522
|
+
return new Promise((resolve) => {
|
|
523
|
+
requestAnimationFrame(() => {
|
|
524
|
+
const newScrollLeft = (focusPoint.contentX * scaleRatio) - focusPoint.viewportX;
|
|
525
|
+
const newScrollTop = (focusPoint.contentY * scaleRatio) - focusPoint.viewportY;
|
|
526
|
+
graphViewport.scrollLeft = newScrollLeft;
|
|
527
|
+
graphViewport.scrollTop = newScrollTop;
|
|
528
|
+
settleGraphViewport().then(resolve);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
const fitGraphToCurrentState = (frame, graphViewport) => {
|
|
533
|
+
return new Promise((resolve) => {
|
|
534
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
535
|
+
const svg = graphViewport.querySelector("svg");
|
|
536
|
+
if (!svg) {
|
|
537
|
+
resolve(false);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const focusIds = (graphViewport.dataset.graphFocusIds ?? "")
|
|
541
|
+
.split(",").map((id) => id.trim()).filter(Boolean);
|
|
542
|
+
const allNodes = Array.from(svg.querySelectorAll(".node"));
|
|
543
|
+
const focusNodes = focusIds.length === 0
|
|
544
|
+
? []
|
|
545
|
+
: allNodes.filter((node) => focusIds.some((focusId) => node.id.includes(focusId)));
|
|
546
|
+
const targetNodes = focusNodes.length > 0
|
|
547
|
+
? focusNodes
|
|
548
|
+
: Array.from(svg.querySelectorAll(".node.current, .node.currentTerminal, .node.unavailable"));
|
|
549
|
+
const viewportRect = graphViewport.getBoundingClientRect();
|
|
550
|
+
const targetRects = targetNodes
|
|
551
|
+
.map((node) => node.getBoundingClientRect())
|
|
552
|
+
.filter((rect) => rect.width > 0 && rect.height > 0);
|
|
553
|
+
if (targetRects.length === 0) {
|
|
554
|
+
resolve(false);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const [firstRect, ...remainingRects] = targetRects;
|
|
558
|
+
const unionRect = remainingRects.reduce((combined, rect) => ({
|
|
559
|
+
left: Math.min(combined.left, rect.left),
|
|
560
|
+
top: Math.min(combined.top, rect.top),
|
|
561
|
+
right: Math.max(combined.right, rect.right),
|
|
562
|
+
bottom: Math.max(combined.bottom, rect.bottom),
|
|
563
|
+
}), { left: firstRect.left, top: firstRect.top, right: firstRect.right, bottom: firstRect.bottom });
|
|
564
|
+
const unionWidth = unionRect.right - unionRect.left;
|
|
565
|
+
zoomGraphViewport(frame, graphViewport, 3, {
|
|
566
|
+
viewportX: viewportRect.width / 2,
|
|
567
|
+
viewportY: viewportRect.height / 2,
|
|
568
|
+
contentX: graphViewport.scrollLeft + ((unionRect.left - viewportRect.left) + (unionWidth / 2)),
|
|
569
|
+
contentY: graphViewport.scrollTop + ((unionRect.top - viewportRect.top) + ((unionRect.bottom - unionRect.top) / 2)),
|
|
570
|
+
}).then(() => {
|
|
571
|
+
resolve(true);
|
|
572
|
+
});
|
|
573
|
+
}));
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
const renderFallback = (message) => {
|
|
577
|
+
graphs.forEach((graph) => {
|
|
578
|
+
const fallback = document.createElement("p");
|
|
579
|
+
fallback.className = "state-graph-render-error";
|
|
580
|
+
fallback.textContent = message;
|
|
581
|
+
graph.replaceWith(fallback);
|
|
582
|
+
});
|
|
583
|
+
};
|
|
584
|
+
const isGraphVisible = (graph) => {
|
|
585
|
+
if (!(graph instanceof HTMLElement)) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
const panel = graph.closest(".tab-content");
|
|
589
|
+
if (panel && !panel.classList.contains("active")) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
const rect = graph.getBoundingClientRect();
|
|
593
|
+
return rect.width > 0 && rect.height > 0;
|
|
594
|
+
};
|
|
595
|
+
const finalizeRenderedGraph = async (graph) => {
|
|
596
|
+
graph.dataset.rendered = "settling";
|
|
597
|
+
const frame = graph.closest(".state-graph-frame");
|
|
598
|
+
if (!frame) {
|
|
599
|
+
graph.dataset.rendered = "true";
|
|
600
|
+
renderedGraphs.add(graph);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
updateFrameScale(frame, Number(frame.dataset.graphScale || 1));
|
|
604
|
+
const graphViewport = frame.querySelector(".mermaid-state-graph");
|
|
605
|
+
if (graphViewport) {
|
|
606
|
+
const focused = await fitGraphToCurrentState(frame, graphViewport);
|
|
607
|
+
if (!focused) {
|
|
608
|
+
await settleGraphViewport();
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
await settleGraphViewport();
|
|
612
|
+
}
|
|
613
|
+
graph.dataset.rendered = "true";
|
|
614
|
+
renderedGraphs.add(graph);
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
frames.forEach((frame) => {
|
|
618
|
+
updateFrameScale(frame, Number(frame.dataset.graphScale || 1));
|
|
619
|
+
frame.querySelector("[data-graph-zoom-in]")?.addEventListener("click", () => {
|
|
620
|
+
updateFrameScale(frame, Number(frame.dataset.graphScale || 1) + 0.25);
|
|
621
|
+
});
|
|
622
|
+
frame.querySelector("[data-graph-zoom-out]")?.addEventListener("click", () => {
|
|
623
|
+
updateFrameScale(frame, Number(frame.dataset.graphScale || 1) - 0.25);
|
|
624
|
+
});
|
|
625
|
+
frame.querySelector("[data-graph-zoom-reset]")?.addEventListener("click", () => {
|
|
626
|
+
updateFrameScale(frame, 1);
|
|
627
|
+
});
|
|
628
|
+
frame.querySelector("[data-graph-fullscreen]")?.addEventListener("click", async () => {
|
|
629
|
+
if (document.fullscreenElement === frame) {
|
|
630
|
+
await document.exitFullscreen?.();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
await frame.requestFullscreen?.();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const graphViewport = frame.querySelector(".mermaid-state-graph");
|
|
637
|
+
if (graphViewport) {
|
|
638
|
+
let dragState = null;
|
|
639
|
+
graphViewport.addEventListener("pointerdown", (event) => {
|
|
640
|
+
if (event.button !== 0) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
dragState = {
|
|
644
|
+
pointerId: event.pointerId,
|
|
645
|
+
startX: event.clientX,
|
|
646
|
+
startY: event.clientY,
|
|
647
|
+
startScrollLeft: graphViewport.scrollLeft,
|
|
648
|
+
startScrollTop: graphViewport.scrollTop,
|
|
649
|
+
};
|
|
650
|
+
graphViewport.dataset.dragging = "true";
|
|
651
|
+
graphViewport.setPointerCapture?.(event.pointerId);
|
|
652
|
+
event.preventDefault();
|
|
653
|
+
});
|
|
654
|
+
graphViewport.addEventListener("pointermove", (event) => {
|
|
655
|
+
if (!dragState || dragState.pointerId !== event.pointerId) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
graphViewport.scrollLeft = dragState.startScrollLeft - (event.clientX - dragState.startX);
|
|
659
|
+
graphViewport.scrollTop = dragState.startScrollTop - (event.clientY - dragState.startY);
|
|
660
|
+
});
|
|
661
|
+
const stopDragging = (event) => {
|
|
662
|
+
if (!dragState || dragState.pointerId !== event.pointerId) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
graphViewport.dataset.dragging = "false";
|
|
666
|
+
graphViewport.releasePointerCapture?.(event.pointerId);
|
|
667
|
+
dragState = null;
|
|
668
|
+
};
|
|
669
|
+
graphViewport.addEventListener("pointerup", stopDragging);
|
|
670
|
+
graphViewport.addEventListener("pointercancel", stopDragging);
|
|
671
|
+
graphViewport.addEventListener("dblclick", (event) => {
|
|
672
|
+
const rect = graphViewport.getBoundingClientRect();
|
|
673
|
+
zoomGraphViewport(
|
|
674
|
+
frame,
|
|
675
|
+
graphViewport,
|
|
676
|
+
Number(frame.dataset.graphScale || 1) + 0.25,
|
|
677
|
+
{
|
|
678
|
+
viewportX: event.clientX - rect.left,
|
|
679
|
+
viewportY: event.clientY - rect.top,
|
|
680
|
+
contentX: graphViewport.scrollLeft + (event.clientX - rect.left),
|
|
681
|
+
contentY: graphViewport.scrollTop + (event.clientY - rect.top),
|
|
682
|
+
},
|
|
683
|
+
);
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
if (graphs.length === 0) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (typeof window.mermaid === "undefined") {
|
|
693
|
+
renderFallback("Graph renderer unavailable. Use the details below or open /snapshot.json.");
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
window.mermaid.initialize({
|
|
698
|
+
startOnLoad: false,
|
|
699
|
+
securityLevel: "strict",
|
|
700
|
+
theme: "base",
|
|
701
|
+
flowchart: {
|
|
702
|
+
useMaxWidth: true,
|
|
703
|
+
htmlLabels: false,
|
|
704
|
+
curve: "basis",
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
let pendingRender = Promise.resolve();
|
|
709
|
+
const queueVisibleGraphRender = () => {
|
|
710
|
+
pendingRender = pendingRender.then(async () => {
|
|
711
|
+
const visibleGraphs = graphs.filter((graph) => !renderedGraphs.has(graph) && isGraphVisible(graph));
|
|
712
|
+
if (visibleGraphs.length === 0) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
await window.mermaid.run({ nodes: visibleGraphs });
|
|
716
|
+
await Promise.all(visibleGraphs.map((graph) => finalizeRenderedGraph(graph)));
|
|
717
|
+
}).catch(() => {
|
|
718
|
+
renderFallback("Could not render graph for this snapshot. Use the details below or open /snapshot.json.");
|
|
719
|
+
});
|
|
720
|
+
return pendingRender;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
document.addEventListener("inspect-run-viewer:tabchange", () => {
|
|
724
|
+
window.requestAnimationFrame(() => {
|
|
725
|
+
void queueVisibleGraphRender();
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
window.addEventListener("load", () => {
|
|
729
|
+
void queueVisibleGraphRender();
|
|
730
|
+
});
|
|
731
|
+
void queueVisibleGraphRender();
|
|
732
|
+
})();
|
|
733
|
+
</script>`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function renderStateVisualizationSection(snapshot, graph = buildInspectionMermaidGraph(snapshot)) {
|
|
737
|
+
if (graph === null) {
|
|
738
|
+
return `<div class="state-graph-block">
|
|
739
|
+
<p>Snapshot unavailable, so no state graph can be rendered yet.</p>
|
|
740
|
+
</div>`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return `<div class="state-graph-block">
|
|
744
|
+
<div class="state-graph-frame" data-graph-scale="1">
|
|
745
|
+
<div class="state-graph-toolbar" aria-label="Graph controls">
|
|
746
|
+
<button type="button" data-graph-zoom-out aria-label="Zoom out">−</button>
|
|
747
|
+
<button type="button" data-graph-zoom-in aria-label="Zoom in">+</button>
|
|
748
|
+
<button type="button" data-graph-zoom-reset aria-label="Reset zoom">100%</button>
|
|
749
|
+
<span class="state-graph-zoom-value" data-graph-zoom-value>100%</span>
|
|
750
|
+
<button type="button" data-graph-fullscreen aria-label="Open graph fullscreen">⤢</button>
|
|
751
|
+
</div>
|
|
752
|
+
<div class="mermaid-state-graph mermaid" data-rendered="pending" data-graph-focus-ids="${escapeHtml((graph.focusIds ?? []).join(","))}" aria-label="Mermaid inspection state graph">${escapeHtml(graph.definition)}</div>
|
|
753
|
+
</div>
|
|
754
|
+
${renderStateGraphLegend()}
|
|
755
|
+
${renderStateGraphDetails(graph)}
|
|
756
|
+
</div>`;
|
|
757
|
+
}
|