supercov 0.0.44 → 0.0.45

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