@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/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 = readTaskDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
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 readTaskDependencyRefs(task)) {
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
- return typeof metadata?.issueType === "string" ? metadata.issueType : null;
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
  };