@h-rig/dependency-graph-plugin 0.0.6-alpha.154 → 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.
@@ -0,0 +1,3 @@
1
+ export declare function extractTaskCode(title: string): string | null;
2
+ export declare function extractTaskGroupKey(title: string): string | null;
3
+ export declare function stripTaskCode(label: string): string;
@@ -0,0 +1,26 @@
1
+ // @bun
2
+ // packages/dependency-graph-plugin/src/graph-model/taskGraphCodes.ts
3
+ var TASK_CODE_RE = /^\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]\s*/;
4
+ function extractTaskCode(title) {
5
+ const match = title.match(TASK_CODE_RE);
6
+ return match?.[1] ?? null;
7
+ }
8
+ function extractTaskGroupKey(title) {
9
+ const code = extractTaskCode(title);
10
+ if (!code)
11
+ return null;
12
+ const parts = code.split("-");
13
+ const suffix = parts.at(-1) ?? "";
14
+ if (/^\d+$/.test(suffix)) {
15
+ return parts.slice(0, -1).join("-");
16
+ }
17
+ return parts[0] ?? code;
18
+ }
19
+ function stripTaskCode(label) {
20
+ return label.replace(TASK_CODE_RE, "");
21
+ }
22
+ export {
23
+ stripTaskCode,
24
+ extractTaskGroupKey,
25
+ extractTaskCode
26
+ };
@@ -0,0 +1,61 @@
1
+ import type { EngineReadModel, TaskSummary } from "@rig/contracts";
2
+ export type TaskGraphLane = Readonly<{
3
+ key: string;
4
+ label: string;
5
+ rowIndex: number;
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ color: string;
11
+ taskCount: number;
12
+ }>;
13
+ export type TaskGraphStage = Readonly<{
14
+ index: number;
15
+ label: string;
16
+ x: number;
17
+ width: number;
18
+ }>;
19
+ export type TaskGraphNode = Readonly<{
20
+ id: string;
21
+ taskId: string;
22
+ task: TaskSummary;
23
+ rowKey: string;
24
+ rowLabel: string;
25
+ rowIndex: number;
26
+ stage: number;
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ color: string;
32
+ taskCode: string | null;
33
+ strippedTitle: string;
34
+ depsIn: number;
35
+ depsOut: number;
36
+ runCount: number;
37
+ hasApprovals: boolean;
38
+ hasPendingUserInput: boolean;
39
+ hasRejectedReview: boolean;
40
+ hasFailedValidations: boolean;
41
+ artifactCount: number;
42
+ }>;
43
+ export type TaskGraphEdge = Readonly<{
44
+ id: string;
45
+ sourceId: string;
46
+ targetId: string;
47
+ color: string;
48
+ kind: "blocking" | "parent-child";
49
+ }>;
50
+ export type TaskGraphLayout = Readonly<{
51
+ lanes: readonly TaskGraphLane[];
52
+ stages: readonly TaskGraphStage[];
53
+ nodes: readonly TaskGraphNode[];
54
+ edges: readonly TaskGraphEdge[];
55
+ totalWidth: number;
56
+ totalHeight: number;
57
+ taskCount: number;
58
+ }>;
59
+ export declare function buildTaskGraphLayout(snapshot: EngineReadModel | null, tasks: readonly TaskSummary[], options?: {
60
+ showParentChild?: boolean;
61
+ }): TaskGraphLayout;
@@ -0,0 +1,346 @@
1
+ // @bun
2
+ // packages/dependency-graph-plugin/src/graph-model/taskGraphLayout.ts
3
+ import {
4
+ buildTaskReferenceIndex,
5
+ readTaskMetadataStringList,
6
+ resolveTaskReference
7
+ } from "@rig/contracts";
8
+ import {
9
+ selectPendingApprovals,
10
+ selectRunsByTask,
11
+ selectUserInputsForRun
12
+ } from "@rig/read-model-plugin";
13
+
14
+ // packages/dependency-graph-plugin/src/graph-model/taskGraphCodes.ts
15
+ var TASK_CODE_RE = /^\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]\s*/;
16
+ function extractTaskCode(title) {
17
+ const match = title.match(TASK_CODE_RE);
18
+ return match?.[1] ?? null;
19
+ }
20
+ function extractTaskGroupKey(title) {
21
+ const code = extractTaskCode(title);
22
+ if (!code)
23
+ return null;
24
+ const parts = code.split("-");
25
+ const suffix = parts.at(-1) ?? "";
26
+ if (/^\d+$/.test(suffix)) {
27
+ return parts.slice(0, -1).join("-");
28
+ }
29
+ return parts[0] ?? code;
30
+ }
31
+ function stripTaskCode(label) {
32
+ return label.replace(TASK_CODE_RE, "");
33
+ }
34
+
35
+ // packages/dependency-graph-plugin/src/graph-model/taskGraphLayout.ts
36
+ var CARD_WIDTH = 200;
37
+ var CARD_HEIGHT = 110;
38
+ var CELL_V_PAD = 12;
39
+ var CELL_H_PAD = 12;
40
+ var ROW_GAP = 28;
41
+ var COL_GAP = 40;
42
+ var LANE_LABEL_W = 120;
43
+ var STAGE_HDR_H = 32;
44
+ var PALETTE = [
45
+ { bg: "#3a2d12", border: "#8d6b19", edge: "#d6a11d" },
46
+ { bg: "#102642", border: "#245fbf", edge: "#66a2ff" },
47
+ { bg: "#2c173f", border: "#7b39d4", edge: "#a76df5" },
48
+ { bg: "#112d1c", border: "#2d8f4e", edge: "#62d882" },
49
+ { bg: "#3a2314", border: "#c86d1c", edge: "#e69654" },
50
+ { bg: "#31152b", border: "#bf3d88", edge: "#f07ebb" },
51
+ { bg: "#132c35", border: "#1783a6", edge: "#53c4e5" },
52
+ { bg: "#26310f", border: "#6d9a19", edge: "#a7da42" }
53
+ ];
54
+ function isObjectRecord(value) {
55
+ return typeof value === "object" && value !== null && !Array.isArray(value);
56
+ }
57
+ function readIssueType(task) {
58
+ const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
59
+ if (typeof metadata?.issueType === "string")
60
+ return metadata.issueType;
61
+ const raw = isObjectRecord(metadata?.raw) ? metadata.raw : null;
62
+ return typeof raw?.issueType === "string" ? raw.issueType : null;
63
+ }
64
+ function isGraphTask(task) {
65
+ return readIssueType(task) !== "epic";
66
+ }
67
+ function findEpicAncestor(task, resolve, tasksById) {
68
+ const visited = new Set;
69
+ let current = task;
70
+ let epic = null;
71
+ while (!visited.has(current.id)) {
72
+ visited.add(current.id);
73
+ const parentRef = readTaskMetadataStringList(current, "parentChildDeps")[0];
74
+ if (!parentRef)
75
+ break;
76
+ const parentId = resolve(parentRef);
77
+ if (!parentId)
78
+ break;
79
+ const parent = tasksById.get(parentId);
80
+ if (!parent)
81
+ break;
82
+ if (readIssueType(parent) === "epic") {
83
+ epic = parent;
84
+ }
85
+ current = parent;
86
+ }
87
+ return epic;
88
+ }
89
+ function getRowKey(task, resolve, tasksById) {
90
+ const epic = findEpicAncestor(task, resolve, tasksById);
91
+ if (epic) {
92
+ return `group:${epic.id}`;
93
+ }
94
+ const codeGroup = extractTaskGroupKey(task.title);
95
+ if (codeGroup) {
96
+ return `code:${codeGroup}`;
97
+ }
98
+ return `type:${readIssueType(task) ?? "task"}`;
99
+ }
100
+ function getRowLabel(task, rowKey, resolve, tasksById) {
101
+ if (rowKey.startsWith("group:")) {
102
+ const groupId = rowKey.slice("group:".length);
103
+ return tasksById.get(groupId)?.title ?? groupId;
104
+ }
105
+ if (rowKey.startsWith("code:")) {
106
+ return rowKey.slice("code:".length);
107
+ }
108
+ const code = extractTaskCode(task.title);
109
+ if (code)
110
+ return code;
111
+ const issueType = readIssueType(task);
112
+ if (issueType === "task")
113
+ return "Tasks";
114
+ if (issueType)
115
+ return `${issueType[0]?.toUpperCase() ?? ""}${issueType.slice(1)}`;
116
+ const parentRef = readTaskMetadataStringList(task, "parentChildDeps")[0];
117
+ if (parentRef) {
118
+ const parentId = resolve(parentRef);
119
+ if (parentId) {
120
+ return tasksById.get(parentId)?.title ?? "Tasks";
121
+ }
122
+ }
123
+ return "Tasks";
124
+ }
125
+ function computeDepths(ids, edges) {
126
+ const blockers = new Map;
127
+ for (const edge of edges) {
128
+ if (!ids.has(edge.source) || !ids.has(edge.target))
129
+ continue;
130
+ const current = blockers.get(edge.target) ?? [];
131
+ current.push(edge.source);
132
+ blockers.set(edge.target, current);
133
+ }
134
+ const memo = new Map;
135
+ const visit = (id, stack) => {
136
+ const cached = memo.get(id);
137
+ if (cached !== undefined)
138
+ return cached;
139
+ if (stack.has(id))
140
+ return 0;
141
+ stack.add(id);
142
+ const deps = blockers.get(id);
143
+ if (!deps || deps.length === 0) {
144
+ memo.set(id, 0);
145
+ return 0;
146
+ }
147
+ let maxDepth = 0;
148
+ for (const dep of deps) {
149
+ maxDepth = Math.max(maxDepth, visit(dep, stack) + 1);
150
+ }
151
+ stack.delete(id);
152
+ memo.set(id, maxDepth);
153
+ return maxDepth;
154
+ };
155
+ for (const id of ids) {
156
+ visit(id, new Set);
157
+ }
158
+ return memo;
159
+ }
160
+ function buildTaskGraphLayout(snapshot, tasks, options) {
161
+ const showParentChild = options?.showParentChild ?? false;
162
+ const graphTasks = tasks.filter(isGraphTask);
163
+ if (graphTasks.length === 0) {
164
+ return {
165
+ lanes: [],
166
+ stages: [],
167
+ nodes: [],
168
+ edges: [],
169
+ totalWidth: 0,
170
+ totalHeight: 0,
171
+ taskCount: 0
172
+ };
173
+ }
174
+ const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
175
+ const resolve = (ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId);
176
+ const rows = new Map;
177
+ const rowLabelByKey = new Map;
178
+ for (const task of graphTasks) {
179
+ const rowKey = getRowKey(task, resolve, tasksById);
180
+ const current = rows.get(rowKey) ?? [];
181
+ current.push(task);
182
+ rows.set(rowKey, current);
183
+ if (!rowLabelByKey.has(rowKey)) {
184
+ rowLabelByKey.set(rowKey, getRowLabel(task, rowKey, resolve, tasksById));
185
+ }
186
+ }
187
+ const orderedRows = [...rows.entries()].sort((left, right) => {
188
+ if (left[1].length !== right[1].length)
189
+ return right[1].length - left[1].length;
190
+ return (rowLabelByKey.get(left[0]) ?? left[0]).localeCompare(rowLabelByKey.get(right[0]) ?? right[0]);
191
+ });
192
+ const rowIndexByKey = new Map(orderedRows.map(([key], index) => [key, index]));
193
+ const rowIndexByTaskId = new Map;
194
+ for (const task of graphTasks) {
195
+ rowIndexByTaskId.set(task.id, rowIndexByKey.get(getRowKey(task, resolve, tasksById)) ?? 0);
196
+ }
197
+ const blockingEdgesRaw = [];
198
+ const depsIn = new Map;
199
+ const depsOut = new Map;
200
+ const edges = [];
201
+ for (const task of graphTasks) {
202
+ for (const ref of readTaskMetadataStringList(task, "dependencies")) {
203
+ const sourceId = resolve(ref);
204
+ if (!sourceId)
205
+ continue;
206
+ blockingEdgesRaw.push({ source: sourceId, target: task.id });
207
+ depsOut.set(sourceId, (depsOut.get(sourceId) ?? 0) + 1);
208
+ depsIn.set(task.id, (depsIn.get(task.id) ?? 0) + 1);
209
+ const color = PALETTE[(rowIndexByTaskId.get(sourceId) ?? 0) % PALETTE.length].edge;
210
+ edges.push({
211
+ id: `blocking:${sourceId}:${task.id}`,
212
+ sourceId,
213
+ targetId: task.id,
214
+ color,
215
+ kind: "blocking"
216
+ });
217
+ }
218
+ if (!showParentChild)
219
+ continue;
220
+ for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
221
+ const sourceId = resolve(ref);
222
+ if (!sourceId)
223
+ continue;
224
+ edges.push({
225
+ id: `parent:${sourceId}:${task.id}`,
226
+ sourceId,
227
+ targetId: task.id,
228
+ color: "#5b6472",
229
+ kind: "parent-child"
230
+ });
231
+ }
232
+ }
233
+ const graphTaskIds = new Set(graphTasks.map((task) => task.id));
234
+ const depths = computeDepths(graphTaskIds, blockingEdgesRaw);
235
+ const maxStage = Math.max(0, ...depths.values());
236
+ const rowMaxStack = new Array(orderedRows.length).fill(0);
237
+ const cells = new Map;
238
+ for (const task of graphTasks) {
239
+ const rowIndex = rowIndexByTaskId.get(task.id) ?? 0;
240
+ const stage = depths.get(task.id) ?? 0;
241
+ const key = `${rowIndex}:${stage}`;
242
+ const current = cells.get(key) ?? [];
243
+ current.push(task);
244
+ cells.set(key, current);
245
+ }
246
+ for (const [cellKey, cellTasks] of cells) {
247
+ const [rowIndexText] = cellKey.split(":");
248
+ const rowIndex = Number.parseInt(rowIndexText ?? "0", 10) || 0;
249
+ cellTasks.sort((left, right) => {
250
+ const leftFanout = depsOut.get(left.id) ?? 0;
251
+ const rightFanout = depsOut.get(right.id) ?? 0;
252
+ if (leftFanout !== rightFanout)
253
+ return rightFanout - leftFanout;
254
+ return left.title.localeCompare(right.title);
255
+ });
256
+ rowMaxStack[rowIndex] = Math.max(rowMaxStack[rowIndex] ?? 0, cellTasks.length);
257
+ }
258
+ const rowHeights = rowMaxStack.map((count) => Math.max(count, 1) * (CARD_HEIGHT + CELL_V_PAD) - CELL_V_PAD + CELL_V_PAD * 2);
259
+ const colWidths = new Array(maxStage + 1).fill(CARD_WIDTH + CELL_H_PAD * 2);
260
+ const colX = [];
261
+ let currentX = LANE_LABEL_W;
262
+ for (let index = 0;index <= maxStage; index += 1) {
263
+ colX.push(currentX);
264
+ currentX += (colWidths[index] ?? 0) + COL_GAP;
265
+ }
266
+ const totalWidth = currentX - COL_GAP;
267
+ const rowY = [];
268
+ let currentY = STAGE_HDR_H;
269
+ for (let rowIndex = 0;rowIndex < orderedRows.length; rowIndex += 1) {
270
+ rowY.push(currentY);
271
+ currentY += (rowHeights[rowIndex] ?? 0) + ROW_GAP;
272
+ }
273
+ const totalHeight = currentY - ROW_GAP;
274
+ const lanes = orderedRows.map(([rowKey, rowTasks], rowIndex) => ({
275
+ key: rowKey,
276
+ label: rowLabelByKey.get(rowKey) ?? rowKey,
277
+ rowIndex,
278
+ x: LANE_LABEL_W - 6,
279
+ y: (rowY[rowIndex] ?? 0) - 6,
280
+ width: totalWidth - LANE_LABEL_W + 12,
281
+ height: (rowHeights[rowIndex] ?? 0) + 12,
282
+ color: PALETTE[rowIndex % PALETTE.length].border,
283
+ taskCount: rowTasks.length
284
+ }));
285
+ const stages = Array.from({ length: maxStage + 1 }, (_, index) => ({
286
+ index,
287
+ label: index === 0 ? "Roots" : `Stage ${index}`,
288
+ x: (colX[index] ?? 0) + CELL_H_PAD,
289
+ width: CARD_WIDTH
290
+ }));
291
+ const pendingApprovalRunIds = new Set(selectPendingApprovals(snapshot).map((approval) => approval.runId));
292
+ const nodes = [];
293
+ for (const [rowIndex, [rowKey]] of orderedRows.entries()) {
294
+ for (let stage = 0;stage <= maxStage; stage += 1) {
295
+ const cellTasks = cells.get(`${rowIndex}:${stage}`) ?? [];
296
+ const baseX = (colX[stage] ?? 0) + CELL_H_PAD;
297
+ const baseY = (rowY[rowIndex] ?? 0) + CELL_V_PAD;
298
+ const palette = PALETTE[rowIndex % PALETTE.length];
299
+ for (const [stackIndex, task] of cellTasks.entries()) {
300
+ const runs = selectRunsByTask(snapshot, task.id);
301
+ const runIds = new Set(runs.map((run) => run.id));
302
+ const hasApprovals = runs.some((run) => pendingApprovalRunIds.has(run.id));
303
+ const hasPendingUserInput = runs.some((run) => selectUserInputsForRun(snapshot, run.id).some((request) => request.status === "pending"));
304
+ const hasRejectedReview = (snapshot?.reviews ?? []).some((review) => runIds.has(review.runId) && review.status === "rejected");
305
+ const hasFailedValidations = (snapshot?.validations ?? []).some((validation) => runIds.has(validation.runId) && validation.status === "failed");
306
+ const artifactCount = (snapshot?.artifacts ?? []).filter((artifact) => runIds.has(artifact.runId)).length;
307
+ nodes.push({
308
+ id: task.id,
309
+ taskId: task.id,
310
+ task,
311
+ rowKey,
312
+ rowLabel: rowLabelByKey.get(rowKey) ?? rowKey,
313
+ rowIndex,
314
+ stage,
315
+ x: baseX,
316
+ y: baseY + stackIndex * (CARD_HEIGHT + CELL_V_PAD),
317
+ width: CARD_WIDTH,
318
+ height: CARD_HEIGHT,
319
+ color: palette.border,
320
+ taskCode: extractTaskCode(task.title),
321
+ strippedTitle: stripTaskCode(task.title),
322
+ depsIn: depsIn.get(task.id) ?? 0,
323
+ depsOut: depsOut.get(task.id) ?? 0,
324
+ runCount: runs.length,
325
+ hasApprovals,
326
+ hasPendingUserInput,
327
+ hasRejectedReview,
328
+ hasFailedValidations,
329
+ artifactCount
330
+ });
331
+ }
332
+ }
333
+ }
334
+ return {
335
+ lanes,
336
+ stages,
337
+ nodes,
338
+ edges,
339
+ totalWidth,
340
+ totalHeight,
341
+ taskCount: graphTasks.length
342
+ };
343
+ }
344
+ export {
345
+ buildTaskGraphLayout
346
+ };
@@ -0,0 +1,33 @@
1
+ import type { BuildDependencyGraphModelOptions, DependencyGraphModel, DependencyNode } from "./graph-model/dependencyGraph";
2
+ import type { TaskLike } from "@rig/core/task-io";
3
+ import type { ActionRiskTier, BlockerClass, BlockerClassification, RunRecord, TaskSummary } from "@rig/contracts";
4
+ import { type WorkspaceBlockers } from "@rig/blocker-classifier-plugin";
5
+ export interface ClientDependencyNode extends Omit<DependencyNode, "blockerClass" | "actionRiskTier"> {
6
+ readonly blockerClass: BlockerClass | null;
7
+ readonly actionRiskTier: ActionRiskTier | null;
8
+ }
9
+ export interface ClientDependencyGraphModel extends Omit<DependencyGraphModel, "nodes"> {
10
+ readonly nodes: readonly ClientDependencyNode[];
11
+ }
12
+ export type DependencyGraphProjector = (tasks: readonly TaskSummary[], options?: BuildDependencyGraphModelOptions) => DependencyGraphModel;
13
+ export interface BuildWorkspaceDependencyGraphInput {
14
+ readonly tasks: readonly TaskLike[];
15
+ readonly runs?: readonly RunRecord[];
16
+ readonly classifications?: ReadonlyMap<string, BlockerClassification>;
17
+ readonly generatedAt?: string;
18
+ readonly graphId?: string;
19
+ readonly workspaceId?: string;
20
+ readonly showParentChild?: boolean;
21
+ readonly projectDependencyGraph?: DependencyGraphProjector;
22
+ }
23
+ export declare function buildWorkspaceDependencyGraph(input: BuildWorkspaceDependencyGraphInput): ClientDependencyGraphModel;
24
+ export declare function getWorkspaceDependencyGraph(projectRoot: string, deps: {
25
+ readonly listTasks: (projectRoot: string) => Promise<readonly TaskLike[]>;
26
+ readonly listRuns?: (projectRoot: string) => Promise<readonly RunRecord[]>;
27
+ readonly classifyBlockers?: (projectRoot: string) => Promise<WorkspaceBlockers>;
28
+ readonly generatedAt?: string;
29
+ readonly graphId?: string;
30
+ readonly workspaceId?: string;
31
+ readonly projectDependencyGraph?: DependencyGraphProjector;
32
+ }): Promise<ClientDependencyGraphModel>;
33
+ export declare function formatDependencyGraphDot(model: Pick<ClientDependencyGraphModel, "nodes" | "edges">): string;