@vzn/vx 0.0.21 → 0.0.22
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 +21 -18
- package/package.json +6 -6
- package/src/bin.ts +52 -3
- package/src/cache/archive.ts +170 -21
- package/src/cache/cache.ts +667 -288
- package/src/cache/chained-cache.ts +14 -2
- package/src/cache/config-evals.ts +29 -6
- package/src/cache/file-hashes.ts +63 -35
- package/src/cache/git-inputs.ts +398 -69
- package/src/cache/index.ts +8 -1
- package/src/cache/inputs.ts +403 -102
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +81 -11
- package/src/cache/layered-cache.ts +199 -32
- package/src/cache/output-index.ts +109 -10
- package/src/cache/run-history.ts +8 -3
- package/src/cache/zstd.ts +53 -30
- package/src/cli/cache.ts +51 -21
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +13 -3
- package/src/cli/index.ts +35 -9
- package/src/cli/info.ts +49 -7
- package/src/cli/init.ts +17 -5
- package/src/cli/last.ts +96 -21
- package/src/cli/lock.ts +34 -17
- package/src/cli/plan-format.ts +4 -4
- package/src/cli/plugin-commands.ts +20 -5
- package/src/cli/run.ts +55 -12
- package/src/cli/select.ts +23 -13
- package/src/cli/show.ts +9 -4
- package/src/cli/upgrade.ts +134 -14
- package/src/cli/watch.ts +611 -102
- package/src/cli/why.ts +8 -16
- package/src/cli/workspace-config.ts +21 -9
- package/src/config.ts +46 -9
- package/src/exec/executor.ts +51 -5
- package/src/exec/index.ts +10 -0
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/runner.ts +327 -82
- package/src/exec/sandbox-binds.ts +34 -5
- package/src/exec/sandbox-paths.ts +22 -2
- package/src/exec/sandbox-runtime.ts +627 -102
- package/src/exec/sandbox-violations.ts +36 -23
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +12 -2
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +182 -134
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +16 -2
- package/src/orchestrator/admission.ts +41 -17
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +22 -7
- package/src/orchestrator/doctor.ts +122 -14
- package/src/orchestrator/events.ts +71 -2
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +331 -37
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +81 -14
- package/src/orchestrator/history.ts +6 -7
- package/src/orchestrator/hit-restore.ts +82 -18
- package/src/orchestrator/index.ts +7 -1
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +152 -32
- package/src/orchestrator/logger.ts +116 -20
- package/src/orchestrator/metrics.ts +156 -60
- package/src/orchestrator/miss-save.ts +106 -43
- package/src/orchestrator/options.ts +30 -4
- package/src/orchestrator/persistent.ts +41 -21
- package/src/orchestrator/placement.ts +55 -25
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +17 -23
- package/src/orchestrator/plugin.ts +31 -8
- package/src/orchestrator/prepare.ts +99 -13
- package/src/orchestrator/projects.ts +19 -8
- package/src/orchestrator/remote-prefetch.ts +21 -1
- package/src/orchestrator/run-artifacts.ts +9 -0
- package/src/orchestrator/run-context.ts +19 -4
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +6 -23
- package/src/orchestrator/run-report.ts +4 -2
- package/src/orchestrator/run.ts +264 -57
- package/src/orchestrator/sandbox-request.ts +388 -62
- package/src/orchestrator/save-lane.ts +4 -7
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +86 -30
- package/src/orchestrator/stable-keys.ts +202 -10
- package/src/orchestrator/status-line.ts +33 -1
- package/src/orchestrator/summary.ts +49 -2
- package/src/orchestrator/task-hash.ts +87 -7
- package/src/orchestrator/task-log-buffer.ts +7 -7
- package/src/orchestrator/telemetry-host.ts +16 -7
- package/src/orchestrator/telemetry.ts +69 -28
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +32 -3
- package/src/util/paths.ts +147 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +4 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +272 -64
- package/src/workspace/config-cache.ts +262 -32
- package/src/workspace/config-eval.ts +49 -12
- package/src/workspace/config-imports.ts +126 -5
- package/src/workspace/config-schema.ts +237 -41
- package/src/workspace/filter.ts +33 -18
- package/src/workspace/fingerprint.ts +29 -11
- package/src/workspace/index.ts +7 -0
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +33 -11
- package/src/workspace/migrate-scripts.ts +60 -7
- package/src/workspace/migration.ts +40 -1
- package/src/workspace/package-graph.ts +154 -16
- package/src/workspace/project-loader.ts +227 -77
- package/src/workspace/workspace.ts +173 -23
|
@@ -139,18 +139,6 @@ export async function parseStraceViolations(
|
|
|
139
139
|
return out
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/**
|
|
143
|
-
* Apply the task's user-provided `sandbox.ignore` grants on top
|
|
144
|
-
* of whatever the macOS log monitor + Linux strace pass produced.
|
|
145
|
-
* Mirrors SRT's own substring-match semantics:
|
|
146
|
-
* - `'*'` entries match every command
|
|
147
|
-
* - other keys match commands whose userCommand string CONTAINS the key
|
|
148
|
-
* - the array of strings under each key is substring-matched against
|
|
149
|
-
* the violation line
|
|
150
|
-
*
|
|
151
|
-
* The defaults installed in `initSandbox` already filter on the macOS
|
|
152
|
-
* side; this pass catches per-task additions + Linux strace results.
|
|
153
|
-
*/
|
|
154
142
|
/**
|
|
155
143
|
* Does a violation line match something the task said to ignore?
|
|
156
144
|
*
|
|
@@ -199,14 +187,15 @@ function describeMacViolation(line: string): Partial<SandboxViolation> {
|
|
|
199
187
|
}
|
|
200
188
|
|
|
201
189
|
/**
|
|
202
|
-
* The violations a task's report should carry: inside the project
|
|
203
|
-
*
|
|
204
|
-
* chose to ignore. Exported so a
|
|
205
|
-
* platform's line shape without needing
|
|
190
|
+
* The violations a task's report should carry: inside the project or
|
|
191
|
+
* under a withheld dependency (`linked`), minus the loopback denial no
|
|
192
|
+
* config can avoid, minus what the task chose to ignore. Exported so a
|
|
193
|
+
* test can drive it with either platform's line shape without needing
|
|
194
|
+
* that platform.
|
|
206
195
|
*/
|
|
207
196
|
export function reportableViolations(
|
|
208
197
|
violations: readonly SandboxViolation[],
|
|
209
|
-
opts: { within: string; config: ResolvedSandboxConfig },
|
|
198
|
+
opts: { within: string; linked?: readonly string[]; config: ResolvedSandboxConfig },
|
|
210
199
|
): SandboxViolation[] {
|
|
211
200
|
// A record the producer did not describe is a seatbelt one, straight
|
|
212
201
|
// from SRT's store — parse it here so the filters below never see a
|
|
@@ -215,7 +204,7 @@ export function reportableViolations(
|
|
|
215
204
|
v.target === undefined ? { ...v, ...describeMacViolation(v.line) } : v,
|
|
216
205
|
)
|
|
217
206
|
return filterIgnored(
|
|
218
|
-
loopbackNoise(withinReported(described, opts.within), opts.config),
|
|
207
|
+
loopbackNoise(withinReported(described, opts.within, opts.linked ?? []), opts.config),
|
|
219
208
|
opts.config.ignore,
|
|
220
209
|
)
|
|
221
210
|
}
|
|
@@ -247,7 +236,7 @@ function loopbackNoise(
|
|
|
247
236
|
}
|
|
248
237
|
|
|
249
238
|
/**
|
|
250
|
-
* Keep only denials on a path inside `within`.
|
|
239
|
+
* Keep only denials on a path inside `within`, or inside one of `linked`.
|
|
251
240
|
*
|
|
252
241
|
* A task may not leave its project — that is enforced by the deny anchor at
|
|
253
242
|
* the workspace root — but being STOPPED at the wall is the sandbox
|
|
@@ -260,17 +249,41 @@ function loopbackNoise(
|
|
|
260
249
|
* that makes a cached artifact wrong. A record with no path at all — a
|
|
261
250
|
* `system-info` probe — is kept, since it is not a boundary crossing and
|
|
262
251
|
* the task can grant it.
|
|
252
|
+
*
|
|
253
|
+
* `linked` are the workspace packages core withheld from a cached task's
|
|
254
|
+
* `node_modules` grant because its key does not answer for them. A denial
|
|
255
|
+
* there is the task reaching for a dependency through its own link — the
|
|
256
|
+
* read that would have been a stale hit, a finding and not the wall.
|
|
257
|
+
* Canonical already: sandbox-request.ts takes them from link targets.
|
|
263
258
|
*/
|
|
264
|
-
function withinReported(
|
|
265
|
-
|
|
259
|
+
function withinReported(
|
|
260
|
+
violations: SandboxViolation[],
|
|
261
|
+
within: string,
|
|
262
|
+
linked: readonly string[],
|
|
263
|
+
): SandboxViolation[] {
|
|
264
|
+
const roots = [toRealPath(within), ...linked]
|
|
266
265
|
// `root === '/'` would otherwise compare against `'//'` and drop
|
|
267
266
|
// everything — the one prefix that needs no separator appended.
|
|
268
|
-
const
|
|
267
|
+
const prefixes = roots.map((root) => (root.endsWith(path.sep) ? root : root + path.sep))
|
|
269
268
|
return violations.filter(
|
|
270
|
-
(v) =>
|
|
269
|
+
(v) =>
|
|
270
|
+
v.path === undefined ||
|
|
271
|
+
roots.some((root, i) => v.path === root || v.path!.startsWith(prefixes[i]!)),
|
|
271
272
|
)
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Apply the task's user-provided `sandbox.ignore` grants on top
|
|
277
|
+
* of whatever the macOS log monitor + Linux strace pass produced.
|
|
278
|
+
* Mirrors SRT's own substring-match semantics:
|
|
279
|
+
* - `'*'` entries match every command
|
|
280
|
+
* - other keys match commands whose userCommand string CONTAINS the key
|
|
281
|
+
* - the array of strings under each key is substring-matched against
|
|
282
|
+
* the violation line
|
|
283
|
+
*
|
|
284
|
+
* The defaults installed in `initSandbox` already filter on the macOS
|
|
285
|
+
* side; this pass catches per-task additions + Linux strace results.
|
|
286
|
+
*/
|
|
274
287
|
function filterIgnored(
|
|
275
288
|
violations: SandboxViolation[],
|
|
276
289
|
ignore: ResolvedSandboxConfig['ignore'],
|
|
@@ -18,20 +18,6 @@ export class DependencySpecError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Parse one `dependsOn` / `cache.inputs.tasks` entry.
|
|
23
|
-
*
|
|
24
|
-
* `name` → self.name
|
|
25
|
-
* `^name` → every dep workspace's `name` task
|
|
26
|
-
* `pkg#name` → specific package's `name` task (cross-edge)
|
|
27
|
-
* `*` → all same-project upstream (filter-only)
|
|
28
|
-
* `^*` → all dep upstream (filter-only)
|
|
29
|
-
* `!<form>` → negation of any of the above (filter-only)
|
|
30
|
-
*
|
|
31
|
-
* Wildcards and negation are accepted everywhere — callers (the graph
|
|
32
|
-
* builder vs the filter) decide which ones make semantic sense and
|
|
33
|
-
* surface their own validation errors. This keeps the parser pure.
|
|
34
|
-
*/
|
|
35
21
|
/** True when a task form is a `*` pattern (`build.*`) rather than an exact name. */
|
|
36
22
|
export function isTaskPattern(task: string): boolean {
|
|
37
23
|
return task.includes('*')
|
|
@@ -48,6 +34,20 @@ export function compileTaskPattern(pattern: string): RegExp {
|
|
|
48
34
|
return new RegExp(`^${escaped}$`)
|
|
49
35
|
}
|
|
50
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Parse one `dependsOn` / `cache.inputs.tasks` entry.
|
|
39
|
+
*
|
|
40
|
+
* `name` → self.name
|
|
41
|
+
* `^name` → every dep workspace's `name` task
|
|
42
|
+
* `pkg#name` → specific package's `name` task (cross-edge)
|
|
43
|
+
* `*` → all same-project upstream (filter-only)
|
|
44
|
+
* `^*` → all dep upstream (filter-only)
|
|
45
|
+
* `!<form>` → negation of any of the above (filter-only)
|
|
46
|
+
*
|
|
47
|
+
* Wildcards and negation are accepted everywhere — callers (the graph
|
|
48
|
+
* builder vs the filter) decide which ones make semantic sense and
|
|
49
|
+
* surface their own validation errors. This keeps the parser pure.
|
|
50
|
+
*/
|
|
51
51
|
export function parseDependencySpec(raw: string): DependencySpec {
|
|
52
52
|
if (raw.length === 0) throw new DependencySpecError(raw, 'empty spec')
|
|
53
53
|
|
package/src/graph/index.ts
CHANGED
|
@@ -8,14 +8,24 @@ export {
|
|
|
8
8
|
parseDependencySpec,
|
|
9
9
|
type DependencySpec,
|
|
10
10
|
} from './dependency-spec.js'
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
type ContinueMode,
|
|
13
|
+
RestoreDemoted,
|
|
14
|
+
runGraph,
|
|
15
|
+
type TaskOutcome,
|
|
16
|
+
type TaskStatus,
|
|
17
|
+
} from './scheduler.js'
|
|
12
18
|
export {
|
|
13
19
|
buildTaskGraph,
|
|
14
|
-
|
|
20
|
+
checkGraph,
|
|
21
|
+
declaredTask,
|
|
22
|
+
excludeDependencies,
|
|
15
23
|
expandRequested,
|
|
16
24
|
isGroupTask,
|
|
17
25
|
markSurfacedDeps,
|
|
26
|
+
outputsOverlap,
|
|
18
27
|
splitTaskId,
|
|
19
28
|
type TaskNode,
|
|
29
|
+
undeclaredDepsError,
|
|
20
30
|
unresolvedRequests,
|
|
21
31
|
} from './task-graph.js'
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// The ready-queue ranking, apart from the scheduler that uses it. Nothing
|
|
2
|
+
// here touches the platform: the Learn page's scheduler simulator
|
|
3
|
+
// (packages/vx-docs, through packages/vx-bench/schedule-policy.ts) bundles
|
|
4
|
+
// these two functions for the browser, and an import of the scheduler would
|
|
5
|
+
// bring `util/` and its load-time `Bun` reads with them.
|
|
6
|
+
|
|
7
|
+
import type { TaskNode } from './task-graph.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `computeReverseDepCount` for a run with a restore tier (item 754). A
|
|
11
|
+
* restore-tier task is a confirmed hit that never waits on its deps, so it
|
|
12
|
+
* blocks only the exec-tier tasks that depend on it: an exec-tier task's
|
|
13
|
+
* count is taken over the exec tier alone, which is exact, and a restore's
|
|
14
|
+
* is the sum over its direct exec-tier dependents of one plus theirs, so a
|
|
15
|
+
* restore that feeds pending work still goes first. A warm run's exec tier
|
|
16
|
+
* is its group tasks with no edges among them, and the closure over the
|
|
17
|
+
* whole graph (476 packages: 1,428 nodes, 12.8k edges of 45-word bitsets)
|
|
18
|
+
* was 6 ms of the run-graph stage for a ranking the restores never needed.
|
|
19
|
+
*/
|
|
20
|
+
export function tieredReverseDepCount(
|
|
21
|
+
nodes: Map<string, TaskNode>,
|
|
22
|
+
restoreTier: ReadonlySet<string>,
|
|
23
|
+
): Map<string, number> {
|
|
24
|
+
const exec = new Map<string, TaskNode>()
|
|
25
|
+
for (const [id, node] of nodes) if (!restoreTier.has(id)) exec.set(id, node)
|
|
26
|
+
const counts = computeReverseDepCount(exec)
|
|
27
|
+
for (const node of exec.values()) {
|
|
28
|
+
const weight = 1 + counts.get(node.id)!
|
|
29
|
+
for (const dep of node.deps) {
|
|
30
|
+
if (restoreTier.has(dep) && nodes.has(dep)) counts.set(dep, (counts.get(dep) ?? 0) + weight)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const id of restoreTier) if (nodes.has(id) && !counts.has(id)) counts.set(id, 0)
|
|
34
|
+
return counts
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute, for each task in the graph, how many OTHER tasks are
|
|
39
|
+
* transitively blocked on it. Tasks with the highest count are the
|
|
40
|
+
* most valuable to schedule first — finishing them unlocks the most
|
|
41
|
+
* downstream work and minimizes worker idle time at the end of the
|
|
42
|
+
* run. Nx's schedule sort ranks by the same idea but counts DIRECT
|
|
43
|
+
* dependents only (`calculateReverseDeps` in
|
|
44
|
+
* `packages/nx/src/tasks-runner/utils.ts`, read 2026-09-24).
|
|
45
|
+
*/
|
|
46
|
+
export function computeReverseDepCount(nodes: Map<string, TaskNode>): Map<string, number> {
|
|
47
|
+
// Exact transitive-dependent COUNTS need closure SETS (diamonds
|
|
48
|
+
// double-count under naive summing). Set-of-strings closures are
|
|
49
|
+
// O(N²) entries and took 8.5s on a 1090-package, 100-layer repo;
|
|
50
|
+
// bitsets make the same closure O(E·N/32) time and N²/8 bits of
|
|
51
|
+
// memory (3270 tasks ≈ 1.3 MB) — single-digit ms at that scale.
|
|
52
|
+
const ids = [...nodes.keys()]
|
|
53
|
+
const index = new Map<string, number>()
|
|
54
|
+
for (let i = 0; i < ids.length; i++) index.set(ids[i]!, i)
|
|
55
|
+
const n = ids.length
|
|
56
|
+
const words = (n + 31) >>> 5
|
|
57
|
+
|
|
58
|
+
// Direct dependents as index lists + in-degree for the topo pass.
|
|
59
|
+
const directReverse: number[][] = Array.from({ length: n }, () => [])
|
|
60
|
+
const indegree = new Uint32Array(n)
|
|
61
|
+
for (const node of nodes.values()) {
|
|
62
|
+
const ni = index.get(node.id)!
|
|
63
|
+
for (const dep of node.deps) {
|
|
64
|
+
const di = index.get(dep)
|
|
65
|
+
if (di === undefined) continue
|
|
66
|
+
directReverse[di]!.push(ni)
|
|
67
|
+
indegree[ni]!++
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Kahn topo order over dependency edges (deps before dependents).
|
|
72
|
+
// Insertion order is topo today, but the closure's correctness
|
|
73
|
+
// must not hinge on an unstated property of buildTaskGraph.
|
|
74
|
+
const topo = new Int32Array(n)
|
|
75
|
+
let head = 0
|
|
76
|
+
let tail = 0
|
|
77
|
+
for (let i = 0; i < n; i++) if (indegree[i] === 0) topo[tail++] = i
|
|
78
|
+
while (head < tail) {
|
|
79
|
+
const v = topo[head++]!
|
|
80
|
+
for (const r of directReverse[v]!) {
|
|
81
|
+
if (--indegree[r]! === 0) topo[tail++] = r
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Reverse-topo sweep: every direct dependent's closure is final
|
|
86
|
+
// before its dependency folds it in. closure[i] = bitset over node
|
|
87
|
+
// indices of i's transitive dependents.
|
|
88
|
+
const closure = new Uint32Array(n * words)
|
|
89
|
+
const counts = new Map<string, number>()
|
|
90
|
+
for (let t = tail - 1; t >= 0; t--) {
|
|
91
|
+
const i = topo[t]!
|
|
92
|
+
const base = i * words
|
|
93
|
+
for (const r of directReverse[i]!) {
|
|
94
|
+
closure[base + (r >>> 5)]! |= 1 << (r & 31)
|
|
95
|
+
const rbase = r * words
|
|
96
|
+
for (let w = 0; w < words; w++) closure[base + w]! |= closure[rbase + w]!
|
|
97
|
+
}
|
|
98
|
+
let count = 0
|
|
99
|
+
for (let w = 0; w < words; w++) {
|
|
100
|
+
let v = closure[base + w]!
|
|
101
|
+
v = v - ((v >>> 1) & 0x55555555)
|
|
102
|
+
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333)
|
|
103
|
+
count += (((v + (v >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24
|
|
104
|
+
}
|
|
105
|
+
counts.set(ids[i]!, count)
|
|
106
|
+
}
|
|
107
|
+
// Cycle-stranded nodes (never topo-visited) can't occur — the graph
|
|
108
|
+
// builder rejects cycles — but a missing Map entry would silently
|
|
109
|
+
// sort as undefined, so default them defensively to 0.
|
|
110
|
+
for (const id of ids) if (!counts.has(id)) counts.set(id, 0)
|
|
111
|
+
return counts
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function mergePriorities(
|
|
115
|
+
baseline: ReadonlyMap<string, number>,
|
|
116
|
+
overrides: ReadonlyMap<string, number>,
|
|
117
|
+
): ReadonlyMap<string, number> {
|
|
118
|
+
// The override caller scored "expected critical-path duration" in ms.
|
|
119
|
+
// Baseline reverse-deps counts are bounded by N; scale the override so
|
|
120
|
+
// it sorts above the baseline for any node it covers, and add the
|
|
121
|
+
// baseline as a tie-break for parity within the override set.
|
|
122
|
+
const SCALE = 1 << 20
|
|
123
|
+
const out = new Map<string, number>()
|
|
124
|
+
for (const [id, w] of baseline) out.set(id, w)
|
|
125
|
+
for (const [id, w] of overrides) {
|
|
126
|
+
const b = baseline.get(id) ?? 0
|
|
127
|
+
out.set(id, w * SCALE + b)
|
|
128
|
+
}
|
|
129
|
+
return out
|
|
130
|
+
}
|