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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,528 @@
1
+ // @bun
2
+ // packages/blocker-classifier-plugin/src/analysis/taskGraphPrimitives.ts
3
+ import {
4
+ OPERATOR_INACTIVE_RUN_STATUSES
5
+ } from "@rig/contracts";
6
+ function isObjectRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ function readStringList(value) {
10
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
11
+ }
12
+ function unique(values) {
13
+ return Array.from(new Set(values));
14
+ }
15
+ function readTaskMetadataStringList(task, key) {
16
+ const taskRecord = task;
17
+ const topLevel = readStringList(taskRecord[key]);
18
+ if (topLevel.length > 0)
19
+ return topLevel;
20
+ const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
21
+ const metadataList = readStringList(metadata?.[key]);
22
+ if (metadataList.length > 0)
23
+ return metadataList;
24
+ if (key === "dependencies") {
25
+ return readStringList(metadata?.deps);
26
+ }
27
+ return [];
28
+ }
29
+ function readTaskBlockingDependencyRefs(task) {
30
+ return readTaskMetadataStringList(task, "dependencies");
31
+ }
32
+ function readTaskDependencyRefs(task) {
33
+ return unique([
34
+ ...readTaskMetadataStringList(task, "dependencies"),
35
+ ...readTaskMetadataStringList(task, "parentChildDeps")
36
+ ]);
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 readTaskScope(task) {
50
+ const taskRecord = task;
51
+ const topLevel = readStringList(taskRecord.scope);
52
+ if (topLevel.length > 0)
53
+ return unique(topLevel.map((entry) => entry.trim()).filter((entry) => entry.length > 0));
54
+ const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
55
+ const metadataScope = readStringList(metadata?.scope);
56
+ if (metadataScope.length > 0)
57
+ return unique(metadataScope.map((entry) => entry.trim()).filter((entry) => entry.length > 0));
58
+ return unique([
59
+ ...readStringList(metadata?.files),
60
+ ...readStringList(metadata?.paths)
61
+ ].map((entry) => entry.trim()).filter((entry) => entry.length > 0));
62
+ }
63
+ function isScopeList(input) {
64
+ return Array.isArray(input);
65
+ }
66
+ function normalizeScopeInput(input) {
67
+ return isScopeList(input) ? unique(input.map((entry) => entry.trim()).filter((entry) => entry.length > 0)) : readTaskScope(input);
68
+ }
69
+ function disjointScope(left, right) {
70
+ const leftScope = new Set(normalizeScopeInput(left));
71
+ if (leftScope.size === 0)
72
+ return true;
73
+ return normalizeScopeInput(right).every((entry) => !leftScope.has(entry));
74
+ }
75
+ function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
76
+ if (tasksById.has(ref))
77
+ return ref;
78
+ return taskIdBySourceIssueId.get(ref) ?? taskIdByExternalRef.get(ref) ?? null;
79
+ }
80
+ function buildTaskReferenceIndex(tasks) {
81
+ return {
82
+ tasksById: new Map(tasks.map((task) => [task.id, task])),
83
+ taskIdByExternalRef: new Map(tasks.flatMap((task) => task.externalId ? [[task.externalId, task.id]] : [])),
84
+ taskIdBySourceIssueId: new Map(tasks.flatMap((task) => {
85
+ const sourceIssueId = readTaskSourceIssueId(task);
86
+ return sourceIssueId ? [[sourceIssueId, task.id]] : [];
87
+ }))
88
+ };
89
+ }
90
+ function computeTaskBlockingDepths(tasks) {
91
+ const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
92
+ const memo = new Map;
93
+ const visit = (taskId, stack) => {
94
+ const cached = memo.get(taskId);
95
+ if (cached !== undefined)
96
+ return cached;
97
+ if (stack.has(taskId))
98
+ return 0;
99
+ const task = tasksById.get(taskId);
100
+ if (!task)
101
+ return 0;
102
+ stack.add(taskId);
103
+ const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
104
+ const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
105
+ stack.delete(taskId);
106
+ memo.set(taskId, depth);
107
+ return depth;
108
+ };
109
+ for (const task of tasks) {
110
+ visit(task.id, new Set);
111
+ }
112
+ return memo;
113
+ }
114
+ function isTaskTerminalStatus(status) {
115
+ switch (status) {
116
+ case "closed":
117
+ case "completed":
118
+ case "done":
119
+ case "cancelled":
120
+ case "canceled":
121
+ return true;
122
+ default:
123
+ return false;
124
+ }
125
+ }
126
+ function isTaskBlockedStatus(status) {
127
+ return status === "blocked";
128
+ }
129
+ function isTaskRunnableStatus(status) {
130
+ if (status === null || status === undefined || status === "")
131
+ return true;
132
+ if (isTaskTerminalStatus(status) || isTaskBlockedStatus(status))
133
+ return false;
134
+ switch (status) {
135
+ case "ready":
136
+ case "open":
137
+ case "failed":
138
+ return true;
139
+ default:
140
+ return false;
141
+ }
142
+ }
143
+ function computeTaskDependencyBadges(tasks) {
144
+ const index = buildTaskReferenceIndex(tasks);
145
+ const blockingDepths = computeTaskBlockingDepths(tasks);
146
+ const dependencyIdsByTask = new Map;
147
+ const unresolvedRefsByTask = new Map;
148
+ const blocksByTask = new Map;
149
+ for (const task of tasks) {
150
+ const dependencyIds = [];
151
+ const unresolvedRefs = [];
152
+ for (const ref of readTaskBlockingDependencyRefs(task)) {
153
+ const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
154
+ if (dependencyId && dependencyId !== task.id) {
155
+ dependencyIds.push(dependencyId);
156
+ const blocks = blocksByTask.get(dependencyId);
157
+ if (blocks) {
158
+ blocks.push(task.id);
159
+ } else {
160
+ blocksByTask.set(dependencyId, [task.id]);
161
+ }
162
+ } else {
163
+ unresolvedRefs.push(ref);
164
+ }
165
+ }
166
+ dependencyIdsByTask.set(task.id, unique(dependencyIds));
167
+ unresolvedRefsByTask.set(task.id, unique(unresolvedRefs));
168
+ }
169
+ const summaries = new Map;
170
+ for (const task of tasks) {
171
+ const dependencyIds = dependencyIdsByTask.get(task.id) ?? [];
172
+ const unresolvedDependencyRefs = unresolvedRefsByTask.get(task.id) ?? [];
173
+ const blockedBy = dependencyIds.filter((dependencyId) => {
174
+ const dependency = index.tasksById.get(dependencyId);
175
+ return dependency ? !isTaskTerminalStatus(dependency.status) : false;
176
+ });
177
+ const blocks = unique(blocksByTask.get(task.id) ?? []);
178
+ const blocked = isTaskBlockedStatus(task.status) || blockedBy.length > 0;
179
+ const ready = isTaskRunnableStatus(task.status) && !blocked;
180
+ const badges = [];
181
+ if (blocked) {
182
+ badges.push({
183
+ kind: "blocked",
184
+ label: blockedBy.length > 0 ? `blocked \xD7${blockedBy.length}` : "blocked",
185
+ description: blockedBy.length > 0 ? `Waiting on ${blockedBy.join(", ")}.` : "Task source marks this task blocked.",
186
+ ...blockedBy.length > 0 ? { count: blockedBy.length } : {},
187
+ taskIds: blockedBy
188
+ });
189
+ } else if (ready) {
190
+ badges.push({
191
+ kind: "ready",
192
+ label: "ready",
193
+ description: "No open dependencies block this task."
194
+ });
195
+ }
196
+ if (dependencyIds.length > 0 || blocks.length > 0 || unresolvedDependencyRefs.length > 0) {
197
+ badges.push({
198
+ kind: "dependency",
199
+ label: `deps ${dependencyIds.length}/${blocks.length}`,
200
+ description: [
201
+ dependencyIds.length > 0 ? `Depends on ${dependencyIds.join(", ")}.` : null,
202
+ blocks.length > 0 ? `Blocks ${blocks.join(", ")}.` : null,
203
+ unresolvedDependencyRefs.length > 0 ? `Unresolved refs: ${unresolvedDependencyRefs.join(", ")}.` : null
204
+ ].filter((part) => part !== null).join(" "),
205
+ count: dependencyIds.length + blocks.length,
206
+ taskIds: unique([...dependencyIds, ...blocks])
207
+ });
208
+ }
209
+ summaries.set(task.id, {
210
+ taskId: task.id,
211
+ blockingDepth: blockingDepths.get(task.id) ?? 0,
212
+ dependencyIds,
213
+ unresolvedDependencyRefs,
214
+ blockedBy,
215
+ blocks,
216
+ blocked,
217
+ ready,
218
+ dependencyCount: dependencyIds.length,
219
+ dependentCount: blocks.length,
220
+ badges
221
+ });
222
+ }
223
+ return summaries;
224
+ }
225
+ var TASK_STATUSES = new Set([
226
+ "draft",
227
+ "open",
228
+ "ready",
229
+ "queued",
230
+ "running",
231
+ "in_progress",
232
+ "under_review",
233
+ "blocked",
234
+ "unknown",
235
+ "completed",
236
+ "failed",
237
+ "cancelled",
238
+ "closed"
239
+ ]);
240
+ function stringValue(value) {
241
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
242
+ }
243
+ function stringArray(value) {
244
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim()) : [];
245
+ }
246
+ function numberOrNull(value) {
247
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
248
+ }
249
+ function metadataOf(task) {
250
+ return isObjectRecord(task.metadata) ? task.metadata : {};
251
+ }
252
+ function normalizeTaskStatus(status) {
253
+ const token = typeof status === "string" ? status.trim().toLowerCase() : "";
254
+ if (token === "done")
255
+ return "completed";
256
+ if (token === "canceled")
257
+ return "cancelled";
258
+ return TASK_STATUSES.has(token) ? token : "unknown";
259
+ }
260
+ function toTaskDependencyProjection(task) {
261
+ const metadata = metadataOf(task);
262
+ return {
263
+ id: String(task.id),
264
+ title: stringValue(task.title),
265
+ status: normalizeTaskStatus(task.status),
266
+ priority: numberOrNull(task.priority),
267
+ metadata,
268
+ externalId: stringValue(task.externalId),
269
+ sourceIssueId: stringValue(task.sourceIssueId),
270
+ dependencies: stringArray(task.dependencies),
271
+ parentChildDeps: stringArray(task.parentChildDeps),
272
+ createdAt: stringValue(task.createdAt) ?? "",
273
+ updatedAt: stringValue(task.updatedAt) ?? "",
274
+ role: stringValue(task.role),
275
+ scope: stringArray(task.scope),
276
+ validationKeys: stringArray(task.validationKeys),
277
+ labels: stringArray(task.labels),
278
+ assignees: task.assignees ?? null,
279
+ assignedTo: task.assignedTo ?? null
280
+ };
281
+ }
282
+ function toTaskSummary(task, defaults = {}) {
283
+ const projection = toTaskDependencyProjection(task);
284
+ const metadata = metadataOf(task);
285
+ const createdAt = stringValue(task.createdAt) ?? "1970-01-01T00:00:00.000Z";
286
+ const updatedAt = stringValue(task.updatedAt) ?? createdAt;
287
+ return {
288
+ id: projection.id,
289
+ workspaceId: stringValue(task.workspaceId) ?? defaults.workspaceId ?? "workspace",
290
+ graphId: stringValue(task.graphId) ?? defaults.graphId ?? null,
291
+ externalId: projection.externalId,
292
+ title: projection.title ?? projection.id,
293
+ description: stringValue(task.description) ?? stringValue(task.body) ?? "",
294
+ status: projection.status,
295
+ priority: numberOrNull(task.priority),
296
+ role: projection.role,
297
+ scope: [...projection.scope ?? []],
298
+ validationKeys: [...projection.validationKeys ?? []],
299
+ ...projection.sourceIssueId ? { sourceIssueId: projection.sourceIssueId } : {},
300
+ dependencies: [...projection.dependencies ?? []],
301
+ parentChildDeps: [...projection.parentChildDeps ?? []],
302
+ metadata,
303
+ createdAt,
304
+ updatedAt
305
+ };
306
+ }
307
+ function taskPriorityValue(task) {
308
+ return typeof task.priority === "number" && Number.isFinite(task.priority) ? task.priority : Number.MAX_SAFE_INTEGER;
309
+ }
310
+ function selectNextReadyTaskByPriority(tasks, options = {}) {
311
+ const excluded = new Set(options.excludeTaskIds ?? []);
312
+ const badges = computeTaskDependencyBadges(tasks);
313
+ 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) => {
314
+ const priorityDelta = taskPriorityValue(left) - taskPriorityValue(right);
315
+ if (priorityDelta !== 0)
316
+ return priorityDelta;
317
+ const createdDelta = (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
318
+ if (createdDelta !== 0)
319
+ return createdDelta;
320
+ return left.id.localeCompare(right.id);
321
+ });
322
+ return candidates[0] ?? null;
323
+ }
324
+ function projectTaskStatusForGrouping(status) {
325
+ switch (status) {
326
+ case "failed":
327
+ return "ready";
328
+ case "closed":
329
+ return "completed";
330
+ case "running":
331
+ case "in_progress":
332
+ case "under_review":
333
+ return "running";
334
+ case null:
335
+ case undefined:
336
+ case "":
337
+ return "unknown";
338
+ default:
339
+ return status;
340
+ }
341
+ }
342
+ function projectRunStatusForTaskGrouping(status) {
343
+ switch (status) {
344
+ case "created":
345
+ case "queued":
346
+ case "preparing":
347
+ return "queued";
348
+ case "running":
349
+ case "waiting-approval":
350
+ case "waiting-user-input":
351
+ case "paused":
352
+ case "validating":
353
+ case "reviewing":
354
+ case "closing-out":
355
+ return "running";
356
+ case "needs-attention":
357
+ return "blocked";
358
+ case "failed":
359
+ case "stopped":
360
+ return "ready";
361
+ case "completed":
362
+ return "completed";
363
+ case null:
364
+ case undefined:
365
+ return null;
366
+ }
367
+ }
368
+ var TASK_GROUP_STATUS_PRIORITY = [
369
+ "running",
370
+ "blocked",
371
+ "queued",
372
+ "ready",
373
+ "open",
374
+ "draft",
375
+ "unknown",
376
+ "cancelled",
377
+ "completed"
378
+ ];
379
+ function taskIdFromSession(session) {
380
+ return session.taskId ?? session.record.taskId ?? null;
381
+ }
382
+ function latestSessionByTask(sessions) {
383
+ const byTask = new Map;
384
+ for (const session of sessions) {
385
+ const taskId = taskIdFromSession(session);
386
+ if (!taskId)
387
+ continue;
388
+ const existing = byTask.get(taskId);
389
+ if (!existing || (session.lastEventAt ?? "").localeCompare(existing.lastEventAt ?? "") >= 0) {
390
+ byTask.set(taskId, session);
391
+ }
392
+ }
393
+ return byTask;
394
+ }
395
+ function projectTaskStatusWithSessions(task, sessionsByTask = new Map) {
396
+ const sessionStatus = projectRunStatusForTaskGrouping(sessionsByTask.get(task.id)?.status);
397
+ return sessionStatus ?? projectTaskStatusForGrouping(task.status);
398
+ }
399
+ function groupTasksByProjectedStatus(tasks, sessions = []) {
400
+ const sessionsByTask = latestSessionByTask(sessions);
401
+ const byStatus = new Map;
402
+ for (const task of tasks) {
403
+ const projectedStatus = projectTaskStatusWithSessions(task, sessionsByTask);
404
+ const group = byStatus.get(projectedStatus);
405
+ if (group) {
406
+ group.push(task);
407
+ } else {
408
+ byStatus.set(projectedStatus, [task]);
409
+ }
410
+ }
411
+ const result = [];
412
+ for (const status of TASK_GROUP_STATUS_PRIORITY) {
413
+ const group = byStatus.get(status);
414
+ if (group && group.length > 0) {
415
+ result.push({ status, tasks: group });
416
+ }
417
+ }
418
+ const overflowStatuses = Array.from(byStatus.keys()).filter((status) => !TASK_GROUP_STATUS_PRIORITY.includes(status)).sort();
419
+ for (const status of overflowStatuses) {
420
+ const group = byStatus.get(status);
421
+ if (group && group.length > 0) {
422
+ result.push({ status, tasks: group });
423
+ }
424
+ }
425
+ return result;
426
+ }
427
+ function selectTasksGroupedByStatus(input) {
428
+ const filteredTasks = input.workspaceId ? input.tasks.filter((task) => {
429
+ const workspaceTask = task;
430
+ return workspaceTask.workspaceId === input.workspaceId;
431
+ }) : input.tasks;
432
+ return groupTasksByProjectedStatus(filteredTasks, input.sessions);
433
+ }
434
+ function normalizeLogin(value) {
435
+ return value.trim().replace(/^@+/, "").toLowerCase();
436
+ }
437
+ function assigneeLoginsFromValue(value) {
438
+ if (!Array.isArray(value))
439
+ return [];
440
+ return value.flatMap((entry) => {
441
+ if (typeof entry === "string" && entry.trim())
442
+ return [normalizeLogin(entry)];
443
+ if (isObjectRecord(entry) && typeof entry.login === "string" && entry.login.trim()) {
444
+ return [normalizeLogin(entry.login)];
445
+ }
446
+ return [];
447
+ });
448
+ }
449
+ function normalizeTaskAssigneeFilter(assignee, currentUserLogin) {
450
+ const trimmed = assignee?.trim();
451
+ if (!trimmed)
452
+ return null;
453
+ if (trimmed === "@me" || trimmed.toLowerCase() === "me") {
454
+ return currentUserLogin?.trim() ? normalizeLogin(currentUserLogin) : null;
455
+ }
456
+ return normalizeLogin(trimmed);
457
+ }
458
+ function readTaskAssigneeLogins(task) {
459
+ const taskRecord = task;
460
+ const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
461
+ const raw = isObjectRecord(metadata?.raw) ? metadata.raw : null;
462
+ return Array.from(new Set([
463
+ ...assigneeLoginsFromValue(taskRecord.assignees),
464
+ ...assigneeLoginsFromValue(metadata?.assignees),
465
+ ...assigneeLoginsFromValue(raw?.assignees)
466
+ ]));
467
+ }
468
+ function taskMatchesAssigneeFilter(task, assignee, options = {}) {
469
+ const normalized = normalizeTaskAssigneeFilter(assignee, options.currentUserLogin);
470
+ if (!normalized)
471
+ return false;
472
+ return readTaskAssigneeLogins(task).includes(normalized);
473
+ }
474
+ function selectTasksAssignedTo(tasks, assignee, options = {}) {
475
+ return tasks.filter((task) => taskMatchesAssigneeFilter(task, assignee, options));
476
+ }
477
+ function selectTasksAssignedToMe(tasks, currentUserLogin) {
478
+ return selectTasksAssignedTo(tasks, "@me", currentUserLogin === undefined ? {} : { currentUserLogin });
479
+ }
480
+ function latestRunByTaskId(runs) {
481
+ const byTask = new Map;
482
+ const stamp = (run) => Date.parse(run.updatedAt ?? run.startedAt ?? "") || 0;
483
+ for (const run of runs) {
484
+ if (!run.taskId)
485
+ continue;
486
+ const current = byTask.get(run.taskId);
487
+ if (!current || stamp(run) >= stamp(current))
488
+ byTask.set(run.taskId, run);
489
+ }
490
+ return byTask;
491
+ }
492
+ function normalizeRunStatusToken(status) {
493
+ return String(status ?? "").trim().toLowerCase().replace(/[\s_]+/g, "-");
494
+ }
495
+ function isOperatorActiveRunStatus(status) {
496
+ const normalized = normalizeRunStatusToken(status);
497
+ if (!normalized)
498
+ return false;
499
+ return !OPERATOR_INACTIVE_RUN_STATUSES.has(normalized);
500
+ }
501
+ export {
502
+ toTaskSummary,
503
+ toTaskDependencyProjection,
504
+ taskMatchesAssigneeFilter,
505
+ selectTasksGroupedByStatus,
506
+ selectTasksAssignedToMe,
507
+ selectTasksAssignedTo,
508
+ selectNextReadyTaskByPriority,
509
+ resolveTaskReference,
510
+ readTaskSourceIssueId,
511
+ readTaskScope,
512
+ readTaskMetadataStringList,
513
+ readTaskDependencyRefs,
514
+ readTaskBlockingDependencyRefs,
515
+ readTaskAssigneeLogins,
516
+ projectTaskStatusWithSessions,
517
+ projectTaskStatusForGrouping,
518
+ projectRunStatusForTaskGrouping,
519
+ normalizeTaskStatus,
520
+ normalizeTaskAssigneeFilter,
521
+ latestRunByTaskId,
522
+ isTaskTerminalStatus,
523
+ isOperatorActiveRunStatus,
524
+ disjointScope,
525
+ computeTaskDependencyBadges,
526
+ computeTaskBlockingDepths,
527
+ buildTaskReferenceIndex
528
+ };
@@ -1,2 +1 @@
1
- export * from "./blockers";
2
1
  export * from "./plugin";