@vzn/vx 0.0.641 → 0.0.642
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/package.json +5 -12
- package/skills/vx/SKILL.md +4 -4
- package/src/cache/archive.ts +525 -104
- package/src/cache/cache.ts +92 -12
- package/src/cache/fold-memo.ts +58 -0
- package/src/cache/git-inputs.ts +213 -31
- package/src/cache/index.ts +6 -2
- package/src/cache/inputs.ts +169 -79
- package/src/cache/key-fold.ts +90 -69
- package/src/cache/layer.ts +2 -6
- package/src/cache/layered-cache.ts +68 -4
- package/src/cache/output-index.ts +25 -4
- package/src/cache/remote-lane.ts +27 -10
- package/src/cache/schema.ts +13 -3
- package/src/cache/scoped-cache.ts +3 -1
- package/src/cache/umask.ts +29 -0
- package/src/cli/docs.ts +71 -7
- package/src/cli/help.ts +4 -4
- package/src/cli/plan-format.ts +4 -1
- package/src/cli/run.ts +10 -3
- package/src/cli/select.ts +2 -0
- package/src/exec/runner.ts +5 -0
- package/src/exec/sandbox-runtime.ts +81 -1
- package/src/graph/priorities.ts +14 -1
- package/src/history/history.ts +43 -33
- package/src/index.ts +10 -0
- package/src/orchestrator/deferred-outputs.ts +3 -2
- package/src/orchestrator/execute-task.ts +20 -14
- package/src/orchestrator/hit-restore.ts +10 -2
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/lockfile-claim.ts +54 -8
- package/src/orchestrator/miss-save.ts +27 -17
- package/src/orchestrator/output-host.ts +9 -1
- package/src/orchestrator/plan.ts +4 -0
- package/src/orchestrator/plugin-host.ts +31 -0
- package/src/orchestrator/plugin.ts +29 -3
- package/src/orchestrator/prepare.ts +74 -5
- package/src/orchestrator/projects.ts +6 -1
- package/src/orchestrator/run.ts +37 -15
- package/src/orchestrator/sandbox-request.ts +1 -48
- package/src/orchestrator/save-lane.ts +17 -10
- package/src/orchestrator/signals.ts +7 -10
- package/src/orchestrator/stable-keys.ts +83 -25
- package/src/orchestrator/task-hash.ts +46 -12
- package/src/report/events.ts +12 -0
- package/src/report/framed-output.ts +2 -1
- package/src/report/logger.ts +53 -13
- package/src/report/status-line.ts +45 -4
- package/src/report/summary.ts +5 -2
- package/src/util/errors.ts +1 -1
- package/src/util/hidden-prompt.ts +74 -0
- package/src/util/index.ts +12 -2
- package/src/util/paths.ts +33 -14
- package/src/util/secrets.ts +150 -0
- package/src/workspace/affected.ts +63 -13
- package/src/workspace/config-schema.ts +37 -3
- package/src/workspace/project-loader.ts +1 -0
- package/src/workspace/workspace.ts +17 -11
- package/docs/caching.md +0 -2376
- package/docs/cli.md +0 -3285
- package/docs/execution.md +0 -676
- package/docs/features.md +0 -178
- package/docs/patterns.md +0 -219
- package/docs/schema.md +0 -2468
- package/docs/security.md +0 -253
- package/src/cli/docs-corpus.ts +0 -21
- package/src/cli/docs-text.d.ts +0 -5
package/src/cache/git-inputs.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
// 2026-09-10: this file talks to git; `inputs.ts` decides which files a
|
|
6
6
|
// task declared. Nothing here reads a config or applies a boundary.
|
|
7
7
|
|
|
8
|
+
import os from 'node:os'
|
|
8
9
|
import path from 'node:path'
|
|
9
10
|
import {
|
|
10
11
|
existsSync,
|
|
11
12
|
lstatSync,
|
|
13
|
+
mkdtempSync,
|
|
12
14
|
readdirSync,
|
|
13
15
|
readFileSync,
|
|
14
16
|
realpathSync,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
type Stats,
|
|
20
22
|
} from 'node:fs'
|
|
21
23
|
import {
|
|
24
|
+
checkIgnoreSync,
|
|
22
25
|
executablePath,
|
|
23
26
|
gitEnv,
|
|
24
27
|
gitSpawnRefusal,
|
|
@@ -30,6 +33,9 @@ import {
|
|
|
30
33
|
} from '../util/index.js'
|
|
31
34
|
import { FILE_HASH_RACY_MS, racyWindowMs } from './layer.js'
|
|
32
35
|
|
|
36
|
+
/** A name no ignore rule names, standing for a file a task may create. */
|
|
37
|
+
const FRESH_NAME = '__vx_probe__'
|
|
38
|
+
|
|
33
39
|
/** Three facts of the repository a directory is in: read off the disk, or one `git rev-parse`. */
|
|
34
40
|
export interface RepoFacts {
|
|
35
41
|
/** `--show-prefix`: the directory's path below the worktree root, `''` at the root. */
|
|
@@ -195,6 +201,47 @@ function configObjectFormat(config: string): 'sha1' | 'sha256' | undefined {
|
|
|
195
201
|
return format
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
interface PartitionDigest {
|
|
205
|
+
snapshot: readonly string[]
|
|
206
|
+
oids: OidSlot | undefined
|
|
207
|
+
digest: string
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A partition's trusted OIDs, built on first read: a warm run whose keys
|
|
212
|
+
* come back from the fold memo reads none, and building 125,000 abs-path
|
|
213
|
+
* entries up front was ~90 ms of a 5,000-project warm run.
|
|
214
|
+
*/
|
|
215
|
+
class OidSlot {
|
|
216
|
+
private built: Map<string, string> | undefined
|
|
217
|
+
/** Paths dropped before the map was built: taken out when it is. */
|
|
218
|
+
private dropped: string[] | undefined
|
|
219
|
+
constructor(private build: () => Map<string, string>) {}
|
|
220
|
+
get map(): Map<string, string> {
|
|
221
|
+
if (this.built === undefined) {
|
|
222
|
+
this.built = this.build()
|
|
223
|
+
for (const p of this.dropped ?? []) this.built.delete(p)
|
|
224
|
+
this.dropped = undefined
|
|
225
|
+
}
|
|
226
|
+
return this.built
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Forget these paths' OIDs. An unbuilt map stays unbuilt: each restore
|
|
230
|
+
* marks its outputs changed, and building the map to drop them was ~28 ms
|
|
231
|
+
* of a 1,000-project restore whose keys no later read needed.
|
|
232
|
+
*/
|
|
233
|
+
drop(paths: Iterable<string>): void {
|
|
234
|
+
if (this.built !== undefined) for (const p of paths) this.built.delete(p)
|
|
235
|
+
else (this.dropped ??= []).push(...paths)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface IgnoredListing {
|
|
240
|
+
root: string
|
|
241
|
+
paths: readonly string[]
|
|
242
|
+
asked: Map<string, boolean>
|
|
243
|
+
}
|
|
244
|
+
|
|
198
245
|
export class GitFilesCache extends Map<string, readonly string[]> {
|
|
199
246
|
private changed = new Map<string, string[]>()
|
|
200
247
|
/**
|
|
@@ -210,7 +257,9 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
210
257
|
* (`Cache.hashFile`) computes the identical blob OID from disk, so
|
|
211
258
|
* dropping is a pure perf concession, never a correctness one.
|
|
212
259
|
*/
|
|
213
|
-
private oids = new Map<string,
|
|
260
|
+
private oids = new Map<string, OidSlot>()
|
|
261
|
+
/** Per partition, `filesDigest`'s answer and the snapshot and OIDs it describes. */
|
|
262
|
+
private digests = new Map<string, PartitionDigest>()
|
|
214
263
|
/**
|
|
215
264
|
* Set when `populateGitFilesCache` stored a WORKSPACE-WIDE partition
|
|
216
265
|
* (any loaded task declares `inputs.workspaceFiles`). Null when the
|
|
@@ -236,8 +285,11 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
236
285
|
* What git tracks, by absolute path, within `trackedScopes`: a file there
|
|
237
286
|
* no clean deletes (`committedIn`). Filled by the
|
|
238
287
|
* enumeration, and per directory on demand for one it did not cover.
|
|
288
|
+
* Built on the first question: a run that cleans nothing asks none, and
|
|
289
|
+
* 125,000 paths joined and hashed were ~90 ms of a warm 5,000-project run.
|
|
239
290
|
*/
|
|
240
291
|
private tracked = new Set<string>()
|
|
292
|
+
private trackedPending: (() => Iterable<string>)[] = []
|
|
241
293
|
private trackedScopes: string[] = []
|
|
242
294
|
/**
|
|
243
295
|
* Under `core.ignorecase` (macOS's default volume) git's `Foo.txt` is the
|
|
@@ -256,10 +308,23 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
256
308
|
}
|
|
257
309
|
|
|
258
310
|
noteTracked(scopes: readonly string[], absPaths: readonly string[]): void {
|
|
259
|
-
|
|
311
|
+
this.noteTrackedLater(scopes, () => absPaths)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** `noteTracked`, the paths asked for only when a clean asks about one. */
|
|
315
|
+
noteTrackedLater(scopes: readonly string[], absPaths: () => Iterable<string>): void {
|
|
316
|
+
this.trackedPending.push(absPaths)
|
|
260
317
|
this.trackedScopes.push(...scopes)
|
|
261
318
|
}
|
|
262
319
|
|
|
320
|
+
private isTracked(abs: string): boolean {
|
|
321
|
+
if (this.trackedPending.length > 0) {
|
|
322
|
+
for (const paths of this.trackedPending.splice(0))
|
|
323
|
+
for (const p of paths()) this.tracked.add(this.trackedKey(p))
|
|
324
|
+
}
|
|
325
|
+
return this.tracked.has(this.trackedKey(abs))
|
|
326
|
+
}
|
|
327
|
+
|
|
263
328
|
/**
|
|
264
329
|
* Whether git tracks a file under `dir`: committed (or `git add`ed)
|
|
265
330
|
* files are the user's, never cleaned (owner, 2026-10-14: "never
|
|
@@ -277,7 +342,7 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
277
342
|
if (!covered) this.noteTracked([dir], listTracked(dir))
|
|
278
343
|
}
|
|
279
344
|
for (let p = abs; p.length > dir.length; p = path.dirname(p))
|
|
280
|
-
if (this.
|
|
345
|
+
if (this.isTracked(p)) return true
|
|
281
346
|
return false
|
|
282
347
|
}
|
|
283
348
|
}
|
|
@@ -296,6 +361,46 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
296
361
|
*/
|
|
297
362
|
ignoredOutputs: ReadonlySet<string> = new Set()
|
|
298
363
|
|
|
364
|
+
/** What `git status --ignored=matching` named, root-relative and sorted, and the root. */
|
|
365
|
+
private ignored: IgnoredListing | null = null
|
|
366
|
+
|
|
367
|
+
setIgnored(root: string, paths: readonly string[]): void {
|
|
368
|
+
this.ignored = { root, paths, asked: new Map() }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Whether git ignores a new file directly in `dir` (absolute) while it
|
|
373
|
+
* reports an ignored path under it: `gen/*` beside a kept `gen/.gitkeep`,
|
|
374
|
+
* or `gen/` with a force-added file. Such a directory is ignored in all
|
|
375
|
+
* but the files git lists, so naming it is asking for its ignored files
|
|
376
|
+
* too. A pattern that ignores only some names (`*.generated.js` in a
|
|
377
|
+
* listed `src`) answers false. Asked once per directory per enumeration,
|
|
378
|
+
* and only where status named an ignored path under it.
|
|
379
|
+
*/
|
|
380
|
+
newFilesIgnored(dir: string): boolean {
|
|
381
|
+
const ig = this.ignored
|
|
382
|
+
if (ig === null || ig.paths.length === 0) return false
|
|
383
|
+
const rel = relPosix(ig.root, dir)
|
|
384
|
+
if (rel === '' || rel.startsWith('../')) return false
|
|
385
|
+
let known = ig.asked.get(rel)
|
|
386
|
+
if (known !== undefined) return known
|
|
387
|
+
const under = `${rel}/`
|
|
388
|
+
let lo = 0
|
|
389
|
+
let hi = ig.paths.length
|
|
390
|
+
while (lo < hi) {
|
|
391
|
+
const mid = (lo + hi) >>> 1
|
|
392
|
+
if (ig.paths[mid]! < under) lo = mid + 1
|
|
393
|
+
else hi = mid
|
|
394
|
+
}
|
|
395
|
+
known = false
|
|
396
|
+
if (ig.paths[lo]?.startsWith(under) === true) {
|
|
397
|
+
const probe = `${under}${FRESH_NAME}`
|
|
398
|
+
known = checkIgnoreSync(ig.root, [probe])?.has(probe) === true
|
|
399
|
+
}
|
|
400
|
+
ig.asked.set(rel, known)
|
|
401
|
+
return known
|
|
402
|
+
}
|
|
403
|
+
|
|
299
404
|
get undecodableNames(): ReadonlySet<string> {
|
|
300
405
|
return this.undecodable
|
|
301
406
|
}
|
|
@@ -396,19 +501,39 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
396
501
|
else this.changed.set(partitionDir, [...relPaths])
|
|
397
502
|
// These paths just changed on disk; their index OIDs (if any) no
|
|
398
503
|
// longer describe the working-tree content.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
504
|
+
this.oids.get(partitionDir)?.drop(relPaths.map((rel) => path.resolve(partitionDir, rel)))
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* A digest of `snapshot`'s paths and index OIDs, when the enumeration
|
|
509
|
+
* trusted an OID for every one, none is a symlink and every name decodes:
|
|
510
|
+
* then the files a task resolves there, and what it folds for them, are
|
|
511
|
+
* a function of this digest and its declaration (`filesToken`). Undefined
|
|
512
|
+
* once anything moved the partition: a re-enumeration, a changed path.
|
|
513
|
+
*/
|
|
514
|
+
filesDigest(projectDir: string, snapshot: readonly string[]): string | undefined {
|
|
515
|
+
const d = this.digests.get(projectDir)
|
|
516
|
+
if (d === undefined || d.snapshot !== snapshot || this.get(projectDir) !== snapshot) return
|
|
517
|
+
if (this.oids.get(projectDir) !== d.oids || this.changed.has(projectDir)) return
|
|
518
|
+
return d.digest
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
noteFilesDigest(projectDir: string, snapshot: readonly string[], digest: string): void {
|
|
522
|
+
this.digests.set(projectDir, { snapshot, oids: this.oids.get(projectDir), digest })
|
|
403
523
|
}
|
|
404
524
|
|
|
405
525
|
/** Trusted index OIDs for a project (abs path → oid), if any survive. */
|
|
406
526
|
oidsFor(projectDir: string): ReadonlyMap<string, string> | undefined {
|
|
407
|
-
return this.oids.get(projectDir)
|
|
527
|
+
return this.oids.get(projectDir)?.map
|
|
408
528
|
}
|
|
409
529
|
|
|
410
530
|
setOids(projectDir: string, oids: Map<string, string>): void {
|
|
411
|
-
this.oids.set(projectDir, oids)
|
|
531
|
+
this.oids.set(projectDir, new OidSlot(() => oids))
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** What builds the project's OIDs when a read first asks. */
|
|
535
|
+
setLazyOids(projectDir: string, build: () => Map<string, string>): void {
|
|
536
|
+
this.oids.set(projectDir, new OidSlot(build))
|
|
412
537
|
}
|
|
413
538
|
|
|
414
539
|
/** Snapshot if still valid for these input globs; undefined → re-spawn. */
|
|
@@ -425,18 +550,21 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
425
550
|
override set(key: string, value: readonly string[]): this {
|
|
426
551
|
this.changed.delete(key)
|
|
427
552
|
this.oids.delete(key)
|
|
553
|
+
this.digests.delete(key)
|
|
428
554
|
return super.set(key, value)
|
|
429
555
|
}
|
|
430
556
|
|
|
431
557
|
override delete(key: string): boolean {
|
|
432
558
|
this.changed.delete(key)
|
|
433
559
|
this.oids.delete(key)
|
|
560
|
+
this.digests.delete(key)
|
|
434
561
|
return super.delete(key)
|
|
435
562
|
}
|
|
436
563
|
|
|
437
564
|
override clear(): void {
|
|
438
565
|
this.changed.clear()
|
|
439
566
|
this.oids.clear()
|
|
567
|
+
this.digests.clear()
|
|
440
568
|
super.clear()
|
|
441
569
|
}
|
|
442
570
|
}
|
|
@@ -1201,17 +1329,20 @@ function parseStatusOutput(
|
|
|
1201
1329
|
untracked: string[]
|
|
1202
1330
|
undecodable: Set<string>
|
|
1203
1331
|
ignoredAttributes: boolean
|
|
1332
|
+
ignored: string[]
|
|
1204
1333
|
} {
|
|
1205
1334
|
const tokens = out.split('\0')
|
|
1206
1335
|
const dirty = new Set<string>()
|
|
1207
1336
|
const untracked: string[] = []
|
|
1208
1337
|
const undecodable = new Set<string>()
|
|
1338
|
+
const ignored: string[] = []
|
|
1209
1339
|
let ignoredAttributes = false
|
|
1210
1340
|
for (const token of tokens) {
|
|
1211
1341
|
if (token.length < 4) continue
|
|
1212
1342
|
if (token[0] === '!') {
|
|
1213
1343
|
if (token === '!! .gitattributes' || token.endsWith('/.gitattributes'))
|
|
1214
1344
|
ignoredAttributes = true
|
|
1345
|
+
ignored.push(token.slice(3))
|
|
1215
1346
|
continue
|
|
1216
1347
|
}
|
|
1217
1348
|
if (undecodableRecords.size > 0 && undecodableRecords.has(token))
|
|
@@ -1224,7 +1355,7 @@ function parseStatusOutput(
|
|
|
1224
1355
|
// rename's source is a record of its own (items 976, A-59).
|
|
1225
1356
|
dirty.add(token.slice(3))
|
|
1226
1357
|
}
|
|
1227
|
-
return { dirty, untracked, undecodable, ignoredAttributes }
|
|
1358
|
+
return { dirty, untracked, undecodable, ignoredAttributes, ignored }
|
|
1228
1359
|
}
|
|
1229
1360
|
|
|
1230
1361
|
/**
|
|
@@ -1309,6 +1440,8 @@ export interface GitEnumeration {
|
|
|
1309
1440
|
untracked: readonly string[] | null
|
|
1310
1441
|
/** Workspace-relative paths whose names are not UTF-8 (`decodeGitZ`). */
|
|
1311
1442
|
undecodable: readonly string[]
|
|
1443
|
+
/** What `git status` named ignored, workspace-relative and sorted (a directory with its `/`). */
|
|
1444
|
+
ignored: readonly string[]
|
|
1312
1445
|
/** `Date.now()` before the spawns: what `trusted` says is true as of no earlier. */
|
|
1313
1446
|
startedAtMs: number
|
|
1314
1447
|
/** The blob-size check's input (`dropResizedOids`). */
|
|
@@ -1446,8 +1579,8 @@ export async function startGitEnumeration(
|
|
|
1446
1579
|
stdin?: string,
|
|
1447
1580
|
env?: Record<string, string>,
|
|
1448
1581
|
): Promise<GitRun | null> => {
|
|
1449
|
-
|
|
1450
|
-
|
|
1582
|
+
const spawn = <Out extends 'pipe' | Bun.BunFile>(stdout: Out) =>
|
|
1583
|
+
Bun.spawn({
|
|
1451
1584
|
// `status` refreshes the index when it can take `index.lock`, so
|
|
1452
1585
|
// the user's own `git add` or `commit` failed on the lock while a
|
|
1453
1586
|
// vx run held it (12 in 1,165 across 80 runs, item 880). Read-only, as
|
|
@@ -1455,19 +1588,45 @@ export async function startGitEnumeration(
|
|
|
1455
1588
|
cmd: [executablePath('git'), '--no-optional-locks', ...args],
|
|
1456
1589
|
cwd: workspaceRoot,
|
|
1457
1590
|
stdin: stdin === undefined ? 'ignore' : new TextEncoder().encode(stdin),
|
|
1458
|
-
stdout
|
|
1591
|
+
stdout,
|
|
1459
1592
|
stderr: 'pipe',
|
|
1460
1593
|
env: env !== undefined ? { ...gitEnv(), ...env } : gitEnv(),
|
|
1461
1594
|
})
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1595
|
+
// To a file, not a pipe: the run's thread is busy discovering and
|
|
1596
|
+
// loading configs while git lists, and a pipe held a 10 MB listing
|
|
1597
|
+
// until the thread came back to drain it 64 KiB at a time (~90 ms of
|
|
1598
|
+
// a warm 5,000-project run). In a directory of its own, so no link
|
|
1599
|
+
// another user planted in a shared temp directory takes the bytes. A
|
|
1600
|
+
// temp directory that takes none falls back to the pipe.
|
|
1601
|
+
let dir: string | undefined
|
|
1602
|
+
try {
|
|
1603
|
+
dir = mkdtempSync(path.join(os.tmpdir(), 'vx-git-'))
|
|
1604
|
+
} catch {
|
|
1605
|
+
dir = undefined
|
|
1606
|
+
}
|
|
1607
|
+
try {
|
|
1608
|
+
let stdout: Uint8Array
|
|
1609
|
+
let stderr: string
|
|
1610
|
+
let exitCode: number
|
|
1611
|
+
if (dir !== undefined) {
|
|
1612
|
+
const out = path.join(dir, 'out')
|
|
1613
|
+
const proc = spawn(Bun.file(out))
|
|
1614
|
+
;[stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited])
|
|
1615
|
+
stdout = await Bun.file(out).bytes()
|
|
1616
|
+
} else {
|
|
1617
|
+
const piped = spawn('pipe')
|
|
1618
|
+
;[stdout, stderr, exitCode] = await Promise.all([
|
|
1619
|
+
new Response(piped.stdout).bytes(),
|
|
1620
|
+
new Response(piped.stderr).text(),
|
|
1621
|
+
piped.exited,
|
|
1622
|
+
])
|
|
1623
|
+
}
|
|
1467
1624
|
const { text, undecodable } = decodeGitZ(stdout)
|
|
1468
1625
|
return { exitCode, stdout: text, stderr, undecodable }
|
|
1469
1626
|
} catch {
|
|
1470
1627
|
return null
|
|
1628
|
+
} finally {
|
|
1629
|
+
if (dir !== undefined) rmSync(dir, { recursive: true, force: true })
|
|
1471
1630
|
}
|
|
1472
1631
|
}
|
|
1473
1632
|
const startedAtMs = Date.now()
|
|
@@ -1692,6 +1851,10 @@ export async function startGitEnumeration(
|
|
|
1692
1851
|
changed: dirty === null ? null : [...dirty, ...untracked],
|
|
1693
1852
|
untracked: dirty === null ? null : untracked,
|
|
1694
1853
|
undecodable,
|
|
1854
|
+
ignored:
|
|
1855
|
+
parsedStatus === null
|
|
1856
|
+
? []
|
|
1857
|
+
: [...stripPrefixFromSet(new Set(parsedStatus.ignored), gitPrefix)].sort(),
|
|
1695
1858
|
startedAtMs,
|
|
1696
1859
|
blobCheck: {
|
|
1697
1860
|
key: indexKey,
|
|
@@ -1758,11 +1921,12 @@ export async function applyGitEnumeration(
|
|
|
1758
1921
|
const { all, trusted } = enumeration
|
|
1759
1922
|
cache.setWorktreeDirty(enumeration.dirty)
|
|
1760
1923
|
cache.setGitVars(enumeration.vars)
|
|
1924
|
+
cache.setIgnored(workspaceRoot, enumeration.ignored)
|
|
1761
1925
|
cache.enumeratedAtMs = enumeration.startedAtMs
|
|
1762
1926
|
cache.markUndecodable(enumeration.undecodable.map((rel) => path.join(workspaceRoot, rel)))
|
|
1763
|
-
cache.
|
|
1927
|
+
cache.noteTrackedLater(
|
|
1764
1928
|
enumeration.scopes.map((rel) => path.join(workspaceRoot, rel)),
|
|
1765
|
-
enumeration.tracked.map((rel) => path.join(workspaceRoot, rel)),
|
|
1929
|
+
() => enumeration.tracked.map((rel) => path.join(workspaceRoot, rel)),
|
|
1766
1930
|
)
|
|
1767
1931
|
// Sort once, then each project's files are a contiguous range found
|
|
1768
1932
|
// by binary search on its `dir/` prefix — O((F+P) log F) instead of
|
|
@@ -1780,6 +1944,8 @@ export async function applyGitEnumeration(
|
|
|
1780
1944
|
}
|
|
1781
1945
|
}
|
|
1782
1946
|
const sorted = inOrder ? all : [...all].sort()
|
|
1947
|
+
const undecodable =
|
|
1948
|
+
enumeration.undecodable.length === 0 ? undefined : new Set(enumeration.undecodable)
|
|
1783
1949
|
// Git prints normalized relative paths, so under an absolute, normalized
|
|
1784
1950
|
// root a join is a concatenation: `path.join` per tracked file and
|
|
1785
1951
|
// `path.relative` per project were most of this pass (I-30's rule).
|
|
@@ -1803,6 +1969,11 @@ export async function applyGitEnumeration(
|
|
|
1803
1969
|
}
|
|
1804
1970
|
return lo
|
|
1805
1971
|
}
|
|
1972
|
+
const rootOids = (): Map<string, string> => {
|
|
1973
|
+
const oids = new Map<string, string>()
|
|
1974
|
+
for (const [rel, oid] of trusted) oids.set(abs(rel), oid)
|
|
1975
|
+
return oids
|
|
1976
|
+
}
|
|
1806
1977
|
for (const projectDir of projectDirs) {
|
|
1807
1978
|
const spelled =
|
|
1808
1979
|
base !== undefined && projectDir.startsWith(base) && path.normalize(projectDir) === projectDir
|
|
@@ -1815,23 +1986,35 @@ export async function applyGitEnumeration(
|
|
|
1815
1986
|
const relPrefix = spelled.normalize('NFC')
|
|
1816
1987
|
if (relPrefix === '' || relPrefix === '.') {
|
|
1817
1988
|
cache.set(projectDir, all)
|
|
1818
|
-
|
|
1819
|
-
for (const [rel, oid] of trusted) rootOids.set(abs(rel), oid)
|
|
1820
|
-
cache.setOids(projectDir, rootOids)
|
|
1989
|
+
cache.setLazyOids(projectDir, rootOids)
|
|
1821
1990
|
continue
|
|
1822
1991
|
}
|
|
1823
1992
|
const prefix = `${relPrefix}/`
|
|
1824
1993
|
const start = lowerBound(prefix)
|
|
1825
1994
|
const end = lowerBound(`${relPrefix}0`)
|
|
1826
1995
|
const matches: string[] = []
|
|
1827
|
-
const projOids = new Map<string, string>()
|
|
1828
1996
|
const projAbs =
|
|
1829
1997
|
relPrefix === spelled ? abs : (rel: string) => path.join(projectDir, rel.slice(prefix.length))
|
|
1998
|
+
const projOids = (): Map<string, string> => {
|
|
1999
|
+
const oids = new Map<string, string>()
|
|
2000
|
+
for (let i = start; i < end; i++) {
|
|
2001
|
+
const rel = sorted[i]!
|
|
2002
|
+
const oid = trusted.get(rel)
|
|
2003
|
+
if (oid !== undefined) oids.set(projAbs(rel), oid)
|
|
2004
|
+
}
|
|
2005
|
+
return oids
|
|
2006
|
+
}
|
|
2007
|
+
// One hash of the records joined: one per record was ~25 ms of 125,000.
|
|
2008
|
+
let records: string | undefined = ''
|
|
1830
2009
|
for (let i = start; i < end; i++) {
|
|
1831
2010
|
const rel = sorted[i]!
|
|
1832
|
-
|
|
2011
|
+
const own = rel.slice(prefix.length)
|
|
2012
|
+
matches.push(own)
|
|
2013
|
+
if (records === undefined) continue
|
|
1833
2014
|
const oid = trusted.get(rel)
|
|
1834
|
-
if (oid
|
|
2015
|
+
if (oid === undefined || oid.startsWith('120000:') || undecodable?.has(rel) === true) {
|
|
2016
|
+
records = undefined
|
|
2017
|
+
} else records += `${own}\0${oid}\n`
|
|
1835
2018
|
}
|
|
1836
2019
|
// An empty slice is a directory git did not see, not an empty project:
|
|
1837
2020
|
// a project has at least its package.json, tracked or untracked. A
|
|
@@ -1847,15 +2030,14 @@ export async function applyGitEnumeration(
|
|
|
1847
2030
|
// trusted from here) — one spawn per such project per run, nothing for
|
|
1848
2031
|
// a workspace without one.
|
|
1849
2032
|
if (matches.length === 0) continue
|
|
1850
|
-
//
|
|
2033
|
+
// setLazyOids AFTER set — set() drops the project's OID slot.
|
|
1851
2034
|
cache.set(projectDir, matches)
|
|
1852
|
-
cache.
|
|
2035
|
+
cache.setLazyOids(projectDir, projOids)
|
|
2036
|
+
if (records !== undefined) cache.noteFilesDigest(projectDir, matches, xxh3hex(records))
|
|
1853
2037
|
}
|
|
1854
2038
|
if (workspaceWide) {
|
|
1855
|
-
const rootOids = new Map<string, string>()
|
|
1856
|
-
for (const [rel, oid] of trusted) rootOids.set(abs(rel), oid)
|
|
1857
2039
|
cache.set(workspaceRoot, all)
|
|
1858
|
-
cache.
|
|
2040
|
+
cache.setLazyOids(workspaceRoot, rootOids)
|
|
1859
2041
|
cache.setWorkspaceRoot(workspaceRoot)
|
|
1860
2042
|
}
|
|
1861
2043
|
}
|
package/src/cache/index.ts
CHANGED
|
@@ -34,7 +34,8 @@ export {
|
|
|
34
34
|
SCHEMA_VERSION,
|
|
35
35
|
vxCacheEntries,
|
|
36
36
|
} from './cache.js'
|
|
37
|
-
export { ABSENT_INPUT, absentOr, CACHE_VERSION } from './key-fold.js'
|
|
37
|
+
export { ABSENT_INPUT, absentOr, CACHE_VERSION, foldHead } from './key-fold.js'
|
|
38
|
+
export type { FoldMemo } from './fold-memo.js'
|
|
38
39
|
export { measureFsClockLag, setFsClockLag } from './fs-clock.js'
|
|
39
40
|
export { filesBehind } from './linked-inputs.js'
|
|
40
41
|
export {
|
|
@@ -46,7 +47,10 @@ export {
|
|
|
46
47
|
declaresInput,
|
|
47
48
|
namedOutright,
|
|
48
49
|
workspaceFilesReachInto,
|
|
50
|
+
filesToken,
|
|
49
51
|
resolveInputs,
|
|
52
|
+
type ResolveInputsArgs,
|
|
53
|
+
resolveOutputTree,
|
|
50
54
|
resolveOutputs,
|
|
51
55
|
resolveWorkspaceOutputs,
|
|
52
56
|
sweepRestoreTemps,
|
|
@@ -75,7 +79,7 @@ export {
|
|
|
75
79
|
type GitEnumeration,
|
|
76
80
|
type BlobSizeMemo,
|
|
77
81
|
} from './git-inputs.js'
|
|
78
|
-
export { LayeredCache, type RemoteCacheLayer } from './layered-cache.js'
|
|
82
|
+
export { LayeredCache, type RemoteCacheLayer, type RemoteOpenContext } from './layered-cache.js'
|
|
79
83
|
export type { ResolvedRemote } from './remote-lane.js'
|
|
80
84
|
export { ScopedCache } from './scoped-cache.js'
|
|
81
85
|
export {
|