@vzn/vx 0.0.650 → 0.0.652

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.
Files changed (146) hide show
  1. package/README.md +7 -23
  2. package/package.json +5 -5
  3. package/schemas/last.json +15 -0
  4. package/schemas/show.json +14 -20
  5. package/src/bin.ts +21 -31
  6. package/src/cache/archive.ts +1 -1
  7. package/src/cache/cache.ts +221 -30
  8. package/src/cache/file-hashes.ts +3 -0
  9. package/src/cache/git-inputs.ts +188 -39
  10. package/src/cache/history-stream.ts +134 -0
  11. package/src/cache/history-writer.ts +76 -0
  12. package/src/cache/index.ts +3 -0
  13. package/src/cache/inputs.ts +77 -14
  14. package/src/cache/layer.ts +17 -6
  15. package/src/cache/layered-cache.ts +4 -0
  16. package/src/cache/linked-inputs.ts +3 -0
  17. package/src/cache/lockfile-memo.ts +5 -3
  18. package/src/cache/output-index.ts +5 -2
  19. package/src/cache/remote-lane.ts +2 -1
  20. package/src/cache/run-history.ts +56 -17
  21. package/src/cache/schema.ts +15 -2
  22. package/src/cli/cache.ts +28 -18
  23. package/src/cli/completions.ts +4 -2
  24. package/src/cli/docs.ts +10 -11
  25. package/src/cli/help.ts +14 -25
  26. package/src/cli/index.ts +28 -9
  27. package/src/cli/info.ts +16 -8
  28. package/src/cli/init.ts +23 -15
  29. package/src/cli/last.ts +246 -204
  30. package/src/cli/lock.ts +55 -5
  31. package/src/cli/plan-format.ts +100 -118
  32. package/src/cli/plugin-commands.ts +8 -2
  33. package/src/cli/plugin-templates.ts +36 -45
  34. package/src/cli/run.ts +46 -27
  35. package/src/cli/select.ts +43 -5
  36. package/src/cli/show.ts +45 -52
  37. package/src/cli/task-verb.ts +22 -0
  38. package/src/cli/upgrade.ts +3 -1
  39. package/src/cli/watch-fs.ts +6 -1
  40. package/src/cli/watch-judge.ts +3 -2
  41. package/src/cli/watch-set.ts +4 -4
  42. package/src/cli/watch.ts +94 -52
  43. package/src/cli/why.ts +123 -89
  44. package/src/cli/workspace-config.ts +2 -1
  45. package/src/config.ts +47 -30
  46. package/src/exec/executor.ts +42 -2
  47. package/src/exec/index.ts +1 -0
  48. package/src/exec/kill-tree.ts +58 -5
  49. package/src/exec/local-executor.ts +1 -1
  50. package/src/exec/pipes.ts +152 -0
  51. package/src/exec/runner.ts +199 -182
  52. package/src/exec/sandbox-binds.ts +1 -1
  53. package/src/exec/sandbox-paths.ts +3 -1
  54. package/src/exec/sandbox-runtime.ts +317 -23
  55. package/src/exec/spawn-off.ts +154 -0
  56. package/src/exec/task-proxy.ts +93 -0
  57. package/src/exec/warm-shell.ts +146 -0
  58. package/src/graph/critical-path.ts +40 -28
  59. package/src/graph/definition.ts +16 -3
  60. package/src/graph/priorities.ts +98 -44
  61. package/src/graph/scheduler.ts +17 -11
  62. package/src/graph/task-graph.ts +51 -24
  63. package/src/handoff.ts +10 -7
  64. package/src/history/failure-mode.ts +24 -1
  65. package/src/history/history.ts +5 -0
  66. package/src/history/index.ts +1 -0
  67. package/src/history/metrics.ts +14 -6
  68. package/src/index.ts +7 -3
  69. package/src/orchestrator/admission.ts +1 -1
  70. package/src/orchestrator/command-windows.ts +1 -1
  71. package/src/orchestrator/deferred-outputs.ts +1 -1
  72. package/src/orchestrator/doctor.ts +1 -1
  73. package/src/orchestrator/download-policy.ts +5 -7
  74. package/src/orchestrator/excluded-keys.ts +1 -1
  75. package/src/orchestrator/execute-task.ts +67 -20
  76. package/src/orchestrator/graph-memo.ts +50 -1
  77. package/src/orchestrator/graph-stage.ts +6 -0
  78. package/src/orchestrator/hit-restore.ts +5 -6
  79. package/src/orchestrator/index.ts +3 -0
  80. package/src/orchestrator/keyed-projects.ts +28 -0
  81. package/src/orchestrator/live-usage.ts +16 -11
  82. package/src/orchestrator/local-shortcircuit.ts +2 -2
  83. package/src/orchestrator/locked-digests.ts +63 -0
  84. package/src/orchestrator/lockfile-claim.ts +108 -29
  85. package/src/orchestrator/miss-save.ts +58 -11
  86. package/src/orchestrator/persistent-task.ts +5 -1
  87. package/src/orchestrator/placement.ts +1 -1
  88. package/src/orchestrator/plan.ts +7 -3
  89. package/src/orchestrator/plugin-host.ts +67 -2
  90. package/src/orchestrator/plugin.ts +53 -2
  91. package/src/orchestrator/prepare.ts +16 -1
  92. package/src/orchestrator/projects.ts +4 -1
  93. package/src/orchestrator/remote-prefetch.ts +1 -1
  94. package/src/orchestrator/run-context.ts +29 -10
  95. package/src/orchestrator/run-failures.ts +62 -13
  96. package/src/orchestrator/run-keys.ts +1 -1
  97. package/src/orchestrator/run-records.ts +88 -59
  98. package/src/orchestrator/run-report.ts +1 -1
  99. package/src/orchestrator/run.ts +145 -41
  100. package/src/orchestrator/sandbox-request.ts +52 -9
  101. package/src/orchestrator/shell-verdict.ts +9 -0
  102. package/src/orchestrator/signals.ts +8 -5
  103. package/src/orchestrator/stable-keys.ts +17 -18
  104. package/src/orchestrator/task-artifacts.ts +1 -1
  105. package/src/orchestrator/task-hash.ts +21 -7
  106. package/src/orchestrator/telemetry.ts +5 -1
  107. package/src/report/events.ts +6 -14
  108. package/src/report/forecast.ts +10 -32
  109. package/src/report/framed-output.ts +283 -175
  110. package/src/report/index.ts +8 -3
  111. package/src/report/logger.ts +74 -39
  112. package/src/report/plain-output.ts +3 -1
  113. package/src/report/status-line.ts +49 -357
  114. package/src/report/summary.ts +105 -136
  115. package/src/settings.ts +8 -8
  116. package/src/ui/ansi.ts +48 -0
  117. package/src/ui/components.ts +506 -0
  118. package/src/ui/index.ts +79 -0
  119. package/src/ui/kit.ts +37 -0
  120. package/src/ui/live-region.ts +363 -0
  121. package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
  122. package/src/ui/terminal.ts +51 -0
  123. package/src/ui/theme.ts +224 -0
  124. package/src/ui/version-line.ts +28 -0
  125. package/src/util/config-frame.ts +191 -14
  126. package/src/util/edit-distance.ts +6 -1
  127. package/src/util/index.ts +2 -15
  128. package/src/util/paths.ts +21 -3
  129. package/src/util/secret-mask.ts +39 -3
  130. package/src/util/settle.ts +33 -1
  131. package/src/util/tail.ts +4 -1
  132. package/src/util/timing.ts +4 -16
  133. package/src/workspace/affected.ts +89 -30
  134. package/src/workspace/bracket-literal.ts +2 -2
  135. package/src/workspace/config-schema.ts +70 -63
  136. package/src/workspace/dependencies-lock.ts +191 -0
  137. package/src/workspace/filter.ts +36 -1
  138. package/src/workspace/index.ts +9 -0
  139. package/src/workspace/lockfile.ts +159 -6
  140. package/src/workspace/migration.ts +16 -9
  141. package/src/workspace/workspace.ts +3 -0
  142. package/src/util/colors.ts +0 -133
  143. package/src/util/report.ts +0 -44
  144. package/src/util/ui.ts +0 -258
  145. /package/src/{report/path-links.ts → ui/links.ts} +0 -0
  146. /package/src/{report → ui}/terminal-signals.ts +0 -0
@@ -89,14 +89,14 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
89
89
  // reached through a no-output intermediate still poisons the key, so each
90
90
  // dep's accumulated producers fold forward.
91
91
  // - outputProjects: the project names of every upstream task declaring
92
- // cache.outputs.files (project-relative outputs land in the producer's
93
- // own dir), and of every upstream with no cache block that may write
92
+ // outputs.files (project-relative outputs land in the producer's
93
+ // own dir), and of every upstream with no `cache: true` that may write
94
94
  // in its own project (`undeclaredWriteReach`, item 743). A bitset over
95
95
  // project indexes: a union per dep was a string Set copied tasks ×
96
96
  // deps × projects times, 101 ms of a 513 ms warm run at 476 packages
97
97
  // (item 744).
98
- // - wsOutputUpstream: any upstream declares cache.outputs.workspaceFiles
99
- // (root-anchored, boundary-ignoring outputs), or has no cache block
98
+ // - wsOutputUpstream: any upstream declares outputs.workspaceFiles
99
+ // (root-anchored, boundary-ignoring outputs), or has no `cache: true`
100
100
  // and a sandbox write grant elsewhere in the workspace, or may rewrite
101
101
  // a root file a plugin claims (a root `pnpm install`): every key folds
102
102
  // what the claimant took from it, once per run.
@@ -125,7 +125,7 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
125
125
  // of what any one reader's upstream producers declare.
126
126
  const outputsOf = new Map<string, string[]>()
127
127
  for (const n of args.nodes.values()) {
128
- const files = n.config.cache?.outputs.files ?? []
128
+ const files = n.config.outputs?.files ?? []
129
129
  if (files.length === 0) continue
130
130
  const list = outputsOf.get(n.projectName)
131
131
  if (list === undefined) outputsOf.set(n.projectName, [...files])
@@ -185,10 +185,10 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
185
185
  const wideOfDep = wideById.get(dep)
186
186
  if (wideOfDep !== undefined) wideUnion.addAll(wideOfDep)
187
187
  if (wsOutputUpstreamById.get(dep) === true) wsOutputUpstream = true
188
- const depOut = depNode.config.cache?.outputs
188
+ const depOut = depNode.config.outputs
189
189
  if ((depOut?.files?.length ?? 0) > 0) outputs.add(depNode.projectName)
190
190
  if ((depOut?.workspaceFiles?.length ?? 0) > 0) wsOutputUpstream = true
191
- // An uncached producer declares no outputs, yet a same-project reader
191
+ // An uncached producer is held to no outputs, yet a same-project reader
192
192
  // after it (`tasks: []` decouples the key, not the order) read the
193
193
  // bytes it wrote: its key taken up front was the pre-producer one, and
194
194
  // the fourth run of seeds A,B,B,A replayed B over A (turborepo#13788).
@@ -198,7 +198,7 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
198
198
  wideUnion.add(depNode.projectName)
199
199
  } else if (reach === 'workspace') wsOutputUpstream = true
200
200
  if (
201
- depNode.config.cache === undefined &&
201
+ depNode.config.cache !== true &&
202
202
  mayWriteFingerprint(depNode, args.workspaceRoot, args.claimedFiles)
203
203
  )
204
204
  wsOutputUpstream = true
@@ -219,7 +219,7 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
219
219
  for (const dep of anyUnfolded ? node.deps : []) {
220
220
  const depNode = args.nodes.get(dep)
221
221
  if (!depNode) continue
222
- const cached = depNode.config.cache !== undefined
222
+ const cached = depNode.config.cache === true
223
223
  // The cached twin: a formatter rewriting its own input ahead of a
224
224
  // reader that folds no key of it replayed seed B's build under seed A
225
225
  // on the fourth run, the same way (item 750).
@@ -323,7 +323,7 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
323
323
  )
324
324
  if (unstable) unstableById.add(id)
325
325
 
326
- const cacheEnabled = node.config.cache !== undefined
326
+ const cacheEnabled = node.config.cache === true
327
327
  if (cacheEnabled && !unstable) stableKeys.push({ hash, node })
328
328
  // An uncached key stays preliminary behind any upstream that may write
329
329
  // into this project, or anywhere: none of its writes is declared.
@@ -438,7 +438,7 @@ export function gatedOutputs(
438
438
  // 1,000 projects' tasks spent 5-8 ms here re-splitting the same globs.
439
439
  const outputsOf = new Map<string, ProjectOutputs>()
440
440
  for (const n of nodes.values()) {
441
- const files = n.config.cache?.outputs.files
441
+ const files = n.config.outputs?.files
442
442
  if (files === undefined || files.length === 0) continue
443
443
  let po = outputsOf.get(n.projectName)
444
444
  if (po === undefined) outputsOf.set(n.projectName, (po = new ProjectOutputs()))
@@ -554,8 +554,8 @@ export function probesAfterWrites(
554
554
  for (const n of nodes.values()) {
555
555
  if (
556
556
  commandWriteReach(n, workspaceRoot) !== 'none' ||
557
- (n.config.cache?.outputs.files.length ?? 0) > 0 ||
558
- (n.config.cache?.outputs.workspaceFiles?.length ?? 0) > 0
557
+ (n.config.outputs?.files.length ?? 0) > 0 ||
558
+ (n.config.outputs?.workspaceFiles?.length ?? 0) > 0
559
559
  )
560
560
  writers.add(n.id)
561
561
  }
@@ -581,7 +581,7 @@ export function probesAfterWrites(
581
581
  }
582
582
  if (writer) writerBelow.add(id)
583
583
  if (unfolded) unfoldedBelow.add(id)
584
- if (unfolded && node.config.cache !== undefined && probes(node)) late.add(id)
584
+ if (unfolded && node.config.cache === true && probes(node)) late.add(id)
585
585
  }
586
586
  return late
587
587
  }
@@ -628,8 +628,8 @@ function synthUpstream(
628
628
  * Conservative stability check: does any TRANSITIVE-upstream task whose
629
629
  * outputs could land where this task reads its inputs make this task's
630
630
  * key preliminary? The producers are pre-folded by `deriveStableKeys`
631
- * (`upstreamOutputProjects` = projects declaring `cache.outputs.files`
632
- * upstream; `hasWsOutputUpstream` = any `cache.outputs.workspaceFiles`
631
+ * (`upstreamOutputProjects` = projects declaring `outputs.files`
632
+ * upstream; `hasWsOutputUpstream` = any `outputs.workspaceFiles`
633
633
  * upstream), so a producer reached through a no-output intermediate is
634
634
  * caught too.
635
635
  *
@@ -663,7 +663,6 @@ export function dependsOnSiblingOutputs(
663
663
  readableOf?: ReadonlyMap<string, readonly string[]>
664
664
  },
665
665
  ): boolean {
666
- const cache = node.config.cache
667
666
  const declared = node.config.inputs
668
667
  // A task with no inputs has no key to take up front.
669
668
  if (declared === undefined) return false
@@ -697,7 +696,7 @@ export function dependsOnSiblingOutputs(
697
696
  // that producer, its restore raced the producer's clean and extract
698
697
  // (M-5; invariant 2 of docs/design/overlapping-outputs-2026-09.md).
699
698
  // `outputsOf` holds this task's own globs too; set them aside once.
700
- const own = cache?.outputs.files ?? []
699
+ const own = node.config.outputs?.files ?? []
701
700
  if (own.length > 0) {
702
701
  const others = [...outputs]
703
702
  for (const g of own) {
@@ -22,7 +22,7 @@ export interface ArtifactTask {
22
22
  /** Absolute. */
23
23
  readonly projectDir: string
24
24
  readonly command: string
25
- /** Its `cache.outputs`: project-relative `files`, root-relative `workspaceFiles`. */
25
+ /** Its `outputs`: project-relative `files`, root-relative `workspaceFiles`. */
26
26
  readonly outputs: {
27
27
  readonly files: readonly string[]
28
28
  readonly workspaceFiles: readonly string[]
@@ -292,7 +292,7 @@ export async function describeTaskInputs(args: ComputeHashArgs): Promise<{
292
292
  * input changed and changed BACK while the command ran matched its digest
293
293
  * again, and the entry saved the other content's output under the key (item
294
294
  * 1015). A write before it — an upstream's — is judged by content, as before.
295
- * `byBytes` (`inputStability.strategy: 'mutable'`) judges a write during
295
+ * `byBytes` (`inputs.stability.strategy: 'mutable'`) judges a write during
296
296
  * the command by content too, when it answers true for that file and its
297
297
  * ctime: the task says the same bytes are its own identical rewrite
298
298
  * (tsdown's package.json), not an edit undone, unless another command that
@@ -353,14 +353,14 @@ export function taskFilesToken(
353
353
  /** What `resolveInputs` is asked for a task's key. */
354
354
  function inputsArgs(args: ComputeHashArgs): ResolveInputsArgs {
355
355
  const cfg = args.node.config
356
- const cacheCfg = cfg.cache
356
+ const cfgOut = cfg.outputs
357
357
  return {
358
358
  projectDir: args.node.projectDir,
359
359
  workspaceRoot: args.workspaceRoot,
360
360
  envSource: process.env,
361
361
  inputs: cfg.inputs,
362
- ownOutputs: cacheCfg?.outputs.files ?? [],
363
- ownWorkspaceOutputs: cacheCfg?.outputs.workspaceFiles ?? [],
362
+ ownOutputs: cfgOut?.files ?? [],
363
+ ownWorkspaceOutputs: cfgOut?.workspaceFiles ?? [],
364
364
  nestedProjectDirs: args.nestedProjectDirs,
365
365
  ...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
366
366
  ...(args.probesAfterWrites?.has(args.node.id) === true ? { runtimeScope: args.node.id } : {}),
@@ -519,9 +519,23 @@ function hashableConfig(cfg: TaskConfig): unknown {
519
519
  // Keys sorted: moving `cache` above `exec`, or a `define` in another
520
520
  // order, missed every key of the task, and a `project` or `graph` plugin
521
521
  // writes a config the lock never sorted (2026-10-25).
522
- if (cfg.exec?.remote === undefined) return keysSorted(cfg)
523
- const { remote: _remote, ...execRest } = cfg.exec
524
- return keysSorted({ ...cfg, exec: execRest })
522
+ const env = cfg.inputs?.env
523
+ const pass = cfg.exec?.env?.passThrough
524
+ if (cfg.exec?.remote === undefined && sortedNames(env) && sortedNames(pass))
525
+ return keysSorted(cfg)
526
+ const { remote: _remote, ...execRest } = cfg.exec ?? ({} as NonNullable<TaskConfig['exec']>)
527
+ // Env names are sets: each value is read by its name, so their order
528
+ // keys nothing (conformance env/declared-order-irrelevant).
529
+ if (!sortedNames(pass)) execRest.env = { ...execRest.env, passThrough: [...pass!].sort() }
530
+ const out: TaskConfig = cfg.exec === undefined ? { ...cfg } : { ...cfg, exec: execRest }
531
+ if (!sortedNames(env)) out.inputs = { ...cfg.inputs!, env: [...env!].sort() }
532
+ return keysSorted(out)
533
+ }
534
+
535
+ function sortedNames(names: readonly string[] | undefined): boolean {
536
+ if (names === undefined) return true
537
+ for (let i = 1; i < names.length; i++) if (names[i - 1]! > names[i]!) return false
538
+ return true
525
539
  }
526
540
 
527
541
  /**
@@ -16,7 +16,7 @@
16
16
  // what or how tasks run. Contrast `cache`/`executor`, which return objects
17
17
  // core calls INTO; those are the behavior capabilities, kept separate.
18
18
 
19
- import { sampleTrees, type MachineReport } from '../exec/index.js'
19
+ import { sampleTrees, type MachineReport, type WorkerAdvice } from '../exec/index.js'
20
20
  import {
21
21
  type CacheSource,
22
22
  deriveCacheSource,
@@ -301,6 +301,8 @@ export interface RunSummaryRecord {
301
301
  * (`TaskExecutor.idealMs`), beside `totalDurationMs`. Additive.
302
302
  */
303
303
  idealMs?: number
304
+ /** The workers that would have run it best (`TaskExecutor.advice`). Additive. */
305
+ advice?: readonly WorkerAdvice[]
304
306
  }
305
307
 
306
308
  /** One stage of a run, a wall window in epoch ms. */
@@ -334,6 +336,7 @@ export function assembleRunSummary(
334
336
  uploads?: { count: number; bytes: number; ms: number; failed: number }
335
337
  machines?: readonly MachineReport[]
336
338
  idealMs?: number | undefined
339
+ advice?: readonly WorkerAdvice[]
337
340
  },
338
341
  ): RunSummaryRecord {
339
342
  let failedCount = 0
@@ -376,6 +379,7 @@ export function assembleRunSummary(
376
379
  ? { machines: timing.machines }
377
380
  : {}),
378
381
  ...(timing.idealMs !== undefined ? { idealMs: timing.idealMs } : {}),
382
+ ...(timing.advice !== undefined && timing.advice.length > 0 ? { advice: timing.advice } : {}),
379
383
  }
380
384
  }
381
385
 
@@ -14,12 +14,12 @@ import type { Logger } from './logger.js'
14
14
  import type { RunContext } from './summary.js'
15
15
 
16
16
  /**
17
- * A task no cache answered for: it declares no `cache`, or the run's policy
17
+ * A task no cache answered for: it has no `cache: true`, or the run's policy
18
18
  * read and wrote nothing. Its run is `no-cache`, never a miss, on every
19
19
  * surface — `--dry`'s plan, the frame, the footer, the run history.
20
20
  */
21
21
  export function ranNoCache(o: { node: TaskNode; cacheOff?: boolean }): boolean {
22
- return o.node.config.cache === undefined || o.cacheOff === true
22
+ return o.node.config.cache !== true || o.cacheOff === true
23
23
  }
24
24
 
25
25
  /** Payload of the `run:start` event — mirrors the Logger.runStart hook. */
@@ -45,7 +45,7 @@ export interface RunStartInfo {
45
45
  */
46
46
  export type RunEvent =
47
47
  | { kind: 'run:start'; info: RunStartInfo }
48
- | { kind: 'task:start'; node: TaskNode; waitedMs?: number }
48
+ | { kind: 'task:start'; node: TaskNode }
49
49
  | { kind: 'task:stdout'; node: TaskNode; chunk: string }
50
50
  | { kind: 'task:stderr'; node: TaskNode; chunk: string }
51
51
  | { kind: 'task:complete'; node: TaskNode; outcome: TaskOutcome }
@@ -139,12 +139,7 @@ export function busLogger(bus: EventBus): Logger {
139
139
  taskStderr: (node, chunk) => bus.emit({ kind: 'task:stderr', node, chunk }),
140
140
  taskComplete: (node, outcome) => bus.emit({ kind: 'task:complete', node, outcome }),
141
141
  runStart: (info) => bus.emit({ kind: 'run:start', info }),
142
- taskStart: (node, info) =>
143
- bus.emit(
144
- info?.waitedMs === undefined
145
- ? { kind: 'task:start', node }
146
- : { kind: 'task:start', node, waitedMs: info.waitedMs },
147
- ),
142
+ taskStart: (node) => bus.emit({ kind: 'task:start', node }),
148
143
  runEnd: () => {
149
144
  if (ended) return
150
145
  ended = true
@@ -179,10 +174,7 @@ export function terminalSubscriber(sink: Logger): RunEventSubscriber {
179
174
  sink.runStart?.(event.info)
180
175
  return
181
176
  case 'task:start':
182
- sink.taskStart?.(
183
- event.node,
184
- event.waitedMs === undefined ? undefined : { waitedMs: event.waitedMs },
185
- )
177
+ sink.taskStart?.(event.node)
186
178
  return
187
179
  case 'task:stdout':
188
180
  sink.taskStdout(event.node, event.chunk)
@@ -289,7 +281,7 @@ export interface OutcomeView {
289
281
  * and counting groups as executed tasks drifts from the terminal.
290
282
  */
291
283
  isGroup?: boolean
292
- /** Executed with no `cache` block: never consulted the cache, not a miss. */
284
+ /** Executed without `cache: true`: never consulted the cache, not a miss. */
293
285
  noCache?: boolean
294
286
  /** Cache hits only: the exec time the entry was stored with — the work
295
287
  * this hit skipped, as opposed to `durationMs`, the restore it cost. */
@@ -12,8 +12,6 @@ export interface LiveForecast {
12
12
  critical?: string
13
13
  /** Tasks not started because a dependency has not finished: the tasks line's `blocked`. */
14
14
  waiting: number
15
- /** Each running task's p50, where history knows it: the row's expected duration. */
16
- expected?: ReadonlyMap<string, number>
17
15
  /**
18
16
  * Tasks not started because a dependency has not finished, in the order
19
17
  * the scheduler would start them, at most one per worker: what an idle
@@ -21,13 +19,12 @@ export interface LiveForecast {
21
19
  */
22
20
  blocked?: readonly BlockedTask[]
23
21
  /**
24
- * Each running task's count of the tasks downstream of it (a group,
25
- * which runs nothing, looked through): its row's `blocks` cell, why they
26
- * are not running. A task downstream of two running tasks counts under both.
22
+ * Each running task's count of what is downstream of it, the
23
+ * scheduler's own count (`computeReverseDepCount`): its row's `blocks`
24
+ * cell, why they are not running. A node downstream of two running tasks
25
+ * counts under both.
27
26
  */
28
27
  blockedBy: ReadonlyMap<string, number>
29
- /** Each task's usual time (its executed p50), for the results' `±usual`. */
30
- usual?: ReadonlyMap<string, number>
31
28
  }
32
29
 
33
30
  /** A task an unfinished dependency holds back, and what holds it: the running task its longest chain waits on, else that dependency. */
@@ -54,8 +51,8 @@ export function createForecast(
54
51
  nodes: ReadonlyMap<string, TaskNode>,
55
52
  p50s: ReadonlyMap<string, number>,
56
53
  concurrency: number,
57
- /** Each task's count of the tasks downstream of it (`blocksCounts`); computed here when absent. */
58
- downstream: ReadonlyMap<string, number> = blocksCounts(nodes),
54
+ /** Every node's dependents count, the scheduler's baseline and the rows' `blocks`; computed here when absent. */
55
+ rank: ReadonlyMap<string, number> = computeReverseDepCount(nodes),
59
56
  ): Forecast {
60
57
  // Dependencies before dependents, once: each call is one pass over it.
61
58
  const order: TaskNode[] = []
@@ -87,9 +84,8 @@ export function createForecast(
87
84
  if (p !== undefined) cost.set(n.id, p)
88
85
  }
89
86
  const workers = Math.max(1, concurrency)
90
- // The scheduler's own baseline ranking, most dependents first, ties in
91
- // graph order: once per run, as the scheduler computes it.
92
- const rank = computeReverseDepCount(nodes)
87
+ // The scheduler's own baseline ranking (`rank`), most dependents first,
88
+ // ties in graph order.
93
89
  const position = new Map(order.map((n, i) => [n.id, i]))
94
90
  const before = (a: string, b: string): boolean => {
95
91
  const d = (rank.get(a) ?? 0) - (rank.get(b) ?? 0)
@@ -97,12 +93,7 @@ export function createForecast(
97
93
  }
98
94
 
99
95
  return (finished, running, nowMs) => {
100
- const expected = new Map<string, number>()
101
96
  let waiting = 0
102
- for (const id of running.keys()) {
103
- const p = cost.get(id)
104
- if (p !== undefined) expected.set(id, p)
105
- }
106
97
  const end = new Map<string, number>()
107
98
  // The running task each node's longest chain waits on.
108
99
  const head = new Map<string, string>()
@@ -153,30 +144,17 @@ export function createForecast(
153
144
  work += own
154
145
  }
155
146
  for (const id of running.keys()) {
156
- const d = downstream.get(id)
147
+ const d = rank.get(id)
157
148
  if (d !== undefined && d > 0) blockedBy.set(id, d)
158
149
  }
159
- if (!known) return { waiting, expected, blocked: held, blockedBy, usual: cost }
150
+ if (!known) return { waiting, blocked: held, blockedBy }
160
151
  const critical = chainEnd === undefined ? undefined : head.get(chainEnd)
161
152
  return {
162
153
  etaMs: Math.max(chain, work / workers),
163
154
  ...(critical !== undefined ? { critical } : {}),
164
155
  waiting,
165
- expected,
166
156
  blocked: held,
167
157
  blockedBy,
168
- usual: cost,
169
158
  }
170
159
  }
171
160
  }
172
-
173
- /**
174
- * Each task's `blocks`: how many tasks, a group's own looked through and
175
- * none counted, sit downstream of it in the run, each once (2026-10-29:
176
- * every task it holds back, not only the next ones).
177
- */
178
- export function blocksCounts(nodes: ReadonlyMap<string, TaskNode>): Map<string, number> {
179
- const tasks = new Map<string, true>()
180
- for (const n of nodes.values()) if (!isGroupTask(n)) tasks.set(n.id, true)
181
- return computeReverseDepCount(nodes, tasks)
182
- }