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.
- package/README.md +21 -12
- package/docs/agent-loop.md +12 -8
- package/docs/assertion-agent.md +156 -0
- package/docs/assertion-evidence.md +9 -694
- package/docs/assertion-maps.md +252 -0
- package/docs/assertions.md +82 -0
- package/docs/cli.md +23 -8
- package/docs/coverage-model.md +12 -0
- package/package.json +34 -35
- package/runtime/javascript/runtime.mjs +18 -33
- package/schemas/assertions.schema.json +276 -0
- package/analyzers/typescript/README.md +0 -59
- package/analyzers/typescript/bin/compiler-identity.mjs +0 -78
- package/analyzers/typescript/bin/identity.mjs +0 -71
- package/analyzers/typescript/bin/query.mjs +0 -29
- package/analyzers/typescript/dist/analyze.js +0 -5273
- package/analyzers/typescript/dist/archive.js +0 -337
- package/analyzers/typescript/dist/awaited-observations.js +0 -376
- package/analyzers/typescript/dist/build-identity.json +0 -1
- package/analyzers/typescript/dist/compiler.js +0 -32
- package/analyzers/typescript/dist/frontend.js +0 -75
- package/analyzers/typescript/dist/mock-counts.js +0 -2517
- package/analyzers/typescript/dist/native-frontend.js +0 -271
- package/analyzers/typescript/dist/pragmas.js +0 -186
- package/analyzers/typescript/dist/types.js +0 -1
- package/analyzers/typescript/package.json +0 -27
- package/analyzers/typescript/src/analyze.ts +0 -6180
- package/analyzers/typescript/src/archive.ts +0 -471
- package/analyzers/typescript/src/awaited-observations.ts +0 -561
- package/analyzers/typescript/src/compiler.ts +0 -49
- package/analyzers/typescript/src/frontend.ts +0 -136
- package/analyzers/typescript/src/mock-counts.ts +0 -3219
- package/analyzers/typescript/src/native-frontend.ts +0 -315
- package/analyzers/typescript/src/pragmas.ts +0 -284
- package/analyzers/typescript/src/types.ts +0 -45
- package/analyzers/typescript/tsconfig.json +0 -12
- 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
|
-
}
|