@h-rig/dependency-graph-plugin 0.0.6-alpha.155 → 0.0.6-alpha.156
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/graph-model/dependencyGraph.d.ts +43 -0
- package/dist/src/graph-model/dependencyGraph.js +485 -0
- package/dist/src/graph-model/rollups.d.ts +6 -0
- package/dist/src/graph-model/rollups.js +166 -0
- package/dist/src/graph-model/taskGraphCodes.d.ts +3 -0
- package/dist/src/graph-model/taskGraphCodes.js +26 -0
- package/dist/src/graph-model/taskGraphLayout.d.ts +61 -0
- package/dist/src/graph-model/taskGraphLayout.js +346 -0
- package/dist/src/graph.d.ts +33 -0
- package/dist/src/graph.js +572 -0
- package/dist/src/index.js +1084 -35
- package/dist/src/plugin.d.ts +7 -2
- package/dist/src/plugin.js +1084 -35
- package/dist/src/rollups.d.ts +20 -0
- package/dist/src/rollups.js +199 -0
- package/dist/src/taskRanking.d.ts +24 -0
- package/dist/src/taskRanking.js +160 -0
- package/dist/src/taskScore.d.ts +17 -0
- package/dist/src/taskScore.js +49 -0
- package/dist/src/taskSelection.d.ts +33 -0
- package/dist/src/taskSelection.js +181 -0
- package/package.json +6 -4
package/dist/src/plugin.js
CHANGED
|
@@ -3,6 +3,1038 @@ var __require = import.meta.require;
|
|
|
3
3
|
|
|
4
4
|
// packages/dependency-graph-plugin/src/plugin.ts
|
|
5
5
|
import { definePlugin } from "@rig/core/config";
|
|
6
|
+
import { classifyWorkspaceBlockers } from "@rig/blocker-classifier-plugin";
|
|
7
|
+
|
|
8
|
+
// packages/dependency-graph-plugin/src/graph-model/dependencyGraph.ts
|
|
9
|
+
import { computeTaskDependencyBadges, readTaskMetadataStringList as readTaskMetadataStringList2, resolveTaskReference as resolveTaskReference2, buildTaskReferenceIndex as buildTaskReferenceIndex2 } from "@rig/contracts";
|
|
10
|
+
|
|
11
|
+
// packages/dependency-graph-plugin/src/graph-model/taskGraphCodes.ts
|
|
12
|
+
var TASK_CODE_RE = /^\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]\s*/;
|
|
13
|
+
function extractTaskCode(title) {
|
|
14
|
+
const match = title.match(TASK_CODE_RE);
|
|
15
|
+
return match?.[1] ?? null;
|
|
16
|
+
}
|
|
17
|
+
function extractTaskGroupKey(title) {
|
|
18
|
+
const code = extractTaskCode(title);
|
|
19
|
+
if (!code)
|
|
20
|
+
return null;
|
|
21
|
+
const parts = code.split("-");
|
|
22
|
+
const suffix = parts.at(-1) ?? "";
|
|
23
|
+
if (/^\d+$/.test(suffix)) {
|
|
24
|
+
return parts.slice(0, -1).join("-");
|
|
25
|
+
}
|
|
26
|
+
return parts[0] ?? code;
|
|
27
|
+
}
|
|
28
|
+
function stripTaskCode(label) {
|
|
29
|
+
return label.replace(TASK_CODE_RE, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// packages/dependency-graph-plugin/src/graph-model/taskGraphLayout.ts
|
|
33
|
+
import {
|
|
34
|
+
buildTaskReferenceIndex,
|
|
35
|
+
readTaskMetadataStringList,
|
|
36
|
+
resolveTaskReference
|
|
37
|
+
} from "@rig/contracts";
|
|
38
|
+
import {
|
|
39
|
+
selectPendingApprovals,
|
|
40
|
+
selectRunsByTask,
|
|
41
|
+
selectUserInputsForRun
|
|
42
|
+
} from "@rig/read-model-plugin";
|
|
43
|
+
var CARD_WIDTH = 200;
|
|
44
|
+
var CARD_HEIGHT = 110;
|
|
45
|
+
var CELL_V_PAD = 12;
|
|
46
|
+
var CELL_H_PAD = 12;
|
|
47
|
+
var ROW_GAP = 28;
|
|
48
|
+
var COL_GAP = 40;
|
|
49
|
+
var LANE_LABEL_W = 120;
|
|
50
|
+
var STAGE_HDR_H = 32;
|
|
51
|
+
var PALETTE = [
|
|
52
|
+
{ bg: "#3a2d12", border: "#8d6b19", edge: "#d6a11d" },
|
|
53
|
+
{ bg: "#102642", border: "#245fbf", edge: "#66a2ff" },
|
|
54
|
+
{ bg: "#2c173f", border: "#7b39d4", edge: "#a76df5" },
|
|
55
|
+
{ bg: "#112d1c", border: "#2d8f4e", edge: "#62d882" },
|
|
56
|
+
{ bg: "#3a2314", border: "#c86d1c", edge: "#e69654" },
|
|
57
|
+
{ bg: "#31152b", border: "#bf3d88", edge: "#f07ebb" },
|
|
58
|
+
{ bg: "#132c35", border: "#1783a6", edge: "#53c4e5" },
|
|
59
|
+
{ bg: "#26310f", border: "#6d9a19", edge: "#a7da42" }
|
|
60
|
+
];
|
|
61
|
+
function isObjectRecord(value) {
|
|
62
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
function readIssueType(task) {
|
|
65
|
+
const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
|
|
66
|
+
if (typeof metadata?.issueType === "string")
|
|
67
|
+
return metadata.issueType;
|
|
68
|
+
const raw = isObjectRecord(metadata?.raw) ? metadata.raw : null;
|
|
69
|
+
return typeof raw?.issueType === "string" ? raw.issueType : null;
|
|
70
|
+
}
|
|
71
|
+
function isGraphTask(task) {
|
|
72
|
+
return readIssueType(task) !== "epic";
|
|
73
|
+
}
|
|
74
|
+
function findEpicAncestor(task, resolve, tasksById) {
|
|
75
|
+
const visited = new Set;
|
|
76
|
+
let current = task;
|
|
77
|
+
let epic = null;
|
|
78
|
+
while (!visited.has(current.id)) {
|
|
79
|
+
visited.add(current.id);
|
|
80
|
+
const parentRef = readTaskMetadataStringList(current, "parentChildDeps")[0];
|
|
81
|
+
if (!parentRef)
|
|
82
|
+
break;
|
|
83
|
+
const parentId = resolve(parentRef);
|
|
84
|
+
if (!parentId)
|
|
85
|
+
break;
|
|
86
|
+
const parent = tasksById.get(parentId);
|
|
87
|
+
if (!parent)
|
|
88
|
+
break;
|
|
89
|
+
if (readIssueType(parent) === "epic") {
|
|
90
|
+
epic = parent;
|
|
91
|
+
}
|
|
92
|
+
current = parent;
|
|
93
|
+
}
|
|
94
|
+
return epic;
|
|
95
|
+
}
|
|
96
|
+
function getRowKey(task, resolve, tasksById) {
|
|
97
|
+
const epic = findEpicAncestor(task, resolve, tasksById);
|
|
98
|
+
if (epic) {
|
|
99
|
+
return `group:${epic.id}`;
|
|
100
|
+
}
|
|
101
|
+
const codeGroup = extractTaskGroupKey(task.title);
|
|
102
|
+
if (codeGroup) {
|
|
103
|
+
return `code:${codeGroup}`;
|
|
104
|
+
}
|
|
105
|
+
return `type:${readIssueType(task) ?? "task"}`;
|
|
106
|
+
}
|
|
107
|
+
function getRowLabel(task, rowKey, resolve, tasksById) {
|
|
108
|
+
if (rowKey.startsWith("group:")) {
|
|
109
|
+
const groupId = rowKey.slice("group:".length);
|
|
110
|
+
return tasksById.get(groupId)?.title ?? groupId;
|
|
111
|
+
}
|
|
112
|
+
if (rowKey.startsWith("code:")) {
|
|
113
|
+
return rowKey.slice("code:".length);
|
|
114
|
+
}
|
|
115
|
+
const code = extractTaskCode(task.title);
|
|
116
|
+
if (code)
|
|
117
|
+
return code;
|
|
118
|
+
const issueType = readIssueType(task);
|
|
119
|
+
if (issueType === "task")
|
|
120
|
+
return "Tasks";
|
|
121
|
+
if (issueType)
|
|
122
|
+
return `${issueType[0]?.toUpperCase() ?? ""}${issueType.slice(1)}`;
|
|
123
|
+
const parentRef = readTaskMetadataStringList(task, "parentChildDeps")[0];
|
|
124
|
+
if (parentRef) {
|
|
125
|
+
const parentId = resolve(parentRef);
|
|
126
|
+
if (parentId) {
|
|
127
|
+
return tasksById.get(parentId)?.title ?? "Tasks";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return "Tasks";
|
|
131
|
+
}
|
|
132
|
+
function computeDepths(ids, edges) {
|
|
133
|
+
const blockers = new Map;
|
|
134
|
+
for (const edge of edges) {
|
|
135
|
+
if (!ids.has(edge.source) || !ids.has(edge.target))
|
|
136
|
+
continue;
|
|
137
|
+
const current = blockers.get(edge.target) ?? [];
|
|
138
|
+
current.push(edge.source);
|
|
139
|
+
blockers.set(edge.target, current);
|
|
140
|
+
}
|
|
141
|
+
const memo = new Map;
|
|
142
|
+
const visit = (id, stack) => {
|
|
143
|
+
const cached = memo.get(id);
|
|
144
|
+
if (cached !== undefined)
|
|
145
|
+
return cached;
|
|
146
|
+
if (stack.has(id))
|
|
147
|
+
return 0;
|
|
148
|
+
stack.add(id);
|
|
149
|
+
const deps = blockers.get(id);
|
|
150
|
+
if (!deps || deps.length === 0) {
|
|
151
|
+
memo.set(id, 0);
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
let maxDepth = 0;
|
|
155
|
+
for (const dep of deps) {
|
|
156
|
+
maxDepth = Math.max(maxDepth, visit(dep, stack) + 1);
|
|
157
|
+
}
|
|
158
|
+
stack.delete(id);
|
|
159
|
+
memo.set(id, maxDepth);
|
|
160
|
+
return maxDepth;
|
|
161
|
+
};
|
|
162
|
+
for (const id of ids) {
|
|
163
|
+
visit(id, new Set);
|
|
164
|
+
}
|
|
165
|
+
return memo;
|
|
166
|
+
}
|
|
167
|
+
function buildTaskGraphLayout(snapshot, tasks, options) {
|
|
168
|
+
const showParentChild = options?.showParentChild ?? false;
|
|
169
|
+
const graphTasks = tasks.filter(isGraphTask);
|
|
170
|
+
if (graphTasks.length === 0) {
|
|
171
|
+
return {
|
|
172
|
+
lanes: [],
|
|
173
|
+
stages: [],
|
|
174
|
+
nodes: [],
|
|
175
|
+
edges: [],
|
|
176
|
+
totalWidth: 0,
|
|
177
|
+
totalHeight: 0,
|
|
178
|
+
taskCount: 0
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
|
|
182
|
+
const resolve = (ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId);
|
|
183
|
+
const rows = new Map;
|
|
184
|
+
const rowLabelByKey = new Map;
|
|
185
|
+
for (const task of graphTasks) {
|
|
186
|
+
const rowKey = getRowKey(task, resolve, tasksById);
|
|
187
|
+
const current = rows.get(rowKey) ?? [];
|
|
188
|
+
current.push(task);
|
|
189
|
+
rows.set(rowKey, current);
|
|
190
|
+
if (!rowLabelByKey.has(rowKey)) {
|
|
191
|
+
rowLabelByKey.set(rowKey, getRowLabel(task, rowKey, resolve, tasksById));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const orderedRows = [...rows.entries()].sort((left, right) => {
|
|
195
|
+
if (left[1].length !== right[1].length)
|
|
196
|
+
return right[1].length - left[1].length;
|
|
197
|
+
return (rowLabelByKey.get(left[0]) ?? left[0]).localeCompare(rowLabelByKey.get(right[0]) ?? right[0]);
|
|
198
|
+
});
|
|
199
|
+
const rowIndexByKey = new Map(orderedRows.map(([key], index) => [key, index]));
|
|
200
|
+
const rowIndexByTaskId = new Map;
|
|
201
|
+
for (const task of graphTasks) {
|
|
202
|
+
rowIndexByTaskId.set(task.id, rowIndexByKey.get(getRowKey(task, resolve, tasksById)) ?? 0);
|
|
203
|
+
}
|
|
204
|
+
const blockingEdgesRaw = [];
|
|
205
|
+
const depsIn = new Map;
|
|
206
|
+
const depsOut = new Map;
|
|
207
|
+
const edges = [];
|
|
208
|
+
for (const task of graphTasks) {
|
|
209
|
+
for (const ref of readTaskMetadataStringList(task, "dependencies")) {
|
|
210
|
+
const sourceId = resolve(ref);
|
|
211
|
+
if (!sourceId)
|
|
212
|
+
continue;
|
|
213
|
+
blockingEdgesRaw.push({ source: sourceId, target: task.id });
|
|
214
|
+
depsOut.set(sourceId, (depsOut.get(sourceId) ?? 0) + 1);
|
|
215
|
+
depsIn.set(task.id, (depsIn.get(task.id) ?? 0) + 1);
|
|
216
|
+
const color = PALETTE[(rowIndexByTaskId.get(sourceId) ?? 0) % PALETTE.length].edge;
|
|
217
|
+
edges.push({
|
|
218
|
+
id: `blocking:${sourceId}:${task.id}`,
|
|
219
|
+
sourceId,
|
|
220
|
+
targetId: task.id,
|
|
221
|
+
color,
|
|
222
|
+
kind: "blocking"
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (!showParentChild)
|
|
226
|
+
continue;
|
|
227
|
+
for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
|
|
228
|
+
const sourceId = resolve(ref);
|
|
229
|
+
if (!sourceId)
|
|
230
|
+
continue;
|
|
231
|
+
edges.push({
|
|
232
|
+
id: `parent:${sourceId}:${task.id}`,
|
|
233
|
+
sourceId,
|
|
234
|
+
targetId: task.id,
|
|
235
|
+
color: "#5b6472",
|
|
236
|
+
kind: "parent-child"
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const graphTaskIds = new Set(graphTasks.map((task) => task.id));
|
|
241
|
+
const depths = computeDepths(graphTaskIds, blockingEdgesRaw);
|
|
242
|
+
const maxStage = Math.max(0, ...depths.values());
|
|
243
|
+
const rowMaxStack = new Array(orderedRows.length).fill(0);
|
|
244
|
+
const cells = new Map;
|
|
245
|
+
for (const task of graphTasks) {
|
|
246
|
+
const rowIndex = rowIndexByTaskId.get(task.id) ?? 0;
|
|
247
|
+
const stage = depths.get(task.id) ?? 0;
|
|
248
|
+
const key = `${rowIndex}:${stage}`;
|
|
249
|
+
const current = cells.get(key) ?? [];
|
|
250
|
+
current.push(task);
|
|
251
|
+
cells.set(key, current);
|
|
252
|
+
}
|
|
253
|
+
for (const [cellKey, cellTasks] of cells) {
|
|
254
|
+
const [rowIndexText] = cellKey.split(":");
|
|
255
|
+
const rowIndex = Number.parseInt(rowIndexText ?? "0", 10) || 0;
|
|
256
|
+
cellTasks.sort((left, right) => {
|
|
257
|
+
const leftFanout = depsOut.get(left.id) ?? 0;
|
|
258
|
+
const rightFanout = depsOut.get(right.id) ?? 0;
|
|
259
|
+
if (leftFanout !== rightFanout)
|
|
260
|
+
return rightFanout - leftFanout;
|
|
261
|
+
return left.title.localeCompare(right.title);
|
|
262
|
+
});
|
|
263
|
+
rowMaxStack[rowIndex] = Math.max(rowMaxStack[rowIndex] ?? 0, cellTasks.length);
|
|
264
|
+
}
|
|
265
|
+
const rowHeights = rowMaxStack.map((count) => Math.max(count, 1) * (CARD_HEIGHT + CELL_V_PAD) - CELL_V_PAD + CELL_V_PAD * 2);
|
|
266
|
+
const colWidths = new Array(maxStage + 1).fill(CARD_WIDTH + CELL_H_PAD * 2);
|
|
267
|
+
const colX = [];
|
|
268
|
+
let currentX = LANE_LABEL_W;
|
|
269
|
+
for (let index = 0;index <= maxStage; index += 1) {
|
|
270
|
+
colX.push(currentX);
|
|
271
|
+
currentX += (colWidths[index] ?? 0) + COL_GAP;
|
|
272
|
+
}
|
|
273
|
+
const totalWidth = currentX - COL_GAP;
|
|
274
|
+
const rowY = [];
|
|
275
|
+
let currentY = STAGE_HDR_H;
|
|
276
|
+
for (let rowIndex = 0;rowIndex < orderedRows.length; rowIndex += 1) {
|
|
277
|
+
rowY.push(currentY);
|
|
278
|
+
currentY += (rowHeights[rowIndex] ?? 0) + ROW_GAP;
|
|
279
|
+
}
|
|
280
|
+
const totalHeight = currentY - ROW_GAP;
|
|
281
|
+
const lanes = orderedRows.map(([rowKey, rowTasks], rowIndex) => ({
|
|
282
|
+
key: rowKey,
|
|
283
|
+
label: rowLabelByKey.get(rowKey) ?? rowKey,
|
|
284
|
+
rowIndex,
|
|
285
|
+
x: LANE_LABEL_W - 6,
|
|
286
|
+
y: (rowY[rowIndex] ?? 0) - 6,
|
|
287
|
+
width: totalWidth - LANE_LABEL_W + 12,
|
|
288
|
+
height: (rowHeights[rowIndex] ?? 0) + 12,
|
|
289
|
+
color: PALETTE[rowIndex % PALETTE.length].border,
|
|
290
|
+
taskCount: rowTasks.length
|
|
291
|
+
}));
|
|
292
|
+
const stages = Array.from({ length: maxStage + 1 }, (_, index) => ({
|
|
293
|
+
index,
|
|
294
|
+
label: index === 0 ? "Roots" : `Stage ${index}`,
|
|
295
|
+
x: (colX[index] ?? 0) + CELL_H_PAD,
|
|
296
|
+
width: CARD_WIDTH
|
|
297
|
+
}));
|
|
298
|
+
const pendingApprovalRunIds = new Set(selectPendingApprovals(snapshot).map((approval) => approval.runId));
|
|
299
|
+
const nodes = [];
|
|
300
|
+
for (const [rowIndex, [rowKey]] of orderedRows.entries()) {
|
|
301
|
+
for (let stage = 0;stage <= maxStage; stage += 1) {
|
|
302
|
+
const cellTasks = cells.get(`${rowIndex}:${stage}`) ?? [];
|
|
303
|
+
const baseX = (colX[stage] ?? 0) + CELL_H_PAD;
|
|
304
|
+
const baseY = (rowY[rowIndex] ?? 0) + CELL_V_PAD;
|
|
305
|
+
const palette = PALETTE[rowIndex % PALETTE.length];
|
|
306
|
+
for (const [stackIndex, task] of cellTasks.entries()) {
|
|
307
|
+
const runs = selectRunsByTask(snapshot, task.id);
|
|
308
|
+
const runIds = new Set(runs.map((run) => run.id));
|
|
309
|
+
const hasApprovals = runs.some((run) => pendingApprovalRunIds.has(run.id));
|
|
310
|
+
const hasPendingUserInput = runs.some((run) => selectUserInputsForRun(snapshot, run.id).some((request) => request.status === "pending"));
|
|
311
|
+
const hasRejectedReview = (snapshot?.reviews ?? []).some((review) => runIds.has(review.runId) && review.status === "rejected");
|
|
312
|
+
const hasFailedValidations = (snapshot?.validations ?? []).some((validation) => runIds.has(validation.runId) && validation.status === "failed");
|
|
313
|
+
const artifactCount = (snapshot?.artifacts ?? []).filter((artifact) => runIds.has(artifact.runId)).length;
|
|
314
|
+
nodes.push({
|
|
315
|
+
id: task.id,
|
|
316
|
+
taskId: task.id,
|
|
317
|
+
task,
|
|
318
|
+
rowKey,
|
|
319
|
+
rowLabel: rowLabelByKey.get(rowKey) ?? rowKey,
|
|
320
|
+
rowIndex,
|
|
321
|
+
stage,
|
|
322
|
+
x: baseX,
|
|
323
|
+
y: baseY + stackIndex * (CARD_HEIGHT + CELL_V_PAD),
|
|
324
|
+
width: CARD_WIDTH,
|
|
325
|
+
height: CARD_HEIGHT,
|
|
326
|
+
color: palette.border,
|
|
327
|
+
taskCode: extractTaskCode(task.title),
|
|
328
|
+
strippedTitle: stripTaskCode(task.title),
|
|
329
|
+
depsIn: depsIn.get(task.id) ?? 0,
|
|
330
|
+
depsOut: depsOut.get(task.id) ?? 0,
|
|
331
|
+
runCount: runs.length,
|
|
332
|
+
hasApprovals,
|
|
333
|
+
hasPendingUserInput,
|
|
334
|
+
hasRejectedReview,
|
|
335
|
+
hasFailedValidations,
|
|
336
|
+
artifactCount
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
lanes,
|
|
343
|
+
stages,
|
|
344
|
+
nodes,
|
|
345
|
+
edges,
|
|
346
|
+
totalWidth,
|
|
347
|
+
totalHeight,
|
|
348
|
+
taskCount: graphTasks.length
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// packages/dependency-graph-plugin/src/graph-model/dependencyGraph.ts
|
|
353
|
+
import { readTaskAssigneeLogins } from "@rig/read-model-plugin";
|
|
354
|
+
function uniqueSorted(values) {
|
|
355
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
356
|
+
}
|
|
357
|
+
function deriveGraphId(tasks) {
|
|
358
|
+
const basis = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right)).join("|");
|
|
359
|
+
let hash = 2166136261;
|
|
360
|
+
for (let index = 0;index < basis.length; index += 1) {
|
|
361
|
+
hash ^= basis.charCodeAt(index);
|
|
362
|
+
hash = Math.imul(hash, 16777619);
|
|
363
|
+
}
|
|
364
|
+
return `graph-${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
365
|
+
}
|
|
366
|
+
function dedupeEdges(edges) {
|
|
367
|
+
const byKey = new Map;
|
|
368
|
+
for (const edge of edges) {
|
|
369
|
+
byKey.set(`${edge.type}\x00${edge.fromTaskId}\x00${edge.toTaskId}`, edge);
|
|
370
|
+
}
|
|
371
|
+
return [...byKey.values()].toSorted((left, right) => {
|
|
372
|
+
const typeDelta = left.type.localeCompare(right.type);
|
|
373
|
+
if (typeDelta !== 0)
|
|
374
|
+
return typeDelta;
|
|
375
|
+
const fromDelta = left.fromTaskId.localeCompare(right.fromTaskId);
|
|
376
|
+
if (fromDelta !== 0)
|
|
377
|
+
return fromDelta;
|
|
378
|
+
return left.toTaskId.localeCompare(right.toTaskId);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function detectBlockingCycles(tasks, edges) {
|
|
382
|
+
const taskIds = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right));
|
|
383
|
+
const adjacency = new Map;
|
|
384
|
+
for (const taskId of taskIds)
|
|
385
|
+
adjacency.set(taskId, []);
|
|
386
|
+
for (const edge of edges) {
|
|
387
|
+
if (edge.type === "blocks")
|
|
388
|
+
adjacency.get(edge.fromTaskId)?.push(edge.toTaskId);
|
|
389
|
+
}
|
|
390
|
+
for (const targets of adjacency.values())
|
|
391
|
+
targets.sort((left, right) => left.localeCompare(right));
|
|
392
|
+
let nextIndex = 0;
|
|
393
|
+
const indexByNode = new Map;
|
|
394
|
+
const lowByNode = new Map;
|
|
395
|
+
const stack = [];
|
|
396
|
+
const onStack = new Set;
|
|
397
|
+
const cycles = [];
|
|
398
|
+
const visit = (node) => {
|
|
399
|
+
indexByNode.set(node, nextIndex);
|
|
400
|
+
lowByNode.set(node, nextIndex);
|
|
401
|
+
nextIndex += 1;
|
|
402
|
+
stack.push(node);
|
|
403
|
+
onStack.add(node);
|
|
404
|
+
for (const target of adjacency.get(node) ?? []) {
|
|
405
|
+
if (!indexByNode.has(target)) {
|
|
406
|
+
visit(target);
|
|
407
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
|
|
408
|
+
} else if (onStack.has(target)) {
|
|
409
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (lowByNode.get(node) !== indexByNode.get(node))
|
|
413
|
+
return;
|
|
414
|
+
const component = [];
|
|
415
|
+
let current;
|
|
416
|
+
do {
|
|
417
|
+
current = stack.pop();
|
|
418
|
+
if (current) {
|
|
419
|
+
onStack.delete(current);
|
|
420
|
+
component.push(current);
|
|
421
|
+
}
|
|
422
|
+
} while (current && current !== node);
|
|
423
|
+
const selfLoop = (adjacency.get(node) ?? []).includes(node);
|
|
424
|
+
if (component.length > 1 || selfLoop)
|
|
425
|
+
cycles.push(component.sort((left, right) => left.localeCompare(right)));
|
|
426
|
+
};
|
|
427
|
+
for (const taskId of taskIds) {
|
|
428
|
+
if (!indexByNode.has(taskId))
|
|
429
|
+
visit(taskId);
|
|
430
|
+
}
|
|
431
|
+
return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
|
|
432
|
+
}
|
|
433
|
+
function buildDependencyGraphModel(tasks, options = {}) {
|
|
434
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
435
|
+
const index = buildTaskReferenceIndex2(tasks);
|
|
436
|
+
const edges = [];
|
|
437
|
+
const unresolvedRefs = [];
|
|
438
|
+
for (const task of tasks) {
|
|
439
|
+
for (const ref of readTaskMetadataStringList2(task, "dependencies")) {
|
|
440
|
+
const dependencyId = resolveTaskReference2(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
441
|
+
if (dependencyId)
|
|
442
|
+
edges.push({ fromTaskId: dependencyId, toTaskId: String(task.id), type: "blocks" });
|
|
443
|
+
else
|
|
444
|
+
unresolvedRefs.push(ref);
|
|
445
|
+
}
|
|
446
|
+
for (const ref of readTaskMetadataStringList2(task, "parentChildDeps")) {
|
|
447
|
+
const parentId = resolveTaskReference2(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
448
|
+
if (parentId)
|
|
449
|
+
edges.push({ fromTaskId: parentId, toTaskId: String(task.id), type: "parent-child" });
|
|
450
|
+
else
|
|
451
|
+
unresolvedRefs.push(ref);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const dedupedEdges = dedupeEdges(edges);
|
|
455
|
+
const nodes = tasks.map((task) => {
|
|
456
|
+
const summary = badges.get(task.id);
|
|
457
|
+
const assignees = readTaskAssigneeLogins(task);
|
|
458
|
+
const groupKey = extractTaskGroupKey(task.title);
|
|
459
|
+
return {
|
|
460
|
+
taskId: String(task.id),
|
|
461
|
+
title: task.title,
|
|
462
|
+
status: task.status,
|
|
463
|
+
priority: task.priority,
|
|
464
|
+
assignee: assignees[0] ?? null,
|
|
465
|
+
blockedBy: summary?.blockedBy ?? [],
|
|
466
|
+
blocks: summary?.blocks ?? [],
|
|
467
|
+
blockingDepth: summary?.blockingDepth ?? 0,
|
|
468
|
+
blockerClass: null,
|
|
469
|
+
actionRiskTier: null,
|
|
470
|
+
epicKey: groupKey,
|
|
471
|
+
groupKey,
|
|
472
|
+
externalId: task.externalId,
|
|
473
|
+
sourceIssueId: task.sourceIssueId ?? null,
|
|
474
|
+
scope: task.scope,
|
|
475
|
+
validationKeys: task.validationKeys
|
|
476
|
+
};
|
|
477
|
+
}).toSorted((left, right) => left.taskId.localeCompare(right.taskId));
|
|
478
|
+
return {
|
|
479
|
+
graphId: options.graphId ?? deriveGraphId(tasks),
|
|
480
|
+
nodes,
|
|
481
|
+
edges: dedupedEdges,
|
|
482
|
+
layout: buildTaskGraphLayout(options.snapshot ?? null, tasks, { showParentChild: options.showParentChild ?? true }),
|
|
483
|
+
cycles: detectBlockingCycles(tasks, dedupedEdges),
|
|
484
|
+
unresolvedRefs: uniqueSorted(unresolvedRefs),
|
|
485
|
+
degraded: false,
|
|
486
|
+
generatedAt: options.generatedAt ?? new Date().toISOString()
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// packages/dependency-graph-plugin/src/graph.ts
|
|
491
|
+
import { toTaskSummary } from "@rig/contracts";
|
|
492
|
+
import { classifyTasks } from "@rig/blocker-classifier-plugin";
|
|
493
|
+
function overlayLayoutRunState(model, runs) {
|
|
494
|
+
if (runs.length === 0)
|
|
495
|
+
return model.layout;
|
|
496
|
+
const runsByTask = new Map;
|
|
497
|
+
for (const run of runs) {
|
|
498
|
+
if (!run.taskId)
|
|
499
|
+
continue;
|
|
500
|
+
runsByTask.set(run.taskId, [...runsByTask.get(run.taskId) ?? [], run]);
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
...model.layout,
|
|
504
|
+
nodes: model.layout.nodes.map((node) => {
|
|
505
|
+
const taskRuns = runsByTask.get(node.taskId) ?? [];
|
|
506
|
+
return {
|
|
507
|
+
...node,
|
|
508
|
+
runCount: taskRuns.length,
|
|
509
|
+
hasApprovals: taskRuns.some((run) => run.pendingApprovals > 0),
|
|
510
|
+
hasPendingUserInput: taskRuns.some((run) => run.pendingInputs > 0),
|
|
511
|
+
hasRejectedReview: taskRuns.some((run) => run.status === "reviewing" && Boolean(run.errorSummary)),
|
|
512
|
+
hasFailedValidations: taskRuns.some((run) => run.status === "failed")
|
|
513
|
+
};
|
|
514
|
+
})
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function buildWorkspaceDependencyGraph(input) {
|
|
518
|
+
const summaries = input.tasks.map((task) => toTaskSummary(task, input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}));
|
|
519
|
+
const model = (input.projectDependencyGraph ?? buildDependencyGraphModel)(summaries, {
|
|
520
|
+
...input.generatedAt !== undefined ? { generatedAt: input.generatedAt } : {},
|
|
521
|
+
...input.graphId !== undefined ? { graphId: input.graphId } : {},
|
|
522
|
+
...input.showParentChild !== undefined ? { showParentChild: input.showParentChild } : {}
|
|
523
|
+
});
|
|
524
|
+
const classifications = input.classifications ?? classifyTasks(input.tasks, input.runs ?? [], input.generatedAt !== undefined ? { generatedAt: input.generatedAt } : {}).byTaskId;
|
|
525
|
+
return {
|
|
526
|
+
...model,
|
|
527
|
+
layout: overlayLayoutRunState(model, input.runs ?? []),
|
|
528
|
+
nodes: model.nodes.map((node) => {
|
|
529
|
+
const classification = classifications.get(node.taskId);
|
|
530
|
+
return {
|
|
531
|
+
...node,
|
|
532
|
+
blockerClass: classification?.blockerClass ?? node.blockerClass,
|
|
533
|
+
actionRiskTier: classification?.actionRiskTier ?? null
|
|
534
|
+
};
|
|
535
|
+
})
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
async function getWorkspaceDependencyGraph(projectRoot, deps) {
|
|
539
|
+
const [tasks, runs, blockers] = await Promise.all([
|
|
540
|
+
deps.listTasks(projectRoot),
|
|
541
|
+
deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([]),
|
|
542
|
+
deps.classifyBlockers ? deps.classifyBlockers(projectRoot) : Promise.resolve(null)
|
|
543
|
+
]);
|
|
544
|
+
return buildWorkspaceDependencyGraph({
|
|
545
|
+
tasks,
|
|
546
|
+
runs,
|
|
547
|
+
...blockers?.byTaskId !== undefined ? { classifications: blockers.byTaskId } : {},
|
|
548
|
+
...deps.generatedAt !== undefined ? { generatedAt: deps.generatedAt } : {},
|
|
549
|
+
...deps.graphId !== undefined ? { graphId: deps.graphId } : {},
|
|
550
|
+
...deps.workspaceId !== undefined ? { workspaceId: deps.workspaceId } : {},
|
|
551
|
+
...deps.projectDependencyGraph !== undefined ? { projectDependencyGraph: deps.projectDependencyGraph } : {}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
function formatDependencyGraphDot(model) {
|
|
555
|
+
const lines = ["digraph RigDependencies {", " rankdir=LR;"];
|
|
556
|
+
for (const node of model.nodes) {
|
|
557
|
+
const color = node.blockerClass === "task-blocked" ? "orange" : node.blockerClass && node.blockerClass !== "not-blocked" ? "red" : "gray";
|
|
558
|
+
lines.push(` "${escapeDot(node.taskId)}" [label="${escapeDot(node.title)}", color="${color}"];`);
|
|
559
|
+
}
|
|
560
|
+
for (const edge of model.edges) {
|
|
561
|
+
lines.push(` "${escapeDot(edge.fromTaskId)}" -> "${escapeDot(edge.toTaskId)}" [label="${edge.type}", color="${edgeColor(edge)}"];`);
|
|
562
|
+
}
|
|
563
|
+
lines.push("}");
|
|
564
|
+
return `${lines.join(`
|
|
565
|
+
`)}
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
function edgeColor(edge) {
|
|
569
|
+
return edge.type === "blocks" ? "black" : edge.type === "parent-child" ? "blue" : "gray";
|
|
570
|
+
}
|
|
571
|
+
function escapeDot(value) {
|
|
572
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// packages/dependency-graph-plugin/src/graph-model/rollups.ts
|
|
576
|
+
import {
|
|
577
|
+
isOperatorActiveRunStatus,
|
|
578
|
+
buildTaskReferenceIndex as buildTaskReferenceIndex3,
|
|
579
|
+
computeTaskDependencyBadges as computeTaskDependencyBadges2,
|
|
580
|
+
isTaskTerminalStatus,
|
|
581
|
+
readTaskMetadataStringList as readTaskMetadataStringList3,
|
|
582
|
+
resolveTaskReference as resolveTaskReference3
|
|
583
|
+
} from "@rig/contracts";
|
|
584
|
+
import { readTaskAssigneeLogins as readTaskAssigneeLogins2 } from "@rig/read-model-plugin";
|
|
585
|
+
var UNASSIGNED_EPIC = "(unassigned-epic)";
|
|
586
|
+
var UNASSIGNED_ASSIGNEE = "(unassigned)";
|
|
587
|
+
var HUMAN_BLOCKER_CLASSES = {
|
|
588
|
+
"human-decision": true,
|
|
589
|
+
"human-approval": true,
|
|
590
|
+
"external-input": true,
|
|
591
|
+
unknown: true
|
|
592
|
+
};
|
|
593
|
+
function isObjectRecord2(value) {
|
|
594
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
595
|
+
}
|
|
596
|
+
function readIssueType2(task) {
|
|
597
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
598
|
+
const raw = isObjectRecord2(metadata?.raw) ? metadata.raw : null;
|
|
599
|
+
const value = raw?.issueType ?? metadata?.issueType;
|
|
600
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
|
|
601
|
+
}
|
|
602
|
+
function isEpicTask(task) {
|
|
603
|
+
return readIssueType2(task) === "epic";
|
|
604
|
+
}
|
|
605
|
+
function isInFlightTaskStatus(status) {
|
|
606
|
+
switch (status) {
|
|
607
|
+
case "queued":
|
|
608
|
+
case "running":
|
|
609
|
+
case "in_progress":
|
|
610
|
+
case "under_review":
|
|
611
|
+
return true;
|
|
612
|
+
default:
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function epicKeyForTask(task, tasksById, resolve) {
|
|
617
|
+
const parentRef = readTaskMetadataStringList3(task, "parentChildDeps")[0];
|
|
618
|
+
const parentId = parentRef ? resolve(parentRef) : null;
|
|
619
|
+
const parent = parentId ? tasksById.get(parentId) : null;
|
|
620
|
+
if (parent)
|
|
621
|
+
return extractTaskGroupKey(parent.title) ?? parent.title ?? parent.id;
|
|
622
|
+
return extractTaskGroupKey(task.title) ?? UNASSIGNED_EPIC;
|
|
623
|
+
}
|
|
624
|
+
function rollupByEpic(tasks, classifications = new Map) {
|
|
625
|
+
const index = buildTaskReferenceIndex3(tasks);
|
|
626
|
+
const badges = computeTaskDependencyBadges2(tasks);
|
|
627
|
+
const resolve = (ref) => resolveTaskReference3(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
628
|
+
const buckets = new Map;
|
|
629
|
+
for (const task of tasks) {
|
|
630
|
+
if (isEpicTask(task))
|
|
631
|
+
continue;
|
|
632
|
+
const epicKey = epicKeyForTask(task, index.tasksById, resolve);
|
|
633
|
+
const current = buckets.get(epicKey) ?? {
|
|
634
|
+
total: 0,
|
|
635
|
+
completed: 0,
|
|
636
|
+
blockedCount: 0,
|
|
637
|
+
humanBlockedCount: 0,
|
|
638
|
+
inFlightCount: 0,
|
|
639
|
+
byStatus: {}
|
|
640
|
+
};
|
|
641
|
+
current.total += 1;
|
|
642
|
+
if (isTaskTerminalStatus(task.status))
|
|
643
|
+
current.completed += 1;
|
|
644
|
+
if (badges.get(task.id)?.blocked === true)
|
|
645
|
+
current.blockedCount += 1;
|
|
646
|
+
if (isInFlightTaskStatus(task.status))
|
|
647
|
+
current.inFlightCount += 1;
|
|
648
|
+
const classification = classifications.get(task.id);
|
|
649
|
+
if (classification && HUMAN_BLOCKER_CLASSES[classification])
|
|
650
|
+
current.humanBlockedCount += 1;
|
|
651
|
+
current.byStatus[task.status] = (current.byStatus[task.status] ?? 0) + 1;
|
|
652
|
+
buckets.set(epicKey, current);
|
|
653
|
+
}
|
|
654
|
+
return [...buckets.entries()].map(([epicKey, bucket]) => ({
|
|
655
|
+
epicKey,
|
|
656
|
+
total: bucket.total,
|
|
657
|
+
percentComplete: bucket.total === 0 ? 0 : Math.round(100 * bucket.completed / bucket.total),
|
|
658
|
+
blockedCount: bucket.blockedCount,
|
|
659
|
+
humanBlockedCount: bucket.humanBlockedCount,
|
|
660
|
+
inFlightCount: bucket.inFlightCount,
|
|
661
|
+
byStatus: Object.fromEntries(Object.entries(bucket.byStatus).sort(([left], [right]) => left.localeCompare(right)))
|
|
662
|
+
})).toSorted((left, right) => left.epicKey.localeCompare(right.epicKey));
|
|
663
|
+
}
|
|
664
|
+
function assigneesForTask(task) {
|
|
665
|
+
const assignees = readTaskAssigneeLogins2(task);
|
|
666
|
+
return assignees.length > 0 ? assignees : [UNASSIGNED_ASSIGNEE];
|
|
667
|
+
}
|
|
668
|
+
function rollupByAssignee(tasks, runs) {
|
|
669
|
+
const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
|
|
670
|
+
const badges = computeTaskDependencyBadges2(tasks);
|
|
671
|
+
const buckets = new Map;
|
|
672
|
+
const ensureBucket = (assignee) => {
|
|
673
|
+
const existing = buckets.get(assignee);
|
|
674
|
+
if (existing)
|
|
675
|
+
return existing;
|
|
676
|
+
const created = { openTaskCount: 0, inFlightRunIds: new Set, prsAwaitingReview: 0, blockers: new Set };
|
|
677
|
+
buckets.set(assignee, created);
|
|
678
|
+
return created;
|
|
679
|
+
};
|
|
680
|
+
for (const task of tasks) {
|
|
681
|
+
if (isEpicTask(task))
|
|
682
|
+
continue;
|
|
683
|
+
for (const assignee of assigneesForTask(task)) {
|
|
684
|
+
const bucket = ensureBucket(assignee);
|
|
685
|
+
if (!isTaskTerminalStatus(task.status))
|
|
686
|
+
bucket.openTaskCount += 1;
|
|
687
|
+
if (badges.get(task.id)?.blocked === true)
|
|
688
|
+
bucket.blockers.add(task.id);
|
|
689
|
+
if (task.status === "under_review")
|
|
690
|
+
bucket.prsAwaitingReview += 1;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
for (const run of runs) {
|
|
694
|
+
const taskId = run.record.taskId;
|
|
695
|
+
const task = taskId ? tasksById.get(taskId) : null;
|
|
696
|
+
if (!task)
|
|
697
|
+
continue;
|
|
698
|
+
if (isEpicTask(task))
|
|
699
|
+
continue;
|
|
700
|
+
for (const assignee of assigneesForTask(task)) {
|
|
701
|
+
const bucket = ensureBucket(assignee);
|
|
702
|
+
if (run.status && isOperatorActiveRunStatus(run.status))
|
|
703
|
+
bucket.inFlightRunIds.add(run.record.runId ?? `${String(task.id)}:${run.lastSeq}`);
|
|
704
|
+
if (run.status === "reviewing" && run.record.prUrl)
|
|
705
|
+
bucket.prsAwaitingReview += 1;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return [...buckets.entries()].map(([assignee, bucket]) => ({
|
|
709
|
+
assignee,
|
|
710
|
+
openTaskCount: bucket.openTaskCount,
|
|
711
|
+
inFlightRunCount: bucket.inFlightRunIds.size,
|
|
712
|
+
prsAwaitingReview: bucket.prsAwaitingReview,
|
|
713
|
+
blockers: [...bucket.blockers].toSorted((left, right) => String(left).localeCompare(String(right)))
|
|
714
|
+
})).toSorted((left, right) => left.assignee.localeCompare(right.assignee));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// packages/dependency-graph-plugin/src/rollups.ts
|
|
718
|
+
import { toTaskSummary as toTaskSummary2 } from "@rig/contracts";
|
|
719
|
+
import { classifyTasks as classifyTasks2 } from "@rig/blocker-classifier-plugin";
|
|
720
|
+
function blockerClassMap(classifications) {
|
|
721
|
+
return new Map([...classifications.entries()].map(([taskId, classification]) => [taskId, classification.blockerClass]));
|
|
722
|
+
}
|
|
723
|
+
function selectWorkspaceRollups(input) {
|
|
724
|
+
const tasks = input.tasks.map((task) => toTaskSummary2(task, input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}));
|
|
725
|
+
const runs = input.runs ?? [];
|
|
726
|
+
const classifications = input.classifications ?? classifyTasks2(input.tasks, runs, input.generatedAt !== undefined ? { generatedAt: input.generatedAt } : {}).byTaskId;
|
|
727
|
+
return {
|
|
728
|
+
epics: [...rollupByEpic(tasks, blockerClassMap(classifications))],
|
|
729
|
+
assignees: [...rollupByAssignee(tasks, runs.map((run) => run.projection))],
|
|
730
|
+
generatedAt: input.generatedAt ?? new Date().toISOString()
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
async function getWorkspaceRollups(projectRoot, deps) {
|
|
734
|
+
const [tasks, runs, blockers] = await Promise.all([
|
|
735
|
+
deps.listTasks(projectRoot),
|
|
736
|
+
deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([]),
|
|
737
|
+
deps.classifyBlockers ? deps.classifyBlockers(projectRoot) : Promise.resolve(null)
|
|
738
|
+
]);
|
|
739
|
+
return selectWorkspaceRollups({
|
|
740
|
+
tasks,
|
|
741
|
+
runs,
|
|
742
|
+
...blockers?.byTaskId !== undefined ? { classifications: blockers.byTaskId } : {},
|
|
743
|
+
...deps.generatedAt !== undefined ? { generatedAt: deps.generatedAt } : {},
|
|
744
|
+
...deps.workspaceId !== undefined ? { workspaceId: deps.workspaceId } : {}
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
var getWorkspaceStatus = getWorkspaceRollups;
|
|
748
|
+
// packages/dependency-graph-plugin/src/taskScore.ts
|
|
749
|
+
var ROLE_WEIGHT = {
|
|
750
|
+
architect: 25,
|
|
751
|
+
extractor: 10
|
|
752
|
+
};
|
|
753
|
+
var CRITICALITY_WEIGHT = {
|
|
754
|
+
core: 20,
|
|
755
|
+
high: 10,
|
|
756
|
+
normal: 0
|
|
757
|
+
};
|
|
758
|
+
function finiteNumber(value, fallback) {
|
|
759
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
760
|
+
}
|
|
761
|
+
function scoreTask(input) {
|
|
762
|
+
const priority = finiteNumber(input.priority, 3);
|
|
763
|
+
const unblockCount = Math.max(0, finiteNumber(input.unblockCount, 0));
|
|
764
|
+
const roleWeight = input.role ? ROLE_WEIGHT[input.role] ?? 0 : 0;
|
|
765
|
+
const criticalityWeight = input.criticality ? CRITICALITY_WEIGHT[input.criticality] ?? 0 : 0;
|
|
766
|
+
const validationWeight = (input.validation ?? []).some((entry) => entry.includes("test-contract") || entry.includes("test-boundary")) ? 8 : 0;
|
|
767
|
+
const queueWeight = finiteNumber(input.queueWeight, 0);
|
|
768
|
+
return unblockCount * 100 + (10 - priority) + roleWeight + criticalityWeight + validationWeight + queueWeight;
|
|
769
|
+
}
|
|
770
|
+
function rankTasks(tasks, scoreInput, idOf) {
|
|
771
|
+
return tasks.map((task) => {
|
|
772
|
+
const input = scoreInput(task);
|
|
773
|
+
return {
|
|
774
|
+
task,
|
|
775
|
+
score: scoreTask(input),
|
|
776
|
+
priority: finiteNumber(input.priority, 3),
|
|
777
|
+
unblockCount: Math.max(0, finiteNumber(input.unblockCount, 0))
|
|
778
|
+
};
|
|
779
|
+
}).toSorted((left, right) => {
|
|
780
|
+
const scoreDelta = right.score - left.score;
|
|
781
|
+
if (scoreDelta !== 0)
|
|
782
|
+
return scoreDelta;
|
|
783
|
+
const unblockDelta = right.unblockCount - left.unblockCount;
|
|
784
|
+
if (unblockDelta !== 0)
|
|
785
|
+
return unblockDelta;
|
|
786
|
+
const priorityDelta = left.priority - right.priority;
|
|
787
|
+
if (priorityDelta !== 0)
|
|
788
|
+
return priorityDelta;
|
|
789
|
+
return idOf(left.task).localeCompare(idOf(right.task));
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
// packages/dependency-graph-plugin/src/taskRanking.ts
|
|
793
|
+
import {
|
|
794
|
+
computeTaskDependencyBadges as computeTaskDependencyBadges3,
|
|
795
|
+
isTaskTerminalStatus as isTaskTerminalStatus2,
|
|
796
|
+
readTaskScope
|
|
797
|
+
} from "@rig/contracts";
|
|
798
|
+
function isObjectRecord3(value) {
|
|
799
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
800
|
+
}
|
|
801
|
+
function readStringList(value) {
|
|
802
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
|
|
803
|
+
}
|
|
804
|
+
function priorityValue(task) {
|
|
805
|
+
return typeof task.priority === "number" && Number.isFinite(task.priority) ? task.priority : Number.MAX_SAFE_INTEGER;
|
|
806
|
+
}
|
|
807
|
+
function readTaskRole(task) {
|
|
808
|
+
if (typeof task.role === "string" && task.role.trim())
|
|
809
|
+
return task.role.trim();
|
|
810
|
+
const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
|
|
811
|
+
return typeof metadata?.role === "string" && metadata.role.trim() ? metadata.role.trim() : null;
|
|
812
|
+
}
|
|
813
|
+
function readTaskCriticality(task) {
|
|
814
|
+
const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
|
|
815
|
+
return typeof metadata?.criticality === "string" && metadata.criticality.trim() ? metadata.criticality.trim() : null;
|
|
816
|
+
}
|
|
817
|
+
function readTaskValidationKeys(task) {
|
|
818
|
+
const taskRecord = task;
|
|
819
|
+
const topLevel = readStringList(taskRecord.validationKeys);
|
|
820
|
+
if (topLevel.length > 0)
|
|
821
|
+
return topLevel;
|
|
822
|
+
const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
|
|
823
|
+
return readStringList(metadata?.validation);
|
|
824
|
+
}
|
|
825
|
+
function readTaskQueueWeight(task) {
|
|
826
|
+
const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
|
|
827
|
+
const queueWeight = metadata?.queueWeight ?? metadata?.queue_weight;
|
|
828
|
+
return typeof queueWeight === "number" && Number.isFinite(queueWeight) ? queueWeight : 0;
|
|
829
|
+
}
|
|
830
|
+
function scoreInputForTask(task, unblockCount) {
|
|
831
|
+
return {
|
|
832
|
+
priority: task.priority,
|
|
833
|
+
unblockCount,
|
|
834
|
+
role: readTaskRole(task),
|
|
835
|
+
criticality: readTaskCriticality(task),
|
|
836
|
+
validation: readTaskValidationKeys(task),
|
|
837
|
+
queueWeight: readTaskQueueWeight(task)
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function selectNextReadyTaskByPriority(tasks, options = {}) {
|
|
841
|
+
const excluded = new Set(options.excludeTaskIds ?? []);
|
|
842
|
+
const badges = computeTaskDependencyBadges3(tasks);
|
|
843
|
+
const candidates = tasks.filter((task) => !excluded.has(task.id)).filter((task) => options.filter?.(task) ?? true).filter((task) => badges.get(task.id)?.ready === true).toSorted((left, right) => {
|
|
844
|
+
const priorityDelta = priorityValue(left) - priorityValue(right);
|
|
845
|
+
if (priorityDelta !== 0)
|
|
846
|
+
return priorityDelta;
|
|
847
|
+
const createdDelta = (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
|
|
848
|
+
if (createdDelta !== 0)
|
|
849
|
+
return createdDelta;
|
|
850
|
+
return left.id.localeCompare(right.id);
|
|
851
|
+
});
|
|
852
|
+
return candidates[0] ?? null;
|
|
853
|
+
}
|
|
854
|
+
function rankReadyTasks(tasks, options = {}) {
|
|
855
|
+
const excluded = new Set(options.excludeTaskIds ?? []);
|
|
856
|
+
const activeTaskIds = new Set(options.activeTaskIds ?? []);
|
|
857
|
+
const badges = computeTaskDependencyBadges3(tasks);
|
|
858
|
+
const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
|
|
859
|
+
const openBlockCount = (task) => (badges.get(task.id)?.blocks ?? []).filter((blockedTaskId) => {
|
|
860
|
+
const blockedTask = tasksById.get(blockedTaskId);
|
|
861
|
+
return blockedTask ? !isTaskTerminalStatus2(blockedTask.status) : false;
|
|
862
|
+
}).length;
|
|
863
|
+
const readyTasks = tasks.filter((task) => !excluded.has(task.id)).filter((task) => !activeTaskIds.has(task.id)).filter((task) => options.filter?.(task) ?? true).filter((task) => badges.get(task.id)?.ready === true);
|
|
864
|
+
const maxUnblockCount = Math.max(0, ...readyTasks.map(openBlockCount));
|
|
865
|
+
const selectedByMode = readyTasks.filter((task) => {
|
|
866
|
+
const unblockCount = openBlockCount(task);
|
|
867
|
+
if (options.selection === "blocking-only")
|
|
868
|
+
return unblockCount > 0;
|
|
869
|
+
if (options.selection === "max-unblock")
|
|
870
|
+
return maxUnblockCount > 0 && unblockCount === maxUnblockCount;
|
|
871
|
+
return true;
|
|
872
|
+
});
|
|
873
|
+
return rankTasks(selectedByMode, (task) => scoreInputForTask(task, openBlockCount(task)), (task) => task.id).map((entry) => ({
|
|
874
|
+
task: entry.task,
|
|
875
|
+
score: entry.score,
|
|
876
|
+
priority: entry.priority,
|
|
877
|
+
unblockCount: entry.unblockCount,
|
|
878
|
+
scope: readTaskScope(entry.task)
|
|
879
|
+
}));
|
|
880
|
+
}
|
|
881
|
+
function selectRankedReadyTasks(tasks, options = {}) {
|
|
882
|
+
const ranked = rankReadyTasks(tasks, options);
|
|
883
|
+
if (options.requireDisjointScopes !== true && !options.disjointWithScopes) {
|
|
884
|
+
return ranked.slice(0, options.limit).map((entry) => entry.task);
|
|
885
|
+
}
|
|
886
|
+
const occupiedScopes = new Set(options.disjointWithScopes ?? []);
|
|
887
|
+
const selected = [];
|
|
888
|
+
for (const entry of ranked) {
|
|
889
|
+
if (entry.scope.some((scope) => occupiedScopes.has(scope)))
|
|
890
|
+
continue;
|
|
891
|
+
selected.push(entry.task);
|
|
892
|
+
for (const scope of entry.scope)
|
|
893
|
+
occupiedScopes.add(scope);
|
|
894
|
+
if (options.limit !== undefined && selected.length >= options.limit)
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
return selected;
|
|
898
|
+
}
|
|
899
|
+
// packages/dependency-graph-plugin/src/taskSelection.ts
|
|
900
|
+
import {
|
|
901
|
+
getTaskForCommand,
|
|
902
|
+
listTasksForCommand,
|
|
903
|
+
normalizeTaskId
|
|
904
|
+
} from "@rig/core/task-io";
|
|
905
|
+
var CLOSED_STATUSES = {
|
|
906
|
+
closed: true,
|
|
907
|
+
completed: true,
|
|
908
|
+
complete: true,
|
|
909
|
+
done: true,
|
|
910
|
+
cancelled: true,
|
|
911
|
+
canceled: true,
|
|
912
|
+
merged: true,
|
|
913
|
+
resolved: true
|
|
914
|
+
};
|
|
915
|
+
var NOT_READY_STATUSES = {
|
|
916
|
+
...CLOSED_STATUSES,
|
|
917
|
+
blocked: true,
|
|
918
|
+
in_progress: true,
|
|
919
|
+
"in-progress": true,
|
|
920
|
+
running: true,
|
|
921
|
+
active: true
|
|
922
|
+
};
|
|
923
|
+
function isRecord(value) {
|
|
924
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
925
|
+
}
|
|
926
|
+
function readTaskStatus(task) {
|
|
927
|
+
const value = task.status;
|
|
928
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
929
|
+
}
|
|
930
|
+
function isReadyTask(task) {
|
|
931
|
+
const status = readTaskStatus(task);
|
|
932
|
+
return status.length === 0 || NOT_READY_STATUSES[status] !== true;
|
|
933
|
+
}
|
|
934
|
+
function asDependencyProjection(task) {
|
|
935
|
+
const record = task;
|
|
936
|
+
return {
|
|
937
|
+
...record,
|
|
938
|
+
status: typeof record.status === "string" && record.status.trim() ? record.status : "open",
|
|
939
|
+
priority: record.priority ?? "P2",
|
|
940
|
+
metadata: isRecord(record.metadata) ? record.metadata : {}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function currentLogin(env = process.env) {
|
|
944
|
+
for (const key of ["RIG_GITHUB_LOGIN", "GITHUB_ACTOR", "GH_USER", "USER", "USERNAME"]) {
|
|
945
|
+
const value = env[key]?.trim();
|
|
946
|
+
if (value)
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
function resolveAssignee(value, env = process.env) {
|
|
952
|
+
const trimmed = value?.trim();
|
|
953
|
+
if (!trimmed)
|
|
954
|
+
return;
|
|
955
|
+
const lowered = trimmed.toLowerCase();
|
|
956
|
+
if (lowered === "me" || lowered === "@me")
|
|
957
|
+
return currentLogin(env) ?? "@me";
|
|
958
|
+
return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
959
|
+
}
|
|
960
|
+
var normalizeAssigneeFilter = resolveAssignee;
|
|
961
|
+
function taskMatchesState(task, state) {
|
|
962
|
+
if (!state)
|
|
963
|
+
return true;
|
|
964
|
+
const closed = CLOSED_STATUSES[readTaskStatus(task)] === true;
|
|
965
|
+
return state === "closed" ? closed : !closed;
|
|
966
|
+
}
|
|
967
|
+
function taskMatchesSearch(task, search) {
|
|
968
|
+
const needle = search?.trim().toLowerCase();
|
|
969
|
+
if (!needle)
|
|
970
|
+
return true;
|
|
971
|
+
const title = typeof task.title === "string" ? task.title : "";
|
|
972
|
+
const body = typeof task.body === "string" ? task.body : "";
|
|
973
|
+
const description = typeof task.description === "string" ? task.description : "";
|
|
974
|
+
return `${task.id}
|
|
975
|
+
${title}
|
|
976
|
+
${body}
|
|
977
|
+
${description}`.toLowerCase().includes(needle);
|
|
978
|
+
}
|
|
979
|
+
function collectAssigneeLogins(value) {
|
|
980
|
+
if (typeof value === "string")
|
|
981
|
+
return [value];
|
|
982
|
+
if (Array.isArray(value))
|
|
983
|
+
return value.flatMap((entry) => collectAssigneeLogins(entry));
|
|
984
|
+
if (isRecord(value))
|
|
985
|
+
return [value.login, value.username, value.name, value.id].flatMap((entry) => collectAssigneeLogins(entry));
|
|
986
|
+
return [];
|
|
987
|
+
}
|
|
988
|
+
function taskAssignees(task) {
|
|
989
|
+
const raw = isRecord(task.raw) ? task.raw : null;
|
|
990
|
+
return [
|
|
991
|
+
task.assignee,
|
|
992
|
+
task.assignees,
|
|
993
|
+
task.assignedTo,
|
|
994
|
+
task.owner,
|
|
995
|
+
raw?.assignee,
|
|
996
|
+
raw?.assignees,
|
|
997
|
+
raw?.assignedTo,
|
|
998
|
+
raw?.owner
|
|
999
|
+
].flatMap((value) => collectAssigneeLogins(value));
|
|
1000
|
+
}
|
|
1001
|
+
function taskMatchesAssignee(task, assignee) {
|
|
1002
|
+
if (!assignee)
|
|
1003
|
+
return true;
|
|
1004
|
+
const needle = assignee.startsWith("@") ? assignee.slice(1) : assignee;
|
|
1005
|
+
return taskAssignees(task).some((login) => {
|
|
1006
|
+
const normalized = login.startsWith("@") ? login.slice(1) : login;
|
|
1007
|
+
return normalized.localeCompare(needle, undefined, { sensitivity: "accent" }) === 0;
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
function applyFilters(tasks, filters = {}) {
|
|
1011
|
+
const filtered = tasks.filter((task) => taskMatchesAssignee(task, resolveAssignee(filters.assignee))).filter((task) => taskMatchesState(task, filters.state)).filter((task) => taskMatchesSearch(task, filters.search));
|
|
1012
|
+
return filters.limit === undefined ? filtered : filtered.slice(0, filters.limit);
|
|
1013
|
+
}
|
|
1014
|
+
function selectNextReadyTask(tasks, filters = {}) {
|
|
1015
|
+
const matching = applyFilters(tasks, { ...filters, limit: undefined }).filter(isReadyTask);
|
|
1016
|
+
const selected = selectNextReadyTaskByPriority(matching.map(asDependencyProjection), {
|
|
1017
|
+
filter: (task) => isReadyTask(task)
|
|
1018
|
+
});
|
|
1019
|
+
return selected ?? matching[0] ?? null;
|
|
1020
|
+
}
|
|
1021
|
+
var selectNextTask = selectNextReadyTask;
|
|
1022
|
+
async function resolveStartTask(input) {
|
|
1023
|
+
if (input.next) {
|
|
1024
|
+
const task2 = selectNextReadyTask(await listTasksForCommand(input.projectRoot, { listTasks: input.listTasks }), input.filters ?? {});
|
|
1025
|
+
const taskId2 = normalizeTaskId(task2?.id);
|
|
1026
|
+
if (!task2 || !taskId2)
|
|
1027
|
+
throw new Error("No ready task found.");
|
|
1028
|
+
return { taskId: taskId2, task: task2 };
|
|
1029
|
+
}
|
|
1030
|
+
const taskId = normalizeTaskId(input.taskId ?? undefined);
|
|
1031
|
+
if (!taskId)
|
|
1032
|
+
throw new Error("Missing task id.");
|
|
1033
|
+
const task = await getTaskForCommand(input.projectRoot, taskId, { getTask: input.getTask }).catch(() => null);
|
|
1034
|
+
return { taskId, task };
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// packages/dependency-graph-plugin/src/plugin.ts
|
|
6
1038
|
var DEPENDENCY_GRAPH_PLUGIN_NAME = "@rig/dependency-graph-plugin";
|
|
7
1039
|
var DEPENDENCY_GRAPH_CLI_ID = "dependency-graph.graph";
|
|
8
1040
|
var WORKSPACE_STATUS_CLI_ID = "dependency-graph.status";
|
|
@@ -10,14 +1042,16 @@ var WORKSPACE_SUMMARY_CLI_ID = "dependency-graph.summary";
|
|
|
10
1042
|
var DEPENDENCY_GRAPH_PANEL_ID = "dependency-graph";
|
|
11
1043
|
var EPICS_PANEL_ID = "epics";
|
|
12
1044
|
var PEOPLE_PANEL_ID = "people";
|
|
13
|
-
function
|
|
1045
|
+
function isRecord2(value) {
|
|
14
1046
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
1047
|
}
|
|
16
1048
|
function panelProjectRoot(context) {
|
|
17
|
-
return
|
|
1049
|
+
return isRecord2(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
18
1050
|
}
|
|
19
|
-
async function
|
|
20
|
-
|
|
1051
|
+
async function loadDependencyGraphClientIo() {
|
|
1052
|
+
const { listTasks } = await import("@rig/core/task-io");
|
|
1053
|
+
const { listRuns } = await import("@rig/run-worker/runs");
|
|
1054
|
+
return { listRuns, listTasks };
|
|
21
1055
|
}
|
|
22
1056
|
function toDependencyGraphPanelPayload(model) {
|
|
23
1057
|
const layoutByTaskId = new Map(model.layout.nodes.map((node) => [node.taskId, node]));
|
|
@@ -46,9 +1080,9 @@ function toDependencyGraphPanelPayload(model) {
|
|
|
46
1080
|
degraded: model.degraded ? "dependency graph projection degraded" : null
|
|
47
1081
|
};
|
|
48
1082
|
}
|
|
49
|
-
function toWorkspaceRollupsPanelPayload(
|
|
1083
|
+
function toWorkspaceRollupsPanelPayload(rollups2) {
|
|
50
1084
|
return {
|
|
51
|
-
epics:
|
|
1085
|
+
epics: rollups2.epics.map((epic) => ({
|
|
52
1086
|
epicKey: epic.epicKey,
|
|
53
1087
|
title: epic.epicKey,
|
|
54
1088
|
total: epic.total,
|
|
@@ -57,7 +1091,7 @@ function toWorkspaceRollupsPanelPayload(rollups) {
|
|
|
57
1091
|
inFlightCount: epic.inFlightCount,
|
|
58
1092
|
humanBlockedCount: epic.humanBlockedCount
|
|
59
1093
|
})),
|
|
60
|
-
people:
|
|
1094
|
+
people: rollups2.assignees.map((assignee) => ({
|
|
61
1095
|
assignee: assignee.assignee,
|
|
62
1096
|
openCount: assignee.openTaskCount,
|
|
63
1097
|
inFlightCount: assignee.inFlightRunCount,
|
|
@@ -71,7 +1105,7 @@ async function produceDependencyGraphPanel(context) {
|
|
|
71
1105
|
const projectRoot = panelProjectRoot(context);
|
|
72
1106
|
if (!projectRoot)
|
|
73
1107
|
return;
|
|
74
|
-
const {
|
|
1108
|
+
const { listRuns, listTasks } = await loadDependencyGraphClientIo();
|
|
75
1109
|
const model = await getWorkspaceDependencyGraph(projectRoot, {
|
|
76
1110
|
listTasks,
|
|
77
1111
|
listRuns,
|
|
@@ -83,13 +1117,13 @@ async function produceWorkspaceRollupsPanel(context) {
|
|
|
83
1117
|
const projectRoot = panelProjectRoot(context);
|
|
84
1118
|
if (!projectRoot)
|
|
85
1119
|
return;
|
|
86
|
-
const {
|
|
87
|
-
const
|
|
1120
|
+
const { listRuns, listTasks } = await loadDependencyGraphClientIo();
|
|
1121
|
+
const rollups2 = await getWorkspaceRollups(projectRoot, {
|
|
88
1122
|
listTasks,
|
|
89
1123
|
listRuns,
|
|
90
1124
|
classifyBlockers: (root) => classifyWorkspaceBlockers(root, { listTasks, listRuns })
|
|
91
1125
|
});
|
|
92
|
-
return toWorkspaceRollupsPanelPayload(
|
|
1126
|
+
return toWorkspaceRollupsPanelPayload(rollups2);
|
|
93
1127
|
}
|
|
94
1128
|
function printJson(value) {
|
|
95
1129
|
console.log(JSON.stringify(value, null, 2));
|
|
@@ -137,7 +1171,7 @@ async function executeGraph(context, args) {
|
|
|
137
1171
|
requireNoExtraArgs(json.rest, "rig graph [--json|--dot]");
|
|
138
1172
|
if (dot.value && json.value)
|
|
139
1173
|
throw new Error("Pass only one of --json or --dot.");
|
|
140
|
-
const {
|
|
1174
|
+
const { listRuns, listTasks } = await loadDependencyGraphClientIo();
|
|
141
1175
|
const model = await getWorkspaceDependencyGraph(context.projectRoot, {
|
|
142
1176
|
listTasks,
|
|
143
1177
|
listRuns,
|
|
@@ -158,24 +1192,24 @@ async function executeStatus(context, args) {
|
|
|
158
1192
|
const json = takeFlag(args, "--json");
|
|
159
1193
|
const epic = takeOption(json.rest, "--epic");
|
|
160
1194
|
requireNoExtraArgs(epic.rest, "rig status [--epic <key>] [--json]");
|
|
161
|
-
const {
|
|
162
|
-
const [
|
|
1195
|
+
const { listRuns, listTasks } = await loadDependencyGraphClientIo();
|
|
1196
|
+
const [rollups2, blockers, runs] = await Promise.all([
|
|
163
1197
|
getWorkspaceRollups(context.projectRoot, { listTasks, listRuns }),
|
|
164
1198
|
classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns }),
|
|
165
1199
|
listRuns(context.projectRoot)
|
|
166
1200
|
]);
|
|
167
|
-
const filteredEpics = epic.value ?
|
|
1201
|
+
const filteredEpics = epic.value ? rollups2.epics.filter((entry) => entry.epicKey === epic.value) : rollups2.epics;
|
|
168
1202
|
const details = {
|
|
169
1203
|
epic: epic.value ?? null,
|
|
170
1204
|
epicRollups: filteredEpics,
|
|
171
1205
|
epics: filteredEpics.length,
|
|
172
|
-
assignees:
|
|
1206
|
+
assignees: rollups2.assignees.length,
|
|
173
1207
|
humanBlockers: blockers.human.length,
|
|
174
1208
|
machineBlockers: blockers.machine.length,
|
|
175
1209
|
runs: runs.length,
|
|
176
1210
|
activeRuns: runs.filter((run) => run.live && !run.stale).length,
|
|
177
1211
|
needsAttention: runs.filter((run) => run.status === "needs-attention" || run.pendingApprovals > 0 || run.pendingInputs > 0).length,
|
|
178
|
-
generatedAt:
|
|
1212
|
+
generatedAt: rollups2.generatedAt
|
|
179
1213
|
};
|
|
180
1214
|
if (context.outputMode === "text") {
|
|
181
1215
|
if (json.value)
|
|
@@ -190,9 +1224,9 @@ async function executeSummary(context, args) {
|
|
|
190
1224
|
const json = takeFlag(args, "--json");
|
|
191
1225
|
const epicOpt = takeOption(json.rest, "--epic");
|
|
192
1226
|
requireNoExtraArgs(epicOpt.rest, "rig summary [--epic <key>] [--json]");
|
|
193
|
-
const {
|
|
194
|
-
const
|
|
195
|
-
const details = epicOpt.value ? { ...
|
|
1227
|
+
const { listRuns, listTasks } = await loadDependencyGraphClientIo();
|
|
1228
|
+
const rollups2 = await getWorkspaceRollups(context.projectRoot, { listTasks, listRuns });
|
|
1229
|
+
const details = epicOpt.value ? { ...rollups2, epics: rollups2.epics.filter((epic) => epic.epicKey === epicOpt.value) } : rollups2;
|
|
196
1230
|
if (context.outputMode === "text") {
|
|
197
1231
|
if (json.value)
|
|
198
1232
|
printJson(details);
|
|
@@ -244,36 +1278,51 @@ var dependencyGraphPlugin = definePlugin({
|
|
|
244
1278
|
{ id: "workspace.rollups", title: "Workspace rollups", commandId: WORKSPACE_SUMMARY_CLI_ID, panelId: EPICS_PANEL_ID }
|
|
245
1279
|
],
|
|
246
1280
|
panels: [
|
|
247
|
-
{ id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph" },
|
|
248
|
-
{ id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups" },
|
|
1281
|
+
{ id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph", produce: produceDependencyGraphPanel },
|
|
1282
|
+
{ id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups", produce: produceWorkspaceRollupsPanel },
|
|
249
1283
|
{ id: PEOPLE_PANEL_ID, slot: "capability", title: "People", capabilityId: "workspace.rollups" }
|
|
250
1284
|
],
|
|
251
|
-
cliCommands: dependencyGraphCliCommands
|
|
252
|
-
}
|
|
253
|
-
}, {
|
|
254
|
-
featureCapabilities: [
|
|
255
|
-
{ id: "workspace.dependency-graph", title: "Workspace dependency graph", commandId: DEPENDENCY_GRAPH_CLI_ID, panelId: DEPENDENCY_GRAPH_PANEL_ID },
|
|
256
|
-
{ id: "workspace.status", title: "Workspace status", commandId: WORKSPACE_STATUS_CLI_ID },
|
|
257
|
-
{ id: "workspace.rollups", title: "Workspace rollups", commandId: WORKSPACE_SUMMARY_CLI_ID, panelId: EPICS_PANEL_ID }
|
|
258
|
-
],
|
|
259
|
-
panels: [
|
|
260
|
-
{ id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph", produce: produceDependencyGraphPanel },
|
|
261
|
-
{ id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups", produce: produceWorkspaceRollupsPanel }
|
|
262
|
-
],
|
|
263
|
-
cliCommands: dependencyGraphCliCommands
|
|
1285
|
+
cliCommands: dependencyGraphCliCommands
|
|
1286
|
+
}
|
|
264
1287
|
});
|
|
265
1288
|
function createDependencyGraphPlugin() {
|
|
266
1289
|
return dependencyGraphPlugin;
|
|
267
1290
|
}
|
|
268
1291
|
var plugin_default = dependencyGraphPlugin;
|
|
269
1292
|
export {
|
|
1293
|
+
taskMatchesState,
|
|
1294
|
+
taskMatchesSearch,
|
|
1295
|
+
taskMatchesAssignee,
|
|
1296
|
+
taskAssignees,
|
|
1297
|
+
selectWorkspaceRollups,
|
|
1298
|
+
selectRankedReadyTasks,
|
|
1299
|
+
selectNextTask,
|
|
1300
|
+
selectNextReadyTaskByPriority,
|
|
1301
|
+
selectNextReadyTask,
|
|
1302
|
+
scoreTask,
|
|
1303
|
+
resolveStartTask,
|
|
1304
|
+
resolveAssignee,
|
|
1305
|
+
readTaskStatus,
|
|
1306
|
+
rankTasks,
|
|
1307
|
+
rankReadyTasks,
|
|
1308
|
+
normalizeAssigneeFilter,
|
|
1309
|
+
isReadyTask,
|
|
1310
|
+
getWorkspaceStatus,
|
|
1311
|
+
getWorkspaceRollups,
|
|
1312
|
+
getWorkspaceDependencyGraph,
|
|
1313
|
+
formatDependencyGraphDot,
|
|
270
1314
|
executeSummary,
|
|
271
1315
|
executeStatus,
|
|
272
1316
|
executeGraph,
|
|
273
1317
|
dependencyGraphPlugin,
|
|
274
1318
|
dependencyGraphCliCommands,
|
|
275
1319
|
plugin_default as default,
|
|
1320
|
+
currentLogin,
|
|
276
1321
|
createDependencyGraphPlugin,
|
|
1322
|
+
collectAssigneeLogins,
|
|
1323
|
+
buildWorkspaceDependencyGraph,
|
|
1324
|
+
asDependencyProjection,
|
|
1325
|
+
applyFilters,
|
|
277
1326
|
WORKSPACE_SUMMARY_CLI_ID,
|
|
278
1327
|
WORKSPACE_STATUS_CLI_ID,
|
|
279
1328
|
PEOPLE_PANEL_ID,
|