@vzn/vx 0.0.636 → 0.0.638
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type CacheLayer,
|
|
7
7
|
type CachePolicy,
|
|
8
8
|
cleanOutputs,
|
|
9
|
+
committedFiles,
|
|
9
10
|
cleanWorkspaceOutputs,
|
|
10
11
|
type OutputStamp,
|
|
11
12
|
ownOutputsSince,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
stampWorkspaceOutputs,
|
|
15
16
|
FULL_CACHE_POLICY,
|
|
16
17
|
type GitFilesCache,
|
|
18
|
+
racyWindowMs,
|
|
17
19
|
} from '../cache/index.js'
|
|
18
20
|
import {
|
|
19
21
|
buildIsolatedEnv,
|
|
@@ -51,7 +53,9 @@ import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '..
|
|
|
51
53
|
import {
|
|
52
54
|
killGraceMs,
|
|
53
55
|
MASKED,
|
|
56
|
+
parseDuration,
|
|
54
57
|
maskedEmitter,
|
|
58
|
+
outputMatcher,
|
|
55
59
|
printable,
|
|
56
60
|
relPosix,
|
|
57
61
|
secretMask,
|
|
@@ -61,12 +65,15 @@ import {
|
|
|
61
65
|
} from '../util/index.js'
|
|
62
66
|
import { forwardedSignal, SIGNAL_SHUTDOWN_GRACE_MS, terminateChildren } from './signals.js'
|
|
63
67
|
import { executorLabel, nameExecutorFailure } from './plugin-host.js'
|
|
68
|
+
import type { CommandWindows } from './command-windows.js'
|
|
64
69
|
import {
|
|
70
|
+
commandWriteReach,
|
|
65
71
|
mayWriteFingerprint,
|
|
66
72
|
type Placeholder,
|
|
67
73
|
placeholderSweeper,
|
|
68
74
|
reachedWithheld,
|
|
69
75
|
sandboxRequestFor,
|
|
76
|
+
sandboxReadable,
|
|
70
77
|
sweepPlaceholders,
|
|
71
78
|
undeclaredWriteReach,
|
|
72
79
|
untouchedPlaceholderLine,
|
|
@@ -88,6 +95,7 @@ import {
|
|
|
88
95
|
computeTaskHash,
|
|
89
96
|
describeTaskInputs,
|
|
90
97
|
type HashCache,
|
|
98
|
+
type InputFact,
|
|
91
99
|
movedInput,
|
|
92
100
|
type TaskInputComponent,
|
|
93
101
|
type UpfrontGroupKey,
|
|
@@ -99,7 +107,6 @@ export interface ExecuteArgs {
|
|
|
99
107
|
node: TaskNode
|
|
100
108
|
upstream: TaskOutcome[]
|
|
101
109
|
workspaceRoot: string
|
|
102
|
-
workspaceFingerprint: string
|
|
103
110
|
cache: CacheLayer
|
|
104
111
|
/** Granular cache read/write policy. Undefined → everything on. */
|
|
105
112
|
cachePolicy?: CachePolicy
|
|
@@ -121,7 +128,7 @@ export interface ExecuteArgs {
|
|
|
121
128
|
*/
|
|
122
129
|
remoteOnly?: boolean
|
|
123
130
|
/**
|
|
124
|
-
* Plan-time `--download` decision for THIS task (run.ts). `'deferred'`
|
|
131
|
+
* Plan-time `--cache-download` decision for THIS task (run.ts). `'deferred'`
|
|
125
132
|
* means: do not clean, do not save — the executor is expected to leave
|
|
126
133
|
* the outputs in the remote store and hand back a closure. An executor
|
|
127
134
|
* that ignores it and materialises anyway simply gets no local entry;
|
|
@@ -163,26 +170,29 @@ export interface ExecuteArgs {
|
|
|
163
170
|
/** Name what a missed key changed (`createMissExplainer`); absent when nobody listens. */
|
|
164
171
|
explainMiss?: MissExplainer
|
|
165
172
|
/**
|
|
166
|
-
* Run-level retry default (`--
|
|
173
|
+
* Run-level retry default (`--retries <n>` / `RunOptions.retries`).
|
|
167
174
|
* Explicit `exec.retries` wins, including an explicit 0. Threaded as
|
|
168
175
|
* an option only — never folded into any hash, so cache keys are
|
|
169
176
|
* byte-identical with and without it.
|
|
170
177
|
*/
|
|
171
178
|
retries?: number
|
|
172
179
|
/**
|
|
173
|
-
*
|
|
174
|
-
* `
|
|
175
|
-
* `exec.timeout` wins. Threaded as an option only — never hashed.
|
|
180
|
+
* `run.timeout` (ms), already resolved from flag, variable and
|
|
181
|
+
* workspace. Per-task `exec.timeout` wins. Never hashed.
|
|
176
182
|
*/
|
|
177
183
|
timeout?: number
|
|
184
|
+
/** `run.readyNotice` (ms), resolved like `timeout`; unset, 10 s. */
|
|
185
|
+
readyNotice?: number
|
|
178
186
|
/**
|
|
179
187
|
* The run's `inputStability` layers around the task's own: `run` from
|
|
180
|
-
* the env vars (above the task), `workspace` from
|
|
188
|
+
* the env vars (above the task), `workspace` from `.vx/workspace` (below).
|
|
181
189
|
*/
|
|
182
190
|
inputStability?: { run: InputStability; workspace?: InputStability }
|
|
191
|
+
/** `cache.outputs.clean` for tasks without their own: `.vx/workspace`. */
|
|
192
|
+
cleanOutputs?: boolean
|
|
183
193
|
/** Per-run memo for `git ls-files` (one entry per project dir). */
|
|
184
194
|
gitFilesCache?: GitFilesCache
|
|
185
|
-
/** Per-run memo for derived hashes (
|
|
195
|
+
/** Per-run memo for derived hashes (task config, file lists). */
|
|
186
196
|
hashCache?: HashCache
|
|
187
197
|
/** The run's `probesAfterWrites`, handed to every key this task takes. */
|
|
188
198
|
probesAfterWrites?: ReadonlySet<string>
|
|
@@ -247,8 +257,14 @@ export interface ExecuteArgs {
|
|
|
247
257
|
*/
|
|
248
258
|
taintedUpstream?: boolean
|
|
249
259
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
260
|
+
* The run keeps its bytes in a remote (run-store.ts): an attempt captures
|
|
261
|
+
* its output at the entry's bounds, and an outcome that saves no entry
|
|
262
|
+
* (a failure, an uncached run) carries it as `runLog`.
|
|
263
|
+
*/
|
|
264
|
+
keepRunLog?: boolean
|
|
265
|
+
/**
|
|
266
|
+
* Whether a task has rewritten a root file a plugin claims (whose bytes
|
|
267
|
+
* the claimant folded) since the run read it (fingerprint-watch.ts): past that, no key taken
|
|
252
268
|
* on the old digest is probed or saved.
|
|
253
269
|
*/
|
|
254
270
|
fingerprintWatch?: FingerprintWatch
|
|
@@ -260,6 +276,8 @@ export interface ExecuteArgs {
|
|
|
260
276
|
noDependants?: true
|
|
261
277
|
/** The task holds vx's terminal (`terminalHolders`): its stdio is vx's own. */
|
|
262
278
|
terminal?: true
|
|
279
|
+
/** When each command of the run ran and where it may write (command-windows.ts). */
|
|
280
|
+
commandWindows?: CommandWindows
|
|
263
281
|
}
|
|
264
282
|
|
|
265
283
|
/**
|
|
@@ -360,9 +378,14 @@ function inputStabilityOf(
|
|
|
360
378
|
}
|
|
361
379
|
}
|
|
362
380
|
|
|
381
|
+
/** The root files the plugins claim, as the run's watch holds them. */
|
|
382
|
+
function claimedFiles(args: ExecuteArgs): readonly string[] {
|
|
383
|
+
return args.fingerprintWatch?.names ?? []
|
|
384
|
+
}
|
|
385
|
+
|
|
363
386
|
/**
|
|
364
|
-
*
|
|
365
|
-
* rewrote the lockfile), said once per run.
|
|
387
|
+
* A claimed file every key here folded through its claimant has moved (a
|
|
388
|
+
* task rewrote the lockfile), said once per run.
|
|
366
389
|
*/
|
|
367
390
|
function fingerprintMoved(args: ExecuteArgs): boolean {
|
|
368
391
|
if (args.fingerprintWatch?.moved() === undefined) return false
|
|
@@ -390,7 +413,6 @@ async function executeGroupTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
390
413
|
node: args.node,
|
|
391
414
|
upstream: args.upstream,
|
|
392
415
|
workspaceRoot: args.workspaceRoot,
|
|
393
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
394
416
|
cache: args.cache,
|
|
395
417
|
forwardArgs: args.forwardArgs,
|
|
396
418
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -486,7 +508,6 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
486
508
|
node,
|
|
487
509
|
upstream: args.upstream,
|
|
488
510
|
workspaceRoot: args.workspaceRoot,
|
|
489
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
490
511
|
cache: args.cache,
|
|
491
512
|
forwardArgs: args.forwardArgs,
|
|
492
513
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -548,6 +569,8 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
548
569
|
const serverSecrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
549
570
|
const serverOut = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStdout(node, t))
|
|
550
571
|
const serverErr = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStderr(node, t))
|
|
572
|
+
// A server writes until the run stops it: its window stays open.
|
|
573
|
+
args.commandWindows?.open(node.id, node.projectDir, commandWriteReach(node, args.workspaceRoot))
|
|
551
574
|
const persistentOpts: Parameters<typeof runPersistent>[0] = {
|
|
552
575
|
command,
|
|
553
576
|
cwd: node.projectDir,
|
|
@@ -566,7 +589,7 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
566
589
|
}
|
|
567
590
|
// For a persistent task the timeout bounds the readiness wait. Per-task
|
|
568
591
|
// `exec.timeout` wins; else the run-level default (env/workspace/--timeout).
|
|
569
|
-
const effectiveTimeout = step
|
|
592
|
+
const effectiveTimeout = taskTimeoutMs(step) ?? args.timeout
|
|
570
593
|
if (effectiveTimeout !== undefined) {
|
|
571
594
|
persistentOpts.timeoutMs = effectiveTimeout
|
|
572
595
|
}
|
|
@@ -617,7 +640,8 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
617
640
|
// it fails, and with no `exec.timeout` the wait never ends, so a run
|
|
618
641
|
// whose `readyWhen` never matched showed nothing at all.
|
|
619
642
|
const readyWhen = step.persistent.readyWhen
|
|
620
|
-
|
|
643
|
+
// How long a server may take to match `readyWhen` before vx says it waits.
|
|
644
|
+
const noticeMs = args.readyNotice ?? 10_000
|
|
621
645
|
const notice =
|
|
622
646
|
readyWhen === undefined
|
|
623
647
|
? undefined
|
|
@@ -660,7 +684,7 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
660
684
|
const message = err instanceof Error ? err.message : String(err)
|
|
661
685
|
// The task's OWN stream, not the process's: the frame is where a
|
|
662
686
|
// reader looks for why a task failed, and a run with a custom logger
|
|
663
|
-
// (an embedder
|
|
687
|
+
// (an embedder) never saw a bare stderr write at all.
|
|
664
688
|
log.taskStderr(node, `\n[vx] ${node.id}: persistent task failed to become ready: ${message}\n`)
|
|
665
689
|
// A server is never traced, so the sandbox's refusals reach no report:
|
|
666
690
|
// a dev server that died on a file outside its grants read only as the
|
|
@@ -724,7 +748,9 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
724
748
|
}
|
|
725
749
|
args.persistentRegistry?.set(node.id, spawn.child)
|
|
726
750
|
forgetUndeclaredWrites(args, undeclaredWriteReach(node, args.workspaceRoot))
|
|
727
|
-
if (mayWriteFingerprint(node, args.workspaceRoot))
|
|
751
|
+
if (mayWriteFingerprint(node, args.workspaceRoot, claimedFiles(args))) {
|
|
752
|
+
args.fingerprintWatch?.wrote()
|
|
753
|
+
}
|
|
728
754
|
return {
|
|
729
755
|
node,
|
|
730
756
|
status: 'success',
|
|
@@ -795,10 +821,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
795
821
|
const outputs = cacheCfg?.outputs.files ?? []
|
|
796
822
|
const wsOutputs = cacheCfg?.outputs.workspaceFiles ?? []
|
|
797
823
|
const effectiveForwardArgs = node.requested ? (args.forwardArgs ?? []) : []
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
|
|
801
|
-
const effectiveTimeout = step.timeout ?? args.timeout
|
|
824
|
+
// Per-task `exec.timeout` beats `run.timeout` (flag, variable,
|
|
825
|
+
// workspace; resolved into `args.timeout` by run.ts).
|
|
826
|
+
const effectiveTimeout = taskTimeoutMs(step) ?? args.timeout
|
|
802
827
|
// `signal` stops a plugin executor on the run's stop AND on
|
|
803
828
|
// `exec.timeout`: core cannot kill a process an executor spawned, so a
|
|
804
829
|
// declared timeout meant nothing on one that kept its own clock (H-12).
|
|
@@ -910,7 +935,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
910
935
|
node,
|
|
911
936
|
upstream,
|
|
912
937
|
workspaceRoot: args.workspaceRoot,
|
|
913
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
914
938
|
cache,
|
|
915
939
|
forwardArgs: args.forwardArgs,
|
|
916
940
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -941,8 +965,13 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
941
965
|
projectDir: node.projectDir,
|
|
942
966
|
outputs,
|
|
943
967
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
968
|
+
committed: committedFiles(args.gitFilesCache, node.projectDir),
|
|
969
|
+
}
|
|
970
|
+
const wsCleanArgs = {
|
|
971
|
+
workspaceRoot: args.workspaceRoot,
|
|
972
|
+
outputs: wsOutputs,
|
|
973
|
+
committed: committedFiles(args.gitFilesCache, args.workspaceRoot),
|
|
944
974
|
}
|
|
945
|
-
const wsCleanArgs = { workspaceRoot: args.workspaceRoot, outputs: wsOutputs }
|
|
946
975
|
// An ADDITIVE task (its outputs overlap an upstream's, with the edge that
|
|
947
976
|
// orders them; item 588) does not clean by glob — the glob selects the
|
|
948
977
|
// upstream's files it adds beside. Its outputs are stamped once before
|
|
@@ -952,6 +981,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
952
981
|
const additive = (node.addsToOutputsOf?.length ?? 0) > 0
|
|
953
982
|
let stampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
954
983
|
let wsStampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
984
|
+
// `cache.outputs.clean: false` leaves the outputs to the first attempt's
|
|
985
|
+
// command, and the save stores whatever they hold after it, a leftover
|
|
986
|
+
// included: the user opted out of the clean-tree guarantee (M,
|
|
987
|
+
// 2026-10-15). A retry still starts clean. An additive task never cleans
|
|
988
|
+
// by glob anyway.
|
|
989
|
+
const keepOutputs = !additive && (cacheCfg?.outputs.clean ?? args.cleanOutputs ?? true) === false
|
|
955
990
|
|
|
956
991
|
// Cache lookup. On hit, time the user-perceived restore op
|
|
957
992
|
// (clean+restore+log-replay) — that's what the framed-block footer
|
|
@@ -990,7 +1025,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
990
1025
|
node,
|
|
991
1026
|
upstream,
|
|
992
1027
|
workspaceRoot: args.workspaceRoot,
|
|
993
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
994
1028
|
cache,
|
|
995
1029
|
forwardArgs: args.forwardArgs,
|
|
996
1030
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -1002,6 +1036,41 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1002
1036
|
: {}),
|
|
1003
1037
|
})
|
|
1004
1038
|
: undefined
|
|
1039
|
+
/** The other command that may have put a moved input back (`ownRewrite`). */
|
|
1040
|
+
let rewroteBy: string | undefined
|
|
1041
|
+
/**
|
|
1042
|
+
* Under `'mutable'`, a write during the command that left the key's bytes
|
|
1043
|
+
* is the task's own identical rewrite only if no other command that may
|
|
1044
|
+
* write the file ran between the describe and that write: one that did
|
|
1045
|
+
* may have changed it and put it back while this command read it.
|
|
1046
|
+
*/
|
|
1047
|
+
const ownRewrite = (file: string, ctimeMs: number): boolean => {
|
|
1048
|
+
rewroteBy = args.commandWindows?.writerDuring(
|
|
1049
|
+
node.id,
|
|
1050
|
+
file,
|
|
1051
|
+
described!.describedAt,
|
|
1052
|
+
ctimeMs + racyWindowMs(ctimeMs, 0),
|
|
1053
|
+
)
|
|
1054
|
+
return rewroteBy === undefined
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* A committed file under the task's own outputs is keyed (`committedFiles`)
|
|
1059
|
+
* and the task may rewrite it (TanStack Router's `routeTree.gen.ts`): the
|
|
1060
|
+
* bytes it leaves are an output the entry saves and a restore writes, and
|
|
1061
|
+
* they follow from the key, so their change is no moved input.
|
|
1062
|
+
*/
|
|
1063
|
+
const ownOutput = outputs.length === 0 ? undefined : outputMatcher(outputs)
|
|
1064
|
+
const ownWsOutput = wsOutputs.length === 0 ? undefined : outputMatcher(wsOutputs)
|
|
1065
|
+
const keyedInputs = (facts: readonly InputFact[]): readonly InputFact[] =>
|
|
1066
|
+
ownOutput === undefined && ownWsOutput === undefined
|
|
1067
|
+
? facts
|
|
1068
|
+
: facts.filter(
|
|
1069
|
+
(f) =>
|
|
1070
|
+
ownOutput?.(relPosix(node.projectDir, f.path)) !== true &&
|
|
1071
|
+
ownWsOutput?.(relPosix(args.workspaceRoot, f.path)) !== true,
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1005
1074
|
// A name only `exec.env.secret` makes secret is not one the name rule
|
|
1006
1075
|
// sees later: its row carries the mark, so `vx why` hides its hash (M-63).
|
|
1007
1076
|
const named = node.config.exec?.env?.secret
|
|
@@ -1061,7 +1130,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1061
1130
|
let withheld: WithheldLink[] = []
|
|
1062
1131
|
|
|
1063
1132
|
// Cache miss path (or caching disabled), up to `1 + retries` attempts.
|
|
1064
|
-
// Explicit config wins over the run-level `--
|
|
1133
|
+
// Explicit config wins over the run-level `--retries` default, including
|
|
1065
1134
|
// an explicit `retries: 0`.
|
|
1066
1135
|
const maxAttempts = 1 + (step.retries ?? args.retries ?? 0)
|
|
1067
1136
|
let attempt = 0
|
|
@@ -1084,6 +1153,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1084
1153
|
exitCode: number
|
|
1085
1154
|
withdrawn?: true
|
|
1086
1155
|
}> {
|
|
1156
|
+
// The first attempt under `clean: false` finds its last outputs (tsc's `.tsbuildinfo`).
|
|
1157
|
+
const clean = willWrite && !deferralRequested && !(keepOutputs && attempt === 1)
|
|
1087
1158
|
// A deferred task's outputs are deliberately NOT coming, so wiping the
|
|
1088
1159
|
// tree would replace a stale build with nothing at all. The eligibility
|
|
1089
1160
|
// gate guarantees no key in this run can see what stays behind, and the
|
|
@@ -1092,7 +1163,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1092
1163
|
const endStamp = span('miss: stamp outputs')
|
|
1093
1164
|
stampedBefore ??= await stampOutputs(cleanArgs)
|
|
1094
1165
|
endStamp()
|
|
1095
|
-
} else if (
|
|
1166
|
+
} else if (clean && outputs.length > 0) {
|
|
1096
1167
|
// Mark the wiped paths, exactly as the workspace twin below does. The
|
|
1097
1168
|
// git snapshot still lists them with their committed index OIDs, and
|
|
1098
1169
|
// resolveFiles SKIPS its existence probe for any path carrying a
|
|
@@ -1102,7 +1173,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1102
1173
|
const endClean = span('miss: clean outputs')
|
|
1103
1174
|
const cleanedRels = await cleanOutputs(cleanArgs)
|
|
1104
1175
|
endClean()
|
|
1105
|
-
args.gitFilesCache?.noteClean(node.id, node.projectDir, cleanedRels)
|
|
1106
1176
|
args.gitFilesCache?.markOutputsChanged(node.projectDir, cleanedRels)
|
|
1107
1177
|
}
|
|
1108
1178
|
if (additive && willWrite && !deferralRequested && wsOutputs.length > 0) {
|
|
@@ -1110,7 +1180,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1110
1180
|
// glob clean deleted the upstream's files before this task, which
|
|
1111
1181
|
// reads them, ran (A-43).
|
|
1112
1182
|
wsStampedBefore ??= await stampWorkspaceOutputs(wsCleanArgs)
|
|
1113
|
-
} else if (
|
|
1183
|
+
} else if (clean && wsOutputs.length > 0) {
|
|
1114
1184
|
// Root-anchored deletions can land in other projects' dirs; mark
|
|
1115
1185
|
// them so stale per-project git snapshots can't survive the wipe.
|
|
1116
1186
|
const cleanedWsRels = await cleanWorkspaceOutputs(wsCleanArgs)
|
|
@@ -1139,6 +1209,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1139
1209
|
// the throw is its rejection, the value its resolution, so both reach
|
|
1140
1210
|
// the naming catch and the cleanup in finally below.
|
|
1141
1211
|
let running: Promise<unknown>
|
|
1212
|
+
const closeWindow =
|
|
1213
|
+
args.commandWindows?.open(
|
|
1214
|
+
node.id,
|
|
1215
|
+
node.projectDir,
|
|
1216
|
+
commandWriteReach(node, args.workspaceRoot),
|
|
1217
|
+
) ?? (() => {})
|
|
1142
1218
|
try {
|
|
1143
1219
|
running = Promise.resolve(args.executor.execute(req))
|
|
1144
1220
|
} catch (thrown) {
|
|
@@ -1177,6 +1253,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1177
1253
|
throw err
|
|
1178
1254
|
})
|
|
1179
1255
|
.finally(() => {
|
|
1256
|
+
closeWindow()
|
|
1180
1257
|
clearTimeout(timeoutTimer)
|
|
1181
1258
|
unlistenStop?.()
|
|
1182
1259
|
unlistenStop = undefined
|
|
@@ -1267,18 +1344,6 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1267
1344
|
// The line quotes the command's first word, which a config may have
|
|
1268
1345
|
// built from a secret: masked as the task's own output is (L-37).
|
|
1269
1346
|
if (verdict !== undefined) log.taskStderr(node, `\n${secrets?.mask(verdict) ?? verdict}\n`)
|
|
1270
|
-
// A committed file another task's clean removed, still gone: vx
|
|
1271
|
-
// cleans outputs before a run where Turbo does not, so a reader with
|
|
1272
|
-
// no edge to the producer fails naming only the file (A-48).
|
|
1273
|
-
if (code !== 0) {
|
|
1274
|
-
for (const c of (args.gitFilesCache?.trackedCleansMissing(node.id) ?? []).slice(0, 3)) {
|
|
1275
|
-
const rel = relPosix(args.workspaceRoot, c.path)
|
|
1276
|
-
log.taskStderr(
|
|
1277
|
-
node,
|
|
1278
|
-
`\n[vx] ${rel} is tracked by git, and ${c.by} removed it as an output before its run; it is still missing. A task that reads it needs dependsOn on ${c.by}.\n`,
|
|
1279
|
-
)
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
1347
|
}
|
|
1283
1348
|
return { result: res, exitCode: code }
|
|
1284
1349
|
}
|
|
@@ -1327,7 +1392,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1327
1392
|
const writeReach =
|
|
1328
1393
|
args.executor.remote === true ? 'none' : undeclaredWriteReach(node, args.workspaceRoot)
|
|
1329
1394
|
const writesFingerprint =
|
|
1330
|
-
args.executor.remote !== true &&
|
|
1395
|
+
args.executor.remote !== true &&
|
|
1396
|
+
mayWriteFingerprint(node, args.workspaceRoot, claimedFiles(args))
|
|
1331
1397
|
// A stop that landed during the awaits above (the key, the probe, the
|
|
1332
1398
|
// input description): the first attempt would wipe the last build and
|
|
1333
1399
|
// hand the executor a request after the run had stopped. Through a call,
|
|
@@ -1404,9 +1470,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1404
1470
|
}
|
|
1405
1471
|
|
|
1406
1472
|
async function buildRequest(local: boolean): Promise<ExecuteRequest> {
|
|
1407
|
-
const kept =
|
|
1408
|
-
|
|
1409
|
-
|
|
1473
|
+
const kept =
|
|
1474
|
+
willSave || args.keepRunLog === true
|
|
1475
|
+
? new BoundedCapture()
|
|
1476
|
+
: new BoundedCapture(FAILED_OUTPUT_HEAD_CHARS, FAILED_OUTPUT_TAIL_CHARS)
|
|
1410
1477
|
output = kept
|
|
1411
1478
|
const toOut = (t: string): void => {
|
|
1412
1479
|
kept.push(t)
|
|
@@ -1474,8 +1541,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1474
1541
|
const wallclockEndNs = process.hrtime.bigint() - args.runStartHrTimeNs
|
|
1475
1542
|
|
|
1476
1543
|
const stability = inputStabilityOf(args.node.config.cache, args.inputStability)
|
|
1477
|
-
let movedFails
|
|
1544
|
+
let movedFails: string | undefined
|
|
1478
1545
|
let own: Awaited<ReturnType<typeof ownOutputs>>
|
|
1546
|
+
let saved = false
|
|
1479
1547
|
if (effectiveExitCode === 0 && willSave && deferralRequested) {
|
|
1480
1548
|
// The outputs never landed here: no artifact, no rows. A partial local
|
|
1481
1549
|
// record (a row with no artifact) is exactly the corrupt-entry shape
|
|
@@ -1527,8 +1595,19 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1527
1595
|
deferSave: args.deferSave,
|
|
1528
1596
|
// A sink listens: say what the save cost.
|
|
1529
1597
|
measure: args.explainMiss !== undefined,
|
|
1598
|
+
...(userSandbox
|
|
1599
|
+
? {
|
|
1600
|
+
readable: sandboxReadable(
|
|
1601
|
+
node,
|
|
1602
|
+
cfg.exec!.sandbox!,
|
|
1603
|
+
args.workspaceRoot,
|
|
1604
|
+
args.nestedProjectDirs,
|
|
1605
|
+
),
|
|
1606
|
+
}
|
|
1607
|
+
: {}),
|
|
1530
1608
|
})
|
|
1531
1609
|
args.deferredSaves?.set(node.id, landed)
|
|
1610
|
+
saved = true
|
|
1532
1611
|
} else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants !== true) {
|
|
1533
1612
|
// Ran here and saves nothing — failed, a read-only policy, a tainted
|
|
1534
1613
|
// upstream, a key that no longer held — yet it wrote what a save would
|
|
@@ -1586,10 +1665,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1586
1665
|
if (described!.hash !== hash) return null
|
|
1587
1666
|
return (
|
|
1588
1667
|
(await movedInput(
|
|
1589
|
-
described!.facts,
|
|
1668
|
+
keyedInputs(described!.facts),
|
|
1590
1669
|
cache,
|
|
1591
1670
|
described!.describedAt,
|
|
1592
|
-
stability.strategy === 'mutable',
|
|
1671
|
+
stability.strategy === 'mutable' ? ownRewrite : undefined,
|
|
1593
1672
|
)) ?? described!.added()
|
|
1594
1673
|
)
|
|
1595
1674
|
}
|
|
@@ -1619,24 +1698,25 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1619
1698
|
endCheck()
|
|
1620
1699
|
if (moved === undefined) return true
|
|
1621
1700
|
const what =
|
|
1622
|
-
moved === null ? 'its inputs' : `\`${printable(relPosix(args.workspaceRoot, moved))}\``
|
|
1701
|
+
(moved === null ? 'its inputs' : `\`${printable(relPosix(args.workspaceRoot, moved))}\``) +
|
|
1702
|
+
' changed after its key was taken' +
|
|
1703
|
+
(rewroteBy === undefined ? '' : ` while ${rewroteBy} ran`)
|
|
1623
1704
|
// A task that regenerates its own input (TanStack Router's committed
|
|
1624
1705
|
// `routeTree.gen.ts`, rewritten to the same bytes by every build) is
|
|
1625
1706
|
// never saved (item 1015), and the line said so without the way out.
|
|
1626
|
-
// Not for `package.json`, which an output may not cover.
|
|
1627
1707
|
const hint =
|
|
1628
|
-
moved === null ||
|
|
1708
|
+
moved === null || rewroteBy !== undefined
|
|
1629
1709
|
? ''
|
|
1630
1710
|
: '; if the task writes it, declare it in cache.outputs'
|
|
1631
1711
|
if (stability.severity === 'error') {
|
|
1632
1712
|
log.status(
|
|
1633
|
-
`[vx] ${node.id}: ${what}
|
|
1713
|
+
`[vx] ${node.id}: ${what} — the result is not saved, ` +
|
|
1634
1714
|
`and inputStability.severity is 'error', so the task fails${hint}`,
|
|
1635
1715
|
)
|
|
1636
|
-
movedFails =
|
|
1716
|
+
movedFails = moved === null ? '' : printable(relPosix(args.workspaceRoot, moved))
|
|
1637
1717
|
} else {
|
|
1638
1718
|
log.status(
|
|
1639
|
-
`[vx] ${node.id}: ${what}
|
|
1719
|
+
`[vx] ${node.id}: ${what} — the result stands, ` +
|
|
1640
1720
|
`but is not saved under a key that no longer describes it${hint}`,
|
|
1641
1721
|
)
|
|
1642
1722
|
}
|
|
@@ -1645,7 +1725,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1645
1725
|
return false
|
|
1646
1726
|
}
|
|
1647
1727
|
|
|
1648
|
-
if (movedFails) effectiveExitCode = 1
|
|
1728
|
+
if (movedFails !== undefined) effectiveExitCode = 1
|
|
1649
1729
|
|
|
1650
1730
|
if (unkeyedUpstream !== undefined && willWrite && effectiveExitCode === 0) {
|
|
1651
1731
|
log.status(
|
|
@@ -1670,10 +1750,15 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1670
1750
|
: {}),
|
|
1671
1751
|
...(inputChanges !== undefined ? { inputChanges } : {}),
|
|
1672
1752
|
...(unkeyed ? { unkeyed: true as const } : {}),
|
|
1753
|
+
...(saved ? { saved: true as const } : {}),
|
|
1673
1754
|
...(effectiveExitCode !== 0 && output !== undefined
|
|
1674
|
-
? { failedOutput: failedOutputLog(output, willSave) }
|
|
1755
|
+
? { failedOutput: failedOutputLog(output, willSave || args.keepRunLog === true) }
|
|
1756
|
+
: {}),
|
|
1757
|
+
...(args.keepRunLog === true && (effectiveExitCode !== 0 || !willSave) && output !== undefined
|
|
1758
|
+
? { runLog: storedOutput() }
|
|
1675
1759
|
: {}),
|
|
1676
1760
|
...(result.timedOut === true && effectiveExitCode !== 0 ? { timedOut: true as const } : {}),
|
|
1761
|
+
...(movedFails !== undefined ? { inputMoved: movedFails } : {}),
|
|
1677
1762
|
...(result.cpuMs !== undefined ? { cpuMs: result.cpuMs } : {}),
|
|
1678
1763
|
...(result.peakRssBytes !== undefined ? { peakRssBytes: result.peakRssBytes } : {}),
|
|
1679
1764
|
...(result.where !== undefined ? { where: result.where } : {}),
|
|
@@ -1704,7 +1789,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1704
1789
|
/**
|
|
1705
1790
|
* Drop the run's facts about files a task may have written without
|
|
1706
1791
|
* declaring them (`undeclaredWriteReach`): the project's git snapshot and
|
|
1707
|
-
* its index OIDs
|
|
1792
|
+
* its index OIDs. The next reader
|
|
1708
1793
|
* re-enumerates the project (one `git ls-files`) and hashes its files by
|
|
1709
1794
|
* content; the per-declaration file-list memos are keyed on the snapshot
|
|
1710
1795
|
* array, so they miss with it (item 743).
|
|
@@ -1713,12 +1798,10 @@ function forgetUndeclaredWrites(args: ExecuteArgs, reach: 'none' | 'project' | '
|
|
|
1713
1798
|
if (reach === 'none') return
|
|
1714
1799
|
if (reach === 'workspace') {
|
|
1715
1800
|
args.gitFilesCache?.clear()
|
|
1716
|
-
args.hashCache?.packageJson.clear()
|
|
1717
1801
|
return
|
|
1718
1802
|
}
|
|
1719
1803
|
args.gitFilesCache?.delete(args.node.projectDir)
|
|
1720
1804
|
args.gitFilesCache?.invalidateWorkspacePartition()
|
|
1721
|
-
args.hashCache?.packageJson.delete(args.node.projectDir)
|
|
1722
1805
|
}
|
|
1723
1806
|
|
|
1724
1807
|
/** The two bin directories a task's PATH starts with: its own, then the root's (once when they coincide). */
|
|
@@ -1765,16 +1848,11 @@ function taskEnv(node: TaskNode, step: ExecConfig, workspaceRoot: string): NodeJ
|
|
|
1765
1848
|
return env
|
|
1766
1849
|
}
|
|
1767
1850
|
|
|
1768
|
-
|
|
1769
|
-
|
|
1851
|
+
/** A task's own `exec.timeout` in ms; the schema has checked its form. */
|
|
1852
|
+
function taskTimeoutMs(step: ExecConfig): number | undefined {
|
|
1853
|
+
return step.timeout === undefined ? undefined : parseDuration(step.timeout)!
|
|
1770
1854
|
}
|
|
1771
1855
|
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
* `VX_READY_NOTICE_MS` overrides it, as `VX_KILL_GRACE_MS` does the grace.
|
|
1775
|
-
*/
|
|
1776
|
-
function readyNoticeMs(): number {
|
|
1777
|
-
const raw = process.env['VX_READY_NOTICE_MS']
|
|
1778
|
-
if (raw !== undefined && /^[0-9]+$/.test(raw) && Number(raw) > 0) return Number(raw)
|
|
1779
|
-
return 10_000
|
|
1856
|
+
function isAborted(signal: AbortSignal | undefined): boolean {
|
|
1857
|
+
return signal?.aborted === true
|
|
1780
1858
|
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// drift into opposite verdicts on identical data. Both build on
|
|
5
5
|
// `mixedOutcomeKeysSql` + `failureModeOf`, so the rule cannot fork again.
|
|
6
6
|
// The per-run detector (`detectFlaky`) and the doctor's list (`flakyTasks`)
|
|
7
|
-
// read the same per-key projection (`keyOutcomesSql`)
|
|
7
|
+
// read the same per-key projection (`keyOutcomesSql`) over the shared
|
|
8
|
+
// store's flaky record (`task_outcomes`, v38), so every worktree of a repo
|
|
9
|
+
// judges a key on what all of them saw.
|
|
8
10
|
|
|
9
11
|
import type { Database } from 'bun:sqlite'
|
|
10
12
|
import { KEYED_RUNS_SQL } from '../cache/index.js'
|
|
@@ -17,6 +19,9 @@ const HIT_STATUSES = `(${TASK_STATUSES.filter(isCacheHit)
|
|
|
17
19
|
.map((s) => `'${s}'`)
|
|
18
20
|
.join(', ')})`
|
|
19
21
|
|
|
22
|
+
/** The shared store's flaky record: what `detectFlaky` and `flakyTasks` read. */
|
|
23
|
+
const OUTCOMES = 'task_outcomes'
|
|
24
|
+
|
|
20
25
|
export type FailureMode = 'stable' | 'flaky-recoverable' | 'flaky-fatal'
|
|
21
26
|
|
|
22
27
|
/**
|
|
@@ -33,6 +38,10 @@ export type FailureMode = 'stable' | 'flaky-recoverable' | 'flaky-fatal'
|
|
|
33
38
|
* projection.
|
|
34
39
|
*/
|
|
35
40
|
function keyOutcomesSql(source: string, where = ''): string {
|
|
41
|
+
// The flaky record keeps only rows that observed their key (run-history.ts
|
|
42
|
+
// `observesKey`), so it carries none of the filter's columns.
|
|
43
|
+
const keyed =
|
|
44
|
+
source === OUTCOMES ? '1' : `${KEYED_RUNS_SQL} AND cached IS NOT 0 AND blocked_by IS NULL`
|
|
36
45
|
return `SELECT project, task, hash, SUM(failed) AS failures, SUM(passed) AS passes,
|
|
37
46
|
SUM(retried) AS retried_failures,
|
|
38
47
|
SUM(CASE WHEN failed = 1 AND id > first_pass THEN 1 ELSE 0 END) AS relapses
|
|
@@ -41,10 +50,10 @@ function keyOutcomesSql(source: string, where = ''): string {
|
|
|
41
50
|
FROM (SELECT project, task, hash, id,
|
|
42
51
|
CASE WHEN status = 'failed' THEN 1 ELSE 0 END AS failed,
|
|
43
52
|
CASE WHEN status = 'success' AND attempts > 1 THEN attempts - 1 ELSE 0 END AS retried,
|
|
44
|
-
CASE WHEN status = 'success' OR status IN ${HIT_STATUSES} OR cache_hit = 1
|
|
53
|
+
CASE WHEN status = 'success' OR status IN ${HIT_STATUSES}${source === OUTCOMES ? '' : ' OR cache_hit = 1'}
|
|
45
54
|
THEN 1 ELSE 0 END AS passed
|
|
46
55
|
FROM ${source}
|
|
47
|
-
WHERE ${
|
|
56
|
+
WHERE ${keyed}${where}))
|
|
48
57
|
GROUP BY project, task, hash`
|
|
49
58
|
}
|
|
50
59
|
|
|
@@ -136,9 +145,9 @@ const keyOf = (c: { project: string; task: string; hash: string }): string =>
|
|
|
136
145
|
* failures is a recovery, not a flake. The counts fold this run in.
|
|
137
146
|
*
|
|
138
147
|
* The cost follows the run's colour. No candidate (every task a hit or a
|
|
139
|
-
* skip): no query. A green miss: one probe of the `
|
|
140
|
-
* index for the keys that ever failed —
|
|
141
|
-
* probe is microseconds however long the history. Only a key that DID fail
|
|
148
|
+
* skip): no query. A green miss: one probe of the `task_outcomes_hash`
|
|
149
|
+
* index for the keys that ever failed — a key holds a handful of rows, so
|
|
150
|
+
* the probe is microseconds however long the history. Only a key that DID fail
|
|
142
151
|
* before, or a task failing now, pays the projection scan over its own
|
|
143
152
|
* keys, and a run that is failing has more to look at than a scan.
|
|
144
153
|
*/
|
|
@@ -149,8 +158,8 @@ export function detectFlaky(db: Database, candidates: readonly FlakyCandidate[])
|
|
|
149
158
|
chunked(passedNow, (chunk) => {
|
|
150
159
|
const rows = db
|
|
151
160
|
.query(
|
|
152
|
-
`SELECT DISTINCT project, task, hash FROM
|
|
153
|
-
WHERE status = 'failed' AND
|
|
161
|
+
`SELECT DISTINCT project, task, hash FROM ${OUTCOMES}
|
|
162
|
+
WHERE status = 'failed' AND hash IN (${chunk.map(() => '?').join(', ')})`,
|
|
154
163
|
)
|
|
155
164
|
.all(...chunk.map((c) => c.hash)) as { project: string; task: string; hash: string }[]
|
|
156
165
|
for (const r of rows) everFailed.add(keyOf(r))
|
|
@@ -162,7 +171,7 @@ export function detectFlaky(db: Database, candidates: readonly FlakyCandidate[])
|
|
|
162
171
|
const counts = new Map<string, { passes: number; failures: number; relapses: number }>()
|
|
163
172
|
chunked(suspects, (chunk) => {
|
|
164
173
|
const rows = db
|
|
165
|
-
.query(keyOutcomesSql(
|
|
174
|
+
.query(keyOutcomesSql(OUTCOMES, ` AND hash IN (${chunk.map(() => '?').join(', ')})`))
|
|
166
175
|
.all(...chunk.map((c) => c.hash)) as {
|
|
167
176
|
project: string
|
|
168
177
|
task: string
|
|
@@ -212,7 +221,7 @@ export function flakyTasks(db: Database): FlakyTask[] {
|
|
|
212
221
|
.query(
|
|
213
222
|
`SELECT project, task, COUNT(*) AS keys, SUM(passes) AS passes, SUM(failures) AS failures
|
|
214
223
|
FROM (SELECT project, task, passes, failures + retried_failures AS failures
|
|
215
|
-
FROM (${keyOutcomesSql(
|
|
224
|
+
FROM (${keyOutcomesSql(OUTCOMES)})
|
|
216
225
|
WHERE relapses > 0 OR retried_failures > 0)
|
|
217
226
|
GROUP BY project, task
|
|
218
227
|
ORDER BY failures DESC, passes DESC, project, task`,
|