@h-rig/core 0.0.6-alpha.14 → 0.0.6-alpha.141
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/define-config.d.ts +18 -0
- package/dist/src/define-config.js +20 -2
- package/dist/src/define-plugin.d.ts +9 -0
- package/dist/src/define-plugin.js +48 -0
- package/dist/src/dependencyGraph.d.ts +43 -0
- package/dist/src/dependencyGraph.js +703 -0
- package/dist/src/engineReadModelReducer.d.ts +12 -0
- package/dist/src/engineReadModelReducer.js +17 -13
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +1318 -42
- package/dist/src/load-config.d.ts +2 -0
- package/dist/src/load-config.js +343 -5
- package/dist/src/plugin-host.d.ts +65 -0
- package/dist/src/plugin-host.js +134 -0
- package/dist/src/plugin-runtime.d.ts +105 -0
- package/dist/src/rig-init-builder.d.ts +30 -0
- package/dist/src/rig-init-builder.js +14 -10
- package/dist/src/rigSelectors.d.ts +220 -0
- package/dist/src/rigSelectors.js +125 -4
- package/dist/src/rollups.d.ts +6 -0
- package/dist/src/rollups.js +377 -0
- package/dist/src/stageResolve.d.ts +77 -0
- package/dist/src/stageResolve.js +361 -0
- package/dist/src/taskGraph.d.ts +64 -0
- package/dist/src/taskGraph.js +321 -8
- package/dist/src/taskGraphCodes.d.ts +3 -0
- package/dist/src/taskGraphLayout.d.ts +61 -0
- package/dist/src/taskGraphLayout.js +27 -4
- package/dist/src/taskScore.d.ts +17 -0
- package/dist/src/taskScore.js +49 -0
- package/package.json +24 -10
package/dist/src/index.js
CHANGED
|
@@ -43,11 +43,76 @@ function definePlugin(meta, runtime) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
const declaredHooks = new Map((validated.contributes?.hooks ?? []).map((h) => [h.id, h]));
|
|
47
|
+
for (const hookId of Object.keys(runtime.hooks ?? {})) {
|
|
48
|
+
const metadata = declaredHooks.get(hookId);
|
|
49
|
+
if (!metadata) {
|
|
50
|
+
throw new Error(`definePlugin(${validated.name}): typed hook "${hookId}" has no matching metadata entry in contributes.hooks`);
|
|
51
|
+
}
|
|
52
|
+
if (metadata.command) {
|
|
53
|
+
throw new Error(`definePlugin(${validated.name}): hook "${hookId}" has both a typed implementation and a command string \u2014 pick one`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (runtime.hooks) {
|
|
57
|
+
for (const h of declaredHooks.values()) {
|
|
58
|
+
if (!runtime.hooks[h.id] && !h.command) {
|
|
59
|
+
throw new Error(`definePlugin(${validated.name}): hook metadata "${h.id}" has no implementation (typed function or command)`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const declaredCapabilities = new Map((validated.contributes?.capabilities ?? []).map((capability) => [capability.id, capability]));
|
|
64
|
+
for (const capability of runtime.featureCapabilities ?? []) {
|
|
65
|
+
if (!declaredCapabilities.has(capability.id)) {
|
|
66
|
+
throw new Error(`definePlugin(${validated.name}): executable capability "${capability.id}" has no matching metadata entry in contributes.capabilities`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const declaredPanels = new Map((validated.contributes?.panels ?? []).map((panel) => [panel.id, panel]));
|
|
70
|
+
for (const panel of runtime.panels ?? []) {
|
|
71
|
+
const metadata = declaredPanels.get(panel.id);
|
|
72
|
+
if (!metadata) {
|
|
73
|
+
throw new Error(`definePlugin(${validated.name}): executable panel "${panel.id}" has no matching metadata entry in contributes.panels`);
|
|
74
|
+
}
|
|
75
|
+
if (metadata.slot !== panel.slot) {
|
|
76
|
+
throw new Error(`definePlugin(${validated.name}): executable panel "${panel.id}" slot "${panel.slot}" does not match metadata slot "${metadata.slot}"`);
|
|
77
|
+
}
|
|
78
|
+
if (metadata.capabilityId !== panel.capabilityId) {
|
|
79
|
+
throw new Error(`definePlugin(${validated.name}): executable panel "${panel.id}" capabilityId "${panel.capabilityId ?? "(none)"}" does not match metadata capabilityId "${metadata.capabilityId ?? "(none)"}"`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const declaredBlockerClassifiers = new Map((validated.contributes?.blockerClassifiers ?? []).map((classifier) => [classifier.id, classifier]));
|
|
83
|
+
for (const classifier of runtime.blockerClassifiers ?? []) {
|
|
84
|
+
if (!declaredBlockerClassifiers.has(classifier.id)) {
|
|
85
|
+
throw new Error(`definePlugin(${validated.name}): executable blocker classifier "${classifier.id}" has no matching metadata entry in contributes.blockerClassifiers`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const declaredCliCommands = new Map((validated.contributes?.cliCommands ?? []).map((command) => [command.id, command]));
|
|
89
|
+
for (const command of runtime.cliCommands ?? []) {
|
|
90
|
+
if (!declaredCliCommands.has(command.id)) {
|
|
91
|
+
throw new Error(`definePlugin(${validated.name}): executable cli command "${command.id}" has no matching metadata entry in contributes.cliCommands`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
46
94
|
return { ...validated, __runtime: runtime };
|
|
47
95
|
}
|
|
48
96
|
// packages/core/src/define-config.ts
|
|
49
97
|
import { Schema as Schema2 } from "effect";
|
|
50
98
|
import { RigConfig } from "@rig/contracts";
|
|
99
|
+
function normalizeWorkspaceConfig(raw) {
|
|
100
|
+
const workspace = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...raw } : { mainRepo: "." };
|
|
101
|
+
workspace.checkout = workspace.checkout ?? workspace.isolation ?? "worktree";
|
|
102
|
+
workspace.isolation = workspace.isolation ?? workspace.checkout;
|
|
103
|
+
workspace.sandbox = workspace.sandbox ?? "enforce";
|
|
104
|
+
return workspace;
|
|
105
|
+
}
|
|
106
|
+
function applyConfigDefaults(raw) {
|
|
107
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
108
|
+
return raw;
|
|
109
|
+
const record = raw;
|
|
110
|
+
return {
|
|
111
|
+
...record,
|
|
112
|
+
plugins: Array.isArray(record.plugins) ? record.plugins : [],
|
|
113
|
+
workspace: normalizeWorkspaceConfig(record.workspace)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
51
116
|
function defineConfig(cfg) {
|
|
52
117
|
const runtimeByName = new Map;
|
|
53
118
|
const plugins = cfg.plugins ?? [];
|
|
@@ -56,7 +121,7 @@ function defineConfig(cfg) {
|
|
|
56
121
|
runtimeByName.set(plugin.name, plugin.__runtime);
|
|
57
122
|
}
|
|
58
123
|
}
|
|
59
|
-
const decoded = Schema2.decodeUnknownSync(RigConfig)(cfg);
|
|
124
|
+
const decoded = Schema2.decodeUnknownSync(RigConfig)(applyConfigDefaults(cfg));
|
|
60
125
|
const decodedPlugins = decoded.plugins.map((p) => {
|
|
61
126
|
const runtime = runtimeByName.get(p.name);
|
|
62
127
|
if (!runtime)
|
|
@@ -124,6 +189,59 @@ function assertRuntimeMatchesMetadata(plugin) {
|
|
|
124
189
|
}
|
|
125
190
|
}
|
|
126
191
|
}
|
|
192
|
+
const declaredHooks = new Map((plugin.contributes?.hooks ?? []).map((hook) => [hook.id, hook]));
|
|
193
|
+
const runtimeHooks = plugin.__runtime?.hooks;
|
|
194
|
+
for (const hookId of Object.keys(runtimeHooks ?? {})) {
|
|
195
|
+
const metadata = declaredHooks.get(hookId);
|
|
196
|
+
if (!metadata) {
|
|
197
|
+
throw new Error(`plugin "${plugin.name}" typed hook "${hookId}" has no matching metadata entry in contributes.hooks`);
|
|
198
|
+
}
|
|
199
|
+
if (metadata.command) {
|
|
200
|
+
throw new Error(`plugin "${plugin.name}" hook "${hookId}" has both a typed implementation and a command string \u2014 pick one`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (runtimeHooks) {
|
|
204
|
+
for (const hook of declaredHooks.values()) {
|
|
205
|
+
if (!runtimeHooks[hook.id] && !hook.command) {
|
|
206
|
+
throw new Error(`plugin "${plugin.name}" hook metadata "${hook.id}" has no implementation (typed function or command)`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const declaredCapabilities = new Map((plugin.contributes?.capabilities ?? []).map((capability) => [capability.id, capability]));
|
|
211
|
+
const runtimeCapabilities = new Map((plugin.__runtime?.featureCapabilities ?? []).map((capability) => [capability.id, capability]));
|
|
212
|
+
for (const capability of runtimeCapabilities.values()) {
|
|
213
|
+
if (!declaredCapabilities.has(capability.id)) {
|
|
214
|
+
throw new Error(`plugin "${plugin.name}" executable capability "${capability.id}" has no matching metadata entry in contributes.capabilities`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const declaredPanels = new Map((plugin.contributes?.panels ?? []).map((panel) => [panel.id, panel]));
|
|
218
|
+
const runtimePanels = new Map((plugin.__runtime?.panels ?? []).map((panel) => [panel.id, panel]));
|
|
219
|
+
for (const panel of runtimePanels.values()) {
|
|
220
|
+
const metadata = declaredPanels.get(panel.id);
|
|
221
|
+
if (!metadata) {
|
|
222
|
+
throw new Error(`plugin "${plugin.name}" executable panel "${panel.id}" has no matching metadata entry in contributes.panels`);
|
|
223
|
+
}
|
|
224
|
+
if (metadata.slot !== panel.slot) {
|
|
225
|
+
throw new Error(`plugin "${plugin.name}" executable panel "${panel.id}" slot "${panel.slot}" does not match metadata slot "${metadata.slot}"`);
|
|
226
|
+
}
|
|
227
|
+
if (metadata.capabilityId !== panel.capabilityId) {
|
|
228
|
+
throw new Error(`plugin "${plugin.name}" executable panel "${panel.id}" capabilityId "${panel.capabilityId ?? "(none)"}" does not match metadata capabilityId "${metadata.capabilityId ?? "(none)"}"`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const declaredBlockerClassifiers = new Map((plugin.contributes?.blockerClassifiers ?? []).map((classifier) => [classifier.id, classifier]));
|
|
232
|
+
const runtimeBlockerClassifiers = new Map((plugin.__runtime?.blockerClassifiers ?? []).map((classifier) => [classifier.id, classifier]));
|
|
233
|
+
for (const classifier of runtimeBlockerClassifiers.values()) {
|
|
234
|
+
if (!declaredBlockerClassifiers.has(classifier.id)) {
|
|
235
|
+
throw new Error(`plugin "${plugin.name}" executable blocker classifier "${classifier.id}" has no matching metadata entry in contributes.blockerClassifiers`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const declaredCliCommands = new Map((plugin.contributes?.cliCommands ?? []).map((command) => [command.id, command]));
|
|
239
|
+
const runtimeCliCommands = new Map((plugin.__runtime?.cliCommands ?? []).map((command) => [command.id, command]));
|
|
240
|
+
for (const command of runtimeCliCommands.values()) {
|
|
241
|
+
if (!declaredCliCommands.has(command.id)) {
|
|
242
|
+
throw new Error(`plugin "${plugin.name}" executable cli command "${command.id}" has no matching metadata entry in contributes.cliCommands`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
127
245
|
}
|
|
128
246
|
function createPluginHost(plugins) {
|
|
129
247
|
assertUniquePluginNames(plugins);
|
|
@@ -138,8 +256,18 @@ function createPluginHost(plugins) {
|
|
|
138
256
|
const taskFieldExtensions = [];
|
|
139
257
|
const taskSources = [];
|
|
140
258
|
const cliCommands = [];
|
|
259
|
+
const capabilities = [];
|
|
260
|
+
const panels = [];
|
|
261
|
+
const blockerClassifiers = [];
|
|
262
|
+
const stages = [];
|
|
263
|
+
const stageMutations = [];
|
|
264
|
+
const stageExecutors = {};
|
|
141
265
|
const executableValidators = [];
|
|
142
266
|
const executableTaskSources = [];
|
|
267
|
+
const executableCapabilities = [];
|
|
268
|
+
const executablePanels = [];
|
|
269
|
+
const executableBlockerClassifiers = [];
|
|
270
|
+
const executableCliCommands = [];
|
|
143
271
|
for (const plugin of plugins) {
|
|
144
272
|
const c = plugin.contributes;
|
|
145
273
|
if (!c)
|
|
@@ -151,6 +279,18 @@ function createPluginHost(plugins) {
|
|
|
151
279
|
if (plugin.__runtime?.taskSources) {
|
|
152
280
|
executableTaskSources.push(...plugin.__runtime.taskSources.map((item) => ({ item, pluginName })));
|
|
153
281
|
}
|
|
282
|
+
if (plugin.__runtime?.featureCapabilities) {
|
|
283
|
+
executableCapabilities.push(...plugin.__runtime.featureCapabilities.map((item) => ({ item, pluginName })));
|
|
284
|
+
}
|
|
285
|
+
if (plugin.__runtime?.panels) {
|
|
286
|
+
executablePanels.push(...plugin.__runtime.panels.map((item) => ({ item, pluginName })));
|
|
287
|
+
}
|
|
288
|
+
if (plugin.__runtime?.blockerClassifiers) {
|
|
289
|
+
executableBlockerClassifiers.push(...plugin.__runtime.blockerClassifiers.map((item) => ({ item, pluginName })));
|
|
290
|
+
}
|
|
291
|
+
if (plugin.__runtime?.cliCommands) {
|
|
292
|
+
executableCliCommands.push(...plugin.__runtime.cliCommands.map((item) => ({ item, pluginName })));
|
|
293
|
+
}
|
|
154
294
|
if (c.validators)
|
|
155
295
|
validators.push(...c.validators.map((item) => ({ item, pluginName })));
|
|
156
296
|
if (c.hooks)
|
|
@@ -167,9 +307,25 @@ function createPluginHost(plugins) {
|
|
|
167
307
|
taskSources.push(...c.taskSources.map((item) => ({ item, pluginName })));
|
|
168
308
|
if (c.cliCommands)
|
|
169
309
|
cliCommands.push(...c.cliCommands.map((item) => ({ item, pluginName })));
|
|
310
|
+
if (c.capabilities)
|
|
311
|
+
capabilities.push(...c.capabilities.map((item) => ({ item, pluginName })));
|
|
312
|
+
if (c.panels)
|
|
313
|
+
panels.push(...c.panels.map((item) => ({ item, pluginName })));
|
|
314
|
+
if (c.blockerClassifiers)
|
|
315
|
+
blockerClassifiers.push(...c.blockerClassifiers.map((item) => ({ item, pluginName })));
|
|
316
|
+
if (c.stages)
|
|
317
|
+
stages.push(...c.stages.map((item) => ({ item, pluginName })));
|
|
318
|
+
if (c.stageMutations)
|
|
319
|
+
stageMutations.push(...c.stageMutations.map((item) => ({ item, pluginName })));
|
|
320
|
+
if (plugin.__runtime?.stages)
|
|
321
|
+
Object.assign(stageExecutors, plugin.__runtime.stages);
|
|
170
322
|
}
|
|
171
323
|
indexById(executableValidators, "executableValidator");
|
|
172
324
|
indexById(executableTaskSources, "executableTaskSource");
|
|
325
|
+
indexById(executableCapabilities, "executableCapability");
|
|
326
|
+
indexById(executablePanels, "executablePanel");
|
|
327
|
+
indexById(executableBlockerClassifiers, "executableBlockerClassifier");
|
|
328
|
+
indexById(executableCliCommands, "executableCliCommand");
|
|
173
329
|
const taskSourceFactoryByKind = new Map;
|
|
174
330
|
const taskSourceKindRegistrant = new Map;
|
|
175
331
|
for (const { item, pluginName } of executableTaskSources) {
|
|
@@ -187,6 +343,9 @@ function createPluginHost(plugins) {
|
|
|
187
343
|
const taskFieldExtMap = indexById(taskFieldExtensions, "taskFieldExtension");
|
|
188
344
|
const taskSourceMap = indexById(taskSources, "taskSource");
|
|
189
345
|
const cliCommandMap = indexById(cliCommands, "cliCommand");
|
|
346
|
+
const capabilityMap = indexById(capabilities, "capability");
|
|
347
|
+
const panelMap = indexById(panels, "panel");
|
|
348
|
+
const blockerClassifierMap = indexById(blockerClassifiers, "blockerClassifier");
|
|
190
349
|
const allValidators = validators.map((c) => c.item);
|
|
191
350
|
const allHooks = hooks.map((c) => c.item);
|
|
192
351
|
const allSkills = skills.map((c) => c.item);
|
|
@@ -195,8 +354,34 @@ function createPluginHost(plugins) {
|
|
|
195
354
|
const allTaskFieldExtensions = taskFieldExtensions.map((c) => c.item);
|
|
196
355
|
const allTaskSources = taskSources.map((c) => c.item);
|
|
197
356
|
const allCliCommands = cliCommands.map((c) => c.item);
|
|
357
|
+
const allStageMutations = stageMutations.map((c) => c.item);
|
|
358
|
+
const allStages = stages.map((c) => c.item);
|
|
359
|
+
const allCapabilities = capabilities.map((c) => c.item);
|
|
360
|
+
const allPanels = panels.map((c) => c.item);
|
|
361
|
+
const allBlockerClassifiers = blockerClassifiers.map((c) => c.item);
|
|
198
362
|
const allExecutableValidators = executableValidators.map((c) => c.item);
|
|
199
363
|
const allExecutableTaskSources = executableTaskSources.map((c) => c.item);
|
|
364
|
+
const allExecutableCapabilities = executableCapabilities.map((c) => c.item);
|
|
365
|
+
const allExecutablePanels = executablePanels.map((c) => c.item);
|
|
366
|
+
const allExecutableBlockerClassifiers = executableBlockerClassifiers.map((c) => c.item);
|
|
367
|
+
const allExecutableCliCommands = executableCliCommands.map((c) => c.item);
|
|
368
|
+
const executableCliCommandByName = new Map;
|
|
369
|
+
const registerExecutableCliCommandSelector = (selector, contribution) => {
|
|
370
|
+
const existing = executableCliCommandByName.get(selector);
|
|
371
|
+
if (existing && existing.item.id !== contribution.item.id) {
|
|
372
|
+
throw new Error(`duplicate executable CLI selector "${selector}" registered by command "${existing.item.id}" from plugin "${existing.pluginName}" and command "${contribution.item.id}" from plugin "${contribution.pluginName}"`);
|
|
373
|
+
}
|
|
374
|
+
executableCliCommandByName.set(selector, contribution);
|
|
375
|
+
};
|
|
376
|
+
for (const contribution of executableCliCommands) {
|
|
377
|
+
const command = contribution.item;
|
|
378
|
+
const family = command.family ?? command.id;
|
|
379
|
+
registerExecutableCliCommandSelector(command.id, contribution);
|
|
380
|
+
registerExecutableCliCommandSelector(family, contribution);
|
|
381
|
+
for (const alias of command.aliases ?? []) {
|
|
382
|
+
registerExecutableCliCommandSelector(alias, contribution);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
200
385
|
return {
|
|
201
386
|
getValidator: (id) => validatorMap.get(id),
|
|
202
387
|
getHook: (id) => hookMap.get(id),
|
|
@@ -206,6 +391,9 @@ function createPluginHost(plugins) {
|
|
|
206
391
|
getTaskFieldExtension: (id) => taskFieldExtMap.get(id),
|
|
207
392
|
getTaskSource: (id) => taskSourceMap.get(id),
|
|
208
393
|
getCliCommand: (id) => cliCommandMap.get(id),
|
|
394
|
+
getCapability: (id) => capabilityMap.get(id),
|
|
395
|
+
getPanel: (id) => panelMap.get(id),
|
|
396
|
+
getBlockerClassifier: (id) => blockerClassifierMap.get(id),
|
|
209
397
|
listValidators: () => allValidators,
|
|
210
398
|
listHooks: () => allHooks,
|
|
211
399
|
listSkills: () => allSkills,
|
|
@@ -214,30 +402,44 @@ function createPluginHost(plugins) {
|
|
|
214
402
|
listTaskFieldExtensions: () => allTaskFieldExtensions,
|
|
215
403
|
listTaskSources: () => allTaskSources,
|
|
216
404
|
listCliCommands: () => allCliCommands,
|
|
405
|
+
listCapabilities: () => allCapabilities,
|
|
406
|
+
listPanels: () => allPanels,
|
|
407
|
+
listBlockerClassifiers: () => allBlockerClassifiers,
|
|
408
|
+
listStages: () => allStages,
|
|
409
|
+
listStageMutations: () => allStageMutations,
|
|
410
|
+
listStageExecutors: () => stageExecutors,
|
|
217
411
|
listExecutableValidators: () => allExecutableValidators,
|
|
218
412
|
listExecutableTaskSources: () => allExecutableTaskSources,
|
|
413
|
+
listExecutableCapabilities: () => allExecutableCapabilities,
|
|
414
|
+
listExecutablePanels: () => allExecutablePanels,
|
|
415
|
+
listExecutableBlockerClassifiers: () => allExecutableBlockerClassifiers,
|
|
416
|
+
listExecutableCliCommands: () => allExecutableCliCommands,
|
|
417
|
+
resolveExecutableCliCommand: (requested) => executableCliCommandByName.get(requested)?.item,
|
|
219
418
|
resolveTaskSourceFactoryByKind: (kind) => taskSourceFactoryByKind.get(kind)
|
|
220
419
|
};
|
|
221
420
|
}
|
|
222
421
|
// packages/core/src/rig-init-builder.ts
|
|
223
422
|
function buildRigInitConfigSource(input) {
|
|
224
423
|
const lines = [`import { defineConfig } from "@rig/core";`];
|
|
225
|
-
if (input.useStandardPlugin
|
|
226
|
-
lines.push(`import
|
|
227
|
-
|
|
228
|
-
|
|
424
|
+
if (input.useStandardPlugin) {
|
|
425
|
+
lines.push(`import { standardProjectPlugins } from "@rig/standard-plugin/bundle";`);
|
|
426
|
+
if (input.taskSource.kind === "github-issues") {
|
|
427
|
+
lines.push(`import { createStateGitHubCredentialProvider } from "@rig/standard-plugin";`);
|
|
428
|
+
}
|
|
229
429
|
}
|
|
230
430
|
lines.push(``, `export default defineConfig({`);
|
|
231
431
|
const projectRepo = input.projectRepo ?? (input.taskSource.kind === "github-issues" ? `${input.taskSource.owner}/${input.taskSource.repo}` : undefined);
|
|
232
432
|
lines.push(projectRepo ? ` project: { name: ${JSON.stringify(input.projectName)}, repo: ${JSON.stringify(projectRepo)} },` : ` project: { name: ${JSON.stringify(input.projectName)} },`);
|
|
233
433
|
if (input.useStandardPlugin && input.taskSource.kind === "github-issues") {
|
|
234
|
-
lines.push(` plugins: [
|
|
235
|
-
lines.push(`
|
|
236
|
-
lines.push(`
|
|
237
|
-
lines.push(`
|
|
434
|
+
lines.push(` plugins: [...standardProjectPlugins({`);
|
|
435
|
+
lines.push(` standard: {`);
|
|
436
|
+
lines.push(` githubCredentialProvider: createStateGitHubCredentialProvider(),`);
|
|
437
|
+
lines.push(` githubWorkspaceId: ${JSON.stringify(`${input.taskSource.owner}/${input.taskSource.repo}`)},`);
|
|
438
|
+
lines.push(` githubUserId: process.env.RIG_GITHUB_USER_ID ?? "server-selected-user",`);
|
|
439
|
+
lines.push(` },`);
|
|
238
440
|
lines.push(` })],`);
|
|
239
441
|
} else {
|
|
240
|
-
lines.push(` plugins: [${input.useStandardPlugin ? "
|
|
442
|
+
lines.push(` plugins: [${input.useStandardPlugin ? "...standardProjectPlugins()" : ""}],`);
|
|
241
443
|
}
|
|
242
444
|
if (input.taskSource.kind === "github-issues") {
|
|
243
445
|
lines.push(` taskSource: {`);
|
|
@@ -257,7 +459,8 @@ function buildRigInitConfigSource(input) {
|
|
|
257
459
|
lines.push(` },`);
|
|
258
460
|
}
|
|
259
461
|
lines.push(` workspace: { mainRepo: ".", isolation: "worktree" },`);
|
|
260
|
-
|
|
462
|
+
const sshTarget = input.sshTarget?.trim();
|
|
463
|
+
lines.push(sshTarget ? ` runtime: { harness: "pi", mode: "yolo", server: { sshTarget: ${JSON.stringify(sshTarget)} } },` : ` runtime: { harness: "pi", mode: "yolo" }, // server.sshTarget unset = local placement`);
|
|
261
464
|
lines.push(` planning: { mode: "auto" },`);
|
|
262
465
|
lines.push(` github: {`);
|
|
263
466
|
lines.push(` issueUpdates: "lifecycle",`);
|
|
@@ -283,6 +486,7 @@ import {
|
|
|
283
486
|
WorkspaceId,
|
|
284
487
|
WorktreeId
|
|
285
488
|
} from "@rig/contracts";
|
|
489
|
+
var CANONICAL_RUNTIME_ADAPTER = "pi";
|
|
286
490
|
function isRecord(value) {
|
|
287
491
|
return typeof value === "object" && value !== null;
|
|
288
492
|
}
|
|
@@ -422,7 +626,7 @@ function mapLegacySessionStatusToRunStatus(status) {
|
|
|
422
626
|
case "error":
|
|
423
627
|
return "failed";
|
|
424
628
|
case "stopped":
|
|
425
|
-
return "
|
|
629
|
+
return "stopped";
|
|
426
630
|
default:
|
|
427
631
|
return "created";
|
|
428
632
|
}
|
|
@@ -458,12 +662,15 @@ function mapTaskStatusFromRunStatus(status, fallback) {
|
|
|
458
662
|
case "paused":
|
|
459
663
|
return "in_progress";
|
|
460
664
|
case "reviewing":
|
|
665
|
+
case "closing-out":
|
|
461
666
|
return "under_review";
|
|
667
|
+
case "needs-attention":
|
|
668
|
+
return "blocked";
|
|
462
669
|
case "completed":
|
|
463
670
|
return "completed";
|
|
464
671
|
case "failed":
|
|
465
672
|
return "ready";
|
|
466
|
-
case "
|
|
673
|
+
case "stopped":
|
|
467
674
|
return "cancelled";
|
|
468
675
|
}
|
|
469
676
|
}
|
|
@@ -647,7 +854,7 @@ function applySyntheticRuntimePreparation(snapshot, event) {
|
|
|
647
854
|
...existingRun,
|
|
648
855
|
taskId: existingRun.taskId ?? nextTaskId,
|
|
649
856
|
runKind: existingRun.runKind === "adhoc" && nextTaskId ? "task" : existingRun.runKind,
|
|
650
|
-
runtimeAdapter:
|
|
857
|
+
runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
|
|
651
858
|
initialPrompt: existingRun.initialPrompt ?? null,
|
|
652
859
|
executionTarget: existingRun.executionTarget ?? "local",
|
|
653
860
|
remoteHostId: existingRun.remoteHostId ?? null,
|
|
@@ -663,7 +870,7 @@ function applySyntheticRuntimePreparation(snapshot, event) {
|
|
|
663
870
|
...runtimeBase,
|
|
664
871
|
workspaceId: nextWorkspaceId,
|
|
665
872
|
runId: runtimeRunId,
|
|
666
|
-
adapterKind:
|
|
873
|
+
adapterKind: CANONICAL_RUNTIME_ADAPTER,
|
|
667
874
|
executionTarget: existingRun.executionTarget ?? "local",
|
|
668
875
|
remoteHostId: existingRun.remoteHostId ?? null,
|
|
669
876
|
status: failed ? "failed" : finished ? "prepared" : "starting",
|
|
@@ -712,7 +919,7 @@ function applySyntheticRuntimePrepared(snapshot, event) {
|
|
|
712
919
|
...snapshot.runtimes.find((runtime) => runtime.runId === runId) ?? makeRuntimeFromRun(existingRun, event.createdAt),
|
|
713
920
|
workspaceId: nextWorkspaceId,
|
|
714
921
|
runId: runtimeRunId,
|
|
715
|
-
adapterKind:
|
|
922
|
+
adapterKind: CANONICAL_RUNTIME_ADAPTER,
|
|
716
923
|
executionTarget: existingRun.executionTarget ?? "local",
|
|
717
924
|
remoteHostId: existingRun.remoteHostId ?? null,
|
|
718
925
|
status: "prepared",
|
|
@@ -755,7 +962,7 @@ function applyLegacyProjectEvent(snapshot, event) {
|
|
|
755
962
|
title,
|
|
756
963
|
rootPath,
|
|
757
964
|
sourceKind: "native",
|
|
758
|
-
defaultRuntimeAdapter:
|
|
965
|
+
defaultRuntimeAdapter: CANONICAL_RUNTIME_ADAPTER,
|
|
759
966
|
defaultModel: readNullableString(payload, "defaultModel") ?? null,
|
|
760
967
|
createdAt,
|
|
761
968
|
updatedAt
|
|
@@ -850,7 +1057,7 @@ function applyLegacyThreadEvent(snapshot, event) {
|
|
|
850
1057
|
runtimeMode: "full-access",
|
|
851
1058
|
interactionMode: "default",
|
|
852
1059
|
status: "created",
|
|
853
|
-
runtimeAdapter:
|
|
1060
|
+
runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
|
|
854
1061
|
model,
|
|
855
1062
|
initialPrompt: null,
|
|
856
1063
|
activeRuntimeId: null,
|
|
@@ -1034,7 +1241,7 @@ function applyLegacyThreadEvent(snapshot, event) {
|
|
|
1034
1241
|
const providerName = readNullableString(session, "providerName");
|
|
1035
1242
|
const nextRun = {
|
|
1036
1243
|
...existingRun,
|
|
1037
|
-
runtimeAdapter:
|
|
1244
|
+
runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
|
|
1038
1245
|
initialPrompt: existingRun.initialPrompt ?? null,
|
|
1039
1246
|
activeRuntimeId: runtimeIdFromRunId(asRunId(runId)),
|
|
1040
1247
|
status: mapLegacySessionStatusToRunStatus(readString(session, "status")),
|
|
@@ -1047,7 +1254,7 @@ function applyLegacyThreadEvent(snapshot, event) {
|
|
|
1047
1254
|
...snapshot.runtimes.find((runtime) => runtime.runId === runId) ?? makeRuntimeFromRun(nextRun, sessionUpdatedAt),
|
|
1048
1255
|
workspaceId: existingRun.workspaceId,
|
|
1049
1256
|
runId: asRunId(runId),
|
|
1050
|
-
adapterKind:
|
|
1257
|
+
adapterKind: CANONICAL_RUNTIME_ADAPTER,
|
|
1051
1258
|
status: mapLegacySessionStatusToRuntimeStatus(readString(session, "status")),
|
|
1052
1259
|
workspaceDir: existingRun.worktreePath,
|
|
1053
1260
|
isolationMode: existingRun.worktreePath ? "worktree" : "env",
|
|
@@ -1281,7 +1488,7 @@ function applyEngineEvent(snapshot, event) {
|
|
|
1281
1488
|
runtimeMode: payload.runtimeMode,
|
|
1282
1489
|
interactionMode: payload.interactionMode,
|
|
1283
1490
|
status: "created",
|
|
1284
|
-
runtimeAdapter:
|
|
1491
|
+
runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
|
|
1285
1492
|
model: payload.model ?? null,
|
|
1286
1493
|
initialPrompt: payload.initialPrompt ?? null,
|
|
1287
1494
|
executionTarget: payload.executionTarget ?? (payload.remoteHostId ? "remote" : "local"),
|
|
@@ -1313,7 +1520,7 @@ function applyEngineEvent(snapshot, event) {
|
|
|
1313
1520
|
...base,
|
|
1314
1521
|
runs: patchById(base.runs, payload.runId, (run) => ({
|
|
1315
1522
|
...run,
|
|
1316
|
-
status: "
|
|
1523
|
+
status: "stopped",
|
|
1317
1524
|
updatedAt: payload.createdAt,
|
|
1318
1525
|
completedAt: payload.createdAt
|
|
1319
1526
|
}))
|
|
@@ -1977,7 +2184,7 @@ function applyEngineEvent(snapshot, event) {
|
|
|
1977
2184
|
runtimeMode: "full-access",
|
|
1978
2185
|
interactionMode: "default",
|
|
1979
2186
|
status: "created",
|
|
1980
|
-
runtimeAdapter:
|
|
2187
|
+
runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
|
|
1981
2188
|
model: null,
|
|
1982
2189
|
initialPrompt: null,
|
|
1983
2190
|
activeRuntimeId: runtimeIdFromRunId(asRunId(runId)),
|
|
@@ -1997,7 +2204,7 @@ function applyEngineEvent(snapshot, event) {
|
|
|
1997
2204
|
workspaceId: asWorkspaceId(workspaceId),
|
|
1998
2205
|
taskId: asTaskId(taskId),
|
|
1999
2206
|
runKind: "task",
|
|
2000
|
-
runtimeAdapter:
|
|
2207
|
+
runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
|
|
2001
2208
|
activeRuntimeId: runtimeIdFromRunId(asRunId(runId)),
|
|
2002
2209
|
updatedAt: event.createdAt
|
|
2003
2210
|
});
|
|
@@ -2085,10 +2292,40 @@ function projectTaskStatusForGrouping(status) {
|
|
|
2085
2292
|
case "in_progress":
|
|
2086
2293
|
case "under_review":
|
|
2087
2294
|
return "running";
|
|
2295
|
+
case null:
|
|
2296
|
+
case undefined:
|
|
2297
|
+
case "":
|
|
2298
|
+
return "unknown";
|
|
2088
2299
|
default:
|
|
2089
2300
|
return status;
|
|
2090
2301
|
}
|
|
2091
2302
|
}
|
|
2303
|
+
function projectRunStatusForTaskGrouping(status) {
|
|
2304
|
+
switch (status) {
|
|
2305
|
+
case "created":
|
|
2306
|
+
case "queued":
|
|
2307
|
+
case "preparing":
|
|
2308
|
+
return "queued";
|
|
2309
|
+
case "running":
|
|
2310
|
+
case "waiting-approval":
|
|
2311
|
+
case "waiting-user-input":
|
|
2312
|
+
case "paused":
|
|
2313
|
+
case "validating":
|
|
2314
|
+
case "reviewing":
|
|
2315
|
+
case "closing-out":
|
|
2316
|
+
return "running";
|
|
2317
|
+
case "needs-attention":
|
|
2318
|
+
return "blocked";
|
|
2319
|
+
case "failed":
|
|
2320
|
+
case "stopped":
|
|
2321
|
+
return "ready";
|
|
2322
|
+
case "completed":
|
|
2323
|
+
return "completed";
|
|
2324
|
+
case null:
|
|
2325
|
+
case undefined:
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2092
2329
|
var STATUS_PRIORITY = [
|
|
2093
2330
|
"running",
|
|
2094
2331
|
"blocked",
|
|
@@ -2100,11 +2337,31 @@ var STATUS_PRIORITY = [
|
|
|
2100
2337
|
"cancelled",
|
|
2101
2338
|
"completed"
|
|
2102
2339
|
];
|
|
2103
|
-
function
|
|
2104
|
-
|
|
2340
|
+
function taskIdFromSession(session) {
|
|
2341
|
+
return session.taskId ?? session.record.taskId ?? null;
|
|
2342
|
+
}
|
|
2343
|
+
function latestSessionByTask(sessions) {
|
|
2344
|
+
const byTask = new Map;
|
|
2345
|
+
for (const session of sessions) {
|
|
2346
|
+
const taskId = taskIdFromSession(session);
|
|
2347
|
+
if (!taskId)
|
|
2348
|
+
continue;
|
|
2349
|
+
const existing = byTask.get(taskId);
|
|
2350
|
+
if (!existing || (session.lastEventAt ?? "").localeCompare(existing.lastEventAt ?? "") >= 0) {
|
|
2351
|
+
byTask.set(taskId, session);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return byTask;
|
|
2355
|
+
}
|
|
2356
|
+
function projectTaskStatusWithSessions(task, sessionsByTask = new Map) {
|
|
2357
|
+
const sessionStatus = projectRunStatusForTaskGrouping(sessionsByTask.get(task.id)?.status);
|
|
2358
|
+
return sessionStatus ?? projectTaskStatusForGrouping(task.status);
|
|
2359
|
+
}
|
|
2360
|
+
function groupTasksByProjectedStatus(tasks, sessions = []) {
|
|
2361
|
+
const sessionsByTask = latestSessionByTask(sessions);
|
|
2105
2362
|
const byStatus = new Map;
|
|
2106
2363
|
for (const task of tasks) {
|
|
2107
|
-
const projectedStatus =
|
|
2364
|
+
const projectedStatus = projectTaskStatusWithSessions(task, sessionsByTask);
|
|
2108
2365
|
const group = byStatus.get(projectedStatus);
|
|
2109
2366
|
if (group) {
|
|
2110
2367
|
group.push(task);
|
|
@@ -2128,6 +2385,69 @@ function selectTasksGroupedByStatus(snapshot, workspaceId) {
|
|
|
2128
2385
|
}
|
|
2129
2386
|
return result;
|
|
2130
2387
|
}
|
|
2388
|
+
function isProjectionGroupingInput(value) {
|
|
2389
|
+
return Boolean(value && typeof value === "object" && !("snapshotSequence" in value) && Array.isArray(value.tasks));
|
|
2390
|
+
}
|
|
2391
|
+
function selectTasksGroupedByStatus(input, workspaceId) {
|
|
2392
|
+
if (isProjectionGroupingInput(input)) {
|
|
2393
|
+
const filteredTasks = input.workspaceId ? input.tasks.filter((task) => {
|
|
2394
|
+
const workspaceTask = task;
|
|
2395
|
+
return workspaceTask.workspaceId === input.workspaceId;
|
|
2396
|
+
}) : input.tasks;
|
|
2397
|
+
return groupTasksByProjectedStatus(filteredTasks, input.sessions);
|
|
2398
|
+
}
|
|
2399
|
+
const tasks = selectTasksByWorkspace(input, workspaceId ?? null);
|
|
2400
|
+
return groupTasksByProjectedStatus(tasks, []);
|
|
2401
|
+
}
|
|
2402
|
+
function isObjectRecord(value) {
|
|
2403
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2404
|
+
}
|
|
2405
|
+
function normalizeLogin(value) {
|
|
2406
|
+
return value.trim().replace(/^@+/, "").toLowerCase();
|
|
2407
|
+
}
|
|
2408
|
+
function assigneeLoginsFromValue(value) {
|
|
2409
|
+
if (!Array.isArray(value))
|
|
2410
|
+
return [];
|
|
2411
|
+
return value.flatMap((entry) => {
|
|
2412
|
+
if (typeof entry === "string" && entry.trim())
|
|
2413
|
+
return [normalizeLogin(entry)];
|
|
2414
|
+
if (isObjectRecord(entry) && typeof entry.login === "string" && entry.login.trim()) {
|
|
2415
|
+
return [normalizeLogin(entry.login)];
|
|
2416
|
+
}
|
|
2417
|
+
return [];
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
function normalizeTaskAssigneeFilter(assignee, currentUserLogin) {
|
|
2421
|
+
const trimmed = assignee?.trim();
|
|
2422
|
+
if (!trimmed)
|
|
2423
|
+
return null;
|
|
2424
|
+
if (trimmed === "@me" || trimmed.toLowerCase() === "me") {
|
|
2425
|
+
return currentUserLogin?.trim() ? normalizeLogin(currentUserLogin) : null;
|
|
2426
|
+
}
|
|
2427
|
+
return normalizeLogin(trimmed);
|
|
2428
|
+
}
|
|
2429
|
+
function readTaskAssigneeLogins(task) {
|
|
2430
|
+
const taskRecord = task;
|
|
2431
|
+
const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
|
|
2432
|
+
const raw = isObjectRecord(metadata?.raw) ? metadata.raw : null;
|
|
2433
|
+
return Array.from(new Set([
|
|
2434
|
+
...assigneeLoginsFromValue(taskRecord.assignees),
|
|
2435
|
+
...assigneeLoginsFromValue(metadata?.assignees),
|
|
2436
|
+
...assigneeLoginsFromValue(raw?.assignees)
|
|
2437
|
+
]));
|
|
2438
|
+
}
|
|
2439
|
+
function taskMatchesAssigneeFilter(task, assignee, options = {}) {
|
|
2440
|
+
const normalized = normalizeTaskAssigneeFilter(assignee, options.currentUserLogin);
|
|
2441
|
+
if (!normalized)
|
|
2442
|
+
return false;
|
|
2443
|
+
return readTaskAssigneeLogins(task).includes(normalized);
|
|
2444
|
+
}
|
|
2445
|
+
function selectTasksAssignedTo(tasks, assignee, options = {}) {
|
|
2446
|
+
return tasks.filter((task) => taskMatchesAssigneeFilter(task, assignee, options));
|
|
2447
|
+
}
|
|
2448
|
+
function selectTasksAssignedToMe(tasks, currentUserLogin) {
|
|
2449
|
+
return selectTasksAssignedTo(tasks, "@me", currentUserLogin === undefined ? {} : { currentUserLogin });
|
|
2450
|
+
}
|
|
2131
2451
|
function selectRun(snapshot, runId) {
|
|
2132
2452
|
if (!runId)
|
|
2133
2453
|
return null;
|
|
@@ -2188,18 +2508,120 @@ function selectUserInputsForWorkspace(snapshot, workspaceId) {
|
|
|
2188
2508
|
const runIds = new Set(selectRunsForWorkspace(snapshot, workspaceId).map((run) => run.id));
|
|
2189
2509
|
return (snapshot.userInputs ?? []).filter((request) => runIds.has(request.runId));
|
|
2190
2510
|
}
|
|
2511
|
+
// packages/core/src/taskScore.ts
|
|
2512
|
+
var ROLE_WEIGHT = {
|
|
2513
|
+
architect: 25,
|
|
2514
|
+
extractor: 10
|
|
2515
|
+
};
|
|
2516
|
+
var CRITICALITY_WEIGHT = {
|
|
2517
|
+
core: 20,
|
|
2518
|
+
high: 10,
|
|
2519
|
+
normal: 0
|
|
2520
|
+
};
|
|
2521
|
+
function finiteNumber(value, fallback) {
|
|
2522
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2523
|
+
}
|
|
2524
|
+
function scoreTask(input) {
|
|
2525
|
+
const priority = finiteNumber(input.priority, 3);
|
|
2526
|
+
const unblockCount = Math.max(0, finiteNumber(input.unblockCount, 0));
|
|
2527
|
+
const roleWeight = input.role ? ROLE_WEIGHT[input.role] ?? 0 : 0;
|
|
2528
|
+
const criticalityWeight = input.criticality ? CRITICALITY_WEIGHT[input.criticality] ?? 0 : 0;
|
|
2529
|
+
const validationWeight = (input.validation ?? []).some((entry) => entry.includes("test-contract") || entry.includes("test-boundary")) ? 8 : 0;
|
|
2530
|
+
const queueWeight = finiteNumber(input.queueWeight, 0);
|
|
2531
|
+
return unblockCount * 100 + (10 - priority) + roleWeight + criticalityWeight + validationWeight + queueWeight;
|
|
2532
|
+
}
|
|
2533
|
+
function rankTasks(tasks, scoreInput, idOf) {
|
|
2534
|
+
return tasks.map((task) => {
|
|
2535
|
+
const input = scoreInput(task);
|
|
2536
|
+
return {
|
|
2537
|
+
task,
|
|
2538
|
+
score: scoreTask(input),
|
|
2539
|
+
priority: finiteNumber(input.priority, 3),
|
|
2540
|
+
unblockCount: Math.max(0, finiteNumber(input.unblockCount, 0))
|
|
2541
|
+
};
|
|
2542
|
+
}).toSorted((left, right) => {
|
|
2543
|
+
const scoreDelta = right.score - left.score;
|
|
2544
|
+
if (scoreDelta !== 0)
|
|
2545
|
+
return scoreDelta;
|
|
2546
|
+
const unblockDelta = right.unblockCount - left.unblockCount;
|
|
2547
|
+
if (unblockDelta !== 0)
|
|
2548
|
+
return unblockDelta;
|
|
2549
|
+
const priorityDelta = left.priority - right.priority;
|
|
2550
|
+
if (priorityDelta !== 0)
|
|
2551
|
+
return priorityDelta;
|
|
2552
|
+
return idOf(left.task).localeCompare(idOf(right.task));
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2191
2556
|
// packages/core/src/taskGraph.ts
|
|
2192
|
-
function
|
|
2557
|
+
function isObjectRecord2(value) {
|
|
2193
2558
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2194
2559
|
}
|
|
2195
|
-
function
|
|
2196
|
-
const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
|
|
2197
|
-
const value = metadata?.[key];
|
|
2560
|
+
function readStringList(value) {
|
|
2198
2561
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
|
|
2199
2562
|
}
|
|
2563
|
+
function unique(values) {
|
|
2564
|
+
return Array.from(new Set(values));
|
|
2565
|
+
}
|
|
2566
|
+
function readTaskMetadataStringList(task, key) {
|
|
2567
|
+
const taskRecord = task;
|
|
2568
|
+
const topLevel = readStringList(taskRecord[key]);
|
|
2569
|
+
if (topLevel.length > 0)
|
|
2570
|
+
return topLevel;
|
|
2571
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2572
|
+
const metadataList = readStringList(metadata?.[key]);
|
|
2573
|
+
if (metadataList.length > 0)
|
|
2574
|
+
return metadataList;
|
|
2575
|
+
if (key === "dependencies") {
|
|
2576
|
+
return readStringList(metadata?.deps);
|
|
2577
|
+
}
|
|
2578
|
+
return [];
|
|
2579
|
+
}
|
|
2580
|
+
function readTaskBlockingDependencyRefs(task) {
|
|
2581
|
+
return readTaskMetadataStringList(task, "dependencies");
|
|
2582
|
+
}
|
|
2583
|
+
function readTaskDependencyRefs(task) {
|
|
2584
|
+
return unique([
|
|
2585
|
+
...readTaskMetadataStringList(task, "dependencies"),
|
|
2586
|
+
...readTaskMetadataStringList(task, "parentChildDeps")
|
|
2587
|
+
]);
|
|
2588
|
+
}
|
|
2200
2589
|
function readTaskSourceIssueId(task) {
|
|
2201
|
-
|
|
2202
|
-
|
|
2590
|
+
if (typeof task.sourceIssueId === "string" && task.sourceIssueId.length > 0) {
|
|
2591
|
+
return task.sourceIssueId;
|
|
2592
|
+
}
|
|
2593
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2594
|
+
if (typeof metadata?.sourceIssueId === "string" && metadata.sourceIssueId.length > 0) {
|
|
2595
|
+
return metadata.sourceIssueId;
|
|
2596
|
+
}
|
|
2597
|
+
const rigMetadata = isObjectRecord2(metadata?._rig) ? metadata._rig : null;
|
|
2598
|
+
return typeof rigMetadata?.sourceIssueId === "string" && rigMetadata.sourceIssueId.length > 0 ? rigMetadata.sourceIssueId : null;
|
|
2599
|
+
}
|
|
2600
|
+
function readTaskScope(task) {
|
|
2601
|
+
const taskRecord = task;
|
|
2602
|
+
const topLevel = readStringList(taskRecord.scope);
|
|
2603
|
+
if (topLevel.length > 0)
|
|
2604
|
+
return unique(topLevel.map((entry) => entry.trim()).filter((entry) => entry.length > 0));
|
|
2605
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2606
|
+
const metadataScope = readStringList(metadata?.scope);
|
|
2607
|
+
if (metadataScope.length > 0)
|
|
2608
|
+
return unique(metadataScope.map((entry) => entry.trim()).filter((entry) => entry.length > 0));
|
|
2609
|
+
return unique([
|
|
2610
|
+
...readStringList(metadata?.files),
|
|
2611
|
+
...readStringList(metadata?.paths)
|
|
2612
|
+
].map((entry) => entry.trim()).filter((entry) => entry.length > 0));
|
|
2613
|
+
}
|
|
2614
|
+
function isScopeList(input) {
|
|
2615
|
+
return Array.isArray(input);
|
|
2616
|
+
}
|
|
2617
|
+
function normalizeScopeInput(input) {
|
|
2618
|
+
return isScopeList(input) ? unique(input.map((entry) => entry.trim()).filter((entry) => entry.length > 0)) : readTaskScope(input);
|
|
2619
|
+
}
|
|
2620
|
+
function disjointScope(left, right) {
|
|
2621
|
+
const leftScope = new Set(normalizeScopeInput(left));
|
|
2622
|
+
if (leftScope.size === 0)
|
|
2623
|
+
return true;
|
|
2624
|
+
return normalizeScopeInput(right).every((entry) => !leftScope.has(entry));
|
|
2203
2625
|
}
|
|
2204
2626
|
function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
|
|
2205
2627
|
if (tasksById.has(ref))
|
|
@@ -2229,11 +2651,7 @@ function computeTaskBlockingDepths(tasks) {
|
|
|
2229
2651
|
if (!task)
|
|
2230
2652
|
return 0;
|
|
2231
2653
|
stack.add(taskId);
|
|
2232
|
-
const
|
|
2233
|
-
...readTaskMetadataStringList(task, "dependencies"),
|
|
2234
|
-
...readTaskMetadataStringList(task, "parentChildDeps")
|
|
2235
|
-
];
|
|
2236
|
-
const blockers = refs.map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
|
|
2654
|
+
const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
|
|
2237
2655
|
const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
|
|
2238
2656
|
stack.delete(taskId);
|
|
2239
2657
|
memo.set(taskId, depth);
|
|
@@ -2244,6 +2662,212 @@ function computeTaskBlockingDepths(tasks) {
|
|
|
2244
2662
|
}
|
|
2245
2663
|
return memo;
|
|
2246
2664
|
}
|
|
2665
|
+
function isTaskTerminalStatus(status) {
|
|
2666
|
+
switch (status) {
|
|
2667
|
+
case "closed":
|
|
2668
|
+
case "completed":
|
|
2669
|
+
case "done":
|
|
2670
|
+
case "cancelled":
|
|
2671
|
+
case "canceled":
|
|
2672
|
+
return true;
|
|
2673
|
+
default:
|
|
2674
|
+
return false;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
function isTaskBlockedStatus(status) {
|
|
2678
|
+
return status === "blocked";
|
|
2679
|
+
}
|
|
2680
|
+
function isTaskRunnableStatus(status) {
|
|
2681
|
+
if (status === null || status === undefined || status === "")
|
|
2682
|
+
return true;
|
|
2683
|
+
if (isTaskTerminalStatus(status) || isTaskBlockedStatus(status))
|
|
2684
|
+
return false;
|
|
2685
|
+
switch (status) {
|
|
2686
|
+
case "ready":
|
|
2687
|
+
case "open":
|
|
2688
|
+
case "failed":
|
|
2689
|
+
return true;
|
|
2690
|
+
default:
|
|
2691
|
+
return false;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
function priorityValue(task) {
|
|
2695
|
+
return typeof task.priority === "number" && Number.isFinite(task.priority) ? task.priority : Number.MAX_SAFE_INTEGER;
|
|
2696
|
+
}
|
|
2697
|
+
function readTaskRole(task) {
|
|
2698
|
+
if (typeof task.role === "string" && task.role.trim())
|
|
2699
|
+
return task.role.trim();
|
|
2700
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2701
|
+
return typeof metadata?.role === "string" && metadata.role.trim() ? metadata.role.trim() : null;
|
|
2702
|
+
}
|
|
2703
|
+
function readTaskCriticality(task) {
|
|
2704
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2705
|
+
return typeof metadata?.criticality === "string" && metadata.criticality.trim() ? metadata.criticality.trim() : null;
|
|
2706
|
+
}
|
|
2707
|
+
function readTaskValidationKeys(task) {
|
|
2708
|
+
const taskRecord = task;
|
|
2709
|
+
const topLevel = readStringList(taskRecord.validationKeys);
|
|
2710
|
+
if (topLevel.length > 0)
|
|
2711
|
+
return topLevel;
|
|
2712
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2713
|
+
return readStringList(metadata?.validation);
|
|
2714
|
+
}
|
|
2715
|
+
function readTaskQueueWeight(task) {
|
|
2716
|
+
const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
|
|
2717
|
+
const queueWeight = metadata?.queueWeight ?? metadata?.queue_weight;
|
|
2718
|
+
return typeof queueWeight === "number" && Number.isFinite(queueWeight) ? queueWeight : 0;
|
|
2719
|
+
}
|
|
2720
|
+
function scoreInputForTask(task, unblockCount) {
|
|
2721
|
+
return {
|
|
2722
|
+
priority: task.priority,
|
|
2723
|
+
unblockCount,
|
|
2724
|
+
role: readTaskRole(task),
|
|
2725
|
+
criticality: readTaskCriticality(task),
|
|
2726
|
+
validation: readTaskValidationKeys(task),
|
|
2727
|
+
queueWeight: readTaskQueueWeight(task)
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
function computeTaskDependencyBadges(tasks) {
|
|
2731
|
+
const index = buildTaskReferenceIndex(tasks);
|
|
2732
|
+
const blockingDepths = computeTaskBlockingDepths(tasks);
|
|
2733
|
+
const dependencyIdsByTask = new Map;
|
|
2734
|
+
const unresolvedRefsByTask = new Map;
|
|
2735
|
+
const blocksByTask = new Map;
|
|
2736
|
+
for (const task of tasks) {
|
|
2737
|
+
const dependencyIds = [];
|
|
2738
|
+
const unresolvedRefs = [];
|
|
2739
|
+
for (const ref of readTaskBlockingDependencyRefs(task)) {
|
|
2740
|
+
const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
2741
|
+
if (dependencyId && dependencyId !== task.id) {
|
|
2742
|
+
dependencyIds.push(dependencyId);
|
|
2743
|
+
const blocks = blocksByTask.get(dependencyId);
|
|
2744
|
+
if (blocks) {
|
|
2745
|
+
blocks.push(task.id);
|
|
2746
|
+
} else {
|
|
2747
|
+
blocksByTask.set(dependencyId, [task.id]);
|
|
2748
|
+
}
|
|
2749
|
+
} else {
|
|
2750
|
+
unresolvedRefs.push(ref);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
dependencyIdsByTask.set(task.id, unique(dependencyIds));
|
|
2754
|
+
unresolvedRefsByTask.set(task.id, unique(unresolvedRefs));
|
|
2755
|
+
}
|
|
2756
|
+
const summaries = new Map;
|
|
2757
|
+
for (const task of tasks) {
|
|
2758
|
+
const dependencyIds = dependencyIdsByTask.get(task.id) ?? [];
|
|
2759
|
+
const unresolvedDependencyRefs = unresolvedRefsByTask.get(task.id) ?? [];
|
|
2760
|
+
const blockedBy = dependencyIds.filter((dependencyId) => {
|
|
2761
|
+
const dependency = index.tasksById.get(dependencyId);
|
|
2762
|
+
return dependency ? !isTaskTerminalStatus(dependency.status) : false;
|
|
2763
|
+
});
|
|
2764
|
+
const blocks = unique(blocksByTask.get(task.id) ?? []);
|
|
2765
|
+
const blocked = isTaskBlockedStatus(task.status) || blockedBy.length > 0;
|
|
2766
|
+
const ready = isTaskRunnableStatus(task.status) && !blocked;
|
|
2767
|
+
const badges = [];
|
|
2768
|
+
if (blocked) {
|
|
2769
|
+
badges.push({
|
|
2770
|
+
kind: "blocked",
|
|
2771
|
+
label: blockedBy.length > 0 ? `blocked \xD7${blockedBy.length}` : "blocked",
|
|
2772
|
+
description: blockedBy.length > 0 ? `Waiting on ${blockedBy.join(", ")}.` : "Task source marks this task blocked.",
|
|
2773
|
+
...blockedBy.length > 0 ? { count: blockedBy.length } : {},
|
|
2774
|
+
taskIds: blockedBy
|
|
2775
|
+
});
|
|
2776
|
+
} else if (ready) {
|
|
2777
|
+
badges.push({
|
|
2778
|
+
kind: "ready",
|
|
2779
|
+
label: "ready",
|
|
2780
|
+
description: "No open dependencies block this task."
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
if (dependencyIds.length > 0 || blocks.length > 0 || unresolvedDependencyRefs.length > 0) {
|
|
2784
|
+
badges.push({
|
|
2785
|
+
kind: "dependency",
|
|
2786
|
+
label: `deps ${dependencyIds.length}/${blocks.length}`,
|
|
2787
|
+
description: [
|
|
2788
|
+
dependencyIds.length > 0 ? `Depends on ${dependencyIds.join(", ")}.` : null,
|
|
2789
|
+
blocks.length > 0 ? `Blocks ${blocks.join(", ")}.` : null,
|
|
2790
|
+
unresolvedDependencyRefs.length > 0 ? `Unresolved refs: ${unresolvedDependencyRefs.join(", ")}.` : null
|
|
2791
|
+
].filter((part) => part !== null).join(" "),
|
|
2792
|
+
count: dependencyIds.length + blocks.length,
|
|
2793
|
+
taskIds: unique([...dependencyIds, ...blocks])
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
summaries.set(task.id, {
|
|
2797
|
+
taskId: task.id,
|
|
2798
|
+
blockingDepth: blockingDepths.get(task.id) ?? 0,
|
|
2799
|
+
dependencyIds,
|
|
2800
|
+
unresolvedDependencyRefs,
|
|
2801
|
+
blockedBy,
|
|
2802
|
+
blocks,
|
|
2803
|
+
blocked,
|
|
2804
|
+
ready,
|
|
2805
|
+
dependencyCount: dependencyIds.length,
|
|
2806
|
+
dependentCount: blocks.length,
|
|
2807
|
+
badges
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
return summaries;
|
|
2811
|
+
}
|
|
2812
|
+
function selectNextReadyTaskByPriority(tasks, options = {}) {
|
|
2813
|
+
const excluded = new Set(options.excludeTaskIds ?? []);
|
|
2814
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
2815
|
+
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) => {
|
|
2816
|
+
const priorityDelta = priorityValue(left) - priorityValue(right);
|
|
2817
|
+
if (priorityDelta !== 0)
|
|
2818
|
+
return priorityDelta;
|
|
2819
|
+
const createdDelta = (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
|
|
2820
|
+
if (createdDelta !== 0)
|
|
2821
|
+
return createdDelta;
|
|
2822
|
+
return left.id.localeCompare(right.id);
|
|
2823
|
+
});
|
|
2824
|
+
return candidates[0] ?? null;
|
|
2825
|
+
}
|
|
2826
|
+
function rankReadyTasks(tasks, options = {}) {
|
|
2827
|
+
const excluded = new Set(options.excludeTaskIds ?? []);
|
|
2828
|
+
const activeTaskIds = new Set(options.activeTaskIds ?? []);
|
|
2829
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
2830
|
+
const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
|
|
2831
|
+
const openBlockCount = (task) => (badges.get(task.id)?.blocks ?? []).filter((blockedTaskId) => {
|
|
2832
|
+
const blockedTask = tasksById.get(blockedTaskId);
|
|
2833
|
+
return blockedTask ? !isTaskTerminalStatus(blockedTask.status) : false;
|
|
2834
|
+
}).length;
|
|
2835
|
+
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);
|
|
2836
|
+
const maxUnblockCount = Math.max(0, ...readyTasks.map(openBlockCount));
|
|
2837
|
+
const selectedByMode = readyTasks.filter((task) => {
|
|
2838
|
+
const unblockCount = openBlockCount(task);
|
|
2839
|
+
if (options.selection === "blocking-only")
|
|
2840
|
+
return unblockCount > 0;
|
|
2841
|
+
if (options.selection === "max-unblock")
|
|
2842
|
+
return maxUnblockCount > 0 && unblockCount === maxUnblockCount;
|
|
2843
|
+
return true;
|
|
2844
|
+
});
|
|
2845
|
+
return rankTasks(selectedByMode, (task) => scoreInputForTask(task, openBlockCount(task)), (task) => task.id).map((entry) => ({
|
|
2846
|
+
task: entry.task,
|
|
2847
|
+
score: entry.score,
|
|
2848
|
+
priority: entry.priority,
|
|
2849
|
+
unblockCount: entry.unblockCount,
|
|
2850
|
+
scope: readTaskScope(entry.task)
|
|
2851
|
+
}));
|
|
2852
|
+
}
|
|
2853
|
+
function selectRankedReadyTasks(tasks, options = {}) {
|
|
2854
|
+
const ranked = rankReadyTasks(tasks, options);
|
|
2855
|
+
if (options.requireDisjointScopes !== true && !options.disjointWithScopes) {
|
|
2856
|
+
return ranked.slice(0, options.limit).map((entry) => entry.task);
|
|
2857
|
+
}
|
|
2858
|
+
const occupiedScopes = new Set(options.disjointWithScopes ?? []);
|
|
2859
|
+
const selected = [];
|
|
2860
|
+
for (const entry of ranked) {
|
|
2861
|
+
if (entry.scope.some((scope) => occupiedScopes.has(scope)))
|
|
2862
|
+
continue;
|
|
2863
|
+
selected.push(entry.task);
|
|
2864
|
+
for (const scope of entry.scope)
|
|
2865
|
+
occupiedScopes.add(scope);
|
|
2866
|
+
if (options.limit !== undefined && selected.length >= options.limit)
|
|
2867
|
+
break;
|
|
2868
|
+
}
|
|
2869
|
+
return selected;
|
|
2870
|
+
}
|
|
2247
2871
|
// packages/core/src/taskGraphCodes.ts
|
|
2248
2872
|
var TASK_CODE_RE = /^\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]\s*/;
|
|
2249
2873
|
function extractTaskCode(title) {
|
|
@@ -2283,12 +2907,15 @@ var PALETTE = [
|
|
|
2283
2907
|
{ bg: "#132c35", border: "#1783a6", edge: "#53c4e5" },
|
|
2284
2908
|
{ bg: "#26310f", border: "#6d9a19", edge: "#a7da42" }
|
|
2285
2909
|
];
|
|
2286
|
-
function
|
|
2910
|
+
function isObjectRecord3(value) {
|
|
2287
2911
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2288
2912
|
}
|
|
2289
2913
|
function readIssueType(task) {
|
|
2290
|
-
const metadata =
|
|
2291
|
-
|
|
2914
|
+
const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
|
|
2915
|
+
if (typeof metadata?.issueType === "string")
|
|
2916
|
+
return metadata.issueType;
|
|
2917
|
+
const raw = isObjectRecord3(metadata?.raw) ? metadata.raw : null;
|
|
2918
|
+
return typeof raw?.issueType === "string" ? raw.issueType : null;
|
|
2292
2919
|
}
|
|
2293
2920
|
function isGraphTask(task) {
|
|
2294
2921
|
return readIssueType(task) !== "epic";
|
|
@@ -2535,6 +3162,7 @@ function buildTaskGraphLayout(snapshot, tasks, options) {
|
|
|
2535
3162
|
const artifactCount = (snapshot?.artifacts ?? []).filter((artifact) => runIds.has(artifact.runId)).length;
|
|
2536
3163
|
nodes.push({
|
|
2537
3164
|
id: task.id,
|
|
3165
|
+
taskId: task.id,
|
|
2538
3166
|
task,
|
|
2539
3167
|
rowKey,
|
|
2540
3168
|
rowLabel: rowLabelByKey.get(rowKey) ?? rowKey,
|
|
@@ -2569,6 +3197,631 @@ function buildTaskGraphLayout(snapshot, tasks, options) {
|
|
|
2569
3197
|
taskCount: graphTasks.length
|
|
2570
3198
|
};
|
|
2571
3199
|
}
|
|
3200
|
+
// packages/core/src/stageResolve.ts
|
|
3201
|
+
class PipelineUnresolvableError extends Error {
|
|
3202
|
+
cycles;
|
|
3203
|
+
contributors;
|
|
3204
|
+
constructor(message, cycles, contributors) {
|
|
3205
|
+
super(message);
|
|
3206
|
+
this.name = "PipelineUnresolvableError";
|
|
3207
|
+
this.cycles = cycles;
|
|
3208
|
+
this.contributors = contributors;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
function uniqueSorted(values) {
|
|
3212
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
3213
|
+
}
|
|
3214
|
+
function wrapperForMutation(mutation) {
|
|
3215
|
+
return "wrapper" in mutation ? mutation.wrapper : mutation.around;
|
|
3216
|
+
}
|
|
3217
|
+
function contributorOf(mutation) {
|
|
3218
|
+
if (mutation.contributedBy?.trim())
|
|
3219
|
+
return mutation.contributedBy;
|
|
3220
|
+
if (mutation.op === "wrap") {
|
|
3221
|
+
const wrapper = wrapperForMutation(mutation);
|
|
3222
|
+
if (wrapper.id?.trim())
|
|
3223
|
+
return wrapper.id;
|
|
3224
|
+
}
|
|
3225
|
+
return "anonymous";
|
|
3226
|
+
}
|
|
3227
|
+
function stableMutationCompare(left, right) {
|
|
3228
|
+
const leftTarget = left.op === "insert" ? left.stage.id : left.id;
|
|
3229
|
+
const rightTarget = right.op === "insert" ? right.stage.id : right.id;
|
|
3230
|
+
const targetDelta = leftTarget.localeCompare(rightTarget);
|
|
3231
|
+
if (targetDelta !== 0)
|
|
3232
|
+
return targetDelta;
|
|
3233
|
+
return contributorOf(left).localeCompare(contributorOf(right));
|
|
3234
|
+
}
|
|
3235
|
+
function ensureUniqueStageIds(stages) {
|
|
3236
|
+
const seen = new Set;
|
|
3237
|
+
for (const stage of stages) {
|
|
3238
|
+
if (seen.has(stage.id)) {
|
|
3239
|
+
throw new PipelineUnresolvableError(`Duplicate stage id: ${stage.id}`, [], []);
|
|
3240
|
+
}
|
|
3241
|
+
seen.add(stage.id);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
function assertNoDuplicateMutationTargets(mutations, op) {
|
|
3245
|
+
const seen = new Map;
|
|
3246
|
+
for (const mutation of mutations.filter((entry) => entry.op === op)) {
|
|
3247
|
+
const id = mutation.op === "insert" ? mutation.stage.id : mutation.id;
|
|
3248
|
+
const previous = seen.get(id);
|
|
3249
|
+
if (previous) {
|
|
3250
|
+
throw new PipelineUnresolvableError(`Duplicate ${op} mutation for stage ${id}: ${previous}, ${contributorOf(mutation)}`, [], uniqueSorted([previous, contributorOf(mutation)]));
|
|
3251
|
+
}
|
|
3252
|
+
seen.set(id, contributorOf(mutation));
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
function mergeAnchors(current, incoming) {
|
|
3256
|
+
return uniqueSorted([...current, ...incoming ?? []]);
|
|
3257
|
+
}
|
|
3258
|
+
function stagePriority(stage) {
|
|
3259
|
+
return typeof stage.priority === "number" && Number.isFinite(stage.priority) ? stage.priority : 0;
|
|
3260
|
+
}
|
|
3261
|
+
function readyCompare(states) {
|
|
3262
|
+
return (left, right) => {
|
|
3263
|
+
const leftState = states.get(left);
|
|
3264
|
+
const rightState = states.get(right);
|
|
3265
|
+
const priorityDelta = stagePriority(rightState?.stage ?? { id: right }) - stagePriority(leftState?.stage ?? { id: left });
|
|
3266
|
+
if (priorityDelta !== 0)
|
|
3267
|
+
return priorityDelta;
|
|
3268
|
+
if (leftState?.baseIndex !== null && rightState?.baseIndex !== null && leftState?.baseIndex !== rightState?.baseIndex) {
|
|
3269
|
+
return (leftState?.baseIndex ?? 0) - (rightState?.baseIndex ?? 0);
|
|
3270
|
+
}
|
|
3271
|
+
if (leftState?.baseIndex !== null && rightState?.baseIndex === null)
|
|
3272
|
+
return -1;
|
|
3273
|
+
if (leftState?.baseIndex === null && rightState?.baseIndex !== null)
|
|
3274
|
+
return 1;
|
|
3275
|
+
return left.localeCompare(right);
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
function findCycles(nodes, edges) {
|
|
3279
|
+
const adjacency = new Map;
|
|
3280
|
+
for (const node of nodes)
|
|
3281
|
+
adjacency.set(node, []);
|
|
3282
|
+
for (const edge of edges)
|
|
3283
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
3284
|
+
for (const targets of adjacency.values())
|
|
3285
|
+
targets.sort((left, right) => left.localeCompare(right));
|
|
3286
|
+
let nextIndex = 0;
|
|
3287
|
+
const indexByNode = new Map;
|
|
3288
|
+
const lowByNode = new Map;
|
|
3289
|
+
const stack = [];
|
|
3290
|
+
const onStack = new Set;
|
|
3291
|
+
const cycles = [];
|
|
3292
|
+
const visit = (node) => {
|
|
3293
|
+
indexByNode.set(node, nextIndex);
|
|
3294
|
+
lowByNode.set(node, nextIndex);
|
|
3295
|
+
nextIndex += 1;
|
|
3296
|
+
stack.push(node);
|
|
3297
|
+
onStack.add(node);
|
|
3298
|
+
for (const target of adjacency.get(node) ?? []) {
|
|
3299
|
+
if (!indexByNode.has(target)) {
|
|
3300
|
+
visit(target);
|
|
3301
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
|
|
3302
|
+
} else if (onStack.has(target)) {
|
|
3303
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (lowByNode.get(node) !== indexByNode.get(node))
|
|
3307
|
+
return;
|
|
3308
|
+
const component = [];
|
|
3309
|
+
let current;
|
|
3310
|
+
do {
|
|
3311
|
+
current = stack.pop();
|
|
3312
|
+
if (current) {
|
|
3313
|
+
onStack.delete(current);
|
|
3314
|
+
component.push(current);
|
|
3315
|
+
}
|
|
3316
|
+
} while (current && current !== node);
|
|
3317
|
+
const hasSelfLoop = edges.some((edge) => edge.from === node && edge.to === node);
|
|
3318
|
+
if (component.length > 1 || hasSelfLoop) {
|
|
3319
|
+
cycles.push(component.sort((left, right) => left.localeCompare(right)));
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
for (const node of [...nodes].sort((left, right) => left.localeCompare(right))) {
|
|
3323
|
+
if (!indexByNode.has(node))
|
|
3324
|
+
visit(node);
|
|
3325
|
+
}
|
|
3326
|
+
return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
|
|
3327
|
+
}
|
|
3328
|
+
function topologicalOrder(states, edges) {
|
|
3329
|
+
const nodes = [...states.keys()];
|
|
3330
|
+
const outgoing = new Map;
|
|
3331
|
+
const indegree = new Map;
|
|
3332
|
+
for (const node of nodes) {
|
|
3333
|
+
outgoing.set(node, []);
|
|
3334
|
+
indegree.set(node, 0);
|
|
3335
|
+
}
|
|
3336
|
+
for (const edge of edges) {
|
|
3337
|
+
outgoing.get(edge.from)?.push(edge.to);
|
|
3338
|
+
indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
|
|
3339
|
+
}
|
|
3340
|
+
for (const targets of outgoing.values())
|
|
3341
|
+
targets.sort((left, right) => left.localeCompare(right));
|
|
3342
|
+
const compare = readyCompare(states);
|
|
3343
|
+
const ready = nodes.filter((node) => (indegree.get(node) ?? 0) === 0).sort(compare);
|
|
3344
|
+
const order = [];
|
|
3345
|
+
while (ready.length > 0) {
|
|
3346
|
+
const node = ready.shift();
|
|
3347
|
+
if (!node)
|
|
3348
|
+
break;
|
|
3349
|
+
order.push(node);
|
|
3350
|
+
for (const target of outgoing.get(node) ?? []) {
|
|
3351
|
+
const next = (indegree.get(target) ?? 0) - 1;
|
|
3352
|
+
indegree.set(target, next);
|
|
3353
|
+
if (next === 0) {
|
|
3354
|
+
ready.push(target);
|
|
3355
|
+
ready.sort(compare);
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
if (order.length === nodes.length)
|
|
3360
|
+
return order;
|
|
3361
|
+
const cycles = findCycles(nodes, edges);
|
|
3362
|
+
const cycleMembers = new Set(cycles.flat());
|
|
3363
|
+
const contributors = uniqueSorted(edges.filter((edge) => cycleMembers.has(edge.from) && cycleMembers.has(edge.to)).flatMap((edge) => edge.contributors));
|
|
3364
|
+
throw new PipelineUnresolvableError(`Stage pipeline has unresolved cycle: ${cycles.map((cycle) => cycle.join(" -> ")).join("; ")}`, cycles, contributors);
|
|
3365
|
+
}
|
|
3366
|
+
function composeStageWrappers(state) {
|
|
3367
|
+
const wrappers = state.wrappers.toSorted((left, right) => {
|
|
3368
|
+
const priorityDelta = (right.wrapper.priority ?? 0) - (left.wrapper.priority ?? 0);
|
|
3369
|
+
if (priorityDelta !== 0)
|
|
3370
|
+
return priorityDelta;
|
|
3371
|
+
return left.contributedBy.localeCompare(right.contributedBy);
|
|
3372
|
+
});
|
|
3373
|
+
let stage = state.stage;
|
|
3374
|
+
for (const wrapper of wrappers.toReversed()) {
|
|
3375
|
+
if (wrapper.wrapper.apply)
|
|
3376
|
+
stage = wrapper.wrapper.apply(stage);
|
|
3377
|
+
}
|
|
3378
|
+
return stage;
|
|
3379
|
+
}
|
|
3380
|
+
function resolveStagePipeline(input) {
|
|
3381
|
+
ensureUniqueStageIds(input.defaultStages);
|
|
3382
|
+
const mutations = [...input.mutations ?? []];
|
|
3383
|
+
assertNoDuplicateMutationTargets(mutations, "insert");
|
|
3384
|
+
assertNoDuplicateMutationTargets(mutations, "replace");
|
|
3385
|
+
const states = new Map;
|
|
3386
|
+
const removedStates = new Map;
|
|
3387
|
+
for (const [index, stage] of input.defaultStages.entries()) {
|
|
3388
|
+
states.set(stage.id, {
|
|
3389
|
+
stage: { ...stage, protected: false },
|
|
3390
|
+
before: [...stage.before ?? []],
|
|
3391
|
+
after: [...stage.after ?? []],
|
|
3392
|
+
baseIndex: index,
|
|
3393
|
+
contributedBy: "default",
|
|
3394
|
+
wrappers: [],
|
|
3395
|
+
droppedAnchors: [],
|
|
3396
|
+
isProtected: false
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
for (const mutation of mutations.filter((entry) => entry.op === "remove").sort(stableMutationCompare)) {
|
|
3400
|
+
const state = states.get(mutation.id);
|
|
3401
|
+
const contributor = contributorOf(mutation);
|
|
3402
|
+
if (!state)
|
|
3403
|
+
continue;
|
|
3404
|
+
const removedState = {
|
|
3405
|
+
...state,
|
|
3406
|
+
removedBy: contributor
|
|
3407
|
+
};
|
|
3408
|
+
removedStates.set(mutation.id, removedState);
|
|
3409
|
+
states.delete(mutation.id);
|
|
3410
|
+
}
|
|
3411
|
+
for (const mutation of mutations.filter((entry) => entry.op === "replace").sort(stableMutationCompare)) {
|
|
3412
|
+
const state = states.get(mutation.id);
|
|
3413
|
+
const contributor = contributorOf(mutation);
|
|
3414
|
+
if (!state)
|
|
3415
|
+
continue;
|
|
3416
|
+
const replacement = { ...mutation.stage, id: mutation.id, protected: false };
|
|
3417
|
+
states.set(mutation.id, {
|
|
3418
|
+
...state,
|
|
3419
|
+
stage: replacement,
|
|
3420
|
+
before: mutation.stage.before ? [...mutation.stage.before] : state.before,
|
|
3421
|
+
after: mutation.stage.after ? [...mutation.stage.after] : state.after,
|
|
3422
|
+
replacedBy: contributor,
|
|
3423
|
+
contributedBy: state.contributedBy,
|
|
3424
|
+
isProtected: false
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
for (const mutation of mutations.filter((entry) => entry.op === "insert").sort(stableMutationCompare)) {
|
|
3428
|
+
const contributor = contributorOf(mutation);
|
|
3429
|
+
if (states.has(mutation.stage.id)) {
|
|
3430
|
+
throw new PipelineUnresolvableError(`Inserted stage ${mutation.stage.id} conflicts with an existing stage`, [], [contributor]);
|
|
3431
|
+
}
|
|
3432
|
+
states.set(mutation.stage.id, {
|
|
3433
|
+
stage: { ...mutation.stage, protected: false },
|
|
3434
|
+
before: [...mutation.stage.before ?? []],
|
|
3435
|
+
after: [...mutation.stage.after ?? []],
|
|
3436
|
+
baseIndex: null,
|
|
3437
|
+
contributedBy: contributor,
|
|
3438
|
+
wrappers: [],
|
|
3439
|
+
droppedAnchors: [],
|
|
3440
|
+
isProtected: false
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
for (const mutation of mutations.filter((entry) => entry.op === "reorder").sort(stableMutationCompare)) {
|
|
3444
|
+
const state = states.get(mutation.id);
|
|
3445
|
+
if (!state)
|
|
3446
|
+
continue;
|
|
3447
|
+
states.set(mutation.id, {
|
|
3448
|
+
...state,
|
|
3449
|
+
before: mergeAnchors(state.before, mutation.before),
|
|
3450
|
+
after: mergeAnchors(state.after, mutation.after)
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
for (const mutation of mutations.filter((entry) => entry.op === "wrap").sort(stableMutationCompare)) {
|
|
3454
|
+
const state = states.get(mutation.id);
|
|
3455
|
+
const contributor = contributorOf(mutation);
|
|
3456
|
+
const wrapper = wrapperForMutation(mutation);
|
|
3457
|
+
if (!state)
|
|
3458
|
+
continue;
|
|
3459
|
+
states.set(mutation.id, {
|
|
3460
|
+
...state,
|
|
3461
|
+
wrappers: [...state.wrappers, { contributedBy: contributor, wrapper }]
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
const contributorsForAnchor = (stageId, direction, anchor, state) => {
|
|
3465
|
+
const contributors = new Set;
|
|
3466
|
+
if (state.replacedBy)
|
|
3467
|
+
contributors.add(state.replacedBy);
|
|
3468
|
+
for (const mutation of mutations) {
|
|
3469
|
+
if (mutation.op === "reorder" && mutation.id === stageId && (direction === "before" ? mutation.before : mutation.after)?.includes(anchor)) {
|
|
3470
|
+
contributors.add(contributorOf(mutation));
|
|
3471
|
+
}
|
|
3472
|
+
if (mutation.op === "insert" && mutation.stage.id === stageId && (direction === "before" ? mutation.stage.before : mutation.stage.after)?.includes(anchor)) {
|
|
3473
|
+
contributors.add(contributorOf(mutation));
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
if (contributors.size === 0)
|
|
3477
|
+
contributors.add(state.contributedBy);
|
|
3478
|
+
return uniqueSorted(contributors);
|
|
3479
|
+
};
|
|
3480
|
+
const edges = [];
|
|
3481
|
+
for (const [stageId, state] of states) {
|
|
3482
|
+
const before = [];
|
|
3483
|
+
const after = [];
|
|
3484
|
+
for (const anchor of state.before) {
|
|
3485
|
+
if (states.has(anchor))
|
|
3486
|
+
before.push(anchor);
|
|
3487
|
+
else {
|
|
3488
|
+
const removed = removedStates.get(anchor);
|
|
3489
|
+
state.droppedAnchors.push({
|
|
3490
|
+
stageId,
|
|
3491
|
+
anchor,
|
|
3492
|
+
direction: "before",
|
|
3493
|
+
reason: removed ? "removed" : "missing",
|
|
3494
|
+
...removed?.removedBy ? { removedBy: removed.removedBy } : {}
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
for (const anchor of state.after) {
|
|
3499
|
+
if (states.has(anchor))
|
|
3500
|
+
after.push(anchor);
|
|
3501
|
+
else {
|
|
3502
|
+
const removed = removedStates.get(anchor);
|
|
3503
|
+
state.droppedAnchors.push({
|
|
3504
|
+
stageId,
|
|
3505
|
+
anchor,
|
|
3506
|
+
direction: "after",
|
|
3507
|
+
reason: removed ? "removed" : "missing",
|
|
3508
|
+
...removed?.removedBy ? { removedBy: removed.removedBy } : {}
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
state.before = uniqueSorted(before);
|
|
3513
|
+
state.after = uniqueSorted(after);
|
|
3514
|
+
for (const target of state.before)
|
|
3515
|
+
edges.push({ from: stageId, to: target, contributors: contributorsForAnchor(stageId, "before", target, state) });
|
|
3516
|
+
for (const source of state.after)
|
|
3517
|
+
edges.push({ from: source, to: stageId, contributors: contributorsForAnchor(stageId, "after", source, state) });
|
|
3518
|
+
}
|
|
3519
|
+
const order = topologicalOrder(states, edges);
|
|
3520
|
+
const stages = [];
|
|
3521
|
+
const record = [];
|
|
3522
|
+
for (const id of order) {
|
|
3523
|
+
const state = states.get(id);
|
|
3524
|
+
if (!state)
|
|
3525
|
+
continue;
|
|
3526
|
+
stages.push(composeStageWrappers(state));
|
|
3527
|
+
const wrappedBy = state.wrappers.toSorted((left, right) => {
|
|
3528
|
+
const priorityDelta = (right.wrapper.priority ?? 0) - (left.wrapper.priority ?? 0);
|
|
3529
|
+
if (priorityDelta !== 0)
|
|
3530
|
+
return priorityDelta;
|
|
3531
|
+
return left.contributedBy.localeCompare(right.contributedBy);
|
|
3532
|
+
}).map((wrapper) => wrapper.contributedBy);
|
|
3533
|
+
record.push({
|
|
3534
|
+
stageId: id,
|
|
3535
|
+
contributedBy: state.contributedBy,
|
|
3536
|
+
...state.replacedBy ? { replacedBy: state.replacedBy } : {},
|
|
3537
|
+
...wrappedBy.length > 0 ? { wrappedBy } : {},
|
|
3538
|
+
...state.droppedAnchors.length > 0 ? { droppedAnchors: state.droppedAnchors.toSorted((left, right) => left.anchor.localeCompare(right.anchor)) } : {},
|
|
3539
|
+
isProtected: state.isProtected
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
record.push(...[...removedStates.entries()].toSorted((left, right) => {
|
|
3543
|
+
const leftIndex = left[1].baseIndex ?? Number.MAX_SAFE_INTEGER;
|
|
3544
|
+
const rightIndex = right[1].baseIndex ?? Number.MAX_SAFE_INTEGER;
|
|
3545
|
+
if (leftIndex !== rightIndex)
|
|
3546
|
+
return leftIndex - rightIndex;
|
|
3547
|
+
return left[0].localeCompare(right[0]);
|
|
3548
|
+
}).map(([stageId, state]) => ({
|
|
3549
|
+
stageId,
|
|
3550
|
+
contributedBy: state.contributedBy,
|
|
3551
|
+
...state.removedBy ? { removedBy: state.removedBy } : {},
|
|
3552
|
+
isProtected: state.isProtected
|
|
3553
|
+
})));
|
|
3554
|
+
return { stages, order, record, cycles: [] };
|
|
3555
|
+
}
|
|
3556
|
+
// packages/core/src/dependencyGraph.ts
|
|
3557
|
+
function uniqueSorted2(values) {
|
|
3558
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
3559
|
+
}
|
|
3560
|
+
function deriveGraphId(tasks) {
|
|
3561
|
+
const basis = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right)).join("|");
|
|
3562
|
+
let hash = 2166136261;
|
|
3563
|
+
for (let index = 0;index < basis.length; index += 1) {
|
|
3564
|
+
hash ^= basis.charCodeAt(index);
|
|
3565
|
+
hash = Math.imul(hash, 16777619);
|
|
3566
|
+
}
|
|
3567
|
+
return `graph-${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
3568
|
+
}
|
|
3569
|
+
function dedupeEdges(edges) {
|
|
3570
|
+
const byKey = new Map;
|
|
3571
|
+
for (const edge of edges) {
|
|
3572
|
+
byKey.set(`${edge.type}\x00${edge.fromTaskId}\x00${edge.toTaskId}`, edge);
|
|
3573
|
+
}
|
|
3574
|
+
return [...byKey.values()].toSorted((left, right) => {
|
|
3575
|
+
const typeDelta = left.type.localeCompare(right.type);
|
|
3576
|
+
if (typeDelta !== 0)
|
|
3577
|
+
return typeDelta;
|
|
3578
|
+
const fromDelta = left.fromTaskId.localeCompare(right.fromTaskId);
|
|
3579
|
+
if (fromDelta !== 0)
|
|
3580
|
+
return fromDelta;
|
|
3581
|
+
return left.toTaskId.localeCompare(right.toTaskId);
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
function detectBlockingCycles(tasks, edges) {
|
|
3585
|
+
const taskIds = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right));
|
|
3586
|
+
const adjacency = new Map;
|
|
3587
|
+
for (const taskId of taskIds)
|
|
3588
|
+
adjacency.set(taskId, []);
|
|
3589
|
+
for (const edge of edges) {
|
|
3590
|
+
if (edge.type === "blocks")
|
|
3591
|
+
adjacency.get(edge.fromTaskId)?.push(edge.toTaskId);
|
|
3592
|
+
}
|
|
3593
|
+
for (const targets of adjacency.values())
|
|
3594
|
+
targets.sort((left, right) => left.localeCompare(right));
|
|
3595
|
+
let nextIndex = 0;
|
|
3596
|
+
const indexByNode = new Map;
|
|
3597
|
+
const lowByNode = new Map;
|
|
3598
|
+
const stack = [];
|
|
3599
|
+
const onStack = new Set;
|
|
3600
|
+
const cycles = [];
|
|
3601
|
+
const visit = (node) => {
|
|
3602
|
+
indexByNode.set(node, nextIndex);
|
|
3603
|
+
lowByNode.set(node, nextIndex);
|
|
3604
|
+
nextIndex += 1;
|
|
3605
|
+
stack.push(node);
|
|
3606
|
+
onStack.add(node);
|
|
3607
|
+
for (const target of adjacency.get(node) ?? []) {
|
|
3608
|
+
if (!indexByNode.has(target)) {
|
|
3609
|
+
visit(target);
|
|
3610
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
|
|
3611
|
+
} else if (onStack.has(target)) {
|
|
3612
|
+
lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
if (lowByNode.get(node) !== indexByNode.get(node))
|
|
3616
|
+
return;
|
|
3617
|
+
const component = [];
|
|
3618
|
+
let current;
|
|
3619
|
+
do {
|
|
3620
|
+
current = stack.pop();
|
|
3621
|
+
if (current) {
|
|
3622
|
+
onStack.delete(current);
|
|
3623
|
+
component.push(current);
|
|
3624
|
+
}
|
|
3625
|
+
} while (current && current !== node);
|
|
3626
|
+
const selfLoop = (adjacency.get(node) ?? []).includes(node);
|
|
3627
|
+
if (component.length > 1 || selfLoop)
|
|
3628
|
+
cycles.push(component.sort((left, right) => left.localeCompare(right)));
|
|
3629
|
+
};
|
|
3630
|
+
for (const taskId of taskIds) {
|
|
3631
|
+
if (!indexByNode.has(taskId))
|
|
3632
|
+
visit(taskId);
|
|
3633
|
+
}
|
|
3634
|
+
return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
|
|
3635
|
+
}
|
|
3636
|
+
function buildDependencyGraphModel(tasks, options = {}) {
|
|
3637
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
3638
|
+
const index = buildTaskReferenceIndex(tasks);
|
|
3639
|
+
const edges = [];
|
|
3640
|
+
const unresolvedRefs = [];
|
|
3641
|
+
for (const task of tasks) {
|
|
3642
|
+
for (const ref of readTaskMetadataStringList(task, "dependencies")) {
|
|
3643
|
+
const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
3644
|
+
if (dependencyId)
|
|
3645
|
+
edges.push({ fromTaskId: dependencyId, toTaskId: String(task.id), type: "blocks" });
|
|
3646
|
+
else
|
|
3647
|
+
unresolvedRefs.push(ref);
|
|
3648
|
+
}
|
|
3649
|
+
for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
|
|
3650
|
+
const parentId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
3651
|
+
if (parentId)
|
|
3652
|
+
edges.push({ fromTaskId: parentId, toTaskId: String(task.id), type: "parent-child" });
|
|
3653
|
+
else
|
|
3654
|
+
unresolvedRefs.push(ref);
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
const dedupedEdges = dedupeEdges(edges);
|
|
3658
|
+
const nodes = tasks.map((task) => {
|
|
3659
|
+
const summary = badges.get(task.id);
|
|
3660
|
+
const assignees = readTaskAssigneeLogins(task);
|
|
3661
|
+
const groupKey = extractTaskGroupKey(task.title);
|
|
3662
|
+
return {
|
|
3663
|
+
taskId: String(task.id),
|
|
3664
|
+
title: task.title,
|
|
3665
|
+
status: task.status,
|
|
3666
|
+
priority: task.priority,
|
|
3667
|
+
assignee: assignees[0] ?? null,
|
|
3668
|
+
blockedBy: summary?.blockedBy ?? [],
|
|
3669
|
+
blocks: summary?.blocks ?? [],
|
|
3670
|
+
blockingDepth: summary?.blockingDepth ?? 0,
|
|
3671
|
+
blockerClass: null,
|
|
3672
|
+
actionRiskTier: null,
|
|
3673
|
+
epicKey: groupKey,
|
|
3674
|
+
groupKey,
|
|
3675
|
+
externalId: task.externalId,
|
|
3676
|
+
sourceIssueId: task.sourceIssueId ?? null,
|
|
3677
|
+
scope: task.scope,
|
|
3678
|
+
validationKeys: task.validationKeys
|
|
3679
|
+
};
|
|
3680
|
+
}).toSorted((left, right) => left.taskId.localeCompare(right.taskId));
|
|
3681
|
+
return {
|
|
3682
|
+
graphId: options.graphId ?? deriveGraphId(tasks),
|
|
3683
|
+
nodes,
|
|
3684
|
+
edges: dedupedEdges,
|
|
3685
|
+
layout: buildTaskGraphLayout(options.snapshot ?? null, tasks, { showParentChild: options.showParentChild ?? true }),
|
|
3686
|
+
cycles: detectBlockingCycles(tasks, dedupedEdges),
|
|
3687
|
+
unresolvedRefs: uniqueSorted2(unresolvedRefs),
|
|
3688
|
+
degraded: false,
|
|
3689
|
+
generatedAt: options.generatedAt ?? new Date().toISOString()
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
// packages/core/src/rollups.ts
|
|
3693
|
+
import { isOperatorActiveRunStatus } from "@rig/contracts";
|
|
3694
|
+
var UNASSIGNED_EPIC = "(unassigned-epic)";
|
|
3695
|
+
var UNASSIGNED_ASSIGNEE = "(unassigned)";
|
|
3696
|
+
var HUMAN_BLOCKER_CLASSES = {
|
|
3697
|
+
"human-decision": true,
|
|
3698
|
+
"human-approval": true,
|
|
3699
|
+
"external-input": true,
|
|
3700
|
+
unknown: true
|
|
3701
|
+
};
|
|
3702
|
+
function isObjectRecord4(value) {
|
|
3703
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3704
|
+
}
|
|
3705
|
+
function readIssueType2(task) {
|
|
3706
|
+
const metadata = isObjectRecord4(task.metadata) ? task.metadata : null;
|
|
3707
|
+
const raw = isObjectRecord4(metadata?.raw) ? metadata.raw : null;
|
|
3708
|
+
const value = raw?.issueType ?? metadata?.issueType;
|
|
3709
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
|
|
3710
|
+
}
|
|
3711
|
+
function isEpicTask(task) {
|
|
3712
|
+
return readIssueType2(task) === "epic";
|
|
3713
|
+
}
|
|
3714
|
+
function isInFlightTaskStatus(status) {
|
|
3715
|
+
switch (status) {
|
|
3716
|
+
case "queued":
|
|
3717
|
+
case "running":
|
|
3718
|
+
case "in_progress":
|
|
3719
|
+
case "under_review":
|
|
3720
|
+
return true;
|
|
3721
|
+
default:
|
|
3722
|
+
return false;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
function epicKeyForTask(task, tasksById, resolve) {
|
|
3726
|
+
const parentRef = readTaskMetadataStringList(task, "parentChildDeps")[0];
|
|
3727
|
+
const parentId = parentRef ? resolve(parentRef) : null;
|
|
3728
|
+
const parent = parentId ? tasksById.get(parentId) : null;
|
|
3729
|
+
if (parent)
|
|
3730
|
+
return extractTaskGroupKey(parent.title) ?? parent.title ?? parent.id;
|
|
3731
|
+
return extractTaskGroupKey(task.title) ?? UNASSIGNED_EPIC;
|
|
3732
|
+
}
|
|
3733
|
+
function rollupByEpic(tasks, classifications = new Map) {
|
|
3734
|
+
const index = buildTaskReferenceIndex(tasks);
|
|
3735
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
3736
|
+
const resolve = (ref) => resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
|
|
3737
|
+
const buckets = new Map;
|
|
3738
|
+
for (const task of tasks) {
|
|
3739
|
+
if (isEpicTask(task))
|
|
3740
|
+
continue;
|
|
3741
|
+
const epicKey = epicKeyForTask(task, index.tasksById, resolve);
|
|
3742
|
+
const current = buckets.get(epicKey) ?? {
|
|
3743
|
+
total: 0,
|
|
3744
|
+
completed: 0,
|
|
3745
|
+
blockedCount: 0,
|
|
3746
|
+
humanBlockedCount: 0,
|
|
3747
|
+
inFlightCount: 0,
|
|
3748
|
+
byStatus: {}
|
|
3749
|
+
};
|
|
3750
|
+
current.total += 1;
|
|
3751
|
+
if (isTaskTerminalStatus(task.status))
|
|
3752
|
+
current.completed += 1;
|
|
3753
|
+
if (badges.get(task.id)?.blocked === true)
|
|
3754
|
+
current.blockedCount += 1;
|
|
3755
|
+
if (isInFlightTaskStatus(task.status))
|
|
3756
|
+
current.inFlightCount += 1;
|
|
3757
|
+
const classification = classifications.get(task.id);
|
|
3758
|
+
if (classification && HUMAN_BLOCKER_CLASSES[classification])
|
|
3759
|
+
current.humanBlockedCount += 1;
|
|
3760
|
+
current.byStatus[task.status] = (current.byStatus[task.status] ?? 0) + 1;
|
|
3761
|
+
buckets.set(epicKey, current);
|
|
3762
|
+
}
|
|
3763
|
+
return [...buckets.entries()].map(([epicKey, bucket]) => ({
|
|
3764
|
+
epicKey,
|
|
3765
|
+
total: bucket.total,
|
|
3766
|
+
percentComplete: bucket.total === 0 ? 0 : Math.round(100 * bucket.completed / bucket.total),
|
|
3767
|
+
blockedCount: bucket.blockedCount,
|
|
3768
|
+
humanBlockedCount: bucket.humanBlockedCount,
|
|
3769
|
+
inFlightCount: bucket.inFlightCount,
|
|
3770
|
+
byStatus: Object.fromEntries(Object.entries(bucket.byStatus).sort(([left], [right]) => left.localeCompare(right)))
|
|
3771
|
+
})).toSorted((left, right) => left.epicKey.localeCompare(right.epicKey));
|
|
3772
|
+
}
|
|
3773
|
+
function assigneesForTask(task) {
|
|
3774
|
+
const assignees = readTaskAssigneeLogins(task);
|
|
3775
|
+
return assignees.length > 0 ? assignees : [UNASSIGNED_ASSIGNEE];
|
|
3776
|
+
}
|
|
3777
|
+
function rollupByAssignee(tasks, runs) {
|
|
3778
|
+
const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
|
|
3779
|
+
const badges = computeTaskDependencyBadges(tasks);
|
|
3780
|
+
const buckets = new Map;
|
|
3781
|
+
const ensureBucket = (assignee) => {
|
|
3782
|
+
const existing = buckets.get(assignee);
|
|
3783
|
+
if (existing)
|
|
3784
|
+
return existing;
|
|
3785
|
+
const created = { openTaskCount: 0, inFlightRunIds: new Set, prsAwaitingReview: 0, blockers: new Set };
|
|
3786
|
+
buckets.set(assignee, created);
|
|
3787
|
+
return created;
|
|
3788
|
+
};
|
|
3789
|
+
for (const task of tasks) {
|
|
3790
|
+
if (isEpicTask(task))
|
|
3791
|
+
continue;
|
|
3792
|
+
for (const assignee of assigneesForTask(task)) {
|
|
3793
|
+
const bucket = ensureBucket(assignee);
|
|
3794
|
+
if (!isTaskTerminalStatus(task.status))
|
|
3795
|
+
bucket.openTaskCount += 1;
|
|
3796
|
+
if (badges.get(task.id)?.blocked === true)
|
|
3797
|
+
bucket.blockers.add(task.id);
|
|
3798
|
+
if (task.status === "under_review")
|
|
3799
|
+
bucket.prsAwaitingReview += 1;
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
for (const run of runs) {
|
|
3803
|
+
const taskId = run.record.taskId;
|
|
3804
|
+
const task = taskId ? tasksById.get(taskId) : null;
|
|
3805
|
+
if (!task)
|
|
3806
|
+
continue;
|
|
3807
|
+
if (isEpicTask(task))
|
|
3808
|
+
continue;
|
|
3809
|
+
for (const assignee of assigneesForTask(task)) {
|
|
3810
|
+
const bucket = ensureBucket(assignee);
|
|
3811
|
+
if (run.status && isOperatorActiveRunStatus(run.status))
|
|
3812
|
+
bucket.inFlightRunIds.add(run.record.runId ?? `${String(task.id)}:${run.lastSeq}`);
|
|
3813
|
+
if (run.status === "reviewing" && run.record.prUrl)
|
|
3814
|
+
bucket.prsAwaitingReview += 1;
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
return [...buckets.entries()].map(([assignee, bucket]) => ({
|
|
3818
|
+
assignee,
|
|
3819
|
+
openTaskCount: bucket.openTaskCount,
|
|
3820
|
+
inFlightRunCount: bucket.inFlightRunIds.size,
|
|
3821
|
+
prsAwaitingReview: bucket.prsAwaitingReview,
|
|
3822
|
+
blockers: [...bucket.blockers].toSorted((left, right) => String(left).localeCompare(String(right)))
|
|
3823
|
+
})).toSorted((left, right) => left.assignee.localeCompare(right.assignee));
|
|
3824
|
+
}
|
|
2572
3825
|
|
|
2573
3826
|
// packages/core/src/index.ts
|
|
2574
3827
|
var RIG_CORE_PACKAGE = "@rig/core";
|
|
@@ -2582,35 +3835,58 @@ export {
|
|
|
2582
3835
|
selectTasksForWorkspace,
|
|
2583
3836
|
selectTasksByWorkspace,
|
|
2584
3837
|
selectTasksByStatus,
|
|
3838
|
+
selectTasksAssignedToMe,
|
|
3839
|
+
selectTasksAssignedTo,
|
|
2585
3840
|
selectTask,
|
|
2586
3841
|
selectRunsForWorkspace,
|
|
2587
3842
|
selectRunsForTask,
|
|
2588
3843
|
selectRunsByTask,
|
|
2589
3844
|
selectRun,
|
|
3845
|
+
selectRankedReadyTasks,
|
|
2590
3846
|
selectQueueForWorkspace,
|
|
2591
3847
|
selectPrimaryWorkspace,
|
|
2592
3848
|
selectPendingApprovals,
|
|
3849
|
+
selectNextReadyTaskByPriority,
|
|
2593
3850
|
selectGraphsForWorkspace,
|
|
2594
3851
|
selectApprovalsForWorkspace,
|
|
2595
3852
|
selectApprovalsForRun,
|
|
2596
3853
|
selectAdhocRunsForWorkspace,
|
|
2597
3854
|
selectAdhocRuns,
|
|
3855
|
+
scoreTask,
|
|
3856
|
+
rollupByEpic,
|
|
3857
|
+
rollupByAssignee,
|
|
2598
3858
|
resolveTaskReference,
|
|
3859
|
+
resolveStagePipeline,
|
|
2599
3860
|
readTaskSourceIssueId,
|
|
3861
|
+
readTaskScope,
|
|
2600
3862
|
readTaskMetadataStringList,
|
|
3863
|
+
readTaskDependencyRefs,
|
|
3864
|
+
readTaskBlockingDependencyRefs,
|
|
3865
|
+
readTaskAssigneeLogins,
|
|
3866
|
+
rankTasks,
|
|
3867
|
+
rankReadyTasks,
|
|
2601
3868
|
pruneQueueEntries,
|
|
3869
|
+
projectTaskStatusWithSessions,
|
|
3870
|
+
projectTaskStatusForGrouping,
|
|
3871
|
+
projectRunStatusForTaskGrouping,
|
|
2602
3872
|
pickDefaultWorkspaceId,
|
|
3873
|
+
normalizeTaskAssigneeFilter,
|
|
3874
|
+
isTaskTerminalStatus,
|
|
2603
3875
|
extractTaskGroupKey,
|
|
2604
3876
|
extractTaskCode,
|
|
3877
|
+
disjointScope,
|
|
2605
3878
|
definePlugin,
|
|
2606
3879
|
defineConfig,
|
|
2607
3880
|
createPluginHost,
|
|
3881
|
+
computeTaskDependencyBadges,
|
|
2608
3882
|
computeTaskBlockingDepths,
|
|
2609
3883
|
buildTaskReferenceIndex,
|
|
2610
3884
|
buildTaskGraphLayout,
|
|
2611
3885
|
buildRigInitConfigSource,
|
|
3886
|
+
buildDependencyGraphModel,
|
|
2612
3887
|
applyEngineEvent as applyRigEvent,
|
|
2613
3888
|
applyEngineEvents,
|
|
2614
3889
|
applyEngineEvent,
|
|
2615
|
-
RIG_CORE_PACKAGE
|
|
3890
|
+
RIG_CORE_PACKAGE,
|
|
3891
|
+
PipelineUnresolvableError
|
|
2616
3892
|
};
|