@h-rig/blocker-classifier-plugin 0.0.6-alpha.157 → 0.0.6-alpha.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -1,14 +1,272 @@
1
1
  // @bun
2
- var __require = import.meta.require;
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/blockers.ts
8
+ // packages/blocker-classifier-plugin/src/analysis/taskGraphPrimitives.ts
5
9
  import {
6
- computeTaskDependencyBadges,
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 { listTasks } = await import("@rig/core/task-io");
111
- const { listRuns } = await import("@rig/run-worker/runs");
112
- return { listRuns, listTasks };
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, blockers2] = await Promise.all([
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: blockers2.classifications.map((classification) => {
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,
@@ -1,15 +1,9 @@
1
1
  import { type RuntimeCliContext } from "@rig/core/config";
2
- export * from "./blockers";
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";