@vzn/vx 0.0.613 → 0.0.614
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/package.json +5 -5
- package/src/cli/help.ts +2 -1
- package/src/cli/select.ts +29 -17
- package/src/cli/show.ts +35 -3
- package/src/orchestrator/affected-tasks.ts +65 -0
- package/src/orchestrator/graph-stage.ts +48 -0
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/prepare.ts +15 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.614",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.614",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.614",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.614",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.614"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/help.ts
CHANGED
|
@@ -210,7 +210,8 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
210
210
|
" vx show <pkg>#<task> Print a single task's resolved config.",
|
|
211
211
|
' vx show <task> That task in every project declaring it.',
|
|
212
212
|
" --filter <pattern> The list (or a task's projects) as `vx run --filter` selects.",
|
|
213
|
-
|
|
213
|
+
" --affected[=<ref>] A task's projects `vx run <task> --affected` runs; no task:",
|
|
214
|
+
' the changed projects and their dependents.',
|
|
214
215
|
' --format <fmt> pretty (default) | json.',
|
|
215
216
|
' vx info Workspace doctor: vx/bun/git versions, project +',
|
|
216
217
|
' task counts, cache dir/entries/size, recent runs,',
|
package/src/cli/select.ts
CHANGED
|
@@ -360,6 +360,10 @@ export async function resolveFilters(
|
|
|
360
360
|
|
|
361
361
|
const unmatched: string[] = []
|
|
362
362
|
const emptyWalks: string[] = []
|
|
363
|
+
// The `[<since>]` includes whose diff itself matched nothing: only then is
|
|
364
|
+
// "nothing affected" the fact. A diff that matched, then lost every
|
|
365
|
+
// project to an exclusion or an empty walk, says which (X-146).
|
|
366
|
+
const emptyDiffs = new Set<(typeof parsed)[number]>()
|
|
363
367
|
const selected = applyFilters({
|
|
364
368
|
filters: parsed,
|
|
365
369
|
projects,
|
|
@@ -371,11 +375,12 @@ export async function resolveFilters(
|
|
|
371
375
|
// matched nothing is worth flagging as a probable typo.
|
|
372
376
|
onNoMatch: (f) => {
|
|
373
377
|
if (f.gitSince === undefined) unmatched.push(f.raw)
|
|
378
|
+
else emptyDiffs.add(f)
|
|
374
379
|
},
|
|
375
380
|
// A pattern that matched, with nothing on the walk it asked for: the
|
|
376
381
|
// fact to say, where "no projects matched" read as a typo (item 1030).
|
|
377
382
|
onEmptyWalk: (f, matched) => {
|
|
378
|
-
if (f.negate
|
|
383
|
+
if (f.negate) return
|
|
379
384
|
const which = matched.join(', ')
|
|
380
385
|
emptyWalks.push(
|
|
381
386
|
f.onlyDeps
|
|
@@ -389,28 +394,35 @@ export async function resolveFilters(
|
|
|
389
394
|
// commit must not red `vx run … --affected=origin/main`. Only report an
|
|
390
395
|
// error when the user named something concrete that failed to resolve.
|
|
391
396
|
const includes = parsed.filter((f) => !f.negate)
|
|
397
|
+
// Every pattern matched and an exclusion took them all back: "no
|
|
398
|
+
// projects matched filter(s): one, !one" read as a typo of `one`.
|
|
399
|
+
const negations = parsed.filter((f) => f.negate).map((f) => f.raw)
|
|
400
|
+
const excluded = `${negations.join(', ')} excluded every project the other filters matched`
|
|
392
401
|
if (includes.length > 0 && includes.every((f) => f.gitSince !== undefined)) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
402
|
+
if (includes.every((f) => emptyDiffs.has(f))) {
|
|
403
|
+
const refs = includes.map((f) => f.gitSince).join(', ')
|
|
404
|
+
// A base that IS HEAD (a single-branch clone whose `origin/HEAD` is
|
|
405
|
+
// the branch under test) can never mark anything affected, and this
|
|
406
|
+
// exit-0 note was the only sign (CI persona, 2026-09-16). Say so.
|
|
407
|
+
const self = includes.find((f) => refIsHead(root, f.gitSince!))
|
|
408
|
+
const hint =
|
|
409
|
+
self === undefined
|
|
410
|
+
? ''
|
|
411
|
+
: ` — ${self.gitSince} is HEAD itself: compare with the branch you merge into (--affected=origin/main) or the previous commit (--affected=HEAD~1)`
|
|
412
|
+
return { empty: `nothing affected since ${refs}${hint}` }
|
|
413
|
+
}
|
|
414
|
+
// The diff matched and a walk or an exclusion left nothing: still the
|
|
415
|
+
// exit-0 outcome (`--affected --filter '!docs'` on a docs-only change
|
|
416
|
+
// must not red CI), said as what happened (X-146).
|
|
417
|
+
return {
|
|
418
|
+
empty: `no projects selected: ${emptyWalks.length > 0 ? emptyWalks.join('; ') : excluded}`,
|
|
419
|
+
}
|
|
403
420
|
}
|
|
404
421
|
if (unmatched.length === 0 && emptyWalks.length > 0) {
|
|
405
422
|
return { error: `no projects selected: ${emptyWalks.join('; ')}` }
|
|
406
423
|
}
|
|
407
|
-
// Every pattern matched and an exclusion took them all back: "no
|
|
408
|
-
// projects matched filter(s): one, !one" read as a typo of `one`.
|
|
409
|
-
const negations = parsed.filter((f) => f.negate).map((f) => f.raw)
|
|
410
424
|
if (unmatched.length === 0 && negations.length > 0) {
|
|
411
|
-
return {
|
|
412
|
-
error: `no projects selected: ${negations.join(', ')} excluded every project the other filters matched`,
|
|
413
|
-
}
|
|
425
|
+
return { error: `no projects selected: ${excluded}` }
|
|
414
426
|
}
|
|
415
427
|
// One line, not a warning per pattern and then an error saying the same:
|
|
416
428
|
// the patterns are in the error, and the nearest project name is the
|
package/src/cli/show.ts
CHANGED
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import type { ProjectConfig, TaskConfig } from '../config.js'
|
|
11
11
|
import { declaredTask } from '../graph/index.js'
|
|
12
|
-
import { isDefaultBuild } from '../orchestrator/index.js'
|
|
12
|
+
import { affectedTaskProjects, isDefaultBuild } from '../orchestrator/index.js'
|
|
13
13
|
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
14
14
|
import { listed, nearMatches, relPosix, secretMask, UserError } from '../util/index.js'
|
|
15
15
|
import { affectedFilterFor, resolveFilters } from './select.js'
|
|
16
|
-
import { discoverCliProjects, loadCliProjects } from './workspace-config.js'
|
|
17
16
|
import {
|
|
17
|
+
discoverCliProjects,
|
|
18
|
+
loadCliProjects,
|
|
19
|
+
loadCliWorkspace,
|
|
20
|
+
warnToStderr,
|
|
21
|
+
} from './workspace-config.js'
|
|
22
|
+
import {
|
|
23
|
+
type AffectedChanges,
|
|
24
|
+
buildPackageGraph,
|
|
18
25
|
findWorkspaceRoot,
|
|
19
26
|
type LoadReads,
|
|
20
27
|
loadWorkspace,
|
|
@@ -124,6 +131,9 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
124
131
|
// `turbo ls --filter` / `nx show projects --affected`: the list, and a
|
|
125
132
|
// task's projects, narrowed by the selection `vx run` would make.
|
|
126
133
|
let selected: Set<string> | undefined
|
|
134
|
+
// A task's projects under `--affected` are the ones whose task the run
|
|
135
|
+
// keeps: the change reaches another project only along a task edge.
|
|
136
|
+
let byDiff: { changes: AffectedChanges; outright?: readonly string[] | undefined } | undefined
|
|
127
137
|
if (parsed.filters.length > 0 || parsed.affected !== undefined) {
|
|
128
138
|
if (scope !== 'all') {
|
|
129
139
|
throw new UserError(
|
|
@@ -142,6 +152,8 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
142
152
|
if ('error' in r) throw new UserError(`vx show: ${r.error}`)
|
|
143
153
|
if ('empty' in r) process.stderr.write(`vx show: ${r.empty}\n`)
|
|
144
154
|
selected = new Set('empty' in r ? [] : r.names)
|
|
155
|
+
if ('names' in r && r.affected !== undefined)
|
|
156
|
+
byDiff = { changes: r.affected, outright: r.outright }
|
|
145
157
|
}
|
|
146
158
|
const inSelection = (name: string): boolean => selected === undefined || selected.has(name)
|
|
147
159
|
|
|
@@ -171,7 +183,27 @@ export async function showCmd(args: readonly string[]): Promise<number> {
|
|
|
171
183
|
`vx show: unknown project or task: "${projectName}"${nx}${suggest(projectName!, [...byName.keys(), ...names], '', 'projects and tasks')}`,
|
|
172
184
|
)
|
|
173
185
|
}
|
|
174
|
-
|
|
186
|
+
let shown = declaring.filter((p) => inSelection(p.name))
|
|
187
|
+
if (byDiff !== undefined) {
|
|
188
|
+
const ws = await loadCliWorkspace(root)
|
|
189
|
+
const kept = new Set(
|
|
190
|
+
await affectedTaskProjects({
|
|
191
|
+
task: projectName!,
|
|
192
|
+
candidates: shown.map((p) => p.name),
|
|
193
|
+
changes: byDiff.changes,
|
|
194
|
+
outright: byDiff.outright,
|
|
195
|
+
projects,
|
|
196
|
+
packageGraph: buildPackageGraph(metas),
|
|
197
|
+
projectMetas: metas,
|
|
198
|
+
plugins: ws.plugins,
|
|
199
|
+
workspaceRoot: root,
|
|
200
|
+
cacheDir: ws.cacheDir,
|
|
201
|
+
rules: ws.workspaceConfig?.rules,
|
|
202
|
+
warn: warnToStderr,
|
|
203
|
+
}),
|
|
204
|
+
)
|
|
205
|
+
shown = shown.filter((p) => kept.has(p.name))
|
|
206
|
+
}
|
|
175
207
|
process.stdout.write(renderTaskAcross(root, shown, projectName!, parsed.format))
|
|
176
208
|
return 0
|
|
177
209
|
}
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
// behind `^build` runs when `ui#build` (or what it depends on) is reached,
|
|
5
5
|
// and a spec edit `ui#build`'s inputs leave out stops at `ui`'s own tasks.
|
|
6
6
|
|
|
7
|
+
import type { WorkspaceRules } from '../config.js'
|
|
7
8
|
import { declaresInput, workspaceFilesReachInto } from '../cache/index.js'
|
|
8
9
|
import {
|
|
10
|
+
buildTaskGraph,
|
|
9
11
|
compileTaskPattern,
|
|
12
|
+
expandRequested,
|
|
10
13
|
isGroupTask,
|
|
11
14
|
isTaskPattern,
|
|
12
15
|
parseDependencySpec,
|
|
@@ -17,7 +20,12 @@ import {
|
|
|
17
20
|
type PackageGraph,
|
|
18
21
|
PROJECT_CONFIG_FILENAMES,
|
|
19
22
|
type ProjectEntry,
|
|
23
|
+
type ProjectMeta,
|
|
20
24
|
} from '../workspace/index.js'
|
|
25
|
+
import { applyGraphStage } from './graph-stage.js'
|
|
26
|
+
import { hasHook } from './plugin-host.js'
|
|
27
|
+
import type { VxPlugin } from './plugin.js'
|
|
28
|
+
import { isDefaultBuild } from './projects.js'
|
|
21
29
|
|
|
22
30
|
/** Every file in this set re-keys each of its project's tasks, whatever their inputs. */
|
|
23
31
|
const KEYED_BY_EVERY_TASK = new Set(['package.json', ...PROJECT_CONFIG_FILENAMES])
|
|
@@ -146,3 +154,60 @@ export function affectedRoots(
|
|
|
146
154
|
}
|
|
147
155
|
return ids.filter(reaches)
|
|
148
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The requested tasks `--affected` keeps, in their order: one the user
|
|
160
|
+
* named (`pkg#task`), one in a project another include selected outright
|
|
161
|
+
* (X-10), and one whose closure the change reaches.
|
|
162
|
+
*/
|
|
163
|
+
export function keptByAffected<R extends { project: string; task: string }>(
|
|
164
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
165
|
+
requested: readonly R[],
|
|
166
|
+
changes: AffectedChanges,
|
|
167
|
+
projects: ReadonlyMap<string, ProjectEntry>,
|
|
168
|
+
packageGraph: PackageGraph,
|
|
169
|
+
keep: { named?: ReadonlySet<string>; outright?: ReadonlySet<string> } = {},
|
|
170
|
+
): R[] {
|
|
171
|
+
const ids = requested.map((r) => `${r.project}#${r.task}`)
|
|
172
|
+
const reached = new Set(affectedRoots(nodes, ids, changes, projects, packageGraph))
|
|
173
|
+
return requested.filter(
|
|
174
|
+
(r, i) =>
|
|
175
|
+
keep.named?.has(ids[i]!) === true ||
|
|
176
|
+
keep.outright?.has(r.project) === true ||
|
|
177
|
+
reached.has(ids[i]!),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The projects whose `task` a `vx run <task> --affected` over `candidates`
|
|
183
|
+
* keeps (`vx show <task> --affected`): the run's graph, `graph` stage and
|
|
184
|
+
* selection, without opening a cache or keying a task.
|
|
185
|
+
*/
|
|
186
|
+
export async function affectedTaskProjects(args: {
|
|
187
|
+
task: string
|
|
188
|
+
candidates: readonly string[]
|
|
189
|
+
changes: AffectedChanges
|
|
190
|
+
outright?: readonly string[] | undefined
|
|
191
|
+
projects: Map<string, ProjectEntry>
|
|
192
|
+
packageGraph: PackageGraph
|
|
193
|
+
projectMetas: readonly ProjectMeta[]
|
|
194
|
+
plugins: readonly VxPlugin[]
|
|
195
|
+
workspaceRoot: string
|
|
196
|
+
cacheDir: string
|
|
197
|
+
rules?: WorkspaceRules | undefined
|
|
198
|
+
warn: (message: string) => void
|
|
199
|
+
}): Promise<string[]> {
|
|
200
|
+
const requested = expandRequested([args.task], args.candidates, args.projects, isDefaultBuild)
|
|
201
|
+
const nodes = buildTaskGraph({
|
|
202
|
+
projects: args.projects,
|
|
203
|
+
packageGraph: args.packageGraph,
|
|
204
|
+
requested,
|
|
205
|
+
workspaceRoot: args.workspaceRoot,
|
|
206
|
+
rules: args.rules,
|
|
207
|
+
})
|
|
208
|
+
if (hasHook(args.plugins, 'graph')) await applyGraphStage(args.plugins, nodes, args)
|
|
209
|
+
const kept = keptByAffected(nodes, requested, args.changes, args.projects, args.packageGraph, {
|
|
210
|
+
outright: new Set(args.outright),
|
|
211
|
+
})
|
|
212
|
+
return kept.map((r) => r.project)
|
|
213
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The `graph` stage as a run applies it, shared by `prepareRun` and the
|
|
2
|
+
// CLI's `--affected` task selection (`vx show <task> --affected`), so both
|
|
3
|
+
// select over the graph the hooks leave.
|
|
4
|
+
|
|
5
|
+
import type { WorkspaceRules } from '../config.js'
|
|
6
|
+
import type { TaskNode } from '../graph/index.js'
|
|
7
|
+
import { type ProjectMeta, validateProjectConfig } from '../workspace/index.js'
|
|
8
|
+
import { applyGraphHooks } from './plugin-host.js'
|
|
9
|
+
import type { VxPlugin } from './plugin.js'
|
|
10
|
+
import { isDefaultBuild } from './projects.js'
|
|
11
|
+
|
|
12
|
+
export async function applyGraphStage(
|
|
13
|
+
plugins: readonly VxPlugin[],
|
|
14
|
+
nodes: Map<string, TaskNode>,
|
|
15
|
+
args: {
|
|
16
|
+
workspaceRoot: string
|
|
17
|
+
cacheDir: string
|
|
18
|
+
projectMetas: readonly ProjectMeta[]
|
|
19
|
+
warn: (message: string) => void
|
|
20
|
+
rules?: WorkspaceRules | undefined
|
|
21
|
+
},
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
// A hook edits each node's config in place, as `project` does, so the
|
|
24
|
+
// edit is held to what the loader accepts from a user: a misspelled
|
|
25
|
+
// `exec` field ran as an empty command and failed with no reason.
|
|
26
|
+
const configPaths = new Map(args.projectMetas.map((m) => [m.name, m.configPath]))
|
|
27
|
+
await applyGraphHooks(
|
|
28
|
+
plugins,
|
|
29
|
+
nodes,
|
|
30
|
+
{
|
|
31
|
+
workspaceRoot: args.workspaceRoot,
|
|
32
|
+
cacheDir: args.cacheDir,
|
|
33
|
+
warn: args.warn,
|
|
34
|
+
requested: [...nodes.values()].filter((n) => n.requested).map((n) => n.id),
|
|
35
|
+
},
|
|
36
|
+
(plugin) => {
|
|
37
|
+
for (const n of nodes.values()) {
|
|
38
|
+
if (isDefaultBuild(n.config)) continue
|
|
39
|
+
const where = configPaths.get(n.projectName) ?? `${n.projectName} (no config file)`
|
|
40
|
+
validateProjectConfig(
|
|
41
|
+
{ tasks: { [n.taskName]: n.config } },
|
|
42
|
+
`${where} (after plugin '${plugin.name}')`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
args.rules,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
resolveCacheDir,
|
|
40
40
|
resolveStoreRoot,
|
|
41
41
|
type ProjectEntry,
|
|
42
|
-
validateProjectConfig,
|
|
43
42
|
} from '../workspace/index.js'
|
|
44
43
|
import {
|
|
45
44
|
buildTaskGraph,
|
|
@@ -51,7 +50,6 @@ import {
|
|
|
51
50
|
unresolvedRequests,
|
|
52
51
|
} from '../graph/index.js'
|
|
53
52
|
import {
|
|
54
|
-
applyGraphHooks,
|
|
55
53
|
applyKeyHooks,
|
|
56
54
|
applyScheduleHooks,
|
|
57
55
|
fingerprintClaims,
|
|
@@ -68,7 +66,8 @@ import {
|
|
|
68
66
|
loadWorkspacePlugins,
|
|
69
67
|
type LoadedProjects,
|
|
70
68
|
} from './projects.js'
|
|
71
|
-
import {
|
|
69
|
+
import { keptByAffected } from './affected-tasks.js'
|
|
70
|
+
import { applyGraphStage } from './graph-stage.js'
|
|
72
71
|
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
73
72
|
import { FingerprintWatch } from './fingerprint-watch.js'
|
|
74
73
|
import type { VxPlugin } from './plugin.js'
|
|
@@ -527,33 +526,14 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
527
526
|
})
|
|
528
527
|
let nodes = graphOf(requested)
|
|
529
528
|
const hasGraphHook = hasHook(plugins, 'graph')
|
|
530
|
-
const graphStage =
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
{
|
|
539
|
-
workspaceRoot,
|
|
540
|
-
cacheDir,
|
|
541
|
-
warn: (m) => log.status(m),
|
|
542
|
-
requested: [...nodes.values()].filter((n) => n.requested).map((n) => n.id),
|
|
543
|
-
},
|
|
544
|
-
(plugin) => {
|
|
545
|
-
for (const n of nodes.values()) {
|
|
546
|
-
if (isDefaultBuild(n.config)) continue
|
|
547
|
-
const where = configPaths.get(n.projectName) ?? `${n.projectName} (no config file)`
|
|
548
|
-
validateProjectConfig(
|
|
549
|
-
{ tasks: { [n.taskName]: n.config } },
|
|
550
|
-
`${where} (after plugin '${plugin.name}')`,
|
|
551
|
-
)
|
|
552
|
-
}
|
|
553
|
-
},
|
|
554
|
-
workspaceConfig?.rules,
|
|
555
|
-
)
|
|
556
|
-
}
|
|
529
|
+
const graphStage = (): Promise<void> =>
|
|
530
|
+
applyGraphStage(plugins, nodes, {
|
|
531
|
+
workspaceRoot,
|
|
532
|
+
cacheDir,
|
|
533
|
+
projectMetas,
|
|
534
|
+
warn: (m) => log.status(m),
|
|
535
|
+
rules: workspaceConfig?.rules,
|
|
536
|
+
})
|
|
557
537
|
// `--affected` selects over the FINAL graph: an edge or an input a
|
|
558
538
|
// `graph` hook adds moves a key, so the hooks run first, over every
|
|
559
539
|
// candidate, and the selection prunes what they left.
|
|
@@ -563,16 +543,13 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
563
543
|
if (options.affected !== undefined) {
|
|
564
544
|
const before = hooksFirst ? new Set(nodes.keys()) : null
|
|
565
545
|
if (hooksFirst) await graphStage()
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const kept = requested.filter(
|
|
571
|
-
(r, i) => named.has(ids[i]!) || outright.has(r.project) || reached.has(ids[i]!),
|
|
572
|
-
)
|
|
546
|
+
const kept = keptByAffected(nodes, requested, options.affected, projects, packageGraph, {
|
|
547
|
+
named: new Set(tasks.filter((t) => t.includes('#'))),
|
|
548
|
+
outright: new Set(options.selectedOutright),
|
|
549
|
+
})
|
|
573
550
|
if (kept.length === 0) return emptyRun('none-affected')
|
|
574
551
|
if (before !== null) {
|
|
575
|
-
const asked = new Set(
|
|
552
|
+
const asked = new Set(requested.map((r) => `${r.project}#${r.task}`))
|
|
576
553
|
// What a hook added or asked for is the plugin's choice, kept as it
|
|
577
554
|
// would be on a run without the diff.
|
|
578
555
|
const roots = [
|