@h-rig/dependency-graph-plugin 0.0.6-alpha.156 → 0.0.6-alpha.158
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/dist/src/analysis/blockers.d.ts +35 -0
- package/dist/src/analysis/blockers.js +355 -0
- package/dist/src/{graph-model → analysis}/dependencyGraph.d.ts +1 -2
- package/dist/src/{graph.js → analysis/dependencyGraph.js} +256 -128
- package/dist/src/{graph.d.ts → analysis/graph.d.ts} +3 -4
- package/dist/src/analysis/graph.js +944 -0
- package/dist/src/analysis/rigSelectors.d.ts +26 -0
- package/dist/src/analysis/rigSelectors.js +172 -0
- package/dist/src/analysis/rollupModel.js +401 -0
- package/dist/src/{rollups.d.ts → analysis/rollups.d.ts} +2 -4
- package/dist/src/analysis/rollups.js +589 -0
- package/dist/src/{graph-model → analysis}/taskGraphCodes.js +1 -1
- package/dist/src/{graph-model → analysis}/taskGraphLayout.d.ts +2 -2
- package/dist/src/{graph-model → analysis}/taskGraphLayout.js +75 -29
- package/dist/src/analysis/taskGraphPrimitives.d.ts +58 -0
- package/dist/src/analysis/taskGraphPrimitives.js +528 -0
- package/dist/src/index.js +567 -396
- package/dist/src/plugin.d.ts +1 -11
- package/dist/src/plugin.js +567 -396
- package/package.json +3 -6
- package/dist/src/graph-model/dependencyGraph.js +0 -485
- package/dist/src/graph-model/rollups.js +0 -166
- package/dist/src/rollups.js +0 -199
- package/dist/src/taskRanking.d.ts +0 -24
- package/dist/src/taskRanking.js +0 -160
- package/dist/src/taskScore.d.ts +0 -17
- package/dist/src/taskScore.js +0 -49
- package/dist/src/taskSelection.d.ts +0 -33
- package/dist/src/taskSelection.js +0 -181
- /package/dist/src/{graph-model/rollups.d.ts → analysis/rollupModel.d.ts} +0 -0
- /package/dist/src/{graph-model → analysis}/taskGraphCodes.d.ts +0 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/dependency-graph-plugin/src/analysis/taskGraphPrimitives.ts
|
|
3
|
+
import {
|
|
4
|
+
OPERATOR_INACTIVE_RUN_STATUSES
|
|
5
|
+
} from "@rig/contracts";
|
|
6
|
+
function isObjectRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function readStringList(value) {
|
|
10
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
|
|
11
|
+
}
|
|
12
|
+
function unique(values) {
|
|
13
|
+
return Array.from(new Set(values));
|
|
14
|
+
}
|
|
15
|
+
function readTaskMetadataStringList(task, key) {
|
|
16
|
+
const taskRecord = task;
|
|
17
|
+
const topLevel = readStringList(taskRecord[key]);
|
|
18
|
+
if (topLevel.length > 0)
|
|
19
|
+
return topLevel;
|
|
20
|
+
const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
|
|
21
|
+
const metadataList = readStringList(metadata?.[key]);
|
|
22
|
+
if (metadataList.length > 0)
|
|
23
|
+
return metadataList;
|
|
24
|
+
if (key === "dependencies") {
|
|
25
|
+
return readStringList(metadata?.deps);
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
function readTaskBlockingDependencyRefs(task) {
|
|
30
|
+
return readTaskMetadataStringList(task, "dependencies");
|
|
31
|
+
}
|
|
32
|
+
function readTaskSourceIssueId(task) {
|
|
33
|
+
if (typeof task.sourceIssueId === "string" && task.sourceIssueId.length > 0) {
|
|
34
|
+
return task.sourceIssueId;
|
|
35
|
+
}
|
|
36
|
+
const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
|
|
37
|
+
if (typeof metadata?.sourceIssueId === "string" && metadata.sourceIssueId.length > 0) {
|
|
38
|
+
return metadata.sourceIssueId;
|
|
39
|
+
}
|
|
40
|
+
const rigMetadata = isObjectRecord(metadata?._rig) ? metadata._rig : null;
|
|
41
|
+
return typeof rigMetadata?.sourceIssueId === "string" && rigMetadata.sourceIssueId.length > 0 ? rigMetadata.sourceIssueId : null;
|
|
42
|
+
}
|
|
43
|
+
function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
|
|
44
|
+
if (tasksById.has(ref))
|
|
45
|
+
return ref;
|
|
46
|
+
return taskIdBySourceIssueId.get(ref) ?? taskIdByExternalRef.get(ref) ?? null;
|
|
47
|
+
}
|
|
48
|
+
function buildTaskReferenceIndex(tasks) {
|
|
49
|
+
return {
|
|
50
|
+
tasksById: new Map(tasks.map((task) => [task.id, task])),
|
|
51
|
+
taskIdByExternalRef: new Map(tasks.flatMap((task) => task.externalId ? [[task.externalId, task.id]] : [])),
|
|
52
|
+
taskIdBySourceIssueId: new Map(tasks.flatMap((task) => {
|
|
53
|
+
const sourceIssueId = readTaskSourceIssueId(task);
|
|
54
|
+
return sourceIssueId ? [[sourceIssueId, task.id]] : [];
|
|
55
|
+
}))
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function computeTaskBlockingDepths(tasks) {
|
|
59
|
+
const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
|
|
60
|
+
const memo = new Map;
|
|
61
|
+
const visit = (taskId, stack) => {
|
|
62
|
+
const cached = memo.get(taskId);
|
|
63
|
+
if (cached !== undefined)
|
|
64
|
+
return cached;
|
|
65
|
+
if (stack.has(taskId))
|
|
66
|
+
return 0;
|
|
67
|
+
const task = tasksById.get(taskId);
|
|
68
|
+
if (!task)
|
|
69
|
+
return 0;
|
|
70
|
+
stack.add(taskId);
|
|
71
|
+
const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
|
|
72
|
+
const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
|
|
73
|
+
stack.delete(taskId);
|
|
74
|
+
memo.set(taskId, depth);
|
|
75
|
+
return depth;
|
|
76
|
+
};
|
|
77
|
+
for (const task of tasks) {
|
|
78
|
+
visit(task.id, new Set);
|
|
79
|
+
}
|
|
80
|
+
return memo;
|
|
81
|
+
}
|
|
82
|
+
function isTaskTerminalStatus(status) {
|
|
83
|
+
switch (status) {
|
|
84
|
+
case "closed":
|
|
85
|
+
case "completed":
|
|
86
|
+
case "done":
|
|
87
|
+
case "cancelled":
|
|
88
|
+
case "canceled":
|
|
89
|
+
return true;
|
|
90
|
+
default:
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function isTaskBlockedStatus(status) {
|
|
95
|
+
return status === "blocked";
|
|
96
|
+
}
|
|
97
|
+
function isTaskRunnableStatus(status) {
|
|
98
|
+
if (status === null || status === undefined || status === "")
|
|
99
|
+
return true;
|
|
100
|
+
if (isTaskTerminalStatus(status) || isTaskBlockedStatus(status))
|
|
101
|
+
return false;
|
|
102
|
+
switch (status) {
|
|
103
|
+
case "ready":
|
|
104
|
+
case "open":
|
|
105
|
+
case "failed":
|
|
106
|
+
return true;
|
|
107
|
+
default:
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function computeTaskDependencyBadges(tasks) {
|
|
112
|
+
const index = buildTaskReferenceIndex(tasks);
|
|
113
|
+
const blockingDepths = computeTaskBlockingDepths(tasks);
|
|
114
|
+
const dependencyIdsByTask = new Map;
|
|
115
|
+
const unresolvedRefsByTask = new Map;
|
|
116
|
+
const blocksByTask = new Map;
|
|
117
|
+
for (const task of tasks) {
|
|
118
|
+
const dependencyIds = [];
|
|
119
|
+
const unresolvedRefs = [];
|
|
120
|
+
for (const ref of readTaskBlockingDependencyRefs(task)) {
|
|
121
|
+
const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
122
|
+
if (dependencyId && dependencyId !== task.id) {
|
|
123
|
+
dependencyIds.push(dependencyId);
|
|
124
|
+
const blocks = blocksByTask.get(dependencyId);
|
|
125
|
+
if (blocks) {
|
|
126
|
+
blocks.push(task.id);
|
|
127
|
+
} else {
|
|
128
|
+
blocksByTask.set(dependencyId, [task.id]);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
unresolvedRefs.push(ref);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
dependencyIdsByTask.set(task.id, unique(dependencyIds));
|
|
135
|
+
unresolvedRefsByTask.set(task.id, unique(unresolvedRefs));
|
|
136
|
+
}
|
|
137
|
+
const summaries = new Map;
|
|
138
|
+
for (const task of tasks) {
|
|
139
|
+
const dependencyIds = dependencyIdsByTask.get(task.id) ?? [];
|
|
140
|
+
const unresolvedDependencyRefs = unresolvedRefsByTask.get(task.id) ?? [];
|
|
141
|
+
const blockedBy = dependencyIds.filter((dependencyId) => {
|
|
142
|
+
const dependency = index.tasksById.get(dependencyId);
|
|
143
|
+
return dependency ? !isTaskTerminalStatus(dependency.status) : false;
|
|
144
|
+
});
|
|
145
|
+
const blocks = unique(blocksByTask.get(task.id) ?? []);
|
|
146
|
+
const blocked = isTaskBlockedStatus(task.status) || blockedBy.length > 0;
|
|
147
|
+
const ready = isTaskRunnableStatus(task.status) && !blocked;
|
|
148
|
+
const badges = [];
|
|
149
|
+
if (blocked) {
|
|
150
|
+
badges.push({
|
|
151
|
+
kind: "blocked",
|
|
152
|
+
label: blockedBy.length > 0 ? `blocked \xD7${blockedBy.length}` : "blocked",
|
|
153
|
+
description: blockedBy.length > 0 ? `Waiting on ${blockedBy.join(", ")}.` : "Task source marks this task blocked.",
|
|
154
|
+
...blockedBy.length > 0 ? { count: blockedBy.length } : {},
|
|
155
|
+
taskIds: blockedBy
|
|
156
|
+
});
|
|
157
|
+
} else if (ready) {
|
|
158
|
+
badges.push({
|
|
159
|
+
kind: "ready",
|
|
160
|
+
label: "ready",
|
|
161
|
+
description: "No open dependencies block this task."
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (dependencyIds.length > 0 || blocks.length > 0 || unresolvedDependencyRefs.length > 0) {
|
|
165
|
+
badges.push({
|
|
166
|
+
kind: "dependency",
|
|
167
|
+
label: `deps ${dependencyIds.length}/${blocks.length}`,
|
|
168
|
+
description: [
|
|
169
|
+
dependencyIds.length > 0 ? `Depends on ${dependencyIds.join(", ")}.` : null,
|
|
170
|
+
blocks.length > 0 ? `Blocks ${blocks.join(", ")}.` : null,
|
|
171
|
+
unresolvedDependencyRefs.length > 0 ? `Unresolved refs: ${unresolvedDependencyRefs.join(", ")}.` : null
|
|
172
|
+
].filter((part) => part !== null).join(" "),
|
|
173
|
+
count: dependencyIds.length + blocks.length,
|
|
174
|
+
taskIds: unique([...dependencyIds, ...blocks])
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
summaries.set(task.id, {
|
|
178
|
+
taskId: task.id,
|
|
179
|
+
blockingDepth: blockingDepths.get(task.id) ?? 0,
|
|
180
|
+
dependencyIds,
|
|
181
|
+
unresolvedDependencyRefs,
|
|
182
|
+
blockedBy,
|
|
183
|
+
blocks,
|
|
184
|
+
blocked,
|
|
185
|
+
ready,
|
|
186
|
+
dependencyCount: dependencyIds.length,
|
|
187
|
+
dependentCount: blocks.length,
|
|
188
|
+
badges
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return summaries;
|
|
192
|
+
}
|
|
193
|
+
var TASK_STATUSES = new Set([
|
|
194
|
+
"draft",
|
|
195
|
+
"open",
|
|
196
|
+
"ready",
|
|
197
|
+
"queued",
|
|
198
|
+
"running",
|
|
199
|
+
"in_progress",
|
|
200
|
+
"under_review",
|
|
201
|
+
"blocked",
|
|
202
|
+
"unknown",
|
|
203
|
+
"completed",
|
|
204
|
+
"failed",
|
|
205
|
+
"cancelled",
|
|
206
|
+
"closed"
|
|
207
|
+
]);
|
|
208
|
+
function stringValue(value) {
|
|
209
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
210
|
+
}
|
|
211
|
+
function stringArray(value) {
|
|
212
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim()) : [];
|
|
213
|
+
}
|
|
214
|
+
function numberOrNull(value) {
|
|
215
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
216
|
+
}
|
|
217
|
+
function metadataOf(task) {
|
|
218
|
+
return isObjectRecord(task.metadata) ? task.metadata : {};
|
|
219
|
+
}
|
|
220
|
+
function normalizeTaskStatus(status) {
|
|
221
|
+
const token = typeof status === "string" ? status.trim().toLowerCase() : "";
|
|
222
|
+
if (token === "done")
|
|
223
|
+
return "completed";
|
|
224
|
+
if (token === "canceled")
|
|
225
|
+
return "cancelled";
|
|
226
|
+
return TASK_STATUSES.has(token) ? token : "unknown";
|
|
227
|
+
}
|
|
228
|
+
function toTaskDependencyProjection(task) {
|
|
229
|
+
const metadata = metadataOf(task);
|
|
230
|
+
return {
|
|
231
|
+
id: String(task.id),
|
|
232
|
+
title: stringValue(task.title),
|
|
233
|
+
status: normalizeTaskStatus(task.status),
|
|
234
|
+
priority: numberOrNull(task.priority),
|
|
235
|
+
metadata,
|
|
236
|
+
externalId: stringValue(task.externalId),
|
|
237
|
+
sourceIssueId: stringValue(task.sourceIssueId),
|
|
238
|
+
dependencies: stringArray(task.dependencies),
|
|
239
|
+
parentChildDeps: stringArray(task.parentChildDeps),
|
|
240
|
+
createdAt: stringValue(task.createdAt) ?? "",
|
|
241
|
+
updatedAt: stringValue(task.updatedAt) ?? "",
|
|
242
|
+
role: stringValue(task.role),
|
|
243
|
+
scope: stringArray(task.scope),
|
|
244
|
+
validationKeys: stringArray(task.validationKeys),
|
|
245
|
+
labels: stringArray(task.labels),
|
|
246
|
+
assignees: task.assignees ?? null,
|
|
247
|
+
assignedTo: task.assignedTo ?? null
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function toTaskSummary(task, defaults = {}) {
|
|
251
|
+
const projection = toTaskDependencyProjection(task);
|
|
252
|
+
const metadata = metadataOf(task);
|
|
253
|
+
const createdAt = stringValue(task.createdAt) ?? "1970-01-01T00:00:00.000Z";
|
|
254
|
+
const updatedAt = stringValue(task.updatedAt) ?? createdAt;
|
|
255
|
+
return {
|
|
256
|
+
id: projection.id,
|
|
257
|
+
workspaceId: stringValue(task.workspaceId) ?? defaults.workspaceId ?? "workspace",
|
|
258
|
+
graphId: stringValue(task.graphId) ?? defaults.graphId ?? null,
|
|
259
|
+
externalId: projection.externalId,
|
|
260
|
+
title: projection.title ?? projection.id,
|
|
261
|
+
description: stringValue(task.description) ?? stringValue(task.body) ?? "",
|
|
262
|
+
status: projection.status,
|
|
263
|
+
priority: numberOrNull(task.priority),
|
|
264
|
+
role: projection.role,
|
|
265
|
+
scope: [...projection.scope ?? []],
|
|
266
|
+
validationKeys: [...projection.validationKeys ?? []],
|
|
267
|
+
...projection.sourceIssueId ? { sourceIssueId: projection.sourceIssueId } : {},
|
|
268
|
+
dependencies: [...projection.dependencies ?? []],
|
|
269
|
+
parentChildDeps: [...projection.parentChildDeps ?? []],
|
|
270
|
+
metadata,
|
|
271
|
+
createdAt,
|
|
272
|
+
updatedAt
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function latestRunByTaskId(runs) {
|
|
276
|
+
const byTask = new Map;
|
|
277
|
+
const stamp = (run) => Date.parse(run.updatedAt ?? run.startedAt ?? "") || 0;
|
|
278
|
+
for (const run of runs) {
|
|
279
|
+
if (!run.taskId)
|
|
280
|
+
continue;
|
|
281
|
+
const current = byTask.get(run.taskId);
|
|
282
|
+
if (!current || stamp(run) >= stamp(current))
|
|
283
|
+
byTask.set(run.taskId, run);
|
|
284
|
+
}
|
|
285
|
+
return byTask;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// packages/dependency-graph-plugin/src/analysis/taskGraphCodes.ts
|
|
289
|
+
var TASK_CODE_RE = /^\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]\s*/;
|
|
290
|
+
function extractTaskCode(title) {
|
|
291
|
+
const match = title.match(TASK_CODE_RE);
|
|
292
|
+
return match?.[1] ?? null;
|
|
293
|
+
}
|
|
294
|
+
function extractTaskGroupKey(title) {
|
|
295
|
+
const code = extractTaskCode(title);
|
|
296
|
+
if (!code)
|
|
297
|
+
return null;
|
|
298
|
+
const parts = code.split("-");
|
|
299
|
+
const suffix = parts.at(-1) ?? "";
|
|
300
|
+
if (/^\d+$/.test(suffix)) {
|
|
301
|
+
return parts.slice(0, -1).join("-");
|
|
302
|
+
}
|
|
303
|
+
return parts[0] ?? code;
|
|
304
|
+
}
|
|
305
|
+
function stripTaskCode(label) {
|
|
306
|
+
return label.replace(TASK_CODE_RE, "");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// packages/dependency-graph-plugin/src/analysis/taskGraphLayout.ts
|
|
310
|
+
var CARD_WIDTH = 200;
|
|
311
|
+
var CARD_HEIGHT = 110;
|
|
312
|
+
var CELL_V_PAD = 12;
|
|
313
|
+
var CELL_H_PAD = 12;
|
|
314
|
+
var ROW_GAP = 28;
|
|
315
|
+
var COL_GAP = 40;
|
|
316
|
+
var LANE_LABEL_W = 120;
|
|
317
|
+
var STAGE_HDR_H = 32;
|
|
318
|
+
var PALETTE = [
|
|
319
|
+
{ bg: "#3a2d12", border: "#8d6b19", edge: "#d6a11d" },
|
|
320
|
+
{ bg: "#102642", border: "#245fbf", edge: "#66a2ff" },
|
|
321
|
+
{ bg: "#2c173f", border: "#7b39d4", edge: "#a76df5" },
|
|
322
|
+
{ bg: "#112d1c", border: "#2d8f4e", edge: "#62d882" },
|
|
323
|
+
{ bg: "#3a2314", border: "#c86d1c", edge: "#e69654" },
|
|
324
|
+
{ bg: "#31152b", border: "#bf3d88", edge: "#f07ebb" },
|
|
325
|
+
{ bg: "#132c35", border: "#1783a6", edge: "#53c4e5" },
|
|
326
|
+
{ bg: "#26310f", border: "#6d9a19", edge: "#a7da42" }
|
|
327
|
+
];
|
|
328
|
+
function isObjectRecord2(value) {
|
|
329
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
330
|
+
}
|
|
331
|
+
function readIssueType(task) {
|
|
332
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
333
|
+
if (typeof metadata?.issueType === "string")
|
|
334
|
+
return metadata.issueType;
|
|
335
|
+
const raw = isObjectRecord2(metadata?.raw) ? metadata.raw : null;
|
|
336
|
+
return typeof raw?.issueType === "string" ? raw.issueType : null;
|
|
337
|
+
}
|
|
338
|
+
function isGraphTask(task) {
|
|
339
|
+
return readIssueType(task) !== "epic";
|
|
340
|
+
}
|
|
341
|
+
function findEpicAncestor(task, resolve, tasksById) {
|
|
342
|
+
const visited = new Set;
|
|
343
|
+
let current = task;
|
|
344
|
+
let epic = null;
|
|
345
|
+
while (!visited.has(current.id)) {
|
|
346
|
+
visited.add(current.id);
|
|
347
|
+
const parentRef = readTaskMetadataStringList(current, "parentChildDeps")[0];
|
|
348
|
+
if (!parentRef)
|
|
349
|
+
break;
|
|
350
|
+
const parentId = resolve(parentRef);
|
|
351
|
+
if (!parentId)
|
|
352
|
+
break;
|
|
353
|
+
const parent = tasksById.get(parentId);
|
|
354
|
+
if (!parent)
|
|
355
|
+
break;
|
|
356
|
+
if (readIssueType(parent) === "epic") {
|
|
357
|
+
epic = parent;
|
|
358
|
+
}
|
|
359
|
+
current = parent;
|
|
360
|
+
}
|
|
361
|
+
return epic;
|
|
362
|
+
}
|
|
363
|
+
function getRowKey(task, resolve, tasksById) {
|
|
364
|
+
const epic = findEpicAncestor(task, resolve, tasksById);
|
|
365
|
+
if (epic) {
|
|
366
|
+
return `group:${epic.id}`;
|
|
367
|
+
}
|
|
368
|
+
const codeGroup = extractTaskGroupKey(task.title);
|
|
369
|
+
if (codeGroup) {
|
|
370
|
+
return `code:${codeGroup}`;
|
|
371
|
+
}
|
|
372
|
+
return `type:${readIssueType(task) ?? "task"}`;
|
|
373
|
+
}
|
|
374
|
+
function getRowLabel(task, rowKey, resolve, tasksById) {
|
|
375
|
+
if (rowKey.startsWith("group:")) {
|
|
376
|
+
const groupId = rowKey.slice("group:".length);
|
|
377
|
+
return tasksById.get(groupId)?.title ?? groupId;
|
|
378
|
+
}
|
|
379
|
+
if (rowKey.startsWith("code:")) {
|
|
380
|
+
return rowKey.slice("code:".length);
|
|
381
|
+
}
|
|
382
|
+
const code = extractTaskCode(task.title);
|
|
383
|
+
if (code)
|
|
384
|
+
return code;
|
|
385
|
+
const issueType = readIssueType(task);
|
|
386
|
+
if (issueType === "task")
|
|
387
|
+
return "Tasks";
|
|
388
|
+
if (issueType)
|
|
389
|
+
return `${issueType[0]?.toUpperCase() ?? ""}${issueType.slice(1)}`;
|
|
390
|
+
const parentRef = readTaskMetadataStringList(task, "parentChildDeps")[0];
|
|
391
|
+
if (parentRef) {
|
|
392
|
+
const parentId = resolve(parentRef);
|
|
393
|
+
if (parentId) {
|
|
394
|
+
return tasksById.get(parentId)?.title ?? "Tasks";
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return "Tasks";
|
|
398
|
+
}
|
|
399
|
+
function computeDepths(ids, edges) {
|
|
400
|
+
const blockers = new Map;
|
|
401
|
+
for (const edge of edges) {
|
|
402
|
+
if (!ids.has(edge.source) || !ids.has(edge.target))
|
|
403
|
+
continue;
|
|
404
|
+
const current = blockers.get(edge.target) ?? [];
|
|
405
|
+
current.push(edge.source);
|
|
406
|
+
blockers.set(edge.target, current);
|
|
407
|
+
}
|
|
408
|
+
const memo = new Map;
|
|
409
|
+
const visit = (id, stack) => {
|
|
410
|
+
const cached = memo.get(id);
|
|
411
|
+
if (cached !== undefined)
|
|
412
|
+
return cached;
|
|
413
|
+
if (stack.has(id))
|
|
414
|
+
return 0;
|
|
415
|
+
stack.add(id);
|
|
416
|
+
const deps = blockers.get(id);
|
|
417
|
+
if (!deps || deps.length === 0) {
|
|
418
|
+
memo.set(id, 0);
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
let maxDepth = 0;
|
|
422
|
+
for (const dep of deps) {
|
|
423
|
+
maxDepth = Math.max(maxDepth, visit(dep, stack) + 1);
|
|
424
|
+
}
|
|
425
|
+
stack.delete(id);
|
|
426
|
+
memo.set(id, maxDepth);
|
|
427
|
+
return maxDepth;
|
|
428
|
+
};
|
|
429
|
+
for (const id of ids) {
|
|
430
|
+
visit(id, new Set);
|
|
431
|
+
}
|
|
432
|
+
return memo;
|
|
433
|
+
}
|
|
434
|
+
function buildTaskGraphLayout(tasks, options) {
|
|
435
|
+
const showParentChild = options?.showParentChild ?? false;
|
|
436
|
+
const graphTasks = tasks.filter(isGraphTask);
|
|
437
|
+
if (graphTasks.length === 0) {
|
|
438
|
+
return {
|
|
439
|
+
lanes: [],
|
|
440
|
+
stages: [],
|
|
441
|
+
nodes: [],
|
|
442
|
+
edges: [],
|
|
443
|
+
totalWidth: 0,
|
|
444
|
+
totalHeight: 0,
|
|
445
|
+
taskCount: 0
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
|
|
449
|
+
const resolve = (ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId);
|
|
450
|
+
const rows = new Map;
|
|
451
|
+
const rowLabelByKey = new Map;
|
|
452
|
+
for (const task of graphTasks) {
|
|
453
|
+
const rowKey = getRowKey(task, resolve, tasksById);
|
|
454
|
+
const current = rows.get(rowKey) ?? [];
|
|
455
|
+
current.push(task);
|
|
456
|
+
rows.set(rowKey, current);
|
|
457
|
+
if (!rowLabelByKey.has(rowKey)) {
|
|
458
|
+
rowLabelByKey.set(rowKey, getRowLabel(task, rowKey, resolve, tasksById));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const orderedRows = [...rows.entries()].sort((left, right) => {
|
|
462
|
+
if (left[1].length !== right[1].length)
|
|
463
|
+
return right[1].length - left[1].length;
|
|
464
|
+
return (rowLabelByKey.get(left[0]) ?? left[0]).localeCompare(rowLabelByKey.get(right[0]) ?? right[0]);
|
|
465
|
+
});
|
|
466
|
+
const rowIndexByKey = new Map(orderedRows.map(([key], index) => [key, index]));
|
|
467
|
+
const rowIndexByTaskId = new Map;
|
|
468
|
+
for (const task of graphTasks) {
|
|
469
|
+
rowIndexByTaskId.set(task.id, rowIndexByKey.get(getRowKey(task, resolve, tasksById)) ?? 0);
|
|
470
|
+
}
|
|
471
|
+
const blockingEdgesRaw = [];
|
|
472
|
+
const depsIn = new Map;
|
|
473
|
+
const depsOut = new Map;
|
|
474
|
+
const edges = [];
|
|
475
|
+
for (const task of graphTasks) {
|
|
476
|
+
for (const ref of readTaskMetadataStringList(task, "dependencies")) {
|
|
477
|
+
const sourceId = resolve(ref);
|
|
478
|
+
if (!sourceId)
|
|
479
|
+
continue;
|
|
480
|
+
blockingEdgesRaw.push({ source: sourceId, target: task.id });
|
|
481
|
+
depsOut.set(sourceId, (depsOut.get(sourceId) ?? 0) + 1);
|
|
482
|
+
depsIn.set(task.id, (depsIn.get(task.id) ?? 0) + 1);
|
|
483
|
+
const color = PALETTE[(rowIndexByTaskId.get(sourceId) ?? 0) % PALETTE.length].edge;
|
|
484
|
+
edges.push({
|
|
485
|
+
id: `blocking:${sourceId}:${task.id}`,
|
|
486
|
+
sourceId,
|
|
487
|
+
targetId: task.id,
|
|
488
|
+
color,
|
|
489
|
+
kind: "blocking"
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (!showParentChild)
|
|
493
|
+
continue;
|
|
494
|
+
for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
|
|
495
|
+
const sourceId = resolve(ref);
|
|
496
|
+
if (!sourceId)
|
|
497
|
+
continue;
|
|
498
|
+
edges.push({
|
|
499
|
+
id: `parent:${sourceId}:${task.id}`,
|
|
500
|
+
sourceId,
|
|
501
|
+
targetId: task.id,
|
|
502
|
+
color: "#5b6472",
|
|
503
|
+
kind: "parent-child"
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const graphTaskIds = new Set(graphTasks.map((task) => task.id));
|
|
508
|
+
const depths = computeDepths(graphTaskIds, blockingEdgesRaw);
|
|
509
|
+
const maxStage = Math.max(0, ...depths.values());
|
|
510
|
+
const rowMaxStack = new Array(orderedRows.length).fill(0);
|
|
511
|
+
const cells = new Map;
|
|
512
|
+
for (const task of graphTasks) {
|
|
513
|
+
const rowIndex = rowIndexByTaskId.get(task.id) ?? 0;
|
|
514
|
+
const stage = depths.get(task.id) ?? 0;
|
|
515
|
+
const key = `${rowIndex}:${stage}`;
|
|
516
|
+
const current = cells.get(key) ?? [];
|
|
517
|
+
current.push(task);
|
|
518
|
+
cells.set(key, current);
|
|
519
|
+
}
|
|
520
|
+
for (const [cellKey, cellTasks] of cells) {
|
|
521
|
+
const [rowIndexText] = cellKey.split(":");
|
|
522
|
+
const rowIndex = Number.parseInt(rowIndexText ?? "0", 10) || 0;
|
|
523
|
+
cellTasks.sort((left, right) => {
|
|
524
|
+
const leftFanout = depsOut.get(left.id) ?? 0;
|
|
525
|
+
const rightFanout = depsOut.get(right.id) ?? 0;
|
|
526
|
+
if (leftFanout !== rightFanout)
|
|
527
|
+
return rightFanout - leftFanout;
|
|
528
|
+
return left.title.localeCompare(right.title);
|
|
529
|
+
});
|
|
530
|
+
rowMaxStack[rowIndex] = Math.max(rowMaxStack[rowIndex] ?? 0, cellTasks.length);
|
|
531
|
+
}
|
|
532
|
+
const rowHeights = rowMaxStack.map((count) => Math.max(count, 1) * (CARD_HEIGHT + CELL_V_PAD) - CELL_V_PAD + CELL_V_PAD * 2);
|
|
533
|
+
const colWidths = new Array(maxStage + 1).fill(CARD_WIDTH + CELL_H_PAD * 2);
|
|
534
|
+
const colX = [];
|
|
535
|
+
let currentX = LANE_LABEL_W;
|
|
536
|
+
for (let index = 0;index <= maxStage; index += 1) {
|
|
537
|
+
colX.push(currentX);
|
|
538
|
+
currentX += (colWidths[index] ?? 0) + COL_GAP;
|
|
539
|
+
}
|
|
540
|
+
const totalWidth = currentX - COL_GAP;
|
|
541
|
+
const rowY = [];
|
|
542
|
+
let currentY = STAGE_HDR_H;
|
|
543
|
+
for (let rowIndex = 0;rowIndex < orderedRows.length; rowIndex += 1) {
|
|
544
|
+
rowY.push(currentY);
|
|
545
|
+
currentY += (rowHeights[rowIndex] ?? 0) + ROW_GAP;
|
|
546
|
+
}
|
|
547
|
+
const totalHeight = currentY - ROW_GAP;
|
|
548
|
+
const lanes = orderedRows.map(([rowKey, rowTasks], rowIndex) => ({
|
|
549
|
+
key: rowKey,
|
|
550
|
+
label: rowLabelByKey.get(rowKey) ?? rowKey,
|
|
551
|
+
rowIndex,
|
|
552
|
+
x: LANE_LABEL_W - 6,
|
|
553
|
+
y: (rowY[rowIndex] ?? 0) - 6,
|
|
554
|
+
width: totalWidth - LANE_LABEL_W + 12,
|
|
555
|
+
height: (rowHeights[rowIndex] ?? 0) + 12,
|
|
556
|
+
color: PALETTE[rowIndex % PALETTE.length].border,
|
|
557
|
+
taskCount: rowTasks.length
|
|
558
|
+
}));
|
|
559
|
+
const stages = Array.from({ length: maxStage + 1 }, (_, index) => ({
|
|
560
|
+
index,
|
|
561
|
+
label: index === 0 ? "Roots" : `Stage ${index}`,
|
|
562
|
+
x: (colX[index] ?? 0) + CELL_H_PAD,
|
|
563
|
+
width: CARD_WIDTH
|
|
564
|
+
}));
|
|
565
|
+
const nodes = [];
|
|
566
|
+
for (const [rowIndex, [rowKey]] of orderedRows.entries()) {
|
|
567
|
+
for (let stage = 0;stage <= maxStage; stage += 1) {
|
|
568
|
+
const cellTasks = cells.get(`${rowIndex}:${stage}`) ?? [];
|
|
569
|
+
const baseX = (colX[stage] ?? 0) + CELL_H_PAD;
|
|
570
|
+
const baseY = (rowY[rowIndex] ?? 0) + CELL_V_PAD;
|
|
571
|
+
const palette = PALETTE[rowIndex % PALETTE.length];
|
|
572
|
+
for (const [stackIndex, task] of cellTasks.entries()) {
|
|
573
|
+
nodes.push({
|
|
574
|
+
id: task.id,
|
|
575
|
+
taskId: task.id,
|
|
576
|
+
task,
|
|
577
|
+
rowKey,
|
|
578
|
+
rowLabel: rowLabelByKey.get(rowKey) ?? rowKey,
|
|
579
|
+
rowIndex,
|
|
580
|
+
stage,
|
|
581
|
+
x: baseX,
|
|
582
|
+
y: baseY + stackIndex * (CARD_HEIGHT + CELL_V_PAD),
|
|
583
|
+
width: CARD_WIDTH,
|
|
584
|
+
height: CARD_HEIGHT,
|
|
585
|
+
color: palette.border,
|
|
586
|
+
taskCode: extractTaskCode(task.title),
|
|
587
|
+
strippedTitle: stripTaskCode(task.title),
|
|
588
|
+
depsIn: depsIn.get(task.id) ?? 0,
|
|
589
|
+
depsOut: depsOut.get(task.id) ?? 0,
|
|
590
|
+
runCount: 0,
|
|
591
|
+
hasApprovals: false,
|
|
592
|
+
hasPendingUserInput: false,
|
|
593
|
+
hasRejectedReview: false,
|
|
594
|
+
hasFailedValidations: false,
|
|
595
|
+
artifactCount: 0
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
lanes,
|
|
602
|
+
stages,
|
|
603
|
+
nodes,
|
|
604
|
+
edges,
|
|
605
|
+
totalWidth,
|
|
606
|
+
totalHeight,
|
|
607
|
+
taskCount: graphTasks.length
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// packages/dependency-graph-plugin/src/analysis/rigSelectors.ts
|
|
612
|
+
function isObjectRecord3(value) {
|
|
613
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
614
|
+
}
|
|
615
|
+
function normalizeLogin(value) {
|
|
616
|
+
return value.trim().replace(/^@+/, "").toLowerCase();
|
|
617
|
+
}
|
|
618
|
+
function assigneeLoginsFromValue(value) {
|
|
619
|
+
if (!Array.isArray(value))
|
|
620
|
+
return [];
|
|
621
|
+
return value.flatMap((entry) => {
|
|
622
|
+
if (typeof entry === "string" && entry.trim())
|
|
623
|
+
return [normalizeLogin(entry)];
|
|
624
|
+
if (isObjectRecord3(entry) && typeof entry.login === "string" && entry.login.trim()) {
|
|
625
|
+
return [normalizeLogin(entry.login)];
|
|
626
|
+
}
|
|
627
|
+
return [];
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
function readTaskAssigneeLogins(task) {
|
|
631
|
+
const taskRecord = task;
|
|
632
|
+
const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
|
|
633
|
+
const raw = isObjectRecord3(metadata?.raw) ? metadata.raw : null;
|
|
634
|
+
return Array.from(new Set([
|
|
635
|
+
...assigneeLoginsFromValue(taskRecord.assignees),
|
|
636
|
+
...assigneeLoginsFromValue(metadata?.assignees),
|
|
637
|
+
...assigneeLoginsFromValue(raw?.assignees)
|
|
638
|
+
]));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// packages/dependency-graph-plugin/src/analysis/dependencyGraph.ts
|
|
642
|
+
function uniqueSorted(values) {
|
|
643
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
644
|
+
}
|
|
645
|
+
function deriveGraphId(tasks) {
|
|
646
|
+
const basis = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right)).join("|");
|
|
647
|
+
let hash = 2166136261;
|
|
648
|
+
for (let index = 0;index < basis.length; index += 1) {
|
|
649
|
+
hash ^= basis.charCodeAt(index);
|
|
650
|
+
hash = Math.imul(hash, 16777619);
|
|
651
|
+
}
|
|
652
|
+
return `graph-${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
653
|
+
}
|
|
654
|
+
function dedupeEdges(edges) {
|
|
655
|
+
const byKey = new Map;
|
|
656
|
+
for (const edge of edges) {
|
|
657
|
+
byKey.set(`${edge.type}\x00${edge.fromTaskId}\x00${edge.toTaskId}`, edge);
|
|
658
|
+
}
|
|
659
|
+
return [...byKey.values()].toSorted((left, right) => {
|
|
660
|
+
const typeDelta = left.type.localeCompare(right.type);
|
|
661
|
+
if (typeDelta !== 0)
|
|
662
|
+
return typeDelta;
|
|
663
|
+
const fromDelta = left.fromTaskId.localeCompare(right.fromTaskId);
|
|
664
|
+
if (fromDelta !== 0)
|
|
665
|
+
return fromDelta;
|
|
666
|
+
return left.toTaskId.localeCompare(right.toTaskId);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function detectBlockingCycles(tasks, edges) {
|
|
670
|
+
const taskIds = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right));
|
|
671
|
+
const adjacency = new Map;
|
|
672
|
+
for (const taskId of taskIds)
|
|
673
|
+
adjacency.set(taskId, []);
|
|
674
|
+
for (const edge of edges) {
|
|
675
|
+
if (edge.type === "blocks")
|
|
676
|
+
adjacency.get(edge.fromTaskId)?.push(edge.toTaskId);
|
|
677
|
+
}
|
|
678
|
+
for (const targets of adjacency.values())
|
|
679
|
+
targets.sort((left, right) => left.localeCompare(right));
|
|
680
|
+
let nextIndex = 0;
|
|
681
|
+
const indexByNode = new Map;
|
|
682
|
+
const lowByNode = new Map;
|
|
683
|
+
const stack = [];
|
|
684
|
+
const onStack = new Set;
|
|
685
|
+
const cycles = [];
|
|
686
|
+
const visit = (node) => {
|
|
687
|
+
indexByNode.set(node, nextIndex);
|
|
688
|
+
lowByNode.set(node, nextIndex);
|
|
689
|
+
nextIndex += 1;
|
|
690
|
+
stack.push(node);
|
|
691
|
+
onStack.add(node);
|
|
692
|
+
for (const target of adjacency.get(node) ?? []) {
|
|
693
|
+
if (!indexByNode.has(target)) {
|
|
694
|
+
visit(target);
|
|
695
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
|
|
696
|
+
} else if (onStack.has(target)) {
|
|
697
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (lowByNode.get(node) !== indexByNode.get(node))
|
|
701
|
+
return;
|
|
702
|
+
const component = [];
|
|
703
|
+
let current;
|
|
704
|
+
do {
|
|
705
|
+
current = stack.pop();
|
|
706
|
+
if (current) {
|
|
707
|
+
onStack.delete(current);
|
|
708
|
+
component.push(current);
|
|
709
|
+
}
|
|
710
|
+
} while (current && current !== node);
|
|
711
|
+
const selfLoop = (adjacency.get(node) ?? []).includes(node);
|
|
712
|
+
if (component.length > 1 || selfLoop)
|
|
713
|
+
cycles.push(component.sort((left, right) => left.localeCompare(right)));
|
|
714
|
+
};
|
|
715
|
+
for (const taskId of taskIds) {
|
|
716
|
+
if (!indexByNode.has(taskId))
|
|
717
|
+
visit(taskId);
|
|
718
|
+
}
|
|
719
|
+
return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
|
|
720
|
+
}
|
|
721
|
+
function buildDependencyGraphModel(tasks, options = {}) {
|
|
722
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
723
|
+
const index = buildTaskReferenceIndex(tasks);
|
|
724
|
+
const edges = [];
|
|
725
|
+
const unresolvedRefs = [];
|
|
726
|
+
for (const task of tasks) {
|
|
727
|
+
for (const ref of readTaskMetadataStringList(task, "dependencies")) {
|
|
728
|
+
const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
729
|
+
if (dependencyId)
|
|
730
|
+
edges.push({ fromTaskId: dependencyId, toTaskId: String(task.id), type: "blocks" });
|
|
731
|
+
else
|
|
732
|
+
unresolvedRefs.push(ref);
|
|
733
|
+
}
|
|
734
|
+
for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
|
|
735
|
+
const parentId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
736
|
+
if (parentId)
|
|
737
|
+
edges.push({ fromTaskId: parentId, toTaskId: String(task.id), type: "parent-child" });
|
|
738
|
+
else
|
|
739
|
+
unresolvedRefs.push(ref);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const dedupedEdges = dedupeEdges(edges);
|
|
743
|
+
const nodes = tasks.map((task) => {
|
|
744
|
+
const summary = badges.get(task.id);
|
|
745
|
+
const assignees = readTaskAssigneeLogins(task);
|
|
746
|
+
const groupKey = extractTaskGroupKey(task.title);
|
|
747
|
+
return {
|
|
748
|
+
taskId: String(task.id),
|
|
749
|
+
title: task.title,
|
|
750
|
+
status: task.status,
|
|
751
|
+
priority: task.priority,
|
|
752
|
+
assignee: assignees[0] ?? null,
|
|
753
|
+
blockedBy: summary?.blockedBy ?? [],
|
|
754
|
+
blocks: summary?.blocks ?? [],
|
|
755
|
+
blockingDepth: summary?.blockingDepth ?? 0,
|
|
756
|
+
blockerClass: null,
|
|
757
|
+
actionRiskTier: null,
|
|
758
|
+
epicKey: groupKey,
|
|
759
|
+
groupKey,
|
|
760
|
+
externalId: task.externalId,
|
|
761
|
+
sourceIssueId: task.sourceIssueId ?? null,
|
|
762
|
+
scope: task.scope,
|
|
763
|
+
validationKeys: task.validationKeys
|
|
764
|
+
};
|
|
765
|
+
}).toSorted((left, right) => left.taskId.localeCompare(right.taskId));
|
|
766
|
+
return {
|
|
767
|
+
graphId: options.graphId ?? deriveGraphId(tasks),
|
|
768
|
+
nodes,
|
|
769
|
+
edges: dedupedEdges,
|
|
770
|
+
layout: buildTaskGraphLayout(tasks, { showParentChild: options.showParentChild ?? true }),
|
|
771
|
+
cycles: detectBlockingCycles(tasks, dedupedEdges),
|
|
772
|
+
unresolvedRefs: uniqueSorted(unresolvedRefs),
|
|
773
|
+
degraded: false,
|
|
774
|
+
generatedAt: options.generatedAt ?? new Date().toISOString()
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// packages/dependency-graph-plugin/src/analysis/blockers.ts
|
|
779
|
+
var HUMAN_BLOCKERS = new Set(["human-decision", "human-approval", "external-input"]);
|
|
780
|
+
function labelsFor(task) {
|
|
781
|
+
return readTaskMetadataStringList(task, "labels").map((label) => label.toLowerCase());
|
|
782
|
+
}
|
|
783
|
+
function configuredTier(task, labels) {
|
|
784
|
+
const metadata = task.metadata && typeof task.metadata === "object" && !Array.isArray(task.metadata) ? task.metadata : {};
|
|
785
|
+
const tier = metadata.riskTier ?? metadata.actionRiskTier;
|
|
786
|
+
if (tier === "t1-read" || tier === "t2-reversible" || tier === "t3-external" || tier === "t4-irreversible")
|
|
787
|
+
return tier;
|
|
788
|
+
if (labels.some((label) => /prod|deploy|migration|billing|delete|irreversible/.test(label)))
|
|
789
|
+
return "t4-irreversible";
|
|
790
|
+
if (labels.some((label) => /customer|vendor|external|secret|credential|approval|decision/.test(label)))
|
|
791
|
+
return "t3-external";
|
|
792
|
+
const scopes = task.scope ?? [];
|
|
793
|
+
if (scopes.some((scope) => /docs|readme|test|spec/.test(scope.toLowerCase())))
|
|
794
|
+
return "t1-read";
|
|
795
|
+
return "t2-reversible";
|
|
796
|
+
}
|
|
797
|
+
function tierOf(task, labels = []) {
|
|
798
|
+
const projection = "metadata" in task && "id" in task && typeof task.id === "string" ? toTaskDependencyProjection(task) : task;
|
|
799
|
+
return configuredTier(projection, labels.length > 0 ? labels : labelsFor(projection)) ?? "t2-reversible";
|
|
800
|
+
}
|
|
801
|
+
function baseClassification(task, blockerClass, source, rationale, labels) {
|
|
802
|
+
return {
|
|
803
|
+
taskId: task.id,
|
|
804
|
+
blockerClass,
|
|
805
|
+
actionRiskTier: tierOf(task, labels),
|
|
806
|
+
rationale,
|
|
807
|
+
source,
|
|
808
|
+
autoApplied: source === "llm"
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function normalizeRunStatus(status) {
|
|
812
|
+
if (status === "waiting-approval" || status === "waiting-user-input" || status === "needs-attention" || status === "completed" || status === "failed" || status === "stopped" || status === "running")
|
|
813
|
+
return status;
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
function isHumanBlockerClass(blockerClass) {
|
|
817
|
+
return HUMAN_BLOCKERS.has(blockerClass);
|
|
818
|
+
}
|
|
819
|
+
function classifyBlocker(input) {
|
|
820
|
+
const labels = input.labels ?? labelsFor(input.task);
|
|
821
|
+
const runStatus = normalizeRunStatus(input.run?.status);
|
|
822
|
+
if (runStatus === "waiting-approval")
|
|
823
|
+
return baseClassification(input.task, "human-approval", "status", "run awaiting approval", labels);
|
|
824
|
+
if (runStatus === "waiting-user-input")
|
|
825
|
+
return baseClassification(input.task, "external-input", "status", "run awaiting user input", labels);
|
|
826
|
+
if (input.task.status !== "blocked")
|
|
827
|
+
return baseClassification(input.task, "not-blocked", "elimination", "task is not blocked", labels);
|
|
828
|
+
const incomplete = (input.badges.get(input.task.id)?.blockedBy ?? []).filter((dependencyId) => {
|
|
829
|
+
const dependency = input.tasksById.get(dependencyId);
|
|
830
|
+
return dependency ? !isTaskTerminalStatus(dependency.status) : false;
|
|
831
|
+
});
|
|
832
|
+
if (incomplete.length > 0)
|
|
833
|
+
return baseClassification(input.task, "task-blocked", "elimination", `blocked by ${incomplete.length} incomplete task(s)`, labels);
|
|
834
|
+
if (labels.includes("needs-decision"))
|
|
835
|
+
return baseClassification(input.task, "human-decision", "label", "needs-decision label", labels);
|
|
836
|
+
if (labels.some((label) => /^waiting-on-/.test(label)))
|
|
837
|
+
return baseClassification(input.task, "external-input", "label", "waiting-on-* label", labels);
|
|
838
|
+
if (input.config?.llm)
|
|
839
|
+
return baseClassification(input.task, "human-decision", "llm", "LLM residue classifier marked this as human-gated", labels);
|
|
840
|
+
return baseClassification(input.task, "human-decision", "elimination", "blocked, all task dependencies terminal; non-task gate remains", labels);
|
|
841
|
+
}
|
|
842
|
+
function classifyTasks(tasks, runs = [], options = {}) {
|
|
843
|
+
const projected = tasks.map(toTaskDependencyProjection);
|
|
844
|
+
const badges = computeTaskDependencyBadges(projected);
|
|
845
|
+
const tasksById = new Map(projected.map((task) => [task.id, task]));
|
|
846
|
+
const runByTask = latestRunByTaskId(runs);
|
|
847
|
+
const classifier = options.classifier ?? classifyBlocker;
|
|
848
|
+
const classifications = projected.map((task) => classifier({ task, badges, tasksById, run: runByTask.get(task.id) ?? null, labels: labelsFor(task) })).filter((classification) => options.humanOnly !== true || isHumanBlockerClass(classification.blockerClass));
|
|
849
|
+
return {
|
|
850
|
+
classifications,
|
|
851
|
+
byTaskId: new Map(classifications.map((classification) => [classification.taskId, classification])),
|
|
852
|
+
human: classifications.filter((classification) => isHumanBlockerClass(classification.blockerClass)),
|
|
853
|
+
machine: classifications.filter((classification) => !isHumanBlockerClass(classification.blockerClass)),
|
|
854
|
+
generatedAt: options.generatedAt ?? new Date().toISOString()
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// packages/dependency-graph-plugin/src/analysis/graph.ts
|
|
859
|
+
function overlayLayoutRunState(model, runs) {
|
|
860
|
+
if (runs.length === 0)
|
|
861
|
+
return model.layout;
|
|
862
|
+
const runsByTask = new Map;
|
|
863
|
+
for (const run of runs) {
|
|
864
|
+
if (!run.taskId)
|
|
865
|
+
continue;
|
|
866
|
+
runsByTask.set(run.taskId, [...runsByTask.get(run.taskId) ?? [], run]);
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
...model.layout,
|
|
870
|
+
nodes: model.layout.nodes.map((node) => {
|
|
871
|
+
const taskRuns = runsByTask.get(node.taskId) ?? [];
|
|
872
|
+
return {
|
|
873
|
+
...node,
|
|
874
|
+
runCount: taskRuns.length,
|
|
875
|
+
hasApprovals: taskRuns.some((run) => run.pendingApprovals > 0),
|
|
876
|
+
hasPendingUserInput: taskRuns.some((run) => run.pendingInputs > 0),
|
|
877
|
+
hasRejectedReview: taskRuns.some((run) => run.status === "reviewing" && Boolean(run.errorSummary)),
|
|
878
|
+
hasFailedValidations: taskRuns.some((run) => run.status === "failed")
|
|
879
|
+
};
|
|
880
|
+
})
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function buildWorkspaceDependencyGraph(input) {
|
|
884
|
+
const summaries = input.tasks.map((task) => toTaskSummary(task, input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}));
|
|
885
|
+
const model = (input.projectDependencyGraph ?? buildDependencyGraphModel)(summaries, {
|
|
886
|
+
...input.generatedAt !== undefined ? { generatedAt: input.generatedAt } : {},
|
|
887
|
+
...input.graphId !== undefined ? { graphId: input.graphId } : {},
|
|
888
|
+
...input.showParentChild !== undefined ? { showParentChild: input.showParentChild } : {}
|
|
889
|
+
});
|
|
890
|
+
const classifications = input.classifications ?? classifyTasks(input.tasks, input.runs ?? [], input.generatedAt !== undefined ? { generatedAt: input.generatedAt } : {}).byTaskId;
|
|
891
|
+
return {
|
|
892
|
+
...model,
|
|
893
|
+
layout: overlayLayoutRunState(model, input.runs ?? []),
|
|
894
|
+
nodes: model.nodes.map((node) => {
|
|
895
|
+
const classification = classifications.get(node.taskId);
|
|
896
|
+
return {
|
|
897
|
+
...node,
|
|
898
|
+
blockerClass: classification?.blockerClass ?? node.blockerClass,
|
|
899
|
+
actionRiskTier: classification?.actionRiskTier ?? null
|
|
900
|
+
};
|
|
901
|
+
})
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
async function getWorkspaceDependencyGraph(projectRoot, deps) {
|
|
905
|
+
const [tasks, runs, blockers] = await Promise.all([
|
|
906
|
+
deps.listTasks(projectRoot),
|
|
907
|
+
deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([]),
|
|
908
|
+
deps.classifyBlockers ? deps.classifyBlockers(projectRoot) : Promise.resolve(null)
|
|
909
|
+
]);
|
|
910
|
+
return buildWorkspaceDependencyGraph({
|
|
911
|
+
tasks,
|
|
912
|
+
runs,
|
|
913
|
+
...blockers?.byTaskId !== undefined ? { classifications: blockers.byTaskId } : {},
|
|
914
|
+
...deps.generatedAt !== undefined ? { generatedAt: deps.generatedAt } : {},
|
|
915
|
+
...deps.graphId !== undefined ? { graphId: deps.graphId } : {},
|
|
916
|
+
...deps.workspaceId !== undefined ? { workspaceId: deps.workspaceId } : {},
|
|
917
|
+
...deps.projectDependencyGraph !== undefined ? { projectDependencyGraph: deps.projectDependencyGraph } : {}
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
function formatDependencyGraphDot(model) {
|
|
921
|
+
const lines = ["digraph RigDependencies {", " rankdir=LR;"];
|
|
922
|
+
for (const node of model.nodes) {
|
|
923
|
+
const color = node.blockerClass === "task-blocked" ? "orange" : node.blockerClass && node.blockerClass !== "not-blocked" ? "red" : "gray";
|
|
924
|
+
lines.push(` "${escapeDot(node.taskId)}" [label="${escapeDot(node.title)}", color="${color}"];`);
|
|
925
|
+
}
|
|
926
|
+
for (const edge of model.edges) {
|
|
927
|
+
lines.push(` "${escapeDot(edge.fromTaskId)}" -> "${escapeDot(edge.toTaskId)}" [label="${edge.type}", color="${edgeColor(edge)}"];`);
|
|
928
|
+
}
|
|
929
|
+
lines.push("}");
|
|
930
|
+
return `${lines.join(`
|
|
931
|
+
`)}
|
|
932
|
+
`;
|
|
933
|
+
}
|
|
934
|
+
function edgeColor(edge) {
|
|
935
|
+
return edge.type === "blocks" ? "black" : edge.type === "parent-child" ? "blue" : "gray";
|
|
936
|
+
}
|
|
937
|
+
function escapeDot(value) {
|
|
938
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
939
|
+
}
|
|
940
|
+
export {
|
|
941
|
+
getWorkspaceDependencyGraph,
|
|
942
|
+
formatDependencyGraphDot,
|
|
943
|
+
buildWorkspaceDependencyGraph
|
|
944
|
+
};
|