supercov 0.0.41 → 0.0.43
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 +24 -4
- package/analyzers/typescript/README.md +55 -0
- package/analyzers/typescript/bin/compiler-identity.mjs +78 -0
- package/analyzers/typescript/bin/identity.mjs +67 -0
- package/analyzers/typescript/bin/query.mjs +29 -0
- package/analyzers/typescript/dist/analyze.js +3972 -0
- package/analyzers/typescript/dist/archive.js +309 -0
- package/analyzers/typescript/dist/build-identity.json +1 -0
- package/analyzers/typescript/dist/compiler.js +32 -0
- package/analyzers/typescript/dist/frontend.js +75 -0
- package/analyzers/typescript/dist/native-frontend.js +271 -0
- package/analyzers/typescript/dist/pragmas.js +143 -0
- package/analyzers/typescript/dist/types.js +1 -0
- package/analyzers/typescript/package.json +27 -0
- package/analyzers/typescript/src/analyze.ts +4538 -0
- package/analyzers/typescript/src/archive.ts +438 -0
- package/analyzers/typescript/src/compiler.ts +49 -0
- package/analyzers/typescript/src/frontend.ts +136 -0
- package/analyzers/typescript/src/native-frontend.ts +315 -0
- package/analyzers/typescript/src/pragmas.ts +218 -0
- package/analyzers/typescript/src/types.ts +45 -0
- package/analyzers/typescript/tsconfig.json +12 -0
- package/docs/agent-loop.md +13 -0
- package/docs/assertion-evidence.md +135 -0
- package/docs/cli.md +10 -0
- package/docs/code-verification.md +182 -0
- package/docs/supported-suites.md +44 -11
- package/docs/troubleshooting.md +13 -0
- package/docs/verification.md +12 -0
- package/package.json +33 -15
- package/runtime/javascript/jest.cjs +134 -0
- package/runtime/javascript/jest.config.mjs +39 -0
- package/runtime/javascript/jestReporter.mjs +77 -0
- package/runtime/javascript/register.mjs +22 -4
- package/runtime/javascript/runtime.mjs +51 -13
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/** Adapter for the exact TypeScript 7.0.2 API, not a legacy compiler fallback. */
|
|
2
|
+
import type ts from "typescript";
|
|
3
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { analysisPath } from "./compiler.js";
|
|
8
|
+
import {
|
|
9
|
+
selectedRootFiles,
|
|
10
|
+
type CompilerFrontend,
|
|
11
|
+
type SyntaxAPI,
|
|
12
|
+
} from "./frontend.js";
|
|
13
|
+
|
|
14
|
+
// The optional project dependency cannot be imported by the TS5 build. Native API
|
|
15
|
+
// objects are confined to this adapter; its public surface is the shared frontend.
|
|
16
|
+
type Native = any;
|
|
17
|
+
|
|
18
|
+
export function nativeFrontend(
|
|
19
|
+
entry: string,
|
|
20
|
+
projectRoot: string,
|
|
21
|
+
): CompilerFrontend {
|
|
22
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
23
|
+
if (major < 22 || (major === 22 && minor < 12))
|
|
24
|
+
throw new Error(
|
|
25
|
+
"The TypeScript 7 frontend requires Node 22.12 or newer (Node 24 is tested)",
|
|
26
|
+
);
|
|
27
|
+
const require = createRequire(pathToFileURL(entry));
|
|
28
|
+
const ast: Native = require("typescript/unstable/ast");
|
|
29
|
+
const native: Native = require("typescript/unstable/sync");
|
|
30
|
+
const root = analysisPath(projectRoot);
|
|
31
|
+
const key = (file: string) => analysisPath(file).replaceAll("\\", "/");
|
|
32
|
+
const virtual = new Map<string, string>();
|
|
33
|
+
let parseSerial = 0;
|
|
34
|
+
const opened = new Set<string>();
|
|
35
|
+
const limitations = new Set<string>();
|
|
36
|
+
let api: Native;
|
|
37
|
+
let snapshot: Native;
|
|
38
|
+
let closed = false;
|
|
39
|
+
function client() {
|
|
40
|
+
if (closed) throw new Error("TypeScript 7 frontend is closed");
|
|
41
|
+
return (api ??= new native.API({
|
|
42
|
+
cwd: root,
|
|
43
|
+
fs: {
|
|
44
|
+
readFile: (file: string) => virtual.get(key(file)),
|
|
45
|
+
fileExists: (file: string) =>
|
|
46
|
+
virtual.has(key(file)) ? true : undefined,
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function update(config: string) {
|
|
51
|
+
const previous = snapshot;
|
|
52
|
+
snapshot = client().updateSnapshot({
|
|
53
|
+
...(!opened.has(config) ? { openProjects: [config] } : {}),
|
|
54
|
+
fileChanges: { invalidateAll: true },
|
|
55
|
+
});
|
|
56
|
+
opened.add(config);
|
|
57
|
+
previous?.dispose();
|
|
58
|
+
const project = snapshot.getProject(config);
|
|
59
|
+
if (!project)
|
|
60
|
+
throw new Error(
|
|
61
|
+
`TypeScript 7 did not open the analysis project: ${config}`,
|
|
62
|
+
);
|
|
63
|
+
return project;
|
|
64
|
+
}
|
|
65
|
+
function overlayConfig(path: string, config: unknown) {
|
|
66
|
+
if (existsSync(path))
|
|
67
|
+
throw new Error(`Reserved analysis config already exists: ${path}`);
|
|
68
|
+
virtual.set(key(path), JSON.stringify(config));
|
|
69
|
+
}
|
|
70
|
+
const kinds = ast.SyntaxKind;
|
|
71
|
+
const functionKinds = new Set(
|
|
72
|
+
[
|
|
73
|
+
"FunctionDeclaration",
|
|
74
|
+
"MethodDeclaration",
|
|
75
|
+
"Constructor",
|
|
76
|
+
"GetAccessor",
|
|
77
|
+
"SetAccessor",
|
|
78
|
+
"FunctionExpression",
|
|
79
|
+
"ArrowFunction",
|
|
80
|
+
"MethodSignature",
|
|
81
|
+
"CallSignature",
|
|
82
|
+
"JSDocSignature",
|
|
83
|
+
"ConstructSignature",
|
|
84
|
+
"IndexSignature",
|
|
85
|
+
"FunctionType",
|
|
86
|
+
"JSDocFunctionType",
|
|
87
|
+
"ConstructorType",
|
|
88
|
+
]
|
|
89
|
+
.map((name) => kinds[name])
|
|
90
|
+
.filter((value) => typeof value === "number"),
|
|
91
|
+
);
|
|
92
|
+
const loopKinds = new Set(
|
|
93
|
+
[
|
|
94
|
+
"ForStatement",
|
|
95
|
+
"ForInStatement",
|
|
96
|
+
"ForOfStatement",
|
|
97
|
+
"WhileStatement",
|
|
98
|
+
"DoStatement",
|
|
99
|
+
].map((name) => kinds[name]),
|
|
100
|
+
);
|
|
101
|
+
// These adapters preserve native nodes and native enum values. No TS5 predicate
|
|
102
|
+
// may inspect a TS7 node: syntax-kind numeric values are different.
|
|
103
|
+
const syntax = {
|
|
104
|
+
...ast,
|
|
105
|
+
SymbolFlags: native.SymbolFlags,
|
|
106
|
+
forEachChild: (node: Native, visit: Native, visitArray?: Native) =>
|
|
107
|
+
node.forEachChild(visit, visitArray),
|
|
108
|
+
isFunctionLike: (node: Native) => !!node && functionKinds.has(node.kind),
|
|
109
|
+
isIterationStatement: (
|
|
110
|
+
node: Native,
|
|
111
|
+
lookInLabeledStatements: boolean,
|
|
112
|
+
): boolean =>
|
|
113
|
+
!!node &&
|
|
114
|
+
(loopKinds.has(node.kind) ||
|
|
115
|
+
(lookInLabeledStatements &&
|
|
116
|
+
node.kind === kinds.LabeledStatement &&
|
|
117
|
+
syntax.isIterationStatement(node.statement, true))),
|
|
118
|
+
isMethodSignature: ast.isMethodSignatureDeclaration,
|
|
119
|
+
isParameter: ast.isParameterDeclaration,
|
|
120
|
+
isTypeAssertionExpression: ast.isTypeAssertion,
|
|
121
|
+
isStringLiteralLike: (node: Native) =>
|
|
122
|
+
!!node &&
|
|
123
|
+
(node.kind === kinds.StringLiteral ||
|
|
124
|
+
node.kind === kinds.NoSubstitutionTemplateLiteral),
|
|
125
|
+
canHaveModifiers: (node: Native) => !!node && "modifiers" in node,
|
|
126
|
+
getModifiers: (node: Native) =>
|
|
127
|
+
node.modifiers?.filter((m: Native) => m.kind !== kinds.Decorator),
|
|
128
|
+
} as unknown as SyntaxAPI;
|
|
129
|
+
for (const name of [
|
|
130
|
+
"isMethodSignature",
|
|
131
|
+
"isParameter",
|
|
132
|
+
"isTypeAssertionExpression",
|
|
133
|
+
"getLeadingCommentRanges",
|
|
134
|
+
"getTrailingCommentRanges",
|
|
135
|
+
] as const)
|
|
136
|
+
if (typeof syntax[name] !== "function")
|
|
137
|
+
throw new Error(`Unsupported TypeScript 7 syntax API: ${name}`);
|
|
138
|
+
|
|
139
|
+
const frontend: CompilerFrontend = {
|
|
140
|
+
kind: "typescript-native-7",
|
|
141
|
+
version: "7.0.2",
|
|
142
|
+
syntax,
|
|
143
|
+
limitations,
|
|
144
|
+
close() {
|
|
145
|
+
if (closed) return;
|
|
146
|
+
closed = true;
|
|
147
|
+
try {
|
|
148
|
+
snapshot?.dispose();
|
|
149
|
+
} finally {
|
|
150
|
+
api?.close();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
parseSource(file, text) {
|
|
154
|
+
const absolute = resolve(root, file);
|
|
155
|
+
virtual.set(key(absolute), text);
|
|
156
|
+
// Do not rely on 7.0.2 reloading an edited virtual config's root-file list.
|
|
157
|
+
const parseConfig = resolve(
|
|
158
|
+
root,
|
|
159
|
+
`.supercov-asserted-parse-${++parseSerial}.tsconfig.json`,
|
|
160
|
+
);
|
|
161
|
+
overlayConfig(parseConfig, {
|
|
162
|
+
files: [absolute],
|
|
163
|
+
compilerOptions: {
|
|
164
|
+
allowJs: true,
|
|
165
|
+
checkJs: true,
|
|
166
|
+
noResolve: true,
|
|
167
|
+
noLib: true,
|
|
168
|
+
noEmit: true,
|
|
169
|
+
types: [],
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const project = update(parseConfig);
|
|
173
|
+
const source = project.program.getSourceFile(absolute);
|
|
174
|
+
if (!source || source.text !== text)
|
|
175
|
+
throw new Error(`Native parser source mismatch: ${file}`);
|
|
176
|
+
const errors = project.program.getSyntacticDiagnostics(absolute);
|
|
177
|
+
if (errors.length)
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Native parser rejected ${file}: ${JSON.stringify(errors)}`,
|
|
180
|
+
);
|
|
181
|
+
return source as ts.SourceFile;
|
|
182
|
+
},
|
|
183
|
+
openProgram(options) {
|
|
184
|
+
const configPath = resolve(root, options.tsconfig ?? "tsconfig.json");
|
|
185
|
+
const hasConfig = existsSync(configPath);
|
|
186
|
+
if (!hasConfig && !options.sourceFiles)
|
|
187
|
+
throw new Error(`Missing TypeScript config: ${configPath}`);
|
|
188
|
+
const configured = hasConfig
|
|
189
|
+
? client().parseConfigFile(configPath).fileNames
|
|
190
|
+
: [];
|
|
191
|
+
const files = selectedRootFiles(options, configured);
|
|
192
|
+
const overlay = resolve(
|
|
193
|
+
dirname(configPath),
|
|
194
|
+
".supercov-asserted-query.tsconfig.json",
|
|
195
|
+
);
|
|
196
|
+
overlayConfig(overlay, {
|
|
197
|
+
...(hasConfig ? { extends: configPath } : {}),
|
|
198
|
+
files,
|
|
199
|
+
include: [],
|
|
200
|
+
exclude: [],
|
|
201
|
+
compilerOptions: {
|
|
202
|
+
...(options.sourceFiles ? { allowJs: true, checkJs: true } : {}),
|
|
203
|
+
noEmit: true,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const project = update(overlay);
|
|
207
|
+
const program = project.program;
|
|
208
|
+
const configErrors = program.getConfigFileParsingDiagnostics();
|
|
209
|
+
if (configErrors.length)
|
|
210
|
+
throw new Error(
|
|
211
|
+
`TypeScript 7 configuration is invalid: ${JSON.stringify(configErrors)}`,
|
|
212
|
+
);
|
|
213
|
+
const checker = project.checker;
|
|
214
|
+
const symbols = new WeakMap<object, ts.Symbol>();
|
|
215
|
+
const nativeSymbols = new WeakMap<object, Native>();
|
|
216
|
+
function wrapSymbol(symbol: Native): ts.Symbol | undefined {
|
|
217
|
+
if (!symbol) return undefined;
|
|
218
|
+
let wrapped = symbols.get(symbol);
|
|
219
|
+
if (!wrapped) {
|
|
220
|
+
wrapped = {
|
|
221
|
+
flags: symbol.flags,
|
|
222
|
+
name: symbol.name,
|
|
223
|
+
escapedName: symbol.escapedName,
|
|
224
|
+
get valueDeclaration() {
|
|
225
|
+
return symbol.valueDeclaration?.resolve(project);
|
|
226
|
+
},
|
|
227
|
+
get declarations() {
|
|
228
|
+
return symbol.declarations.map((handle: Native) => {
|
|
229
|
+
const node = handle.resolve(project);
|
|
230
|
+
if (!node)
|
|
231
|
+
throw new Error(
|
|
232
|
+
"TypeScript 7 declaration handle did not resolve",
|
|
233
|
+
);
|
|
234
|
+
return node;
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
} as ts.Symbol;
|
|
238
|
+
symbols.set(symbol, wrapped);
|
|
239
|
+
nativeSymbols.set(wrapped, symbol);
|
|
240
|
+
}
|
|
241
|
+
return wrapped;
|
|
242
|
+
}
|
|
243
|
+
const allFiles: ts.SourceFile[] = program
|
|
244
|
+
.getSourceFileNames()
|
|
245
|
+
.map((file: string) => {
|
|
246
|
+
const source = program.getSourceFile(file);
|
|
247
|
+
if (!source) throw new Error(`Native source disappeared: ${file}`);
|
|
248
|
+
return source;
|
|
249
|
+
});
|
|
250
|
+
const resolutionCache = new Map<string, string | undefined>();
|
|
251
|
+
return {
|
|
252
|
+
files: allFiles,
|
|
253
|
+
checker: {
|
|
254
|
+
getSymbolAtLocation: (node) =>
|
|
255
|
+
wrapSymbol(checker.getSymbolAtLocation(node)),
|
|
256
|
+
getShorthandAssignmentValueSymbol: (node) =>
|
|
257
|
+
wrapSymbol(checker.getShorthandAssignmentValueSymbol(node)),
|
|
258
|
+
getAliasedSymbol: (symbol) => {
|
|
259
|
+
const original = nativeSymbols.get(symbol);
|
|
260
|
+
if (!original)
|
|
261
|
+
throw new Error("Symbol belongs to a different compiler session");
|
|
262
|
+
return wrapSymbol(checker.getAliasedSymbol(original))!;
|
|
263
|
+
},
|
|
264
|
+
getResolvedSignature: (node) => {
|
|
265
|
+
const signature = checker.getResolvedSignature(node);
|
|
266
|
+
return signature
|
|
267
|
+
? ({
|
|
268
|
+
getParameters: () =>
|
|
269
|
+
signature.getParameters().map(wrapSymbol),
|
|
270
|
+
} as ts.Signature)
|
|
271
|
+
: undefined;
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
resolveModule(specifier, from) {
|
|
275
|
+
const cacheKey = `${key(from)}\0${specifier}`;
|
|
276
|
+
if (resolutionCache.has(cacheKey))
|
|
277
|
+
return resolutionCache.get(cacheKey);
|
|
278
|
+
const source = program.getSourceFile(from);
|
|
279
|
+
const paths = new Set<string>();
|
|
280
|
+
const visit = (node: Native) => {
|
|
281
|
+
if (syntax.isStringLiteralLike(node) && node.text === specifier) {
|
|
282
|
+
// Ask the native checker about actual module-reference syntax. A
|
|
283
|
+
// vi.mock string is not a TypeScript import and is not guessed.
|
|
284
|
+
const p: Native = node.parent;
|
|
285
|
+
if (
|
|
286
|
+
p &&
|
|
287
|
+
(ast.isImportDeclaration(p) ||
|
|
288
|
+
ast.isExportDeclaration(p) ||
|
|
289
|
+
(ast.isCallExpression(p) &&
|
|
290
|
+
p.expression.kind === kinds.ImportKeyword))
|
|
291
|
+
) {
|
|
292
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
293
|
+
for (const handle of symbol?.declarations ?? []) {
|
|
294
|
+
const declaration = handle.resolve(project);
|
|
295
|
+
if (declaration && syntax.isSourceFile(declaration))
|
|
296
|
+
paths.add(declaration.fileName);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
node.forEachChild(visit);
|
|
301
|
+
};
|
|
302
|
+
if (source) visit(source);
|
|
303
|
+
const result = paths.size === 1 ? [...paths][0] : undefined;
|
|
304
|
+
if (!result && !isBuiltin(specifier))
|
|
305
|
+
limitations.add(
|
|
306
|
+
`native-module-resolution: ${key(from)} → ${specifier}`,
|
|
307
|
+
);
|
|
308
|
+
resolutionCache.set(cacheKey, result);
|
|
309
|
+
return result;
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
return frontend;
|
|
315
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
2
|
+
import type { Site } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export type AssertionPhase = { source?: string; op: string; status?: string };
|
|
5
|
+
export function assertionWitnessIssue(
|
|
6
|
+
phases: AssertionPhase[] | undefined,
|
|
7
|
+
source?: string,
|
|
8
|
+
method?: string,
|
|
9
|
+
) {
|
|
10
|
+
const matches = (phases ?? []).filter(
|
|
11
|
+
(p) => p.source === source && p.op.split(".").pop() === method,
|
|
12
|
+
);
|
|
13
|
+
if (source && matches.length && matches.every((p) => p.status === "passed"))
|
|
14
|
+
return undefined;
|
|
15
|
+
const statuses = new Set(matches.map((p) => p.status ?? "unknown"));
|
|
16
|
+
return !phases
|
|
17
|
+
? ("capture-unavailable" as const)
|
|
18
|
+
: !source
|
|
19
|
+
? ("uninstrumented-observation" as const)
|
|
20
|
+
: !matches.length
|
|
21
|
+
? ("call-not-recorded" as const)
|
|
22
|
+
: statuses.size > 1
|
|
23
|
+
? ("mixed-call-outcomes" as const)
|
|
24
|
+
: statuses.has("failed")
|
|
25
|
+
? ("call-failed" as const)
|
|
26
|
+
: ("call-incomplete" as const);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Attachment {
|
|
30
|
+
testKey: string;
|
|
31
|
+
source: string;
|
|
32
|
+
method: string;
|
|
33
|
+
inert: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface Comment {
|
|
36
|
+
id: string;
|
|
37
|
+
where: string;
|
|
38
|
+
raw: string;
|
|
39
|
+
target?: { file: string; function: string; snippet?: string; via?: string };
|
|
40
|
+
candidateSites: string[];
|
|
41
|
+
issue?: string;
|
|
42
|
+
attachments: Attachment[];
|
|
43
|
+
}
|
|
44
|
+
export interface PragmaHint {
|
|
45
|
+
id: string;
|
|
46
|
+
where: string;
|
|
47
|
+
raw: string;
|
|
48
|
+
target?: Comment["target"];
|
|
49
|
+
candidateSites: string[];
|
|
50
|
+
issue?: string;
|
|
51
|
+
test?: string;
|
|
52
|
+
assertionSource?: string;
|
|
53
|
+
assertionMethod?: string;
|
|
54
|
+
witness: "passed" | "unavailable";
|
|
55
|
+
witnessIssue?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Source hints are kept OUT of observations. A comment never adds a boundary. */
|
|
59
|
+
export function collectPragmas(
|
|
60
|
+
compiler: import("./frontend.js").SyntaxAPI,
|
|
61
|
+
files: ts.SourceFile[],
|
|
62
|
+
relativeFile: (file: ts.SourceFile) => string,
|
|
63
|
+
sites: Site[],
|
|
64
|
+
) {
|
|
65
|
+
const comments = new Map<string, Comment>();
|
|
66
|
+
const key = (sf: ts.SourceFile, pos: number) => `${relativeFile(sf)}:${pos}`;
|
|
67
|
+
const location = (sf: ts.SourceFile, pos: number) => {
|
|
68
|
+
const p = sf.getLineAndCharacterOfPosition(pos);
|
|
69
|
+
return `${relativeFile(sf)}:${p.line + 1}:${p.character + 1}`;
|
|
70
|
+
};
|
|
71
|
+
for (const sf of files) {
|
|
72
|
+
const collect = (ranges: ts.CommentRange[] | undefined) => {
|
|
73
|
+
for (const range of ranges ?? []) {
|
|
74
|
+
const raw = sf.text.slice(range.pos, range.end);
|
|
75
|
+
if (!/^\/\/\s*observes:/.test(raw) || comments.has(key(sf, range.pos)))
|
|
76
|
+
continue;
|
|
77
|
+
const parsed = /^\/\/\s*observes:\s*(\S+?)#(\S+)(?:\s+(.+?))?\s*$/.exec(
|
|
78
|
+
raw,
|
|
79
|
+
);
|
|
80
|
+
const suffix = parsed?.[3]?.split(/(?:^|\s+)via\s+/);
|
|
81
|
+
const target = parsed
|
|
82
|
+
? {
|
|
83
|
+
file: parsed[1],
|
|
84
|
+
function: parsed[2],
|
|
85
|
+
snippet: suffix?.[0]?.trim() || undefined,
|
|
86
|
+
via: suffix?.slice(1).join(" via ").trim() || undefined,
|
|
87
|
+
}
|
|
88
|
+
: undefined;
|
|
89
|
+
const validPath =
|
|
90
|
+
target &&
|
|
91
|
+
!/[\\:]/.test(target.file) &&
|
|
92
|
+
target.file
|
|
93
|
+
.split("/")
|
|
94
|
+
.every((part) => part && part !== "." && part !== "..");
|
|
95
|
+
const candidates = validPath
|
|
96
|
+
? sites
|
|
97
|
+
.filter(
|
|
98
|
+
(s) =>
|
|
99
|
+
s.file === target.file &&
|
|
100
|
+
(s.owner === target.function || s.fn === target.function) &&
|
|
101
|
+
(!target.snippet || s.text.includes(target.snippet)),
|
|
102
|
+
)
|
|
103
|
+
.map((s) => s.id)
|
|
104
|
+
: [];
|
|
105
|
+
comments.set(key(sf, range.pos), {
|
|
106
|
+
id: location(sf, range.pos),
|
|
107
|
+
where: location(sf, range.pos),
|
|
108
|
+
raw,
|
|
109
|
+
target,
|
|
110
|
+
candidateSites: candidates,
|
|
111
|
+
issue: !target
|
|
112
|
+
? "invalid-syntax"
|
|
113
|
+
: !validPath
|
|
114
|
+
? "invalid-target-path"
|
|
115
|
+
: !candidates.length
|
|
116
|
+
? "target-not-in-inventory"
|
|
117
|
+
: candidates.length > 1
|
|
118
|
+
? "ambiguous-target"
|
|
119
|
+
: undefined,
|
|
120
|
+
attachments: [],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const visit = (node: ts.Node) => {
|
|
125
|
+
collect(compiler.getLeadingCommentRanges(sf.text, node.getFullStart()));
|
|
126
|
+
collect(compiler.getTrailingCommentRanges(sf.text, node.getEnd()));
|
|
127
|
+
compiler.forEachChild(node, visit);
|
|
128
|
+
};
|
|
129
|
+
visit(sf);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
register(
|
|
133
|
+
node: ts.CallExpression,
|
|
134
|
+
method: string,
|
|
135
|
+
testKey: string,
|
|
136
|
+
inert: boolean,
|
|
137
|
+
) {
|
|
138
|
+
let statement: ts.Node = node;
|
|
139
|
+
while (statement.parent && !compiler.isStatement(statement))
|
|
140
|
+
statement = statement.parent;
|
|
141
|
+
// A comment on an if/block/declaration is not a claim about every assertion inside it.
|
|
142
|
+
if (!compiler.isExpressionStatement(statement)) return;
|
|
143
|
+
const sf = node.getSourceFile();
|
|
144
|
+
for (const range of compiler.getLeadingCommentRanges(
|
|
145
|
+
sf.text,
|
|
146
|
+
statement.getFullStart(),
|
|
147
|
+
) ?? []) {
|
|
148
|
+
const comment = comments.get(key(sf, range.pos));
|
|
149
|
+
if (!comment) continue;
|
|
150
|
+
const attachment = {
|
|
151
|
+
testKey,
|
|
152
|
+
source: location(sf, node.getStart(sf)),
|
|
153
|
+
method,
|
|
154
|
+
inert,
|
|
155
|
+
};
|
|
156
|
+
if (
|
|
157
|
+
!comment.attachments.some(
|
|
158
|
+
(a) =>
|
|
159
|
+
a.testKey === testKey &&
|
|
160
|
+
a.source === attachment.source &&
|
|
161
|
+
a.method === method,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
comment.attachments.push(attachment);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
finish(
|
|
168
|
+
bindings: { testKey: string; id: string; phases?: AssertionPhase[] }[],
|
|
169
|
+
): PragmaHint[] {
|
|
170
|
+
return [...comments.values()].flatMap<PragmaHint>(
|
|
171
|
+
({ attachments, ...comment }) => {
|
|
172
|
+
const sources = new Set(
|
|
173
|
+
attachments.map((a) => `${a.source}#${a.method}`),
|
|
174
|
+
);
|
|
175
|
+
const issue =
|
|
176
|
+
comment.issue ??
|
|
177
|
+
(sources.size > 1
|
|
178
|
+
? "ambiguous-assertion"
|
|
179
|
+
: !sources.size
|
|
180
|
+
? "unattached-assertion"
|
|
181
|
+
: undefined);
|
|
182
|
+
const matched = bindings.flatMap((b) =>
|
|
183
|
+
attachments
|
|
184
|
+
.filter((a) => a.testKey === b.testKey && !a.inert)
|
|
185
|
+
.map((a) => ({ a, b })),
|
|
186
|
+
);
|
|
187
|
+
if (sources.size !== 1 || !matched.length)
|
|
188
|
+
return [
|
|
189
|
+
{
|
|
190
|
+
...comment,
|
|
191
|
+
issue: issue ?? "no-owning-passed-test",
|
|
192
|
+
witness: "unavailable" as const,
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
return matched.map(({ a, b }) => {
|
|
196
|
+
const witnessIssue = assertionWitnessIssue(
|
|
197
|
+
b.phases,
|
|
198
|
+
a.source,
|
|
199
|
+
a.method,
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
...comment,
|
|
203
|
+
id: `${comment.id}@${b.id}`,
|
|
204
|
+
issue,
|
|
205
|
+
test: b.id,
|
|
206
|
+
assertionSource: a.source,
|
|
207
|
+
assertionMethod: a.method,
|
|
208
|
+
witness: witnessIssue
|
|
209
|
+
? ("unavailable" as const)
|
|
210
|
+
: ("passed" as const),
|
|
211
|
+
witnessIssue,
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Source inventory supplied by the frontend. Positions are original-source UTF-16 offsets. */
|
|
2
|
+
export interface Site {
|
|
3
|
+
id: string;
|
|
4
|
+
file: string;
|
|
5
|
+
kind: "decision" | "effect";
|
|
6
|
+
category: string;
|
|
7
|
+
classification: "contractual" | "incidental" | "review";
|
|
8
|
+
start: { line: number; column: number };
|
|
9
|
+
end: { line: number; column: number };
|
|
10
|
+
pos: number;
|
|
11
|
+
endPos: number;
|
|
12
|
+
text: string;
|
|
13
|
+
fn: string;
|
|
14
|
+
owner: string;
|
|
15
|
+
exported: boolean;
|
|
16
|
+
note?: string;
|
|
17
|
+
chain?: string[];
|
|
18
|
+
method?: string;
|
|
19
|
+
arg0?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AnalyzeOptions {
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
/** Query-local evidence supplied in memory by the archive adapter, using original-source positions. */
|
|
25
|
+
evidenceFiles: Record<string, string>;
|
|
26
|
+
/** Exact files selected by the archive integration, including JS and nonstandard test directories. */
|
|
27
|
+
sourceFiles?: string[];
|
|
28
|
+
testFiles?: string[];
|
|
29
|
+
sourceDir?: string;
|
|
30
|
+
testDir?: string;
|
|
31
|
+
tsconfig?: string;
|
|
32
|
+
/** Defaults to the compiler API installed in the project being analyzed. */
|
|
33
|
+
typescript?: typeof import("typescript");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AnalysisDiagnostics {
|
|
37
|
+
runtimeTests: number;
|
|
38
|
+
linkedTests: number;
|
|
39
|
+
staticTests: number;
|
|
40
|
+
linkedByAssertionLines: number;
|
|
41
|
+
linkedByTitle: number;
|
|
42
|
+
linkWarnings: string[];
|
|
43
|
+
unrecognizedOperands: { shape: string; count: number }[];
|
|
44
|
+
compilerVersion: string;
|
|
45
|
+
}
|
package/docs/agent-loop.md
CHANGED
|
@@ -59,6 +59,19 @@ The `line` query is useful before writing a test because it shows which tests
|
|
|
59
59
|
already reach that line. Extending a nearby test is often better than adding a
|
|
60
60
|
duplicate.
|
|
61
61
|
|
|
62
|
+
For supported JS/TS projects, inspect assertion evidence before adding tests:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npx supercov runs latest assertions --file app/checkout/session.ts --limit 5 --json
|
|
66
|
+
npx supercov runs latest assertions --pragmas --json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This post-run query needs the matching source and a compatible
|
|
70
|
+
project TypeScript API. An `evident` candidate is not a proof of safety; keep
|
|
71
|
+
test gaps separate from analysis limits. Follow the returned evidence pointers
|
|
72
|
+
and `pagination.nextOffset`, pinning `--analysis` and the run id while paging.
|
|
73
|
+
See [assertion evidence](assertion-evidence.md) for requirements and examples.
|
|
74
|
+
|
|
62
75
|
## A complete prompt for longer runs
|
|
63
76
|
|
|
64
77
|
```text
|