@h-rig/blocker-classifier-plugin 0.0.6-alpha.157 → 0.0.6-alpha.159
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/{blockers.d.ts → analysis/blockers.d.ts} +1 -2
- package/dist/src/analysis/blockers.js +355 -0
- package/dist/src/analysis/taskGraphPrimitives.d.ts +58 -0
- package/dist/src/analysis/taskGraphPrimitives.js +528 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +277 -21
- package/dist/src/plugin.d.ts +1 -7
- package/dist/src/plugin.js +274 -21
- package/package.json +3 -4
- package/dist/src/blockers.js +0 -101
package/dist/src/index.js
CHANGED
|
@@ -1,14 +1,272 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
2
|
+
// packages/blocker-classifier-plugin/src/plugin.ts
|
|
3
|
+
import { definePlugin } from "@rig/core/config";
|
|
4
|
+
import { defineCapability } from "@rig/core/capability";
|
|
5
|
+
import { requireCapabilityForRoot } from "@rig/core/capability-loaders";
|
|
6
|
+
import { RUN_READ_MODEL, TASK_IO_SERVICE_CAPABILITY } from "@rig/contracts";
|
|
3
7
|
|
|
4
|
-
// packages/blocker-classifier-plugin/src/
|
|
8
|
+
// packages/blocker-classifier-plugin/src/analysis/taskGraphPrimitives.ts
|
|
5
9
|
import {
|
|
6
|
-
|
|
7
|
-
isTaskTerminalStatus,
|
|
8
|
-
latestRunByTaskId,
|
|
9
|
-
readTaskMetadataStringList,
|
|
10
|
-
toTaskDependencyProjection
|
|
10
|
+
OPERATOR_INACTIVE_RUN_STATUSES
|
|
11
11
|
} from "@rig/contracts";
|
|
12
|
+
function isObjectRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function readStringList(value) {
|
|
16
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
|
|
17
|
+
}
|
|
18
|
+
function unique(values) {
|
|
19
|
+
return Array.from(new Set(values));
|
|
20
|
+
}
|
|
21
|
+
function readTaskMetadataStringList(task, key) {
|
|
22
|
+
const taskRecord = task;
|
|
23
|
+
const topLevel = readStringList(taskRecord[key]);
|
|
24
|
+
if (topLevel.length > 0)
|
|
25
|
+
return topLevel;
|
|
26
|
+
const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
|
|
27
|
+
const metadataList = readStringList(metadata?.[key]);
|
|
28
|
+
if (metadataList.length > 0)
|
|
29
|
+
return metadataList;
|
|
30
|
+
if (key === "dependencies") {
|
|
31
|
+
return readStringList(metadata?.deps);
|
|
32
|
+
}
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
function readTaskBlockingDependencyRefs(task) {
|
|
36
|
+
return readTaskMetadataStringList(task, "dependencies");
|
|
37
|
+
}
|
|
38
|
+
function readTaskSourceIssueId(task) {
|
|
39
|
+
if (typeof task.sourceIssueId === "string" && task.sourceIssueId.length > 0) {
|
|
40
|
+
return task.sourceIssueId;
|
|
41
|
+
}
|
|
42
|
+
const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
|
|
43
|
+
if (typeof metadata?.sourceIssueId === "string" && metadata.sourceIssueId.length > 0) {
|
|
44
|
+
return metadata.sourceIssueId;
|
|
45
|
+
}
|
|
46
|
+
const rigMetadata = isObjectRecord(metadata?._rig) ? metadata._rig : null;
|
|
47
|
+
return typeof rigMetadata?.sourceIssueId === "string" && rigMetadata.sourceIssueId.length > 0 ? rigMetadata.sourceIssueId : null;
|
|
48
|
+
}
|
|
49
|
+
function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
|
|
50
|
+
if (tasksById.has(ref))
|
|
51
|
+
return ref;
|
|
52
|
+
return taskIdBySourceIssueId.get(ref) ?? taskIdByExternalRef.get(ref) ?? null;
|
|
53
|
+
}
|
|
54
|
+
function buildTaskReferenceIndex(tasks) {
|
|
55
|
+
return {
|
|
56
|
+
tasksById: new Map(tasks.map((task) => [task.id, task])),
|
|
57
|
+
taskIdByExternalRef: new Map(tasks.flatMap((task) => task.externalId ? [[task.externalId, task.id]] : [])),
|
|
58
|
+
taskIdBySourceIssueId: new Map(tasks.flatMap((task) => {
|
|
59
|
+
const sourceIssueId = readTaskSourceIssueId(task);
|
|
60
|
+
return sourceIssueId ? [[sourceIssueId, task.id]] : [];
|
|
61
|
+
}))
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function computeTaskBlockingDepths(tasks) {
|
|
65
|
+
const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
|
|
66
|
+
const memo = new Map;
|
|
67
|
+
const visit = (taskId, stack) => {
|
|
68
|
+
const cached = memo.get(taskId);
|
|
69
|
+
if (cached !== undefined)
|
|
70
|
+
return cached;
|
|
71
|
+
if (stack.has(taskId))
|
|
72
|
+
return 0;
|
|
73
|
+
const task = tasksById.get(taskId);
|
|
74
|
+
if (!task)
|
|
75
|
+
return 0;
|
|
76
|
+
stack.add(taskId);
|
|
77
|
+
const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
|
|
78
|
+
const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
|
|
79
|
+
stack.delete(taskId);
|
|
80
|
+
memo.set(taskId, depth);
|
|
81
|
+
return depth;
|
|
82
|
+
};
|
|
83
|
+
for (const task of tasks) {
|
|
84
|
+
visit(task.id, new Set);
|
|
85
|
+
}
|
|
86
|
+
return memo;
|
|
87
|
+
}
|
|
88
|
+
function isTaskTerminalStatus(status) {
|
|
89
|
+
switch (status) {
|
|
90
|
+
case "closed":
|
|
91
|
+
case "completed":
|
|
92
|
+
case "done":
|
|
93
|
+
case "cancelled":
|
|
94
|
+
case "canceled":
|
|
95
|
+
return true;
|
|
96
|
+
default:
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function isTaskBlockedStatus(status) {
|
|
101
|
+
return status === "blocked";
|
|
102
|
+
}
|
|
103
|
+
function isTaskRunnableStatus(status) {
|
|
104
|
+
if (status === null || status === undefined || status === "")
|
|
105
|
+
return true;
|
|
106
|
+
if (isTaskTerminalStatus(status) || isTaskBlockedStatus(status))
|
|
107
|
+
return false;
|
|
108
|
+
switch (status) {
|
|
109
|
+
case "ready":
|
|
110
|
+
case "open":
|
|
111
|
+
case "failed":
|
|
112
|
+
return true;
|
|
113
|
+
default:
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function computeTaskDependencyBadges(tasks) {
|
|
118
|
+
const index = buildTaskReferenceIndex(tasks);
|
|
119
|
+
const blockingDepths = computeTaskBlockingDepths(tasks);
|
|
120
|
+
const dependencyIdsByTask = new Map;
|
|
121
|
+
const unresolvedRefsByTask = new Map;
|
|
122
|
+
const blocksByTask = new Map;
|
|
123
|
+
for (const task of tasks) {
|
|
124
|
+
const dependencyIds = [];
|
|
125
|
+
const unresolvedRefs = [];
|
|
126
|
+
for (const ref of readTaskBlockingDependencyRefs(task)) {
|
|
127
|
+
const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
128
|
+
if (dependencyId && dependencyId !== task.id) {
|
|
129
|
+
dependencyIds.push(dependencyId);
|
|
130
|
+
const blocks = blocksByTask.get(dependencyId);
|
|
131
|
+
if (blocks) {
|
|
132
|
+
blocks.push(task.id);
|
|
133
|
+
} else {
|
|
134
|
+
blocksByTask.set(dependencyId, [task.id]);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
unresolvedRefs.push(ref);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
dependencyIdsByTask.set(task.id, unique(dependencyIds));
|
|
141
|
+
unresolvedRefsByTask.set(task.id, unique(unresolvedRefs));
|
|
142
|
+
}
|
|
143
|
+
const summaries = new Map;
|
|
144
|
+
for (const task of tasks) {
|
|
145
|
+
const dependencyIds = dependencyIdsByTask.get(task.id) ?? [];
|
|
146
|
+
const unresolvedDependencyRefs = unresolvedRefsByTask.get(task.id) ?? [];
|
|
147
|
+
const blockedBy = dependencyIds.filter((dependencyId) => {
|
|
148
|
+
const dependency = index.tasksById.get(dependencyId);
|
|
149
|
+
return dependency ? !isTaskTerminalStatus(dependency.status) : false;
|
|
150
|
+
});
|
|
151
|
+
const blocks = unique(blocksByTask.get(task.id) ?? []);
|
|
152
|
+
const blocked = isTaskBlockedStatus(task.status) || blockedBy.length > 0;
|
|
153
|
+
const ready = isTaskRunnableStatus(task.status) && !blocked;
|
|
154
|
+
const badges = [];
|
|
155
|
+
if (blocked) {
|
|
156
|
+
badges.push({
|
|
157
|
+
kind: "blocked",
|
|
158
|
+
label: blockedBy.length > 0 ? `blocked \xD7${blockedBy.length}` : "blocked",
|
|
159
|
+
description: blockedBy.length > 0 ? `Waiting on ${blockedBy.join(", ")}.` : "Task source marks this task blocked.",
|
|
160
|
+
...blockedBy.length > 0 ? { count: blockedBy.length } : {},
|
|
161
|
+
taskIds: blockedBy
|
|
162
|
+
});
|
|
163
|
+
} else if (ready) {
|
|
164
|
+
badges.push({
|
|
165
|
+
kind: "ready",
|
|
166
|
+
label: "ready",
|
|
167
|
+
description: "No open dependencies block this task."
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (dependencyIds.length > 0 || blocks.length > 0 || unresolvedDependencyRefs.length > 0) {
|
|
171
|
+
badges.push({
|
|
172
|
+
kind: "dependency",
|
|
173
|
+
label: `deps ${dependencyIds.length}/${blocks.length}`,
|
|
174
|
+
description: [
|
|
175
|
+
dependencyIds.length > 0 ? `Depends on ${dependencyIds.join(", ")}.` : null,
|
|
176
|
+
blocks.length > 0 ? `Blocks ${blocks.join(", ")}.` : null,
|
|
177
|
+
unresolvedDependencyRefs.length > 0 ? `Unresolved refs: ${unresolvedDependencyRefs.join(", ")}.` : null
|
|
178
|
+
].filter((part) => part !== null).join(" "),
|
|
179
|
+
count: dependencyIds.length + blocks.length,
|
|
180
|
+
taskIds: unique([...dependencyIds, ...blocks])
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
summaries.set(task.id, {
|
|
184
|
+
taskId: task.id,
|
|
185
|
+
blockingDepth: blockingDepths.get(task.id) ?? 0,
|
|
186
|
+
dependencyIds,
|
|
187
|
+
unresolvedDependencyRefs,
|
|
188
|
+
blockedBy,
|
|
189
|
+
blocks,
|
|
190
|
+
blocked,
|
|
191
|
+
ready,
|
|
192
|
+
dependencyCount: dependencyIds.length,
|
|
193
|
+
dependentCount: blocks.length,
|
|
194
|
+
badges
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return summaries;
|
|
198
|
+
}
|
|
199
|
+
var TASK_STATUSES = new Set([
|
|
200
|
+
"draft",
|
|
201
|
+
"open",
|
|
202
|
+
"ready",
|
|
203
|
+
"queued",
|
|
204
|
+
"running",
|
|
205
|
+
"in_progress",
|
|
206
|
+
"under_review",
|
|
207
|
+
"blocked",
|
|
208
|
+
"unknown",
|
|
209
|
+
"completed",
|
|
210
|
+
"failed",
|
|
211
|
+
"cancelled",
|
|
212
|
+
"closed"
|
|
213
|
+
]);
|
|
214
|
+
function stringValue(value) {
|
|
215
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
216
|
+
}
|
|
217
|
+
function stringArray(value) {
|
|
218
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim()) : [];
|
|
219
|
+
}
|
|
220
|
+
function numberOrNull(value) {
|
|
221
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
222
|
+
}
|
|
223
|
+
function metadataOf(task) {
|
|
224
|
+
return isObjectRecord(task.metadata) ? task.metadata : {};
|
|
225
|
+
}
|
|
226
|
+
function normalizeTaskStatus(status) {
|
|
227
|
+
const token = typeof status === "string" ? status.trim().toLowerCase() : "";
|
|
228
|
+
if (token === "done")
|
|
229
|
+
return "completed";
|
|
230
|
+
if (token === "canceled")
|
|
231
|
+
return "cancelled";
|
|
232
|
+
return TASK_STATUSES.has(token) ? token : "unknown";
|
|
233
|
+
}
|
|
234
|
+
function toTaskDependencyProjection(task) {
|
|
235
|
+
const metadata = metadataOf(task);
|
|
236
|
+
return {
|
|
237
|
+
id: String(task.id),
|
|
238
|
+
title: stringValue(task.title),
|
|
239
|
+
status: normalizeTaskStatus(task.status),
|
|
240
|
+
priority: numberOrNull(task.priority),
|
|
241
|
+
metadata,
|
|
242
|
+
externalId: stringValue(task.externalId),
|
|
243
|
+
sourceIssueId: stringValue(task.sourceIssueId),
|
|
244
|
+
dependencies: stringArray(task.dependencies),
|
|
245
|
+
parentChildDeps: stringArray(task.parentChildDeps),
|
|
246
|
+
createdAt: stringValue(task.createdAt) ?? "",
|
|
247
|
+
updatedAt: stringValue(task.updatedAt) ?? "",
|
|
248
|
+
role: stringValue(task.role),
|
|
249
|
+
scope: stringArray(task.scope),
|
|
250
|
+
validationKeys: stringArray(task.validationKeys),
|
|
251
|
+
labels: stringArray(task.labels),
|
|
252
|
+
assignees: task.assignees ?? null,
|
|
253
|
+
assignedTo: task.assignedTo ?? null
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function latestRunByTaskId(runs) {
|
|
257
|
+
const byTask = new Map;
|
|
258
|
+
const stamp = (run) => Date.parse(run.updatedAt ?? run.startedAt ?? "") || 0;
|
|
259
|
+
for (const run of runs) {
|
|
260
|
+
if (!run.taskId)
|
|
261
|
+
continue;
|
|
262
|
+
const current = byTask.get(run.taskId);
|
|
263
|
+
if (!current || stamp(run) >= stamp(current))
|
|
264
|
+
byTask.set(run.taskId, run);
|
|
265
|
+
}
|
|
266
|
+
return byTask;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// packages/blocker-classifier-plugin/src/analysis/blockers.ts
|
|
12
270
|
var HUMAN_BLOCKERS = new Set(["human-decision", "human-approval", "external-input"]);
|
|
13
271
|
function labelsFor(task) {
|
|
14
272
|
return readTaskMetadataStringList(task, "labels").map((label) => label.toLowerCase());
|
|
@@ -94,22 +352,25 @@ async function classifyWorkspaceBlockers(projectRoot, deps) {
|
|
|
94
352
|
]);
|
|
95
353
|
return classifyTasks(tasks, runs, deps);
|
|
96
354
|
}
|
|
355
|
+
|
|
97
356
|
// packages/blocker-classifier-plugin/src/plugin.ts
|
|
98
|
-
import { definePlugin } from "@rig/core/config";
|
|
99
357
|
var BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
|
|
100
358
|
var BLOCKERS_CLI_ID = "blocker-classifier.blockers";
|
|
101
359
|
var DEFAULT_BLOCKER_CLASSIFIER_ID = "blocker-classifier.default";
|
|
102
360
|
var HUMAN_BLOCKERS_PANEL_ID = "human-blockers";
|
|
361
|
+
var RunReadModelCap = defineCapability(RUN_READ_MODEL);
|
|
103
362
|
function isRecord(value) {
|
|
104
363
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
105
364
|
}
|
|
106
365
|
function panelProjectRoot(context) {
|
|
107
366
|
return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
108
367
|
}
|
|
109
|
-
async function loadBlockerClientIo() {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
368
|
+
async function loadBlockerClientIo(projectRoot) {
|
|
369
|
+
const [taskIo, runReadModel] = await Promise.all([
|
|
370
|
+
requireCapabilityForRoot(projectRoot, defineCapability(TASK_IO_SERVICE_CAPABILITY), "No task-sources plugin provides task IO for this project root."),
|
|
371
|
+
requireCapabilityForRoot(projectRoot, RunReadModelCap, "No run-worker plugin provides run read-model for this project root.")
|
|
372
|
+
]);
|
|
373
|
+
return { listRuns: (root) => runReadModel.listRuns({ projectRoot: root }), listTasks: taskIo.listTasks };
|
|
113
374
|
}
|
|
114
375
|
function taskEpicKey(task) {
|
|
115
376
|
const direct = typeof task?.epicKey === "string" ? task.epicKey : null;
|
|
@@ -122,15 +383,15 @@ async function produceHumanBlockersPanel(context) {
|
|
|
122
383
|
const projectRoot = panelProjectRoot(context);
|
|
123
384
|
if (!projectRoot)
|
|
124
385
|
return;
|
|
125
|
-
const { listRuns, listTasks } = await loadBlockerClientIo();
|
|
126
|
-
const [tasks,
|
|
386
|
+
const { listRuns, listTasks } = await loadBlockerClientIo(projectRoot);
|
|
387
|
+
const [tasks, blockers] = await Promise.all([
|
|
127
388
|
listTasks(projectRoot),
|
|
128
389
|
classifyWorkspaceBlockers(projectRoot, { listTasks, listRuns, humanOnly: true })
|
|
129
390
|
]);
|
|
130
391
|
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
131
392
|
return {
|
|
132
393
|
humanOnly: true,
|
|
133
|
-
items:
|
|
394
|
+
items: blockers.classifications.map((classification) => {
|
|
134
395
|
const task = tasksById.get(classification.taskId);
|
|
135
396
|
return {
|
|
136
397
|
taskId: classification.taskId,
|
|
@@ -167,7 +428,7 @@ async function executeBlockers(context, args) {
|
|
|
167
428
|
const json = takeFlag(args, "--json");
|
|
168
429
|
const humanOnly = takeFlag(json.rest, "--human-only");
|
|
169
430
|
requireNoExtraArgs(humanOnly.rest, "rig blockers [--human-only] [--json]");
|
|
170
|
-
const { listRuns, listTasks } = await loadBlockerClientIo();
|
|
431
|
+
const { listRuns, listTasks } = await loadBlockerClientIo(context.projectRoot);
|
|
171
432
|
const result = await classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns, humanOnly: humanOnly.value });
|
|
172
433
|
const details = { classifications: result.classifications, human: result.human, machine: result.machine, generatedAt: result.generatedAt };
|
|
173
434
|
if (context.outputMode === "text") {
|
|
@@ -219,13 +480,8 @@ function createBlockerClassifierPlugin() {
|
|
|
219
480
|
return blockerClassifierPlugin;
|
|
220
481
|
}
|
|
221
482
|
export {
|
|
222
|
-
tierOf,
|
|
223
|
-
isHumanBlockerClass,
|
|
224
483
|
executeBlockers,
|
|
225
484
|
createBlockerClassifierPlugin,
|
|
226
|
-
classifyWorkspaceBlockers,
|
|
227
|
-
classifyTasks,
|
|
228
|
-
classifyBlocker,
|
|
229
485
|
blockerClassifierPlugin,
|
|
230
486
|
blockerClassifierCliCommands,
|
|
231
487
|
HUMAN_BLOCKERS_PANEL_ID,
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { type RuntimeCliContext } from "@rig/core/config";
|
|
2
|
-
|
|
2
|
+
import { type CommandOutcome } from "@rig/contracts";
|
|
3
3
|
export declare const BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
|
|
4
4
|
export declare const BLOCKERS_CLI_ID = "blocker-classifier.blockers";
|
|
5
5
|
export declare const DEFAULT_BLOCKER_CLASSIFIER_ID = "blocker-classifier.default";
|
|
6
6
|
export declare const HUMAN_BLOCKERS_PANEL_ID = "human-blockers";
|
|
7
|
-
type CommandOutcome = {
|
|
8
|
-
readonly ok: boolean;
|
|
9
|
-
readonly group: string;
|
|
10
|
-
readonly command: string;
|
|
11
|
-
readonly details?: Record<string, unknown>;
|
|
12
|
-
};
|
|
13
7
|
export declare function executeBlockers(context: RuntimeCliContext, args: readonly string[]): Promise<CommandOutcome>;
|
|
14
8
|
export declare const blockerClassifierCliCommands: readonly [{
|
|
15
9
|
readonly id: "blocker-classifier.blockers";
|