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,45 @@
1
+ import { resolve as resolvePath } from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ const GENERATED_TARGET = "__SUPERCOV_PLAYWRIGHT_MODULE__";
5
+ const TARGET =
6
+ process.env.SUPERCOV_PLAYWRIGHT_MODULE ??
7
+ (GENERATED_TARGET.startsWith("__") ? "@playwright/test" : GENERATED_TARGET);
8
+ const REPLACEMENT =
9
+ process.env.SUPERCOV_PLAYWRIGHT_WRAPPER ??
10
+ "./.supercov/playwright.ts";
11
+ const PROJECT_ROOT = process.env.SUPERCOV_PROJECT_ROOT;
12
+
13
+ function belongsToProject(parentURL) {
14
+ if (!parentURL || parentURL.includes("/node_modules/")) return false;
15
+ if (parentURL.includes("/.supercov/")) return false;
16
+ if (!PROJECT_ROOT) return parentURL.includes("/tests/");
17
+ const normalizedRoot = PROJECT_ROOT.replaceAll("\\", "/").replace(/\/$/, "");
18
+ return parentURL.startsWith(`file://${normalizedRoot}/`);
19
+ }
20
+
21
+ export async function resolve(specifier, context, nextResolve) {
22
+ if (
23
+ TARGET &&
24
+ REPLACEMENT &&
25
+ specifier === TARGET &&
26
+ belongsToProject(context.parentURL)
27
+ ) {
28
+ if (process.env.SUPERCOV_DEBUG === "1") {
29
+ console.error(
30
+ `[supercov] redirected ${specifier} for ${context.parentURL}`,
31
+ );
32
+ }
33
+ if (REPLACEMENT.startsWith("file:")) {
34
+ return { url: REPLACEMENT, shortCircuit: true };
35
+ }
36
+ if (REPLACEMENT.startsWith(".")) {
37
+ return {
38
+ url: pathToFileURL(resolvePath(process.cwd(), REPLACEMENT)).href,
39
+ shortCircuit: true,
40
+ };
41
+ }
42
+ return nextResolve(REPLACEMENT, context);
43
+ }
44
+ return nextResolve(specifier, context);
45
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,656 @@
1
+ import type {
2
+ CoverageCarrier,
3
+ CoverageExecutionScope,
4
+ CoverageRuntimeSnapshot,
5
+ CoverageRuntimeEvent,
6
+ CoverageServerRecord,
7
+ McdcDecisionMeta,
8
+ McdcDecisionSnapshot,
9
+ McdcVector,
10
+ } from "./types.ts";
11
+ import {
12
+ backgroundEvidenceDirectory,
13
+ backgroundEvidencePath,
14
+ COVERAGE_CARRIER_ENV,
15
+ COVERAGE_PHASE_HEADER,
16
+ COVERAGE_SCOPE_HEADER,
17
+ decodeCoverageCarrier,
18
+ decodeCoverageScope,
19
+ encodeCoverageCarrier,
20
+ encodeCoverageScope,
21
+ serverEvidenceDirectory,
22
+ serverEvidencePath,
23
+ } from "./transport.ts";
24
+
25
+ interface DecisionFrame {
26
+ meta: McdcDecisionMeta;
27
+ values: Array<boolean | null>;
28
+ }
29
+
30
+ interface SelectionFrame {
31
+ shortId: string;
32
+ rightId: string;
33
+ rightEvaluated: boolean;
34
+ }
35
+
36
+ interface TryFrame {
37
+ successId: string;
38
+ catchId: string;
39
+ caught: boolean;
40
+ }
41
+
42
+ interface LoopFrame {
43
+ zeroId: string;
44
+ enteredId: string;
45
+ entered: boolean;
46
+ }
47
+
48
+ interface RuntimeState {
49
+ decisions: Map<
50
+ string,
51
+ { meta: McdcDecisionMeta; vectors: Map<string, McdcVector> }
52
+ >;
53
+ hits: Set<string>;
54
+ events: CoverageRuntimeEvent[];
55
+ eventKeys: Set<string>;
56
+ }
57
+
58
+ type McdcGlobal = typeof globalThis & {
59
+ __SUPERCOV_MCDC_STATE__?: RuntimeState;
60
+ __SUPERCOV_MCDC_TEST_ID__?: string;
61
+ __SUPERCOV_PHASE_ID__?: string;
62
+ __SUPERCOV_SERVER_PHASE_STORAGE__?: RequestStorage;
63
+ __SUPERCOV_FETCH_PATCHED__?: boolean;
64
+ __SUPERCOV_CHILD_PATCHED__?: boolean;
65
+ __SUPERCOV_MCDC_SNAPSHOT__?: () => McdcDecisionSnapshot[];
66
+ __SUPERCOV_COVERAGE_SNAPSHOT__?: () => CoverageRuntimeSnapshot;
67
+ __SUPERCOV_RESET__?: (testId?: string) => void;
68
+ };
69
+
70
+ interface CoverageRequestContext {
71
+ scope?: CoverageExecutionScope;
72
+ phaseId?: string;
73
+ }
74
+
75
+ interface RequestStorage {
76
+ getStore(): CoverageRequestContext | undefined;
77
+ run<T>(store: CoverageRequestContext, callback: () => T): T;
78
+ }
79
+
80
+ interface AsyncHooksBuiltin {
81
+ AsyncLocalStorage: new () => RequestStorage;
82
+ }
83
+
84
+ interface FsBuiltin {
85
+ appendFileSync(path: string, data: string): void;
86
+ mkdirSync(path: string, options: { recursive: boolean }): void;
87
+ }
88
+
89
+ const runtimeGlobal = globalThis as McdcGlobal;
90
+ const isBrowser = !(
91
+ typeof process !== "undefined" &&
92
+ typeof process.versions?.node === "string"
93
+ );
94
+ const testId = runtimeGlobal.__SUPERCOV_MCDC_TEST_ID__ ?? "unscoped";
95
+ const storageKey = "__supercov_coverage_" + testId;
96
+ const phaseStorageKey = "__supercov_phase";
97
+ const pendingDefaults = new Map<string, number>();
98
+
99
+ function vectorKey(vector: McdcVector): string {
100
+ return (
101
+ vector.values
102
+ .map((value) => (value === null ? "-" : value ? "T" : "F"))
103
+ .join("") +
104
+ ":" +
105
+ (vector.outcome ? "T" : "F")
106
+ );
107
+ }
108
+
109
+ function getFs(): FsBuiltin | undefined {
110
+ if (isBrowser || typeof process === "undefined") return undefined;
111
+ try {
112
+ const getBuiltinModule = (
113
+ process as typeof process & {
114
+ getBuiltinModule?: (name: string) => FsBuiltin;
115
+ }
116
+ ).getBuiltinModule;
117
+ return getBuiltinModule?.("node:fs");
118
+ } catch {
119
+ return undefined;
120
+ }
121
+ }
122
+
123
+ function createState(): RuntimeState {
124
+ const state: RuntimeState = {
125
+ decisions: new Map(),
126
+ hits: new Set(),
127
+ events: [],
128
+ eventKeys: new Set(),
129
+ };
130
+ if (!isBrowser) return state;
131
+ try {
132
+ const stored = JSON.parse(
133
+ localStorage.getItem(storageKey) ?? "{}",
134
+ ) as Partial<CoverageRuntimeSnapshot>;
135
+ for (const snapshot of stored.decisions ?? []) {
136
+ state.decisions.set(snapshot.meta.id, {
137
+ meta: snapshot.meta,
138
+ vectors: new Map(
139
+ snapshot.vectors.map((vector) => [vectorKey(vector), vector]),
140
+ ),
141
+ });
142
+ }
143
+ for (const id of stored.hits ?? []) state.hits.add(id);
144
+ for (const event of stored.events ?? []) {
145
+ state.events.push(event);
146
+ state.eventKeys.add(eventKey(event));
147
+ }
148
+ } catch {
149
+ // Corrupt or unavailable storage must not affect application execution.
150
+ }
151
+ return state;
152
+ }
153
+
154
+ const state = runtimeGlobal.__SUPERCOV_MCDC_STATE__ ?? createState();
155
+ runtimeGlobal.__SUPERCOV_MCDC_STATE__ = state;
156
+
157
+ function createServerPhaseStorage(): RequestStorage | undefined {
158
+ if (isBrowser || typeof process === "undefined") return undefined;
159
+ try {
160
+ const getBuiltinModule = (
161
+ process as typeof process & {
162
+ getBuiltinModule?: (name: string) => AsyncHooksBuiltin;
163
+ }
164
+ ).getBuiltinModule;
165
+ const AsyncLocalStorage =
166
+ getBuiltinModule?.("node:async_hooks")?.AsyncLocalStorage;
167
+ return AsyncLocalStorage ? new AsyncLocalStorage() : undefined;
168
+ } catch {
169
+ return undefined;
170
+ }
171
+ }
172
+
173
+ const serverPhaseStorage =
174
+ runtimeGlobal.__SUPERCOV_SERVER_PHASE_STORAGE__ ??
175
+ createServerPhaseStorage();
176
+ if (serverPhaseStorage)
177
+ runtimeGlobal.__SUPERCOV_SERVER_PHASE_STORAGE__ =
178
+ serverPhaseStorage;
179
+
180
+ function decisionSnapshot(): McdcDecisionSnapshot[] {
181
+ return [...state.decisions.values()].map((decision) => ({
182
+ meta: decision.meta,
183
+ vectors: [...decision.vectors.values()],
184
+ }));
185
+ }
186
+
187
+ export function coverageSnapshot(): CoverageRuntimeSnapshot {
188
+ return {
189
+ decisions: decisionSnapshot(),
190
+ hits: [...state.hits],
191
+ events: state.events,
192
+ };
193
+ }
194
+
195
+ export function resetCoverage(testId?: string): void {
196
+ state.decisions.clear();
197
+ state.hits.clear();
198
+ state.events.length = 0;
199
+ state.eventKeys.clear();
200
+ if (testId) runtimeGlobal.__SUPERCOV_MCDC_TEST_ID__ = testId;
201
+ if (isBrowser) {
202
+ try {
203
+ localStorage.removeItem(storageKey);
204
+ } catch {
205
+ // Storage is optional in test environments.
206
+ }
207
+ }
208
+ }
209
+
210
+ runtimeGlobal.__SUPERCOV_MCDC_SNAPSHOT__ = decisionSnapshot;
211
+ runtimeGlobal.__SUPERCOV_COVERAGE_SNAPSHOT__ = coverageSnapshot;
212
+ runtimeGlobal.__SUPERCOV_RESET__ = resetCoverage;
213
+
214
+ function persistBrowser(): void {
215
+ if (!isBrowser) return;
216
+ try {
217
+ localStorage.setItem(storageKey, JSON.stringify(coverageSnapshot()));
218
+ } catch {
219
+ // Coverage persistence is best-effort and must never change app behavior.
220
+ }
221
+ }
222
+
223
+ function appendServer(record: CoverageServerRecord): void {
224
+ const fs = getFs();
225
+ if (!fs) return;
226
+ const context = currentRequestContext();
227
+ const scope = context.scope;
228
+ const runId =
229
+ scope?.runId ??
230
+ (typeof process !== "undefined"
231
+ ? process.env["SUPERCOV_RUN_ID"]
232
+ : undefined);
233
+ if (!runId) return;
234
+ try {
235
+ const directory = scope
236
+ ? serverEvidenceDirectory(scope)
237
+ : backgroundEvidenceDirectory(runId);
238
+ const path = scope
239
+ ? serverEvidencePath(scope)
240
+ : backgroundEvidencePath(runId);
241
+ fs.mkdirSync(directory, { recursive: true });
242
+ fs.appendFileSync(
243
+ path,
244
+ JSON.stringify({ ...record, ...(scope ? { scope } : {}) }) + "\n",
245
+ );
246
+ } catch {
247
+ // The instrumented build must remain behaviorally identical if collection fails.
248
+ }
249
+ }
250
+
251
+ function environmentRequestContext(): CoverageRequestContext | undefined {
252
+ if (isBrowser || typeof process === "undefined") return undefined;
253
+ const carrier = decodeCoverageCarrier(process.env[COVERAGE_CARRIER_ENV]);
254
+ return carrier
255
+ ? {
256
+ ...(carrier.scope ? { scope: carrier.scope } : {}),
257
+ ...(carrier.phaseId ? { phaseId: carrier.phaseId } : {}),
258
+ }
259
+ : undefined;
260
+ }
261
+
262
+ function currentRequestContext(): CoverageRequestContext {
263
+ return serverPhaseStorage?.getStore() ?? environmentRequestContext() ?? {};
264
+ }
265
+
266
+ export function coverageCarrier(): CoverageCarrier {
267
+ const context = currentRequestContext();
268
+ return {
269
+ version: 1,
270
+ ...(context.scope ? { scope: context.scope } : {}),
271
+ ...(context.phaseId ? { phaseId: context.phaseId } : {}),
272
+ };
273
+ }
274
+
275
+ export function withCoverageCarrier<T>(
276
+ carrier: CoverageCarrier | string | undefined,
277
+ callback: () => T,
278
+ ): T {
279
+ const decoded =
280
+ typeof carrier === "string" ? decodeCoverageCarrier(carrier) : carrier;
281
+ if (!serverPhaseStorage || !decoded) return callback();
282
+ return serverPhaseStorage.run(
283
+ {
284
+ ...(decoded.scope ? { scope: decoded.scope } : {}),
285
+ ...(decoded.phaseId ? { phaseId: decoded.phaseId } : {}),
286
+ },
287
+ callback,
288
+ );
289
+ }
290
+
291
+ export function bindCoverageContext<T extends (...args: never[]) => unknown>(
292
+ callback: T,
293
+ carrier = coverageCarrier(),
294
+ ): T {
295
+ return function boundCoverageContext(
296
+ this: unknown,
297
+ ...args: Parameters<T>
298
+ ): ReturnType<T> {
299
+ return withCoverageCarrier(carrier, () =>
300
+ Reflect.apply(callback, this, args),
301
+ ) as ReturnType<T>;
302
+ } as T;
303
+ }
304
+
305
+ export function coverageContextHeaders(): Record<string, string> {
306
+ const context = currentRequestContext();
307
+ if (!context.scope) return {};
308
+ return {
309
+ [COVERAGE_SCOPE_HEADER]: encodeCoverageScope(context.scope),
310
+ ...(context.phaseId
311
+ ? { [COVERAGE_PHASE_HEADER]: context.phaseId }
312
+ : {}),
313
+ };
314
+ }
315
+
316
+ export function coverageContextEnvironment(): Record<string, string> {
317
+ return { [COVERAGE_CARRIER_ENV]: encodeCoverageCarrier(coverageCarrier()) };
318
+ }
319
+
320
+ function installServerFetchPropagation(): void {
321
+ if (
322
+ isBrowser ||
323
+ runtimeGlobal.__SUPERCOV_FETCH_PATCHED__ ||
324
+ typeof globalThis.fetch !== "function"
325
+ )
326
+ return;
327
+ const originalFetch = globalThis.fetch.bind(globalThis);
328
+ globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
329
+ const coverage = coverageContextHeaders();
330
+ if (Object.keys(coverage).length === 0) return originalFetch(input, init);
331
+ const headers = new Headers(
332
+ init?.headers ?? (input instanceof Request ? input.headers : undefined),
333
+ );
334
+ for (const [name, value] of Object.entries(coverage))
335
+ headers.set(name, value);
336
+ return originalFetch(input, { ...init, headers });
337
+ }) as typeof globalThis.fetch;
338
+ runtimeGlobal.__SUPERCOV_FETCH_PATCHED__ = true;
339
+ }
340
+
341
+ installServerFetchPropagation();
342
+
343
+ function installServerChildPropagation(): void {
344
+ if (
345
+ isBrowser ||
346
+ runtimeGlobal.__SUPERCOV_CHILD_PATCHED__ ||
347
+ typeof process === "undefined"
348
+ )
349
+ return;
350
+ const getBuiltinModule = (
351
+ process as typeof process & {
352
+ getBuiltinModule?: (name: string) => Record<string, unknown>;
353
+ }
354
+ ).getBuiltinModule;
355
+ const child = getBuiltinModule?.("node:child_process");
356
+ if (!child) return;
357
+ const mutableChild = child as Record<string, unknown>;
358
+ const optionIndex = (method: string, args: unknown[]): number => {
359
+ if (
360
+ method === "spawn" ||
361
+ method === "spawnSync" ||
362
+ method === "fork" ||
363
+ method === "execFile" ||
364
+ method === "execFileSync"
365
+ )
366
+ return Array.isArray(args[1]) ? 2 : 1;
367
+ return 1;
368
+ };
369
+ for (const method of [
370
+ "exec",
371
+ "execFile",
372
+ "execFileSync",
373
+ "execSync",
374
+ "fork",
375
+ "spawn",
376
+ "spawnSync",
377
+ ]) {
378
+ const original = mutableChild[method];
379
+ if (typeof original !== "function") continue;
380
+ mutableChild[method] = function (...args: unknown[]): unknown {
381
+ const index = optionIndex(method, args);
382
+ const existing =
383
+ args[index] && typeof args[index] === "object"
384
+ ? (args[index] as { env?: Record<string, string | undefined> })
385
+ : {};
386
+ const options = {
387
+ ...existing,
388
+ env: {
389
+ ...process.env,
390
+ ...(existing.env ?? {}),
391
+ ...coverageContextEnvironment(),
392
+ },
393
+ };
394
+ const scoped = [...args];
395
+ if (typeof scoped[index] === "function") scoped.splice(index, 0, options);
396
+ else scoped[index] = options;
397
+ return Reflect.apply(original, child, scoped);
398
+ };
399
+ }
400
+ const moduleBuiltin = getBuiltinModule?.("node:module") as
401
+ | { syncBuiltinESMExports?: () => void }
402
+ | undefined;
403
+ moduleBuiltin?.syncBuiltinESMExports?.();
404
+ runtimeGlobal.__SUPERCOV_CHILD_PATCHED__ = true;
405
+ }
406
+
407
+ installServerChildPropagation();
408
+
409
+ function currentPhaseId(): string | undefined {
410
+ if (runtimeGlobal.__SUPERCOV_PHASE_ID__)
411
+ return runtimeGlobal.__SUPERCOV_PHASE_ID__;
412
+ if (!isBrowser) return currentRequestContext().phaseId;
413
+ try {
414
+ const local = localStorage.getItem(phaseStorageKey);
415
+ if (local) return local;
416
+ return undefined;
417
+ } catch {
418
+ return undefined;
419
+ }
420
+ }
421
+
422
+ function requestHeaders(
423
+ value: unknown,
424
+ ): { get(name: string): unknown } | undefined {
425
+ if (!value || typeof value !== "object") return undefined;
426
+ const directHeaders = (value as { headers?: unknown }).headers;
427
+ const request =
428
+ directHeaders && typeof directHeaders === "object"
429
+ ? value
430
+ : (value as { request?: unknown }).request;
431
+ if (!request || typeof request !== "object") return undefined;
432
+ const headers = (request as { headers?: unknown }).headers;
433
+ if (!headers || typeof headers !== "object") return undefined;
434
+ const get = (headers as { get?: unknown }).get;
435
+ if (typeof get === "function")
436
+ return {
437
+ get(name: string): unknown {
438
+ return Reflect.apply(get, headers, [name]);
439
+ },
440
+ };
441
+ const values = headers as Record<string, unknown>;
442
+ return Object.keys(values).length > 0
443
+ ? {
444
+ get(name: string): unknown {
445
+ return values[name] ?? values[name.toLowerCase()];
446
+ },
447
+ }
448
+ : undefined;
449
+ }
450
+
451
+ function requestCoverageContext(value: unknown): CoverageRequestContext {
452
+ const headers = requestHeaders(value);
453
+ if (!headers) return {};
454
+ const encodedScope = headers.get(COVERAGE_SCOPE_HEADER);
455
+ const rawPhaseId = headers.get(COVERAGE_PHASE_HEADER);
456
+ const scope = decodeCoverageScope(
457
+ typeof encodedScope === "string" ? encodedScope : undefined,
458
+ );
459
+ const phaseId =
460
+ typeof rawPhaseId === "string" && rawPhaseId.length > 0
461
+ ? rawPhaseId
462
+ : undefined;
463
+ return {
464
+ ...(scope ? { scope } : {}),
465
+ ...(phaseId ? { phaseId } : {}),
466
+ };
467
+ }
468
+
469
+ export function withRequestPhase<T extends (...args: never[]) => unknown>(
470
+ handler: T,
471
+ ): T {
472
+ if (!serverPhaseStorage) return handler;
473
+ return function coverageRequestPhase(
474
+ this: unknown,
475
+ ...args: Parameters<T>
476
+ ): ReturnType<T> {
477
+ const requestContext = args
478
+ .map((argument) => requestCoverageContext(argument))
479
+ .find((context) => context.scope || context.phaseId) ?? {};
480
+ const inheritedContext = currentRequestContext();
481
+ const context = {
482
+ ...(requestContext.scope ?? inheritedContext.scope
483
+ ? { scope: requestContext.scope ?? inheritedContext.scope }
484
+ : {}),
485
+ ...(requestContext.phaseId ?? inheritedContext.phaseId
486
+ ? { phaseId: requestContext.phaseId ?? inheritedContext.phaseId }
487
+ : {}),
488
+ };
489
+ const invoke = () => Reflect.apply(handler, this, args) as ReturnType<T>;
490
+ return context.scope || context.phaseId
491
+ ? serverPhaseStorage.run(context, invoke)
492
+ : invoke();
493
+ } as T;
494
+ }
495
+
496
+ function eventKey(event: CoverageRuntimeEvent): string {
497
+ const suffix =
498
+ event.type === "decision"
499
+ ? `${event.id}:${vectorKey(event.vector)}`
500
+ : event.id;
501
+ return `${event.phaseId ?? "unscoped"}:${event.type}:${suffix}`;
502
+ }
503
+
504
+ function recordBrowserEvent(event: CoverageRuntimeEvent): boolean {
505
+ const key = eventKey(event);
506
+ if (state.eventKeys.has(key)) return false;
507
+ state.eventKeys.add(key);
508
+ state.events.push(event);
509
+ return true;
510
+ }
511
+
512
+ export function coverageHit(id: string): void {
513
+ state.hits.add(id);
514
+ const timestampMs = Date.now();
515
+ const phaseId = currentPhaseId();
516
+ if (isBrowser) {
517
+ if (
518
+ recordBrowserEvent({
519
+ type: "hit",
520
+ id,
521
+ timestampMs,
522
+ ...(phaseId ? { phaseId } : {}),
523
+ environment: "browser",
524
+ })
525
+ )
526
+ persistBrowser();
527
+ } else {
528
+ // The server process cannot directly see the browser's active phase.
529
+ // Keep repeated executions and correlate them to phase time windows in
530
+ // the analyzer; global first-hit de-duplication would lose later actions.
531
+ appendServer({
532
+ type: "hit",
533
+ id,
534
+ timestampMs,
535
+ ...(phaseId ? { phaseId } : {}),
536
+ });
537
+ }
538
+ }
539
+
540
+ export function selectionBegin(
541
+ shortId: string,
542
+ rightId: string,
543
+ ): SelectionFrame {
544
+ return { shortId, rightId, rightEvaluated: false };
545
+ }
546
+
547
+ export function selectionRight<T>(frame: SelectionFrame, value: T): T {
548
+ frame.rightEvaluated = true;
549
+ return value;
550
+ }
551
+
552
+ export function selectionEnd<T>(frame: SelectionFrame, value: T): T {
553
+ coverageHit(frame.rightEvaluated ? frame.rightId : frame.shortId);
554
+ return value;
555
+ }
556
+
557
+ export function optionalSelect<T>(
558
+ shortId: string,
559
+ continuedId: string,
560
+ value: T,
561
+ ): T {
562
+ coverageHit(value === null || value === undefined ? shortId : continuedId);
563
+ return value;
564
+ }
565
+
566
+ export function defaultSelected<T>(defaultId: string, value: T): T {
567
+ pendingDefaults.set(defaultId, (pendingDefaults.get(defaultId) ?? 0) + 1);
568
+ return value;
569
+ }
570
+
571
+ export function defaultEntered(defaultId: string, providedId: string): void {
572
+ const pending = pendingDefaults.get(defaultId) ?? 0;
573
+ if (pending > 0) {
574
+ pendingDefaults.set(defaultId, pending - 1);
575
+ coverageHit(defaultId);
576
+ } else {
577
+ coverageHit(providedId);
578
+ }
579
+ }
580
+
581
+ export function tryBegin(successId: string, catchId: string): TryFrame {
582
+ return { successId, catchId, caught: false };
583
+ }
584
+
585
+ export function tryCatch<T>(frame: TryFrame, value: T): T {
586
+ frame.caught = true;
587
+ return value;
588
+ }
589
+
590
+ export function tryEnd(frame: TryFrame): void {
591
+ coverageHit(frame.caught ? frame.catchId : frame.successId);
592
+ }
593
+
594
+ export function loopBegin(zeroId: string, enteredId: string): LoopFrame {
595
+ return { zeroId, enteredId, entered: false };
596
+ }
597
+
598
+ export function loopEntered(frame: LoopFrame): void {
599
+ frame.entered = true;
600
+ }
601
+
602
+ export function loopEnd(frame: LoopFrame): void {
603
+ coverageHit(frame.entered ? frame.enteredId : frame.zeroId);
604
+ }
605
+
606
+ export function mcdcBegin(id: string, meta: McdcDecisionMeta): DecisionFrame {
607
+ if (!state.decisions.has(id)) {
608
+ state.decisions.set(id, { meta, vectors: new Map() });
609
+ }
610
+ return {
611
+ meta,
612
+ values: Array.from({ length: meta.conditions.length }, () => null),
613
+ };
614
+ }
615
+
616
+ export function mcdcCondition<T>(
617
+ frame: DecisionFrame,
618
+ index: number,
619
+ value: T,
620
+ ): T {
621
+ frame.values[index] = Boolean(value);
622
+ return value;
623
+ }
624
+
625
+ export function mcdcEnd<T>(frame: DecisionFrame, value: T): T {
626
+ const decision = state.decisions.get(frame.meta.id);
627
+ if (!decision) return value;
628
+
629
+ const vector: McdcVector = { values: frame.values, outcome: Boolean(value) };
630
+ const key = vectorKey(vector);
631
+ decision.vectors.set(key, vector);
632
+ const timestampMs = Date.now();
633
+ const phaseId = currentPhaseId();
634
+ if (isBrowser) {
635
+ if (
636
+ recordBrowserEvent({
637
+ type: "decision",
638
+ id: decision.meta.id,
639
+ vector,
640
+ timestampMs,
641
+ ...(phaseId ? { phaseId } : {}),
642
+ environment: "browser",
643
+ })
644
+ )
645
+ persistBrowser();
646
+ } else {
647
+ appendServer({
648
+ type: "decision",
649
+ meta: decision.meta,
650
+ vector,
651
+ timestampMs,
652
+ ...(phaseId ? { phaseId } : {}),
653
+ });
654
+ }
655
+ return value;
656
+ }