supercov 0.0.42 → 0.0.44

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.
Files changed (47) hide show
  1. package/README.md +23 -3
  2. package/analyzers/typescript/README.md +59 -0
  3. package/analyzers/typescript/bin/compiler-identity.mjs +78 -0
  4. package/analyzers/typescript/bin/identity.mjs +71 -0
  5. package/analyzers/typescript/bin/query.mjs +29 -0
  6. package/analyzers/typescript/dist/analyze.js +5273 -0
  7. package/analyzers/typescript/dist/archive.js +337 -0
  8. package/analyzers/typescript/dist/awaited-observations.js +376 -0
  9. package/analyzers/typescript/dist/build-identity.json +1 -0
  10. package/analyzers/typescript/dist/compiler.js +32 -0
  11. package/analyzers/typescript/dist/frontend.js +75 -0
  12. package/analyzers/typescript/dist/mock-counts.js +2517 -0
  13. package/analyzers/typescript/dist/native-frontend.js +271 -0
  14. package/analyzers/typescript/dist/pragmas.js +186 -0
  15. package/analyzers/typescript/dist/types.js +1 -0
  16. package/analyzers/typescript/package.json +27 -0
  17. package/analyzers/typescript/src/analyze.ts +6180 -0
  18. package/analyzers/typescript/src/archive.ts +471 -0
  19. package/analyzers/typescript/src/awaited-observations.ts +561 -0
  20. package/analyzers/typescript/src/compiler.ts +49 -0
  21. package/analyzers/typescript/src/frontend.ts +136 -0
  22. package/analyzers/typescript/src/mock-counts.ts +3219 -0
  23. package/analyzers/typescript/src/native-frontend.ts +315 -0
  24. package/analyzers/typescript/src/pragmas.ts +284 -0
  25. package/analyzers/typescript/src/types.ts +45 -0
  26. package/analyzers/typescript/tsconfig.json +12 -0
  27. package/docs/agent-loop.md +129 -31
  28. package/docs/assertion-evidence.md +694 -0
  29. package/docs/cli.md +25 -15
  30. package/docs/code-verification.md +4 -0
  31. package/docs/coverage-model.md +6 -6
  32. package/docs/evidence.md +6 -6
  33. package/docs/getting-started.md +60 -72
  34. package/docs/performance.md +4 -4
  35. package/docs/supported-suites.md +40 -9
  36. package/docs/troubleshooting.md +8 -8
  37. package/docs/verification.md +14 -2
  38. package/docs/workspace-isolation.md +1 -1
  39. package/package.json +35 -15
  40. package/runtime/javascript/jest.cjs +134 -0
  41. package/runtime/javascript/jest.config.mjs +39 -0
  42. package/runtime/javascript/jestReporter.mjs +77 -0
  43. package/runtime/javascript/nodeAssertAdapter.mjs +32 -8
  44. package/runtime/javascript/nodeTest.mjs +13 -5
  45. package/runtime/javascript/register.mjs +22 -4
  46. package/runtime/javascript/runnerEvidence.mjs +33 -11
  47. package/runtime/javascript/runtime.mjs +73 -16
@@ -0,0 +1,471 @@
1
+ /** Post-run adapter. Rust validates archive framing, freshness and record schemas first. */
2
+ import type ts from "typescript";
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { analyzeWithFrontend } from "./analyze.js";
6
+ import { analysisPath } from "./compiler.js";
7
+ import { createFrontend, type CompilerFrontend } from "./frontend.js";
8
+ import type { Site } from "./types.js";
9
+
10
+ export const PROTOCOL = {
11
+ abi: 1,
12
+ factsSchema: 1,
13
+ rules: "source-linked-v3/archive-3",
14
+ capabilities: [
15
+ "requiresTotal-v1",
16
+ "assertion-witness-issues-v1",
17
+ "complete-passed-test-inventory-v1",
18
+ "witnessed-callback-scope-v1",
19
+ "assertion-hints-v1",
20
+ "awaited-observation-sources-v1",
21
+ "first-test-call-omission-v1",
22
+ "closed-count-sensitivity-v1",
23
+ "closed-payload-sensitivity-v1",
24
+ "payload-native-predicates-v1",
25
+ "first-test-direct-return-v1",
26
+ "mock-observation-projections-v1",
27
+ "assertion-comparison-relations-v2",
28
+ "process-exit-source-v1",
29
+ "process-exit-consumer-v1",
30
+ "mock-count-lifetimes-v1",
31
+ "mock-count-factories-v1",
32
+ "mock-count-rows-v1",
33
+ "mock-count-array-projections-v1",
34
+ "primitive-decision-sensitivity-v1",
35
+ ],
36
+ };
37
+ type Location = {
38
+ file: string;
39
+ line: number;
40
+ column: number;
41
+ source: string;
42
+ id: string;
43
+ };
44
+ type Point = Location & { kind: string };
45
+ type Decision = Location & { conditions: string[] };
46
+ type Event = {
47
+ type: string;
48
+ id: string;
49
+ phaseId?: string;
50
+ statementId?: string;
51
+ };
52
+ type Snapshot = {
53
+ hits?: string[];
54
+ events?: Event[];
55
+ decisions?: {
56
+ meta: Decision;
57
+ vectors: { outcome: boolean; values: (boolean | null)[] }[];
58
+ }[];
59
+ };
60
+ type Scope = {
61
+ version: number;
62
+ runId: string;
63
+ workerId: string;
64
+ testId: string;
65
+ testKey: string;
66
+ retry: number;
67
+ attemptId: string;
68
+ };
69
+ type ServerRecord = {
70
+ type: string;
71
+ id?: string;
72
+ meta?: Decision;
73
+ vector?: { outcome: boolean; values: (boolean | null)[] };
74
+ phaseId?: string;
75
+ statementId?: string;
76
+ scope?: Scope;
77
+ };
78
+ function sameScope(a?: Scope, b?: Scope): boolean {
79
+ return (
80
+ !!a &&
81
+ !!b &&
82
+ a.version === b.version &&
83
+ a.runId === b.runId &&
84
+ a.workerId === b.workerId &&
85
+ a.testId === b.testId &&
86
+ a.testKey === b.testKey &&
87
+ a.retry === b.retry &&
88
+ a.attemptId === b.attemptId
89
+ );
90
+ }
91
+ function serverSnapshot(records: ServerRecord[], scope?: Scope): Snapshot {
92
+ const hits: string[] = [],
93
+ events: Event[] = [];
94
+ const decisions: NonNullable<Snapshot["decisions"]> = [];
95
+ for (const record of records) {
96
+ if (!sameScope(record.scope, scope)) continue;
97
+ const id = record.type === "decision" ? record.meta?.id : record.id;
98
+ if (!id) throw new Error("Missing scoped server event identity");
99
+ if (record.type === "decision" && record.meta && record.vector)
100
+ decisions.push({ meta: record.meta, vectors: [record.vector] });
101
+ else if (record.type === "hit") hits.push(id);
102
+ else throw new Error("Invalid scoped server event");
103
+ events.push({
104
+ type: record.type,
105
+ id,
106
+ phaseId: record.phaseId,
107
+ statementId: record.statementId,
108
+ });
109
+ }
110
+ return { hits, events, decisions };
111
+ }
112
+ type RecordData = {
113
+ test: string;
114
+ title?: string;
115
+ testFile?: string;
116
+ testId?: string;
117
+ retry?: number;
118
+ role: string;
119
+ status?: string;
120
+ expectedStatus?: string;
121
+ flaky?: boolean;
122
+ provenance?: { runner?: string };
123
+ scope?: Scope;
124
+ runtime: Snapshot[];
125
+ browser: Snapshot[];
126
+ server: ServerRecord[];
127
+ phases: {
128
+ id: string;
129
+ kind: string;
130
+ operation: string;
131
+ source?: string;
132
+ status?: string;
133
+ causedByPhaseId?: string;
134
+ }[];
135
+ };
136
+ export interface ArchiveInput {
137
+ protocol: typeof PROTOCOL;
138
+ projectRoot: string;
139
+ runId: string;
140
+ sourceFiles: string[];
141
+ effects: (Omit<Site, "kind" | "fn" | "pos" | "endPos"> & {
142
+ function: string;
143
+ })[];
144
+ manifest: {
145
+ points: Point[];
146
+ decisions: Decision[];
147
+ branches: (Location & { alternatives: { id: string }[] })[];
148
+ };
149
+ records: RecordData[];
150
+ limitations: string[];
151
+ }
152
+
153
+ export function analyzeArchive(
154
+ input: ArchiveInput,
155
+ suppliedCompiler?: typeof ts,
156
+ ) {
157
+ const frontend = createFrontend(input.projectRoot, suppliedCompiler);
158
+ try {
159
+ return analyzeArchiveWithFrontend(input, frontend);
160
+ } finally {
161
+ frontend.close();
162
+ }
163
+ }
164
+
165
+ function analyzeArchiveWithFrontend(
166
+ input: ArchiveInput,
167
+ frontend: CompilerFrontend,
168
+ ) {
169
+ const compiler = frontend.syntax;
170
+ if (
171
+ input.protocol.abi !== PROTOCOL.abi ||
172
+ input.protocol.factsSchema !== PROTOCOL.factsSchema ||
173
+ input.protocol.rules !== PROTOCOL.rules ||
174
+ JSON.stringify(input.protocol.capabilities) !==
175
+ JSON.stringify(PROTOCOL.capabilities)
176
+ )
177
+ throw new Error(
178
+ "Unsupported assertion analyzer protocol/rule revision/capabilities",
179
+ );
180
+ const root = analysisPath(input.projectRoot);
181
+ const sources = new Map(
182
+ input.sourceFiles.map((file) => [
183
+ file,
184
+ frontend.parseSource(file, readFileSync(resolve(root, file), "utf8")),
185
+ ]),
186
+ );
187
+ function offset(file: string, p: { line: number; column: number }) {
188
+ const sf = sources.get(file);
189
+ if (!sf || p.line < 1 || p.column < 1)
190
+ throw new Error(`Invalid site position in ${file}`);
191
+ const n = sf.getPositionOfLineAndCharacter(p.line - 1, p.column - 1);
192
+ if (n > sf.text.length) throw new Error(`Site outside source: ${file}`);
193
+ return n;
194
+ }
195
+ const sites: Site[] = input.effects.map((e) => ({
196
+ ...e,
197
+ kind: "effect",
198
+ fn: e.function,
199
+ pos: offset(e.file, e.start),
200
+ endPos: offset(e.file, e.end),
201
+ }));
202
+ // Decision atoms retain their archived decision id and index. Logical-value operands are not
203
+ // fabricated from truthiness: in particular ?? does not establish the truthiness of its left side.
204
+ for (const d of input.manifest.decisions) {
205
+ const sf = sources.get(d.file);
206
+ if (!sf) continue;
207
+ const start = offset(d.file, d);
208
+ let expression: ts.Node | undefined;
209
+ function visit(n: ts.Node) {
210
+ if (n.getStart(sf!) === start && n.getText(sf!) === d.source)
211
+ expression ??= n;
212
+ compiler.forEachChild(n, visit);
213
+ }
214
+ visit(sf);
215
+ if (!expression)
216
+ throw new Error(`Archived decision no longer matches source: ${d.id}`);
217
+ const atoms: ts.Node[] = [];
218
+ function flatten(n: ts.Node) {
219
+ if (compiler.isParenthesizedExpression(n)) flatten(n.expression);
220
+ else if (
221
+ compiler.isBinaryExpression(n) &&
222
+ [
223
+ compiler.SyntaxKind.AmpersandAmpersandToken,
224
+ compiler.SyntaxKind.BarBarToken,
225
+ ].includes(n.operatorToken.kind)
226
+ ) {
227
+ flatten(n.left);
228
+ flatten(n.right);
229
+ } else atoms.push(n);
230
+ }
231
+ flatten(expression);
232
+ if (atoms.length !== d.conditions.length)
233
+ throw new Error(`Unsupported decision atom layout: ${d.id}`);
234
+ atoms.forEach((n, i) => {
235
+ const begin = sf.getLineAndCharacterOfPosition(n.getStart(sf)),
236
+ end = sf.getLineAndCharacterOfPosition(n.getEnd());
237
+ // This owner also participates in source-hint selection. Named arrow
238
+ // bindings must not be mislabeled <module> merely because they are not
239
+ // function declarations.
240
+ let parent: ts.Node | undefined = n.parent,
241
+ owner = "<module>";
242
+ while (parent) {
243
+ if (compiler.isFunctionDeclaration(parent) && parent.name) {
244
+ owner = parent.name.text;
245
+ break;
246
+ }
247
+ if (
248
+ (compiler.isArrowFunction(parent) ||
249
+ compiler.isFunctionExpression(parent)) &&
250
+ compiler.isVariableDeclaration(parent.parent) &&
251
+ compiler.isIdentifier(parent.parent.name)
252
+ ) {
253
+ owner = parent.parent.name.text;
254
+ break;
255
+ }
256
+ parent = parent.parent;
257
+ }
258
+ sites.push({
259
+ id: `${d.id}#${i}`,
260
+ file: d.file,
261
+ kind: "decision",
262
+ category: "condition",
263
+ classification: "contractual",
264
+ start: { line: begin.line + 1, column: begin.character + 1 },
265
+ end: { line: end.line + 1, column: end.character + 1 },
266
+ pos: n.getStart(sf),
267
+ endPos: n.getEnd(),
268
+ text: n.getText(sf),
269
+ fn: owner,
270
+ owner,
271
+ exported: false,
272
+ });
273
+ });
274
+ }
275
+ const files: Record<string, string> = {
276
+ "inventory.json": JSON.stringify({ sites }),
277
+ };
278
+ const points = new Map(input.manifest.points.map((p) => [p.id, p]));
279
+ const decisions = new Map(input.manifest.decisions.map((d) => [d.id, d]));
280
+ const locations = new Map<string, { file: string; line: number }>(points);
281
+ for (const b of input.manifest.branches)
282
+ for (const a of b.alternatives) locations.set(a.id, b);
283
+ const key = (p: { file: string; line: number; column: number }) =>
284
+ `${p.file}:${p.line}:${p.column}`;
285
+ const index: object[] = [];
286
+ const executionLinks: object[] = [];
287
+ const attempts: object[] = [];
288
+ const limitations = new Set(input.limitations);
289
+ limitations.add(
290
+ "Candidate analysis: no site has a checked semantic proof; no assertion percentage is reported.",
291
+ );
292
+ limitations.add(
293
+ "Phase/statement execution is not data dependence. Candidate flow rules remain unverified; helper-name contracts and temporal value inference are disabled.",
294
+ );
295
+ limitations.add(
296
+ "Logical-value operand sites and type-dependent effect classifications are not yet a complete denominator.",
297
+ );
298
+ const accepted = input.records.filter(
299
+ (r) =>
300
+ r.role === "test" &&
301
+ r.status === "passed" &&
302
+ !r.flaky &&
303
+ r.expectedStatus !== "failed" &&
304
+ r.scope &&
305
+ r.scope.runId === input.runId &&
306
+ // Keep uncertain/retried/multiple records out instead of mixing their observations.
307
+ (r.retry ?? 0) === 0 &&
308
+ r.scope.retry === 0 &&
309
+ input.records.filter(
310
+ (other) =>
311
+ other.role === "test" &&
312
+ (other.testId ?? other.test) === (r.testId ?? r.test),
313
+ ).length === 1,
314
+ );
315
+ if (!accepted.length)
316
+ throw new Error(
317
+ "No uniquely attributed, passed, non-retried test attempts in archive",
318
+ );
319
+ for (const [i, r] of accepted.entries()) {
320
+ const id = `A${i + 1}`;
321
+ if (!r.testFile) {
322
+ // Dropping even one passing attempt could turn its executed/asserted sites
323
+ // into apparently certain gaps. Ordinary coverage can still be queried.
324
+ throw new Error(
325
+ "A passed test has no source file; assertion analysis requires test-source provenance. Recapture with a supported runner and stack formatter.",
326
+ );
327
+ }
328
+ if (r.browser.length)
329
+ limitations.add(
330
+ "Browser evidence is not joined in this archive adapter.",
331
+ );
332
+ if (r.server.some((record) => !sameScope(record.scope, r.scope)))
333
+ limitations.add(
334
+ "Server events with missing or foreign attempt scopes were excluded.",
335
+ );
336
+ const snapshots = [...r.runtime, serverSnapshot(r.server, r.scope)];
337
+ const marked = new Map<string, Set<number>>();
338
+ function mark(p?: { file: string; line: number }) {
339
+ if (!p || !sources.has(p.file)) return;
340
+ if (!marked.has(p.file)) marked.set(p.file, new Set());
341
+ marked.get(p.file)!.add(p.line);
342
+ }
343
+ const outcomes: Record<string, number[]> = {};
344
+ const events = snapshots.flatMap((s) => s.events ?? []);
345
+ for (const snap of snapshots) {
346
+ for (const h of snap.hits ?? []) {
347
+ if (!locations.has(h)) throw new Error(`Unknown archived hit ${h}`);
348
+ mark(locations.get(h));
349
+ }
350
+ for (const d of snap.decisions ?? []) {
351
+ const meta = decisions.get(d.meta.id);
352
+ if (!meta) throw new Error(`Unknown archived decision ${d.meta.id}`);
353
+ mark(meta);
354
+ for (const v of d.vectors) {
355
+ (outcomes[`${key(meta)}#d`] ??= [0, 0])[v.outcome ? 0 : 1] = 1;
356
+ v.values.forEach((value, n) => {
357
+ if (typeof value === "boolean")
358
+ (outcomes[`${key(meta)}#${n}`] ??= [0, 0])[value ? 0 : 1] = 1;
359
+ });
360
+ }
361
+ }
362
+ }
363
+ function describe(events: Event[]) {
364
+ return {
365
+ fns: [
366
+ ...new Set(
367
+ events
368
+ .filter(
369
+ (e) =>
370
+ e.type === "hit" && points.get(e.id)?.kind === "function",
371
+ )
372
+ .map((e) => key(points.get(e.id)!)),
373
+ ),
374
+ ],
375
+ decs: [
376
+ ...new Set(
377
+ events
378
+ .filter((e) => e.type === "decision" && decisions.has(e.id))
379
+ .map((e) => key(decisions.get(e.id)!)),
380
+ ),
381
+ ],
382
+ stmts: events.filter(
383
+ (e) => e.type === "hit" && points.get(e.id)?.kind === "statement",
384
+ ).length,
385
+ };
386
+ }
387
+ const phases = r.phases
388
+ .filter((p) => p.kind === "assertion" && p.source)
389
+ .map((p) => {
390
+ const evs = events.filter((e) => e.phaseId === p.id);
391
+ for (const e of p.status === "passed" ? evs : [])
392
+ executionLinks.push({
393
+ attempt: id,
394
+ point: e.id,
395
+ location:
396
+ points.get(e.id) ?? decisions.get(e.id) ?? locations.get(e.id),
397
+ phase: p.id,
398
+ statement: e.statementId,
399
+ operation: p.operation,
400
+ assertionSource: p.source,
401
+ meaning: "execution-only",
402
+ });
403
+ return {
404
+ op: p.operation,
405
+ source: p.source!,
406
+ status: p.status,
407
+ ...describe(evs),
408
+ };
409
+ });
410
+ const statements: Record<string, ReturnType<typeof describe>> = {};
411
+ for (const e of events)
412
+ if (e.statementId && !statements[e.statementId])
413
+ statements[e.statementId] = describe(
414
+ events.filter((other) => other.statementId === e.statementId),
415
+ );
416
+ const phaseLines = phases
417
+ .map((p) => /^(.*):(\d+):(\d+)$/.exec(p.source))
418
+ .filter((m) => m && m[1] === r.testFile)
419
+ .map((m) => Number(m![2]));
420
+ index.push({
421
+ id,
422
+ name: r.test,
423
+ title: r.title ?? r.test,
424
+ file: r.testFile,
425
+ line: 0,
426
+ ok: true,
427
+ phaseLines,
428
+ runner: r.provenance?.runner,
429
+ });
430
+ attempts.push({
431
+ id,
432
+ testId: r.testId,
433
+ name: r.test,
434
+ file: r.testFile,
435
+ retry: r.retry ?? 0,
436
+ });
437
+ files[`cov/${id}.lcov`] = [...marked]
438
+ .map(
439
+ ([file, lines]) =>
440
+ `SF:${file}\n${[...lines].map((l) => `DA:${l},1`).join("\n")}\nend_of_record\n`,
441
+ )
442
+ .join("");
443
+ files[`cov/${id}.outcomes.json`] = JSON.stringify(outcomes);
444
+ files[`cov/${id}.phases.json`] = JSON.stringify(phases);
445
+ files[`cov/${id}.statements.json`] = JSON.stringify(statements);
446
+ }
447
+ if (accepted.length !== input.records.filter((r) => r.role === "test").length)
448
+ limitations.add(
449
+ "Failed, flaky, retried, duplicate or unattributed test records were excluded; this is not whole-suite assertion coverage.",
450
+ );
451
+ files["cov/index.json"] = JSON.stringify(index);
452
+ const result = analyzeWithFrontend(
453
+ {
454
+ projectRoot: root,
455
+ evidenceFiles: files,
456
+ sourceFiles: input.sourceFiles,
457
+ testFiles: [
458
+ ...new Set(accepted.flatMap((r) => (r.testFile ? [r.testFile] : []))),
459
+ ],
460
+ },
461
+ frontend,
462
+ );
463
+ return {
464
+ protocol: PROTOCOL,
465
+ ...result,
466
+ inventory: sites,
467
+ executionLinks,
468
+ attempts,
469
+ limitations: [...limitations, ...frontend.limitations].sort(),
470
+ };
471
+ }