@nyxa/nyx-agent 0.9.0 → 0.9.2
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/README.md +4 -3
- package/dist/runtime/runPipeline.js +22 -2
- package/dist/runtime/selectionConfirmation.js +62 -9
- package/dist/runtime/workItems.js +305 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,9 +21,10 @@ For every run NyxAgent:
|
|
|
21
21
|
The agent only implements, reviews, and revises. Every git/gh side effect —
|
|
22
22
|
commit, push, pull request — is performed by the engine, so closing the loop
|
|
23
23
|
never depends on the model. Issues are closed by GitHub when the PR merges
|
|
24
|
-
(`Closes #n` in the PR body); parent PRD/plan issues
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
(`Closes #n` in the PR body); parent PRD/plan issues referenced explicitly from
|
|
25
|
+
child descriptions are grouped for selection, while parent close lines are added
|
|
26
|
+
only when NyxAgent can prove the PR completes their remaining open executable
|
|
27
|
+
children. The human merges the PR.
|
|
27
28
|
|
|
28
29
|
The workflow shape is fixed (not configurable). Only `.nyxagent/prompts/execution.md`
|
|
29
30
|
is editable.
|
|
@@ -13,7 +13,7 @@ import { REVIEW_CHALLENGE_SCHEMA, REVIEW_DISCOVERY_SCHEMA, GLOBAL_REVIEW_SCHEMA,
|
|
|
13
13
|
import { commitAll, commitsAhead, createPullRequest, pushBranch, rangeDiff, stageAllAndDiff, } from "./scm.js";
|
|
14
14
|
import { buildSelectionSections, confirmWorkItemSelection, } from "./selectionConfirmation.js";
|
|
15
15
|
import { createRunId } from "./time.js";
|
|
16
|
-
import { filterAvailable, listGitHubWorkItemInventory, resolveSelectedQueue, } from "./workItems.js";
|
|
16
|
+
import { filterAvailable, listGitHubWorkItemInventory, orderWorkItemsByDependencies, resolveSelectedQueue, } from "./workItems.js";
|
|
17
17
|
const MAX_CANDIDATES = 50;
|
|
18
18
|
const EXCERPT_CHARS = 800;
|
|
19
19
|
const CORRECTION_VALIDATION_MAX_ATTEMPTS = 3;
|
|
@@ -96,7 +96,14 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
96
96
|
reporter.info("No work items selected. Nothing to do.");
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
|
-
const
|
|
99
|
+
const orderedSelection = orderWorkItemsByDependencies(selected);
|
|
100
|
+
if (orderedSelection.cycleKeys.length > 0) {
|
|
101
|
+
reporter.detail(`Dependency cycle detected among selected work items; preserving confirmed order inside cycle(s): ${formatDependencyCycles({
|
|
102
|
+
cycles: orderedSelection.cycleKeys,
|
|
103
|
+
items: selected,
|
|
104
|
+
})}`);
|
|
105
|
+
}
|
|
106
|
+
const planned = orderedSelection.queue.slice(0, config.max_iterations);
|
|
100
107
|
reporter.info(`Selected ${planned.length} work item(s):`);
|
|
101
108
|
for (const item of planned) {
|
|
102
109
|
reporter.info(` - ${item.title} (#${item.number})`);
|
|
@@ -874,6 +881,7 @@ function workItemSummary(item) {
|
|
|
874
881
|
url: item.url,
|
|
875
882
|
labels: item.labels,
|
|
876
883
|
parent: item.parent,
|
|
884
|
+
blocked_by: item.blocked_by,
|
|
877
885
|
};
|
|
878
886
|
}
|
|
879
887
|
function selectionCandidateSummary(candidate) {
|
|
@@ -883,6 +891,7 @@ function selectionCandidateSummary(candidate) {
|
|
|
883
891
|
title: candidate.title,
|
|
884
892
|
labels: candidate.labels,
|
|
885
893
|
parent: candidate.parent,
|
|
894
|
+
blocked_by: candidate.blocked_by,
|
|
886
895
|
excerpt: candidate.excerpt,
|
|
887
896
|
};
|
|
888
897
|
}
|
|
@@ -973,6 +982,17 @@ function completedParentClosures(input) {
|
|
|
973
982
|
function uniqueNumbers(numbers) {
|
|
974
983
|
return [...new Set(numbers)];
|
|
975
984
|
}
|
|
985
|
+
function formatDependencyCycles(input) {
|
|
986
|
+
const itemByKey = new Map(input.items.map((item) => [item.key, item]));
|
|
987
|
+
return input.cycles
|
|
988
|
+
.map((cycle) => cycle
|
|
989
|
+
.map((key) => {
|
|
990
|
+
const item = itemByKey.get(key);
|
|
991
|
+
return item ? `#${item.number}` : key;
|
|
992
|
+
})
|
|
993
|
+
.join(" -> "))
|
|
994
|
+
.join("; ");
|
|
995
|
+
}
|
|
976
996
|
/** Render unresolved blockers as a bullet list to append to a failure message. */
|
|
977
997
|
function formatBlockers(blockers) {
|
|
978
998
|
if (blockers.length === 0) {
|
|
@@ -31,15 +31,49 @@ export async function confirmWorkItemSelection(input) {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
export function buildSelectionChoiceItems(input) {
|
|
34
|
-
const
|
|
34
|
+
const candidateByKey = new Map(input.candidates.map((candidate) => [candidate.key, candidate]));
|
|
35
|
+
const proposedCandidates = [];
|
|
36
|
+
const proposedKeys = new Set();
|
|
37
|
+
for (const proposed of input.proposed) {
|
|
38
|
+
const candidate = candidateByKey.get(proposed.key);
|
|
39
|
+
if (!candidate || proposedKeys.has(candidate.key)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
proposedCandidates.push(candidate);
|
|
43
|
+
proposedKeys.add(candidate.key);
|
|
44
|
+
}
|
|
45
|
+
const remainingCandidates = input.candidates.filter((candidate) => !proposedKeys.has(candidate.key));
|
|
35
46
|
const items = [];
|
|
36
|
-
|
|
47
|
+
let activeSectionLabel;
|
|
48
|
+
activeSectionLabel = appendSelectionChoices({
|
|
49
|
+
items,
|
|
50
|
+
sections: buildSelectionSections(proposedCandidates),
|
|
51
|
+
proposedKeys,
|
|
52
|
+
activeSectionLabel,
|
|
53
|
+
});
|
|
54
|
+
appendSelectionChoices({
|
|
55
|
+
items,
|
|
56
|
+
sections: buildSelectionSections(remainingCandidates),
|
|
57
|
+
proposedKeys,
|
|
58
|
+
activeSectionLabel,
|
|
59
|
+
});
|
|
60
|
+
return items;
|
|
61
|
+
}
|
|
62
|
+
function appendSelectionChoices(input) {
|
|
63
|
+
let activeSectionLabel = input.activeSectionLabel;
|
|
64
|
+
for (const section of input.sections) {
|
|
37
65
|
if (section.label) {
|
|
38
|
-
|
|
66
|
+
if (activeSectionLabel !== section.label) {
|
|
67
|
+
input.items.push({ type: "separator", label: section.label });
|
|
68
|
+
}
|
|
69
|
+
activeSectionLabel = section.label;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
activeSectionLabel = undefined;
|
|
39
73
|
}
|
|
40
74
|
for (const candidate of section.candidates) {
|
|
41
|
-
const proposed = proposedKeys.has(candidate.key);
|
|
42
|
-
items.push({
|
|
75
|
+
const proposed = input.proposedKeys.has(candidate.key);
|
|
76
|
+
input.items.push({
|
|
43
77
|
type: "choice",
|
|
44
78
|
value: candidate.key,
|
|
45
79
|
name: `#${candidate.number} ${candidate.title}${proposed ? " (agent)" : ""}`,
|
|
@@ -47,7 +81,7 @@ export function buildSelectionChoiceItems(input) {
|
|
|
47
81
|
});
|
|
48
82
|
}
|
|
49
83
|
}
|
|
50
|
-
return
|
|
84
|
+
return activeSectionLabel;
|
|
51
85
|
}
|
|
52
86
|
export function buildSelectionSections(candidates) {
|
|
53
87
|
const sections = [];
|
|
@@ -105,9 +139,14 @@ function formatParentGroup(parent) {
|
|
|
105
139
|
if (!parent) {
|
|
106
140
|
return "Parent";
|
|
107
141
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
if (!parent.title) {
|
|
143
|
+
return `Parent #${parent.number}`;
|
|
144
|
+
}
|
|
145
|
+
const titledGroup = parseLeadingTitleGroup(parent.title);
|
|
146
|
+
if (titledGroup) {
|
|
147
|
+
return `${titledGroup.kind} #${parent.number}: ${titledGroup.name}`;
|
|
148
|
+
}
|
|
149
|
+
return `Parent #${parent.number}: ${parent.title}`;
|
|
111
150
|
}
|
|
112
151
|
function parseGroupLabel(label) {
|
|
113
152
|
const match = /^(plan|prd)\s*[:/=-]\s*(.+)$/i.exec(label.trim());
|
|
@@ -123,6 +162,20 @@ function parseBracketedTitleGroup(title) {
|
|
|
123
162
|
}
|
|
124
163
|
return formatGroupLabel(match[1], match[2]);
|
|
125
164
|
}
|
|
165
|
+
function parseLeadingTitleGroup(title) {
|
|
166
|
+
const match = /^(plan|prd)\s*[:/=-]\s*(.+)$/i.exec(title.trim());
|
|
167
|
+
if (!match) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const name = match[2].trim();
|
|
171
|
+
if (!name) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
kind: match[1].toLowerCase() === "prd" ? "PRD" : "Plan",
|
|
176
|
+
name,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
126
179
|
function formatGroupLabel(kind, rawName) {
|
|
127
180
|
const name = rawName.trim();
|
|
128
181
|
if (!name) {
|
|
@@ -118,11 +118,13 @@ async function listOpenGitHubIssueRecords(input) {
|
|
|
118
118
|
if (!Array.isArray(issues)) {
|
|
119
119
|
throw new Error("gh issue list returned JSON that is not an array");
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
const records = issues.slice(0, input.maxCandidates).map((issue) => normalizeIssueRecord({
|
|
122
122
|
issue,
|
|
123
123
|
repo: input.repo,
|
|
124
124
|
excerptChars: input.excerptChars,
|
|
125
125
|
}));
|
|
126
|
+
attachBlockedByDependencies({ records, repo: input.repo });
|
|
127
|
+
return records;
|
|
126
128
|
}
|
|
127
129
|
export function filterAvailable(input) {
|
|
128
130
|
const completed = new Set(input.completedKeys);
|
|
@@ -155,6 +157,79 @@ export function resolveSelectedQueue(input) {
|
|
|
155
157
|
}
|
|
156
158
|
return { ok: true, queue };
|
|
157
159
|
}
|
|
160
|
+
export function orderWorkItemsByDependencies(queue) {
|
|
161
|
+
const selectedKeys = new Set(queue.map((item) => item.key));
|
|
162
|
+
const itemByKey = new Map(queue.map((item) => [item.key, item]));
|
|
163
|
+
const originalIndex = new Map(queue.map((item, index) => [item.key, index]));
|
|
164
|
+
const dependenciesByKey = new Map();
|
|
165
|
+
for (const item of queue) {
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
const dependencies = (item.blocked_by ?? [])
|
|
168
|
+
.map((dependency) => dependency.key)
|
|
169
|
+
.filter((key) => {
|
|
170
|
+
if (!selectedKeys.has(key) || seen.has(key)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
seen.add(key);
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
dependenciesByKey.set(item.key, dependencies);
|
|
177
|
+
}
|
|
178
|
+
const components = findDependencyComponents({
|
|
179
|
+
keys: queue.map((item) => item.key),
|
|
180
|
+
dependenciesByKey,
|
|
181
|
+
});
|
|
182
|
+
const componentByKey = new Map();
|
|
183
|
+
for (const [index, component] of components.entries()) {
|
|
184
|
+
for (const key of component) {
|
|
185
|
+
componentByKey.set(key, index);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const componentDependsOn = new Map();
|
|
189
|
+
const dependentsByComponent = new Map();
|
|
190
|
+
for (const [key, dependencies] of dependenciesByKey) {
|
|
191
|
+
const component = componentByKey.get(key);
|
|
192
|
+
if (component === undefined) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
for (const dependencyKey of dependencies) {
|
|
196
|
+
const dependencyComponent = componentByKey.get(dependencyKey);
|
|
197
|
+
if (dependencyComponent === undefined ||
|
|
198
|
+
dependencyComponent === component) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
let dependsOn = componentDependsOn.get(component);
|
|
202
|
+
if (!dependsOn) {
|
|
203
|
+
dependsOn = new Set();
|
|
204
|
+
componentDependsOn.set(component, dependsOn);
|
|
205
|
+
}
|
|
206
|
+
dependsOn.add(dependencyComponent);
|
|
207
|
+
let dependents = dependentsByComponent.get(dependencyComponent);
|
|
208
|
+
if (!dependents) {
|
|
209
|
+
dependents = new Set();
|
|
210
|
+
dependentsByComponent.set(dependencyComponent, dependents);
|
|
211
|
+
}
|
|
212
|
+
dependents.add(component);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const orderedComponentIndexes = orderDependencyComponents({
|
|
216
|
+
components,
|
|
217
|
+
componentDependsOn,
|
|
218
|
+
dependentsByComponent,
|
|
219
|
+
itemByKey,
|
|
220
|
+
originalIndex,
|
|
221
|
+
});
|
|
222
|
+
const ordered = orderedComponentIndexes.flatMap((componentIndex) => sortKeysByOriginalOrder(components[componentIndex], originalIndex).flatMap((key) => {
|
|
223
|
+
const item = itemByKey.get(key);
|
|
224
|
+
return item ? [item] : [];
|
|
225
|
+
}));
|
|
226
|
+
return {
|
|
227
|
+
queue: ordered,
|
|
228
|
+
cycleKeys: components
|
|
229
|
+
.filter((component) => isCyclicDependencyComponent(component, dependenciesByKey))
|
|
230
|
+
.map((component) => sortKeysByOriginalOrder(component, originalIndex)),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
158
233
|
function normalizeIssueRecord(input) {
|
|
159
234
|
if (!isRecord(input.issue)) {
|
|
160
235
|
throw new Error("gh issue list returned a non-object issue");
|
|
@@ -216,6 +291,31 @@ function normalizeIssueRecord(input) {
|
|
|
216
291
|
details,
|
|
217
292
|
};
|
|
218
293
|
}
|
|
294
|
+
function attachBlockedByDependencies(input) {
|
|
295
|
+
const detailsByKey = new Map(input.records.map((record) => [record.candidate.key, record.details]));
|
|
296
|
+
for (const record of input.records) {
|
|
297
|
+
const dependencies = parseBlockedByDependencies({
|
|
298
|
+
body: record.body,
|
|
299
|
+
repo: input.repo,
|
|
300
|
+
}).map((dependency) => {
|
|
301
|
+
const details = detailsByKey.get(dependency.key);
|
|
302
|
+
if (!details) {
|
|
303
|
+
return dependency;
|
|
304
|
+
}
|
|
305
|
+
const enriched = { ...dependency };
|
|
306
|
+
if (details.title) {
|
|
307
|
+
enriched.title = details.title;
|
|
308
|
+
}
|
|
309
|
+
if (details.state) {
|
|
310
|
+
enriched.state = details.state;
|
|
311
|
+
}
|
|
312
|
+
return enriched;
|
|
313
|
+
});
|
|
314
|
+
if (dependencies.length > 0) {
|
|
315
|
+
record.candidate.blocked_by = dependencies;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
219
319
|
function buildExcerpt(content, maxChars) {
|
|
220
320
|
if (maxChars <= 0) {
|
|
221
321
|
return undefined;
|
|
@@ -376,13 +476,216 @@ function parseExplicitParentReferenceLine(line) {
|
|
|
376
476
|
.replace(/^[-*]\s+/, "")
|
|
377
477
|
.replace(/^>\s+/, "");
|
|
378
478
|
const issueRef = "(?:#|[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#|https://github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/issues/)(\\d+)";
|
|
379
|
-
const match = new RegExp(`^(?:parent(?:\\s+issue)?|prd|plan)\\s*(?::|=|-)?\\s*${issueRef}\\s*$`, "i").exec(trimmed);
|
|
479
|
+
const match = new RegExp(`^(?:parent(?:\\s+(?:issue|prd|plan))?|prd|plan)\\s*(?::|=|-)?\\s*${issueRef}\\s*$`, "i").exec(trimmed);
|
|
380
480
|
if (!match) {
|
|
381
481
|
return undefined;
|
|
382
482
|
}
|
|
383
483
|
const number = Number.parseInt(match[1], 10);
|
|
384
484
|
return Number.isInteger(number) && number > 0 ? number : undefined;
|
|
385
485
|
}
|
|
486
|
+
function parseBlockedByDependencies(input) {
|
|
487
|
+
if (!input.body) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
const dependencies = [];
|
|
491
|
+
let blockedBySectionLevel;
|
|
492
|
+
for (const line of input.body.split(/\r?\n/)) {
|
|
493
|
+
const heading = parseMarkdownHeading(line);
|
|
494
|
+
if (heading) {
|
|
495
|
+
if (blockedBySectionLevel !== undefined &&
|
|
496
|
+
heading.level <= blockedBySectionLevel) {
|
|
497
|
+
blockedBySectionLevel = undefined;
|
|
498
|
+
}
|
|
499
|
+
if (isBlockedByHeading(heading.title)) {
|
|
500
|
+
blockedBySectionLevel = heading.level;
|
|
501
|
+
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
dependencies.push(...parseDirectBlockedByLine({
|
|
505
|
+
line,
|
|
506
|
+
repo: input.repo,
|
|
507
|
+
}));
|
|
508
|
+
if (blockedBySectionLevel !== undefined) {
|
|
509
|
+
dependencies.push(...parseBlockedBySectionBullet({
|
|
510
|
+
line,
|
|
511
|
+
repo: input.repo,
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return dedupeDependencies(dependencies);
|
|
516
|
+
}
|
|
517
|
+
function parseDirectBlockedByLine(input) {
|
|
518
|
+
const line = normalizeMarkdownLinePrefix(input.line);
|
|
519
|
+
const match = /^blocked\s+by\s*:\s*(.+)$/i.exec(line);
|
|
520
|
+
return match
|
|
521
|
+
? parseIssueReferences({ text: match[1], repo: input.repo })
|
|
522
|
+
: [];
|
|
523
|
+
}
|
|
524
|
+
function parseBlockedBySectionBullet(input) {
|
|
525
|
+
const line = normalizeMarkdownLinePrefix(input.line);
|
|
526
|
+
const match = /^[-*+]\s+(?:\[[ xX]\]\s+)?(.+)$/.exec(line);
|
|
527
|
+
return match
|
|
528
|
+
? parseIssueReferences({ text: match[1], repo: input.repo })
|
|
529
|
+
: [];
|
|
530
|
+
}
|
|
531
|
+
function parseIssueReferences(input) {
|
|
532
|
+
const dependencies = [];
|
|
533
|
+
const issueRefPattern = /#(\d+)|([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#(\d+)|https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/issues\/(\d+)/gi;
|
|
534
|
+
for (const match of input.text.matchAll(issueRefPattern)) {
|
|
535
|
+
const repo = match[2] ??
|
|
536
|
+
(match[4] && match[5] ? `${match[4]}/${match[5]}` : input.repo);
|
|
537
|
+
const rawNumber = match[1] ?? match[3] ?? match[6];
|
|
538
|
+
const number = Number.parseInt(rawNumber, 10);
|
|
539
|
+
if (!repo || !Number.isInteger(number) || number <= 0) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const locator = `${repo}#${number}`;
|
|
543
|
+
dependencies.push({
|
|
544
|
+
key: `github:${locator}`,
|
|
545
|
+
number,
|
|
546
|
+
source: { type: "github", locator },
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return dedupeDependencies(dependencies);
|
|
550
|
+
}
|
|
551
|
+
function parseMarkdownHeading(line) {
|
|
552
|
+
const match = /^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
553
|
+
if (!match) {
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
level: match[1].length,
|
|
558
|
+
title: match[2].trim().replace(/[::]\s*$/, ""),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function isBlockedByHeading(title) {
|
|
562
|
+
return /^blocked\s+by$/i.test(title.trim());
|
|
563
|
+
}
|
|
564
|
+
function normalizeMarkdownLinePrefix(line) {
|
|
565
|
+
return line.trim().replace(/^>\s+/, "").trim();
|
|
566
|
+
}
|
|
567
|
+
function dedupeDependencies(dependencies) {
|
|
568
|
+
const byKey = new Map();
|
|
569
|
+
for (const dependency of dependencies) {
|
|
570
|
+
byKey.set(dependency.key, dependency);
|
|
571
|
+
}
|
|
572
|
+
return [...byKey.values()];
|
|
573
|
+
}
|
|
574
|
+
function findDependencyComponents(input) {
|
|
575
|
+
const components = [];
|
|
576
|
+
const indexByKey = new Map();
|
|
577
|
+
const lowLinkByKey = new Map();
|
|
578
|
+
const stack = [];
|
|
579
|
+
const onStack = new Set();
|
|
580
|
+
let nextIndex = 0;
|
|
581
|
+
const visit = (key) => {
|
|
582
|
+
indexByKey.set(key, nextIndex);
|
|
583
|
+
lowLinkByKey.set(key, nextIndex);
|
|
584
|
+
nextIndex += 1;
|
|
585
|
+
stack.push(key);
|
|
586
|
+
onStack.add(key);
|
|
587
|
+
for (const dependencyKey of input.dependenciesByKey.get(key) ?? []) {
|
|
588
|
+
if (!indexByKey.has(dependencyKey)) {
|
|
589
|
+
visit(dependencyKey);
|
|
590
|
+
lowLinkByKey.set(key, Math.min(lowLinkByKey.get(key) ?? 0, lowLinkByKey.get(dependencyKey) ?? 0));
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (onStack.has(dependencyKey)) {
|
|
594
|
+
lowLinkByKey.set(key, Math.min(lowLinkByKey.get(key) ?? 0, indexByKey.get(dependencyKey) ?? 0));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (lowLinkByKey.get(key) !== indexByKey.get(key)) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const component = [];
|
|
601
|
+
let member;
|
|
602
|
+
do {
|
|
603
|
+
member = stack.pop();
|
|
604
|
+
if (!member) {
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
onStack.delete(member);
|
|
608
|
+
component.push(member);
|
|
609
|
+
} while (member !== key);
|
|
610
|
+
components.push(component);
|
|
611
|
+
};
|
|
612
|
+
for (const key of input.keys) {
|
|
613
|
+
if (!indexByKey.has(key)) {
|
|
614
|
+
visit(key);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return components;
|
|
618
|
+
}
|
|
619
|
+
function orderDependencyComponents(input) {
|
|
620
|
+
const indegree = input.components.map((_, index) => input.componentDependsOn.get(index)?.size ?? 0);
|
|
621
|
+
const available = input.components
|
|
622
|
+
.map((_, index) => index)
|
|
623
|
+
.filter((index) => indegree[index] === 0);
|
|
624
|
+
const ordered = [];
|
|
625
|
+
while (available.length > 0) {
|
|
626
|
+
available.sort((a, b) => compareDependencyComponents({
|
|
627
|
+
a,
|
|
628
|
+
b,
|
|
629
|
+
components: input.components,
|
|
630
|
+
itemByKey: input.itemByKey,
|
|
631
|
+
originalIndex: input.originalIndex,
|
|
632
|
+
}));
|
|
633
|
+
const component = available.shift();
|
|
634
|
+
if (component === undefined) {
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
ordered.push(component);
|
|
638
|
+
for (const dependent of input.dependentsByComponent.get(component) ?? []) {
|
|
639
|
+
indegree[dependent] -= 1;
|
|
640
|
+
if (indegree[dependent] === 0) {
|
|
641
|
+
available.push(dependent);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (ordered.length === input.components.length) {
|
|
646
|
+
return ordered;
|
|
647
|
+
}
|
|
648
|
+
const orderedSet = new Set(ordered);
|
|
649
|
+
return [
|
|
650
|
+
...ordered,
|
|
651
|
+
...input.components
|
|
652
|
+
.map((_, index) => index)
|
|
653
|
+
.filter((index) => !orderedSet.has(index))
|
|
654
|
+
.sort((a, b) => compareDependencyComponents({
|
|
655
|
+
a,
|
|
656
|
+
b,
|
|
657
|
+
components: input.components,
|
|
658
|
+
itemByKey: input.itemByKey,
|
|
659
|
+
originalIndex: input.originalIndex,
|
|
660
|
+
})),
|
|
661
|
+
];
|
|
662
|
+
}
|
|
663
|
+
function compareDependencyComponents(input) {
|
|
664
|
+
const aIndex = minOriginalIndex(input.components[input.a], input.originalIndex);
|
|
665
|
+
const bIndex = minOriginalIndex(input.components[input.b], input.originalIndex);
|
|
666
|
+
if (aIndex !== bIndex) {
|
|
667
|
+
return aIndex - bIndex;
|
|
668
|
+
}
|
|
669
|
+
return (minIssueNumber(input.components[input.a], input.itemByKey) -
|
|
670
|
+
minIssueNumber(input.components[input.b], input.itemByKey));
|
|
671
|
+
}
|
|
672
|
+
function minOriginalIndex(keys, originalIndex) {
|
|
673
|
+
return Math.min(...keys.map((key) => originalIndex.get(key) ?? Number.MAX_SAFE_INTEGER));
|
|
674
|
+
}
|
|
675
|
+
function minIssueNumber(keys, itemByKey) {
|
|
676
|
+
return Math.min(...keys.map((key) => itemByKey.get(key)?.number ?? Number.MAX_SAFE_INTEGER));
|
|
677
|
+
}
|
|
678
|
+
function sortKeysByOriginalOrder(keys, originalIndex) {
|
|
679
|
+
return [...keys].sort((a, b) => (originalIndex.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
680
|
+
(originalIndex.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
681
|
+
}
|
|
682
|
+
function isCyclicDependencyComponent(component, dependenciesByKey) {
|
|
683
|
+
if (component.length > 1) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
const key = component[0];
|
|
687
|
+
return (dependenciesByKey.get(key) ?? []).includes(key);
|
|
688
|
+
}
|
|
386
689
|
function parseGitHubRepo(repo) {
|
|
387
690
|
const [owner, repoName, ...rest] = repo.split("/");
|
|
388
691
|
if (!owner || !repoName || rest.length > 0) {
|