@vzn/vx 0.0.16 → 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 +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- 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 +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- 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/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- 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 +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
// Git-backed input enumeration: one `git ls-files -s` + one `git status
|
|
2
|
+
// --porcelain -z` per run (plus `check-attr` where a filter could convert
|
|
3
|
+
// bytes), parsed into the per-project `GitFilesCache` that `inputs.ts`'s
|
|
4
|
+
// glob resolution trusts for blob OIDs. Split from `inputs.ts` on
|
|
5
|
+
// 2026-09-10: this file talks to git; `inputs.ts` decides which files a
|
|
6
|
+
// task declared. Nothing here reads a config or applies a boundary.
|
|
7
|
+
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { UserError } from '../util/index.js'
|
|
11
|
+
|
|
12
|
+
export class GitFilesCache extends Map<string, readonly string[]> {
|
|
13
|
+
private changed = new Map<string, string[]>()
|
|
14
|
+
/**
|
|
15
|
+
* Per-project trusted index OIDs: absolute path → git blob OID, for
|
|
16
|
+
* tracked regular files whose working-tree state matched the index
|
|
17
|
+
* at populate time (per one `git status --porcelain` snapshot).
|
|
18
|
+
* These feed `Cache.key` via `CacheKeyInput.fileHashes` so a clean
|
|
19
|
+
* tree derives input hashes with zero reads / stats / SQLite.
|
|
20
|
+
*
|
|
21
|
+
* Dropped wholesale on `set` / `delete`: a mid-run re-enumeration
|
|
22
|
+
* proves the project's tree changed, and index OIDs can't be
|
|
23
|
+
* re-trusted without a fresh status — the per-file fallback
|
|
24
|
+
* (`Cache.hashFile`) computes the identical blob OID from disk, so
|
|
25
|
+
* dropping is a pure perf concession, never a correctness one.
|
|
26
|
+
*/
|
|
27
|
+
private oids = new Map<string, Map<string, string>>()
|
|
28
|
+
/**
|
|
29
|
+
* Set when `populateGitFilesCache` stored a WORKSPACE-WIDE partition
|
|
30
|
+
* (any loaded task declares `inputs.workspaceFiles`). Null when the
|
|
31
|
+
* feature is unused — every workspace-partition hook below is then a
|
|
32
|
+
* no-op, so unused-feature behavior stays byte-identical.
|
|
33
|
+
*/
|
|
34
|
+
private wsRoot: string | null = null
|
|
35
|
+
/**
|
|
36
|
+
* Whether the worktree had any uncommitted changes at populate time,
|
|
37
|
+
* derived from the SAME `git status --porcelain` spawn that prunes
|
|
38
|
+
* dirty paths from the trusted-OID set. Lets the Tier-3 invocation
|
|
39
|
+
* record report `dirty` without a SECOND status spawn. `null` until
|
|
40
|
+
* populate runs, or when the status spawn failed (non-repo).
|
|
41
|
+
*/
|
|
42
|
+
private dirty: boolean | null = null
|
|
43
|
+
|
|
44
|
+
setWorkspaceRoot(root: string): void {
|
|
45
|
+
this.wsRoot = root
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Aggregate worktree dirtiness from the populate-time status spawn. */
|
|
49
|
+
get worktreeDirty(): boolean | null {
|
|
50
|
+
return this.dirty
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setWorktreeDirty(dirty: boolean | null): void {
|
|
54
|
+
this.dirty = dirty
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
markOutputsChanged(projectDir: string, relPaths: readonly string[]): void {
|
|
58
|
+
this.recordChanged(projectDir, relPaths)
|
|
59
|
+
// The workspace-wide partition sees the same files under
|
|
60
|
+
// root-relative names; forward so a downstream workspaceFiles
|
|
61
|
+
// task can't reuse a snapshot its globs could now contradict.
|
|
62
|
+
if (this.wsRoot !== null && this.wsRoot !== projectDir) {
|
|
63
|
+
this.recordChanged(
|
|
64
|
+
this.wsRoot,
|
|
65
|
+
relPaths.map((rel) =>
|
|
66
|
+
path.relative(this.wsRoot!, path.resolve(projectDir, rel)).split(path.sep).join('/'),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Record root-anchored changed paths (cleaned/restored workspace
|
|
74
|
+
* outputs) against EVERY partition that can see them: the workspace
|
|
75
|
+
* partition under their root-relative names, and any project
|
|
76
|
+
* partition whose dir contains them (workspace outputs may land
|
|
77
|
+
* inside other projects' dirs — the no-boundary escape hatch).
|
|
78
|
+
*/
|
|
79
|
+
markWorkspaceOutputsChanged(workspaceRoot: string, relPaths: readonly string[]): void {
|
|
80
|
+
if (relPaths.length === 0) return
|
|
81
|
+
for (const key of this.keys()) {
|
|
82
|
+
if (key === workspaceRoot) {
|
|
83
|
+
this.recordChanged(key, relPaths)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
const under: string[] = []
|
|
87
|
+
for (const rel of relPaths) {
|
|
88
|
+
const abs = path.resolve(workspaceRoot, rel)
|
|
89
|
+
if (abs.startsWith(key + path.sep)) {
|
|
90
|
+
under.push(path.relative(key, abs).split(path.sep).join('/'))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (under.length > 0) this.recordChanged(key, under)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Drop the workspace-wide partition (if one exists). Called after a
|
|
99
|
+
* cache-miss save — an executed task may have written undeclared
|
|
100
|
+
* files anywhere in its project dir, and the workspace partition
|
|
101
|
+
* spans that subtree; only a fresh enumeration can see them. No-op
|
|
102
|
+
* when the feature is unused.
|
|
103
|
+
*/
|
|
104
|
+
invalidateWorkspacePartition(): void {
|
|
105
|
+
if (this.wsRoot !== null) this.delete(this.wsRoot)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private recordChanged(partitionDir: string, relPaths: readonly string[]): void {
|
|
109
|
+
if (!this.has(partitionDir)) return
|
|
110
|
+
const cur = this.changed.get(partitionDir)
|
|
111
|
+
if (cur) cur.push(...relPaths)
|
|
112
|
+
else this.changed.set(partitionDir, [...relPaths])
|
|
113
|
+
// These paths just changed on disk; their index OIDs (if any) no
|
|
114
|
+
// longer describe the working-tree content.
|
|
115
|
+
const partitionOids = this.oids.get(partitionDir)
|
|
116
|
+
if (partitionOids) {
|
|
117
|
+
for (const rel of relPaths) partitionOids.delete(path.resolve(partitionDir, rel))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Trusted index OIDs for a project (abs path → oid), if any survive. */
|
|
122
|
+
oidsFor(projectDir: string): ReadonlyMap<string, string> | undefined {
|
|
123
|
+
return this.oids.get(projectDir)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setOids(projectDir: string, oids: Map<string, string>): void {
|
|
127
|
+
this.oids.set(projectDir, oids)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Snapshot if still valid for these input globs; undefined → re-spawn. */
|
|
131
|
+
snapshotFor(projectDir: string, inputGlobs: readonly Bun.Glob[]): readonly string[] | undefined {
|
|
132
|
+
const snap = this.get(projectDir)
|
|
133
|
+
if (snap === undefined) return undefined
|
|
134
|
+
const pending = this.changed.get(projectDir)
|
|
135
|
+
if (pending !== undefined && pending.some((p) => inputGlobs.some((g) => g.match(p)))) {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
return snap
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override set(key: string, value: readonly string[]): this {
|
|
142
|
+
this.changed.delete(key)
|
|
143
|
+
this.oids.delete(key)
|
|
144
|
+
return super.set(key, value)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override delete(key: string): boolean {
|
|
148
|
+
this.changed.delete(key)
|
|
149
|
+
this.oids.delete(key)
|
|
150
|
+
return super.delete(key)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** No glob metacharacter — the entry names one exact path. */
|
|
155
|
+
|
|
156
|
+
interface GitLsResult {
|
|
157
|
+
/** cwd-relative paths — same visibility set as `--cached --others`. */
|
|
158
|
+
files: string[]
|
|
159
|
+
/**
|
|
160
|
+
* cwd-relative path → index blob OID, for tracked regular files
|
|
161
|
+
* (mode 100644 / 100755) and symlinks (120000) at stage 0 only. A
|
|
162
|
+
* symlink's OID is the blob of its target string, which is exactly
|
|
163
|
+
* what `Cache.hashFile` computes for one, so the two paths agree.
|
|
164
|
+
* Merge-conflict stages and gitlinks are excluded. NOT yet filtered
|
|
165
|
+
* by working-tree dirtiness — callers intersect with `git status`
|
|
166
|
+
* before trusting.
|
|
167
|
+
*/
|
|
168
|
+
oids: Map<string, string>
|
|
169
|
+
/** Paths flagged skip-worktree / assume-unchanged (only when `-v` was passed). */
|
|
170
|
+
flagged: Set<string>
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const LS_FILES_STAGE_RE = /^(?:([A-Za-z]) )?([0-7]{6}) ([0-9a-f]{40,64}) ([0-3])\t/
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Run `git ls-files -s --others --exclude-standard -z .` in `cwd`.
|
|
177
|
+
* One spawn yields BOTH the file list (identical visibility to the
|
|
178
|
+
* pre-v20 `--cached --others` form, verified empirically: same set
|
|
179
|
+
* including staged-but-deleted files and per-stage conflict
|
|
180
|
+
* duplicates) AND each tracked file's index OID — the heart of the
|
|
181
|
+
* Turbo-parity "hashes come from git's index" fast path. Throws a
|
|
182
|
+
* `UserError` when git is unavailable or `cwd` isn't a git work
|
|
183
|
+
* tree; vx requires git. `-z` survives filenames with newlines /
|
|
184
|
+
* spaces.
|
|
185
|
+
*/
|
|
186
|
+
export function runGitLsFiles(cwd: string): GitLsResult {
|
|
187
|
+
let proc
|
|
188
|
+
try {
|
|
189
|
+
proc = Bun.spawnSync({
|
|
190
|
+
cmd: ['git', 'ls-files', '-s', '--others', '--exclude-standard', '-z', '.'],
|
|
191
|
+
cwd,
|
|
192
|
+
stdout: 'pipe',
|
|
193
|
+
stderr: 'pipe',
|
|
194
|
+
})
|
|
195
|
+
} catch {
|
|
196
|
+
throw new UserError(
|
|
197
|
+
`vx requires git: failed to spawn 'git' (working dir: ${cwd}). Install git and re-run.`,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
if (proc.exitCode !== 0) {
|
|
201
|
+
// Exit 128 = not a git work tree; other non-zero = git failure.
|
|
202
|
+
// Either way we can't enumerate inputs reliably.
|
|
203
|
+
const stderr = new TextDecoder().decode(proc.stderr).trim()
|
|
204
|
+
throw new UserError(
|
|
205
|
+
`vx requires git: ${cwd} is not inside a git work tree. ` +
|
|
206
|
+
`Run 'git init' in your workspace root.${stderr ? ` (git: ${stderr})` : ''}`,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
return parseLsFilesOutput(new TextDecoder().decode(proc.stdout))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Each `ls-files -s -v` record is `[<flag> ]<mode> <oid> <stage>\t<path>` —
|
|
213
|
+
// the staged-entry form, with an optional cache-state letter (`H`, `S`,
|
|
214
|
+
// `h`, …) in front. `--others` paths print bare; with `-z`, core.quotePath
|
|
215
|
+
// quoting is off, so a bare path containing a literal tab still cannot
|
|
216
|
+
// match the fixed-form prefix. Both answers come from ONE spawn.
|
|
217
|
+
function parseLsFilesOutput(out: string): GitLsResult {
|
|
218
|
+
const files: string[] = []
|
|
219
|
+
const oids = new Map<string, string>()
|
|
220
|
+
const flagged = new Set<string>()
|
|
221
|
+
if (out.length === 0) return { files, oids, flagged }
|
|
222
|
+
// NUL-separated; trailing NUL produces an empty segment we skip.
|
|
223
|
+
for (const record of out.split('\0')) {
|
|
224
|
+
if (record.length === 0) continue
|
|
225
|
+
const m = LS_FILES_STAGE_RE.exec(record)
|
|
226
|
+
if (m === null) {
|
|
227
|
+
files.push(record) // --others entry: bare path
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
const filePath = record.slice(m[0].length)
|
|
231
|
+
files.push(filePath)
|
|
232
|
+
const mode = m[2]!
|
|
233
|
+
const stage = m[4]!
|
|
234
|
+
if ((mode === '100644' || mode === '100755' || mode === '120000') && stage === '0') {
|
|
235
|
+
oids.set(filePath, m[3]!)
|
|
236
|
+
}
|
|
237
|
+
// A LOWERCASE letter means skip-worktree or assume-unchanged (`S` is the
|
|
238
|
+
// older spelling of skip-worktree): git has been told to stop looking at
|
|
239
|
+
// the worktree file, so its index OID says nothing about the disk.
|
|
240
|
+
const flag = m[1]
|
|
241
|
+
if (flag !== undefined && (flag === 'S' || (flag >= 'a' && flag <= 'z'))) flagged.add(filePath)
|
|
242
|
+
}
|
|
243
|
+
return { files, oids, flagged }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** One completed `git` invocation. */
|
|
247
|
+
interface GitRun {
|
|
248
|
+
exitCode: number
|
|
249
|
+
stdout: string
|
|
250
|
+
stderr: string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse `git check-attr -z text eol ident` output — a flat stream of
|
|
255
|
+
* `<path>\0<attr>\0<value>\0` triples — into the set of paths where a clean
|
|
256
|
+
* filter can rewrite bytes. `unspecified` means no rule matched and
|
|
257
|
+
* `unset` (`-text`) explicitly disables conversion; both leave the index blob
|
|
258
|
+
* byte-identical to the worktree file, so those OIDs stay trusted.
|
|
259
|
+
*/
|
|
260
|
+
export function parseCheckAttrOutput(out: string): Set<string> {
|
|
261
|
+
const affected = new Set<string>()
|
|
262
|
+
const fields = out.split('\0')
|
|
263
|
+
for (let i = 0; i + 2 < fields.length; i += 3) {
|
|
264
|
+
const value = fields[i + 2]!
|
|
265
|
+
if (value !== 'unspecified' && value !== 'unset') affected.add(fields[i]!)
|
|
266
|
+
}
|
|
267
|
+
return affected
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Remove from `trusted` every path whose index blob may differ from its
|
|
272
|
+
* worktree bytes because a clean filter applies.
|
|
273
|
+
*
|
|
274
|
+
* Gated in two steps so the common case pays NOTHING. The precise answer —
|
|
275
|
+
* `git ls-files --eol`, comparing `i/` to `w/` — was measured at 240 ms on a
|
|
276
|
+
* 15k-file tree because it must READ every worktree file: 13x the entire
|
|
277
|
+
* enumeration, on a run whose warm total is ~130 ms, and in the common Linux
|
|
278
|
+
* case it finds nothing. So instead:
|
|
279
|
+
*
|
|
280
|
+
* 1. If `core.autocrlf` is true/input, conversion applies to every
|
|
281
|
+
* auto-detected text file with no attribute needed — trust nothing.
|
|
282
|
+
* 2. Else, if no attributes source exists anywhere (no in-tree
|
|
283
|
+
* `.gitattributes`, no `$GIT_DIR/info/attributes`, no
|
|
284
|
+
* `core.attributesFile`), no rule can name a filter — return untouched,
|
|
285
|
+
* zero extra work. This is the default `git init` repo.
|
|
286
|
+
* 3. Otherwise ask `git check-attr` (measured 21 ms; it resolves attributes
|
|
287
|
+
* from the index WITHOUT reading worktree content) and drop only the
|
|
288
|
+
* paths that actually carry `text`/`eol`/`ident`.
|
|
289
|
+
*
|
|
290
|
+
* Never throws: a probe that fails leaves the map as-is, which is exactly the
|
|
291
|
+
* behaviour before this gate existed.
|
|
292
|
+
*/
|
|
293
|
+
async function dropFilteredOids(
|
|
294
|
+
trusted: Map<string, string>,
|
|
295
|
+
args: {
|
|
296
|
+
workspaceRoot: string
|
|
297
|
+
gitDir: string
|
|
298
|
+
coreConfig: string
|
|
299
|
+
spawnGit: (a: string[], stdin?: string) => Promise<GitRun | null>
|
|
300
|
+
},
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
if (trusted.size === 0) return
|
|
303
|
+
if (autocrlfConverts(args.coreConfig)) {
|
|
304
|
+
trusted.clear()
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
// Cheap checks first, and nothing is materialized until one of them fires —
|
|
308
|
+
// on a 15k-file repo with no attributes this whole function is a size check,
|
|
309
|
+
// a substring scan of an empty string, one stat, and a key walk that exits
|
|
310
|
+
// on the first `.gitattributes` it does not find.
|
|
311
|
+
let attributesPossible =
|
|
312
|
+
args.coreConfig.toLowerCase().includes('core.attributesfile') ||
|
|
313
|
+
(args.gitDir !== '' &&
|
|
314
|
+
existsSync(path.resolve(args.workspaceRoot, args.gitDir, 'info', 'attributes')))
|
|
315
|
+
if (!attributesPossible) {
|
|
316
|
+
for (const rel of trusted.keys()) {
|
|
317
|
+
if (rel === '.gitattributes' || rel.endsWith('/.gitattributes')) {
|
|
318
|
+
attributesPossible = true
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!attributesPossible) return
|
|
324
|
+
|
|
325
|
+
const res = await args.spawnGit(
|
|
326
|
+
['check-attr', '--stdin', '-z', 'text', 'eol', 'ident'],
|
|
327
|
+
[...trusted.keys()].join('\0'),
|
|
328
|
+
)
|
|
329
|
+
if (res === null || res.exitCode !== 0) return
|
|
330
|
+
for (const rel of parseCheckAttrOutput(res.stdout)) trusted.delete(rel)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** `true` when git may rewrite bytes for EVERY auto-detected text file. */
|
|
334
|
+
export function autocrlfConverts(coreConfig: string): boolean {
|
|
335
|
+
for (const line of coreConfig.split('\n')) {
|
|
336
|
+
const [name, value] = [line.slice(0, line.indexOf(' ')), line.slice(line.indexOf(' ') + 1)]
|
|
337
|
+
if (name.toLowerCase() === 'core.autocrlf') {
|
|
338
|
+
const v = value.trim().toLowerCase()
|
|
339
|
+
return v === 'true' || v === 'input'
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseStatusOutput(out: string): { dirty: Set<string>; untracked: string[] } {
|
|
346
|
+
const tokens = out.split('\0')
|
|
347
|
+
const dirty = new Set<string>()
|
|
348
|
+
const untracked: string[] = []
|
|
349
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
350
|
+
const token = tokens[i]!
|
|
351
|
+
if (token.length < 4) continue
|
|
352
|
+
if (token[0] === '?') {
|
|
353
|
+
untracked.push(token.slice(3))
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
dirty.add(token.slice(3))
|
|
357
|
+
// X = R or C → the next token is the rename/copy source path.
|
|
358
|
+
const x = token[0]
|
|
359
|
+
if ((x === 'R' || x === 'C') && i + 1 < tokens.length && tokens[i + 1]!.length > 0) {
|
|
360
|
+
i++
|
|
361
|
+
dirty.add(tokens[i]!)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return { dirty, untracked }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Strip a repo→workspace `--show-prefix` (e.g. `code/`) from each path, keeping
|
|
369
|
+
* only paths inside the workspace — so a repo-root-relative set (from `git
|
|
370
|
+
* status`/`diff`) is re-keyed workspace-relative to match `git ls-files`. Empty
|
|
371
|
+
* prefix → returns the set unchanged (workspace root IS the git root).
|
|
372
|
+
*/
|
|
373
|
+
function stripPrefixFromSet(paths: Set<string>, prefix: string): Set<string> {
|
|
374
|
+
if (prefix === '') return paths
|
|
375
|
+
const out = new Set<string>()
|
|
376
|
+
for (const p of paths) if (p.startsWith(prefix)) out.add(p.slice(prefix.length))
|
|
377
|
+
return out
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Run `git ls-files` ONCE at the workspace root, then partition the
|
|
382
|
+
* result by project. Populates `cache` for every project in
|
|
383
|
+
* `projectDirs` with project-relative path lists matching what a
|
|
384
|
+
* per-project spawn would have produced. Throws `UserError` if the
|
|
385
|
+
* workspace isn't a git work tree (vx requires git).
|
|
386
|
+
*
|
|
387
|
+
* Why bulk: each spawn costs ~5-10ms (fork+exec). On a 200-project
|
|
388
|
+
* workspace that's 1-2s of pure overhead reclaimed.
|
|
389
|
+
*
|
|
390
|
+
* Files in nested-project subtrees stay in their parent's list — the
|
|
391
|
+
* boundary-ignore globs in `resolveFiles` filter them out the same way
|
|
392
|
+
* they did before. Cheaper to filter once-per-task than to subtract
|
|
393
|
+
* here.
|
|
394
|
+
*
|
|
395
|
+
* v20: the same `ls-files -s` spawn also yields each tracked file's
|
|
396
|
+
* index OID. A second spawn (`git status --porcelain`) prunes paths
|
|
397
|
+
* whose working tree diverges from the index; what survives is
|
|
398
|
+
* stored per project via `cache.setOids` and feeds `Cache.key`
|
|
399
|
+
* directly — clean-tree input hashing costs zero reads/stats/SQLite.
|
|
400
|
+
*
|
|
401
|
+
* `workspaceWide`: set when any loaded task declares
|
|
402
|
+
* `cache.inputs.workspaceFiles`. Disables pathspec scoping (those
|
|
403
|
+
* globs must see every file from the root) and additionally stores a
|
|
404
|
+
* workspace-wide partition keyed by `workspaceRoot` (files + trusted
|
|
405
|
+
* OIDs), which `resolveWorkspaceFiles` consumes. When false, the
|
|
406
|
+
* enumeration behavior — pathspecs, spawn count, stored partitions —
|
|
407
|
+
* is byte-identical to the pre-workspaceFiles code.
|
|
408
|
+
*/
|
|
409
|
+
export async function populateGitFilesCache(
|
|
410
|
+
workspaceRoot: string,
|
|
411
|
+
projectDirs: readonly string[],
|
|
412
|
+
cache: GitFilesCache,
|
|
413
|
+
workspaceWide = false,
|
|
414
|
+
): Promise<void> {
|
|
415
|
+
const enumeration = await startGitEnumeration(
|
|
416
|
+
workspaceRoot,
|
|
417
|
+
gitPathspecs(workspaceRoot, projectDirs, workspaceWide),
|
|
418
|
+
)
|
|
419
|
+
applyGitEnumeration(enumeration, workspaceRoot, projectDirs, cache, workspaceWide)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** What one workspace-wide enumeration learned, before it is partitioned per project. */
|
|
423
|
+
export interface GitEnumeration {
|
|
424
|
+
/** Every enumerated path, workspace-relative, tracked and untracked. */
|
|
425
|
+
all: string[]
|
|
426
|
+
/** Workspace-relative path → index OID, for files whose worktree state matched the index. */
|
|
427
|
+
trusted: Map<string, string>
|
|
428
|
+
/** Whether the worktree had uncommitted changes; null when `git status` failed. */
|
|
429
|
+
dirty: boolean | null
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Pathspec scoping: when the run only needs a handful of projects
|
|
434
|
+
* (scoped config loading), let git scan just those dirs — 75 ms →
|
|
435
|
+
* 11 ms on an 11k-file repo. Above 64 dirs (or when a project IS
|
|
436
|
+
* the root) the whole-tree scan wins on arg/exec overhead anyway.
|
|
437
|
+
*/
|
|
438
|
+
export function gitPathspecs(
|
|
439
|
+
workspaceRoot: string,
|
|
440
|
+
projectDirs: readonly string[],
|
|
441
|
+
workspaceWide: boolean,
|
|
442
|
+
): string[] {
|
|
443
|
+
const rels = projectDirs.map((d) => path.relative(workspaceRoot, d).split(path.sep).join('/'))
|
|
444
|
+
const scoped =
|
|
445
|
+
!workspaceWide &&
|
|
446
|
+
rels.length > 0 &&
|
|
447
|
+
rels.length <= 64 &&
|
|
448
|
+
rels.every((r) => r !== '' && r !== '.')
|
|
449
|
+
return scoped ? rels : ['.']
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* The spawn half of `populateGitFilesCache`, separated so an UNSCOPED run
|
|
454
|
+
* can start it before the configs are evaluated — the enumeration needs
|
|
455
|
+
* only the pathspecs, and `['.']` is right for every unscoped run — and
|
|
456
|
+
* overlap ~60 ms of git with ~80 ms of config evaluation on a
|
|
457
|
+
* 1000-project tree instead of paying them back to back.
|
|
458
|
+
*/
|
|
459
|
+
export async function startGitEnumeration(
|
|
460
|
+
workspaceRoot: string,
|
|
461
|
+
pathspecs: readonly string[],
|
|
462
|
+
): Promise<GitEnumeration> {
|
|
463
|
+
// The spawns are independent — run them concurrently so the
|
|
464
|
+
// bulk-populate costs max(ls-files, status) wall time, not the sum
|
|
465
|
+
// (status alone is ~74 ms on a 1000-project tree; serial spawning
|
|
466
|
+
// was a measurable warm-path regression vs the pre-OID code).
|
|
467
|
+
const spawnGit = async (args: string[], stdin?: string): Promise<GitRun | null> => {
|
|
468
|
+
try {
|
|
469
|
+
const proc = Bun.spawn({
|
|
470
|
+
cmd: ['git', ...args],
|
|
471
|
+
cwd: workspaceRoot,
|
|
472
|
+
stdin: stdin === undefined ? 'ignore' : new TextEncoder().encode(stdin),
|
|
473
|
+
stdout: 'pipe',
|
|
474
|
+
stderr: 'pipe',
|
|
475
|
+
})
|
|
476
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
477
|
+
new Response(proc.stdout).text(),
|
|
478
|
+
new Response(proc.stderr).text(),
|
|
479
|
+
proc.exited,
|
|
480
|
+
])
|
|
481
|
+
return { exitCode, stdout, stderr }
|
|
482
|
+
} catch {
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Four spawns, one worktree walk. `ls-files -s -v` reads the INDEX only
|
|
487
|
+
// (~9 ms on a 1000-project tree) and answers two questions at once: every
|
|
488
|
+
// tracked path's OID and its cache-state flag. `status -uall` is the one
|
|
489
|
+
// command that walks the worktree, and it answers two as well: which
|
|
490
|
+
// tracked paths are dirty AND which files are untracked. Asking
|
|
491
|
+
// `ls-files --others` for the untracked set walked the same tree a second
|
|
492
|
+
// time (~50 ms of CPU, concurrent with status but contending with it).
|
|
493
|
+
const [ls, status, prefixRes, coreCfg] = await Promise.all([
|
|
494
|
+
spawnGit(['ls-files', '-s', '-v', '-z', '--', ...pathspecs]),
|
|
495
|
+
spawnGit(['status', '--porcelain', '-z', '-uall', '--', ...pathspecs]),
|
|
496
|
+
// Two answers from one spawn. `--show-prefix` is the repo→workspace path
|
|
497
|
+
// (empty when the workspace root IS the git root): `ls-files` prints
|
|
498
|
+
// cwd(workspace)-relative paths but `status` prints repo-root-relative
|
|
499
|
+
// ones, so when the workspace root is a SUBDIR of the git repo the two
|
|
500
|
+
// disagree; this lets us key both the same way below. `--git-dir` locates
|
|
501
|
+
// `info/attributes` for the filter gate (it is not always `.git/` — a
|
|
502
|
+
// linked worktree's `.git` is a FILE pointing elsewhere).
|
|
503
|
+
spawnGit(['rev-parse', '--show-prefix', '--git-dir']),
|
|
504
|
+
// Reads three config keys, no tree scan — the gate for whether a clean
|
|
505
|
+
// filter can rewrite bytes between the index and the worktree. Exits 1
|
|
506
|
+
// when none are set, which is the common case and means "no gate".
|
|
507
|
+
spawnGit(['config', '--get-regexp', '^core\\.(autocrlf|eol|attributesfile)$']),
|
|
508
|
+
])
|
|
509
|
+
if (ls === null) {
|
|
510
|
+
throw new UserError(
|
|
511
|
+
`vx requires git: failed to spawn 'git' (working dir: ${workspaceRoot}). Install git and re-run.`,
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
if (ls.exitCode !== 0) {
|
|
515
|
+
const stderr = ls.stderr.trim()
|
|
516
|
+
throw new UserError(
|
|
517
|
+
`vx requires git: ${workspaceRoot} is not inside a git work tree. ` +
|
|
518
|
+
`Run 'git init' in your workspace root.${stderr ? ` (git: ${stderr})` : ''}`,
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
const { files: tracked, oids, flagged } = parseLsFilesOutput(ls.stdout)
|
|
522
|
+
// Normalize `status`'s repo-root-relative paths to workspace-relative (strip
|
|
523
|
+
// the `--show-prefix`) so the dirty set is keyed identically to the trusted
|
|
524
|
+
// OID map. Without this, when the workspace root is a git subdir, a modified
|
|
525
|
+
// tracked file is never pruned from `trusted` and keeps its committed OID —
|
|
526
|
+
// a STALE cache hit serving old outputs. Empty prefix (workspace == git root,
|
|
527
|
+
// the common case) is a zero-cost no-op. Paths above the workspace can't be
|
|
528
|
+
// inputs, so they drop out of the set.
|
|
529
|
+
// One spawn, two lines: `--show-prefix` then `--git-dir`.
|
|
530
|
+
const revLines =
|
|
531
|
+
prefixRes !== null && prefixRes.exitCode === 0 ? prefixRes.stdout.split('\n') : []
|
|
532
|
+
const gitPrefix = (revLines[0] ?? '').trim()
|
|
533
|
+
const gitDir = (revLines[1] ?? '').trim()
|
|
534
|
+
const parsedStatus =
|
|
535
|
+
status !== null && status.exitCode === 0 ? parseStatusOutput(status.stdout) : null
|
|
536
|
+
const dirty = parsedStatus === null ? null : stripPrefixFromSet(parsedStatus.dirty, gitPrefix)
|
|
537
|
+
// Untracked files are inputs too (a new file is the commonest edit there
|
|
538
|
+
// is). They come from the status walk, repo-root-relative like the dirty
|
|
539
|
+
// set. Without a status answer the enumeration is the index alone.
|
|
540
|
+
const untracked =
|
|
541
|
+
parsedStatus === null ? [] : [...stripPrefixFromSet(new Set(parsedStatus.untracked), gitPrefix)]
|
|
542
|
+
const all = untracked.length === 0 ? tracked : tracked.concat(untracked)
|
|
543
|
+
// Aggregate dirtiness for the Tier-3 invocation record — derived from
|
|
544
|
+
// this same status spawn so `run()` needs no second `git status`.
|
|
545
|
+
// null when the status spawn failed (non-repo / git error).
|
|
546
|
+
const worktreeDirty = dirty === null ? null : dirty.size > 0
|
|
547
|
+
const trusted = dirty === null ? new Map<string, string>() : oids
|
|
548
|
+
if (dirty !== null) {
|
|
549
|
+
for (const rel of dirty) trusted.delete(rel)
|
|
550
|
+
}
|
|
551
|
+
// A skip-worktree / assume-unchanged path sits at stage 0 and `git status`
|
|
552
|
+
// reports nothing for it, so it would otherwise keep a trusted OID — and
|
|
553
|
+
// resolveFiles SKIPS its existence probe for OID-carrying paths. A sparse
|
|
554
|
+
// checkout would then count a file that is not on disk as an input, so
|
|
555
|
+
// materializing it later changes no key and the old artifact is replayed.
|
|
556
|
+
// Dropping the OID sends these back through the probe, where they correctly
|
|
557
|
+
// fall out of the input set while unmaterialized.
|
|
558
|
+
for (const rel of flagged) trusted.delete(rel)
|
|
559
|
+
// An index OID is only the file's content hash when git stores the worktree
|
|
560
|
+
// bytes VERBATIM. Under a clean filter (`text`/`eol`/`ident`) the blob is a
|
|
561
|
+
// DIFFERENT sequence of bytes — the LF-normalized form — while the task
|
|
562
|
+
// reads the CRLF worktree file. `git status` compares AFTER filtering, so
|
|
563
|
+
// such a file reports clean and keeps its OID: the CRLF and LF states then
|
|
564
|
+
// fold the SAME key and a real content change is invisible.
|
|
565
|
+
//
|
|
566
|
+
// Dropping an OID is not over-invalidation. It routes the path to
|
|
567
|
+
// `hashFile`, which hashes the worktree bytes — the source that was correct
|
|
568
|
+
// all along. The only cost is the read, so the gate below is about paying it
|
|
569
|
+
// ONLY where a filter can actually apply.
|
|
570
|
+
await dropFilteredOids(trusted, {
|
|
571
|
+
workspaceRoot,
|
|
572
|
+
gitDir,
|
|
573
|
+
coreConfig: coreCfg !== null && coreCfg.exitCode === 0 ? coreCfg.stdout : '',
|
|
574
|
+
spawnGit,
|
|
575
|
+
})
|
|
576
|
+
return { all, trusted, dirty: worktreeDirty }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** The partition half of `populateGitFilesCache`: store per-project slices of one enumeration. */
|
|
580
|
+
export function applyGitEnumeration(
|
|
581
|
+
enumeration: GitEnumeration,
|
|
582
|
+
workspaceRoot: string,
|
|
583
|
+
projectDirs: readonly string[],
|
|
584
|
+
cache: GitFilesCache,
|
|
585
|
+
workspaceWide = false,
|
|
586
|
+
): void {
|
|
587
|
+
const { all, trusted } = enumeration
|
|
588
|
+
cache.setWorktreeDirty(enumeration.dirty)
|
|
589
|
+
// Sort once, then each project's files are a contiguous range found
|
|
590
|
+
// by binary search on its `dir/` prefix — O((F+P) log F) instead of
|
|
591
|
+
// the O(P·F) per-project startsWith scan (54 ms at 1090 projects ×
|
|
592
|
+
// ~9k files; ~5 ms this way). '/' sorts below most filename chars,
|
|
593
|
+
// so the range [prefix, prefix+'\xff…') is contiguous in the sorted
|
|
594
|
+
// array; lowerBound on `prefix` and on `prefix + ''` bracket it.
|
|
595
|
+
const sorted = [...all].sort()
|
|
596
|
+
const lowerBound = (key: string): number => {
|
|
597
|
+
let lo = 0
|
|
598
|
+
let hi = sorted.length
|
|
599
|
+
while (lo < hi) {
|
|
600
|
+
const mid = (lo + hi) >>> 1
|
|
601
|
+
if (sorted[mid]! < key) lo = mid + 1
|
|
602
|
+
else hi = mid
|
|
603
|
+
}
|
|
604
|
+
return lo
|
|
605
|
+
}
|
|
606
|
+
for (const projectDir of projectDirs) {
|
|
607
|
+
const relPrefix = path.relative(workspaceRoot, projectDir).split(path.sep).join('/')
|
|
608
|
+
if (relPrefix === '' || relPrefix === '.') {
|
|
609
|
+
cache.set(projectDir, all)
|
|
610
|
+
const rootOids = new Map<string, string>()
|
|
611
|
+
for (const [rel, oid] of trusted) rootOids.set(path.join(workspaceRoot, rel), oid)
|
|
612
|
+
cache.setOids(projectDir, rootOids)
|
|
613
|
+
continue
|
|
614
|
+
}
|
|
615
|
+
const prefix = `${relPrefix}/`
|
|
616
|
+
const start = lowerBound(prefix)
|
|
617
|
+
const end = lowerBound(`${prefix}`)
|
|
618
|
+
const matches: string[] = []
|
|
619
|
+
const projOids = new Map<string, string>()
|
|
620
|
+
for (let i = start; i < end; i++) {
|
|
621
|
+
const rel = sorted[i]!
|
|
622
|
+
matches.push(rel.slice(prefix.length))
|
|
623
|
+
const oid = trusted.get(rel)
|
|
624
|
+
if (oid !== undefined) projOids.set(path.join(workspaceRoot, rel), oid)
|
|
625
|
+
}
|
|
626
|
+
// setOids AFTER set — set() drops the project's OID slot.
|
|
627
|
+
cache.set(projectDir, matches)
|
|
628
|
+
cache.setOids(projectDir, projOids)
|
|
629
|
+
}
|
|
630
|
+
if (workspaceWide) {
|
|
631
|
+
const rootOids = new Map<string, string>()
|
|
632
|
+
for (const [rel, oid] of trusted) rootOids.set(path.join(workspaceRoot, rel), oid)
|
|
633
|
+
cache.set(workspaceRoot, all)
|
|
634
|
+
cache.setOids(workspaceRoot, rootOids)
|
|
635
|
+
cache.setWorkspaceRoot(workspaceRoot)
|
|
636
|
+
}
|
|
637
|
+
}
|
package/src/cache/index.ts
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
1
|
// Module contract. Cross-module imports must come through here; see
|
|
2
2
|
// docs/design/module-isolation-2026-06.md and tests/module-boundaries.test.ts.
|
|
3
|
-
// tar.ts
|
|
3
|
+
// archive.ts / tar-stream.ts / zstd.ts are internal — artifact pack, extract
|
|
4
|
+
// and framing are implementation details; the layer CONTRACT is layer.ts,
|
|
5
|
+
// re-exported through cache.ts.
|
|
4
6
|
|
|
5
7
|
export {
|
|
6
8
|
Cache,
|
|
7
9
|
type CacheEntry,
|
|
10
|
+
type CacheKeyInput,
|
|
8
11
|
type CacheLayer,
|
|
9
12
|
type CachePolicy,
|
|
13
|
+
EXECUTED_RUNS_SQL,
|
|
10
14
|
FULL_CACHE_POLICY,
|
|
11
15
|
type InvocationRecord,
|
|
16
|
+
KEYED_RUNS_SQL,
|
|
12
17
|
parseCachePolicy,
|
|
13
18
|
type RunRecord,
|
|
14
19
|
type TaskInputRow,
|
|
15
20
|
WORKSPACE_OUTPUT_PREFIX,
|
|
21
|
+
type OutputDirRow,
|
|
22
|
+
OUTPUT_DIRS_CAP,
|
|
23
|
+
FILE_HASH_RACY_MS,
|
|
24
|
+
OUTPUT_DIRS_RACY_MS,
|
|
25
|
+
noteSchemaReset,
|
|
26
|
+
type SchemaReset,
|
|
27
|
+
CACHE_VERSION,
|
|
28
|
+
SCHEMA_VERSION,
|
|
16
29
|
} from './cache.js'
|
|
17
|
-
export { type CASBackend, FsCASBackend, MemoryCASBackend } from './cas-backend.js'
|
|
18
|
-
export { type Digest, digestEqual, digestString, makeDigest, parseDigest } from './digest.js'
|
|
19
30
|
export {
|
|
20
31
|
cleanOutputs,
|
|
21
32
|
cleanWorkspaceOutputs,
|
|
22
|
-
GitFilesCache,
|
|
23
|
-
populateGitFilesCache,
|
|
24
33
|
resolveInputs,
|
|
25
34
|
resolveOutputs,
|
|
26
35
|
resolveWorkspaceOutputs,
|
|
36
|
+
asTrees,
|
|
37
|
+
type WorkspaceFilesCache,
|
|
27
38
|
} from './inputs.js'
|
|
39
|
+
export {
|
|
40
|
+
GitFilesCache,
|
|
41
|
+
applyGitEnumeration,
|
|
42
|
+
gitPathspecs,
|
|
43
|
+
startGitEnumeration,
|
|
44
|
+
type GitEnumeration,
|
|
45
|
+
} from './git-inputs.js'
|
|
28
46
|
export { LayeredCache, type RemoteCacheLayer } from './layered-cache.js'
|
|
47
|
+
export { ChainedCache } from './chained-cache.js'
|