vdelta 0.2.1 → 0.3.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 CHANGED
@@ -18,7 +18,7 @@ another" — a verification delta can, and when two runs are not comparable it
18
18
  - Runner support (MVP): **vitest v4** (native reporter, structured channel only)
19
19
  - Zero runtime dependencies; Node ≥ 22
20
20
  - Machine-verified against the [conformance suite](conformance/) —
21
- 42 fixtures covering the spec's invariants, adversarial inputs, and a
21
+ 46 fixtures covering the spec's invariants, adversarial inputs, and a
22
22
  10-mutation cheating corpus with 100% detection recall
23
23
 
24
24
  ## Quickstart (5 minutes)
@@ -225,7 +225,7 @@ intended success mode.
225
225
 
226
226
  ```bash
227
227
  npm test # unit + full conformance suite
228
- npm run test:conformance # the 42-fixture suite only
228
+ npm run test:conformance # the 46-fixture suite only
229
229
  ```
230
230
 
231
231
  The suite is authored independently of this implementation (the fixture
@@ -239,6 +239,36 @@ staleness (INV-11), and read-only object-database recording (tree
239
239
  digesting never writes loose objects into the observed repository's
240
240
  `.git`, even when it is not writable).
241
241
 
242
+ ## Supported platforms
243
+
244
+ vdelta requires Node.js ≥ 22 (see `engines.node`) and is developed and
245
+ tested on Linux and macOS. Windows support is experimental.
246
+
247
+ | OS | Support level |
248
+ |---|---|
249
+ | Linux (ubuntu) | Fully tested in CI: unit + full conformance suite, on every PR, across Node 22 and 24 |
250
+ | macOS | Tested in CI: unit + CLI suites on every PR (Node 24); full conformance suite runs weekly and on manual `workflow_dispatch` |
251
+ | Windows | Experimental: unit + CLI suites run on every PR as a non-blocking check (Node 24); failures do not block merges. Full conformance suite runs weekly and on manual `workflow_dispatch` |
252
+
253
+ ### CI coverage policy
254
+
255
+ The full conformance suite (46 fixtures, ~230 seconds, spawning real
256
+ `vitest` child processes per fixture) runs on every pull request for
257
+ Linux only. macOS and Windows run a lighter `unit` + `cli` suite on every
258
+ PR instead, and the full conformance suite for those two platforms is
259
+ deferred to a weekly scheduled run and to manual `workflow_dispatch`
260
+ runs. This keeps required PR checks fast and keeps runner cost down —
261
+ macOS and Windows GitHub-hosted runners are billed at several times the
262
+ Linux rate, and the `cli` project already exercises real CLI process
263
+ spawning, temp-file handling, and path handling, which gives
264
+ representative cross-platform coverage without paying the full
265
+ conformance suite's runtime on every PR.
266
+
267
+ The Windows leg runs as a non-blocking check (`continue-on-error`), so
268
+ its pass/fail status never blocks a merge; results are still visible in
269
+ the CI run logs. If it stays consistently green over time, it can be
270
+ promoted to a required, blocking check in a follow-up change.
271
+
242
272
  ## License
243
273
 
244
274
  MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The adapter seam. The core (`run` / `compare` / `gate`) never reaches into a
3
+ * concrete runner: it resolves an {@link Adapter} descriptor from the registry
4
+ * (`src/adapters/registry.ts`) and talks to it through this interface only.
5
+ *
6
+ * Every type here is a promotion of a shape that already existed on the vitest
7
+ * side, so the seam is a rearrangement of what already ran rather than a new
8
+ * mechanism: {@link RecordContext} is the recorder's context object verbatim,
9
+ * {@link DetectResult} is the runner-token scan's return value,
10
+ * {@link InstrumentedChild} is the argv/env injection `vdelta run` already
11
+ * performed, and {@link CommandSelector} is the §6.4 inclusion-intent split.
12
+ *
13
+ * Dependency direction inside an adapter is one-way: `detect` / `instrument` /
14
+ * `splitCommandSelector` know only the runner's *argv surface*, `record` knows
15
+ * only its *structured channel*.
16
+ */
17
+ import type { RunRecord } from './schema.js';
18
+ /**
19
+ * The three-valued capability convention (§3.4). The value set is closed
20
+ * (§14); capability *names* stay open so an adapter may declare its own.
21
+ */
22
+ export declare const CAPABILITY_VALUES: readonly ["pass", "fail", "unsupported"];
23
+ export type CapabilityValue = (typeof CAPABILITY_VALUES)[number];
24
+ export type CapabilityDeclaration = Readonly<Record<string, CapabilityValue>>;
25
+ /**
26
+ * Where the adapter's structured channel lands. A single capture file is the
27
+ * only kind today; per-process channels are follow-up F-2, at which point the
28
+ * channel's creation and teardown move from the core into the adapter.
29
+ */
30
+ export interface CaptureChannel {
31
+ readonly kind: 'single-file';
32
+ readonly path: string;
33
+ }
34
+ /** What {@link Adapter.detect} returns; `null` means only "not mine". */
35
+ export interface DetectResult {
36
+ /** argv index of the runner binary token. */
37
+ readonly tokenIndex: number;
38
+ }
39
+ /**
40
+ * The instrumented child invocation. `argv` fully replaces the caller's argv;
41
+ * `env` carries only the additions to layer onto the inherited environment.
42
+ */
43
+ export interface InstrumentedChild {
44
+ readonly argv: readonly string[];
45
+ readonly env: Readonly<Record<string, string>>;
46
+ }
47
+ /** The §6.4 inclusion-intent split of a child argv (§5.1 canonical command). */
48
+ export interface CommandSelector {
49
+ command: string[];
50
+ selector: string[];
51
+ }
52
+ /**
53
+ * Everything the recorder needs beyond the runner's own structured channel:
54
+ * repo identity, provenance, the canonicalized invocation and the child's raw
55
+ * streams. Runner-neutral by construction — no field carries runner vocabulary.
56
+ */
57
+ export interface RecordContext {
58
+ worktree: string;
59
+ repoIdentity: string;
60
+ branch: string;
61
+ cwdRel: string;
62
+ command: string[];
63
+ selector: string[];
64
+ head: string | null;
65
+ treeDigest: string;
66
+ dirtyDiffDigest: string;
67
+ childExitCode: number;
68
+ rawStdout: string;
69
+ rawStderr: string;
70
+ adapterVersion: string;
71
+ recordedAtMs: number;
72
+ }
73
+ /**
74
+ * The capability names a declaration says the composition cannot meet, sorted
75
+ * (§4.2 disclosure rule). `unsupported` is the only degraded value: `fail`
76
+ * means the capability is met and the observation is red, not that evidence is
77
+ * missing. This is what a report's `failure_evidence.degraded_capabilities`
78
+ * and every per-test claim that carries it are derived from.
79
+ */
80
+ export declare function degradedCapabilities(caps: CapabilityDeclaration): string[];
81
+ /** §6.4 selector containment, as declared by an adapter that can decide it. */
82
+ export type SelectorRelation = 'equal' | 'subset' | 'superset' | 'disjoint' | 'unknown';
83
+ export type SelectorMatch = 'yes' | 'no' | 'unknown';
84
+ /**
85
+ * The capture channel is unreadable, of an unsupported version, or malformed.
86
+ * The core maps this onto degraded raw passthrough (INV-5): veridelta is never
87
+ * worse than its absence.
88
+ */
89
+ export declare class AdapterCaptureError extends Error {
90
+ constructor(message: string);
91
+ }
92
+ export interface Adapter {
93
+ readonly name: string;
94
+ readonly compositionId: string;
95
+ readonly declaredCapabilities: CapabilityDeclaration;
96
+ readonly declaredEnvVars: readonly string[];
97
+ /**
98
+ * Whether this argv is this adapter's invocation. `null` means "not mine"
99
+ * and nothing else — an adapter alone can never conclude "this is a wrapper
100
+ * command"; that is a registry-level conclusion.
101
+ */
102
+ detect(argv: readonly string[]): DetectResult | null;
103
+ /**
104
+ * The env that points this adapter's reporter at the channel.
105
+ *
106
+ * Exported to the child *whether or not* the adapter claimed the argv. A
107
+ * reporter registered in the project's own configuration (spec §4.2 ambient
108
+ * recording — the RECOMMENDED deployment) has no other way to find the
109
+ * channel, and it is deliberately inert without it. Gating this on argv
110
+ * detection would silently stop recording every wrapper invocation
111
+ * (`vdelta run -- npm test`), which is exactly the stream-severing §4.2
112
+ * warns about.
113
+ *
114
+ * The channel is one file today, so two adapters pointing their reporters at
115
+ * it would race (last writer wins). That is the pre-existing single-channel
116
+ * hazard follow-up F-2 exists for, not something detection was protecting
117
+ * against — pre-seam this env was exported unconditionally.
118
+ */
119
+ channelEnv(channel: CaptureChannel): Readonly<Record<string, string>>;
120
+ /**
121
+ * The reporter-injected argv plus the env additions for the child. Only
122
+ * applied when this adapter recognizes the argv: injecting runner flags into
123
+ * a command that is not that runner kills it outright (INV-5 — veridelta is
124
+ * never worse than its absence), and a wrapper cannot forward them anyway
125
+ * (§4.3-7). {@link channelEnv} is what covers the uninstrumented case.
126
+ */
127
+ instrument(argv: readonly string[], channel: CaptureChannel): InstrumentedChild;
128
+ /**
129
+ * Whether the capture now sitting in the channel is this adapter's, decided
130
+ * on the channel's own payload rather than on argv.
131
+ *
132
+ * Consulted only when no adapter claimed the argv: the ambient case, where
133
+ * the reporter came from the project's configuration instead of from
134
+ * {@link instrument}, so the argv carries no evidence of which runner ran.
135
+ * MUST NOT throw — an absent, empty, foreign or malformed channel is simply
136
+ * not a claim. A claim asserts authorship only; a payload this adapter owns
137
+ * but cannot use still fails in {@link record}, with its own diagnostic.
138
+ */
139
+ claimsCapture(channel: CaptureChannel): boolean;
140
+ /** §6.4 inclusion intent, decided purely on the runner's own CLI surface. */
141
+ splitCommandSelector(argv: readonly string[]): CommandSelector;
142
+ /**
143
+ * Read the capture channel, validate it, and build the Run record. Parsing
144
+ * and version checking belong to the *adapter* — the core knows nothing
145
+ * about the channel's payload. Failures throw {@link AdapterCaptureError},
146
+ * which the core maps onto degraded passthrough.
147
+ */
148
+ record(channel: CaptureChannel, ctx: RecordContext): RunRecord;
149
+ /**
150
+ * §6.4, optional. Undeclared means every relation is `unknown`, which is
151
+ * exactly the comparator's pre-seam behavior (`selector-relation-unknown`).
152
+ */
153
+ selectorRelation?(a: readonly string[], b: readonly string[]): SelectorRelation;
154
+ selectorMatches?(selector: readonly string[], testId: string): SelectorMatch;
155
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The three-valued capability convention (§3.4). The value set is closed
3
+ * (§14); capability *names* stay open so an adapter may declare its own.
4
+ */
5
+ export const CAPABILITY_VALUES = ['pass', 'fail', 'unsupported'];
6
+ /**
7
+ * The capability names a declaration says the composition cannot meet, sorted
8
+ * (§4.2 disclosure rule). `unsupported` is the only degraded value: `fail`
9
+ * means the capability is met and the observation is red, not that evidence is
10
+ * missing. This is what a report's `failure_evidence.degraded_capabilities`
11
+ * and every per-test claim that carries it are derived from.
12
+ */
13
+ export function degradedCapabilities(caps) {
14
+ return Object.entries(caps)
15
+ .filter(([, value]) => value === 'unsupported')
16
+ .map(([name]) => name)
17
+ .sort();
18
+ }
19
+ /**
20
+ * The capture channel is unreadable, of an unsupported version, or malformed.
21
+ * The core maps this onto degraded raw passthrough (INV-5): veridelta is never
22
+ * worse than its absence.
23
+ */
24
+ export class AdapterCaptureError extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = 'AdapterCaptureError';
28
+ }
29
+ }
30
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAU,CAAA;AAyDzE;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAA2B;IAC9D,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,aAAa,CAAC;SAC9C,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC;SACrB,IAAI,EAAE,CAAA;AACX,CAAC;AAWD;;;;GAIG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;IACnC,CAAC;CACF"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The adapter registry: the one place the core is allowed to know which
3
+ * concrete adapters exist. A static array in a fixed order — no registration
4
+ * API until adapters become external plugins.
5
+ *
6
+ * Detection is deliberately *total*: every adapter's `detect` is evaluated and
7
+ * the outcome distinguishes zero, one, and several matches, because only the
8
+ * registry can tell "no adapter recognizes this argv" (a wrapper command such
9
+ * as `pnpm test`) from "this adapter is not the one". Zero and ambiguous both
10
+ * stay recordable-but-degraded at the call site, never a hard failure (INV-5).
11
+ */
12
+ import type { Adapter, CaptureChannel, DetectResult } from '../adapter.js';
13
+ /** Every known adapter, in a deterministic order. */
14
+ export declare const ADAPTERS: readonly Adapter[];
15
+ /** Known adapter names, in registry order — for diagnostics and `--adapter`. */
16
+ export declare function adapterNames(adapters?: readonly Adapter[]): readonly string[];
17
+ /**
18
+ * Look an adapter up by name without failing. Callers that must stay
19
+ * fail-closed rather than abort — the comparator and the gate, which resolve
20
+ * `record.instrument.adapter` from records they did not write — use this.
21
+ */
22
+ export declare function findAdapter(name: string, adapters?: readonly Adapter[]): Adapter | undefined;
23
+ /**
24
+ * Resolve an explicitly requested adapter name. An unknown name is user input
25
+ * error, not a degradation path: silently passing through would turn a typo
26
+ * into a silent loss of recording, which is the opposite of INV-5's intent.
27
+ */
28
+ export declare function resolveAdapter(name: string, adapters?: readonly Adapter[]): Adapter;
29
+ /**
30
+ * The env additions every registered adapter needs to find the channel, in
31
+ * registry order. Exported to the child unconditionally, detection or no
32
+ * detection: a reporter configured in the project itself (spec §4.2 ambient
33
+ * recording) is inert without it, and pre-seam `vdelta run` set it on every
34
+ * child for exactly that reason.
35
+ *
36
+ * Later adapters win on a colliding name. With one single-file channel that
37
+ * collision is the F-2 hazard, not a decision this function can make.
38
+ */
39
+ export declare function ambientChannelEnv(channel: CaptureChannel, adapters?: readonly Adapter[]): Record<string, string>;
40
+ /**
41
+ * Which adapter owns the capture that landed in the channel, or `null` when
42
+ * none claims it. The ambient counterpart of {@link detectAdapter}: the argv
43
+ * said nothing, so the payload decides.
44
+ *
45
+ * Registry order breaks a tie. Two adapters claiming one capture would mean
46
+ * two reporters wrote to the same single-file channel, which is the F-2
47
+ * last-writer-wins hazard rather than an ambiguity the user could resolve —
48
+ * there is no argv to name a winner with, so degrading here would discard a
49
+ * capture that is genuinely present.
50
+ */
51
+ export declare function claimCapture(channel: CaptureChannel, adapters?: readonly Adapter[]): Adapter | null;
52
+ /**
53
+ * Outcome of evaluating every adapter's `detect` against one child argv.
54
+ * `none` and `ambiguous` are both "the registry declines to choose": the
55
+ * caller degrades to raw passthrough and says why.
56
+ */
57
+ export type AdapterDetection = {
58
+ readonly kind: 'none';
59
+ } | {
60
+ readonly kind: 'unique';
61
+ readonly adapter: Adapter;
62
+ readonly detected: DetectResult;
63
+ } | {
64
+ readonly kind: 'ambiguous';
65
+ readonly candidates: readonly Adapter[];
66
+ };
67
+ /**
68
+ * Evaluate *all* adapters against the argv. Never short-circuits on the first
69
+ * match: two adapters claiming the same argv is a distinguishable outcome that
70
+ * must reach the user as a candidate list, not be hidden by registry order.
71
+ */
72
+ export declare function detectAdapter(argv: readonly string[], adapters?: readonly Adapter[]): AdapterDetection;
@@ -0,0 +1,78 @@
1
+ import { vitestAdapter } from './vitest/adapter.js';
2
+ /** Every known adapter, in a deterministic order. */
3
+ export const ADAPTERS = [vitestAdapter];
4
+ /** Known adapter names, in registry order — for diagnostics and `--adapter`. */
5
+ export function adapterNames(adapters = ADAPTERS) {
6
+ return adapters.map((a) => a.name);
7
+ }
8
+ /**
9
+ * Look an adapter up by name without failing. Callers that must stay
10
+ * fail-closed rather than abort — the comparator and the gate, which resolve
11
+ * `record.instrument.adapter` from records they did not write — use this.
12
+ */
13
+ export function findAdapter(name, adapters = ADAPTERS) {
14
+ return adapters.find((a) => a.name === name);
15
+ }
16
+ /**
17
+ * Resolve an explicitly requested adapter name. An unknown name is user input
18
+ * error, not a degradation path: silently passing through would turn a typo
19
+ * into a silent loss of recording, which is the opposite of INV-5's intent.
20
+ */
21
+ export function resolveAdapter(name, adapters = ADAPTERS) {
22
+ const adapter = findAdapter(name, adapters);
23
+ if (adapter === undefined) {
24
+ throw new Error(`unknown adapter '${name}' — known adapters: ${adapterNames(adapters).join(', ')}`);
25
+ }
26
+ return adapter;
27
+ }
28
+ /**
29
+ * The env additions every registered adapter needs to find the channel, in
30
+ * registry order. Exported to the child unconditionally, detection or no
31
+ * detection: a reporter configured in the project itself (spec §4.2 ambient
32
+ * recording) is inert without it, and pre-seam `vdelta run` set it on every
33
+ * child for exactly that reason.
34
+ *
35
+ * Later adapters win on a colliding name. With one single-file channel that
36
+ * collision is the F-2 hazard, not a decision this function can make.
37
+ */
38
+ export function ambientChannelEnv(channel, adapters = ADAPTERS) {
39
+ const env = {};
40
+ for (const adapter of adapters) {
41
+ Object.assign(env, adapter.channelEnv(channel));
42
+ }
43
+ return env;
44
+ }
45
+ /**
46
+ * Which adapter owns the capture that landed in the channel, or `null` when
47
+ * none claims it. The ambient counterpart of {@link detectAdapter}: the argv
48
+ * said nothing, so the payload decides.
49
+ *
50
+ * Registry order breaks a tie. Two adapters claiming one capture would mean
51
+ * two reporters wrote to the same single-file channel, which is the F-2
52
+ * last-writer-wins hazard rather than an ambiguity the user could resolve —
53
+ * there is no argv to name a winner with, so degrading here would discard a
54
+ * capture that is genuinely present.
55
+ */
56
+ export function claimCapture(channel, adapters = ADAPTERS) {
57
+ return adapters.find((a) => a.claimsCapture(channel)) ?? null;
58
+ }
59
+ /**
60
+ * Evaluate *all* adapters against the argv. Never short-circuits on the first
61
+ * match: two adapters claiming the same argv is a distinguishable outcome that
62
+ * must reach the user as a candidate list, not be hidden by registry order.
63
+ */
64
+ export function detectAdapter(argv, adapters = ADAPTERS) {
65
+ const matches = [];
66
+ for (const adapter of adapters) {
67
+ const detected = adapter.detect(argv);
68
+ if (detected !== null)
69
+ matches.push({ adapter, detected });
70
+ }
71
+ if (matches.length === 0)
72
+ return { kind: 'none' };
73
+ if (matches.length > 1)
74
+ return { kind: 'ambiguous', candidates: matches.map((m) => m.adapter) };
75
+ const only = matches[0];
76
+ return { kind: 'unique', adapter: only.adapter, detected: only.detected };
77
+ }
78
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/adapters/registry.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,qDAAqD;AACrD,MAAM,CAAC,MAAM,QAAQ,GAAuB,CAAC,aAAa,CAAC,CAAA;AAE3D,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAC1B,WAA+B,QAAQ;IAEvC,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,IAAY,EACZ,WAA+B,QAAQ;IAEvC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAC9C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,WAA+B,QAAQ;IAEvC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IAC3C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,oBAAoB,IAAI,uBAAuB,YAAY,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACnF,CAAA;IACH,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAuB,EACvB,WAA+B,QAAQ;IAEvC,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAA;IACjD,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAuB,EACvB,WAA+B,QAAQ;IAEvC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,IAAI,CAAA;AAC/D,CAAC;AAgBD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAuB,EACvB,WAA+B,QAAQ;IAEvC,MAAM,OAAO,GAAmD,EAAE,CAAA;IAClE,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACrC,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAA;IAC5D,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IACjD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAA;IACzE,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAE,CAAA;IACxB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAA;AAC3E,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { type Adapter, type CapabilityDeclaration, type CommandSelector } from '../../adapter.js';
2
+ /**
3
+ * The invocation's selector is its inclusion intent (§6.4): the vitest CLI
4
+ * positional filters. The canonical command excludes them (§5.1).
5
+ *
6
+ * `--flag value` pairs for known value-taking flags (see
7
+ * {@link VITEST_VALUE_FLAGS}) are folded into a single `--flag=value`
8
+ * canonical token so that this form and the pre-joined `--flag=value` form
9
+ * produce byte-identical `command` arrays (and thus the same stream key).
10
+ */
11
+ export declare function splitCommandSelector(cmd: readonly string[]): CommandSelector;
12
+ /**
13
+ * Initial capability declaration for `vitest-native/1` (§3.4). Reproduces the
14
+ * composition's documented standing byte for byte: only `source-region-text`
15
+ * is degraded (CE-1 — vitest's structured channel carries no failing-source
16
+ * region text), everything else this composition claims is met.
17
+ */
18
+ export declare const VITEST_CAPABILITIES: CapabilityDeclaration;
19
+ export declare const vitestAdapter: Adapter;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * vitest adapter, descriptor side: the runner-facing half of the seam
3
+ * (`src/adapter.ts`). Everything vitest-specific about *invoking* a run lives
4
+ * here — locating the vitest binary in the child argv, injecting the capture
5
+ * reporter, and splitting inclusion intent out of the command (§6.4) using
6
+ * vitest's own CLI surface. The recorder half (capture → RunRecord) stays in
7
+ * `./recorder.ts`; this module only owns reading and parsing the channel.
8
+ */
9
+ import { readFileSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { AdapterCaptureError, } from '../../adapter.js';
13
+ import { ADAPTER_NAME, buildRunRecord, COMPOSITION_ID, DECLARED_ENV_VARS, } from './recorder.js';
14
+ /**
15
+ * Absolute path of the in-process vitest reporter that writes the capture.
16
+ * Resolved relative to this module so it points at the built sibling
17
+ * (`dist/adapters/vitest/reporter.js`) rather than at a source path.
18
+ */
19
+ function reporterModulePath() {
20
+ return join(dirname(fileURLToPath(import.meta.url)), 'reporter.js');
21
+ }
22
+ /**
23
+ * The one env var the capture reporter reads (`reporter.ts:61`). Held here
24
+ * rather than inline so `channelEnv` and the reporter's contract stay a single
25
+ * fact; the reporter is inert without it, by design, so that it can sit
26
+ * permanently in a project's vitest config (spec §4.2 ambient recording).
27
+ */
28
+ const CAPTURE_FILE_ENV = 'VDELTA_CAPTURE_FILE';
29
+ function channelEnv(channel) {
30
+ return { [CAPTURE_FILE_ENV]: channel.path };
31
+ }
32
+ /** Parse the channel, or `undefined` when there is nothing readable in it. */
33
+ function readCapture(channel) {
34
+ try {
35
+ return JSON.parse(readFileSync(channel.path, 'utf8'));
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ /** Locate the vitest invocation inside the child argv; null when absent. */
42
+ function findVitestToken(cmd) {
43
+ for (let i = 0; i < cmd.length; i++) {
44
+ const token = cmd[i];
45
+ if (/(^|[\\/])vitest(\.mjs|\.js)?$/.test(token) || token === 'vitest')
46
+ return i;
47
+ }
48
+ return null;
49
+ }
50
+ /**
51
+ * vitest 4.x CLI flags that always take their value as a *separate* argv
52
+ * token (`--flag value`), never combined into the flag token itself. Used
53
+ * by {@link splitCommandSelector} to recognize `--flag value` pairs and fold
54
+ * them into a single `--flag=value` canonical token so that space-separated
55
+ * and `=`-joined invocations normalize to the same command array (and
56
+ * therefore the same stream key — see `streamKey` in src/compare.ts).
57
+ *
58
+ * Deliberately excludes flags whose value is *optional*
59
+ * (`--changed`, `--silent`, `--coverage`, `--browser`, `--inspect`, etc.):
60
+ * for those, the token following the flag cannot be distinguished from a
61
+ * positional selector without vitest's own arg-parsing rules, so folding
62
+ * them here would risk silently swallowing a selector token. Flags outside
63
+ * this list keep the historical (pre-fix) behavior: a space-separated value
64
+ * is treated as a selector token, which may cause selector-based stream
65
+ * splitting and an abstain (`comparability: 'none'`) rather than a
66
+ * false-positive comparison.
67
+ *
68
+ * Maintenance: this list targets vitest 4.x. Revisit when bumping the
69
+ * vitest minor/major version (see issue #15 Open Question — no automated
70
+ * mechanism keeps this in sync with vitest's own CLI surface).
71
+ */
72
+ const VITEST_VALUE_FLAGS = new Set([
73
+ '--project',
74
+ '--config',
75
+ '-c',
76
+ '--root',
77
+ '-r',
78
+ '--dir',
79
+ '--reporter',
80
+ '--outputFile',
81
+ '--pool',
82
+ '--maxWorkers',
83
+ '--minWorkers',
84
+ '--environment',
85
+ '--testNamePattern',
86
+ '-t',
87
+ '--testTimeout',
88
+ '--hookTimeout',
89
+ '--teardownTimeout',
90
+ '--retry',
91
+ '--bail',
92
+ '--maxConcurrency',
93
+ '--shard',
94
+ '--exclude',
95
+ '--mode',
96
+ '--workspace',
97
+ ]);
98
+ /**
99
+ * The invocation's selector is its inclusion intent (§6.4): the vitest CLI
100
+ * positional filters. The canonical command excludes them (§5.1).
101
+ *
102
+ * `--flag value` pairs for known value-taking flags (see
103
+ * {@link VITEST_VALUE_FLAGS}) are folded into a single `--flag=value`
104
+ * canonical token so that this form and the pre-joined `--flag=value` form
105
+ * produce byte-identical `command` arrays (and thus the same stream key).
106
+ */
107
+ export function splitCommandSelector(cmd) {
108
+ const idx = findVitestToken(cmd);
109
+ if (idx === null)
110
+ return { command: [...cmd], selector: [] };
111
+ const command = cmd.slice(0, idx + 1);
112
+ const selector = [];
113
+ for (let i = idx + 1; i < cmd.length; i++) {
114
+ const token = cmd[i];
115
+ if (token === 'run' && i === idx + 1) {
116
+ command.push(token);
117
+ continue;
118
+ }
119
+ if (!token.startsWith('-')) {
120
+ selector.push(token);
121
+ continue;
122
+ }
123
+ if (token.includes('=')) {
124
+ command.push(token);
125
+ continue;
126
+ }
127
+ const next = cmd[i + 1];
128
+ if (VITEST_VALUE_FLAGS.has(token) &&
129
+ next !== undefined &&
130
+ !next.startsWith('-')) {
131
+ command.push(`${token}=${next}`);
132
+ i++;
133
+ continue;
134
+ }
135
+ command.push(token);
136
+ }
137
+ return { command, selector };
138
+ }
139
+ /**
140
+ * Initial capability declaration for `vitest-native/1` (§3.4). Reproduces the
141
+ * composition's documented standing byte for byte: only `source-region-text`
142
+ * is degraded (CE-1 — vitest's structured channel carries no failing-source
143
+ * region text), everything else this composition claims is met.
144
+ */
145
+ export const VITEST_CAPABILITIES = {
146
+ verdicts: 'pass',
147
+ 'source-location': 'pass',
148
+ suppression: 'pass',
149
+ inventory: 'pass',
150
+ 'failure-evidence': 'pass',
151
+ 'source-region-text': 'unsupported',
152
+ };
153
+ export const vitestAdapter = {
154
+ name: ADAPTER_NAME,
155
+ compositionId: COMPOSITION_ID,
156
+ declaredCapabilities: VITEST_CAPABILITIES,
157
+ declaredEnvVars: DECLARED_ENV_VARS,
158
+ detect(argv) {
159
+ const i = findVitestToken(argv);
160
+ return i === null ? null : { tokenIndex: i };
161
+ },
162
+ channelEnv,
163
+ instrument(argv, channel) {
164
+ return {
165
+ argv: [
166
+ ...argv,
167
+ '--reporter=default',
168
+ `--reporter=${reporterModulePath()}`,
169
+ '--includeTaskLocation',
170
+ ],
171
+ env: channelEnv(channel),
172
+ };
173
+ },
174
+ splitCommandSelector,
175
+ claimsCapture(channel) {
176
+ // Authorship only, and from the payload's own self-identification
177
+ // (`capture.ts:34`, a literal `'vitest'`). Deliberately not a version or
178
+ // shape check: a capture this adapter wrote but cannot read must reach
179
+ // `record` so the run degrades with *that* diagnostic ("unsupported
180
+ // capture version N") instead of the generic "is the child a vitest
181
+ // invocation?", which is what the pre-seam code path said.
182
+ return readCapture(channel)?.runner === 'vitest';
183
+ },
184
+ record(channel, ctx) {
185
+ const capture = readCapture(channel);
186
+ if (capture === undefined) {
187
+ throw new AdapterCaptureError('no capture from the vitest reporter — is the child a vitest invocation?');
188
+ }
189
+ return buildRunRecord(capture, ctx);
190
+ },
191
+ };
192
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../../src/adapters/vitest/adapter.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  * out-of-process recorder (`vdelta run`). Raw structured-channel data only;
4
4
  * canonicalization/redaction/digesting happen in the recorder.
5
5
  */
6
- export declare const CAPTURE_VERSION = 1;
6
+ export declare const CAPTURE_VERSION = 2;
7
7
  export interface CapturedError {
8
8
  name: string;
9
9
  message: string;
@@ -47,4 +47,14 @@ export interface Capture {
47
47
  rel: string;
48
48
  messages: string[];
49
49
  }[];
50
+ /**
51
+ * Absolute paths of the config files vite/vitest actually resolved for this
52
+ * run: the union of `configFile` and `configFileDependencies` across the
53
+ * global vite dev server and every workspace `TestProject`'s dev server.
54
+ * Sorted and deduped by the reporter. The recorder digests each of these
55
+ * paths directly (no more decide-file-name guessing) and keys the result
56
+ * with a worktree-relative path, or `external:<abs path>` for anything
57
+ * outside the worktree.
58
+ */
59
+ config_files: string[];
50
60
  }
@@ -3,5 +3,5 @@
3
3
  * out-of-process recorder (`vdelta run`). Raw structured-channel data only;
4
4
  * canonicalization/redaction/digesting happen in the recorder.
5
5
  */
6
- export const CAPTURE_VERSION = 1;
6
+ export const CAPTURE_VERSION = 2;
7
7
  //# sourceMappingURL=capture.js.map