vdelta 0.1.1 → 0.2.0
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 +31 -5
- package/dist/cli-io.d.ts +10 -0
- package/dist/cli-io.js +48 -0
- package/dist/cli-io.js.map +1 -0
- package/dist/cli.js +111 -28
- package/dist/cli.js.map +1 -1
- package/dist/compare.d.ts +10 -3
- package/dist/compare.js +17 -8
- package/dist/compare.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/run.d.ts +5 -0
- package/dist/run.js +99 -6
- package/dist/run.js.map +1 -1
- package/dist/store.d.ts +154 -5
- package/dist/store.js +349 -8
- package/dist/store.js.map +1 -1
- package/package.json +2 -2
package/dist/run.js
CHANGED
|
@@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
import { buildRunRecord, } from './adapters/vitest/recorder.js';
|
|
16
16
|
import { buildComparisonReport } from './compare.js';
|
|
17
17
|
import { canonicalDigest } from './digest.js';
|
|
18
|
-
import { LockHeldError, RunStore } from './store.js';
|
|
18
|
+
import { defaultGcPolicy, LockHeldError, RunStore } from './store.js';
|
|
19
19
|
import { dirtyDiffMaterial, gitBranch, gitHead, gitRepoRoot, treeDigest, } from './tree-digest.js';
|
|
20
20
|
const pkg = createRequire(import.meta.url)('../package.json');
|
|
21
21
|
export const VDELTA_VERSION = pkg.version;
|
|
@@ -31,9 +31,62 @@ function findVitestToken(cmd) {
|
|
|
31
31
|
}
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* vitest 4.x CLI flags that always take their value as a *separate* argv
|
|
36
|
+
* token (`--flag value`), never combined into the flag token itself. Used
|
|
37
|
+
* by {@link splitCommandSelector} to recognize `--flag value` pairs and fold
|
|
38
|
+
* them into a single `--flag=value` canonical token so that space-separated
|
|
39
|
+
* and `=`-joined invocations normalize to the same command array (and
|
|
40
|
+
* therefore the same stream key — see `streamKey` in src/compare.ts).
|
|
41
|
+
*
|
|
42
|
+
* Deliberately excludes flags whose value is *optional*
|
|
43
|
+
* (`--changed`, `--silent`, `--coverage`, `--browser`, `--inspect`, etc.):
|
|
44
|
+
* for those, the token following the flag cannot be distinguished from a
|
|
45
|
+
* positional selector without vitest's own arg-parsing rules, so folding
|
|
46
|
+
* them here would risk silently swallowing a selector token. Flags outside
|
|
47
|
+
* this list keep the historical (pre-fix) behavior: a space-separated value
|
|
48
|
+
* is treated as a selector token, which may cause selector-based stream
|
|
49
|
+
* splitting and an abstain (`comparability: 'none'`) rather than a
|
|
50
|
+
* false-positive comparison.
|
|
51
|
+
*
|
|
52
|
+
* Maintenance: this list targets vitest 4.x. Revisit when bumping the
|
|
53
|
+
* vitest minor/major version (see issue #15 Open Question — no automated
|
|
54
|
+
* mechanism keeps this in sync with vitest's own CLI surface).
|
|
55
|
+
*/
|
|
56
|
+
const VITEST_VALUE_FLAGS = new Set([
|
|
57
|
+
'--project',
|
|
58
|
+
'--config',
|
|
59
|
+
'-c',
|
|
60
|
+
'--root',
|
|
61
|
+
'-r',
|
|
62
|
+
'--dir',
|
|
63
|
+
'--reporter',
|
|
64
|
+
'--outputFile',
|
|
65
|
+
'--pool',
|
|
66
|
+
'--maxWorkers',
|
|
67
|
+
'--minWorkers',
|
|
68
|
+
'--environment',
|
|
69
|
+
'--testNamePattern',
|
|
70
|
+
'-t',
|
|
71
|
+
'--testTimeout',
|
|
72
|
+
'--hookTimeout',
|
|
73
|
+
'--teardownTimeout',
|
|
74
|
+
'--retry',
|
|
75
|
+
'--bail',
|
|
76
|
+
'--maxConcurrency',
|
|
77
|
+
'--shard',
|
|
78
|
+
'--exclude',
|
|
79
|
+
'--mode',
|
|
80
|
+
'--workspace',
|
|
81
|
+
]);
|
|
34
82
|
/**
|
|
35
83
|
* The invocation's selector is its inclusion intent (§6.4): the vitest CLI
|
|
36
84
|
* positional filters. The canonical command excludes them (§5.1).
|
|
85
|
+
*
|
|
86
|
+
* `--flag value` pairs for known value-taking flags (see
|
|
87
|
+
* {@link VITEST_VALUE_FLAGS}) are folded into a single `--flag=value`
|
|
88
|
+
* canonical token so that this form and the pre-joined `--flag=value` form
|
|
89
|
+
* produce byte-identical `command` arrays (and thus the same stream key).
|
|
37
90
|
*/
|
|
38
91
|
export function splitCommandSelector(cmd) {
|
|
39
92
|
const idx = findVitestToken(cmd);
|
|
@@ -45,13 +98,25 @@ export function splitCommandSelector(cmd) {
|
|
|
45
98
|
const token = cmd[i];
|
|
46
99
|
if (token === 'run' && i === idx + 1) {
|
|
47
100
|
command.push(token);
|
|
101
|
+
continue;
|
|
48
102
|
}
|
|
49
|
-
|
|
103
|
+
if (!token.startsWith('-')) {
|
|
104
|
+
selector.push(token);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (token.includes('=')) {
|
|
50
108
|
command.push(token);
|
|
109
|
+
continue;
|
|
51
110
|
}
|
|
52
|
-
|
|
53
|
-
|
|
111
|
+
const next = cmd[i + 1];
|
|
112
|
+
if (VITEST_VALUE_FLAGS.has(token) &&
|
|
113
|
+
next !== undefined &&
|
|
114
|
+
!next.startsWith('-')) {
|
|
115
|
+
command.push(`${token}=${next}`);
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
54
118
|
}
|
|
119
|
+
command.push(token);
|
|
55
120
|
}
|
|
56
121
|
return { command, selector };
|
|
57
122
|
}
|
|
@@ -149,7 +214,10 @@ export async function runAndRecord(cmd, cwd) {
|
|
|
149
214
|
const record = buildRunRecord(capture, ctx);
|
|
150
215
|
const store = new RunStore(worktree);
|
|
151
216
|
store.ensure();
|
|
152
|
-
store.acquireLock();
|
|
217
|
+
const { reclaimed } = store.acquireLock();
|
|
218
|
+
if (reclaimed) {
|
|
219
|
+
diagnostics.push(`vdelta: reclaimed stale advisory lock at ${join(store.dir, 'lock')}`);
|
|
220
|
+
}
|
|
153
221
|
let runId;
|
|
154
222
|
try {
|
|
155
223
|
runId = store.writeRun(record).runId;
|
|
@@ -160,6 +228,28 @@ export async function runAndRecord(cmd, cwd) {
|
|
|
160
228
|
const report = buildComparisonReport(store, runId, {
|
|
161
229
|
mode: 'previous-comparable',
|
|
162
230
|
});
|
|
231
|
+
// Auto-GC (§4.1 SHOULD be bounded): keep the store from growing
|
|
232
|
+
// unbounded across repeated `vdelta run` invocations. Runs *after* the
|
|
233
|
+
// comparison above so the baseline it just resolved cannot be evicted
|
|
234
|
+
// out from under it — a comparison performed here always saw its
|
|
235
|
+
// baseline still present. The record just written is `last` and the
|
|
236
|
+
// resolved baseline (if any) are both protected for this pass, so
|
|
237
|
+
// neither is evicted even under a tight VDELTA_GC_MAX_COUNT/BYTES
|
|
238
|
+
// (AC-3). A GC failure (including a held lock) must not fail the run
|
|
239
|
+
// itself (INV-5 spirit) — it only downgrades to a diagnostic.
|
|
240
|
+
try {
|
|
241
|
+
store.acquireLock();
|
|
242
|
+
try {
|
|
243
|
+
const protectedIds = report.baseline ? [report.baseline.run_id] : [];
|
|
244
|
+
store.gc(defaultGcPolicy(), protectedIds);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
store.releaseLock();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
diagnostics.push(`vdelta: gc skipped (${e instanceof Error ? e.message : String(e)})`);
|
|
252
|
+
}
|
|
163
253
|
return {
|
|
164
254
|
exitCode: child.exitCode,
|
|
165
255
|
report,
|
|
@@ -171,7 +261,10 @@ export async function runAndRecord(cmd, cwd) {
|
|
|
171
261
|
}
|
|
172
262
|
catch (err) {
|
|
173
263
|
if (err instanceof LockHeldError) {
|
|
174
|
-
|
|
264
|
+
// err.message already carries the lock path and the `rm -rf` recovery
|
|
265
|
+
// hint (see LockHeldError in store.ts); acquireLock() has already
|
|
266
|
+
// auto-reclaimed any stale lock, so reaching here means a live holder.
|
|
267
|
+
return degraded(err.message);
|
|
175
268
|
}
|
|
176
269
|
return degraded(err instanceof Error ? err.message : String(err));
|
|
177
270
|
}
|
package/dist/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EACL,cAAc,GAEf,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
CHANGED
|
@@ -2,26 +2,152 @@
|
|
|
2
2
|
* Content-addressed run store (spec §4): immutable records, atomic writes,
|
|
3
3
|
* fail-open advisory lock, enforced gitignore. Recency is store insertion
|
|
4
4
|
* order (the append-only index), never timestamps (§7.8).
|
|
5
|
+
*
|
|
6
|
+
* The advisory lock (`.veridelta/lock`, mkdir-based for atomicity) is
|
|
7
|
+
* fail-open per INV-5: a lock that cannot be proven live degrades the
|
|
8
|
+
* caller to transparent passthrough rather than blocking indefinitely.
|
|
9
|
+
* On top of that, acquireLock() auto-reclaims *stale* locks so a crashed
|
|
10
|
+
* process doesn't wedge recording forever:
|
|
11
|
+
* - if the lock carries `meta.json` ({pid, acquired_at_ms}), staleness is
|
|
12
|
+
* decided purely by PID liveness (dead PID => stale, reclaim now;
|
|
13
|
+
* regardless of age otherwise a live holder is never stolen);
|
|
14
|
+
* - if `meta.json` is missing or unreadable/malformed (a legacy lock, or
|
|
15
|
+
* a write that failed), staleness falls back to an mtime threshold
|
|
16
|
+
* (`staleLockMs`, default 10 minutes) — this also preserves INV-5 for
|
|
17
|
+
* the existing fail-open-held-lock conformance fixture, whose fresh
|
|
18
|
+
* bare-mkdir lock must still degrade to passthrough.
|
|
19
|
+
* Reclaim moves the stale lock dir aside via renameSync (atomic: at most
|
|
20
|
+
* one concurrent reclaimer can walk off with that specific directory —
|
|
21
|
+
* a second racer's rename fails outright instead of silently trampling
|
|
22
|
+
* whatever now occupies the path) and deletes the moved-aside copy, then
|
|
23
|
+
* retries mkdir once; if that retry loses the race, we throw
|
|
24
|
+
* LockHeldError (fail-open), never loop. acquireLock() reports back
|
|
25
|
+
* whether it reclaimed a stale lock (and that lock's prior meta, if any)
|
|
26
|
+
* so callers can surface the event instead of reclaiming silently.
|
|
5
27
|
*/
|
|
6
|
-
import { type RunRecord } from './schema.js';
|
|
28
|
+
import { type CompletenessStatus, type RunRecord } from './schema.js';
|
|
7
29
|
export declare class StoreCorruptError extends Error {
|
|
8
30
|
constructor(message: string);
|
|
9
31
|
}
|
|
10
32
|
export declare class LockHeldError extends Error {
|
|
11
|
-
|
|
33
|
+
readonly lockPath: string;
|
|
34
|
+
constructor(lockPath: string);
|
|
12
35
|
}
|
|
13
36
|
/** run_id = content address of the record excluding the recording group (§3.5). */
|
|
14
37
|
export declare function computeRunId(record: RunRecord): string;
|
|
38
|
+
/** Retention limits for {@link RunStore.gc}. `undefined` disables that limit. */
|
|
39
|
+
export interface GcPolicy {
|
|
40
|
+
maxCount?: number;
|
|
41
|
+
maxBytes?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Outcome of a {@link RunStore.gc} pass. */
|
|
44
|
+
export interface GcResult {
|
|
45
|
+
removed: string[];
|
|
46
|
+
keptCount: number;
|
|
47
|
+
keptBytes: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Default retention policy (§4.1 SHOULD be bounded). Overridable via
|
|
51
|
+
* VDELTA_GC_MAX_COUNT / VDELTA_GC_MAX_BYTES (positive integers only; unset,
|
|
52
|
+
* empty, zero, or non-numeric values fall back to defaults or disable the
|
|
53
|
+
* limit respectively).
|
|
54
|
+
*/
|
|
55
|
+
export declare function defaultGcPolicy(): GcPolicy;
|
|
56
|
+
/** Lightweight, non-strict view of a run record's addressing fields. */
|
|
57
|
+
export interface RunMeta {
|
|
58
|
+
repo: {
|
|
59
|
+
identity: string;
|
|
60
|
+
worktree: string;
|
|
61
|
+
branch: string;
|
|
62
|
+
cwd: string;
|
|
63
|
+
};
|
|
64
|
+
invocation: {
|
|
65
|
+
command: string[];
|
|
66
|
+
selector: string[];
|
|
67
|
+
};
|
|
68
|
+
instrument: {
|
|
69
|
+
adapter: string;
|
|
70
|
+
adapter_version: string;
|
|
71
|
+
composition_id: string;
|
|
72
|
+
config_digest: string;
|
|
73
|
+
};
|
|
74
|
+
provenance: {
|
|
75
|
+
head: string | null;
|
|
76
|
+
tree_digest: string;
|
|
77
|
+
};
|
|
78
|
+
completeness: {
|
|
79
|
+
status: CompletenessStatus;
|
|
80
|
+
child_exit_code: number;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Options for RunStore's advisory lock stale-detection (testability + tuning). */
|
|
84
|
+
export interface RunStoreOptions {
|
|
85
|
+
/** mtime threshold (ms) for reclaiming a meta-less legacy lock. Default 10 minutes. */
|
|
86
|
+
staleLockMs?: number;
|
|
87
|
+
/** PID liveness probe. Default: process.kill(pid, 0) (ESRCH => dead, else alive). */
|
|
88
|
+
isPidAlive?: (pid: number) => boolean;
|
|
89
|
+
}
|
|
90
|
+
/** Parsed contents of a lock's `meta.json`. */
|
|
91
|
+
export interface LockMeta {
|
|
92
|
+
pid: number;
|
|
93
|
+
acquired_at_ms: number;
|
|
94
|
+
}
|
|
95
|
+
/** Result of acquireLock(): whether a stale lock had to be reclaimed. */
|
|
96
|
+
export interface AcquireLockResult {
|
|
97
|
+
/** True if the lock we now hold was reclaimed from a stale prior holder. */
|
|
98
|
+
reclaimed: boolean;
|
|
99
|
+
/** The reclaimed lock's prior meta.json, if it had one and it was readable. */
|
|
100
|
+
staleMeta: LockMeta | null;
|
|
101
|
+
}
|
|
15
102
|
export declare class RunStore {
|
|
16
103
|
readonly dir: string;
|
|
17
|
-
|
|
104
|
+
private readonly staleLockMs;
|
|
105
|
+
private readonly isPidAlive;
|
|
106
|
+
constructor(worktreeRoot: string, options?: RunStoreOptions);
|
|
18
107
|
private get runsDir();
|
|
19
108
|
private get indexPath();
|
|
20
109
|
private get lastPath();
|
|
21
110
|
private get lockPath();
|
|
111
|
+
private get lockMetaPath();
|
|
22
112
|
ensure(): void;
|
|
23
|
-
/**
|
|
24
|
-
|
|
113
|
+
/** Best-effort: write lock metadata for future stale-detection. Never throws. */
|
|
114
|
+
private writeLockMeta;
|
|
115
|
+
/**
|
|
116
|
+
* Read and parse the currently-held lock's meta.json.
|
|
117
|
+
* Returns null if it's missing, unreadable, unparseable, or malformed
|
|
118
|
+
* (no finite numeric `pid`) — i.e. whenever the lock must be treated as
|
|
119
|
+
* a legacy, meta-less lock.
|
|
120
|
+
*/
|
|
121
|
+
private readLockMeta;
|
|
122
|
+
/**
|
|
123
|
+
* Decide whether the currently-held lock is stale and may be reclaimed.
|
|
124
|
+
* - meta.json present and parseable with a finite numeric pid: stale iff
|
|
125
|
+
* that pid is not alive (PID liveness is authoritative, any age).
|
|
126
|
+
* - meta.json missing/unreadable/unparseable/malformed (legacy lock):
|
|
127
|
+
* stale iff the lock dir's mtime is older than staleLockMs (strict >).
|
|
128
|
+
*/
|
|
129
|
+
private isLockStale;
|
|
130
|
+
private isLegacyLockStale;
|
|
131
|
+
/**
|
|
132
|
+
* Move the stale lock dir aside (renameSync, atomic) and delete the
|
|
133
|
+
* moved-aside copy. If the rename fails — another process already moved
|
|
134
|
+
* or recreated the lock — we do nothing further: the mkdir retry in
|
|
135
|
+
* acquireLock() is the sole arbiter of who actually ends up holding the
|
|
136
|
+
* lock, so a lost race here just falls through to that retry instead of
|
|
137
|
+
* blindly deleting whatever now occupies the path (which is what let two
|
|
138
|
+
* racers both end up "holding" the lock under a plain rmSync).
|
|
139
|
+
*/
|
|
140
|
+
private reclaimStaleLock;
|
|
141
|
+
/**
|
|
142
|
+
* Advisory lock via mkdir (atomic). A stale lock (dead PID, or an aged
|
|
143
|
+
* legacy lock past staleLockMs) is auto-reclaimed: rename the stale dir
|
|
144
|
+
* aside and delete it, then retry mkdir once. If the lock is live, or
|
|
145
|
+
* the retry loses a race, throws LockHeldError — fail-open at the
|
|
146
|
+
* caller (INV-5). The return value reports whether a reclaim happened
|
|
147
|
+
* (and the reclaimed lock's prior meta, if any) so callers can surface
|
|
148
|
+
* the event rather than reclaiming silently.
|
|
149
|
+
*/
|
|
150
|
+
acquireLock(): AcquireLockResult;
|
|
25
151
|
releaseLock(): void;
|
|
26
152
|
/**
|
|
27
153
|
* Persist a record. Atomic (tmp+rename). Content-identical re-records are
|
|
@@ -38,6 +164,29 @@ export declare class RunStore {
|
|
|
38
164
|
resolveRunId(idOrPrefix: string): string | null;
|
|
39
165
|
/** Read and validate a record; parse/validation failures are store corruption. */
|
|
40
166
|
readRun(runId: string): RunRecord;
|
|
167
|
+
/**
|
|
168
|
+
* Read a record's addressing/provenance fields without the full §9.4
|
|
169
|
+
* schema validation (used for cheap baseline pre-filtering). Same
|
|
170
|
+
* missing/unparseable error text as {@link readRun}; extracted-field
|
|
171
|
+
* absence or type mismatch is also a StoreCorruptError. Does not inspect
|
|
172
|
+
* observations/finding/recording.
|
|
173
|
+
*/
|
|
174
|
+
readRunMeta(runId: string): RunMeta;
|
|
41
175
|
/** INV-10: recompute the content address and compare with the stored id. */
|
|
42
176
|
verifyIntegrity(runId: string): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Enforce a retention policy (§4.1 SHOULD be bounded). Evicts whole
|
|
179
|
+
* records (file + index entry), oldest first, until both limits are
|
|
180
|
+
* satisfied. The record pointed to by `last`, and any id passed in
|
|
181
|
+
* `protectedIds` (e.g. a baseline just selected for a comparison), are
|
|
182
|
+
* never evicted, even if one alone exceeds maxBytes (AC-3).
|
|
183
|
+
*
|
|
184
|
+
* PRECONDITION: the caller holds the advisory lock (see acquireLock()).
|
|
185
|
+
* gc() itself does not take the lock — callers that gc concurrently with a
|
|
186
|
+
* writer risk racing writeRun()'s index append.
|
|
187
|
+
*
|
|
188
|
+
* Index ids whose record file is already missing (dangling) are dropped
|
|
189
|
+
* from the index unconditionally, independent of the policy limits.
|
|
190
|
+
*/
|
|
191
|
+
gc(policy: GcPolicy, protectedIds?: readonly string[]): GcResult;
|
|
43
192
|
}
|