@h-rig/core 0.0.6-alpha.132 → 0.0.6-alpha.134
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/dependencyGraph.d.ts +43 -0
- package/dist/src/dependencyGraph.js +703 -0
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +811 -4
- package/dist/src/rigSelectors.d.ts +1 -1
- package/dist/src/rollups.d.ts +6 -0
- package/dist/src/rollups.js +377 -0
- package/dist/src/stageResolve.d.ts +84 -0
- package/dist/src/stageResolve.js +375 -0
- package/dist/src/taskGraph.d.ts +24 -1
- package/dist/src/taskGraph.js +159 -2
- package/dist/src/taskGraphLayout.d.ts +1 -0
- package/dist/src/taskGraphLayout.js +5 -1
- package/dist/src/taskScore.d.ts +17 -0
- package/dist/src/taskScore.js +49 -0
- package/package.json +22 -2
package/dist/src/index.js
CHANGED
|
@@ -2358,6 +2358,51 @@ function selectUserInputsForWorkspace(snapshot, workspaceId) {
|
|
|
2358
2358
|
const runIds = new Set(selectRunsForWorkspace(snapshot, workspaceId).map((run) => run.id));
|
|
2359
2359
|
return (snapshot.userInputs ?? []).filter((request) => runIds.has(request.runId));
|
|
2360
2360
|
}
|
|
2361
|
+
// packages/core/src/taskScore.ts
|
|
2362
|
+
var ROLE_WEIGHT = {
|
|
2363
|
+
architect: 25,
|
|
2364
|
+
extractor: 10
|
|
2365
|
+
};
|
|
2366
|
+
var CRITICALITY_WEIGHT = {
|
|
2367
|
+
core: 20,
|
|
2368
|
+
high: 10,
|
|
2369
|
+
normal: 0
|
|
2370
|
+
};
|
|
2371
|
+
function finiteNumber(value, fallback) {
|
|
2372
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2373
|
+
}
|
|
2374
|
+
function scoreTask(input) {
|
|
2375
|
+
const priority = finiteNumber(input.priority, 3);
|
|
2376
|
+
const unblockCount = Math.max(0, finiteNumber(input.unblockCount, 0));
|
|
2377
|
+
const roleWeight = input.role ? ROLE_WEIGHT[input.role] ?? 0 : 0;
|
|
2378
|
+
const criticalityWeight = input.criticality ? CRITICALITY_WEIGHT[input.criticality] ?? 0 : 0;
|
|
2379
|
+
const validationWeight = (input.validation ?? []).some((entry) => entry.includes("test-contract") || entry.includes("test-boundary")) ? 8 : 0;
|
|
2380
|
+
const queueWeight = finiteNumber(input.queueWeight, 0);
|
|
2381
|
+
return unblockCount * 100 + (10 - priority) + roleWeight + criticalityWeight + validationWeight + queueWeight;
|
|
2382
|
+
}
|
|
2383
|
+
function rankTasks(tasks, scoreInput, idOf) {
|
|
2384
|
+
return tasks.map((task) => {
|
|
2385
|
+
const input = scoreInput(task);
|
|
2386
|
+
return {
|
|
2387
|
+
task,
|
|
2388
|
+
score: scoreTask(input),
|
|
2389
|
+
priority: finiteNumber(input.priority, 3),
|
|
2390
|
+
unblockCount: Math.max(0, finiteNumber(input.unblockCount, 0))
|
|
2391
|
+
};
|
|
2392
|
+
}).toSorted((left, right) => {
|
|
2393
|
+
const scoreDelta = right.score - left.score;
|
|
2394
|
+
if (scoreDelta !== 0)
|
|
2395
|
+
return scoreDelta;
|
|
2396
|
+
const unblockDelta = right.unblockCount - left.unblockCount;
|
|
2397
|
+
if (unblockDelta !== 0)
|
|
2398
|
+
return unblockDelta;
|
|
2399
|
+
const priorityDelta = left.priority - right.priority;
|
|
2400
|
+
if (priorityDelta !== 0)
|
|
2401
|
+
return priorityDelta;
|
|
2402
|
+
return idOf(left.task).localeCompare(idOf(right.task));
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2361
2406
|
// packages/core/src/taskGraph.ts
|
|
2362
2407
|
function isObjectRecord2(value) {
|
|
2363
2408
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2382,6 +2427,9 @@ function readTaskMetadataStringList(task, key) {
|
|
|
2382
2427
|
}
|
|
2383
2428
|
return [];
|
|
2384
2429
|
}
|
|
2430
|
+
function readTaskBlockingDependencyRefs(task) {
|
|
2431
|
+
return readTaskMetadataStringList(task, "dependencies");
|
|
2432
|
+
}
|
|
2385
2433
|
function readTaskDependencyRefs(task) {
|
|
2386
2434
|
return unique([
|
|
2387
2435
|
...readTaskMetadataStringList(task, "dependencies"),
|
|
@@ -2399,6 +2447,32 @@ function readTaskSourceIssueId(task) {
|
|
|
2399
2447
|
const rigMetadata = isObjectRecord2(metadata?._rig) ? metadata._rig : null;
|
|
2400
2448
|
return typeof rigMetadata?.sourceIssueId === "string" && rigMetadata.sourceIssueId.length > 0 ? rigMetadata.sourceIssueId : null;
|
|
2401
2449
|
}
|
|
2450
|
+
function readTaskScope(task) {
|
|
2451
|
+
const taskRecord = task;
|
|
2452
|
+
const topLevel = readStringList(taskRecord.scope);
|
|
2453
|
+
if (topLevel.length > 0)
|
|
2454
|
+
return unique(topLevel.map((entry) => entry.trim()).filter((entry) => entry.length > 0));
|
|
2455
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2456
|
+
const metadataScope = readStringList(metadata?.scope);
|
|
2457
|
+
if (metadataScope.length > 0)
|
|
2458
|
+
return unique(metadataScope.map((entry) => entry.trim()).filter((entry) => entry.length > 0));
|
|
2459
|
+
return unique([
|
|
2460
|
+
...readStringList(metadata?.files),
|
|
2461
|
+
...readStringList(metadata?.paths)
|
|
2462
|
+
].map((entry) => entry.trim()).filter((entry) => entry.length > 0));
|
|
2463
|
+
}
|
|
2464
|
+
function isScopeList(input) {
|
|
2465
|
+
return Array.isArray(input);
|
|
2466
|
+
}
|
|
2467
|
+
function normalizeScopeInput(input) {
|
|
2468
|
+
return isScopeList(input) ? unique(input.map((entry) => entry.trim()).filter((entry) => entry.length > 0)) : readTaskScope(input);
|
|
2469
|
+
}
|
|
2470
|
+
function disjointScope(left, right) {
|
|
2471
|
+
const leftScope = new Set(normalizeScopeInput(left));
|
|
2472
|
+
if (leftScope.size === 0)
|
|
2473
|
+
return true;
|
|
2474
|
+
return normalizeScopeInput(right).every((entry) => !leftScope.has(entry));
|
|
2475
|
+
}
|
|
2402
2476
|
function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
|
|
2403
2477
|
if (tasksById.has(ref))
|
|
2404
2478
|
return ref;
|
|
@@ -2427,7 +2501,7 @@ function computeTaskBlockingDepths(tasks) {
|
|
|
2427
2501
|
if (!task)
|
|
2428
2502
|
return 0;
|
|
2429
2503
|
stack.add(taskId);
|
|
2430
|
-
const blockers =
|
|
2504
|
+
const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
|
|
2431
2505
|
const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
|
|
2432
2506
|
stack.delete(taskId);
|
|
2433
2507
|
memo.set(taskId, depth);
|
|
@@ -2470,6 +2544,39 @@ function isTaskRunnableStatus(status) {
|
|
|
2470
2544
|
function priorityValue(task) {
|
|
2471
2545
|
return typeof task.priority === "number" && Number.isFinite(task.priority) ? task.priority : Number.MAX_SAFE_INTEGER;
|
|
2472
2546
|
}
|
|
2547
|
+
function readTaskRole(task) {
|
|
2548
|
+
if (typeof task.role === "string" && task.role.trim())
|
|
2549
|
+
return task.role.trim();
|
|
2550
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2551
|
+
return typeof metadata?.role === "string" && metadata.role.trim() ? metadata.role.trim() : null;
|
|
2552
|
+
}
|
|
2553
|
+
function readTaskCriticality(task) {
|
|
2554
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2555
|
+
return typeof metadata?.criticality === "string" && metadata.criticality.trim() ? metadata.criticality.trim() : null;
|
|
2556
|
+
}
|
|
2557
|
+
function readTaskValidationKeys(task) {
|
|
2558
|
+
const taskRecord = task;
|
|
2559
|
+
const topLevel = readStringList(taskRecord.validationKeys);
|
|
2560
|
+
if (topLevel.length > 0)
|
|
2561
|
+
return topLevel;
|
|
2562
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2563
|
+
return readStringList(metadata?.validation);
|
|
2564
|
+
}
|
|
2565
|
+
function readTaskQueueWeight(task) {
|
|
2566
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2567
|
+
const queueWeight = metadata?.queueWeight ?? metadata?.queue_weight;
|
|
2568
|
+
return typeof queueWeight === "number" && Number.isFinite(queueWeight) ? queueWeight : 0;
|
|
2569
|
+
}
|
|
2570
|
+
function scoreInputForTask(task, unblockCount) {
|
|
2571
|
+
return {
|
|
2572
|
+
priority: task.priority,
|
|
2573
|
+
unblockCount,
|
|
2574
|
+
role: readTaskRole(task),
|
|
2575
|
+
criticality: readTaskCriticality(task),
|
|
2576
|
+
validation: readTaskValidationKeys(task),
|
|
2577
|
+
queueWeight: readTaskQueueWeight(task)
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2473
2580
|
function computeTaskDependencyBadges(tasks) {
|
|
2474
2581
|
const index = buildTaskReferenceIndex(tasks);
|
|
2475
2582
|
const blockingDepths = computeTaskBlockingDepths(tasks);
|
|
@@ -2479,7 +2586,7 @@ function computeTaskDependencyBadges(tasks) {
|
|
|
2479
2586
|
for (const task of tasks) {
|
|
2480
2587
|
const dependencyIds = [];
|
|
2481
2588
|
const unresolvedRefs = [];
|
|
2482
|
-
for (const ref of
|
|
2589
|
+
for (const ref of readTaskBlockingDependencyRefs(task)) {
|
|
2483
2590
|
const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
2484
2591
|
if (dependencyId && dependencyId !== task.id) {
|
|
2485
2592
|
dependencyIds.push(dependencyId);
|
|
@@ -2566,6 +2673,51 @@ function selectNextReadyTaskByPriority(tasks, options = {}) {
|
|
|
2566
2673
|
});
|
|
2567
2674
|
return candidates[0] ?? null;
|
|
2568
2675
|
}
|
|
2676
|
+
function rankReadyTasks(tasks, options = {}) {
|
|
2677
|
+
const excluded = new Set(options.excludeTaskIds ?? []);
|
|
2678
|
+
const activeTaskIds = new Set(options.activeTaskIds ?? []);
|
|
2679
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
2680
|
+
const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
|
|
2681
|
+
const openBlockCount = (task) => (badges.get(task.id)?.blocks ?? []).filter((blockedTaskId) => {
|
|
2682
|
+
const blockedTask = tasksById.get(blockedTaskId);
|
|
2683
|
+
return blockedTask ? !isTaskTerminalStatus(blockedTask.status) : false;
|
|
2684
|
+
}).length;
|
|
2685
|
+
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);
|
|
2686
|
+
const maxUnblockCount = Math.max(0, ...readyTasks.map(openBlockCount));
|
|
2687
|
+
const selectedByMode = readyTasks.filter((task) => {
|
|
2688
|
+
const unblockCount = openBlockCount(task);
|
|
2689
|
+
if (options.selection === "blocking-only")
|
|
2690
|
+
return unblockCount > 0;
|
|
2691
|
+
if (options.selection === "max-unblock")
|
|
2692
|
+
return maxUnblockCount > 0 && unblockCount === maxUnblockCount;
|
|
2693
|
+
return true;
|
|
2694
|
+
});
|
|
2695
|
+
return rankTasks(selectedByMode, (task) => scoreInputForTask(task, openBlockCount(task)), (task) => task.id).map((entry) => ({
|
|
2696
|
+
task: entry.task,
|
|
2697
|
+
score: entry.score,
|
|
2698
|
+
priority: entry.priority,
|
|
2699
|
+
unblockCount: entry.unblockCount,
|
|
2700
|
+
scope: readTaskScope(entry.task)
|
|
2701
|
+
}));
|
|
2702
|
+
}
|
|
2703
|
+
function selectRankedReadyTasks(tasks, options = {}) {
|
|
2704
|
+
const ranked = rankReadyTasks(tasks, options);
|
|
2705
|
+
if (options.requireDisjointScopes !== true && !options.disjointWithScopes) {
|
|
2706
|
+
return ranked.slice(0, options.limit).map((entry) => entry.task);
|
|
2707
|
+
}
|
|
2708
|
+
const occupiedScopes = new Set(options.disjointWithScopes ?? []);
|
|
2709
|
+
const selected = [];
|
|
2710
|
+
for (const entry of ranked) {
|
|
2711
|
+
if (entry.scope.some((scope) => occupiedScopes.has(scope)))
|
|
2712
|
+
continue;
|
|
2713
|
+
selected.push(entry.task);
|
|
2714
|
+
for (const scope of entry.scope)
|
|
2715
|
+
occupiedScopes.add(scope);
|
|
2716
|
+
if (options.limit !== undefined && selected.length >= options.limit)
|
|
2717
|
+
break;
|
|
2718
|
+
}
|
|
2719
|
+
return selected;
|
|
2720
|
+
}
|
|
2569
2721
|
// packages/core/src/taskGraphCodes.ts
|
|
2570
2722
|
var TASK_CODE_RE = /^\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]\s*/;
|
|
2571
2723
|
function extractTaskCode(title) {
|
|
@@ -2610,7 +2762,10 @@ function isObjectRecord3(value) {
|
|
|
2610
2762
|
}
|
|
2611
2763
|
function readIssueType(task) {
|
|
2612
2764
|
const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
|
|
2613
|
-
|
|
2765
|
+
if (typeof metadata?.issueType === "string")
|
|
2766
|
+
return metadata.issueType;
|
|
2767
|
+
const raw = isObjectRecord3(metadata?.raw) ? metadata.raw : null;
|
|
2768
|
+
return typeof raw?.issueType === "string" ? raw.issueType : null;
|
|
2614
2769
|
}
|
|
2615
2770
|
function isGraphTask(task) {
|
|
2616
2771
|
return readIssueType(task) !== "epic";
|
|
@@ -2857,6 +3012,7 @@ function buildTaskGraphLayout(snapshot, tasks, options) {
|
|
|
2857
3012
|
const artifactCount = (snapshot?.artifacts ?? []).filter((artifact) => runIds.has(artifact.runId)).length;
|
|
2858
3013
|
nodes.push({
|
|
2859
3014
|
id: task.id,
|
|
3015
|
+
taskId: task.id,
|
|
2860
3016
|
task,
|
|
2861
3017
|
rowKey,
|
|
2862
3018
|
rowLabel: rowLabelByKey.get(rowKey) ?? rowKey,
|
|
@@ -2891,6 +3047,645 @@ function buildTaskGraphLayout(snapshot, tasks, options) {
|
|
|
2891
3047
|
taskCount: graphTasks.length
|
|
2892
3048
|
};
|
|
2893
3049
|
}
|
|
3050
|
+
// packages/core/src/stageResolve.ts
|
|
3051
|
+
class PipelineUnresolvableError extends Error {
|
|
3052
|
+
cycles;
|
|
3053
|
+
contributors;
|
|
3054
|
+
constructor(message, cycles, contributors) {
|
|
3055
|
+
super(message);
|
|
3056
|
+
this.name = "PipelineUnresolvableError";
|
|
3057
|
+
this.cycles = cycles;
|
|
3058
|
+
this.contributors = contributors;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
function uniqueSorted(values) {
|
|
3062
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
3063
|
+
}
|
|
3064
|
+
function wrapperForMutation(mutation) {
|
|
3065
|
+
return "wrapper" in mutation ? mutation.wrapper : mutation.around;
|
|
3066
|
+
}
|
|
3067
|
+
function contributorOf(mutation) {
|
|
3068
|
+
if (mutation.contributedBy?.trim())
|
|
3069
|
+
return mutation.contributedBy;
|
|
3070
|
+
if (mutation.op === "wrap") {
|
|
3071
|
+
const wrapper = wrapperForMutation(mutation);
|
|
3072
|
+
if (wrapper.id?.trim())
|
|
3073
|
+
return wrapper.id;
|
|
3074
|
+
}
|
|
3075
|
+
return "anonymous";
|
|
3076
|
+
}
|
|
3077
|
+
function stableMutationCompare(left, right) {
|
|
3078
|
+
const leftTarget = left.op === "insert" ? left.stage.id : left.id;
|
|
3079
|
+
const rightTarget = right.op === "insert" ? right.stage.id : right.id;
|
|
3080
|
+
const targetDelta = leftTarget.localeCompare(rightTarget);
|
|
3081
|
+
if (targetDelta !== 0)
|
|
3082
|
+
return targetDelta;
|
|
3083
|
+
return contributorOf(left).localeCompare(contributorOf(right));
|
|
3084
|
+
}
|
|
3085
|
+
function ensureUniqueStageIds(stages) {
|
|
3086
|
+
const seen = new Set;
|
|
3087
|
+
for (const stage of stages) {
|
|
3088
|
+
if (seen.has(stage.id)) {
|
|
3089
|
+
throw new PipelineUnresolvableError(`Duplicate stage id: ${stage.id}`, [], []);
|
|
3090
|
+
}
|
|
3091
|
+
seen.add(stage.id);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
function assertNoDuplicateMutationTargets(mutations, op) {
|
|
3095
|
+
const seen = new Map;
|
|
3096
|
+
for (const mutation of mutations.filter((entry) => entry.op === op)) {
|
|
3097
|
+
const id = mutation.op === "insert" ? mutation.stage.id : mutation.id;
|
|
3098
|
+
const previous = seen.get(id);
|
|
3099
|
+
if (previous) {
|
|
3100
|
+
throw new PipelineUnresolvableError(`Duplicate ${op} mutation for stage ${id}: ${previous}, ${contributorOf(mutation)}`, [], uniqueSorted([previous, contributorOf(mutation)]));
|
|
3101
|
+
}
|
|
3102
|
+
seen.set(id, contributorOf(mutation));
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
function hasProtectedGrant(grants, stageId, pluginId, op) {
|
|
3106
|
+
return grants.some((grant) => grant.stageId === stageId && grant.pluginId === pluginId && (grant.op === undefined || grant.op === op));
|
|
3107
|
+
}
|
|
3108
|
+
function mergeAnchors(current, incoming) {
|
|
3109
|
+
return uniqueSorted([...current, ...incoming ?? []]);
|
|
3110
|
+
}
|
|
3111
|
+
function stagePriority(stage) {
|
|
3112
|
+
return typeof stage.priority === "number" && Number.isFinite(stage.priority) ? stage.priority : 0;
|
|
3113
|
+
}
|
|
3114
|
+
function readyCompare(states) {
|
|
3115
|
+
return (left, right) => {
|
|
3116
|
+
const leftState = states.get(left);
|
|
3117
|
+
const rightState = states.get(right);
|
|
3118
|
+
const priorityDelta = stagePriority(rightState?.stage ?? { id: right }) - stagePriority(leftState?.stage ?? { id: left });
|
|
3119
|
+
if (priorityDelta !== 0)
|
|
3120
|
+
return priorityDelta;
|
|
3121
|
+
if (leftState?.baseIndex !== null && rightState?.baseIndex !== null && leftState?.baseIndex !== rightState?.baseIndex) {
|
|
3122
|
+
return (leftState?.baseIndex ?? 0) - (rightState?.baseIndex ?? 0);
|
|
3123
|
+
}
|
|
3124
|
+
if (leftState?.baseIndex !== null && rightState?.baseIndex === null)
|
|
3125
|
+
return -1;
|
|
3126
|
+
if (leftState?.baseIndex === null && rightState?.baseIndex !== null)
|
|
3127
|
+
return 1;
|
|
3128
|
+
return left.localeCompare(right);
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
function findCycles(nodes, edges) {
|
|
3132
|
+
const adjacency = new Map;
|
|
3133
|
+
for (const node of nodes)
|
|
3134
|
+
adjacency.set(node, []);
|
|
3135
|
+
for (const edge of edges)
|
|
3136
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
3137
|
+
for (const targets of adjacency.values())
|
|
3138
|
+
targets.sort((left, right) => left.localeCompare(right));
|
|
3139
|
+
let nextIndex = 0;
|
|
3140
|
+
const indexByNode = new Map;
|
|
3141
|
+
const lowByNode = new Map;
|
|
3142
|
+
const stack = [];
|
|
3143
|
+
const onStack = new Set;
|
|
3144
|
+
const cycles = [];
|
|
3145
|
+
const visit = (node) => {
|
|
3146
|
+
indexByNode.set(node, nextIndex);
|
|
3147
|
+
lowByNode.set(node, nextIndex);
|
|
3148
|
+
nextIndex += 1;
|
|
3149
|
+
stack.push(node);
|
|
3150
|
+
onStack.add(node);
|
|
3151
|
+
for (const target of adjacency.get(node) ?? []) {
|
|
3152
|
+
if (!indexByNode.has(target)) {
|
|
3153
|
+
visit(target);
|
|
3154
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
|
|
3155
|
+
} else if (onStack.has(target)) {
|
|
3156
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
if (lowByNode.get(node) !== indexByNode.get(node))
|
|
3160
|
+
return;
|
|
3161
|
+
const component = [];
|
|
3162
|
+
let current;
|
|
3163
|
+
do {
|
|
3164
|
+
current = stack.pop();
|
|
3165
|
+
if (current) {
|
|
3166
|
+
onStack.delete(current);
|
|
3167
|
+
component.push(current);
|
|
3168
|
+
}
|
|
3169
|
+
} while (current && current !== node);
|
|
3170
|
+
const hasSelfLoop = edges.some((edge) => edge.from === node && edge.to === node);
|
|
3171
|
+
if (component.length > 1 || hasSelfLoop) {
|
|
3172
|
+
cycles.push(component.sort((left, right) => left.localeCompare(right)));
|
|
3173
|
+
}
|
|
3174
|
+
};
|
|
3175
|
+
for (const node of [...nodes].sort((left, right) => left.localeCompare(right))) {
|
|
3176
|
+
if (!indexByNode.has(node))
|
|
3177
|
+
visit(node);
|
|
3178
|
+
}
|
|
3179
|
+
return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
|
|
3180
|
+
}
|
|
3181
|
+
function topologicalOrder(states, edges) {
|
|
3182
|
+
const nodes = [...states.keys()];
|
|
3183
|
+
const outgoing = new Map;
|
|
3184
|
+
const indegree = new Map;
|
|
3185
|
+
for (const node of nodes) {
|
|
3186
|
+
outgoing.set(node, []);
|
|
3187
|
+
indegree.set(node, 0);
|
|
3188
|
+
}
|
|
3189
|
+
for (const edge of edges) {
|
|
3190
|
+
outgoing.get(edge.from)?.push(edge.to);
|
|
3191
|
+
indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
|
|
3192
|
+
}
|
|
3193
|
+
for (const targets of outgoing.values())
|
|
3194
|
+
targets.sort((left, right) => left.localeCompare(right));
|
|
3195
|
+
const compare = readyCompare(states);
|
|
3196
|
+
const ready = nodes.filter((node) => (indegree.get(node) ?? 0) === 0).sort(compare);
|
|
3197
|
+
const order = [];
|
|
3198
|
+
while (ready.length > 0) {
|
|
3199
|
+
const node = ready.shift();
|
|
3200
|
+
if (!node)
|
|
3201
|
+
break;
|
|
3202
|
+
order.push(node);
|
|
3203
|
+
for (const target of outgoing.get(node) ?? []) {
|
|
3204
|
+
const next = (indegree.get(target) ?? 0) - 1;
|
|
3205
|
+
indegree.set(target, next);
|
|
3206
|
+
if (next === 0) {
|
|
3207
|
+
ready.push(target);
|
|
3208
|
+
ready.sort(compare);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
if (order.length === nodes.length)
|
|
3213
|
+
return order;
|
|
3214
|
+
const cycles = findCycles(nodes, edges);
|
|
3215
|
+
const cycleMembers = new Set(cycles.flat());
|
|
3216
|
+
const contributors = uniqueSorted(edges.filter((edge) => cycleMembers.has(edge.from) && cycleMembers.has(edge.to)).flatMap((edge) => edge.contributors));
|
|
3217
|
+
throw new PipelineUnresolvableError(`Stage pipeline has unresolved cycle: ${cycles.map((cycle) => cycle.join(" -> ")).join("; ")}`, cycles, contributors);
|
|
3218
|
+
}
|
|
3219
|
+
function composeStageWrappers(state) {
|
|
3220
|
+
const wrappers = state.wrappers.toSorted((left, right) => {
|
|
3221
|
+
const priorityDelta = (right.wrapper.priority ?? 0) - (left.wrapper.priority ?? 0);
|
|
3222
|
+
if (priorityDelta !== 0)
|
|
3223
|
+
return priorityDelta;
|
|
3224
|
+
return left.contributedBy.localeCompare(right.contributedBy);
|
|
3225
|
+
});
|
|
3226
|
+
let stage = state.stage;
|
|
3227
|
+
for (const wrapper of wrappers.toReversed()) {
|
|
3228
|
+
if (wrapper.wrapper.apply)
|
|
3229
|
+
stage = wrapper.wrapper.apply(stage);
|
|
3230
|
+
}
|
|
3231
|
+
return stage;
|
|
3232
|
+
}
|
|
3233
|
+
function resolveStagePipeline(input) {
|
|
3234
|
+
ensureUniqueStageIds(input.defaultStages);
|
|
3235
|
+
const mutations = [...input.mutations ?? []];
|
|
3236
|
+
assertNoDuplicateMutationTargets(mutations, "insert");
|
|
3237
|
+
assertNoDuplicateMutationTargets(mutations, "replace");
|
|
3238
|
+
const grants = input.protectedStageGrants ?? [];
|
|
3239
|
+
const states = new Map;
|
|
3240
|
+
const removedStates = new Map;
|
|
3241
|
+
for (const [index, stage] of input.defaultStages.entries()) {
|
|
3242
|
+
states.set(stage.id, {
|
|
3243
|
+
stage,
|
|
3244
|
+
before: [...stage.before ?? []],
|
|
3245
|
+
after: [...stage.after ?? []],
|
|
3246
|
+
baseIndex: index,
|
|
3247
|
+
contributedBy: "default",
|
|
3248
|
+
wrappers: [],
|
|
3249
|
+
droppedAnchors: [],
|
|
3250
|
+
isProtected: stage.protected === true
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
for (const mutation of mutations.filter((entry) => entry.op === "remove").sort(stableMutationCompare)) {
|
|
3254
|
+
const state = states.get(mutation.id);
|
|
3255
|
+
const contributor = contributorOf(mutation);
|
|
3256
|
+
if (!state)
|
|
3257
|
+
continue;
|
|
3258
|
+
if (state.isProtected && !hasProtectedGrant(grants, mutation.id, contributor, "remove")) {
|
|
3259
|
+
throw new PipelineUnresolvableError(`Protected stage ${mutation.id} cannot be removed by ${contributor} without an explicit grant`, [], [contributor]);
|
|
3260
|
+
}
|
|
3261
|
+
const removedState = {
|
|
3262
|
+
...state,
|
|
3263
|
+
removedBy: contributor,
|
|
3264
|
+
...state.isProtected ? { grantUsedBy: contributor } : {}
|
|
3265
|
+
};
|
|
3266
|
+
removedStates.set(mutation.id, removedState);
|
|
3267
|
+
states.delete(mutation.id);
|
|
3268
|
+
}
|
|
3269
|
+
for (const mutation of mutations.filter((entry) => entry.op === "replace").sort(stableMutationCompare)) {
|
|
3270
|
+
const state = states.get(mutation.id);
|
|
3271
|
+
const contributor = contributorOf(mutation);
|
|
3272
|
+
if (!state)
|
|
3273
|
+
continue;
|
|
3274
|
+
if (state.isProtected && !hasProtectedGrant(grants, mutation.id, contributor, "replace")) {
|
|
3275
|
+
throw new PipelineUnresolvableError(`Protected stage ${mutation.id} cannot be replaced by ${contributor} without an explicit grant`, [], [contributor]);
|
|
3276
|
+
}
|
|
3277
|
+
const replacement = { ...mutation.stage, id: mutation.id };
|
|
3278
|
+
states.set(mutation.id, {
|
|
3279
|
+
...state,
|
|
3280
|
+
stage: replacement,
|
|
3281
|
+
before: mutation.stage.before ? [...mutation.stage.before] : state.before,
|
|
3282
|
+
after: mutation.stage.after ? [...mutation.stage.after] : state.after,
|
|
3283
|
+
replacedBy: contributor,
|
|
3284
|
+
contributedBy: state.contributedBy,
|
|
3285
|
+
isProtected: mutation.stage.protected ?? state.isProtected,
|
|
3286
|
+
...state.isProtected ? { grantUsedBy: contributor } : {}
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
for (const mutation of mutations.filter((entry) => entry.op === "insert").sort(stableMutationCompare)) {
|
|
3290
|
+
const contributor = contributorOf(mutation);
|
|
3291
|
+
if (states.has(mutation.stage.id)) {
|
|
3292
|
+
throw new PipelineUnresolvableError(`Inserted stage ${mutation.stage.id} conflicts with an existing stage`, [], [contributor]);
|
|
3293
|
+
}
|
|
3294
|
+
states.set(mutation.stage.id, {
|
|
3295
|
+
stage: mutation.stage,
|
|
3296
|
+
before: [...mutation.stage.before ?? []],
|
|
3297
|
+
after: [...mutation.stage.after ?? []],
|
|
3298
|
+
baseIndex: null,
|
|
3299
|
+
contributedBy: contributor,
|
|
3300
|
+
wrappers: [],
|
|
3301
|
+
droppedAnchors: [],
|
|
3302
|
+
isProtected: mutation.stage.protected === true
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
for (const mutation of mutations.filter((entry) => entry.op === "reorder").sort(stableMutationCompare)) {
|
|
3306
|
+
const state = states.get(mutation.id);
|
|
3307
|
+
if (!state)
|
|
3308
|
+
continue;
|
|
3309
|
+
states.set(mutation.id, {
|
|
3310
|
+
...state,
|
|
3311
|
+
before: mergeAnchors(state.before, mutation.before),
|
|
3312
|
+
after: mergeAnchors(state.after, mutation.after)
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
for (const mutation of mutations.filter((entry) => entry.op === "wrap").sort(stableMutationCompare)) {
|
|
3316
|
+
const state = states.get(mutation.id);
|
|
3317
|
+
const contributor = contributorOf(mutation);
|
|
3318
|
+
const wrapper = wrapperForMutation(mutation);
|
|
3319
|
+
if (!state)
|
|
3320
|
+
continue;
|
|
3321
|
+
states.set(mutation.id, {
|
|
3322
|
+
...state,
|
|
3323
|
+
wrappers: [...state.wrappers, { contributedBy: contributor, wrapper }]
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
const contributorsForAnchor = (stageId, direction, anchor, state) => {
|
|
3327
|
+
const contributors = new Set;
|
|
3328
|
+
if (state.replacedBy)
|
|
3329
|
+
contributors.add(state.replacedBy);
|
|
3330
|
+
for (const mutation of mutations) {
|
|
3331
|
+
if (mutation.op === "reorder" && mutation.id === stageId && (direction === "before" ? mutation.before : mutation.after)?.includes(anchor)) {
|
|
3332
|
+
contributors.add(contributorOf(mutation));
|
|
3333
|
+
}
|
|
3334
|
+
if (mutation.op === "insert" && mutation.stage.id === stageId && (direction === "before" ? mutation.stage.before : mutation.stage.after)?.includes(anchor)) {
|
|
3335
|
+
contributors.add(contributorOf(mutation));
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
if (contributors.size === 0)
|
|
3339
|
+
contributors.add(state.contributedBy);
|
|
3340
|
+
return uniqueSorted(contributors);
|
|
3341
|
+
};
|
|
3342
|
+
const edges = [];
|
|
3343
|
+
for (const [stageId, state] of states) {
|
|
3344
|
+
const before = [];
|
|
3345
|
+
const after = [];
|
|
3346
|
+
for (const anchor of state.before) {
|
|
3347
|
+
if (states.has(anchor))
|
|
3348
|
+
before.push(anchor);
|
|
3349
|
+
else {
|
|
3350
|
+
const removed = removedStates.get(anchor);
|
|
3351
|
+
state.droppedAnchors.push({
|
|
3352
|
+
stageId,
|
|
3353
|
+
anchor,
|
|
3354
|
+
direction: "before",
|
|
3355
|
+
reason: removed ? "removed" : "missing",
|
|
3356
|
+
...removed?.removedBy ? { removedBy: removed.removedBy } : {}
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
for (const anchor of state.after) {
|
|
3361
|
+
if (states.has(anchor))
|
|
3362
|
+
after.push(anchor);
|
|
3363
|
+
else {
|
|
3364
|
+
const removed = removedStates.get(anchor);
|
|
3365
|
+
state.droppedAnchors.push({
|
|
3366
|
+
stageId,
|
|
3367
|
+
anchor,
|
|
3368
|
+
direction: "after",
|
|
3369
|
+
reason: removed ? "removed" : "missing",
|
|
3370
|
+
...removed?.removedBy ? { removedBy: removed.removedBy } : {}
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
state.before = uniqueSorted(before);
|
|
3375
|
+
state.after = uniqueSorted(after);
|
|
3376
|
+
for (const target of state.before)
|
|
3377
|
+
edges.push({ from: stageId, to: target, contributors: contributorsForAnchor(stageId, "before", target, state) });
|
|
3378
|
+
for (const source of state.after)
|
|
3379
|
+
edges.push({ from: source, to: stageId, contributors: contributorsForAnchor(stageId, "after", source, state) });
|
|
3380
|
+
}
|
|
3381
|
+
const order = topologicalOrder(states, edges);
|
|
3382
|
+
const stages = [];
|
|
3383
|
+
const record = [];
|
|
3384
|
+
for (const id of order) {
|
|
3385
|
+
const state = states.get(id);
|
|
3386
|
+
if (!state)
|
|
3387
|
+
continue;
|
|
3388
|
+
stages.push(composeStageWrappers(state));
|
|
3389
|
+
const wrappedBy = state.wrappers.toSorted((left, right) => {
|
|
3390
|
+
const priorityDelta = (right.wrapper.priority ?? 0) - (left.wrapper.priority ?? 0);
|
|
3391
|
+
if (priorityDelta !== 0)
|
|
3392
|
+
return priorityDelta;
|
|
3393
|
+
return left.contributedBy.localeCompare(right.contributedBy);
|
|
3394
|
+
}).map((wrapper) => wrapper.contributedBy);
|
|
3395
|
+
record.push({
|
|
3396
|
+
stageId: id,
|
|
3397
|
+
contributedBy: state.contributedBy,
|
|
3398
|
+
...state.replacedBy ? { replacedBy: state.replacedBy } : {},
|
|
3399
|
+
...wrappedBy.length > 0 ? { wrappedBy } : {},
|
|
3400
|
+
...state.droppedAnchors.length > 0 ? { droppedAnchors: state.droppedAnchors.toSorted((left, right) => left.anchor.localeCompare(right.anchor)) } : {},
|
|
3401
|
+
isProtected: state.isProtected,
|
|
3402
|
+
...state.grantUsedBy ? { grantUsedBy: state.grantUsedBy } : {}
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
record.push(...[...removedStates.entries()].toSorted((left, right) => {
|
|
3406
|
+
const leftIndex = left[1].baseIndex ?? Number.MAX_SAFE_INTEGER;
|
|
3407
|
+
const rightIndex = right[1].baseIndex ?? Number.MAX_SAFE_INTEGER;
|
|
3408
|
+
if (leftIndex !== rightIndex)
|
|
3409
|
+
return leftIndex - rightIndex;
|
|
3410
|
+
return left[0].localeCompare(right[0]);
|
|
3411
|
+
}).map(([stageId, state]) => ({
|
|
3412
|
+
stageId,
|
|
3413
|
+
contributedBy: state.contributedBy,
|
|
3414
|
+
...state.removedBy ? { removedBy: state.removedBy } : {},
|
|
3415
|
+
isProtected: state.isProtected,
|
|
3416
|
+
...state.grantUsedBy ? { grantUsedBy: state.grantUsedBy } : {}
|
|
3417
|
+
})));
|
|
3418
|
+
return { stages, order, record, cycles: [] };
|
|
3419
|
+
}
|
|
3420
|
+
// packages/core/src/dependencyGraph.ts
|
|
3421
|
+
function uniqueSorted2(values) {
|
|
3422
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
3423
|
+
}
|
|
3424
|
+
function deriveGraphId(tasks) {
|
|
3425
|
+
const basis = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right)).join("|");
|
|
3426
|
+
let hash = 2166136261;
|
|
3427
|
+
for (let index = 0;index < basis.length; index += 1) {
|
|
3428
|
+
hash ^= basis.charCodeAt(index);
|
|
3429
|
+
hash = Math.imul(hash, 16777619);
|
|
3430
|
+
}
|
|
3431
|
+
return `graph-${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
3432
|
+
}
|
|
3433
|
+
function dedupeEdges(edges) {
|
|
3434
|
+
const byKey = new Map;
|
|
3435
|
+
for (const edge of edges) {
|
|
3436
|
+
byKey.set(`${edge.type}\x00${edge.fromTaskId}\x00${edge.toTaskId}`, edge);
|
|
3437
|
+
}
|
|
3438
|
+
return [...byKey.values()].toSorted((left, right) => {
|
|
3439
|
+
const typeDelta = left.type.localeCompare(right.type);
|
|
3440
|
+
if (typeDelta !== 0)
|
|
3441
|
+
return typeDelta;
|
|
3442
|
+
const fromDelta = left.fromTaskId.localeCompare(right.fromTaskId);
|
|
3443
|
+
if (fromDelta !== 0)
|
|
3444
|
+
return fromDelta;
|
|
3445
|
+
return left.toTaskId.localeCompare(right.toTaskId);
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
function detectBlockingCycles(tasks, edges) {
|
|
3449
|
+
const taskIds = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right));
|
|
3450
|
+
const adjacency = new Map;
|
|
3451
|
+
for (const taskId of taskIds)
|
|
3452
|
+
adjacency.set(taskId, []);
|
|
3453
|
+
for (const edge of edges) {
|
|
3454
|
+
if (edge.type === "blocks")
|
|
3455
|
+
adjacency.get(edge.fromTaskId)?.push(edge.toTaskId);
|
|
3456
|
+
}
|
|
3457
|
+
for (const targets of adjacency.values())
|
|
3458
|
+
targets.sort((left, right) => left.localeCompare(right));
|
|
3459
|
+
let nextIndex = 0;
|
|
3460
|
+
const indexByNode = new Map;
|
|
3461
|
+
const lowByNode = new Map;
|
|
3462
|
+
const stack = [];
|
|
3463
|
+
const onStack = new Set;
|
|
3464
|
+
const cycles = [];
|
|
3465
|
+
const visit = (node) => {
|
|
3466
|
+
indexByNode.set(node, nextIndex);
|
|
3467
|
+
lowByNode.set(node, nextIndex);
|
|
3468
|
+
nextIndex += 1;
|
|
3469
|
+
stack.push(node);
|
|
3470
|
+
onStack.add(node);
|
|
3471
|
+
for (const target of adjacency.get(node) ?? []) {
|
|
3472
|
+
if (!indexByNode.has(target)) {
|
|
3473
|
+
visit(target);
|
|
3474
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
|
|
3475
|
+
} else if (onStack.has(target)) {
|
|
3476
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
if (lowByNode.get(node) !== indexByNode.get(node))
|
|
3480
|
+
return;
|
|
3481
|
+
const component = [];
|
|
3482
|
+
let current;
|
|
3483
|
+
do {
|
|
3484
|
+
current = stack.pop();
|
|
3485
|
+
if (current) {
|
|
3486
|
+
onStack.delete(current);
|
|
3487
|
+
component.push(current);
|
|
3488
|
+
}
|
|
3489
|
+
} while (current && current !== node);
|
|
3490
|
+
const selfLoop = (adjacency.get(node) ?? []).includes(node);
|
|
3491
|
+
if (component.length > 1 || selfLoop)
|
|
3492
|
+
cycles.push(component.sort((left, right) => left.localeCompare(right)));
|
|
3493
|
+
};
|
|
3494
|
+
for (const taskId of taskIds) {
|
|
3495
|
+
if (!indexByNode.has(taskId))
|
|
3496
|
+
visit(taskId);
|
|
3497
|
+
}
|
|
3498
|
+
return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
|
|
3499
|
+
}
|
|
3500
|
+
function buildDependencyGraphModel(tasks, options = {}) {
|
|
3501
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
3502
|
+
const index = buildTaskReferenceIndex(tasks);
|
|
3503
|
+
const edges = [];
|
|
3504
|
+
const unresolvedRefs = [];
|
|
3505
|
+
for (const task of tasks) {
|
|
3506
|
+
for (const ref of readTaskMetadataStringList(task, "dependencies")) {
|
|
3507
|
+
const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
3508
|
+
if (dependencyId)
|
|
3509
|
+
edges.push({ fromTaskId: dependencyId, toTaskId: String(task.id), type: "blocks" });
|
|
3510
|
+
else
|
|
3511
|
+
unresolvedRefs.push(ref);
|
|
3512
|
+
}
|
|
3513
|
+
for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
|
|
3514
|
+
const parentId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
3515
|
+
if (parentId)
|
|
3516
|
+
edges.push({ fromTaskId: parentId, toTaskId: String(task.id), type: "parent-child" });
|
|
3517
|
+
else
|
|
3518
|
+
unresolvedRefs.push(ref);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
const dedupedEdges = dedupeEdges(edges);
|
|
3522
|
+
const nodes = tasks.map((task) => {
|
|
3523
|
+
const summary = badges.get(task.id);
|
|
3524
|
+
const assignees = readTaskAssigneeLogins(task);
|
|
3525
|
+
const groupKey = extractTaskGroupKey(task.title);
|
|
3526
|
+
return {
|
|
3527
|
+
taskId: String(task.id),
|
|
3528
|
+
title: task.title,
|
|
3529
|
+
status: task.status,
|
|
3530
|
+
priority: task.priority,
|
|
3531
|
+
assignee: assignees[0] ?? null,
|
|
3532
|
+
blockedBy: summary?.blockedBy ?? [],
|
|
3533
|
+
blocks: summary?.blocks ?? [],
|
|
3534
|
+
blockingDepth: summary?.blockingDepth ?? 0,
|
|
3535
|
+
blockerClass: null,
|
|
3536
|
+
actionRiskTier: null,
|
|
3537
|
+
epicKey: groupKey,
|
|
3538
|
+
groupKey,
|
|
3539
|
+
externalId: task.externalId,
|
|
3540
|
+
sourceIssueId: task.sourceIssueId ?? null,
|
|
3541
|
+
scope: task.scope,
|
|
3542
|
+
validationKeys: task.validationKeys
|
|
3543
|
+
};
|
|
3544
|
+
}).toSorted((left, right) => left.taskId.localeCompare(right.taskId));
|
|
3545
|
+
return {
|
|
3546
|
+
graphId: options.graphId ?? deriveGraphId(tasks),
|
|
3547
|
+
nodes,
|
|
3548
|
+
edges: dedupedEdges,
|
|
3549
|
+
layout: buildTaskGraphLayout(options.snapshot ?? null, tasks, { showParentChild: options.showParentChild ?? true }),
|
|
3550
|
+
cycles: detectBlockingCycles(tasks, dedupedEdges),
|
|
3551
|
+
unresolvedRefs: uniqueSorted2(unresolvedRefs),
|
|
3552
|
+
degraded: false,
|
|
3553
|
+
generatedAt: options.generatedAt ?? new Date().toISOString()
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
// packages/core/src/rollups.ts
|
|
3557
|
+
import { isOperatorActiveRunStatus } from "@rig/contracts";
|
|
3558
|
+
var UNASSIGNED_EPIC = "(unassigned-epic)";
|
|
3559
|
+
var UNASSIGNED_ASSIGNEE = "(unassigned)";
|
|
3560
|
+
var HUMAN_BLOCKER_CLASSES = {
|
|
3561
|
+
"human-decision": true,
|
|
3562
|
+
"human-approval": true,
|
|
3563
|
+
"external-input": true,
|
|
3564
|
+
unknown: true
|
|
3565
|
+
};
|
|
3566
|
+
function isObjectRecord4(value) {
|
|
3567
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3568
|
+
}
|
|
3569
|
+
function readIssueType2(task) {
|
|
3570
|
+
const metadata = isObjectRecord4(task.metadata) ? task.metadata : null;
|
|
3571
|
+
const raw = isObjectRecord4(metadata?.raw) ? metadata.raw : null;
|
|
3572
|
+
const value = raw?.issueType ?? metadata?.issueType;
|
|
3573
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
|
|
3574
|
+
}
|
|
3575
|
+
function isEpicTask(task) {
|
|
3576
|
+
return readIssueType2(task) === "epic";
|
|
3577
|
+
}
|
|
3578
|
+
function isInFlightTaskStatus(status) {
|
|
3579
|
+
switch (status) {
|
|
3580
|
+
case "queued":
|
|
3581
|
+
case "running":
|
|
3582
|
+
case "in_progress":
|
|
3583
|
+
case "under_review":
|
|
3584
|
+
return true;
|
|
3585
|
+
default:
|
|
3586
|
+
return false;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
function epicKeyForTask(task, tasksById, resolve) {
|
|
3590
|
+
const parentRef = readTaskMetadataStringList(task, "parentChildDeps")[0];
|
|
3591
|
+
const parentId = parentRef ? resolve(parentRef) : null;
|
|
3592
|
+
const parent = parentId ? tasksById.get(parentId) : null;
|
|
3593
|
+
if (parent)
|
|
3594
|
+
return extractTaskGroupKey(parent.title) ?? parent.title ?? parent.id;
|
|
3595
|
+
return extractTaskGroupKey(task.title) ?? UNASSIGNED_EPIC;
|
|
3596
|
+
}
|
|
3597
|
+
function rollupByEpic(tasks, classifications = new Map) {
|
|
3598
|
+
const index = buildTaskReferenceIndex(tasks);
|
|
3599
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
3600
|
+
const resolve = (ref) => resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
3601
|
+
const buckets = new Map;
|
|
3602
|
+
for (const task of tasks) {
|
|
3603
|
+
if (isEpicTask(task))
|
|
3604
|
+
continue;
|
|
3605
|
+
const epicKey = epicKeyForTask(task, index.tasksById, resolve);
|
|
3606
|
+
const current = buckets.get(epicKey) ?? {
|
|
3607
|
+
total: 0,
|
|
3608
|
+
completed: 0,
|
|
3609
|
+
blockedCount: 0,
|
|
3610
|
+
humanBlockedCount: 0,
|
|
3611
|
+
inFlightCount: 0,
|
|
3612
|
+
byStatus: {}
|
|
3613
|
+
};
|
|
3614
|
+
current.total += 1;
|
|
3615
|
+
if (isTaskTerminalStatus(task.status))
|
|
3616
|
+
current.completed += 1;
|
|
3617
|
+
if (badges.get(task.id)?.blocked === true)
|
|
3618
|
+
current.blockedCount += 1;
|
|
3619
|
+
if (isInFlightTaskStatus(task.status))
|
|
3620
|
+
current.inFlightCount += 1;
|
|
3621
|
+
const classification = classifications.get(task.id);
|
|
3622
|
+
if (classification && HUMAN_BLOCKER_CLASSES[classification])
|
|
3623
|
+
current.humanBlockedCount += 1;
|
|
3624
|
+
current.byStatus[task.status] = (current.byStatus[task.status] ?? 0) + 1;
|
|
3625
|
+
buckets.set(epicKey, current);
|
|
3626
|
+
}
|
|
3627
|
+
return [...buckets.entries()].map(([epicKey, bucket]) => ({
|
|
3628
|
+
epicKey,
|
|
3629
|
+
total: bucket.total,
|
|
3630
|
+
percentComplete: bucket.total === 0 ? 0 : Math.round(100 * bucket.completed / bucket.total),
|
|
3631
|
+
blockedCount: bucket.blockedCount,
|
|
3632
|
+
humanBlockedCount: bucket.humanBlockedCount,
|
|
3633
|
+
inFlightCount: bucket.inFlightCount,
|
|
3634
|
+
byStatus: Object.fromEntries(Object.entries(bucket.byStatus).sort(([left], [right]) => left.localeCompare(right)))
|
|
3635
|
+
})).toSorted((left, right) => left.epicKey.localeCompare(right.epicKey));
|
|
3636
|
+
}
|
|
3637
|
+
function assigneesForTask(task) {
|
|
3638
|
+
const assignees = readTaskAssigneeLogins(task);
|
|
3639
|
+
return assignees.length > 0 ? assignees : [UNASSIGNED_ASSIGNEE];
|
|
3640
|
+
}
|
|
3641
|
+
function rollupByAssignee(tasks, runs) {
|
|
3642
|
+
const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
|
|
3643
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
3644
|
+
const buckets = new Map;
|
|
3645
|
+
const ensureBucket = (assignee) => {
|
|
3646
|
+
const existing = buckets.get(assignee);
|
|
3647
|
+
if (existing)
|
|
3648
|
+
return existing;
|
|
3649
|
+
const created = { openTaskCount: 0, inFlightRunIds: new Set, prsAwaitingReview: 0, blockers: new Set };
|
|
3650
|
+
buckets.set(assignee, created);
|
|
3651
|
+
return created;
|
|
3652
|
+
};
|
|
3653
|
+
for (const task of tasks) {
|
|
3654
|
+
if (isEpicTask(task))
|
|
3655
|
+
continue;
|
|
3656
|
+
for (const assignee of assigneesForTask(task)) {
|
|
3657
|
+
const bucket = ensureBucket(assignee);
|
|
3658
|
+
if (!isTaskTerminalStatus(task.status))
|
|
3659
|
+
bucket.openTaskCount += 1;
|
|
3660
|
+
if (badges.get(task.id)?.blocked === true)
|
|
3661
|
+
bucket.blockers.add(task.id);
|
|
3662
|
+
if (task.status === "under_review")
|
|
3663
|
+
bucket.prsAwaitingReview += 1;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
for (const run of runs) {
|
|
3667
|
+
const taskId = run.record.taskId;
|
|
3668
|
+
const task = taskId ? tasksById.get(taskId) : null;
|
|
3669
|
+
if (!task)
|
|
3670
|
+
continue;
|
|
3671
|
+
if (isEpicTask(task))
|
|
3672
|
+
continue;
|
|
3673
|
+
for (const assignee of assigneesForTask(task)) {
|
|
3674
|
+
const bucket = ensureBucket(assignee);
|
|
3675
|
+
if (run.status && isOperatorActiveRunStatus(run.status))
|
|
3676
|
+
bucket.inFlightRunIds.add(run.record.runId ?? `${String(task.id)}:${run.lastSeq}`);
|
|
3677
|
+
if (run.status === "reviewing" && run.record.prUrl)
|
|
3678
|
+
bucket.prsAwaitingReview += 1;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
return [...buckets.entries()].map(([assignee, bucket]) => ({
|
|
3682
|
+
assignee,
|
|
3683
|
+
openTaskCount: bucket.openTaskCount,
|
|
3684
|
+
inFlightRunCount: bucket.inFlightRunIds.size,
|
|
3685
|
+
prsAwaitingReview: bucket.prsAwaitingReview,
|
|
3686
|
+
blockers: [...bucket.blockers].toSorted((left, right) => String(left).localeCompare(String(right)))
|
|
3687
|
+
})).toSorted((left, right) => left.assignee.localeCompare(right.assignee));
|
|
3688
|
+
}
|
|
2894
3689
|
|
|
2895
3690
|
// packages/core/src/index.ts
|
|
2896
3691
|
var RIG_CORE_PACKAGE = "@rig/core";
|
|
@@ -2911,6 +3706,7 @@ export {
|
|
|
2911
3706
|
selectRunsForTask,
|
|
2912
3707
|
selectRunsByTask,
|
|
2913
3708
|
selectRun,
|
|
3709
|
+
selectRankedReadyTasks,
|
|
2914
3710
|
selectQueueForWorkspace,
|
|
2915
3711
|
selectPrimaryWorkspace,
|
|
2916
3712
|
selectPendingApprovals,
|
|
@@ -2920,11 +3716,19 @@ export {
|
|
|
2920
3716
|
selectApprovalsForRun,
|
|
2921
3717
|
selectAdhocRunsForWorkspace,
|
|
2922
3718
|
selectAdhocRuns,
|
|
3719
|
+
scoreTask,
|
|
3720
|
+
rollupByEpic,
|
|
3721
|
+
rollupByAssignee,
|
|
2923
3722
|
resolveTaskReference,
|
|
3723
|
+
resolveStagePipeline,
|
|
2924
3724
|
readTaskSourceIssueId,
|
|
3725
|
+
readTaskScope,
|
|
2925
3726
|
readTaskMetadataStringList,
|
|
2926
3727
|
readTaskDependencyRefs,
|
|
3728
|
+
readTaskBlockingDependencyRefs,
|
|
2927
3729
|
readTaskAssigneeLogins,
|
|
3730
|
+
rankTasks,
|
|
3731
|
+
rankReadyTasks,
|
|
2928
3732
|
pruneQueueEntries,
|
|
2929
3733
|
projectTaskStatusWithSessions,
|
|
2930
3734
|
projectTaskStatusForGrouping,
|
|
@@ -2934,6 +3738,7 @@ export {
|
|
|
2934
3738
|
isTaskTerminalStatus,
|
|
2935
3739
|
extractTaskGroupKey,
|
|
2936
3740
|
extractTaskCode,
|
|
3741
|
+
disjointScope,
|
|
2937
3742
|
definePlugin,
|
|
2938
3743
|
defineConfig,
|
|
2939
3744
|
createPluginHost,
|
|
@@ -2942,8 +3747,10 @@ export {
|
|
|
2942
3747
|
buildTaskReferenceIndex,
|
|
2943
3748
|
buildTaskGraphLayout,
|
|
2944
3749
|
buildRigInitConfigSource,
|
|
3750
|
+
buildDependencyGraphModel,
|
|
2945
3751
|
applyEngineEvent as applyRigEvent,
|
|
2946
3752
|
applyEngineEvents,
|
|
2947
3753
|
applyEngineEvent,
|
|
2948
|
-
RIG_CORE_PACKAGE
|
|
3754
|
+
RIG_CORE_PACKAGE,
|
|
3755
|
+
PipelineUnresolvableError
|
|
2949
3756
|
};
|