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/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
- else if (token.startsWith('-')) {
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
- else {
53
- selector.push(token);
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
- return degraded('advisory lock is held');
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;AACpD,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,WAAW,EACX,UAAU,GACX,MAAM,kBAAkB,CAAA;AAEzB,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAE3D,CAAA;AAED,MAAM,CAAC,MAAM,cAAc,GAAW,GAAG,CAAC,OAAO,CAAA;AAiBjD,SAAS,kBAAkB;IACzB,OAAO,IAAI,CACT,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EACvC,UAAU,EACV,QAAQ,EACR,aAAa,CACd,CAAA;AACH,CAAC;AAED,4EAA4E;AAC5E,SAAS,eAAe,CAAC,GAAa;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAE,CAAA;QACrB,IAAI,+BAA+B,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,QAAQ;YACnE,OAAO,CAAC,CAAA;IACZ,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAa;IAIhD,MAAM,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAA;IACvD,MAAM,OAAO,GAAa,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAA;IAC/C,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,KAAK,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAE,CAAA;QACrB,IAAI,KAAK,KAAK,KAAK,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,EAAE,CAAC;YACrC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;aAAM,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACtB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;AAC9B,CAAC;AAED,SAAS,QAAQ,CAAC,GAAa,EAAE,WAAmB;IAClD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;YACzC,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,mBAAmB,EAAE,WAAW,EAAE;YACzD,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC;SACnC,CAAC,CAAA;QACF,MAAM,GAAG,GAAa,EAAE,CAAA;QACxB,MAAM,GAAG,GAAa,EAAE,CAAA;QACxB,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QACnD,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QACnD,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QACzB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YACjC,MAAM,QAAQ,GACZ,IAAI,KAAK,IAAI;gBACX,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YACxD,OAAO,CAAC;gBACN,QAAQ;gBACR,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC;gBAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC;aAC3B,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,MAAsB;IAC1C,MAAM,KAAK,GAA4C;QACrD,MAAM,EAAE,CAAC;QACT,MAAM,EAAE,CAAC;QACT,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE;KACZ,CAAA;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAa,EACb,GAAW;IAEX,MAAM,WAAW,GAAa,EAAE,CAAA;IAChC,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,kBAAkB,UAAU,EAAE,OAAO,CAAC,CAAA;IAEzE,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,QAAQ,GACZ,SAAS,KAAK,IAAI;QAChB,CAAC,CAAC;YACE,GAAG,GAAG;YACN,oBAAoB;YACpB,cAAc,kBAAkB,EAAE,EAAE;YACpC,uBAAuB;SACxB;QACH,CAAC,CAAC,GAAG,CAAA;IAET,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;IAEnD,MAAM,QAAQ,GAAG,CAAC,GAAW,EAAa,EAAE;QAC1C,WAAW,CAAC,IAAI,CAAC,wCAAwC,GAAG,GAAG,CAAC,CAAA;QAChE,OAAO;YACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,IAAI;YACd,WAAW;YACX,SAAS,EAAE,KAAK,CAAC,MAAM;YACvB,SAAS,EAAE,KAAK,CAAC,MAAM;SACxB,CAAA;IACH,CAAC,CAAA;IAED,IAAI,OAAgB,CAAA;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAY,CAAA;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CACb,yEAAyE,CAC1E,CAAA;IACH,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACtC,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAA;QACvC,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,QAAQ,CAAC,2BAA2B,CAAC,CAAA;QAEnE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAA;QACvD,MAAM,GAAG,GAAkB;YACzB,QAAQ;YACR,YAAY,EAAE,QAAQ;YACtB,MAAM,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC;YACjC,MAAM,EAAE,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YAC9D,OAAO;YACP,QAAQ;YACR,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC;YAC7B,UAAU,EAAE,MAAM,UAAU,CAAC,QAAQ,CAAC;YACtC,eAAe,EAAE,eAAe,CAAC,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;YACnE,aAAa,EAAE,KAAK,CAAC,QAAQ;YAC7B,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YACxC,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YACxC,cAAc,EAAE,cAAc;YAC9B,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE;SACzB,CAAA;QACD,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;QAE3C,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAA;QACpC,KAAK,CAAC,MAAM,EAAE,CAAA;QACd,KAAK,CAAC,WAAW,EAAE,CAAA;QACnB,IAAI,KAAa,CAAA;QACjB,IAAI,CAAC;YACH,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAA;QACtC,CAAC;gBAAS,CAAC;YACT,KAAK,CAAC,WAAW,EAAE,CAAA;QACrB,CAAC;QAED,MAAM,MAAM,GAAG,qBAAqB,CAAC,KAAK,EAAE,KAAK,EAAE;YACjD,IAAI,EAAE,qBAAqB;SAC5B,CAAC,CAAA;QACF,OAAO;YACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,MAAM;YACN,QAAQ,EAAE,KAAK;YACf,WAAW;YACX,SAAS,EAAE,KAAK,CAAC,MAAM;YACvB,SAAS,EAAE,KAAK,CAAC,MAAM;SACxB,CAAA;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,aAAa,EAAE,CAAC;YACjC,OAAO,QAAQ,CAAC,uBAAuB,CAAC,CAAA;QAC1C,CAAC;QACD,OAAO,QAAQ,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;IACnE,CAAC;AACH,CAAC"}
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
- constructor();
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
- constructor(worktreeRoot: string);
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
- /** Advisory lock via mkdir; throws LockHeldError when already held (fail-open at the caller, INV-5). */
24
- acquireLock(): void;
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
  }