@vzn/vx 0.0.649 → 0.0.651

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 (122) hide show
  1. package/README.md +5 -20
  2. package/package.json +5 -5
  3. package/schemas/last.json +15 -0
  4. package/src/bin.ts +9 -31
  5. package/src/cache/cache.ts +225 -51
  6. package/src/cache/file-hashes.ts +3 -0
  7. package/src/cache/fold-memo.ts +8 -1
  8. package/src/cache/git-inputs.ts +188 -39
  9. package/src/cache/index.ts +12 -0
  10. package/src/cache/inputs.ts +3 -0
  11. package/src/cache/layer.ts +11 -0
  12. package/src/cache/layered-cache.ts +4 -0
  13. package/src/cache/linked-inputs.ts +3 -0
  14. package/src/cache/lockfile-memo.ts +120 -0
  15. package/src/cache/output-index.ts +5 -2
  16. package/src/cache/remote-hits.ts +9 -1
  17. package/src/cache/remote-lane.ts +2 -1
  18. package/src/cache/run-history.ts +11 -1
  19. package/src/cache/run-notes.ts +123 -0
  20. package/src/cache/schema.ts +54 -1
  21. package/src/cli/cache.ts +26 -16
  22. package/src/cli/completions.ts +4 -2
  23. package/src/cli/docs.ts +10 -11
  24. package/src/cli/help.ts +12 -23
  25. package/src/cli/index.ts +4 -9
  26. package/src/cli/info.ts +15 -7
  27. package/src/cli/init.ts +23 -15
  28. package/src/cli/last.ts +224 -223
  29. package/src/cli/lock.ts +55 -5
  30. package/src/cli/plan-format.ts +100 -118
  31. package/src/cli/plugin-commands.ts +10 -3
  32. package/src/cli/plugin-templates.ts +2 -2
  33. package/src/cli/run.ts +46 -27
  34. package/src/cli/select.ts +43 -5
  35. package/src/cli/show.ts +38 -42
  36. package/src/cli/task-verb.ts +22 -0
  37. package/src/cli/upgrade.ts +3 -1
  38. package/src/cli/watch-fs.ts +6 -1
  39. package/src/cli/watch-judge.ts +2 -1
  40. package/src/cli/watch.ts +83 -44
  41. package/src/cli/why.ts +126 -92
  42. package/src/cli/workspace-config.ts +2 -1
  43. package/src/exec/executor.ts +2 -1
  44. package/src/exec/kill-tree.ts +58 -5
  45. package/src/exec/local-executor.ts +1 -1
  46. package/src/exec/pipes.ts +152 -0
  47. package/src/exec/runner.ts +199 -182
  48. package/src/exec/sandbox-binds.ts +1 -1
  49. package/src/exec/sandbox-runtime.ts +11 -2
  50. package/src/exec/spawn-off.ts +154 -0
  51. package/src/exec/warm-shell.ts +146 -0
  52. package/src/graph/critical-path.ts +40 -28
  53. package/src/graph/priorities.ts +98 -44
  54. package/src/graph/scheduler.ts +12 -2
  55. package/src/graph/task-graph.ts +27 -1
  56. package/src/handoff.ts +10 -7
  57. package/src/history/history.ts +39 -2
  58. package/src/history/index.ts +3 -0
  59. package/src/history/metrics.ts +9 -1
  60. package/src/index.ts +12 -3
  61. package/src/orchestrator/doctor.ts +3 -3
  62. package/src/orchestrator/execute-task.ts +16 -2
  63. package/src/orchestrator/graph-memo.ts +50 -1
  64. package/src/orchestrator/graph-stage.ts +6 -0
  65. package/src/orchestrator/hit-restore.ts +3 -0
  66. package/src/orchestrator/index.ts +3 -1
  67. package/src/orchestrator/live-usage.ts +98 -0
  68. package/src/orchestrator/locked-digests.ts +63 -0
  69. package/src/orchestrator/lockfile-claim.ts +176 -102
  70. package/src/orchestrator/miss-save.ts +31 -3
  71. package/src/orchestrator/persistent-task.ts +3 -1
  72. package/src/orchestrator/plan.ts +5 -1
  73. package/src/orchestrator/plugin-host.ts +61 -0
  74. package/src/orchestrator/plugin.ts +49 -4
  75. package/src/orchestrator/prepare.ts +18 -2
  76. package/src/orchestrator/projects.ts +4 -1
  77. package/src/orchestrator/run-context.ts +29 -10
  78. package/src/orchestrator/run-failures.ts +35 -114
  79. package/src/orchestrator/run-records.ts +10 -0
  80. package/src/orchestrator/run.ts +89 -26
  81. package/src/orchestrator/sandbox-request.ts +2 -2
  82. package/src/orchestrator/signals.ts +8 -5
  83. package/src/orchestrator/steps.ts +2 -2
  84. package/src/orchestrator/task-hash.ts +17 -3
  85. package/src/report/forecast.ts +12 -33
  86. package/src/report/framed-output.ts +388 -161
  87. package/src/report/index.ts +8 -2
  88. package/src/report/logger.ts +100 -36
  89. package/src/report/status-line.ts +81 -365
  90. package/src/report/summary.ts +104 -131
  91. package/src/ui/ansi.ts +48 -0
  92. package/src/ui/components.ts +506 -0
  93. package/src/ui/index.ts +79 -0
  94. package/src/ui/kit.ts +37 -0
  95. package/src/ui/live-region.ts +363 -0
  96. package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
  97. package/src/ui/terminal.ts +51 -0
  98. package/src/ui/theme.ts +224 -0
  99. package/src/ui/version-line.ts +28 -0
  100. package/src/util/config-frame.ts +65 -10
  101. package/src/util/edit-distance.ts +6 -1
  102. package/src/util/hidden-prompt.ts +8 -2
  103. package/src/util/index.ts +2 -15
  104. package/src/util/paths.ts +20 -2
  105. package/src/util/secret-mask.ts +39 -3
  106. package/src/util/secrets.ts +3 -0
  107. package/src/util/settle.ts +31 -0
  108. package/src/util/timing.ts +4 -16
  109. package/src/workspace/affected.ts +89 -30
  110. package/src/workspace/config-schema.ts +4 -2
  111. package/src/workspace/dependencies-lock.ts +191 -0
  112. package/src/workspace/filter.ts +28 -0
  113. package/src/workspace/index.ts +9 -0
  114. package/src/workspace/lockfile.ts +159 -6
  115. package/src/workspace/migration.ts +4 -3
  116. package/src/workspace/workspace.ts +3 -0
  117. package/src/orchestrator/run-steps.ts +0 -93
  118. package/src/util/colors.ts +0 -130
  119. package/src/util/report.ts +0 -44
  120. package/src/util/ui.ts +0 -258
  121. /package/src/{report/path-links.ts → ui/links.ts} +0 -0
  122. /package/src/{report → ui}/terminal-signals.ts +0 -0
package/src/index.ts CHANGED
@@ -66,8 +66,15 @@ export { run, planRun, prepareRun } from './orchestrator/index.js'
66
66
  // (`@vzn/vx-history` does; a plugin package reaches core only
67
67
  // through this façade, which is what put these here).
68
68
  export { currentBranch } from './orchestrator/index.js'
69
- export { LocalHistoryProvider } from './history/index.js'
70
- export type { HistoryProvider, HistoryTable, TaskHistory, TaskResources } from './history/index.js'
69
+ export { localHistory } from './history/index.js'
70
+ export type {
71
+ HistoryProvider,
72
+ HistoryTable,
73
+ LocalHistory,
74
+ LocalHistoryOptions,
75
+ TaskHistory,
76
+ TaskResources,
77
+ } from './history/index.js'
71
78
  export type { PreparedRun } from './orchestrator/index.js'
72
79
  // `RunOptions.affected`: what `--affected`'s diff changed, per project.
73
80
  export type { AffectedChanges } from './workspace/index.js'
@@ -138,7 +145,7 @@ export {
138
145
  taskNameProblem,
139
146
  } from './workspace/index.js'
140
147
  // Output components: a plugin draws its lines with core's, so they look alike.
141
- export { ui } from './util/index.js'
148
+ export { ui } from './ui/index.js'
142
149
  export type {
143
150
  ApplyMigrationArgs,
144
151
  MigrationFormat,
@@ -179,10 +186,12 @@ export type {
179
186
  CacheFacts,
180
187
  TaskArtifacts,
181
188
  FingerprintChange,
189
+ FingerprintDigests,
182
190
  FingerprintClaim,
183
191
  FingerprintContext,
184
192
  GraphHookContext,
185
193
  KeyHookContext,
194
+ LockedFile,
186
195
  PluginCommand,
187
196
  ProjectHookContext,
188
197
  ScheduleHookContext,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import os from 'node:os'
9
9
  import path from 'node:path'
10
- import { Cache, CACHE_VERSION, SCHEMA_VERSION } from '../cache/index.js'
10
+ import { indexDb, Cache, CACHE_VERSION, SCHEMA_VERSION } from '../cache/index.js'
11
11
  import { PLUGIN_HOOKS, type WorkspaceConfig } from '../config.js'
12
12
  import {
13
13
  cgroupCpuQuota,
@@ -19,8 +19,8 @@ import {
19
19
  machineMemoryBytes,
20
20
  machineParallelism,
21
21
  treeBytes,
22
- ui,
23
22
  } from '../util/index.js'
23
+ import * as ui from '../ui/index.js'
24
24
  import { vxPath } from '../handoff.js'
25
25
  import { VERSION } from '../version.js'
26
26
  import { probeSandbox, resetSandbox, untracedReason } from '../exec/index.js'
@@ -245,7 +245,7 @@ async function collectWorkspaceInfo(
245
245
  indexStore = cache.storeDir
246
246
  stats = cache.stats()
247
247
  orphans = await cache.orphanStats()
248
- flaky = flakyTasks(cache.dbHandle())
248
+ flaky = flakyTasks(indexDb(cache))
249
249
  } finally {
250
250
  cache.close()
251
251
  }
@@ -72,6 +72,7 @@ import {
72
72
  bracketLiteralHint,
73
73
  markUnsaved,
74
74
  saveMiss,
75
+ unsavedOutputBytes,
75
76
  type OutputDirSnapshot,
76
77
  type SaveFacts,
77
78
  } from './miss-save.js'
@@ -1347,6 +1348,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
1347
1348
  let movedFails: string | undefined
1348
1349
  let own: Awaited<ReturnType<typeof ownOutputs>>
1349
1350
  let saved = false
1351
+ // An unsaved miss's outputs, as a save's are counted: its row's `outputs`.
1352
+ let unsavedBytes: number | undefined
1350
1353
  if (
1351
1354
  effectiveExitCode === 0 &&
1352
1355
  willSave &&
@@ -1408,7 +1411,17 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
1408
1411
  // entry is in; without one, the save is taken as done.
1409
1412
  if (args.deferredSaves !== undefined) args.deferredSaves.set(node.id, landed)
1410
1413
  else saved = true
1411
- } else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants !== true) {
1414
+ } else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants === true) {
1415
+ // Nothing reads after it, so nothing to mark: only its row's `outputs`.
1416
+ if (effectiveExitCode === 0)
1417
+ unsavedBytes = await unsavedOutputBytes({
1418
+ node,
1419
+ workspaceRoot: args.workspaceRoot,
1420
+ nestedProjectDirs: args.nestedProjectDirs,
1421
+ outputs,
1422
+ wsOutputs,
1423
+ })
1424
+ } else if (cfgCacheable && !remoteOnly && !deferralRequested) {
1412
1425
  // Ran here and saves nothing — failed, a read-only policy, a tainted
1413
1426
  // upstream, a key that no longer held — yet it wrote what a save would
1414
1427
  // have marked, and may have rewritten an input the re-check before a
@@ -1422,7 +1435,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
1422
1435
  if (moved !== undefined) {
1423
1436
  forgetUndeclaredWrites(args, wsOutputs.length > 0 ? 'workspace' : 'project')
1424
1437
  } else {
1425
- await markUnsaved({
1438
+ unsavedBytes = await markUnsaved({
1426
1439
  node,
1427
1440
  workspaceRoot: args.workspaceRoot,
1428
1441
  nestedProjectDirs: args.nestedProjectDirs,
@@ -1572,6 +1585,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
1572
1585
  ...(unkeyed ? { unkeyed: true as const } : {}),
1573
1586
  ...(keylessBy !== undefined ? { keylessBy } : {}),
1574
1587
  ...(saved ? { saved: true as const } : {}),
1588
+ ...(unsavedBytes !== undefined ? { outputBytes: unsavedBytes } : {}),
1575
1589
  ...(effectiveExitCode !== 0 && output !== undefined
1576
1590
  ? { failedOutput: failedOutputLog(output, willSave || args.keepRun !== undefined, storeMask) }
1577
1591
  : {}),
@@ -12,7 +12,12 @@
12
12
  // it is never remembered.
13
13
 
14
14
  import type { FoldMemo } from '../cache/index.js'
15
- import { buildTaskGraph, declaredTask, type TaskNode } from '../graph/index.js'
15
+ import {
16
+ buildTaskGraph,
17
+ computeReverseDepCount,
18
+ declaredTask,
19
+ type TaskNode,
20
+ } from '../graph/index.js'
16
21
  import { xxh3hex } from '../util/index.js'
17
22
  import type { ProjectMeta } from '../workspace/index.js'
18
23
  import { codeIdentity } from './run-keys.js'
@@ -138,3 +143,47 @@ function rebuilt(
138
143
  }
139
144
  return nodes
140
145
  }
146
+
147
+ /**
148
+ * The run's dependents count, the scheduler's baseline and every row's
149
+ * `blocks`, read back with the graph it was counted on: the closure over a
150
+ * 9,603-task graph (11k nodes) was ~100 ms of every warm run, the same each
151
+ * time (2026-10-29). `token` names the graph's edges (`memoGraph`, undefined
152
+ * once anything edited them), the node ids which of them this run kept
153
+ * (`--affected` prunes under the same token).
154
+ */
155
+ export function graphCounts(
156
+ nodes: ReadonlyMap<string, TaskNode>,
157
+ folds: FoldMemo | undefined,
158
+ token: string | undefined,
159
+ ): Map<string, number> {
160
+ if (folds === undefined || token === undefined) return computeReverseDepCount(nodes)
161
+ // Not `counts`: those rows held a second count the rows no longer show.
162
+ const id = `\0dependents\0${token}`
163
+ const probe = xxh3hex([...nodes.keys()].join('\0'))
164
+ const row = folds.get(id, probe)
165
+ if (row !== undefined) {
166
+ const back = countsOf(row, nodes)
167
+ if (back !== undefined) return back
168
+ }
169
+ const counts = computeReverseDepCount(nodes)
170
+ folds.set(id, probe, JSON.stringify([...nodes.keys()].map((k) => counts.get(k)!)))
171
+ return counts
172
+ }
173
+
174
+ function countsOf(
175
+ row: string,
176
+ nodes: ReadonlyMap<string, TaskNode>,
177
+ ): Map<string, number> | undefined {
178
+ let all: number[]
179
+ try {
180
+ all = JSON.parse(row) as number[]
181
+ } catch {
182
+ return undefined
183
+ }
184
+ if (all?.length !== nodes.size) return undefined
185
+ const counts = new Map<string, number>()
186
+ let i = 0
187
+ for (const id of nodes.keys()) counts.set(id, all[i++]!)
188
+ return counts
189
+ }
@@ -29,6 +29,12 @@ export async function applyGraphStage(
29
29
  // keyed and cleaned its outputs there: past the hard project boundary.
30
30
  const dirs = new Map(args.projectMetas.map((m) => [m.name, path.resolve(m.dir)]))
31
31
  const shown = (dir: string): string => relPosix(args.workspaceRoot, dir) || '.'
32
+ // The lock shares a task config two tasks declare alike: an edit in place
33
+ // works on this node's own copy.
34
+ if (plugins.some((p) => p.graph !== undefined)) {
35
+ for (const n of nodes.values())
36
+ if (!isDefaultBuild(n.config)) n.config = structuredClone(n.config)
37
+ }
32
38
  await applyGraphHooks(
33
39
  plugins,
34
40
  nodes,
@@ -458,6 +458,9 @@ function hitOutcome(restore: RestoreHitArgs, restored: boolean): TaskOutcome {
458
458
  ...(hit.cpuMs !== undefined ? { storedCpuMs: hit.cpuMs } : {}),
459
459
  ...(hit.peakRssBytes !== undefined ? { storedPeakRssBytes: hit.peakRssBytes } : {}),
460
460
  ...(hit.sizeBytes !== undefined ? { artifactBytes: hit.sizeBytes } : {}),
461
+ ...(hit.outputRows !== undefined
462
+ ? { outputBytes: hit.outputRows.reduce((n, r) => n + r.size, 0) }
463
+ : {}),
461
464
  ...(hit.fetchMs !== undefined ? { fetchMs: hit.fetchMs } : {}),
462
465
  hash,
463
466
  restored: restored || hit.source === 'remote',
@@ -60,6 +60,7 @@ export {
60
60
  type CommandContext,
61
61
  type GraphHookContext,
62
62
  type KeyHookContext,
63
+ type LockedFile,
63
64
  type PluginCommand,
64
65
  type ProjectHookContext,
65
66
  type ScheduleHookContext,
@@ -78,6 +79,7 @@ export {
78
79
  type RunReport,
79
80
  type RunReportTask,
80
81
  type FingerprintChange,
82
+ type FingerprintDigests,
81
83
  type FingerprintClaim,
82
84
  type FingerprintContext,
83
85
  type VxPlugin,
@@ -95,6 +97,7 @@ export {
95
97
  applyScheduleHooks,
96
98
  claimedAffected,
97
99
  claimedExtraFiles,
100
+ claimedDigests,
98
101
  fingerprintClaims,
99
102
  hasHook,
100
103
  REMOTE_CACHE_METHODS,
@@ -146,7 +149,6 @@ export {
146
149
  type TaskLog,
147
150
  taskLog,
148
151
  } from './run-failures.js'
149
- export { type RunStep, runSteps, runWarnings } from './run-steps.js'
150
152
  export { resolveRunId, shortRunId } from './run-id.js'
151
153
  export { collectInfo, type CollectInfoOptions, type InfoFacts } from './doctor.js'
152
154
  export { localFloor, type LocalFloor } from './local-floor.js'
@@ -0,0 +1,98 @@
1
+ // The live list's `cpu` and `mem`: each running task's process tree's
2
+ // average cores since it started and its peak memory so far, the values
3
+ // its results row shows once it ends (2026-10-29: the live row showed the
4
+ // last half second's, and a task's row changed meaning as it finished).
5
+ // Looked at on a timer only once the list asks for a row, so a run with
6
+ // no live list (a pipe, CI) registers pids and reads nothing.
7
+ // The look is async and off the dispatch path: a slow one skips a tick.
8
+
9
+ import { sampleTrees, type TreeUsage } from '../exec/index.js'
10
+ import type { LiveUsage } from '../report/index.js'
11
+
12
+ /** How often a tracked tree is looked at while the live list shows. */
13
+ const LOOK_MS = 500
14
+
15
+ type Every = (ms: number, fn: () => Promise<void>) => () => void
16
+
17
+ const everyInterval: Every = (ms, fn) => {
18
+ const timer = setInterval(() => void fn(), ms)
19
+ timer.unref()
20
+ return () => clearInterval(timer)
21
+ }
22
+
23
+ interface Tracked {
24
+ pid: number
25
+ startedAt: number
26
+ /** The most CPU time the tree has shown: a descendant that exits takes its share out of the sum. */
27
+ cpuMs: number
28
+ peakRssBytes: number
29
+ now?: LiveUsage
30
+ }
31
+
32
+ export function createLiveUsage(
33
+ opts: {
34
+ every?: Every
35
+ now?: () => number
36
+ sample?: (pids: number[]) => Promise<Map<number, TreeUsage>>
37
+ } = {},
38
+ ): {
39
+ track(taskId: string, pid: number): () => void
40
+ usage(taskId: string): LiveUsage | undefined
41
+ } {
42
+ const every = opts.every ?? everyInterval
43
+ const now = opts.now ?? Date.now
44
+ const sample = opts.sample ?? sampleTrees
45
+ const tracked = new Map<string, Tracked>()
46
+ let asked = false
47
+ let looking = false
48
+ let stop: (() => void) | undefined
49
+
50
+ const look = async (): Promise<void> => {
51
+ if (looking) return
52
+ looking = true
53
+ try {
54
+ const entries = [...tracked]
55
+ const usage = await sample(entries.map(([, t]) => t.pid))
56
+ const ts = now()
57
+ for (const [id, t] of entries) {
58
+ const u = usage.get(t.pid)
59
+ if (u === undefined || tracked.get(id) !== t) continue
60
+ t.cpuMs = Math.max(t.cpuMs, u.cpuMs)
61
+ t.peakRssBytes = Math.max(t.peakRssBytes, u.rssBytes)
62
+ if (ts > t.startedAt) {
63
+ const cores = t.cpuMs / (ts - t.startedAt)
64
+ t.now = { cores: Math.round(cores * 100) / 100, rssBytes: t.peakRssBytes }
65
+ }
66
+ }
67
+ } catch {
68
+ // A failed look is a missing point, never a broken run.
69
+ } finally {
70
+ looking = false
71
+ }
72
+ }
73
+ const start = (): void => {
74
+ if (asked && stop === undefined && tracked.size > 0) stop = every(LOOK_MS, look)
75
+ }
76
+
77
+ return {
78
+ track(taskId, pid) {
79
+ const t: Tracked = { pid, startedAt: now(), cpuMs: 0, peakRssBytes: 0 }
80
+ tracked.set(taskId, t)
81
+ start()
82
+ return () => {
83
+ if (tracked.get(taskId) === t) tracked.delete(taskId)
84
+ if (tracked.size === 0 && stop !== undefined) {
85
+ stop()
86
+ stop = undefined
87
+ }
88
+ }
89
+ },
90
+ usage(taskId) {
91
+ if (!asked) {
92
+ asked = true
93
+ start()
94
+ }
95
+ return tracked.get(taskId)?.now
96
+ },
97
+ }
98
+ }
@@ -0,0 +1,63 @@
1
+ // A run keys a claimed file from what `vx lock` recorded of it in
2
+ // `.vx/dependencies.lock.json`, as it takes configs from the config lock:
3
+ // the digests are read, never derived, so no run parses a lockfile for
4
+ // its key. The record holds only while the file's bytes hash to what was
5
+ // recorded: one that moved (an install) refuses the run before any key is
6
+ // folded, naming the file and `vx lock`. A present file with no record,
7
+ // or no lock at all, refuses when a claimant asks for it: an absent record
8
+ // is never read as "no dependencies". The other files the digests read
9
+ // (`extras`: a `link:` directory, a patch) are handed to the claimant with
10
+ // their recorded hashes: a linked directory is edited with no install, so
11
+ // one that moved is the claimant's to digest anew, not a refusal.
12
+
13
+ import {
14
+ DEPENDENCIES_LOCK_NAME,
15
+ type DependenciesLock,
16
+ lockKey,
17
+ parseDependenciesLock,
18
+ recordedDigests,
19
+ } from '../workspace/index.js'
20
+ import { UserError, xxh3hex } from '../util/index.js'
21
+ import type { KeyHookContext, LockedFile } from './plugin.js'
22
+
23
+ const refused = (message: string): UserError => new UserError(message, 'VX_E_LOCK')
24
+
25
+ /**
26
+ * `KeyHookContext.locked` for one run, over the claimed files' bytes as
27
+ * the run read them (`readClaimedFiles`; absent ones are missing) and the
28
+ * lock's bytes (null when there is none). Each recorded claimed file is
29
+ * checked first: one whose bytes moved, or that is gone, throws here.
30
+ */
31
+ export function lockedFiles(
32
+ workspaceRoot: string,
33
+ claimed: readonly string[],
34
+ bytes: ReadonlyMap<string, Uint8Array>,
35
+ lockBytes: Uint8Array | null,
36
+ ): KeyHookContext['locked'] {
37
+ const lock: DependenciesLock | null = parseDependenciesLock(lockBytes)
38
+ const records = new Map<string, LockedFile>()
39
+ for (const file of claimed) {
40
+ const rec = lock === null ? undefined : recordedDigests(lock, file)
41
+ if (rec === undefined) continue
42
+ const now = bytes.get(file)
43
+ if (now === undefined || xxh3hex(now) !== rec.hash) {
44
+ throw refused(`${file} changed since \`vx lock\`: run \`vx lock\``)
45
+ }
46
+ records.set(file, {
47
+ extras: rec.extras,
48
+ digest(projectDir) {
49
+ const dir = lockKey(workspaceRoot, projectDir)
50
+ if (!rec.has(dir))
51
+ throw refused(`${dir} is not in ${DEPENDENCIES_LOCK_NAME}: run \`vx lock\``)
52
+ return rec.of(dir)
53
+ },
54
+ })
55
+ }
56
+ return async (file) => {
57
+ const rec = records.get(file)
58
+ if (rec !== undefined || !bytes.has(file)) return rec
59
+ if (lockBytes === null) throw refused(`no ${DEPENDENCIES_LOCK_NAME}: run \`vx lock\``)
60
+ if (lock === null) throw refused(`${DEPENDENCIES_LOCK_NAME} is invalid, run \`vx lock\``)
61
+ throw refused(`${file} is not in ${DEPENDENCIES_LOCK_NAME}: run \`vx lock\``)
62
+ }
63
+ }