supercov 0.0.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.
@@ -0,0 +1,132 @@
1
+ import type { CoverageCarrier, CoverageExecutionScope } from "./types.ts";
2
+
3
+ export const COVERAGE_SCOPE_HEADER = "x-supercov-scope";
4
+ export const COVERAGE_PHASE_HEADER = "x-supercov-phase";
5
+ export const COVERAGE_CARRIER_ENV = "SUPERCOV_CONTEXT";
6
+ export const DEFAULT_SERVER_EVIDENCE_ROOT =
7
+ "/tmp/supercov-server-evidence";
8
+
9
+ function nonEmpty(value: string | null): value is string {
10
+ return typeof value === "string" && value.length > 0;
11
+ }
12
+
13
+ function safeKey(value: string): boolean {
14
+ return /^[a-zA-Z0-9_-]+$/.test(value);
15
+ }
16
+
17
+ function pathComponent(value: string): string {
18
+ const safe = value.replace(/[^a-zA-Z0-9_-]/g, "_");
19
+ return safe || "unscoped";
20
+ }
21
+
22
+ export function encodeCoverageScope(scope: CoverageExecutionScope): string {
23
+ return new URLSearchParams({
24
+ v: String(scope.version),
25
+ r: scope.runId,
26
+ w: scope.workerId,
27
+ t: scope.testId,
28
+ k: scope.testKey,
29
+ a: String(scope.retry),
30
+ i: scope.attemptId,
31
+ }).toString();
32
+ }
33
+
34
+ export function decodeCoverageScope(
35
+ encoded: string | undefined,
36
+ ): CoverageExecutionScope | undefined {
37
+ if (!encoded) return undefined;
38
+ try {
39
+ const values = new URLSearchParams(encoded);
40
+ const runId = values.get("r");
41
+ const workerId = values.get("w");
42
+ const testId = values.get("t");
43
+ const testKey = values.get("k");
44
+ const attemptId = values.get("i");
45
+ const retry = Number(values.get("a"));
46
+ if (
47
+ values.get("v") !== "1" ||
48
+ !nonEmpty(runId) ||
49
+ !nonEmpty(workerId) ||
50
+ !nonEmpty(testId) ||
51
+ !nonEmpty(testKey) ||
52
+ !safeKey(testKey) ||
53
+ !nonEmpty(attemptId) ||
54
+ !safeKey(attemptId) ||
55
+ !Number.isSafeInteger(retry) ||
56
+ retry < 0
57
+ )
58
+ return undefined;
59
+ return {
60
+ version: 1,
61
+ runId,
62
+ workerId,
63
+ testId,
64
+ testKey,
65
+ retry,
66
+ attemptId,
67
+ };
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ export function encodeCoverageCarrier(carrier: CoverageCarrier): string {
74
+ return Buffer.from(JSON.stringify(carrier), "utf8").toString("base64url");
75
+ }
76
+
77
+ export function decodeCoverageCarrier(
78
+ encoded: string | undefined,
79
+ ): CoverageCarrier | undefined {
80
+ if (!encoded) return undefined;
81
+ try {
82
+ const value = JSON.parse(
83
+ Buffer.from(encoded, "base64url").toString("utf8"),
84
+ ) as CoverageCarrier;
85
+ if (value.version !== 1) return undefined;
86
+ if (value.scope) {
87
+ const roundTrip = decodeCoverageScope(encodeCoverageScope(value.scope));
88
+ if (!roundTrip) return undefined;
89
+ }
90
+ if (value.phaseId !== undefined && value.phaseId.length === 0)
91
+ return undefined;
92
+ return value;
93
+ } catch {
94
+ return undefined;
95
+ }
96
+ }
97
+
98
+ export function serverRunEvidenceDirectory(
99
+ runId: string,
100
+ root = DEFAULT_SERVER_EVIDENCE_ROOT,
101
+ ): string {
102
+ return `${root.replace(/\/+$/, "")}/${pathComponent(runId)}`;
103
+ }
104
+
105
+ export function serverEvidenceDirectory(
106
+ scope: CoverageExecutionScope,
107
+ root = DEFAULT_SERVER_EVIDENCE_ROOT,
108
+ ): string {
109
+ return `${serverRunEvidenceDirectory(scope.runId, root)}/${pathComponent(scope.workerId)}/${scope.testKey}/${scope.retry}`;
110
+ }
111
+
112
+ export function serverEvidencePath(
113
+ scope: CoverageExecutionScope,
114
+ root = DEFAULT_SERVER_EVIDENCE_ROOT,
115
+ ): string {
116
+ return `${serverEvidenceDirectory(scope, root)}/server.jsonl`;
117
+ }
118
+
119
+ export function backgroundEvidenceDirectory(
120
+ runId: string,
121
+ root = DEFAULT_SERVER_EVIDENCE_ROOT,
122
+ ): string {
123
+ return `${serverRunEvidenceDirectory(runId, root)}/background`;
124
+ }
125
+
126
+ export function backgroundEvidencePath(
127
+ runId: string,
128
+ processId = typeof process === "undefined" ? "unknown" : String(process.pid),
129
+ root = DEFAULT_SERVER_EVIDENCE_ROOT,
130
+ ): string {
131
+ return `${backgroundEvidenceDirectory(runId, root)}/${pathComponent(processId)}.jsonl`;
132
+ }
package/src/types.ts ADDED
@@ -0,0 +1,412 @@
1
+ export interface McdcDecisionMeta {
2
+ id: string;
3
+ file: string;
4
+ line: number;
5
+ column: number;
6
+ source: string;
7
+ conditions: string[];
8
+ kind: "if" | "ternary" | "while" | "do-while" | "for";
9
+ }
10
+
11
+ export interface McdcVector {
12
+ values: Array<boolean | null>;
13
+ outcome: boolean;
14
+ }
15
+
16
+ export interface McdcDecisionSnapshot {
17
+ meta: McdcDecisionMeta;
18
+ vectors: McdcVector[];
19
+ }
20
+
21
+ export type CoveragePointKind = "statement" | "function";
22
+
23
+ export interface CoveragePointMeta {
24
+ id: string;
25
+ kind: CoveragePointKind;
26
+ file: string;
27
+ line: number;
28
+ column: number;
29
+ source: string;
30
+ label?: string;
31
+ }
32
+
33
+ export type CoverageBranchKind =
34
+ | "logical-value"
35
+ | "logical-assignment"
36
+ | "optional-chain"
37
+ | "default-value"
38
+ | "try-catch"
39
+ | "for-in"
40
+ | "for-of"
41
+ | "switch"
42
+ | "dynamic-code";
43
+
44
+ export interface CoverageBranchMeta {
45
+ id: string;
46
+ kind: CoverageBranchKind;
47
+ file: string;
48
+ line: number;
49
+ column: number;
50
+ source: string;
51
+ alternatives: Array<{
52
+ id: string;
53
+ label: string;
54
+ }>;
55
+ }
56
+
57
+ export interface CoverageManifest {
58
+ decisions: McdcDecisionMeta[];
59
+ points: CoveragePointMeta[];
60
+ branches: CoverageBranchMeta[];
61
+ limitations?: CoverageLimitation[];
62
+ }
63
+
64
+ export interface CoverageLimitation {
65
+ id: string;
66
+ kind: "dynamic-code";
67
+ file: string;
68
+ line: number;
69
+ column: number;
70
+ source: string;
71
+ reason: string;
72
+ }
73
+
74
+ export interface CoverageRuntimeSnapshot {
75
+ decisions: McdcDecisionSnapshot[];
76
+ hits: string[];
77
+ events?: CoverageRuntimeEvent[];
78
+ }
79
+
80
+ export interface CoverageExecutionScope {
81
+ version: 1;
82
+ runId: string;
83
+ workerId: string;
84
+ testId: string;
85
+ testKey: string;
86
+ retry: number;
87
+ attemptId: string;
88
+ }
89
+
90
+ export type CoverageRuntimeEvent =
91
+ | {
92
+ type: "hit";
93
+ id: string;
94
+ timestampMs: number;
95
+ phaseId?: string;
96
+ environment: "browser" | "server";
97
+ }
98
+ | {
99
+ type: "decision";
100
+ id: string;
101
+ vector: McdcVector;
102
+ timestampMs: number;
103
+ phaseId?: string;
104
+ environment: "browser" | "server";
105
+ };
106
+
107
+ export type CoverageServerRecord =
108
+ | {
109
+ type: "decision";
110
+ meta: McdcDecisionMeta;
111
+ vector: McdcVector;
112
+ timestampMs?: number;
113
+ phaseId?: string;
114
+ scope?: CoverageExecutionScope;
115
+ }
116
+ | {
117
+ type: "hit";
118
+ id: string;
119
+ timestampMs?: number;
120
+ phaseId?: string;
121
+ scope?: CoverageExecutionScope;
122
+ };
123
+
124
+ export interface CoverageCarrier {
125
+ version: 1;
126
+ scope?: CoverageExecutionScope;
127
+ phaseId?: string;
128
+ }
129
+
130
+ export interface CoveragePhase {
131
+ id: string;
132
+ kind: "action" | "assertion";
133
+ operation: string;
134
+ source?: string;
135
+ causedByPhaseId?: string;
136
+ startedAtMs: number;
137
+ endedAtMs?: number;
138
+ status?: "passed" | "failed";
139
+ error?: string;
140
+ }
141
+
142
+ export interface TestProvenance {
143
+ /** The process responsible for executing the test, such as playwright or vitest. */
144
+ runner: string;
145
+ /** The semantic testing level, such as unit, integration, e2e, or component. */
146
+ kind: string;
147
+ project?: string;
148
+ /** How the kind was established so inferred labels are never presented as explicit. */
149
+ source: "explicit" | "project" | "path" | "runner-default" | "unknown";
150
+ }
151
+
152
+ export interface McdcRawTestResult {
153
+ testId?: string;
154
+ /** Run/worker/test/retry identity for an exact concurrently executed attempt. */
155
+ scope?: CoverageExecutionScope;
156
+ test: string;
157
+ testFile?: string;
158
+ title?: string;
159
+ retry?: number;
160
+ /** Outcome of this exact attempt, normalized across test runners. */
161
+ status?: TestAttemptStatus;
162
+ /** Runner-level expected outcome (notably Playwright's test.fail()). */
163
+ expectedStatus?: TestAttemptStatus;
164
+ /** True when the runner reports that an earlier attempt failed. */
165
+ flaky?: boolean;
166
+ provenance?: TestProvenance;
167
+ role?: "test" | "setup" | "background";
168
+ phases?: CoveragePhase[];
169
+ runtime?: CoverageRuntimeSnapshot[];
170
+ browser: CoverageRuntimeSnapshot[];
171
+ server: CoverageServerRecord[];
172
+ }
173
+
174
+ export type TestAttemptStatus =
175
+ | "passed"
176
+ | "failed"
177
+ | "skipped"
178
+ | "timedOut"
179
+ | "interrupted"
180
+ | "unknown";
181
+
182
+ export type TestOutcome =
183
+ | "passed"
184
+ | "failed"
185
+ | "flaky"
186
+ | "skipped"
187
+ | "timedOut"
188
+ | "interrupted"
189
+ | "unknown";
190
+
191
+ export interface TestAttemptResult {
192
+ retry: number;
193
+ status: TestAttemptStatus;
194
+ expectedStatus?: TestAttemptStatus;
195
+ }
196
+
197
+ export interface McdcVectorObservation {
198
+ vector: McdcVector;
199
+ tests: string[];
200
+ phases?: string[];
201
+ explicitPhases?: string[];
202
+ confidence?: CoverageConfidence;
203
+ }
204
+
205
+ export interface CoverageConfidence {
206
+ level: "unexecuted" | "executed" | "action" | "asserted";
207
+ setupOnly: boolean;
208
+ backgroundOnly: boolean;
209
+ asserted: boolean;
210
+ tests: string[];
211
+ assertedTests: string[];
212
+ runners: string[];
213
+ kinds: string[];
214
+ e2e: boolean;
215
+ }
216
+
217
+ export interface McdcConditionResult {
218
+ index: number;
219
+ source: string;
220
+ covered: boolean;
221
+ witness?: [McdcVector, McdcVector];
222
+ witnessTests?: [string[], string[]];
223
+ assertionCovered?: boolean;
224
+ }
225
+
226
+ export interface McdcDecisionResult {
227
+ meta: McdcDecisionMeta;
228
+ executed: boolean;
229
+ covered: boolean;
230
+ vectors: McdcVector[];
231
+ vectorObservations: McdcVectorObservation[];
232
+ conditions: McdcConditionResult[];
233
+ tests: string[];
234
+ confidence?: CoverageConfidence;
235
+ }
236
+
237
+ export interface CoverageCount {
238
+ covered: number;
239
+ total: number;
240
+ percentage: number;
241
+ }
242
+
243
+ export interface CoveragePointResult {
244
+ meta: CoveragePointMeta;
245
+ covered: boolean;
246
+ tests: string[];
247
+ phases?: string[];
248
+ confidence?: CoverageConfidence;
249
+ }
250
+
251
+ export interface CoverageBranchResult {
252
+ meta: CoverageBranchMeta;
253
+ covered: boolean;
254
+ alternatives: Array<{
255
+ id: string;
256
+ label: string;
257
+ covered: boolean;
258
+ tests: string[];
259
+ phases?: string[];
260
+ confidence?: CoverageConfidence;
261
+ }>;
262
+ }
263
+
264
+ export interface CoveragePhaseResult extends CoveragePhase {
265
+ test: string;
266
+ hits: string[];
267
+ decisions: Array<{
268
+ id: string;
269
+ vectors: McdcVector[];
270
+ }>;
271
+ lines: Array<{
272
+ file: string;
273
+ line: number;
274
+ }>;
275
+ browserEvents: number;
276
+ serverEvents: number;
277
+ explicitEvents: number;
278
+ inferredEvents: number;
279
+ explicitBrowserEvents: number;
280
+ inferredBrowserEvents: number;
281
+ explicitServerEvents: number;
282
+ inferredServerEvents: number;
283
+ }
284
+
285
+ export interface TestCoverageResult {
286
+ id: string;
287
+ name: string;
288
+ file?: string;
289
+ title?: string;
290
+ retries: number[];
291
+ attempts: TestAttemptResult[];
292
+ outcome: TestOutcome;
293
+ provenance: TestProvenance;
294
+ role: "test" | "setup" | "background";
295
+ hits: string[];
296
+ decisions: Array<{
297
+ id: string;
298
+ vectors: McdcVector[];
299
+ }>;
300
+ lines: Array<{
301
+ file: string;
302
+ line: number;
303
+ }>;
304
+ }
305
+
306
+ export interface TestFileCoverageResult {
307
+ file: string;
308
+ tests: string[];
309
+ runners: string[];
310
+ kinds: string[];
311
+ lines: Array<{
312
+ file: string;
313
+ line: number;
314
+ }>;
315
+ }
316
+
317
+ export interface CoverageSummary {
318
+ decisions: number;
319
+ executedDecisions: number;
320
+ coveredDecisions: number;
321
+ conditions: number;
322
+ coveredConditions: number;
323
+ conditionCoveragePct: number;
324
+ lines: CoverageCount;
325
+ statements: CoverageCount;
326
+ functions: CoverageCount;
327
+ branches: CoverageCount;
328
+ decisionOutcomes: CoverageCount;
329
+ conditionOutcomes: CoverageCount;
330
+ valueSelections: CoverageCount;
331
+ coverageComplete: boolean;
332
+ completenessBlocked?: boolean;
333
+ }
334
+
335
+ export interface CoverageRunFingerprint {
336
+ algorithm: "sha256";
337
+ source: string;
338
+ tests: string;
339
+ dependencies: string;
340
+ configuration: string;
341
+ instrumenter: string;
342
+ combined: string;
343
+ sourceFiles: number;
344
+ testFiles: number;
345
+ }
346
+
347
+ export interface CoverageRunIntegrity {
348
+ schemaVersion: number;
349
+ instrumenterVersion: string;
350
+ git?: {
351
+ revision?: string;
352
+ dirty: boolean;
353
+ };
354
+ fingerprint: CoverageRunFingerprint;
355
+ stale?: boolean;
356
+ staleReasons?: string[];
357
+ }
358
+
359
+ export interface McdcCoverageView {
360
+ generatedAt: string;
361
+ variant: "masking-short-circuit";
362
+ model: {
363
+ name: string;
364
+ completenessMeaning: string;
365
+ measured: string[];
366
+ notMeasured: string[];
367
+ };
368
+ integrity?: CoverageRunIntegrity;
369
+ limitations?: CoverageLimitation[];
370
+ summary: CoverageSummary;
371
+ coverageByKind: Array<{
372
+ kind: string;
373
+ tests: number;
374
+ setups: number;
375
+ summary: CoverageSummary;
376
+ }>;
377
+ coverageByRunner: Array<{
378
+ runner: string;
379
+ tests: number;
380
+ setups: number;
381
+ summary: CoverageSummary;
382
+ }>;
383
+ decisions: McdcDecisionResult[];
384
+ points: CoveragePointResult[];
385
+ branches: CoverageBranchResult[];
386
+ tests: TestCoverageResult[];
387
+ testFiles: TestFileCoverageResult[];
388
+ phases: CoveragePhaseResult[];
389
+ lines: Array<{
390
+ file: string;
391
+ line: number;
392
+ covered: boolean;
393
+ tests: string[];
394
+ runners: string[];
395
+ kinds: string[];
396
+ exclusiveKind?: string;
397
+ phases?: string[];
398
+ confidence?: CoverageConfidence;
399
+ }>;
400
+ }
401
+
402
+ export interface McdcReport extends McdcCoverageView {
403
+ execution?: {
404
+ testExitCode?: number | null;
405
+ valid: boolean;
406
+ };
407
+ /** Materialized evidence filters; top-level coverage contains all attempts. */
408
+ filters?: {
409
+ passed: McdcCoverageView;
410
+ failed: McdcCoverageView;
411
+ };
412
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { dirname, relative, resolve, sep } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import type { Plugin } from "vite";
11
+ import { instrumentMcdc, mcdcRuntimeModuleId } from "./instrumenter.ts";
12
+ import type {
13
+ CoverageBranchMeta,
14
+ CoverageLimitation,
15
+ CoveragePointMeta,
16
+ McdcDecisionMeta,
17
+ } from "./types.ts";
18
+
19
+ export interface McdcVitePluginOptions {
20
+ root?: string;
21
+ sourceRoots?: string[];
22
+ manifestPath?: string;
23
+ }
24
+
25
+ export function mcdcVitePlugin(options: McdcVitePluginOptions = {}): Plugin {
26
+ const root = options.root ?? process.cwd();
27
+ const sourceRoots = (options.sourceRoots ?? ["app", "src"])
28
+ .map((directory) => resolve(root, directory))
29
+ .filter((directory) => existsSync(directory));
30
+ const runtimePath = fileURLToPath(new URL("./runtime.ts", import.meta.url));
31
+ const manifestPath = options.manifestPath
32
+ ? resolve(root, options.manifestPath)
33
+ : resolve(root, ".supercov/mcdc-manifest.json");
34
+ const markerPath = resolve(dirname(manifestPath), ".mcdc-enabled");
35
+ const decisions = new Map<string, McdcDecisionMeta>();
36
+ const points = new Map<string, CoveragePointMeta>();
37
+ const branches = new Map<string, CoverageBranchMeta>();
38
+ const limitations = new Map<string, CoverageLimitation>();
39
+
40
+ const recordManifest = (manifest: {
41
+ decisions: McdcDecisionMeta[];
42
+ points: CoveragePointMeta[];
43
+ branches: CoverageBranchMeta[];
44
+ limitations?: CoverageLimitation[];
45
+ }): void => {
46
+ for (const decision of manifest.decisions)
47
+ decisions.set(decision.id, decision);
48
+ for (const point of manifest.points) points.set(point.id, point);
49
+ for (const branch of manifest.branches) branches.set(branch.id, branch);
50
+ for (const limitation of manifest.limitations ?? [])
51
+ limitations.set(limitation.id, limitation);
52
+ };
53
+
54
+ const sourceFiles = (directory: string): string[] =>
55
+ readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
56
+ const path = resolve(directory, entry.name);
57
+ if (entry.isDirectory()) return sourceFiles(path);
58
+ return /\.[cm]?[jt]sx?$/.test(entry.name) ? [path] : [];
59
+ });
60
+
61
+ return {
62
+ name: "supercov-mcdc",
63
+ enforce: "pre",
64
+ resolveId(id) {
65
+ if (id === mcdcRuntimeModuleId) return runtimePath;
66
+ return null;
67
+ },
68
+ buildStart() {
69
+ // Vite only transforms modules reachable from a build entry. Scan every
70
+ // source file up front so never-imported executable code is still in the
71
+ // denominator and appears as uncovered rather than disappearing.
72
+ for (const sourceRoot of sourceRoots) {
73
+ for (const id of sourceFiles(sourceRoot)) {
74
+ const file = relative(root, id).split(sep).join("/");
75
+ recordManifest(
76
+ instrumentMcdc(readFileSync(id, "utf8"), file).manifest,
77
+ );
78
+ }
79
+ }
80
+ },
81
+ transform(code, rawId) {
82
+ const id = rawId.split("?")[0] ?? rawId;
83
+ if (
84
+ !sourceRoots.some(
85
+ (sourceRoot) =>
86
+ id === sourceRoot || id.startsWith(`${sourceRoot}${sep}`),
87
+ ) ||
88
+ !/\.[cm]?[jt]sx?$/.test(id)
89
+ )
90
+ return null;
91
+ const file = relative(root, id).split(sep).join("/");
92
+ const result = instrumentMcdc(code, file);
93
+ recordManifest(result.manifest);
94
+ return {
95
+ code: result.code,
96
+ ...(result.map ? { map: JSON.parse(JSON.stringify(result.map)) } : {}),
97
+ };
98
+ },
99
+ closeBundle() {
100
+ mkdirSync(dirname(manifestPath), { recursive: true });
101
+ const sortByLocation = <
102
+ T extends { file: string; line: number; column: number },
103
+ >(
104
+ values: T[],
105
+ ): T[] =>
106
+ values.sort((left, right) =>
107
+ left.file === right.file
108
+ ? left.line - right.line || left.column - right.column
109
+ : left.file.localeCompare(right.file),
110
+ );
111
+ const manifest = {
112
+ decisions: sortByLocation([...decisions.values()]),
113
+ points: sortByLocation([...points.values()]),
114
+ branches: sortByLocation([...branches.values()]),
115
+ limitations: sortByLocation([...limitations.values()]),
116
+ };
117
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
118
+ writeFileSync(markerPath, "coverage-completeness-v2\n");
119
+ },
120
+ };
121
+ }