@vzn/vx 0.0.18 → 0.0.21
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/LICENSE +1 -1
- package/README.md +99 -65
- package/package.json +10 -20
- package/src/bin.ts +12 -2
- package/src/cache/archive.ts +120 -18
- package/src/cache/cache.ts +313 -1324
- package/src/cache/chained-cache.ts +0 -8
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +16 -7
- package/src/cache/inputs.ts +246 -733
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +12 -17
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +39 -11
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +64 -36
- package/src/cli/index.ts +54 -15
- package/src/cli/info.ts +120 -91
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +38 -6
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +14 -3
- package/src/cli/plugin-commands.ts +15 -8
- package/src/cli/run.ts +84 -302
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +2 -1
- package/src/cli/watch.ts +647 -97
- package/src/cli/why.ts +44 -14
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +156 -136
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +11 -26
- package/src/exec/index.ts +6 -2
- package/src/{plugins/local-executor/index.ts → exec/local-executor.ts} +12 -16
- package/src/exec/runner.ts +57 -18
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +582 -370
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +1 -9
- package/src/graph/scheduler.ts +158 -203
- package/src/graph/task-graph.ts +2 -2
- package/src/index.ts +97 -102
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +3 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/events.ts +42 -8
- package/src/orchestrator/execute-task.ts +145 -554
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/framed-output.ts +41 -36
- package/src/orchestrator/history.ts +118 -98
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +35 -4
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +13 -22
- package/src/orchestrator/metrics.ts +53 -20
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +34 -31
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plugin-host.ts +195 -38
- package/src/orchestrator/plugin.ts +162 -9
- package/src/orchestrator/prepare.ts +77 -132
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-artifacts.ts +25 -3
- package/src/orchestrator/run-context.ts +1 -1
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +2 -2
- package/src/orchestrator/run.ts +246 -597
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +60 -3
- package/src/orchestrator/status-line.ts +2 -1
- package/src/orchestrator/summary.ts +68 -25
- package/src/orchestrator/task-hash.ts +20 -18
- package/src/orchestrator/task-log-buffer.ts +8 -11
- package/src/orchestrator/telemetry-host.ts +11 -1
- package/src/orchestrator/telemetry.ts +3 -17
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/index.ts +11 -3
- package/src/util/paths.ts +23 -2
- package/src/util/settle.ts +19 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +111 -11
- package/src/workspace/config-cache.ts +11 -2
- package/src/workspace/config-eval.ts +11 -4
- package/src/workspace/config-imports.ts +5 -4
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +43 -6
- package/src/workspace/fingerprint.ts +37 -3
- package/src/workspace/index.ts +30 -3
- package/src/workspace/lockfile.ts +12 -7
- package/src/{cli → workspace}/migrate-scripts.ts +26 -19
- package/src/{cli/migrate.ts → workspace/migration.ts} +133 -124
- package/src/workspace/package-graph.ts +98 -28
- package/src/workspace/project-loader.ts +76 -746
- package/src/workspace/workspace.ts +98 -13
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/schedule-history/index.ts +0 -109
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/** Shown by every "no <capability> plugin declared" error. */
|
|
2
|
-
export const MISSING_PLUGIN_HINT = `vx runs nothing it was not told to. Declare the plugins in vx.workspace.ts:
|
|
3
|
-
|
|
4
|
-
import { defineWorkspace } from '@vzn/vx'
|
|
5
|
-
import { localExecutorPlugin } from '@vzn/vx/plugins/local-executor'
|
|
6
|
-
import { localCachePlugin } from '@vzn/vx/plugins/local-cache'
|
|
7
|
-
export default defineWorkspace({ plugins: [localExecutorPlugin(), localCachePlugin()] })
|
|
8
|
-
|
|
9
|
-
Put a remote executor or cache plugin BEFORE the local one to prefer it.`
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Resolve `exec.resources` declarations into the absolute per-task costs the
|
|
2
|
-
// scheduler's 2-D admission gate packs against. Pure; the loader has already
|
|
3
|
-
// validated every form (this module trusts internal code), so an unparseable
|
|
4
|
-
// value can't reach it from a real config.
|
|
5
|
-
//
|
|
6
|
-
// The declared units are CPU CORES and MEGABYTES — the same numbers on this
|
|
7
|
-
// machine and on a remote worker, which is why they are also what
|
|
8
|
-
// `TaskPlacement.resources` hands an executor verbatim. Percent forms were
|
|
9
|
-
// removed on 2026-08-30: a percentage names a fraction of THIS run's budget,
|
|
10
|
-
// and an executor placing the task on someone else's machine has no way to
|
|
11
|
-
// mean anything by it.
|
|
12
|
-
|
|
13
|
-
import type { ResourceCost, TaskNode } from '../graph/index.js'
|
|
14
|
-
|
|
15
|
-
const BYTES_PER_MB = 1024 * 1024
|
|
16
|
-
|
|
17
|
-
/** `cpus` → CPU units. Absent → 0 = reserve nothing. */
|
|
18
|
-
export function resolveCpu(v: number | undefined): number {
|
|
19
|
-
return v ?? 0
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** `memory` (megabytes) → bytes, which is what the budget axis counts. */
|
|
23
|
-
export function resolveMem(v: number | undefined): number {
|
|
24
|
-
return v === undefined ? 0 : v * BYTES_PER_MB
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Resolve the whole graph's declared reservations, OMITTING zero-cost
|
|
29
|
-
* tasks: an empty map means "no reservations declared", the single gate
|
|
30
|
-
* the scheduler (and run.ts's option threading) keys off to keep every
|
|
31
|
-
* current run byte-identical.
|
|
32
|
-
*/
|
|
33
|
-
export function resolveResourceCosts(
|
|
34
|
-
nodes: ReadonlyMap<string, TaskNode>,
|
|
35
|
-
): Map<string, ResourceCost> {
|
|
36
|
-
const out = new Map<string, ResourceCost>()
|
|
37
|
-
for (const node of nodes.values()) {
|
|
38
|
-
const declared = node.config.exec?.resources
|
|
39
|
-
if (declared === undefined) continue
|
|
40
|
-
const cpu = resolveCpu(declared.cpus)
|
|
41
|
-
const mem = resolveMem(declared.memory)
|
|
42
|
-
if (cpu > 0 || mem > 0) out.set(node.id, { cpu, mem })
|
|
43
|
-
}
|
|
44
|
-
return out
|
|
45
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// Cache-correctness verification (Phase 1: determinism). Pure helpers for the
|
|
2
|
-
// `vx run --verify` hook in execute-task.ts: content-fingerprint a task's
|
|
3
|
-
// output tree (mtime-independent) and diff two runs' trees, so a task whose
|
|
4
|
-
// outputs are NOT a pure function of its declared inputs — a non-hermetic task
|
|
5
|
-
// whose cache entry would replay arbitrary past bytes — is caught and named.
|
|
6
|
-
// See docs/design/cache-correctness-2026-07.md.
|
|
7
|
-
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
import type { OutputFingerprint, TaskOutcome, VerifyVerdict } from '../graph/index.js'
|
|
10
|
-
import { xxh3hex } from '../util/index.js'
|
|
11
|
-
|
|
12
|
-
/** An output file to fingerprint: absolute path + a stable, run-independent
|
|
13
|
-
* key (project-relative, or `workspace-outputs/<rel-to-root>` for workspace
|
|
14
|
-
* outputs — matching the cache artifact's namespace so the two never collide). */
|
|
15
|
-
export interface OutputRef {
|
|
16
|
-
abs: string
|
|
17
|
-
key: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Build the fingerprint key set from a task's resolved output paths. */
|
|
21
|
-
export function outputRefs(
|
|
22
|
-
projectDir: string,
|
|
23
|
-
outputFiles: readonly string[],
|
|
24
|
-
workspaceRoot: string,
|
|
25
|
-
workspaceOutputFiles: readonly string[],
|
|
26
|
-
): OutputRef[] {
|
|
27
|
-
const rel = (from: string, abs: string): string =>
|
|
28
|
-
path.relative(from, abs).split(path.sep).join('/')
|
|
29
|
-
return [
|
|
30
|
-
...outputFiles.map((abs) => ({ abs, key: rel(projectDir, abs) })),
|
|
31
|
-
...workspaceOutputFiles.map((abs) => ({
|
|
32
|
-
abs,
|
|
33
|
-
key: `workspace-outputs/${rel(workspaceRoot, abs)}`,
|
|
34
|
-
})),
|
|
35
|
-
]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Content-fingerprint an output tree: key → xxh3 of the file's BYTES, read
|
|
39
|
-
* unconditionally from disk. Deliberately NOT `Cache.hashFile`: its
|
|
40
|
-
* mtime+size memo would return attempt 1's digest for a re-run output with
|
|
41
|
-
* equal size and mtime (e.g. a build that normalizes mtimes — standard
|
|
42
|
-
* reproducible-build practice), silently proving a divergent task
|
|
43
|
-
* deterministic. fp1/fp2 are only ever compared to each other, so any
|
|
44
|
-
* content hash works — and the proof must not trust a cache. */
|
|
45
|
-
export async function hashOutputTree(refs: readonly OutputRef[]): Promise<Map<string, string>> {
|
|
46
|
-
const out = new Map<string, string>()
|
|
47
|
-
for (const r of refs) {
|
|
48
|
-
out.set(r.key, xxh3hex(new Uint8Array(await Bun.file(r.abs).arrayBuffer())))
|
|
49
|
-
}
|
|
50
|
-
return out
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Per-file map cap on a shipped fingerprint (~40 KB at the cap). The tree
|
|
54
|
-
* digest always folds ALL entries, so detection never depends on the map. */
|
|
55
|
-
const FP_MAX_FILES = 500
|
|
56
|
-
|
|
57
|
-
/** Roll an output-tree fingerprint map into the shippable `OutputFingerprint`
|
|
58
|
-
* payload (Phase 4, cross-machine diff): a tree digest over ALL sorted
|
|
59
|
-
* entries folded `key\0hash\n` (\0 boundaries so parts can't alias), plus
|
|
60
|
-
* the first-`cap` pairs — deterministic truncation, so two machines'
|
|
61
|
-
* truncated maps cover the same key subset and stay diffable. */
|
|
62
|
-
export function foldFingerprint(fp: Map<string, string>, cap = FP_MAX_FILES): OutputFingerprint {
|
|
63
|
-
const keys = [...fp.keys()].sort()
|
|
64
|
-
let folded = ''
|
|
65
|
-
for (const k of keys) folded += `${k}\0${fp.get(k)!}\n`
|
|
66
|
-
return {
|
|
67
|
-
tree: xxh3hex(folded),
|
|
68
|
-
fileCount: keys.length,
|
|
69
|
-
files: keys.slice(0, cap).map((k) => [k, fp.get(k)!] as const),
|
|
70
|
-
...(keys.length > cap ? { truncated: true } : {}),
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The output keys whose content OID differs between two fingerprints
|
|
75
|
-
* (added, removed, or changed), sorted. */
|
|
76
|
-
export function diffOutputTrees(a: Map<string, string>, b: Map<string, string>): string[] {
|
|
77
|
-
const changed: string[] = []
|
|
78
|
-
for (const k of new Set([...a.keys(), ...b.keys()])) {
|
|
79
|
-
if (a.get(k) !== b.get(k)) changed.push(k)
|
|
80
|
-
}
|
|
81
|
-
return changed.sort()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Turn a determinism diff into a verdict, honoring the `--verify-allow`
|
|
85
|
-
* escape hatch (an allowed divergence is reported but never fails the run). */
|
|
86
|
-
export function classifyDeterminism(changed: readonly string[], allowed: boolean): VerifyVerdict {
|
|
87
|
-
if (changed.length === 0) return { kind: 'proven-deterministic' }
|
|
88
|
-
return allowed
|
|
89
|
-
? { kind: 'allowed-nondeterministic', changed }
|
|
90
|
-
: { kind: 'nondeterministic', changed }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Extract the workspace-relative paths a task read outside its declared inputs
|
|
94
|
-
* (Phase 2, `--verify=inputs`). A strace-derived violation line carries the
|
|
95
|
-
* offending absolute path in trailing brackets (`… [/abs/path]`); pull it out,
|
|
96
|
-
* make it workspace-relative, dedup, and sort. Lines without a bracketed path
|
|
97
|
-
* (e.g. macOS syscall lines) fall back to the raw line so nothing is lost. */
|
|
98
|
-
export function undeclaredInputPaths(
|
|
99
|
-
violations: readonly { line: string }[],
|
|
100
|
-
workspaceRoot: string,
|
|
101
|
-
): string[] {
|
|
102
|
-
const rels = new Set<string>()
|
|
103
|
-
for (const v of violations) {
|
|
104
|
-
// Two wire shapes: Linux strace lines carry `[path]`; macOS
|
|
105
|
-
// sandbox-exec lines end in a bare absolute path
|
|
106
|
-
// (`node(123) deny(1) file-read-data /abs/path`). Fall back to the
|
|
107
|
-
// raw line only when neither parses — a path the user can act on
|
|
108
|
-
// beats a log line every time.
|
|
109
|
-
const m = /\[([^\]]+)\]\s*$/.exec(v.line) ?? /\s(\/\S+)\s*$/.exec(v.line)
|
|
110
|
-
const raw = m ? m[1]! : v.line
|
|
111
|
-
rels.add(
|
|
112
|
-
path.isAbsolute(raw) ? path.relative(workspaceRoot, raw).split(path.sep).join('/') : raw,
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
return [...rels].sort()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** The end-of-run `--verify` summary section: a counts line plus a
|
|
119
|
-
* failures block naming each non-hermetic task and its diverging outputs.
|
|
120
|
-
* Empty when no task carried a verdict. */
|
|
121
|
-
export function formatVerifySection(outcomes: readonly TaskOutcome[]): string[] {
|
|
122
|
-
const verdicts = outcomes.filter((o) => o.verify !== undefined)
|
|
123
|
-
if (verdicts.length === 0) return []
|
|
124
|
-
let proven = 0
|
|
125
|
-
let bad = 0
|
|
126
|
-
let na = 0
|
|
127
|
-
let notVerified = 0
|
|
128
|
-
for (const o of verdicts) {
|
|
129
|
-
switch (o.verify!.kind) {
|
|
130
|
-
case 'proven-deterministic':
|
|
131
|
-
case 'proven-complete':
|
|
132
|
-
proven++
|
|
133
|
-
break
|
|
134
|
-
case 'nondeterministic':
|
|
135
|
-
case 'rerun-failed':
|
|
136
|
-
case 'undeclared-inputs':
|
|
137
|
-
bad++
|
|
138
|
-
break
|
|
139
|
-
case 'allowed-nondeterministic':
|
|
140
|
-
case 'no-outputs':
|
|
141
|
-
na++
|
|
142
|
-
break
|
|
143
|
-
case 'not-verified':
|
|
144
|
-
notVerified++
|
|
145
|
-
break
|
|
146
|
-
case 'unverifiable-remote-only':
|
|
147
|
-
na++
|
|
148
|
-
break
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const lines = [
|
|
152
|
-
'',
|
|
153
|
-
` Verify: ${proven} proven · ${bad} unsafe · ${na} n/a · ${notVerified} not-verified`,
|
|
154
|
-
]
|
|
155
|
-
for (const o of outcomes) {
|
|
156
|
-
const v = o.verify
|
|
157
|
-
if (v === undefined) continue
|
|
158
|
-
if (v.kind === 'nondeterministic') {
|
|
159
|
-
lines.push(` ✗ ${o.node.id} — nondeterministic`)
|
|
160
|
-
lines.push(` changed: ${v.changed.join(', ')}`)
|
|
161
|
-
} else if (v.kind === 'undeclared-inputs') {
|
|
162
|
-
lines.push(` ✗ ${o.node.id} — read undeclared inputs`)
|
|
163
|
-
if (v.paths.length > 0) lines.push(` ${v.paths.join(', ')}`)
|
|
164
|
-
lines.push(' add them to cache.inputs.files / workspaceFiles')
|
|
165
|
-
} else if (v.kind === 'rerun-failed') {
|
|
166
|
-
lines.push(` ✗ ${o.node.id} — verify re-run failed (exit ${v.exitCode})`)
|
|
167
|
-
} else if (v.kind === 'unverifiable-remote-only') {
|
|
168
|
-
lines.push(` ⚠ ${o.node.id} — remote-only: no local execution to prove (unverified)`)
|
|
169
|
-
} else if (v.kind === 'allowed-nondeterministic') {
|
|
170
|
-
lines.push(` ⚠ ${o.node.id} — nondeterministic (allowed)`)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return lines
|
|
174
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Core's cache as a plugin: the `.vx/cache` handle the host opened (the run
|
|
2
|
-
// index + the local artifact store). Declared like any other cache layer;
|
|
3
|
-
// put a remote layer BEFORE it to look there first.
|
|
4
|
-
|
|
5
|
-
import type { VxPlugin } from '@vzn/vx'
|
|
6
|
-
|
|
7
|
-
export const LOCAL_CACHE_PLUGIN = 'vx/local-cache'
|
|
8
|
-
|
|
9
|
-
export function localCachePlugin(): VxPlugin {
|
|
10
|
-
return { name: LOCAL_CACHE_PLUGIN, cache: (ctx) => ctx.localCache }
|
|
11
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// History-based scheduling as a plugin: order ready tasks by their expected
|
|
2
|
-
// REMAINING critical-path duration (own p50 + the longest chain of
|
|
3
|
-
// dependents), learned from the local run history. This was core's opt-in
|
|
4
|
-
// `predictive` mode until 2026-09-02; it is the reference `schedule` plugin
|
|
5
|
-
// now — declared like any other, and paying its history read only in
|
|
6
|
-
// workspaces that ask for it.
|
|
7
|
-
//
|
|
8
|
-
// Cache hits are NOT modelled as zero-cost: predicting cache state needs
|
|
9
|
-
// the key and a probe, which the scheduler handles at run time (a confirmed
|
|
10
|
-
// hit is backfill, never a critical-path task). The estimate here is "what
|
|
11
|
-
// if everything ran", which is exactly the case where order matters.
|
|
12
|
-
|
|
13
|
-
import { LocalHistoryProvider, type HistoryTable, type TaskNode, type VxPlugin } from '@vzn/vx'
|
|
14
|
-
|
|
15
|
-
const SCHEDULE_HISTORY_PLUGIN = 'vx/schedule-history'
|
|
16
|
-
|
|
17
|
-
/** Default duration when neither task history nor a workspace median exists. */
|
|
18
|
-
const DEFAULT_DURATION_MS = 1000
|
|
19
|
-
|
|
20
|
-
export interface ScheduleHistoryOptions {
|
|
21
|
-
/** How many recent runs per task to learn from. Default 20. */
|
|
22
|
-
readonly window?: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function scheduleHistoryPlugin(options: ScheduleHistoryOptions = {}): VxPlugin {
|
|
26
|
-
return {
|
|
27
|
-
name: SCHEDULE_HISTORY_PLUGIN,
|
|
28
|
-
async schedule(nodes, ctx) {
|
|
29
|
-
const provider =
|
|
30
|
-
options.window === undefined
|
|
31
|
-
? new LocalHistoryProvider(ctx.localCache.dbHandle())
|
|
32
|
-
: new LocalHistoryProvider(ctx.localCache.dbHandle(), options.window)
|
|
33
|
-
let table: HistoryTable
|
|
34
|
-
try {
|
|
35
|
-
table = await provider.loadFor([...nodes.keys()])
|
|
36
|
-
} catch (err) {
|
|
37
|
-
// Failing open: a broken history read costs the ordering, never the run.
|
|
38
|
-
ctx.warn(
|
|
39
|
-
`[vx] schedule-history: falling back to the baseline order: ${err instanceof Error ? err.message : String(err)}`,
|
|
40
|
-
)
|
|
41
|
-
return undefined
|
|
42
|
-
}
|
|
43
|
-
return criticalPathPriorities([...nodes.values()], table)
|
|
44
|
-
},
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* For each node, the expected remaining critical-path duration: its own p50
|
|
50
|
-
* plus the maximum over its dependents. A node with no history takes the
|
|
51
|
-
* workspace median; an empty history takes a flat default.
|
|
52
|
-
*/
|
|
53
|
-
export function criticalPathPriorities(
|
|
54
|
-
nodes: readonly TaskNode[],
|
|
55
|
-
history: HistoryTable,
|
|
56
|
-
): ReadonlyMap<string, number> {
|
|
57
|
-
const p50s: number[] = []
|
|
58
|
-
for (const h of history.values()) {
|
|
59
|
-
if (h.p50DurationMs !== undefined) p50s.push(h.p50DurationMs)
|
|
60
|
-
}
|
|
61
|
-
p50s.sort((a, b) => a - b)
|
|
62
|
-
const workspaceMedian =
|
|
63
|
-
p50s.length > 0
|
|
64
|
-
? (p50s[Math.floor(p50s.length / 2)] ?? DEFAULT_DURATION_MS)
|
|
65
|
-
: DEFAULT_DURATION_MS
|
|
66
|
-
|
|
67
|
-
const dependentsOf = new Map<string, string[]>()
|
|
68
|
-
for (const n of nodes) {
|
|
69
|
-
for (const upstreamId of n.deps) {
|
|
70
|
-
const list = dependentsOf.get(upstreamId)
|
|
71
|
-
if (list) list.push(n.id)
|
|
72
|
-
else dependentsOf.set(upstreamId, [n.id])
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const ownDuration = (n: TaskNode): number => history.get(n.id)?.p50DurationMs ?? workspaceMedian
|
|
76
|
-
|
|
77
|
-
// Reverse-topological pass: a node's value is final once every dependent's
|
|
78
|
-
// is, so start from the sinks (no dependents) and release each upstream
|
|
79
|
-
// when its last dependent has been scored.
|
|
80
|
-
const memo = new Map<string, number>()
|
|
81
|
-
const nodeById = new Map<string, TaskNode>(nodes.map((n) => [n.id, n]))
|
|
82
|
-
const pending = new Map<string, number>()
|
|
83
|
-
const queue: TaskNode[] = []
|
|
84
|
-
for (const n of nodes) {
|
|
85
|
-
const count = dependentsOf.get(n.id)?.length ?? 0
|
|
86
|
-
pending.set(n.id, count)
|
|
87
|
-
if (count === 0) queue.push(n)
|
|
88
|
-
}
|
|
89
|
-
let head = 0
|
|
90
|
-
while (head < queue.length) {
|
|
91
|
-
const n = queue[head++]!
|
|
92
|
-
let downstream = 0
|
|
93
|
-
for (const dep of dependentsOf.get(n.id) ?? []) {
|
|
94
|
-
const d = memo.get(dep) ?? 0
|
|
95
|
-
if (d > downstream) downstream = d
|
|
96
|
-
}
|
|
97
|
-
memo.set(n.id, ownDuration(n) + downstream)
|
|
98
|
-
for (const up of n.deps) {
|
|
99
|
-
const left = (pending.get(up) ?? 0) - 1
|
|
100
|
-
pending.set(up, left)
|
|
101
|
-
if (left === 0) {
|
|
102
|
-
const upNode = nodeById.get(up)
|
|
103
|
-
if (upNode) queue.push(upNode)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
for (const n of nodes) if (!memo.has(n.id)) memo.set(n.id, ownDuration(n))
|
|
108
|
-
return memo
|
|
109
|
-
}
|