@abloh/core 0.1.3 → 1.0.1
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/dist/index.d.ts +41424 -2361
- package/dist/index.js +35732 -3474
- package/dist/playground-admission.d.ts +142 -0
- package/dist/playground-admission.js +166 -0
- package/dist/playground-ingress.d.ts +18 -0
- package/dist/playground-ingress.js +62 -0
- package/dist/source-analysis/index.d.ts +714 -0
- package/dist/source-analysis/index.js +1945 -0
- package/package.json +19 -2
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export { default as ts } from 'typescript';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared TypeScript-AST utility (Extensions 2 + 5). Regex cannot safely reason about TSX,
|
|
6
|
+
* template literals, nested braces, comments, or modern syntax, so both the error-handler scanner
|
|
7
|
+
* (Extension 5) and the tautological-assertion scanner (Extension 2) parse with the real compiler.
|
|
8
|
+
*
|
|
9
|
+
* Parsing is best-effort and NEVER throws: `createSourceFile` recovers from syntax errors, so we
|
|
10
|
+
* surface a `parseOk` flag from the parser's own diagnostics. Callers MUST degrade to a
|
|
11
|
+
* `partial`/`unavailable` result on `parseOk === false` rather than reporting a clean empty scan —
|
|
12
|
+
* a file we could not fully parse must not read as "no findings".
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface ParsedSource {
|
|
16
|
+
sourceFile: ts.SourceFile;
|
|
17
|
+
/** false when the parser recorded syntax errors — treat the scan of this file as partial */
|
|
18
|
+
parseOk: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Parse TS/TSX/JS/JSX source into a SourceFile with parent pointers set. Never throws. */
|
|
21
|
+
declare function parseSource(fileName: string, source: string): ParsedSource;
|
|
22
|
+
/** 1-based source line of a character position. */
|
|
23
|
+
declare function lineOf(sourceFile: ts.SourceFile, pos: number): number;
|
|
24
|
+
/**
|
|
25
|
+
* True when the text span of `node` contains a real COMMENT token (single- or multi-line) whose
|
|
26
|
+
* text matches `pattern`. Uses the scanner (not a regex over source) so a `TODO` inside a string
|
|
27
|
+
* literal or identifier is never mistaken for a comment.
|
|
28
|
+
*/
|
|
29
|
+
declare function commentInNodeMatches(node: ts.Node, sourceFile: ts.SourceFile, source: string, pattern: RegExp): boolean;
|
|
30
|
+
|
|
31
|
+
interface MutationRecipe {
|
|
32
|
+
/** workdir-relative file the edit targets */
|
|
33
|
+
file: string;
|
|
34
|
+
/** UTF-16 code-unit offsets of the span to replace, half-open [startOffset, endOffset) */
|
|
35
|
+
startOffset: number;
|
|
36
|
+
endOffset: number;
|
|
37
|
+
/** the exact source slice at [startOffset,endOffset) when the recipe was built (drift guard) */
|
|
38
|
+
original: string;
|
|
39
|
+
/** the replacement text */
|
|
40
|
+
replacement: string;
|
|
41
|
+
/** sha256 of the whole file source when the recipe was built (drift guard) */
|
|
42
|
+
sourceDigest: string;
|
|
43
|
+
/** stable content-hash id — independent of LLM/report order (dedup + selection key) */
|
|
44
|
+
id: string;
|
|
45
|
+
/** realistic-mutant category, e.g. "missing-await" (presentational + composition metadata) */
|
|
46
|
+
category?: string;
|
|
47
|
+
}
|
|
48
|
+
declare function sha256(text: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Line/column → UTF-16 offset. `line` is 1-based; `column` is 1-based (the mutation-testing report
|
|
51
|
+
* schema convention). `\n` is the line separator, so a `\r` in a CRLF file stays as the last code
|
|
52
|
+
* unit of its line and is counted by the column offset — the recipe operates on the exact bytes,
|
|
53
|
+
* CRLF included. Out-of-range coordinates clamp to the source length.
|
|
54
|
+
*/
|
|
55
|
+
declare function offsetOf(source: string, line: number, column: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a 1-based (line, column) to an offset that provably lies ON that line, or null.
|
|
58
|
+
*
|
|
59
|
+
* `offsetOf` clamps only against the whole file, so a column past the end of its line silently walks
|
|
60
|
+
* into LATER lines. Scope validation checks a proposal's line numbers, so that clamping is a scope
|
|
61
|
+
* escape: a proposal declaring a changed line can land on unchanged code. Out-of-range coordinates
|
|
62
|
+
* are therefore REJECTED here rather than clamped.
|
|
63
|
+
*/
|
|
64
|
+
declare function strictOffsetOf(source: string, line: number, column: number): number | null;
|
|
65
|
+
/** Build a recipe's stable content-hash id from its source-derived fields (order-independent). */
|
|
66
|
+
declare function recipeId(file: string, startOffset: number, endOffset: number, original: string, replacement: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Build a recipe from 1-based line/column span coordinates. The BOM (if present) is part of the
|
|
69
|
+
* source and counted in offsets, so digests + slices stay exact.
|
|
70
|
+
*/
|
|
71
|
+
declare function recipeFromSpan(file: string, source: string, span: {
|
|
72
|
+
startLine: number;
|
|
73
|
+
startColumn: number;
|
|
74
|
+
endLine: number;
|
|
75
|
+
endColumn: number;
|
|
76
|
+
}, replacement: string, category?: string): MutationRecipe;
|
|
77
|
+
/**
|
|
78
|
+
* Span → recipe, or null when the coordinates do not lie within their declared lines. Callers
|
|
79
|
+
* handling UNTRUSTED coordinates (model proposals) must use this and reject the null rather than
|
|
80
|
+
* let an out-of-line column clamp into unchanged code — see {@link strictOffsetOf}.
|
|
81
|
+
*/
|
|
82
|
+
declare function tryRecipeFromSpan(file: string, source: string, span: {
|
|
83
|
+
startLine: number;
|
|
84
|
+
startColumn: number;
|
|
85
|
+
endLine: number;
|
|
86
|
+
endColumn: number;
|
|
87
|
+
}, replacement: string, category?: string): MutationRecipe | null;
|
|
88
|
+
/**
|
|
89
|
+
* Build a recipe directly from UTF-16 offsets (half-open [startOffset, endOffset)). Used where a
|
|
90
|
+
* span is already known in offset form (e.g. AST node bounds) so no line/column round-trip is needed.
|
|
91
|
+
*/
|
|
92
|
+
declare function recipeFromOffsets(file: string, source: string, startOffset: number, endOffset: number, replacement: string, category?: string): MutationRecipe;
|
|
93
|
+
type ApplyResult = {
|
|
94
|
+
ok: true;
|
|
95
|
+
result: string;
|
|
96
|
+
} | {
|
|
97
|
+
ok: false;
|
|
98
|
+
reason: "source-drift" | "slice-mismatch" | "bad-span";
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Materialize a recipe against `source`, returning the mutated text. Fails closed on any drift:
|
|
102
|
+
* a changed file (digest mismatch), a moved span (original-slice mismatch), or an inverted span.
|
|
103
|
+
*/
|
|
104
|
+
declare function applyRecipe(source: string, recipe: MutationRecipe): ApplyResult;
|
|
105
|
+
/**
|
|
106
|
+
* Make a file path safe to use as a Stryker `mutate` entry that carries a line range.
|
|
107
|
+
*
|
|
108
|
+
* Stryker validates a ranged entry with `new Minimatch(entry).hasMagic()` and REFUSES it when the
|
|
109
|
+
* pattern looks like a glob — "Cannot combine a glob expression with a mutation range". A Next.js
|
|
110
|
+
* App Router directory is literally named `[workspaceId]`, so every changed file under a dynamic
|
|
111
|
+
* route made the whole mutation run exit 1 before a single mutant was generated. It is not a rare
|
|
112
|
+
* shape: it took one real repository to hit it across 50+ files at once.
|
|
113
|
+
*
|
|
114
|
+
* Backslash-escaping the magic characters clears `hasMagic()` while still matching the literal
|
|
115
|
+
* path, which is the behaviour Minimatch documents and what the fix is verified against.
|
|
116
|
+
*/
|
|
117
|
+
declare function escapeMutatePath(path: string): string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Normalized result contract `attest-results/v2` (build plan §4.2.1).
|
|
121
|
+
*
|
|
122
|
+
* Two-layer rule: the producer's own report, in whichever dialect it writes, is preserved verbatim
|
|
123
|
+
* as evidence; THIS schema is what every downstream consumer reads. `@abloh/measure` maps a raw
|
|
124
|
+
* report to this one and the mutation engine's seam produces it directly; a future producer
|
|
125
|
+
* implements the same mapping. We never extend or mutate someone else's schema.
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
type HandlerAntiPatternKind = "empty-catch" | "catch-all-abort" | "todo-in-handler";
|
|
129
|
+
interface HandlerFinding {
|
|
130
|
+
file: string;
|
|
131
|
+
startLine: number;
|
|
132
|
+
endLine: number;
|
|
133
|
+
kind: HandlerAntiPatternKind;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface HandlerSpan {
|
|
137
|
+
file: string;
|
|
138
|
+
startLine: number;
|
|
139
|
+
endLine: number;
|
|
140
|
+
}
|
|
141
|
+
interface FileHandlerScan {
|
|
142
|
+
antiPatterns: HandlerFinding[];
|
|
143
|
+
/** every changed catch-handler span (mutant classification keys off these later) */
|
|
144
|
+
changedHandlers: HandlerSpan[];
|
|
145
|
+
/** false when the file did not parse cleanly — caller must treat the scan as partial */
|
|
146
|
+
parseOk: boolean;
|
|
147
|
+
}
|
|
148
|
+
/** Scan one file's source for changed-handler anti-patterns. `changedLines` are 1-based. */
|
|
149
|
+
declare function scanFileHandlers(fileName: string, source: string, changedLines: ReadonlySet<number>): FileHandlerScan;
|
|
150
|
+
/**
|
|
151
|
+
* Materialize deterministic FORCED handler mutations (Extension 5 reserve). For each changed catch
|
|
152
|
+
* clause we neuter its FIRST executable statement (`stmt` → `;`), a real handler-behavior change
|
|
153
|
+
* that the carrier smuggles through Stryker so the handler is always probed even when the sampler
|
|
154
|
+
* would not have reached it. No LLM — purely structural, so it runs at every tier. Empty handlers
|
|
155
|
+
* and handlers whose first statement is not carriable are skipped (nothing to force). Deterministic
|
|
156
|
+
* ordering by (file, offset); a file that does not parse yields no recipes.
|
|
157
|
+
*/
|
|
158
|
+
declare function forcedHandlerRecipes(fileName: string, source: string, changedLines: ReadonlySet<number>): MutationRecipe[];
|
|
159
|
+
interface HandlerScanInput {
|
|
160
|
+
file: string;
|
|
161
|
+
source: string;
|
|
162
|
+
/** 1-based changed lines for this file (from diff-scope inspectionScopes) */
|
|
163
|
+
changedLines: ReadonlySet<number>;
|
|
164
|
+
}
|
|
165
|
+
interface ErrorHandlerScan {
|
|
166
|
+
antiPatterns: HandlerFinding[];
|
|
167
|
+
changedHandlers: HandlerSpan[];
|
|
168
|
+
quality: "complete" | "partial";
|
|
169
|
+
}
|
|
170
|
+
/** Scan several files; a single unparseable file downgrades overall `quality` to `partial`. */
|
|
171
|
+
declare function scanErrorHandlers(inputs: readonly HandlerScanInput[]): ErrorHandlerScan;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Tautological-assertion scanner (Extension 2). Conservative and high-confidence: it flags a test
|
|
175
|
+
* ONLY when every assertion it makes is a recognized self-confirming form (`assert.equal(x, x)`,
|
|
176
|
+
* `expect(x).toBe(x)`, `assert(true)`, …) AND no unrecognized assertion shape appears — so a test
|
|
177
|
+
* doing real work is never mislabeled. Assertion SOURCE is never emitted; only the canonical test
|
|
178
|
+
* identity (`<file>::<fullName>`) is returned, and the CLI reduces even that to a digest at egress.
|
|
179
|
+
*
|
|
180
|
+
* Uses the shared TS-AST utility (a regex cannot tell a `TODO` in a comment from one in a string,
|
|
181
|
+
* nor `toBe(x)` on `x` from `toBe(y)`). A file that fails to parse downgrades quality to `partial`.
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
interface TautologyScanInput {
|
|
185
|
+
/** test file path (report key) */
|
|
186
|
+
file: string;
|
|
187
|
+
source: string;
|
|
188
|
+
}
|
|
189
|
+
interface TautologyResult {
|
|
190
|
+
/** canonical `<file>::<fullName>` of tests whose every recognized assertion is tautological */
|
|
191
|
+
tautologicalTests: string[];
|
|
192
|
+
quality: "complete" | "partial";
|
|
193
|
+
}
|
|
194
|
+
/** Exported so corpus mining derives fullNames by the SAME rule the runner matches on. */
|
|
195
|
+
declare const TEST_FNS: Set<string>;
|
|
196
|
+
declare const DESCRIBE_FNS: Set<string>;
|
|
197
|
+
/**
|
|
198
|
+
* The base test/describe identifier for a call, following modifier chains, or null.
|
|
199
|
+
*
|
|
200
|
+
* `it("…")` is an identifier call, but `it.only("…")`, `test.each(table)("…")` and
|
|
201
|
+
* `describe.concurrent("…")` are property-access (and, for `each`, call) chains. Recognizing only
|
|
202
|
+
* the bare identifiers meant modified tests were never scanned at all — while the scan still
|
|
203
|
+
* reported `complete` quality, which overstates coverage of the file.
|
|
204
|
+
*/
|
|
205
|
+
/** Is this a parameterized declaration (`test.each(...)`) whose real test names we cannot know? */
|
|
206
|
+
declare function isParameterized(expr: ts.Expression): boolean;
|
|
207
|
+
declare function testCallName(expr: ts.Expression): string | null;
|
|
208
|
+
declare function stringArg(call: ts.CallExpression): string | null;
|
|
209
|
+
/**
|
|
210
|
+
* Scan test files for fully-tautological tests. One unparseable file → `quality: "partial"`.
|
|
211
|
+
*
|
|
212
|
+
* SCANNING NOTHING IS NOT SCANNING CLEANLY. `quality` began as `"complete"` and was only ever
|
|
213
|
+
* downgraded inside the loop, so an empty input list — a runner whose report carries no test
|
|
214
|
+
* sources, coverage switched off, a command runner — returned `{ tautologicalTests: [], quality:
|
|
215
|
+
* "complete" }`. That is a positive claim: we read your suite and found no test whose every
|
|
216
|
+
* assertion is a tautology. We had read nothing. An empty input is reported `partial`, which is the
|
|
217
|
+
* existing word for "this scan did not cover everything".
|
|
218
|
+
*/
|
|
219
|
+
declare function scanTautologies(inputs: readonly TautologyScanInput[], workDir?: string): TautologyResult;
|
|
220
|
+
|
|
221
|
+
/** How the mutated code can be reached from outside its module. */
|
|
222
|
+
type Reachability =
|
|
223
|
+
/** the function holding the mutation is itself exported */
|
|
224
|
+
"exported-directly"
|
|
225
|
+
/** private, but an exported symbol calls it (see `entryPoints`) */
|
|
226
|
+
| "reachable-via"
|
|
227
|
+
/** private, and nothing exported in this file reaches it — the decline signal */
|
|
228
|
+
| "unreachable"
|
|
229
|
+
/** we could not tell; callers must NOT decline on this */
|
|
230
|
+
| "unknown";
|
|
231
|
+
interface ParameterInfo {
|
|
232
|
+
name: string;
|
|
233
|
+
optional: boolean;
|
|
234
|
+
/** the type as WRITTEN (`Packet`, `{ a: number }`, `string[]`), or null when untyped */
|
|
235
|
+
type: string | null;
|
|
236
|
+
rest: boolean;
|
|
237
|
+
}
|
|
238
|
+
interface EntryPoint {
|
|
239
|
+
/** exported symbol name, or "default" */
|
|
240
|
+
name: string;
|
|
241
|
+
kind: "function" | "method" | "variable" | "default";
|
|
242
|
+
/** rendered from the syntax, e.g. `tableBlock(packet: Packet, view: View): Block` */
|
|
243
|
+
signature: string;
|
|
244
|
+
parameters: ParameterInfo[];
|
|
245
|
+
returnType: string | null;
|
|
246
|
+
isAsync: boolean;
|
|
247
|
+
/** ["tableBlock", "buildFlatTable"] — first is the export, last holds the mutation */
|
|
248
|
+
path: string[];
|
|
249
|
+
/** the source line of each call along the path (one fewer than `path`) */
|
|
250
|
+
callSites: string[];
|
|
251
|
+
/** distinct type names named in the signature — what `collectTypeContext` resolves */
|
|
252
|
+
typeNames: string[];
|
|
253
|
+
}
|
|
254
|
+
interface EnclosingFunction {
|
|
255
|
+
/** `buildFlatTable`, `Class.method`, or null when anonymous */
|
|
256
|
+
name: string | null;
|
|
257
|
+
/** the FULL declaration text including leading JSDoc — not a character window */
|
|
258
|
+
text: string;
|
|
259
|
+
startLine: number;
|
|
260
|
+
endLine: number;
|
|
261
|
+
exported: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Type names in THIS function's own signature.
|
|
264
|
+
*
|
|
265
|
+
* Separate from the entry point's, and both are needed. A private worker is usually called with
|
|
266
|
+
* values the entry point derives rather than with its own arguments — `tableBlock(packet, view)`
|
|
267
|
+
* calls `buildFlatTable(columns: Column[], …)`, and `Column` appears in no entry signature. On a
|
|
268
|
+
* real run the model therefore never saw `Column`, passed plain strings where `{ key }` objects
|
|
269
|
+
* were required, and the test failed identically on real and mutated code.
|
|
270
|
+
*/
|
|
271
|
+
typeNames: string[];
|
|
272
|
+
}
|
|
273
|
+
interface ExportSurface {
|
|
274
|
+
/** sorted export names — byte-identical to the CLI's previous listModuleExports */
|
|
275
|
+
named: string[];
|
|
276
|
+
hasDefault: boolean;
|
|
277
|
+
/** name → declaration, so a signature can be read. `named` never depends on this. */
|
|
278
|
+
declarations: ReadonlyMap<string, ts.Declaration>;
|
|
279
|
+
}
|
|
280
|
+
interface ReachabilityAnalysis {
|
|
281
|
+
/** "unavailable" ⇔ the file did not parse, or the offsets landed in no node */
|
|
282
|
+
quality: "complete" | "unavailable";
|
|
283
|
+
reachability: Reachability;
|
|
284
|
+
enclosing: EnclosingFunction | null;
|
|
285
|
+
/** shortest paths first, then lexicographic — deterministic across runs */
|
|
286
|
+
entryPoints: EntryPoint[];
|
|
287
|
+
exportSurface: ExportSurface;
|
|
288
|
+
/** module specifiers this file imports that name a known external service */
|
|
289
|
+
serviceImports: string[];
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Every node containing [start, end], smallest span first. Lifted from carrier.ts, which had it
|
|
293
|
+
* privately; that file now imports it so the two can never drift.
|
|
294
|
+
*/
|
|
295
|
+
declare function enclosingNodes(sf: ts.SourceFile, start: number, end: number): ts.Node[];
|
|
296
|
+
/**
|
|
297
|
+
* A module's export surface.
|
|
298
|
+
*
|
|
299
|
+
* `named` and `hasDefault` are byte-identical to the CLI's previous `listModuleExports`, which is
|
|
300
|
+
* load-bearing rather than tidy: the invariant engine hashes that list into its cache key, so a
|
|
301
|
+
* change of even one name silently invalidates every cached proposal. `declarations` is additive —
|
|
302
|
+
* the old function returned strings and discarded the node, which made it impossible to read a
|
|
303
|
+
* signature back off an export.
|
|
304
|
+
*/
|
|
305
|
+
declare function moduleExportSurface(sourceFile: ts.SourceFile): ExportSurface;
|
|
306
|
+
interface AnalyzeInput {
|
|
307
|
+
fileName: string;
|
|
308
|
+
source: string;
|
|
309
|
+
/** byte offsets of the mutated span */
|
|
310
|
+
startOffset: number;
|
|
311
|
+
endOffset: number;
|
|
312
|
+
maxEntryPoints?: number;
|
|
313
|
+
maxDepth?: number;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Analyze how (and whether) a test can reach the mutated span. Never throws; see the module header
|
|
317
|
+
* for the degradation contract that callers must honor.
|
|
318
|
+
*/
|
|
319
|
+
declare function analyzeReachability(input: AnalyzeInput): ReachabilityAnalysis;
|
|
320
|
+
interface TypeContextInput {
|
|
321
|
+
fileName: string;
|
|
322
|
+
source: string;
|
|
323
|
+
names: readonly string[];
|
|
324
|
+
/** injected so this stays pure and unit-testable; the CLI passes a sandbox-scoped reader */
|
|
325
|
+
readFile: (absPath: string) => string | null;
|
|
326
|
+
maxChars?: number;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The TEXT of the type declarations a signature names — the difference between a model knowing a
|
|
330
|
+
* `Packet` exists and being able to construct one.
|
|
331
|
+
*
|
|
332
|
+
* Local declarations first, then ONE hop through this file's own relative imports. One hop, not
|
|
333
|
+
* transitive: it bounds both the cost and the prompt, and a type two modules away is rarely the
|
|
334
|
+
* one the entry point takes. Anything unresolved is reported by name so the prompt can tell the
|
|
335
|
+
* model to build a minimal object and cast it, rather than leaving it to guess in silence.
|
|
336
|
+
*/
|
|
337
|
+
declare function collectTypeContext(input: TypeContextInput): {
|
|
338
|
+
text: string;
|
|
339
|
+
resolved: string[];
|
|
340
|
+
unresolved: string[];
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* How several survivors in one place are presented as ONE finding.
|
|
345
|
+
*
|
|
346
|
+
* Declared here, in core, because it is a config value the engine reads — the engine depends on
|
|
347
|
+
* core, never the reverse, so the closed set has to live on this side of that edge.
|
|
348
|
+
*
|
|
349
|
+
* - `line` — group by source line. Wrong in both directions: two operands of one `if` on
|
|
350
|
+
* separate lines render as two gaps, while two unrelated statements sharing a
|
|
351
|
+
* line render as one. Retained ONLY because the hosted control plane never
|
|
352
|
+
* receives source and so cannot do better.
|
|
353
|
+
* - `structural` — group by the smallest enclosing STATEMENT, with containment capped so a
|
|
354
|
+
* whole-function survivor cannot absorb everything inside it. A fact about the
|
|
355
|
+
* syntax tree, so no merge can be wrong. Fixes both of line's errors. THE DEFAULT.
|
|
356
|
+
*
|
|
357
|
+
* TWO STRATEGIES WERE REMOVED, both on measurement rather than taste:
|
|
358
|
+
*
|
|
359
|
+
* - `data-flow` merged clusters sharing a resolved binding inside one function. On 189 real
|
|
360
|
+
* survivors across 17 files it produced **2 merges beyond structural**, in 2 files. A setting
|
|
361
|
+
* that changes nothing is worse than an absent one, because someone eventually enables it and
|
|
362
|
+
* wonders why their output is identical. The scope resolver it was built on REMAINS — see
|
|
363
|
+
* `core/src/source-analysis/scope.ts`; the `wrong-variable` mutator depends on it to avoid substituting
|
|
364
|
+
* a name that is not in scope, and nothing else can tell two same-named variables apart.
|
|
365
|
+
* - `semantic` asked a model to merge. gpt-5.6-terra at `xhigh` also managed **2 merges** on the
|
|
366
|
+
* same corpus — a mechanical analyser and the best available model independently agreeing there is
|
|
367
|
+
* nothing to merge beyond the syntax tree.
|
|
368
|
+
*
|
|
369
|
+
* Every strategy keeps each mutant individually in the artifact and displays each cluster's member
|
|
370
|
+
* count, so no signed number depends on this setting.
|
|
371
|
+
*/
|
|
372
|
+
declare const CLUSTER_STRATEGIES: readonly ["line", "structural"];
|
|
373
|
+
type ClusterStrategy = (typeof CLUSTER_STRATEGIES)[number];
|
|
374
|
+
|
|
375
|
+
/** One mutant, as the clusterer needs it. A subset of `GapFinding` — no triage, no status. */
|
|
376
|
+
interface ClusterInput {
|
|
377
|
+
mutantId: string;
|
|
378
|
+
file: string;
|
|
379
|
+
startLine: number;
|
|
380
|
+
endLine: number;
|
|
381
|
+
mutator: string;
|
|
382
|
+
/** 1-based, optional — absent on historical artifacts */
|
|
383
|
+
startColumn?: number;
|
|
384
|
+
endColumn?: number;
|
|
385
|
+
/** the exact source slice this mutant replaced; used to VERIFY the span, never to render */
|
|
386
|
+
originalText?: string;
|
|
387
|
+
}
|
|
388
|
+
interface Cluster {
|
|
389
|
+
/** stable id, derived from the first member — matches the previous `mutant:<id>` convention */
|
|
390
|
+
id: string;
|
|
391
|
+
file: string;
|
|
392
|
+
startLine: number;
|
|
393
|
+
endLine: number;
|
|
394
|
+
/** every mutant in this cluster. `members.length` is the count a surface must display. */
|
|
395
|
+
members: ClusterInput[];
|
|
396
|
+
mutators: string[];
|
|
397
|
+
/**
|
|
398
|
+
* The strategy that actually decided THIS cluster. It can be `line` even when a different
|
|
399
|
+
* strategy was requested — see {@link ClusterResult.degraded}. Recording it per cluster is the
|
|
400
|
+
* point: a run must never present a fallback as if it were the thing that was asked for.
|
|
401
|
+
*/
|
|
402
|
+
by: ClusterStrategy;
|
|
403
|
+
}
|
|
404
|
+
interface ClusterResult {
|
|
405
|
+
clusters: Cluster[];
|
|
406
|
+
/** the strategy that was requested */
|
|
407
|
+
requested: ClusterStrategy;
|
|
408
|
+
/**
|
|
409
|
+
* Mutants that fell back to line grouping because their span could not be located.
|
|
410
|
+
*
|
|
411
|
+
* Reported, never swallowed. A stage that silently degrades and renders its own failure as a fact
|
|
412
|
+
* about the customer's code is the defect class this repo spent a week removing; a clusterer that
|
|
413
|
+
* quietly grouped by line while claiming to group structurally would be another instance of it.
|
|
414
|
+
*/
|
|
415
|
+
degraded: {
|
|
416
|
+
mutantId: string;
|
|
417
|
+
reason: string;
|
|
418
|
+
}[];
|
|
419
|
+
}
|
|
420
|
+
/** Today's rule, kept verbatim so the hosted path can call it instead of re-implementing it. */
|
|
421
|
+
declare function lineKey(m: {
|
|
422
|
+
file: string;
|
|
423
|
+
startLine: number;
|
|
424
|
+
endLine: number;
|
|
425
|
+
}): string;
|
|
426
|
+
/**
|
|
427
|
+
* Group mutants into clusters.
|
|
428
|
+
*
|
|
429
|
+
* `sources` maps a workdir-relative file path to its current source. A file that is missing from
|
|
430
|
+
* the map is not an error — its mutants fall back to line grouping and are reported in `degraded`.
|
|
431
|
+
*/
|
|
432
|
+
declare function clusterMutants(mutants: readonly ClusterInput[], sources: ReadonlyMap<string, string>, strategy: ClusterStrategy): ClusterResult;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Lexical scope resolution — which DECLARATION does this identifier refer to.
|
|
436
|
+
*
|
|
437
|
+
* Everything else in this engine answers that question by comparing `Identifier.text` against a
|
|
438
|
+
* flat, file-global namespace. `buildCallGraph` keys functions by bare name and the first
|
|
439
|
+
* declaration wins, so two same-named functions collapse into one node. `localNode` looks only at
|
|
440
|
+
* top-level statements. `referencedElsewhere` counts any identifier whose text happens to match.
|
|
441
|
+
* `ts.isBlock` appears nowhere in the repo. That is fine for the heuristics those functions feed —
|
|
442
|
+
* they widen a search or add a call edge, and a spurious match costs recall, not correctness.
|
|
443
|
+
*
|
|
444
|
+
* It is NOT fine for finding clustering, where the question is "do these two mutants touch the same
|
|
445
|
+
* data". There, matching on spelling merges
|
|
446
|
+
*
|
|
447
|
+
* const total = a + b; // mutant 1
|
|
448
|
+
* { const total = c + d; } // mutant 2 — a DIFFERENT variable
|
|
449
|
+
*
|
|
450
|
+
* into one finding, and one of the two mutants stops being visible as its own gap. A merge that
|
|
451
|
+
* hides a gap is the one failure this product cannot ship: everywhere else in the pipeline we spent
|
|
452
|
+
* the week removing places where our own silence was rendered as a fact about the customer.
|
|
453
|
+
*
|
|
454
|
+
* So this module resolves properly. SYNTAX ONLY, like the rest of the engine — no `ts.Program`, no
|
|
455
|
+
* `TypeChecker`, no customer tsconfig, no module resolution. A scope chain built from the syntax
|
|
456
|
+
* tree answers shadowing exactly; what it cannot answer, it declines by returning null.
|
|
457
|
+
*
|
|
458
|
+
* The bias is deliberate and one-directional: an identifier we cannot resolve merges with NOTHING.
|
|
459
|
+
* Unresolved is never treated as "matches everything", so every failure of this module costs the
|
|
460
|
+
* author an extra finding, never a missing one.
|
|
461
|
+
*/
|
|
462
|
+
|
|
463
|
+
/** How a name was introduced. `var` and `function` hoist to the function scope; the rest do not. */
|
|
464
|
+
type BindingKind = "const" | "let" | "var" | "param" | "function" | "class" | "import" | "catch";
|
|
465
|
+
interface Binding {
|
|
466
|
+
name: string;
|
|
467
|
+
/**
|
|
468
|
+
* The declaration node — this is the IDENTITY. Two identifiers refer to the same thing when they
|
|
469
|
+
* resolve to the same node, never when they merely spell the same. Destructuring gives each bound
|
|
470
|
+
* name its own `BindingElement`, so `const { a, b } = x` yields two distinct identities.
|
|
471
|
+
*/
|
|
472
|
+
decl: ts.Declaration;
|
|
473
|
+
kind: BindingKind;
|
|
474
|
+
}
|
|
475
|
+
interface Scope {
|
|
476
|
+
parent: Scope | null;
|
|
477
|
+
/** var/function declarations hoist past block scopes to the nearest scope with this set */
|
|
478
|
+
isFunctionScope: boolean;
|
|
479
|
+
bindings: Map<string, Binding>;
|
|
480
|
+
}
|
|
481
|
+
/** The scope table for one file: every scope-opening node mapped to its bindings. */
|
|
482
|
+
interface ScopeTable {
|
|
483
|
+
byNode: Map<ts.Node, Scope>;
|
|
484
|
+
sourceFile: ts.SourceFile;
|
|
485
|
+
/**
|
|
486
|
+
* Memo for {@link isReassigned}, which walks the whole file per query. Clustering asks about the
|
|
487
|
+
* same handful of bindings once per mutant, so without this a file with 200 survivors re-walks
|
|
488
|
+
* its own AST hundreds of times.
|
|
489
|
+
*/
|
|
490
|
+
reassigned: Map<ts.Declaration, boolean>;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Build the scope chain for a file.
|
|
494
|
+
*
|
|
495
|
+
* One pass. Declarations are routed to the scope that actually owns them: a `var` or function
|
|
496
|
+
* declaration walks out to the nearest function scope, everything else lands in the innermost
|
|
497
|
+
* scope. A function's NAME belongs to the enclosing scope while its PARAMETERS belong to its own —
|
|
498
|
+
* getting that backwards is what makes a parameter appear to shadow nothing.
|
|
499
|
+
*/
|
|
500
|
+
declare function buildScopes(sourceFile: ts.SourceFile): ScopeTable;
|
|
501
|
+
/**
|
|
502
|
+
* Resolve an identifier to the declaration it names, or null.
|
|
503
|
+
*
|
|
504
|
+
* Null means "we could not tell", and every caller must treat it as "matches nothing" rather than
|
|
505
|
+
* "matches anything" — see the module header. Null is returned for globals, for names imported via
|
|
506
|
+
* paths we do not follow, and for identifiers in a naming rather than referencing position.
|
|
507
|
+
*/
|
|
508
|
+
declare function resolveBinding(table: ScopeTable, id: ts.Identifier): ts.Declaration | null;
|
|
509
|
+
/**
|
|
510
|
+
* Is this binding ever REASSIGNED anywhere in the file?
|
|
511
|
+
*
|
|
512
|
+
* This is what makes alias-following sound. `const b = a` says `b` and `a` are the same value —
|
|
513
|
+
* but only while `a` cannot change under it:
|
|
514
|
+
*
|
|
515
|
+
* let a = 1; const b = a; a = 2; // b is a SNAPSHOT, not an alias
|
|
516
|
+
*
|
|
517
|
+
* Merging findings on `b` with findings on `a` there would be a genuine over-merge, the one
|
|
518
|
+
* direction that can hide a gap. So the alias chain refuses to follow whenever either end is
|
|
519
|
+
* reassigned.
|
|
520
|
+
*
|
|
521
|
+
* Assignment to a PROPERTY (`obj.x = 1`) is not reassignment of `obj` — the binding still names the
|
|
522
|
+
* same object — so only a bare identifier on the left counts.
|
|
523
|
+
*/
|
|
524
|
+
declare function isReassigned(table: ScopeTable, decl: ts.Declaration): boolean;
|
|
525
|
+
/**
|
|
526
|
+
* Resolve an identifier, then follow `const b = a` alias chains to the ORIGINAL binding.
|
|
527
|
+
*
|
|
528
|
+
* Without this, `b.total` and `a.total` cluster separately even though they are the same data —
|
|
529
|
+
* an under-merge, harmless but avoidable. With it they cluster together, and the reassignment gate
|
|
530
|
+
* above keeps the merge sound.
|
|
531
|
+
*
|
|
532
|
+
* ONLY a bare identifier initializer is followed. `const b = a.x` is deliberately NOT followed:
|
|
533
|
+
* `b` and `a` are different data (the second is a whole object, the first one of its properties),
|
|
534
|
+
* so resolving `b` to `a`'s declaration would merge two findings that are not the same gap. That
|
|
535
|
+
* case is an under-merge and stays one. Calls, `await` and `new` are never followed for the same
|
|
536
|
+
* reason — their result is a new value, not the named one.
|
|
537
|
+
*/
|
|
538
|
+
declare function resolveThroughAliases(table: ScopeTable, id: ts.Identifier): ts.Declaration | null;
|
|
539
|
+
/**
|
|
540
|
+
* Every name in scope at a position, as a set.
|
|
541
|
+
*
|
|
542
|
+
* Used by the `wrong-variable` mutator, which today collects every identifier in the FILE and
|
|
543
|
+
* substitutes one, reasoning that "an invented name would be a compile error". A real name that is
|
|
544
|
+
* not in scope at the mutation site is equally a compile error — those mutants consume a run slot
|
|
545
|
+
* and come back `build-error`, which is wasted budget, not evidence.
|
|
546
|
+
*/
|
|
547
|
+
declare function namesInScopeAt(table: ScopeTable, node: ts.Node): Set<string>;
|
|
548
|
+
|
|
549
|
+
declare const SAMPLING_ALGORITHM = "line-first-rr-v1";
|
|
550
|
+
/**
|
|
551
|
+
* One enumerated (not yet run) mutant. Lines are 1-BASED here (matching FileScope, git diffs, and
|
|
552
|
+
* the human-facing config strings); columns are 0-based babel columns, end exclusive.
|
|
553
|
+
*
|
|
554
|
+
* CONVENTION MAP (each verified in the respective package's source, not assumed):
|
|
555
|
+
* - config string "file.js:5:4-6:4": 1-based lines, 0-based columns (core project-reader.js
|
|
556
|
+
* parses with parseInt(line) - 1 and passes columns through).
|
|
557
|
+
* - FileDescription.mutate: 0-based lines (instrumenter's toBabelLineNumber ADDS 1 for babel).
|
|
558
|
+
* - api Mutant.location (what instrument() returns): 0-based lines, 0-based columns.
|
|
559
|
+
* This module converts at its boundaries so callers only ever see 1-based lines.
|
|
560
|
+
*/
|
|
561
|
+
interface EnumeratedMutant {
|
|
562
|
+
/** workdir-relative file (posix separators) */
|
|
563
|
+
file: string;
|
|
564
|
+
mutatorName: string;
|
|
565
|
+
startLine: number;
|
|
566
|
+
startColumn: number;
|
|
567
|
+
endLine: number;
|
|
568
|
+
endColumn: number;
|
|
569
|
+
/**
|
|
570
|
+
* The mutated text this mutant substitutes.
|
|
571
|
+
*
|
|
572
|
+
* Load-bearing for IDENTITY, not decoration: file + span + operator is NOT unique — one operator
|
|
573
|
+
* routinely emits several mutants at the same span (measured on a real 315-mutant file: 57 of
|
|
574
|
+
* them shared a span+operator with another, so keying without the replacement collapsed 315
|
|
575
|
+
* distinct mutants into 258 and made the executed-set accounting undercount). Optional because a
|
|
576
|
+
* mutant may legitimately carry no replacement text.
|
|
577
|
+
*/
|
|
578
|
+
replacement?: string;
|
|
579
|
+
}
|
|
580
|
+
interface SamplePlan {
|
|
581
|
+
algorithm: typeof SAMPLING_ALGORITHM;
|
|
582
|
+
seed: string;
|
|
583
|
+
cap: number;
|
|
584
|
+
/** every mutant the changed ranges allow (the honest denominator for disclosure) */
|
|
585
|
+
eligible: number;
|
|
586
|
+
/** the picked subset — the mutants this plan deliberately selected */
|
|
587
|
+
sampled: EnumeratedMutant[];
|
|
588
|
+
/**
|
|
589
|
+
* Every mutant that will ACTUALLY RUN: the picks plus their containment closure.
|
|
590
|
+
*
|
|
591
|
+
* A Stryker `mutate` entry is a RANGE, so selecting a mutant also selects every mutant nested
|
|
592
|
+
* inside its span (an arrow-function pick drags in each mutant in its body). Before this was
|
|
593
|
+
* charged at plan time a cap of 500 executed 717 mutants and the run summary contradicted
|
|
594
|
+
* itself. The budget is charged against this set, so `executed.length <= cap` holds.
|
|
595
|
+
*/
|
|
596
|
+
executed: EnumeratedMutant[];
|
|
597
|
+
/** changed lines bearing at least one eligible mutant */
|
|
598
|
+
linesEligible: number;
|
|
599
|
+
/** lines with at least one EXECUTED mutant — breadth, the disclosed metric */
|
|
600
|
+
linesProbed: number;
|
|
601
|
+
/**
|
|
602
|
+
* Picks skipped because their containment closure alone exceeded the remaining budget. Disclosed
|
|
603
|
+
* rather than silently dropped: a large pick that never fits means those lines were probed only
|
|
604
|
+
* by smaller mutants, or not at all.
|
|
605
|
+
*/
|
|
606
|
+
skippedOversizeClosures: number;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Stable identity of an enumerated mutant: file + exact span + operator + REPLACEMENT.
|
|
610
|
+
* The replacement is part of the identity because one operator emits several mutants at the same
|
|
611
|
+
* span (see {@link EnumeratedMutant.replacement}); dropping it silently merges distinct mutants.
|
|
612
|
+
*/
|
|
613
|
+
declare const mutantKey: (m: EnumeratedMutant) => string;
|
|
614
|
+
/**
|
|
615
|
+
* Line-first round-robin sample of `cap` mutants. Line order and the within-line order are both
|
|
616
|
+
* seeded-deterministic; round k takes each line's (k+1)-th mutant while budget remains, so every
|
|
617
|
+
* mutant-bearing line is probed once before any line is probed twice.
|
|
618
|
+
*/
|
|
619
|
+
declare function lineFirstSample(mutants: readonly EnumeratedMutant[], cap: number, seed: string): SamplePlan;
|
|
620
|
+
/**
|
|
621
|
+
* Express the picks as Stryker `mutate` entries, column-precise, one per sampled mutant
|
|
622
|
+
* ("src/a.js:5:4-5:12"). Stryker's range columns are the instrumenter's own convention — the
|
|
623
|
+
* locations came from the same package that will re-place the mutants, so a pick selects exactly
|
|
624
|
+
* the mutant it enumerated (asserted by the round-trip unit test).
|
|
625
|
+
*/
|
|
626
|
+
declare function sampleToMutateTargets(plan: SamplePlan, subdir?: string | null): string[];
|
|
627
|
+
|
|
628
|
+
/** Categories this baseline covers — the same six the model is asked for. */
|
|
629
|
+
declare const DETERMINISTIC_CATEGORIES: readonly ["missing-await", "exception-swallow", "argument-order", "off-by-one", "wrong-variable", "wrong-constant"];
|
|
630
|
+
interface DeterministicOptions {
|
|
631
|
+
/** only emit recipes whose span intersects these 1-based lines (the diff scope) */
|
|
632
|
+
changedLines?: ReadonlySet<number>;
|
|
633
|
+
/** cap per category, so one dense file cannot crowd out every other rule */
|
|
634
|
+
maxPerCategory?: number;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* "Similar identifier", made executable rather than left as prose. Two names are confusable when
|
|
638
|
+
* they are close in edit distance but NOT the same name — the mistake being modelled is reaching for
|
|
639
|
+
* the wrong one of two names that look alike (`startIndex` / `startIdx`, `res` / `req`).
|
|
640
|
+
* Very short names are excluded: at length 2 every name is within distance 2 of every other, which
|
|
641
|
+
* would make the rule fire everywhere and mean nothing.
|
|
642
|
+
*/
|
|
643
|
+
declare function isConfusableIdentifier(a: string, b: string): boolean;
|
|
644
|
+
/**
|
|
645
|
+
* "Plausible constant", made executable. A replacement is plausible when it is the kind of value
|
|
646
|
+
* someone would actually type by mistake: an adjacent number, a unit confusion, an off-by-a-power,
|
|
647
|
+
* or the opposite boolean. Arbitrary values are not — replacing 7 with 913 tests nothing a real
|
|
648
|
+
* mistake would produce.
|
|
649
|
+
*/
|
|
650
|
+
declare function plausibleConstants(literal: string): string[];
|
|
651
|
+
/**
|
|
652
|
+
* Generate the deterministic baseline's recipes for one file.
|
|
653
|
+
*
|
|
654
|
+
* Returns recipes in the same shape the validated LLM proposals take, so both arms can be run
|
|
655
|
+
* through one pipeline and compared on identical terms.
|
|
656
|
+
*/
|
|
657
|
+
declare function deterministicRecipes(file: string, source: string, opts?: DeterministicOptions): MutationRecipe[];
|
|
658
|
+
/** Per-category tally, so a run can report which rules actually had opportunity in a corpus. */
|
|
659
|
+
declare function categoryCounts(recipes: readonly MutationRecipe[]): Record<string, number>;
|
|
660
|
+
|
|
661
|
+
declare const PRODUCTION_OPERATOR_VERSION = "production-operators-v2";
|
|
662
|
+
declare const PRODUCTION_OPERATOR_CATEGORIES: readonly ["statement-deletion", "return-deletion", "control-flow-deletion", "argument-omission", "argument-order", "argument-replacement", "identifier-replacement", "property-substitution", "assignment-operator", "assignment-rhs", "missing-await", "nullish-fallback", "optional-chain-removal", "call-chain-omission", "parameter-default-removal", "class-field-initializer-removal"];
|
|
663
|
+
interface ProductionOperatorOptions {
|
|
664
|
+
changedLines?: ReadonlySet<number>;
|
|
665
|
+
maxPerCategory?: number;
|
|
666
|
+
/** Include the frozen six-family control inventory. Default true. */
|
|
667
|
+
includeControl?: boolean;
|
|
668
|
+
}
|
|
669
|
+
declare function productionRecipes(file: string, source: string, options?: ProductionOperatorOptions): MutationRecipe[];
|
|
670
|
+
declare function productionCategoryCounts(recipes: readonly MutationRecipe[]): Record<string, number>;
|
|
671
|
+
|
|
672
|
+
/** One operator, and the transformation it performs on source text. */
|
|
673
|
+
interface ClassicOperatorEntry {
|
|
674
|
+
/** the operator's identity in the pass that runs it */
|
|
675
|
+
id: string;
|
|
676
|
+
/** what it rewrites - a statement about the EDIT, checkable against a proposal */
|
|
677
|
+
rewrite: string;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Stryker's built-in mutators, as this product runs them.
|
|
681
|
+
*
|
|
682
|
+
* THE WHOLE SET, because the classic pass writes no `mutator` block and Stryker's default is all of
|
|
683
|
+
* them. Each sentence describes the edit at the level a reader can apply: "replaces a string literal
|
|
684
|
+
* with an empty string" is checkable; "string mutations" is not.
|
|
685
|
+
*/
|
|
686
|
+
declare const STRYKER_BUILTIN_OPERATORS: readonly ClassicOperatorEntry[];
|
|
687
|
+
/**
|
|
688
|
+
* The operators the deterministic pass runs: the frozen control inventory plus the production set.
|
|
689
|
+
*
|
|
690
|
+
* ONE LIST BECAUSE ONE PASS RUNS THEM. `productionRecipes` composes both by default
|
|
691
|
+
* (`includeControl`), so a customer's run either has all of these or none of them, and splitting
|
|
692
|
+
* them here would offer a distinction the prompt could not act on.
|
|
693
|
+
*/
|
|
694
|
+
declare const DETERMINISTIC_PASS_OPERATORS: readonly ClassicOperatorEntry[];
|
|
695
|
+
/**
|
|
696
|
+
* The operator set a run actually ran, for the planting prompt's exclusion block.
|
|
697
|
+
*
|
|
698
|
+
* `deterministic` IS THE POLICY FLAG, NOT AN OPINION. `classicMutation.deterministicMutants.enabled`
|
|
699
|
+
* decides whether the deterministic pass runs at all, and a run that did not run it must not tell
|
|
700
|
+
* the model those rewrites are already covered - the model would then decline to write a bug nobody
|
|
701
|
+
* else is writing. The Stryker built-ins are unconditional because the classic pass is.
|
|
702
|
+
*/
|
|
703
|
+
declare function classicOperatorInventory(input: {
|
|
704
|
+
deterministic: boolean;
|
|
705
|
+
}): ClassicOperatorEntry[];
|
|
706
|
+
/**
|
|
707
|
+
* The category names the deterministic pass can emit, as its own modules declare them.
|
|
708
|
+
*
|
|
709
|
+
* Exported so the drift test can compare against this inventory without importing two constants and
|
|
710
|
+
* re-deriving the union at the assertion site.
|
|
711
|
+
*/
|
|
712
|
+
declare const DETERMINISTIC_PASS_CATEGORIES: readonly string[];
|
|
713
|
+
|
|
714
|
+
export { type ApplyResult, type Binding, type BindingKind, type ClassicOperatorEntry, type Cluster, type ClusterInput, type ClusterResult, DESCRIBE_FNS, DETERMINISTIC_CATEGORIES, DETERMINISTIC_PASS_CATEGORIES, DETERMINISTIC_PASS_OPERATORS, type DeterministicOptions, type EnclosingFunction, type EntryPoint, type EnumeratedMutant, type ErrorHandlerScan, type ExportSurface, type FileHandlerScan, type HandlerScanInput, type HandlerSpan, type MutationRecipe, PRODUCTION_OPERATOR_CATEGORIES, PRODUCTION_OPERATOR_VERSION, type ParameterInfo, type ParsedSource, type ProductionOperatorOptions, type Reachability, type ReachabilityAnalysis, SAMPLING_ALGORITHM, STRYKER_BUILTIN_OPERATORS, type SamplePlan, type ScopeTable, TEST_FNS, type TautologyResult, type TautologyScanInput, analyzeReachability, applyRecipe, buildScopes, categoryCounts, classicOperatorInventory, clusterMutants, collectTypeContext, commentInNodeMatches, deterministicRecipes, enclosingNodes, escapeMutatePath, forcedHandlerRecipes, isConfusableIdentifier, isParameterized, isReassigned, lineFirstSample, lineKey, lineOf, moduleExportSurface, mutantKey, namesInScopeAt, offsetOf, parseSource, plausibleConstants, productionCategoryCounts, productionRecipes, recipeFromOffsets, recipeFromSpan, recipeId, sha256 as recipeSha256, resolveBinding, resolveThroughAliases, sampleToMutateTargets, scanErrorHandlers, scanFileHandlers, scanTautologies, strictOffsetOf, stringArg, testCallName, tryRecipeFromSpan };
|