@abloh/core 0.1.0
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/LICENSE +6 -0
- package/dist/index.d.ts +2772 -0
- package/dist/index.js +4288 -0
- package/package.json +32 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2772 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE single source of truth for per-runner capability facts.
|
|
3
|
+
*
|
|
4
|
+
* Seven sites used to re-enumerate these facts independently (engine layer0Capable, cli
|
|
5
|
+
* engineRunner, corpus DRIVABLE, the api draft provider matrix, the action handoff normalizers,
|
|
6
|
+
* the conformance fixture roster) and each stale copy surfaced as a run-time failure during
|
|
7
|
+
* capability work. Consumers must either import these views or — where they cannot import
|
|
8
|
+
* TypeScript (the composite-action .mjs / workflow-template jq) — be pinned to this table by a
|
|
9
|
+
* text-sync test. Never re-type a capability list by hand.
|
|
10
|
+
*
|
|
11
|
+
* Changing a row here is a CAPABILITY decision, not bookkeeping: Layer 0 admission additionally
|
|
12
|
+
* requires the Tier B conformance fixture demanded by scripts/diffCoverageAxis-conformance.mts, and the
|
|
13
|
+
* bun row is deliberately disabled (measured false covered line; see engine coverage.ts).
|
|
14
|
+
*/
|
|
15
|
+
type RunnerEcosystem = "js" | "python";
|
|
16
|
+
type MutationMode = "stryker-plugin" | "whole-suite-command" | "cosmic-ray";
|
|
17
|
+
type RawCoverageFormatName = "istanbul-coverage-final-v1" | "lcov-v1" | "coverage-py-json-v1";
|
|
18
|
+
interface RunnerCapability {
|
|
19
|
+
readonly runner: string;
|
|
20
|
+
readonly ecosystem: RunnerEcosystem;
|
|
21
|
+
/** Provider names admissible for this runner inside a signed artifact (historical included). */
|
|
22
|
+
readonly layer0Providers: readonly string[];
|
|
23
|
+
/** May abloh PRODUCE new Layer 0 evidence with this runner today. */
|
|
24
|
+
readonly layer0Enabled: boolean;
|
|
25
|
+
readonly mutation: MutationMode;
|
|
26
|
+
/** Can the engine drive one test at a time (ProofRunner surface). */
|
|
27
|
+
readonly perTestDrivable: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Raw evidence sidecar format each provider writes; consumers derive format checks from this. */
|
|
30
|
+
declare const PROVIDER_RAW_FORMAT: Readonly<Record<string, RawCoverageFormatName>>;
|
|
31
|
+
declare const RUNNER_CAPABILITIES: readonly RunnerCapability[];
|
|
32
|
+
declare function capability(runner: string): RunnerCapability;
|
|
33
|
+
declare function knownRunner(runner: string): boolean;
|
|
34
|
+
declare function jsRunners(): string[];
|
|
35
|
+
declare function layer0EnabledRunners(ecosystem?: RunnerEcosystem): string[];
|
|
36
|
+
declare function strykerPluginRunners(): string[];
|
|
37
|
+
declare function recognizedProvidersFor(runner: string): ReadonlySet<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Closed vocabulary of `results.target` keys in the SIGNED artifact (attest-results/v2).
|
|
40
|
+
*
|
|
41
|
+
* Lockstep gates: packages/core/src/schema.ts (the type) and apps/action/prepare-upload.mjs (the
|
|
42
|
+
* structural allowlist) must carry exactly these keys — a key present in one and not the other
|
|
43
|
+
* silently forks the uploaded evidence from the digest that binds the raw artifact. Pinned by the
|
|
44
|
+
* target-disclosure sync test.
|
|
45
|
+
*
|
|
46
|
+
* The CONTROL-PLANE handoff target is a narrower FROZEN shape (see HANDOFF_TARGET_KEYS): the
|
|
47
|
+
* reusable-workflow template's jq produces it and apps/api/src/ci-handoff.ts exactKeys-rejects
|
|
48
|
+
* anything else. Widening the handoff is a versioned API change, never a side effect of adding
|
|
49
|
+
* artifact disclosure.
|
|
50
|
+
*/
|
|
51
|
+
declare const TARGET_ARTIFACT_KEYS: readonly ["repo", "baseSha", "sha", "runner", "directory", "targetSelection"];
|
|
52
|
+
/** Frozen control-plane handoff target keys — widening requires a versioned API change. */
|
|
53
|
+
declare const HANDOFF_TARGET_KEYS: readonly ["baseSha", "sha", "runner"];
|
|
54
|
+
/**
|
|
55
|
+
* The exact top-level evidence keys a hosted-handoff body carries, per handoff SHAPE version.
|
|
56
|
+
* The server selects the required shape from the AUTHENTICATED reusable-workflow SHA (each
|
|
57
|
+
* workflow release pins one CLI build, so SHA ⟹ producer ⟹ shape). Every copy of a shape —
|
|
58
|
+
* server exactKeys, the workflow jq projection, the in-runner validator — is sync-test-pinned
|
|
59
|
+
* to THIS list so a partial widening cannot ship green and 400 in production.
|
|
60
|
+
*/
|
|
61
|
+
declare const HANDOFF_EVIDENCE_KEYS_V1: readonly ["schema", "engine", "target", "scope", "diffCoverage", "rawCoverageDigest", "rawCoverageFormat", "mutationExecution", "mutationScope", "tier", "mutantsPlanned", "mutantsRun", "counts", "scores", "floor", "gate", "baseline", "findingCount", "findings", "policy", "rationalesDigest", "rawReportDigest", "skipBaseline"];
|
|
62
|
+
/**
|
|
63
|
+
* The widened WS3 shape: + evidenceProfile (the worst-of compat signal — previously unreachable
|
|
64
|
+
* on any CI path), + packages (per-package evidence rows), + mutantRoster (bounded per-mutant
|
|
65
|
+
* (file,status) — the server-side basis for per-package mutation verification). A workflow
|
|
66
|
+
* release that emits this shape maps its SHA to V2 in ci-handoff's code-resident table IN THE
|
|
67
|
+
* SAME COMMIT; the server deploys before the workflow publishes.
|
|
68
|
+
*/
|
|
69
|
+
declare const HANDOFF_EVIDENCE_KEYS_V2: readonly ["schema", "engine", "target", "scope", "diffCoverage", "rawCoverageDigest", "rawCoverageFormat", "mutationExecution", "mutationScope", "tier", "mutantsPlanned", "mutantsRun", "counts", "scores", "floor", "gate", "baseline", "findingCount", "findings", "policy", "rationalesDigest", "rawReportDigest", "skipBaseline", "evidenceProfile", "packages", "mutantRoster", "patchRevert"];
|
|
70
|
+
/** How the measured directory was chosen; always recorded so a verifier reads a stated value. */
|
|
71
|
+
declare const TARGET_SELECTION_KINDS: readonly ["declared-policy", "declared-cli", "auto-diff", "auto-diff-multi", "repository-root"];
|
|
72
|
+
type TargetSelectionKind = (typeof TARGET_SELECTION_KINDS)[number];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Normalized result contract `attest-results/v2` (build plan §4.2.1).
|
|
76
|
+
*
|
|
77
|
+
* Two-layer rule: raw engine reports (Stryker's mutation-testing report-schema) are
|
|
78
|
+
* preserved verbatim as evidence; THIS schema is what every downstream consumer reads.
|
|
79
|
+
* `engine-stryker` maps raw → this; future engines implement the same mapping. We never
|
|
80
|
+
* extend or mutate someone else's schema.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
declare const MUTANT_STATUSES: readonly ["killed", "timeout", "survived", "no-coverage", "runtime-error", "build-error", "skipped-by-cap"];
|
|
84
|
+
type MutantStatus = (typeof MUTANT_STATUSES)[number];
|
|
85
|
+
type Counts = Record<MutantStatus, number>;
|
|
86
|
+
declare function emptyCounts(): Counts;
|
|
87
|
+
interface NormalizedMutant {
|
|
88
|
+
/**
|
|
89
|
+
* `<file>:<line>:<col>:<mutator>`, plus a `#N` suffix when several mutants share that prefix.
|
|
90
|
+
*
|
|
91
|
+
* UNIQUE WITHIN A RUN, and nothing more. `#N` is an ordinal over the mutants the engine reported,
|
|
92
|
+
* so which physical mutation owns a given ordinal depends on the report — and on which siblings
|
|
93
|
+
* were sampled into this run at all. Never key anything durable on it, and never compare ids
|
|
94
|
+
* between two runs: use `mutantIdentity()` below, which is derived from what was actually mutated.
|
|
95
|
+
*/
|
|
96
|
+
id: string;
|
|
97
|
+
file: string;
|
|
98
|
+
startLine: number;
|
|
99
|
+
endLine: number;
|
|
100
|
+
mutator: string;
|
|
101
|
+
replacement: string | null;
|
|
102
|
+
status: MutantStatus;
|
|
103
|
+
/** raw engine test ids that killed it (killed/timeout only) — Stryker emits opaque ids like "1" */
|
|
104
|
+
killedBy: string[];
|
|
105
|
+
/**
|
|
106
|
+
* `killedBy` ids resolved to human test names via the engine report's test dictionary
|
|
107
|
+
* (Stryker `testFiles[*].tests[]`), used for the §4.9.3 flaky-quarantine comparison — the
|
|
108
|
+
* flaky set is keyed by test NAME, not by raw id. Optional: present only when the engine
|
|
109
|
+
* carried a resolvable dictionary; absent/short (fewer names than `killedBy`) means the
|
|
110
|
+
* attribution could not be fully resolved, and the quarantine downgrade must NO-OP that
|
|
111
|
+
* mutant rather than compare on a broken mapping.
|
|
112
|
+
*/
|
|
113
|
+
killedByNames?: string[];
|
|
114
|
+
/** number of tests covering the location */
|
|
115
|
+
coveredBy: number;
|
|
116
|
+
/**
|
|
117
|
+
* Which producer pass emitted this mutant. Absent ⇒ treated as `"stryker"` (a classic built-in
|
|
118
|
+
* operator). `"realistic"` = an LLM-shaped mutant materialized through a Stryker carrier;
|
|
119
|
+
* `"error-path"` = a forced changed-handler mutant. Presentational + composition metadata only.
|
|
120
|
+
*/
|
|
121
|
+
origin?: "stryker" | "realistic" | "deterministic" | "error-path";
|
|
122
|
+
/**
|
|
123
|
+
* Engine-pass identity — namespaces raw test/mutant ids so classic and carrier passes cannot
|
|
124
|
+
* collide when their reports are ingested into one array. Absent on single-pass historical runs.
|
|
125
|
+
*/
|
|
126
|
+
passId?: string;
|
|
127
|
+
/** exact 1-based columns of the mutated span (multipass remapping/proof needs sub-line precision) */
|
|
128
|
+
startColumn?: number;
|
|
129
|
+
endColumn?: number;
|
|
130
|
+
/**
|
|
131
|
+
* The exact source text this mutant replaced, taken from the engine report's own file source.
|
|
132
|
+
*
|
|
133
|
+
* LOCAL ONLY — it is a source slice, so it never egresses (the API's sanitizers reject it). It
|
|
134
|
+
* exists because reconstructing a mutation span from (line, column) alone is not verifiable: the
|
|
135
|
+
* engine report's column base cannot be confirmed from the report itself, and an off-by-one would
|
|
136
|
+
* mutate the wrong bytes with no drift guard able to fire. Carrying the text lets a consumer
|
|
137
|
+
* LOCATE the span and refuse when it cannot be found unambiguously.
|
|
138
|
+
*/
|
|
139
|
+
originalText?: string;
|
|
140
|
+
/** raw engine ids of the covering tests (the un-collapsed form of `coveredBy`); absent ⇒ unknown */
|
|
141
|
+
coveredByIds?: string[];
|
|
142
|
+
/** covering tests as canonical `<workdir-relative-file>::<fullName>` identities; absent ⇒ unknown */
|
|
143
|
+
coveredByTests?: string[];
|
|
144
|
+
/** killer tests as canonical `<workdir-relative-file>::<fullName>` identities (killed/timeout only) */
|
|
145
|
+
killedByTests?: string[];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* WHAT was mutated, as a string — the identity that survives between runs, unlike `id`.
|
|
149
|
+
*
|
|
150
|
+
* THE INCIDENT. A mutant's `id` ends in a `#N` ordinal assigned in engine-report order, so the same
|
|
151
|
+
* physical mutation is `…:EqualityOperator` in one run and `…:EqualityOperator#2` in the next. The
|
|
152
|
+
* triage verdict cache keyed on that id, so one mutation occupied several cache slots, each answered
|
|
153
|
+
* by its own model call — and a run read whichever slot matched the ordinal it drew that time.
|
|
154
|
+
* Measured on one commit, twice, with zero live calls on either side:
|
|
155
|
+
*
|
|
156
|
+
* nonEmpty.length < columns.length → nonEmpty.length <= columns.length
|
|
157
|
+
* run A: likely-equivalent run B: real-gap
|
|
158
|
+
*
|
|
159
|
+
* A `likely-equivalent` verdict DROPS the survivor from the reported gaps, so identical code reported
|
|
160
|
+
* a different number of gaps, and a real gap was dismissed on an answer computed for a different
|
|
161
|
+
* mutation on the same line. Both runs looked clean and fully cached.
|
|
162
|
+
*
|
|
163
|
+
* WHY THESE FIELDS. The span END is load-bearing, not padding: two `ConditionalExpression` mutants
|
|
164
|
+
* can share file, line, start column AND replacement (both `"true"`) and differ only in how much of
|
|
165
|
+
* the expression they replaced — `a > 0` versus `a > 0 && b < c`. Without `endColumn`/`originalText`
|
|
166
|
+
* those two collapse to one identity, which is the same class of bug pointing the other way.
|
|
167
|
+
*
|
|
168
|
+
* `originalText`, `startColumn` and `endColumn` are optional (absent on @abloh/engine-python and on
|
|
169
|
+
* historical artifacts). Absent fields render empty rather than being skipped, so a run that has them
|
|
170
|
+
* can never collide with one that does not, and an engine without them degrades to exactly the
|
|
171
|
+
* discrimination it has today.
|
|
172
|
+
*
|
|
173
|
+
* ONE definition, exported, because three separate caches need it. Each deriving its own tuple is how
|
|
174
|
+
* they drifted apart in the first place.
|
|
175
|
+
*/
|
|
176
|
+
declare function mutantIdentity(m: NormalizedMutant): string;
|
|
177
|
+
/** One source file's changed-line ranges, valid at the evidence SHA. */
|
|
178
|
+
interface FileScope {
|
|
179
|
+
file: string;
|
|
180
|
+
/** inclusive [start, end] line ranges */
|
|
181
|
+
ranges: Array<[number, number]>;
|
|
182
|
+
lines: number;
|
|
183
|
+
}
|
|
184
|
+
interface EvidenceFloor {
|
|
185
|
+
minMutantsExecuted: boolean;
|
|
186
|
+
maxErrorRate: boolean;
|
|
187
|
+
minSamplingFraction: boolean;
|
|
188
|
+
passed: boolean;
|
|
189
|
+
}
|
|
190
|
+
type DiffCoverageLineState =
|
|
191
|
+
/** at least one starting statement counter on the line is positive */
|
|
192
|
+
"covered"
|
|
193
|
+
/** the line has counters, all zero */
|
|
194
|
+
| "uncovered"
|
|
195
|
+
/** the reported file has no counter for this changed line — counts as zero-touch */
|
|
196
|
+
| "not-instrumented"
|
|
197
|
+
/**
|
|
198
|
+
* The line cannot execute at all: blank, comment-only, or nothing but closing punctuation.
|
|
199
|
+
*
|
|
200
|
+
* Disclosed rather than dropped. Every changed line must appear exactly once — that invariant is
|
|
201
|
+
* what stops a producer quietly omitting the lines it would rather not answer for — so a
|
|
202
|
+
* non-executable line is STATED and then left out of the denominator, instead of vanishing.
|
|
203
|
+
*/
|
|
204
|
+
| "not-executable";
|
|
205
|
+
interface DiffCoverageLine {
|
|
206
|
+
file: string;
|
|
207
|
+
line: number;
|
|
208
|
+
state: DiffCoverageLineState;
|
|
209
|
+
}
|
|
210
|
+
interface DiffCoverageCounts {
|
|
211
|
+
/** changed source lines that CAN execute — the Layer-0 denominator */
|
|
212
|
+
changed: number;
|
|
213
|
+
covered: number;
|
|
214
|
+
uncovered: number;
|
|
215
|
+
notInstrumented: number;
|
|
216
|
+
/** changed lines excluded as non-executable (comment/blank/brace); never part of `changed` */
|
|
217
|
+
notExecutable?: number;
|
|
218
|
+
}
|
|
219
|
+
/** Which runner + coverage provider produced the report. */
|
|
220
|
+
interface CoverageProvider {
|
|
221
|
+
/** the identified test runner, e.g. "jest" | "vitest" | "mocha" | "node-test" | "bun" | "pytest".
|
|
222
|
+
* The authoritative capability set is `layer0Capable` in the engine — never re-enumerate it in a
|
|
223
|
+
* consumer, because a copied list goes stale silently as providers are added. */
|
|
224
|
+
runner: string;
|
|
225
|
+
/** coverage provider, e.g. "v8" | "babel" */
|
|
226
|
+
provider: string;
|
|
227
|
+
/** installed runner version, null when it could not be resolved */
|
|
228
|
+
runnerVersion: string | null;
|
|
229
|
+
/** installed coverage-provider version, distinct from the test runner version */
|
|
230
|
+
providerVersion?: string | null;
|
|
231
|
+
}
|
|
232
|
+
/** Closed identifier for the verbatim Layer-0 sidecar bytes bound by rawCoverageDigest. */
|
|
233
|
+
type RawCoverageFormat = "istanbul-coverage-final-v1" | "lcov-v1" | "coverage-py-json-v1";
|
|
234
|
+
/** Why Layer 0 did not produce a measured pass/fail. */
|
|
235
|
+
type DiffCoverageNotApplicableReason =
|
|
236
|
+
/** the identified runner has no Layer 0 coverage provider (`layer0Capable` is the source of
|
|
237
|
+
* truth; do not restate the supported set here — it drifts as providers are added) */
|
|
238
|
+
"unsupported-runner"
|
|
239
|
+
/** the diff scope is empty — no changed source lines to evaluate */
|
|
240
|
+
| "empty-scope"
|
|
241
|
+
/** changed source files exist, but every scoped line is non-executable */
|
|
242
|
+
| "no-executable-lines"
|
|
243
|
+
/**
|
|
244
|
+
* The change only REMOVED source lines. There is no new-side line to measure and none is
|
|
245
|
+
* invented, but this is emphatically not `empty-scope`: code behaviour changed. Reported
|
|
246
|
+
* distinctly so a deletion-only commit can never be read as "nothing changed".
|
|
247
|
+
*/
|
|
248
|
+
| "deletion-only";
|
|
249
|
+
type DiffCoverageNotRunReason = "baseline-abort" | "pre-mutation-deadline";
|
|
250
|
+
/**
|
|
251
|
+
* Layer-0 result, discriminated on `state`:
|
|
252
|
+
* - `completed`: a measured result — every changed line classified covered/uncovered/not-instrumented.
|
|
253
|
+
* - `cannot-attest`: coverage could not be trusted (missing/malformed report, requested file absent).
|
|
254
|
+
* - `not-applicable`: no measurement was appropriate (runner without a coverage provider, empty
|
|
255
|
+
* scope, or a scope whose every changed line is non-executable — the last carries a provider,
|
|
256
|
+
* counts and lines, so it is a MEASURED zero-denominator result, not an absence of measurement).
|
|
257
|
+
* - `not-run`: the run aborted before Layer 0 (red/strict-flaky baseline).
|
|
258
|
+
*/
|
|
259
|
+
type DiffCoverageResult = {
|
|
260
|
+
state: "completed";
|
|
261
|
+
provider: CoverageProvider;
|
|
262
|
+
counts: DiffCoverageCounts;
|
|
263
|
+
/** one entry per changed line, canonical (file, then line) order */
|
|
264
|
+
lines: DiffCoverageLine[];
|
|
265
|
+
/**
|
|
266
|
+
* ADVISORY structural evidence over the changed scope. NEVER gating: not in the Layer-0 gate,
|
|
267
|
+
* not in the score, not in the evidence floor. Layer 0's claim is "a test executed this line";
|
|
268
|
+
* "this changed function was never invoked" is a strictly stronger statement, and the evidence
|
|
269
|
+
* for it is only as good as the provider's fn/branch tables.
|
|
270
|
+
*
|
|
271
|
+
* `null` — NOT `{total: 0, invoked: 0}` — when the provider carried no such tables (lcov,
|
|
272
|
+
* coverage.py). A zero here would assert "no changed functions", which is a measurement, and
|
|
273
|
+
* nothing was measured. Same absent-vs-zero rule as `filesUnmeasured` below.
|
|
274
|
+
*
|
|
275
|
+
* Absent (undefined) on artifacts written before this field existed.
|
|
276
|
+
*/
|
|
277
|
+
changedFunctions?: {
|
|
278
|
+
total: number;
|
|
279
|
+
invoked: number;
|
|
280
|
+
} | null;
|
|
281
|
+
/** Branch ARMS in the changed scope, keyed by each arm's own start line. Advisory; see above. */
|
|
282
|
+
changedBranches?: {
|
|
283
|
+
total: number;
|
|
284
|
+
taken: number;
|
|
285
|
+
} | null;
|
|
286
|
+
/**
|
|
287
|
+
* How many acquisition executions produced this result. 1 for the normal path. 2 means the
|
|
288
|
+
* first coverage run exited nonzero and was DISCARDED UNREAD; every signed field derives
|
|
289
|
+
* exclusively from the green retry, and `wallMs` spans both attempts. An UNDISCLOSED retry
|
|
290
|
+
* is itself a process false-claim — this field is what makes retry-once honest.
|
|
291
|
+
* Absent (undefined) on artifacts written before retries existed.
|
|
292
|
+
*/
|
|
293
|
+
acquisitionAttempts?: number;
|
|
294
|
+
wallMs: number;
|
|
295
|
+
} | {
|
|
296
|
+
state: "cannot-attest";
|
|
297
|
+
reason: string;
|
|
298
|
+
provider: CoverageProvider | null;
|
|
299
|
+
/** See the completed variant — 2 means the retry ALSO failed; wallMs spans both attempts. */
|
|
300
|
+
acquisitionAttempts?: number;
|
|
301
|
+
wallMs: number;
|
|
302
|
+
} | {
|
|
303
|
+
state: "not-applicable";
|
|
304
|
+
reason: "no-executable-lines";
|
|
305
|
+
/** Provider evidence and the complete line mapping remain available even with a zero denominator. */
|
|
306
|
+
provider: CoverageProvider;
|
|
307
|
+
counts: DiffCoverageCounts;
|
|
308
|
+
lines: DiffCoverageLine[];
|
|
309
|
+
/** See the completed variant. */
|
|
310
|
+
acquisitionAttempts?: number;
|
|
311
|
+
wallMs: number;
|
|
312
|
+
} | {
|
|
313
|
+
state: "not-applicable";
|
|
314
|
+
reason: Exclude<DiffCoverageNotApplicableReason, "no-executable-lines">;
|
|
315
|
+
} | {
|
|
316
|
+
state: "not-run";
|
|
317
|
+
reason: DiffCoverageNotRunReason;
|
|
318
|
+
};
|
|
319
|
+
/** Why Layer 1 (mutation) was skipped after Layer 0. */
|
|
320
|
+
type MutationSkipReason =
|
|
321
|
+
/** policy `shortCircuit:true` and Layer 0 failed */
|
|
322
|
+
"layer-0-failed"
|
|
323
|
+
/** Layer 0 failed and no changed line is covered, so there is nothing worth mutating */
|
|
324
|
+
| "no-covered-changed-lines"
|
|
325
|
+
/** coverage could not be acquired and policy `shortCircuit:true` */
|
|
326
|
+
| "layer-0-unavailable";
|
|
327
|
+
type MutationNotRunReason = "empty-scope" | "baseline-abort" | "pre-mutation-deadline" | "no-runner"
|
|
328
|
+
/** the mutation engine failed to execute; Layer 0 evidence stands, mutation carries none */
|
|
329
|
+
| "engine-error";
|
|
330
|
+
/**
|
|
331
|
+
* How the mutation scope was chosen:
|
|
332
|
+
* - `original` = the full diff scope;
|
|
333
|
+
* - `covered-only` = the covered-changed-line reconstruction (Layer-0 fail, diagnostic run);
|
|
334
|
+
* - `covered-plus-error-handlers` = covered changed lines ∪ changed executable handler lines;
|
|
335
|
+
* - `error-handlers-only` = changed executable handler lines only (Layer-0 short-circuit + forcing).
|
|
336
|
+
*/
|
|
337
|
+
type MutationScopeKind = "original" | "covered-only" | "covered-plus-error-handlers" | "error-handlers-only";
|
|
338
|
+
/**
|
|
339
|
+
* Engine scope → the three-value vocabulary every GitHub surface renders.
|
|
340
|
+
*
|
|
341
|
+
* Three separate copies of this table existed, all keyed on strings that are NOT members of
|
|
342
|
+
* {@link MutationScopeKind} ("covered-lines", "error-handlers"). Only "original" ever matched, so
|
|
343
|
+
* every reduced-scope run fell through to "full" and the renderer's "· covered lines only" note
|
|
344
|
+
* could never appear — a run that mutated only the already-covered subset was presented as having
|
|
345
|
+
* mutated the whole diff. One exported mapping, keyed on the type, so a new member is a compile
|
|
346
|
+
* error rather than a silent overstatement.
|
|
347
|
+
*/
|
|
348
|
+
declare const MUTATION_SCOPE_SURFACE: Record<MutationScopeKind, "full" | "covered-only" | "error-handlers-only">;
|
|
349
|
+
/** How Layer 1 executed relative to Layer 0. */
|
|
350
|
+
type MutationExecution = {
|
|
351
|
+
state: "completed";
|
|
352
|
+
scope: MutationScopeKind;
|
|
353
|
+
} | {
|
|
354
|
+
state: "skipped";
|
|
355
|
+
reason: MutationSkipReason;
|
|
356
|
+
} | {
|
|
357
|
+
state: "not-run";
|
|
358
|
+
reason: MutationNotRunReason;
|
|
359
|
+
};
|
|
360
|
+
interface Scores {
|
|
361
|
+
/** (killed+timeout)/(killed+timeout+survived+no-coverage) */
|
|
362
|
+
rawScore: number | null;
|
|
363
|
+
/** raw denominator minus confirmed-equivalent survivors; null in Tier 0 */
|
|
364
|
+
triagedScore: number | null;
|
|
365
|
+
denominator: number;
|
|
366
|
+
errorCount: number;
|
|
367
|
+
confirmedEquivalent: number;
|
|
368
|
+
/** false until the human-labeled triage eval gate has passed — triaged numbers are unvalidated until then */
|
|
369
|
+
triageValidated: boolean;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* The evidence schedule that produced this score. Pull-request and release scores are separate
|
|
373
|
+
* records at the same target SHA; no field exists for blending them.
|
|
374
|
+
*
|
|
375
|
+
* `diff-only` is deliberately explicit. Automatic impact expansion outside the diff is not part
|
|
376
|
+
* of this version: a release result must not imply that dependency analysis ran when it did not.
|
|
377
|
+
*/
|
|
378
|
+
interface EvidenceProfile {
|
|
379
|
+
kind: "pull-request" | "release";
|
|
380
|
+
scope: "diff-only";
|
|
381
|
+
testExecution: "engine-selected" | "full-suite-per-mutant";
|
|
382
|
+
staticMutants: "policy-controlled" | "included";
|
|
383
|
+
operatorSet: "configured";
|
|
384
|
+
sampling: {
|
|
385
|
+
mode: "uncapped";
|
|
386
|
+
stability: "not-applicable";
|
|
387
|
+
} | {
|
|
388
|
+
mode: "seeded-capped";
|
|
389
|
+
stability: "not-checked" | "verified" | "not-run";
|
|
390
|
+
algorithm: string;
|
|
391
|
+
seed: string;
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* "worst-of-packages": the gate is worst(pooled gate, per-package gates) over `packages[]` —
|
|
395
|
+
* a weak package can no longer hide inside the pooled score. Requires `packages[]`; the
|
|
396
|
+
* server refuses the literal without it and re-derives every per-package quantity itself.
|
|
397
|
+
*/
|
|
398
|
+
scoreAggregation: "standalone" | "worst-of-packages";
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Explicit lifecycle state shared by the optional Layer-1 feature blocks.
|
|
402
|
+
* - `completed` — the feature ran and produced results;
|
|
403
|
+
* - `disabled` — turned off by policy/CLI;
|
|
404
|
+
* - `disabled-by-tier`— an LLM-backed feature forced off because tier 0 permits no model calls;
|
|
405
|
+
* - `unavailable` — prerequisites absent (command runner, missing dictionary, baseline abort…);
|
|
406
|
+
* - `not-run` — mutation itself did not run, so there was nothing to analyze.
|
|
407
|
+
*/
|
|
408
|
+
type FeatureState = "completed" | "disabled" | "disabled-by-tier" | "unavailable" | "not-run";
|
|
409
|
+
/** How completely per-test kill attribution could be resolved from the engine report(s). */
|
|
410
|
+
type AttributionQuality = "complete" | "partial";
|
|
411
|
+
type PerTestFindingKind = "asserts-nothing" | "duplicate-effectiveness" | "tautological-assertion";
|
|
412
|
+
/** Structural per-test finding. The test NAME/FILE stay in the local sidecar; only the identity
|
|
413
|
+
* DIGEST and counts are structural (Tier-1: names are customer-authored free text). */
|
|
414
|
+
interface PerTestFinding {
|
|
415
|
+
/** sha256 of the canonical `<workdir-relative-file>::<fullName>` */
|
|
416
|
+
testIdentityDigest: string;
|
|
417
|
+
kind: PerTestFindingKind;
|
|
418
|
+
coveredCount: number;
|
|
419
|
+
creditedKillCount: number;
|
|
420
|
+
/** identifies the duplicate group (duplicate-effectiveness only) */
|
|
421
|
+
duplicateGroupDigest?: string;
|
|
422
|
+
}
|
|
423
|
+
type PerTestBlock = {
|
|
424
|
+
state: "completed";
|
|
425
|
+
analyzerVersion: string;
|
|
426
|
+
attribution: AttributionQuality;
|
|
427
|
+
findings: PerTestFinding[];
|
|
428
|
+
/** canonical digest over the sorted structural findings */
|
|
429
|
+
findingsDigest: string;
|
|
430
|
+
} | {
|
|
431
|
+
state: Exclude<FeatureState, "completed">;
|
|
432
|
+
reason?: string;
|
|
433
|
+
};
|
|
434
|
+
type HandlerAntiPatternKind = "empty-catch" | "catch-all-abort" | "todo-in-handler";
|
|
435
|
+
interface HandlerFinding {
|
|
436
|
+
file: string;
|
|
437
|
+
startLine: number;
|
|
438
|
+
endLine: number;
|
|
439
|
+
kind: HandlerAntiPatternKind;
|
|
440
|
+
}
|
|
441
|
+
type ErrorHandlersBlock = {
|
|
442
|
+
state: "completed";
|
|
443
|
+
analyzerVersion: string;
|
|
444
|
+
quality: "complete" | "partial";
|
|
445
|
+
changedHandlerCount: number;
|
|
446
|
+
untestedHandlerMutantCount: number;
|
|
447
|
+
antiPatterns: HandlerFinding[];
|
|
448
|
+
/** canonical digest over the source-side handler analysis (bound, not recomputed by the API) */
|
|
449
|
+
analysisDigest: string;
|
|
450
|
+
} | {
|
|
451
|
+
state: Exclude<FeatureState, "completed">;
|
|
452
|
+
reason?: string;
|
|
453
|
+
};
|
|
454
|
+
/** The closed set of realistic-mutant categories the model may propose (shared by the triage
|
|
455
|
+
* generator and the engine validator so they cannot drift). */
|
|
456
|
+
declare const REALISTIC_CATEGORIES: readonly ["missing-await", "wrong-variable", "argument-order", "exception-swallow", "off-by-one", "wrong-constant"];
|
|
457
|
+
type RealisticCategory = (typeof REALISTIC_CATEGORIES)[number];
|
|
458
|
+
/** One LLM-proposed realistic mutant, before machine validation. Source-derived — stays local. */
|
|
459
|
+
interface ProposedMutant {
|
|
460
|
+
/** workdir-relative file (must be in the mutation scope) */
|
|
461
|
+
file: string;
|
|
462
|
+
/** 1-based span coordinates of the region to replace */
|
|
463
|
+
startLine: number;
|
|
464
|
+
startColumn: number;
|
|
465
|
+
endLine: number;
|
|
466
|
+
endColumn: number;
|
|
467
|
+
category: RealisticCategory;
|
|
468
|
+
/** the mutated text for the span */
|
|
469
|
+
replacement: string;
|
|
470
|
+
}
|
|
471
|
+
/** Deterministic production candidates beyond Stryker's built-in inventory. */
|
|
472
|
+
type DeterministicMutantsBlock = {
|
|
473
|
+
state: "completed";
|
|
474
|
+
operatorVersion: string;
|
|
475
|
+
inventoryDigest: string;
|
|
476
|
+
eligible: number;
|
|
477
|
+
planned: number;
|
|
478
|
+
run: number;
|
|
479
|
+
skipped: number;
|
|
480
|
+
byCategory: Record<string, number>;
|
|
481
|
+
/** Structural digest of the local carrier evidence entries. */
|
|
482
|
+
evidenceDigest: string;
|
|
483
|
+
} | {
|
|
484
|
+
state: Exclude<FeatureState, "completed">;
|
|
485
|
+
reason?: string;
|
|
486
|
+
};
|
|
487
|
+
type FixProofVerdict = "proven" | "target-proven" | "rejected" | "not-attempted-cap";
|
|
488
|
+
interface FixProofSummary {
|
|
489
|
+
/** the survivor this candidate targets */
|
|
490
|
+
mutantId: string;
|
|
491
|
+
/** sha256 of the generated candidate test body (the body itself stays in the local sidecar) */
|
|
492
|
+
candidateDigest: string;
|
|
493
|
+
verdict: FixProofVerdict;
|
|
494
|
+
/** free string like target.runner — the schema is engine-agnostic; the proof names its driver */
|
|
495
|
+
runner: string;
|
|
496
|
+
model: string | null;
|
|
497
|
+
promptVersion: string;
|
|
498
|
+
/**
|
|
499
|
+
* Whole-suite outcome with the candidate present. `green` means the candidate was run against
|
|
500
|
+
* the ENTIRE suite and broke nothing — which is what makes a proven fix installable rather than
|
|
501
|
+
* merely mutation-detecting. `regressed` means it passed alone but broke other tests;
|
|
502
|
+
* `not-checked` means the check was disabled. A `proven` verdict requires `green`.
|
|
503
|
+
*/
|
|
504
|
+
suite?: "green" | "regressed" | "pending" | "not-checked";
|
|
505
|
+
}
|
|
506
|
+
type FixLoopBlock = {
|
|
507
|
+
state: "completed";
|
|
508
|
+
proven: number;
|
|
509
|
+
/** Targeted differential proof succeeded; whole-suite confirmation is still pending. */
|
|
510
|
+
targetProven?: number;
|
|
511
|
+
rejected: number;
|
|
512
|
+
omitted: number;
|
|
513
|
+
summaries: FixProofSummary[];
|
|
514
|
+
/** sha256 of the local attest-fix-proofs.json sidecar */
|
|
515
|
+
proofsDigest: string;
|
|
516
|
+
} | {
|
|
517
|
+
state: Exclude<FeatureState, "completed">;
|
|
518
|
+
reason?: string;
|
|
519
|
+
};
|
|
520
|
+
/**
|
|
521
|
+
* Classic-mutant budget sampling disclosure (policy `mutation.maxMutantsPerRun`). Present exactly
|
|
522
|
+
* when a cap was configured; a run without this block executed every eligible mutant. eligible vs
|
|
523
|
+
* sampled is the honest "how much of the plan ran" reading — mutantsPlanned/mutantsRun continue to
|
|
524
|
+
* describe the EXECUTED report (the certificate API recomputes those from mutant records and must
|
|
525
|
+
* keep matching), so a capped run is disclosed here rather than by redefining those counters.
|
|
526
|
+
*/
|
|
527
|
+
interface ClassicSamplingBlock {
|
|
528
|
+
/** sampling algorithm identifier, e.g. "line-first-rr-v1" */
|
|
529
|
+
algorithm: string;
|
|
530
|
+
/** run seed the plan derives from (replayable via --seed) */
|
|
531
|
+
seed: string;
|
|
532
|
+
cap: number;
|
|
533
|
+
/** every mutant the changed ranges allow */
|
|
534
|
+
eligible: number;
|
|
535
|
+
/** picked to run (== eligible when the diff fits under the cap) */
|
|
536
|
+
sampled: number;
|
|
537
|
+
/** changed lines bearing >=1 eligible mutant */
|
|
538
|
+
linesEligible: number;
|
|
539
|
+
/** lines with >=1 sampled mutant — breadth, the metric line-first maximizes */
|
|
540
|
+
linesProbed: number;
|
|
541
|
+
}
|
|
542
|
+
interface AttestResultsV2 {
|
|
543
|
+
schema: "attest-results/v2";
|
|
544
|
+
engine: {
|
|
545
|
+
name: string;
|
|
546
|
+
version: string;
|
|
547
|
+
/**
|
|
548
|
+
* Whether we patched the engine's vitest runner for stryker-js#6073, and why not when we did not.
|
|
549
|
+
*
|
|
550
|
+
* EVIDENCE ABOUT THE VERDICTS, not telemetry. Unpatched, a mutant run that executed zero tests
|
|
551
|
+
* is recorded by Stryker as SURVIVED — so an unpatched run's survivor count may contain mutants
|
|
552
|
+
* that never activated, and a reader must be able to tell the two situations apart. Absent when
|
|
553
|
+
* the runner is not vitest.
|
|
554
|
+
*
|
|
555
|
+
* LOCAL field: `FINDING_ALLOWED_FIELDS` and the summary allowlists do not carry it, so it stays
|
|
556
|
+
* on the customer's disk like `replacement` and the mutant columns.
|
|
557
|
+
*/
|
|
558
|
+
runnerPatch?: {
|
|
559
|
+
state: "applied" | "already-applied" | "skipped";
|
|
560
|
+
reason?: string;
|
|
561
|
+
};
|
|
562
|
+
/** Exact digest of the engine-owned Stryker package trees and executable entry points. */
|
|
563
|
+
toolchainDigest?: string;
|
|
564
|
+
};
|
|
565
|
+
target: {
|
|
566
|
+
repo: string;
|
|
567
|
+
/** Exact merge-base commit used to derive the changed-line scope. Absent only on historical artifacts. */
|
|
568
|
+
baseSha?: string;
|
|
569
|
+
/** Exact measured HEAD commit. Retained as `sha` for control-plane compatibility. */
|
|
570
|
+
sha: string;
|
|
571
|
+
runner: string;
|
|
572
|
+
/**
|
|
573
|
+
* Repo-relative directory the run measured, null for the repository root. Always emitted by
|
|
574
|
+
* the current producer (absent only on historical artifacts) so a verifier reads a stated
|
|
575
|
+
* value, never interprets an absence. Key set is pinned by TARGET_ARTIFACT_KEYS in
|
|
576
|
+
* capability-registry.ts; the control-plane handoff target stays frozen at
|
|
577
|
+
* HANDOFF_TARGET_KEYS and never carries these fields.
|
|
578
|
+
*/
|
|
579
|
+
directory?: string | null;
|
|
580
|
+
/** How `directory` was chosen. Closed vocabulary TARGET_SELECTION_KINDS. */
|
|
581
|
+
targetSelection?: TargetSelectionKind;
|
|
582
|
+
};
|
|
583
|
+
/** Explicit PR/release schedule; absent only on historical artifacts. */
|
|
584
|
+
evidenceProfile?: EvidenceProfile;
|
|
585
|
+
tier: 0 | 1 | 2;
|
|
586
|
+
scope: FileScope[];
|
|
587
|
+
/** Layer 0 — diff-coverage gate result. Always populated by the current producer. */
|
|
588
|
+
diffCoverage: DiffCoverageResult;
|
|
589
|
+
/** how Layer 1 (mutation) executed relative to Layer 0 */
|
|
590
|
+
mutationExecution: MutationExecution;
|
|
591
|
+
/** the scope actually handed to mutation — equals `scope` for a full run, the covered-only
|
|
592
|
+
* reconstruction for a reduced run, and `[]` when mutation was skipped or not run. */
|
|
593
|
+
mutationScope: FileScope[];
|
|
594
|
+
mutantsPlanned: number;
|
|
595
|
+
mutantsRun: number;
|
|
596
|
+
counts: Counts;
|
|
597
|
+
scores: Scores;
|
|
598
|
+
/** evidence floor — null when mutation did not complete (skipped / not-run) */
|
|
599
|
+
floor: EvidenceFloor | null;
|
|
600
|
+
mutants: NormalizedMutant[];
|
|
601
|
+
wallMs: number;
|
|
602
|
+
/**
|
|
603
|
+
* How long the mutation phase took, on its own.
|
|
604
|
+
*
|
|
605
|
+
* `wallMs` above is the whole run and cannot answer "what did mutation cost me" — it also
|
|
606
|
+
* contains scope detection, the baseline, coverage acquisition, triage and every extension.
|
|
607
|
+
* Mutation is the phase that dominates a run's bill and the one a repository owner reduces
|
|
608
|
+
* scope to control, so it is measured separately. Optional: artifacts written before this
|
|
609
|
+
* existed have none, and the reader shows no duration rather than a zero.
|
|
610
|
+
*/
|
|
611
|
+
mutationWallMs?: number;
|
|
612
|
+
/** How long survivor triage took. Null when no classifier ran (tier 0, or nothing survived). */
|
|
613
|
+
triageWallMs?: number | null;
|
|
614
|
+
/** Extension 2 — per-test verdicts (advisory) */
|
|
615
|
+
perTest?: PerTestBlock;
|
|
616
|
+
/** Extension 5 — changed error-handler analysis + anti-patterns */
|
|
617
|
+
errorHandlers?: ErrorHandlersBlock;
|
|
618
|
+
/** Production-candidate deterministic operators; disabled until benchmark-promoted. */
|
|
619
|
+
deterministicMutants?: DeterministicMutantsBlock;
|
|
620
|
+
/** Extension 3 — candidate killing-test fix-loop proof summaries */
|
|
621
|
+
fixLoop?: FixLoopBlock;
|
|
622
|
+
/** classic-mutant budget sampling disclosure — present iff policy set mutation.maxMutantsPerRun */
|
|
623
|
+
classicSampling?: ClassicSamplingBlock;
|
|
624
|
+
/**
|
|
625
|
+
* Per-package evidence rows for multi-package runs — COUNTS AND STATES ONLY, no line slices.
|
|
626
|
+
* The server re-derives each row's Layer-0 slice from the global bijection-validated `lines`
|
|
627
|
+
* by directory prefix, and each row's mutation quantities from `mutantRoster`, then compares
|
|
628
|
+
* claimed vs derived; a disagreement refuses the upload. Rows are disclosed on abort paths too
|
|
629
|
+
* (state + reason + reached stage, no counts). Absent (undefined) on single-target artifacts
|
|
630
|
+
* and on artifacts written before this field existed.
|
|
631
|
+
*/
|
|
632
|
+
packages?: PackageEvidence[];
|
|
633
|
+
/**
|
|
634
|
+
* Bounded per-mutant roster — the server-side basis for per-package mutation verification.
|
|
635
|
+
* Egressing (file, status) pairs is a DELIBERATE carve-out of the mutants-stay-customer-side
|
|
636
|
+
* rule (Kenneth, 2026-08-04): without it, per-package killed counts are producer assertions a
|
|
637
|
+
* forger can shuffle between packages while every total still reconciles. File paths follow
|
|
638
|
+
* the scope canonicalization rules; entries are capped at MAX_MUTANT_ROSTER with mandatory
|
|
639
|
+
* truncation disclosure. Present iff `packages[]` is present.
|
|
640
|
+
*/
|
|
641
|
+
mutantRoster?: MutantRosterEntry[];
|
|
642
|
+
}
|
|
643
|
+
interface MutantRosterEntry {
|
|
644
|
+
/** Repo-relative path, canonicalized like scope files; prefixed with the package directory. */
|
|
645
|
+
file: string;
|
|
646
|
+
/** The pooled counts bucket this mutant landed in — same closed set, drift-proof. */
|
|
647
|
+
status: MutantStatus;
|
|
648
|
+
}
|
|
649
|
+
/** Hard cap on roster entries; a run planning more discloses truncation, never silently drops. */
|
|
650
|
+
declare const MAX_MUTANT_ROSTER = 20000;
|
|
651
|
+
/** One measured (or honestly-unmeasured) package in a multi-package run. */
|
|
652
|
+
interface PackageEvidence {
|
|
653
|
+
/** Workspace-relative package directory — normalized, unique, never nested under another row. */
|
|
654
|
+
directory: string;
|
|
655
|
+
/** This package's test runner (rows may differ under mixed-runner runs). */
|
|
656
|
+
runner: string;
|
|
657
|
+
diffCoverage: {
|
|
658
|
+
state: "completed" | "cannot-attest" | "not-applicable" | "not-run";
|
|
659
|
+
reason?: string;
|
|
660
|
+
/** Present iff state is "completed". */
|
|
661
|
+
counts?: DiffCoverageCounts;
|
|
662
|
+
provider?: CoverageProvider;
|
|
663
|
+
wallMs?: number;
|
|
664
|
+
acquisitionAttempts?: number;
|
|
665
|
+
};
|
|
666
|
+
mutation: {
|
|
667
|
+
state: "completed" | "cannot-attest" | "not-applicable" | "not-run";
|
|
668
|
+
reason?: string;
|
|
669
|
+
mutantsPlanned: number;
|
|
670
|
+
mutantsRun: number;
|
|
671
|
+
/** Present iff state is "completed"; derived server-side from `mutantRoster`. */
|
|
672
|
+
counts?: Counts;
|
|
673
|
+
/** Survivors confirmed equivalent by validated triage, attributable from findings. */
|
|
674
|
+
confirmedEquivalent?: number;
|
|
675
|
+
};
|
|
676
|
+
/** Per-package baseline identity disclosure (command runners have no per-test identities). */
|
|
677
|
+
baseline?: {
|
|
678
|
+
testCount: number;
|
|
679
|
+
testIdentityCount: number;
|
|
680
|
+
ambiguousIdentityCount: number;
|
|
681
|
+
};
|
|
682
|
+
/** sha256 of this package's environment contract (the contract itself never egresses). */
|
|
683
|
+
environmentContractDigest?: string;
|
|
684
|
+
/** Furthest stage this package reached before a run-level abort (abort artifacts only). */
|
|
685
|
+
reachedStage?: "detected" | "baselined" | "diffCoverageAxis" | "mutation";
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* One mapping from a layer's state to the execution axis every surface renders.
|
|
690
|
+
*
|
|
691
|
+
* There were three copies — `apps/cli/src/report-comment.ts`, `apps/api/src/github-delivery.ts`,
|
|
692
|
+
* `apps/api/src/v1-live.ts` — and they had already drifted. The Layer-0 mapper failed CLOSED
|
|
693
|
+
* (an unrecognised state became `unavailable`, so the row read "not measured"); the Layer-1 mapper
|
|
694
|
+
* failed OPEN (`return "completed"`), so a malformed or truncated artifact rendered
|
|
695
|
+
*
|
|
696
|
+
* | Mutation score (mechanically verified) | — 0 bugs planted, 0 caught, 0 survived |
|
|
697
|
+
*
|
|
698
|
+
* which is the fabricated all-clear `buildCheckRunSummary`'s own docstring forbids. Nothing about
|
|
699
|
+
* that run had been measured; the default arm simply said it had.
|
|
700
|
+
*
|
|
701
|
+
* This is the same device, and the same reason, as `MUTATION_SCOPE_SURFACE` in `schema.ts` — whose
|
|
702
|
+
* comment records that three copies of THAT table existed, keyed on strings that were not members
|
|
703
|
+
* of the union, so only one key ever matched and every reduced-scope run was presented as a
|
|
704
|
+
* full-diff run. Keying on the union makes a new state a compile error instead of a silent
|
|
705
|
+
* overstatement.
|
|
706
|
+
*/
|
|
707
|
+
|
|
708
|
+
/** how a surface describes whether a layer ran */
|
|
709
|
+
type ExecutionAxis = "completed" | "skipped" | "disabled" | "unavailable" | "not-applicable";
|
|
710
|
+
/**
|
|
711
|
+
* Exhaustive by construction: adding a member to `DiffCoverageResult["state"]` without deciding
|
|
712
|
+
* how it renders is a build error, not a silent fall-through.
|
|
713
|
+
*/
|
|
714
|
+
declare const LAYER0_EXECUTION: Record<DiffCoverageResult["state"], ExecutionAxis>;
|
|
715
|
+
declare const LAYER1_EXECUTION: Record<MutationExecution["state"], ExecutionAxis>;
|
|
716
|
+
/** Layer 0. An unrecognised state is "we cannot tell", which is what the row must say. */
|
|
717
|
+
declare function diffCoverageExecution(state: string | undefined | null): ExecutionAxis;
|
|
718
|
+
/**
|
|
719
|
+
* Layer 1. ABSENT and UNRECOGNISED are different facts and must not share an answer.
|
|
720
|
+
*
|
|
721
|
+
* An artifact with no `mutationExecution` at all predates the block, and those runs mutated the
|
|
722
|
+
* full scope unconditionally — so absence honestly means `completed`. A state we do not recognise
|
|
723
|
+
* means the artifact is malformed or newer than us, and the only truthful answer is that we cannot
|
|
724
|
+
* tell. Collapsing the second into the first is what produced the all-clear above.
|
|
725
|
+
*/
|
|
726
|
+
declare function classicMutationExecution(state: string | undefined | null): ExecutionAxis;
|
|
727
|
+
/**
|
|
728
|
+
* The exhaustiveness guard this repo wrote twice by hand and never shared
|
|
729
|
+
* (`engine-stryker/src/runner.ts`, where an unhandled runner silently produced a vitest command:
|
|
730
|
+
* no error, an empty report, and every result reading "test not found").
|
|
731
|
+
*/
|
|
732
|
+
declare function assertNever(value: never, context: string): never;
|
|
733
|
+
|
|
734
|
+
declare const SOURCE_RE: RegExp;
|
|
735
|
+
/** Python source. Passed via {@link ScopeOptions.sourceRe} rather than widening SOURCE_RE — the
|
|
736
|
+
* JS and Python pipelines run separately and must not accidentally scope each other's files. */
|
|
737
|
+
declare const PYTHON_SOURCE_RE: RegExp;
|
|
738
|
+
/**
|
|
739
|
+
* Python's exclusions, mirroring EXCLUDE_RE's intent under Python's conventions.
|
|
740
|
+
*
|
|
741
|
+
* Python names tests `test_*.py` / `*_test.py` and puts them in `tests/`; packaging output lands
|
|
742
|
+
* in `build/` and `dist/`; virtualenvs (`.venv`, `venv`, `site-packages`) contain other people's
|
|
743
|
+
* code entirely. `conftest.py` is test infrastructure, and `__init__.py` is usually re-exports —
|
|
744
|
+
* but it is NOT excluded, because a package can legitimately hold logic there and excluding it
|
|
745
|
+
* would silently shrink the measured scope.
|
|
746
|
+
*/
|
|
747
|
+
declare const PYTHON_EXCLUDE_RE: RegExp;
|
|
748
|
+
/**
|
|
749
|
+
* Excludes tests, type decls, config, docs, examples, benchmarks, vendored dot-dirs,
|
|
750
|
+
* build output. Generic directory names (tests/, scripts/, build/, tools/, …) match at
|
|
751
|
+
* the PATH START ONLY so nested source dirs like `src/tools/` stay in scope; __tests__,
|
|
752
|
+
* __mocks__, dot-segments, and test-file basename patterns match anywhere. The vitest
|
|
753
|
+
* match is config-file-shaped so `packages/vitest/src/*.ts` source stays in scope.
|
|
754
|
+
*/
|
|
755
|
+
declare const EXCLUDE_RE: RegExp;
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* A `git diff` invocation failed (bad base/head ref, not a git repo, git missing). Distinct
|
|
759
|
+
* from a genuinely empty diff (which parses to zero scopes) so a failed acquisition can never
|
|
760
|
+
* masquerade as "no changed source lines" and yield a vacuous cannot-attest at exit 0.
|
|
761
|
+
*/
|
|
762
|
+
declare class DiffScopeError extends Error {
|
|
763
|
+
readonly cause?: unknown | undefined;
|
|
764
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
765
|
+
}
|
|
766
|
+
interface ScopeResult {
|
|
767
|
+
scopes: FileScope[];
|
|
768
|
+
totalLines: number;
|
|
769
|
+
/** changed files that were filtered out of scope, with the reason — disclosure, not silence */
|
|
770
|
+
excluded: Array<{
|
|
771
|
+
file: string;
|
|
772
|
+
reason: "not-source" | "excluded-pattern";
|
|
773
|
+
}>;
|
|
774
|
+
/**
|
|
775
|
+
* Extension 5 — the unfiltered added-line scope used by structural inspection. For the normal
|
|
776
|
+
* Layer-0 path this is identical to `scopes`; when a specialist caller explicitly asks to drop
|
|
777
|
+
* comments, this remains the complete scope. Blank/comment lines are retained here because
|
|
778
|
+
* omission and handler scanners may need their surrounding structure.
|
|
779
|
+
*/
|
|
780
|
+
inspectionScopes: FileScope[];
|
|
781
|
+
/**
|
|
782
|
+
* Hunks that REMOVED source lines and added none — a pure deletion.
|
|
783
|
+
*
|
|
784
|
+
* These produce no entry in `scopes`, and correctly so: there is no new-side line to measure, and
|
|
785
|
+
* synthesizing one would fabricate coverage for text that no longer exists. But "no new line" is
|
|
786
|
+
* not "no behavioral change". Without this, a deletion-only commit reached the empty-scope path
|
|
787
|
+
* and was reported `not-applicable: empty-scope` — indistinguishable from a commit that changed
|
|
788
|
+
* nothing at all.
|
|
789
|
+
*
|
|
790
|
+
* Recorded so the caller can say which one happened, and route the change to patch-revert
|
|
791
|
+
* mutation (the carrier is the parent commit's version of the hunk) instead of silently
|
|
792
|
+
* attesting to an absence of work. Empty for every diff that adds at least one line.
|
|
793
|
+
*/
|
|
794
|
+
deletions: Array<{
|
|
795
|
+
file: string;
|
|
796
|
+
oldStart: number;
|
|
797
|
+
oldLines: number;
|
|
798
|
+
}>;
|
|
799
|
+
}
|
|
800
|
+
interface ScopeOptions {
|
|
801
|
+
sourceRe?: RegExp;
|
|
802
|
+
excludeRe?: RegExp;
|
|
803
|
+
dropComments?: boolean;
|
|
804
|
+
/**
|
|
805
|
+
* Which side of the diff to report ranges for. Defaults to `"new"` — the lines that exist at HEAD,
|
|
806
|
+
* which is what mutation scoping needs, since you can only mutate code that is there.
|
|
807
|
+
*
|
|
808
|
+
* `"old"` reports the lines the diff REMOVED, numbered in the pre-change file. Corpus mining needs
|
|
809
|
+
* this: to find which commit introduced a bug, you blame the lines the fix deleted or replaced, at
|
|
810
|
+
* the fix's parent. Those lines do not exist at HEAD, so the new side cannot answer it.
|
|
811
|
+
*
|
|
812
|
+
* A fix that only ADDS lines has no old side at all. That is not a parser limitation — it means the
|
|
813
|
+
* bug was an omission, and there is nothing to blame. Such a file yields no ranges, and the caller
|
|
814
|
+
* must record it as a drop rather than silently attributing it to whatever sat nearby.
|
|
815
|
+
*/
|
|
816
|
+
side?: "new" | "old";
|
|
817
|
+
/**
|
|
818
|
+
* Scope against the WORKING TREE instead of the head commit (`abloh run --uncommitted`).
|
|
819
|
+
*
|
|
820
|
+
* The default `base...head` form compares two COMMITS, but Layer 0 coverage and classic mutation
|
|
821
|
+
* read the source out of the working tree. When a developer runs the CLI with uncommitted edits
|
|
822
|
+
* those two disagree: the line numbers come from the committed diff and are applied to files that
|
|
823
|
+
* have since shifted, so a finding can name a line nobody touched. This option moves the scope
|
|
824
|
+
* half onto the same code the measurement half already reads.
|
|
825
|
+
*
|
|
826
|
+
* MERGE-BASE semantics are preserved (see {@link computeScope}): the merge base of `base` and HEAD
|
|
827
|
+
* is resolved first and diffed against the tree, which is `A...working-tree`. Plain two-dot
|
|
828
|
+
* `git diff <base>` would drag every commit landed on the base since divergence back in as
|
|
829
|
+
* reversed changes, in files the author never touched.
|
|
830
|
+
*/
|
|
831
|
+
againstWorkingTree?: boolean;
|
|
832
|
+
}
|
|
833
|
+
/** Parse a `git diff -U0` unified diff into per-file changed-line ranges. See {@link ScopeOptions.side}. */
|
|
834
|
+
declare function parseUnifiedDiff(diffText: string, opts?: ScopeOptions): ScopeResult;
|
|
835
|
+
/**
|
|
836
|
+
* Run `git diff <base>...<head> -U0` in `cwd` and scope it.
|
|
837
|
+
*
|
|
838
|
+
* THREE dots, not two, and the difference is the whole meaning of the scope. `A..B` is "everything
|
|
839
|
+
* that differs between these two commits", which on a branch whose base has moved on includes every
|
|
840
|
+
* commit landed on the base since — as REVERSED changes, in files the author never touched. `A...B`
|
|
841
|
+
* is "what this branch introduces since it diverged", i.e. the diff from the merge base, which is
|
|
842
|
+
* exactly what GitHub shows in a pull request and exactly what its author is answerable for.
|
|
843
|
+
*
|
|
844
|
+
* Found on the first live PR: a branch cut from an older commit scoped 8 files and 18 lines instead
|
|
845
|
+
* of the 2 files and 8 lines it actually changed, and Layer 0 then refused the whole run because
|
|
846
|
+
* one of those unrelated files (a script no test imports) was absent from the coverage report.
|
|
847
|
+
*
|
|
848
|
+
* The two forms are identical whenever the base is an ancestor of the head, which is the common
|
|
849
|
+
* case, so this only ever changes the answer where the old answer was wrong.
|
|
850
|
+
*/
|
|
851
|
+
declare function computeScope(cwd: string, base: string, head?: string, opts?: ScopeOptions & {
|
|
852
|
+
pathspec?: string;
|
|
853
|
+
}): Promise<ScopeResult>;
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Layer 0 — diff-coverage intersection (build plan Layer 0).
|
|
857
|
+
*
|
|
858
|
+
* Pure domain logic: given the authoritative changed-line set (`FileScope[]` from the
|
|
859
|
+
* diff-scoper) and a normalized per-line coverage map (produced by the engine adapter from
|
|
860
|
+
* the runner's Istanbul JSON), classify every changed line as covered / uncovered /
|
|
861
|
+
* not-instrumented. No git, no runner, no I/O — the engine collects coverage; core decides.
|
|
862
|
+
*
|
|
863
|
+
* Only aggregate execution is known: "some test executed this line", never which test.
|
|
864
|
+
*/
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Normalized coverage: file → (line → hit count), keyed by repo-root-relative POSIX path
|
|
868
|
+
* matching `FileScope.file`. A line present with 0 is instrumented-but-unexecuted; a line
|
|
869
|
+
* absent is not-instrumented. A file absent entirely is an acquisition-integrity concern for
|
|
870
|
+
* the caller (→ cannot-attest via {@link missingScopedFiles}); the classifier below only maps
|
|
871
|
+
* whatever it is given and treats a missing file's lines as not-instrumented defensively.
|
|
872
|
+
*/
|
|
873
|
+
type NormalizedCoverage = Map<string, Map<number, number>>;
|
|
874
|
+
/**
|
|
875
|
+
* Scope-of-claim disclosure for every Layer-0 result. Lives here, in the domain that defines the
|
|
876
|
+
* measurement, and is re-exported to the renderers rather than retyped in each — a sentence about
|
|
877
|
+
* Layer 0 copied into a renderer is how "v1 collects coverage for Jest/Vitest only" outlived the
|
|
878
|
+
* surface it described and reached signed certificates.
|
|
879
|
+
*
|
|
880
|
+
* What a `covered` line establishes: at least one test executed it during ONE aggregate coverage run
|
|
881
|
+
* at the measured commit. Three things it does not establish, none of them visible in the number:
|
|
882
|
+
*
|
|
883
|
+
* - WHICH test executed the line — coverage here is aggregate hit counts, with no per-test
|
|
884
|
+
* attribution (see this file's header).
|
|
885
|
+
* - That the line is reached REPEATABLY. Coverage is collected once, so a flaky test that took a
|
|
886
|
+
* different path — an early timeout, a lost race — could leave the same line unexecuted on the
|
|
887
|
+
* next run with no code change in between. Fast PR verification runs one untouched-HEAD
|
|
888
|
+
* baseline; historical full-suite observations and repeated finding proofs identify instability,
|
|
889
|
+
* but neither can turn this one coverage observation into repeatable coverage evidence.
|
|
890
|
+
* - That the executing test ASSERTS anything about the line. That is Layer 1's question, and the
|
|
891
|
+
* reason Layer 0 is a precondition rather than a score.
|
|
892
|
+
*
|
|
893
|
+
* Stating this is the cheap half of the honesty problem. Strengthening the claim would mean repeated
|
|
894
|
+
* or per-test coverage collection — additional full instrumented suite runs — which is a cost
|
|
895
|
+
* decision, not a wording one.
|
|
896
|
+
*/
|
|
897
|
+
declare const LAYER0_SCOPE_DISCLOSURE: string;
|
|
898
|
+
/** One instrumented function or branch arm: the line it starts on, and how often it ran. */
|
|
899
|
+
interface StructuralSite {
|
|
900
|
+
line: number;
|
|
901
|
+
hits: number;
|
|
902
|
+
}
|
|
903
|
+
interface FileStructural {
|
|
904
|
+
/** function declarations, keyed by their DECLARATION line */
|
|
905
|
+
functions: StructuralSite[];
|
|
906
|
+
/** branch ARMS, keyed by each arm's own start line — never the branch point */
|
|
907
|
+
branches: StructuralSite[];
|
|
908
|
+
}
|
|
909
|
+
/** file → structural sites. A NULL StructuralCoverage means the provider carried none. */
|
|
910
|
+
type StructuralCoverage = ReadonlyMap<string, FileStructural>;
|
|
911
|
+
/**
|
|
912
|
+
* Advisory-only structural counts over the CHANGED scope: how many changed functions were invoked,
|
|
913
|
+
* and how many changed branch arms were taken.
|
|
914
|
+
*
|
|
915
|
+
* ADVISORY MEANS ADVISORY. These never enter the Layer-0 gate, the score, or the evidence floor.
|
|
916
|
+
* Layer 0's claim is "a test executed this line"; a function that was never invoked is a strictly
|
|
917
|
+
* stronger statement and the evidence for it is only as good as the provider's fn/branch tables.
|
|
918
|
+
* The change-based-coverage result this is motivated by (changed-branch coverage strongest in 7 of
|
|
919
|
+
* 12 version pairs) is three Java programs with seeded faults — enough for a benchmark arm, not for
|
|
920
|
+
* a blocking gate on JavaScript.
|
|
921
|
+
*
|
|
922
|
+
* Returns NULL when `structural` is null — the provider reported no such tables. Emitting
|
|
923
|
+
* `{invoked: 0, total: 0}` there would read as "no changed functions", which is a measurement, and
|
|
924
|
+
* nothing was measured. Same absent-vs-zero rule the rest of the schema is built on.
|
|
925
|
+
*/
|
|
926
|
+
interface ChangedStructuralCounts {
|
|
927
|
+
functions: {
|
|
928
|
+
total: number;
|
|
929
|
+
invoked: number;
|
|
930
|
+
};
|
|
931
|
+
branches: {
|
|
932
|
+
total: number;
|
|
933
|
+
taken: number;
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
declare function changedStructuralCoverage(scope: FileScope[], structural: StructuralCoverage | null): ChangedStructuralCounts | null;
|
|
937
|
+
/** Upper bound on the number of changed lines this helper will expand — a guard so the exported
|
|
938
|
+
* function itself cannot be driven into an unbounded/never-terminating loop by a huge or
|
|
939
|
+
* unsafe-integer range (callers should also validate, but the helper is safe on its own). */
|
|
940
|
+
declare const MAX_EXPANDED_LINES = 1000000;
|
|
941
|
+
/** A separate cap on the input shape prevents a million duplicate/overlapping ranges from
|
|
942
|
+
* consuming unbounded sort/merge work while expanding to only a handful of unique lines. */
|
|
943
|
+
declare const MAX_SCOPE_RANGES = 100000;
|
|
944
|
+
/** Expand `FileScope[]` ranges into a de-duplicated, canonically ordered (file, line) list. */
|
|
945
|
+
declare function expandScopeLines(scope: FileScope[]): Array<{
|
|
946
|
+
file: string;
|
|
947
|
+
line: number;
|
|
948
|
+
}>;
|
|
949
|
+
/** Files present in the changed-line scope but entirely absent from the coverage map. */
|
|
950
|
+
declare function missingScopedFiles(scope: FileScope[], coverage: NormalizedCoverage): string[];
|
|
951
|
+
/**
|
|
952
|
+
* Classify every changed line against the normalized coverage map. Returns one entry per
|
|
953
|
+
* unique changed line in canonical (file, then line) order, plus the tallied counts.
|
|
954
|
+
*/
|
|
955
|
+
declare function classifyDiffCoverage(scope: FileScope[], coverage: NormalizedCoverage): {
|
|
956
|
+
lines: DiffCoverageLine[];
|
|
957
|
+
counts: DiffCoverageCounts;
|
|
958
|
+
};
|
|
959
|
+
/**
|
|
960
|
+
* Is this line of source non-executable by construction — a comment, a blank, or pure punctuation?
|
|
961
|
+
*
|
|
962
|
+
* Coverage providers do not report such lines, and `classifyDiffCoverage` calls anything it cannot
|
|
963
|
+
* find `not-instrumented`, which counts as a Layer-0 gap. That is the right fail-closed default
|
|
964
|
+
* when the cause is unknown, but a comment is not an unknown: it can never execute, and counting it
|
|
965
|
+
* as untested code makes a diff fail for containing documentation.
|
|
966
|
+
*
|
|
967
|
+
* Measured on a real customer diff: 3 of 8 changed lines were flagged untested, and they were two
|
|
968
|
+
* lines of a JSDoc block and a closing brace. Every diff touches comments and braces, so this was
|
|
969
|
+
* not an edge case — it was the common one.
|
|
970
|
+
*
|
|
971
|
+
* The test is deliberately SYNTACTIC and conservative. `} else {` and `} catch (e) {` carry control
|
|
972
|
+
* flow and are not matched; only lines that are blank, comment-only, or nothing but closing
|
|
973
|
+
* punctuation are. Anything ambiguous stays in the denominator, where fail-closed belongs.
|
|
974
|
+
*/
|
|
975
|
+
declare function isNonExecutableLine(text: string, file?: string, line?: number): boolean;
|
|
976
|
+
/**
|
|
977
|
+
* Re-state changed lines that cannot execute as `not-executable`, returning the full list and how
|
|
978
|
+
* many were marked. Nothing is removed: the control plane requires exactly one entry per changed
|
|
979
|
+
* line, and dropping entries would both break that check and let a producer omit lines silently.
|
|
980
|
+
* countsFromLines then leaves the marked lines out of the denominator.
|
|
981
|
+
*/
|
|
982
|
+
declare function markNonExecutable(lines: DiffCoverageLine[], sourceOf: (file: string) => string[] | null): {
|
|
983
|
+
lines: DiffCoverageLine[];
|
|
984
|
+
marked: number;
|
|
985
|
+
};
|
|
986
|
+
/** Tally a classified line list. `changed` is the total (the Layer-0 denominator). */
|
|
987
|
+
declare function countsFromLines(lines: DiffCoverageLine[]): DiffCoverageCounts;
|
|
988
|
+
/** Changed lines with no covering test (uncovered + not-instrumented) — the Layer-0 gap. */
|
|
989
|
+
declare function uncoveredCount(counts: DiffCoverageCounts): number;
|
|
990
|
+
/**
|
|
991
|
+
* Reconstruct the covered changed lines into merged `FileScope[]` ranges — the reduced scope
|
|
992
|
+
* for a diagnostic Layer-1 run after a Layer-0 failure (mutate only what a test at least runs).
|
|
993
|
+
*/
|
|
994
|
+
declare function coveredLinesToScope(lines: DiffCoverageLine[]): FileScope[];
|
|
995
|
+
/**
|
|
996
|
+
* Validate a claimed classified line list against the authoritative scope (untrusted-input
|
|
997
|
+
* hardening for the control plane, Codex B-series): exactly one entry per expanded scoped
|
|
998
|
+
* line, no duplicates, no out-of-scope entries, and counts consistent with the entries.
|
|
999
|
+
* Throws `Error` on the first violation; returns the recomputed counts on success.
|
|
1000
|
+
*/
|
|
1001
|
+
declare function validateDiffCoverageLines(scope: FileScope[], lines: DiffCoverageLine[]): DiffCoverageCounts;
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Score and gate semantics — versioned, not vibes (build plan §4.3).
|
|
1005
|
+
*
|
|
1006
|
+
* raw = (killed+timeout) / (killed+timeout+survived+no-coverage)
|
|
1007
|
+
* triaged = (killed+timeout) / (that denominator − confirmed-equivalent)
|
|
1008
|
+
* runtime-error/build-error are excluded from the denominator, reported separately.
|
|
1009
|
+
* no-coverage counts AGAINST the score. `unclear` triage counts as a real gap (caller
|
|
1010
|
+
* passes only confirmed-equivalent into confirmedEquivalent).
|
|
1011
|
+
*
|
|
1012
|
+
* The triaged score is REPORTED always and GATED ON only when `triageValidated` — see
|
|
1013
|
+
* {@link evaluateGate}. Subtracting equivalents can only raise the score, so an unvalidated
|
|
1014
|
+
* classifier deciding the gate would let an unchecked model talk a failing change into passing.
|
|
1015
|
+
*/
|
|
1016
|
+
|
|
1017
|
+
declare function tallyCounts(mutants: NormalizedMutant[]): Counts;
|
|
1018
|
+
declare function denominatorOf(counts: Counts): number;
|
|
1019
|
+
declare function errorCountOf(counts: Counts): number;
|
|
1020
|
+
interface ScoreInput {
|
|
1021
|
+
counts: Counts;
|
|
1022
|
+
/** confirmed-equivalent survivors (customer-overridable, not just LLM-claimed) */
|
|
1023
|
+
confirmedEquivalent?: number;
|
|
1024
|
+
/** Tier 0 => triaged score absent */
|
|
1025
|
+
tier?: 0 | 1 | 2;
|
|
1026
|
+
/** has the human-labeled triage eval gate passed? stays false until it has */
|
|
1027
|
+
triageValidated?: boolean;
|
|
1028
|
+
}
|
|
1029
|
+
declare function computeScores({ counts, confirmedEquivalent, tier, triageValidated }: ScoreInput): Scores;
|
|
1030
|
+
interface FloorPolicy {
|
|
1031
|
+
minMutantsExecuted: number;
|
|
1032
|
+
maxErrorRate: number;
|
|
1033
|
+
minSamplingFraction: number;
|
|
1034
|
+
}
|
|
1035
|
+
declare const DEFAULT_FLOOR: FloorPolicy;
|
|
1036
|
+
/** Evidence-completeness floor — a gate precondition (§4.3). Breach ⇒ cannot-attest. */
|
|
1037
|
+
declare function evaluateFloor(counts: Counts, mutantsPlanned: number, mutantsRun: number, policy?: FloorPolicy): EvidenceFloor;
|
|
1038
|
+
type GateStatus = "pass" | "fail" | "cannot-attest";
|
|
1039
|
+
interface GateResult {
|
|
1040
|
+
status: GateStatus;
|
|
1041
|
+
score: number | null;
|
|
1042
|
+
threshold: number;
|
|
1043
|
+
reason: string;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Gate the score of changed code against a policy threshold. Tier 0 gates on raw score.
|
|
1047
|
+
* Advisory mode is the caller's concern (report neutral); this returns the hard verdict.
|
|
1048
|
+
*/
|
|
1049
|
+
declare function evaluateGate(scores: Scores, floor: EvidenceFloor, opts: {
|
|
1050
|
+
threshold: number;
|
|
1051
|
+
tier?: 0 | 1 | 2;
|
|
1052
|
+
}): GateResult;
|
|
1053
|
+
/** Fixed Layer-0 threshold — every changed line must be covered. */
|
|
1054
|
+
declare const DIFF_COVERAGE_THRESHOLD = 100;
|
|
1055
|
+
/**
|
|
1056
|
+
* Layer-0 gate. Returns a verdict only for a measured (`completed`) or `cannot-attest`
|
|
1057
|
+
* result; `not-applicable` / `not-run` have no Layer-0 verdict and return `null` (the
|
|
1058
|
+
* overall gate then follows Layer 1 or the empty-scope path — see {@link combineLayerGates}).
|
|
1059
|
+
*/
|
|
1060
|
+
declare function evaluateDiffCoverageGate(dc: DiffCoverageResult): GateResult | null;
|
|
1061
|
+
type DecisiveLayer = "layer-0" | "layer-1" | "none";
|
|
1062
|
+
interface CombinedGate {
|
|
1063
|
+
overall: GateResult;
|
|
1064
|
+
decisiveLayer: DecisiveLayer;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Combine the Layer-0 and Layer-1 gates into the overall verdict (build plan gate table):
|
|
1068
|
+
* - Layer-0 measured FAILURE is always the overall failure (decisive Layer 0), even when a
|
|
1069
|
+
* diagnostic mutation run still executed on the covered lines.
|
|
1070
|
+
* - Layer-0 acquisition failure (`cannot-attest`) is always overall cannot-attest.
|
|
1071
|
+
* - Layer-0 pass, or `not-applicable: unsupported-runner` (command runner), delegates to the
|
|
1072
|
+
* mutation gate exactly as today (decisive Layer 1).
|
|
1073
|
+
* - `not-applicable: empty-scope` preserves the existing no-scoreable cannot-attest.
|
|
1074
|
+
* Reuses the mutation gate's own reason strings — it never invents a new gate reason.
|
|
1075
|
+
*/
|
|
1076
|
+
declare function combineLayerGates(diffCoverage: DiffCoverageResult, _diffCoverageGate: GateResult | null, mutationGate: GateResult | null, opts: {
|
|
1077
|
+
threshold: number;
|
|
1078
|
+
}): CombinedGate;
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Classic-headroom ceiling — the cheapest falsifier in the benchmark, and it costs nothing.
|
|
1082
|
+
*
|
|
1083
|
+
* THE QUESTION. The Layer-1 extension claims an LLM finds test gaps that classic Stryker operators
|
|
1084
|
+
* miss. Before paying a model to try, there is an arithmetic upper bound on how well it could
|
|
1085
|
+
* possibly do: the fraction of known omissions that classic mutation ALREADY catches is the fraction
|
|
1086
|
+
* no model can improve on. What is left is the entire headroom.
|
|
1087
|
+
*
|
|
1088
|
+
* ceiling = qualified historical omissions classic MISSED / all qualified historical omissions
|
|
1089
|
+
*
|
|
1090
|
+
* If classic already catches nearly everything, the best conceivable model gains almost nothing, and
|
|
1091
|
+
* that is knowable with zero API spend. Run classic mutation on the frozen corpus first, compute
|
|
1092
|
+
* this, and compare it to the minimum useful margin declared in advance. If the ceiling cannot reach
|
|
1093
|
+
* the margin, stop — no model result can rescue it.
|
|
1094
|
+
*
|
|
1095
|
+
* WHY THE BOUND, NOT THE POINT ESTIMATE. With a handful of omissions the point estimate is nearly
|
|
1096
|
+
* meaningless: 3 of 8 is 37.5%, but so is 2 of 8 plus one coin flip. The stop rule therefore uses
|
|
1097
|
+
* the UPPER confidence bound — stop only when even the optimistic end of the interval falls short,
|
|
1098
|
+
* so a small corpus produces "inconclusive" rather than a false stop. A Wilson score interval is
|
|
1099
|
+
* used because the normal approximation is badly wrong at these counts and at rates near 0 or 1,
|
|
1100
|
+
* which is exactly where this measurement lands.
|
|
1101
|
+
*
|
|
1102
|
+
* The consequence is worth stating plainly: a small corpus will usually return `inconclusive`. That
|
|
1103
|
+
* is the honest answer, and it is a finding about the corpus, not a failure of the test.
|
|
1104
|
+
*
|
|
1105
|
+
* ORDER MATTERS. The corpus is frozen before this runs, and classic-detected items are NEVER
|
|
1106
|
+
* replaced after seeing the result. Swapping in fresh items once the ceiling looks bad is choosing
|
|
1107
|
+
* the outcome, which is the whole reason the rule is written down before any data exists.
|
|
1108
|
+
*/
|
|
1109
|
+
/** One preregistered historical omission, and whether classic mutation caught it. */
|
|
1110
|
+
interface QualifiedOmission {
|
|
1111
|
+
/** stable id from the frozen corpus manifest */
|
|
1112
|
+
id: string;
|
|
1113
|
+
/**
|
|
1114
|
+
* Did CLASSIC (non-LLM) mutation detect this omission, under the detection definition in the
|
|
1115
|
+
* plan — in the causal hunk, stable under both suites, and attributable to the removed assertion?
|
|
1116
|
+
*/
|
|
1117
|
+
classicDetected: boolean;
|
|
1118
|
+
}
|
|
1119
|
+
type HeadroomVerdict = "stop" | "proceed" | "inconclusive";
|
|
1120
|
+
interface HeadroomResult {
|
|
1121
|
+
/** qualified omissions in the frozen corpus */
|
|
1122
|
+
total: number;
|
|
1123
|
+
/** how many classic mutation MISSED — the only ones a model could ever claim */
|
|
1124
|
+
classicMissed: number;
|
|
1125
|
+
/** the maximum possible realistic-only rate, as a percentage; null when the corpus is empty */
|
|
1126
|
+
ceilingPct: number | null;
|
|
1127
|
+
/** Wilson score interval on that ceiling, as percentages */
|
|
1128
|
+
lowerPct: number | null;
|
|
1129
|
+
upperPct: number | null;
|
|
1130
|
+
/** confidence used for the interval */
|
|
1131
|
+
confidence: number;
|
|
1132
|
+
/** the pre-declared minimum useful margin this was tested against */
|
|
1133
|
+
marginPct: number;
|
|
1134
|
+
verdict: HeadroomVerdict;
|
|
1135
|
+
reason: string;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Wilson score interval for a binomial proportion. Chosen over the normal approximation because at
|
|
1139
|
+
* these sample sizes, and at rates near 0 or 1, the normal interval can extend below 0 or above 1
|
|
1140
|
+
* and badly understates uncertainty — which would turn "we cannot tell" into a confident stop.
|
|
1141
|
+
*/
|
|
1142
|
+
declare function wilsonInterval(successes: number, n: number, confidence?: number): {
|
|
1143
|
+
lower: number;
|
|
1144
|
+
upper: number;
|
|
1145
|
+
};
|
|
1146
|
+
/**
|
|
1147
|
+
* Compute the ceiling and apply the pre-registered futility rule.
|
|
1148
|
+
*
|
|
1149
|
+
* `marginPct` is the minimum useful margin — the smallest realistic-only rate that would justify
|
|
1150
|
+
* shipping the extension. It MUST be declared before any of this data exists; choosing it afterwards
|
|
1151
|
+
* is choosing the answer.
|
|
1152
|
+
*/
|
|
1153
|
+
declare function classicHeadroom(omissions: readonly QualifiedOmission[], opts: {
|
|
1154
|
+
marginPct: number;
|
|
1155
|
+
confidence?: number;
|
|
1156
|
+
}): HeadroomResult;
|
|
1157
|
+
/**
|
|
1158
|
+
* The rate that matters, and the rate the ceiling above does NOT bound.
|
|
1159
|
+
*
|
|
1160
|
+
* conditional rate = omissions the method caught / omissions CLASSIC MISSED
|
|
1161
|
+
*
|
|
1162
|
+
* The unconditional figure (`caught / all omissions`) is the product of two things:
|
|
1163
|
+
*
|
|
1164
|
+
* P(realistic-only) = P(classic misses) x P(method detects | classic misses)
|
|
1165
|
+
*
|
|
1166
|
+
* Only the SECOND term is a property of the method. The first is a property of the corpus, and this
|
|
1167
|
+
* corpus is deliberately opportunity-enriched — selected to contain async and error-handling code
|
|
1168
|
+
* that mechanical operators cannot reach. Its miss rate is therefore an artifact of selection, not a
|
|
1169
|
+
* fact about real code, and multiplying by it yields a number shaped like product value that is not
|
|
1170
|
+
* measuring the world. So the margin is declared against the CONDITIONAL rate, and the prevalence
|
|
1171
|
+
* needed to turn that into product value must come from a representative sample, never from here.
|
|
1172
|
+
*
|
|
1173
|
+
* The consequence for the ceiling: it can no longer prove futility. A conditional rate can be 100%
|
|
1174
|
+
* however small the headroom is. What headroom governs instead is POWER — with few classic-missed
|
|
1175
|
+
* omissions there are few observations to estimate the conditional rate from, and no margin can be
|
|
1176
|
+
* tested against one observation. {@link conditionalPower} answers that question.
|
|
1177
|
+
*/
|
|
1178
|
+
interface ConditionalPowerResult {
|
|
1179
|
+
/** the informative N: only omissions classic MISSED can test the conditional rate */
|
|
1180
|
+
informativeN: number;
|
|
1181
|
+
marginPct: number;
|
|
1182
|
+
confidence: number;
|
|
1183
|
+
/** smallest detection count whose lower bound clears the margin; null if even a perfect run fails */
|
|
1184
|
+
detectionsNeeded: number | null;
|
|
1185
|
+
/** the lower bound a PERFECT run would produce — the ceiling on what this corpus can demonstrate */
|
|
1186
|
+
bestPossibleLowerPct: number;
|
|
1187
|
+
verdict: "adequate" | "underpowered";
|
|
1188
|
+
reason: string;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Can this corpus test the declared margin on the conditional rate at all?
|
|
1192
|
+
*
|
|
1193
|
+
* Two ways it cannot. If even a perfect result's lower bound falls short of the margin, no outcome
|
|
1194
|
+
* can succeed — the bar is unreachable by construction rather than by any failing of a model. And if
|
|
1195
|
+
* ONLY a perfect result clears it, the experiment has no power against any realistic effect: one
|
|
1196
|
+
* miss, from one flaky item, and a genuinely good method reads as a failure. Both are reported as
|
|
1197
|
+
* underpowered, because both mean the answer was decided by the corpus rather than by the method.
|
|
1198
|
+
*/
|
|
1199
|
+
declare function conditionalPower(informativeN: number, opts: {
|
|
1200
|
+
marginPct: number;
|
|
1201
|
+
confidence?: number;
|
|
1202
|
+
}): ConditionalPowerResult;
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Historical-pair qualification and the perfect-proposer control — Stage A's remaining model-free
|
|
1206
|
+
* checks. Both must pass before any model is called, and both can invalidate the benchmark outright.
|
|
1207
|
+
*
|
|
1208
|
+
* THE CORPUS. Each item is one real bug from a repository's history. Production source is frozen at
|
|
1209
|
+
* the FIXED version. The STRONG suite includes the regression test that shipped with the fix; the
|
|
1210
|
+
* WEAK suite has only that test's bug-specific assertion removed. The omission is then known from
|
|
1211
|
+
* history rather than from somebody's judgement about what looks "plausibly weak".
|
|
1212
|
+
*
|
|
1213
|
+
* WHY QUALIFICATION IS NEEDED. That story can be false in ways that are invisible unless checked.
|
|
1214
|
+
* The bug may already be covered by a second test, so removing the first creates no omission at all.
|
|
1215
|
+
* The regression test may be flaky. It may fail for an unrelated reason — a timeout, a build error,
|
|
1216
|
+
* a shared fixture it happened to set up. Any of those turns an item into noise that still LOOKS
|
|
1217
|
+
* like a valid measurement, and a benchmark built on it measures nothing.
|
|
1218
|
+
*
|
|
1219
|
+
* So every item is run through a four-cell table with the REAL historical fault:
|
|
1220
|
+
*
|
|
1221
|
+
* strong suite weak suite
|
|
1222
|
+
* fixed code pass pass
|
|
1223
|
+
* historical fault FAIL, by the PASS
|
|
1224
|
+
* intended assertion
|
|
1225
|
+
*
|
|
1226
|
+
* Every cell is repeated, and every repeat must agree. The two cells that carry the argument:
|
|
1227
|
+
* - fault/weak must PASS. If the fault still fails under weak, removing that test did not create
|
|
1228
|
+
* the claimed omission, and the item proves nothing.
|
|
1229
|
+
* - fault/strong must fail BY THE INTENDED ASSERTION. A timeout or a build error is not detection;
|
|
1230
|
+
* it is the harness noticing that something broke, which any mutation would achieve.
|
|
1231
|
+
*
|
|
1232
|
+
* THE PERFECT-PROPOSER CONTROL. Qualification checks the corpus. This checks the INSTRUMENT. Inject
|
|
1233
|
+
* the actual historical fault as though it were a proposed mutant and run it through the complete
|
|
1234
|
+
* measurement pipeline. A perfect proposer — one that emits exactly the real bug — must land in
|
|
1235
|
+
* strong-fail/weak-pass. If it does not, the pipeline cannot detect a known bug it was handed, and
|
|
1236
|
+
* no model result measured through it means anything. That is a failure of the assay, not of a
|
|
1237
|
+
* model, and it is strictly more serious than any individual item being rejected.
|
|
1238
|
+
*/
|
|
1239
|
+
/** What one suite run reported. `error` is a harness failure, never evidence about tests. */
|
|
1240
|
+
type SuiteOutcome = "pass" | "fail" | "error";
|
|
1241
|
+
/** Repeated runs of ONE cell. Repetition is what separates a real signal from a flaky test. */
|
|
1242
|
+
interface CellObservation {
|
|
1243
|
+
/** one entry per repeat; all must agree or the cell is unstable */
|
|
1244
|
+
outcomes: readonly SuiteOutcome[];
|
|
1245
|
+
/**
|
|
1246
|
+
* For the fault-under-strong cell only: did the suite fail because of the removed regression
|
|
1247
|
+
* assertion, established by that test's canonical identity or by running it alone? A failure from
|
|
1248
|
+
* a timeout, a build error, or an unrelated flaky test is not detection.
|
|
1249
|
+
*/
|
|
1250
|
+
failedByIntendedAssertion?: boolean;
|
|
1251
|
+
}
|
|
1252
|
+
/** Proof that strong and weak differ by exactly one test hunk and nothing else. */
|
|
1253
|
+
interface PairIsolation {
|
|
1254
|
+
/** digest of every file EXCEPT the differing test hunk, under strong */
|
|
1255
|
+
strongDigestExcludingHunk: string;
|
|
1256
|
+
/** the same digest under weak — must be identical, or the pair differs by more than one thing */
|
|
1257
|
+
weakDigestExcludingHunk: string;
|
|
1258
|
+
/** how many test hunks differ between the two trees; must be exactly 1 */
|
|
1259
|
+
differingTestHunks: number;
|
|
1260
|
+
}
|
|
1261
|
+
interface HistoricalPair {
|
|
1262
|
+
/** stable id from the frozen corpus manifest */
|
|
1263
|
+
id: string;
|
|
1264
|
+
isolation: PairIsolation;
|
|
1265
|
+
fixedUnderStrong: CellObservation;
|
|
1266
|
+
fixedUnderWeak: CellObservation;
|
|
1267
|
+
faultUnderStrong: CellObservation;
|
|
1268
|
+
faultUnderWeak: CellObservation;
|
|
1269
|
+
}
|
|
1270
|
+
type QualificationVerdict = "qualified" | "rejected";
|
|
1271
|
+
interface QualificationResult {
|
|
1272
|
+
id: string;
|
|
1273
|
+
verdict: QualificationVerdict;
|
|
1274
|
+
/** every reason it failed — all of them, so one run of the table fixes everything at once */
|
|
1275
|
+
reasons: string[];
|
|
1276
|
+
}
|
|
1277
|
+
/** The single outcome of a cell, or null when its repeats disagree (i.e. it is flaky). */
|
|
1278
|
+
declare function stableOutcome(cell: CellObservation): SuiteOutcome | null;
|
|
1279
|
+
/** Minimum repeats before a cell's outcome is treated as established rather than a single sample. */
|
|
1280
|
+
declare const MIN_CELL_REPEATS = 3;
|
|
1281
|
+
/**
|
|
1282
|
+
* Qualify one historical pair against the four-cell table.
|
|
1283
|
+
*
|
|
1284
|
+
* Rejection is the safe direction and is never fatal to the benchmark — a rejected item is simply
|
|
1285
|
+
* not used. Accepting a bad item IS fatal, because it enters the denominator and every rate computed
|
|
1286
|
+
* from it is wrong, with nothing downstream able to notice.
|
|
1287
|
+
*/
|
|
1288
|
+
declare function qualifyHistoricalPair(pair: HistoricalPair, opts?: {
|
|
1289
|
+
minRepeats?: number;
|
|
1290
|
+
}): QualificationResult;
|
|
1291
|
+
/** Where a mutant landed in the 2x2 the benchmark actually scores on. */
|
|
1292
|
+
type DetectionCell = "strong-fail-weak-pass" | "strong-fail-weak-fail" | "strong-pass-weak-pass" | "strong-pass-weak-fail";
|
|
1293
|
+
interface OracleObservation {
|
|
1294
|
+
/** the pair this oracle mutant was built from */
|
|
1295
|
+
pairId: string;
|
|
1296
|
+
/** where the injected historical fault actually landed, measured through the FULL pipeline */
|
|
1297
|
+
observed: DetectionCell;
|
|
1298
|
+
/** did the pipeline attribute the strong-suite failure to the intended assertion? */
|
|
1299
|
+
attributedToIntendedAssertion: boolean;
|
|
1300
|
+
/** did the mutant survive every structural gate (carrier encoding, validation) and actually run? */
|
|
1301
|
+
reachedExecution: boolean;
|
|
1302
|
+
}
|
|
1303
|
+
type AssayVerdict = "valid" | "invalid";
|
|
1304
|
+
interface AssayResult {
|
|
1305
|
+
verdict: AssayVerdict;
|
|
1306
|
+
checked: number;
|
|
1307
|
+
failures: Array<{
|
|
1308
|
+
pairId: string;
|
|
1309
|
+
reason: string;
|
|
1310
|
+
}>;
|
|
1311
|
+
reason: string;
|
|
1312
|
+
}
|
|
1313
|
+
/** The only cell a perfect proposer may land in: caught by the strong suite, missed by the weak one. */
|
|
1314
|
+
declare const EXPECTED_ORACLE_CELL: DetectionCell;
|
|
1315
|
+
/**
|
|
1316
|
+
* Run the perfect-proposer control over every qualified pair.
|
|
1317
|
+
*
|
|
1318
|
+
* A model that emitted exactly the real historical bug is the best any proposer could possibly do.
|
|
1319
|
+
* If the pipeline cannot place THAT in the expected cell, it cannot detect a known bug handed to it
|
|
1320
|
+
* on a plate, and every model number measured through it is meaningless. So this returns a verdict
|
|
1321
|
+
* on the ASSAY: `invalid` means stop and fix the instrument, not "this model did badly".
|
|
1322
|
+
*
|
|
1323
|
+
* There is no tolerance threshold. One perfect mutant failing to reach the expected cell is a defect
|
|
1324
|
+
* in the measurement, and a rate would only invite arguing about how much brokenness is acceptable.
|
|
1325
|
+
*/
|
|
1326
|
+
declare function evaluateAssay(observations: readonly OracleObservation[]): AssayResult;
|
|
1327
|
+
/** Convenience: qualify a whole corpus and report what survived. */
|
|
1328
|
+
declare function qualifyCorpus(pairs: readonly HistoricalPair[], opts?: {
|
|
1329
|
+
minRepeats?: number;
|
|
1330
|
+
}): {
|
|
1331
|
+
qualified: QualificationResult[];
|
|
1332
|
+
rejected: QualificationResult[];
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* The frozen experiment manifest, and the gate that stands between it and any model spend.
|
|
1337
|
+
*
|
|
1338
|
+
* WHY THIS IS CODE AND NOT A DOCUMENT. Plan v2 said "no model data before these parameters are
|
|
1339
|
+
* frozen". That is a promise, and a promise is not a preregistration — nothing stops a parameter
|
|
1340
|
+
* being chosen after the first results are in, which is exactly how a null result becomes a positive
|
|
1341
|
+
* one. Every value below is therefore a required field, validated, with the run refusing to start
|
|
1342
|
+
* while any of them is missing or still a placeholder.
|
|
1343
|
+
*
|
|
1344
|
+
* The value this protects is specific. A benchmark's conclusions are only as trustworthy as the
|
|
1345
|
+
* decisions made BEFORE the data existed: the minimum margin that would make the extension worth
|
|
1346
|
+
* shipping, how models are selected, what counts as detection, what happens to failed calls. Each is
|
|
1347
|
+
* a lever that can turn the same measurements into a different answer, and each is invisible in the
|
|
1348
|
+
* final number.
|
|
1349
|
+
*
|
|
1350
|
+
* {@link canStartStageB} is the gate. It refuses while the manifest is unfrozen, while Stage A's
|
|
1351
|
+
* model-free checks have not passed, and while the deterministic baseline has not been run — because
|
|
1352
|
+
* a model result with no baseline cannot distinguish "the LLM is valuable" from "Stryker's operator
|
|
1353
|
+
* set is incomplete and anything would have scored here".
|
|
1354
|
+
*/
|
|
1355
|
+
|
|
1356
|
+
/** A model entered into the benchmark, named exactly — an alias can resolve to a dated version. */
|
|
1357
|
+
interface ManifestModel {
|
|
1358
|
+
/** the identifier sent on the wire, e.g. "gpt-5.6-sol" */
|
|
1359
|
+
id: string;
|
|
1360
|
+
/** transport, so the cost basis of a cell is unambiguous */
|
|
1361
|
+
provider: "hosted" | "anthropic" | "claude-cli";
|
|
1362
|
+
/** effort level where the model exposes one; part of the cell identity, not a footnote */
|
|
1363
|
+
effort?: string;
|
|
1364
|
+
}
|
|
1365
|
+
interface ExperimentManifest {
|
|
1366
|
+
/** manifest schema version, so a later change to these fields is visible in the artifact */
|
|
1367
|
+
schema: "attest-benchmark-manifest/v1";
|
|
1368
|
+
/** ISO date the manifest was frozen, supplied by the caller — never generated at validation time */
|
|
1369
|
+
frozenAt: string;
|
|
1370
|
+
/** digest over the frozen corpus (repos, SHAs, the strong/weak hunks) */
|
|
1371
|
+
corpusHash: string;
|
|
1372
|
+
/** digest over the analysis code that turns observations into the reported number */
|
|
1373
|
+
analysisCodeHash: string;
|
|
1374
|
+
/** digest over the exact prompt text sent to proposers */
|
|
1375
|
+
promptHash: string;
|
|
1376
|
+
/** every model in the experiment, enumerated */
|
|
1377
|
+
models: ManifestModel[];
|
|
1378
|
+
/** how many independent repository clusters the corpus contains */
|
|
1379
|
+
corpusClusters: number;
|
|
1380
|
+
/** how many held-out clusters are reserved, and frozen before Stage B */
|
|
1381
|
+
heldOutClusters: number;
|
|
1382
|
+
/** independent draws per cell */
|
|
1383
|
+
drawsPerCell: number;
|
|
1384
|
+
/**
|
|
1385
|
+
* The smallest realistic-only rate that would justify shipping the extension. THE most consequential
|
|
1386
|
+
* number here: it decides whether any given result is a success, and choosing it after seeing data
|
|
1387
|
+
* is choosing the answer.
|
|
1388
|
+
*/
|
|
1389
|
+
minimumUsefulMarginPct: number;
|
|
1390
|
+
/** confidence level for every interval and every stop rule */
|
|
1391
|
+
confidence: number;
|
|
1392
|
+
/** the fraction of eligible sites a model must return valid output on to clear Stage B */
|
|
1393
|
+
stageBValidOutputFraction: number;
|
|
1394
|
+
/** multiplicity correction across the declared comparison family */
|
|
1395
|
+
multiplicityProcedure: "holm" | "bonferroni" | "none-single-comparison";
|
|
1396
|
+
/** how finalists are picked, stated as a procedure rather than decided later */
|
|
1397
|
+
topSelectionRule: string;
|
|
1398
|
+
/** the blinded-reviewer agreement threshold above which a detection counts as realistic */
|
|
1399
|
+
reviewerRealismThresholdPct: number;
|
|
1400
|
+
/** per-category quotas, so category is not confounded with repository by accident */
|
|
1401
|
+
categoryQuotas: Record<string, number>;
|
|
1402
|
+
/** fixed proposal slots per omission — detection is "any mutant hits", so volume must not buy luck */
|
|
1403
|
+
proposalSlotsPerOmission: number;
|
|
1404
|
+
/**
|
|
1405
|
+
* Order in which items are excluded or replaced when something fails. Declared in advance because
|
|
1406
|
+
* replacing an inconvenient item AFTER seeing its result is the most direct way to fake a finding.
|
|
1407
|
+
*/
|
|
1408
|
+
exclusionOrder: string[];
|
|
1409
|
+
/** the exact conditions that stop the programme, in words that can be checked against the code */
|
|
1410
|
+
stopRules: string[];
|
|
1411
|
+
}
|
|
1412
|
+
interface ManifestValidation {
|
|
1413
|
+
frozen: boolean;
|
|
1414
|
+
problems: string[];
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Validate the manifest. Fails CLOSED: anything missing, blank, or obviously a placeholder makes the
|
|
1418
|
+
* manifest unfrozen, and an unfrozen manifest cannot start a paid stage.
|
|
1419
|
+
*/
|
|
1420
|
+
declare function validateManifest(m: Partial<ExperimentManifest> | undefined): ManifestValidation;
|
|
1421
|
+
interface StageBReadiness {
|
|
1422
|
+
/** the ONLY value that permits spending money */
|
|
1423
|
+
ready: boolean;
|
|
1424
|
+
/** every blocker, so one pass fixes all of them rather than discovering them one at a time */
|
|
1425
|
+
blockers: string[];
|
|
1426
|
+
}
|
|
1427
|
+
interface StageBInputs {
|
|
1428
|
+
manifest: Partial<ExperimentManifest> | undefined;
|
|
1429
|
+
/** Stage A: the classic-headroom ceiling — informational, and the input to the power check */
|
|
1430
|
+
headroom: HeadroomResult | undefined;
|
|
1431
|
+
/**
|
|
1432
|
+
* Stage A: can the corpus TEST the declared margin on the conditional rate? The ceiling cannot
|
|
1433
|
+
* decide this — a conditional rate can be 100% however small the headroom is. What headroom
|
|
1434
|
+
* governs is how many observations exist to establish that rate from.
|
|
1435
|
+
*/
|
|
1436
|
+
power: ConditionalPowerResult | undefined;
|
|
1437
|
+
/** Stage A: the perfect-proposer positive control */
|
|
1438
|
+
assay: AssayResult | undefined;
|
|
1439
|
+
/** Stage A: which historical pairs survived qualification */
|
|
1440
|
+
qualification: {
|
|
1441
|
+
qualified: QualificationResult[];
|
|
1442
|
+
rejected: QualificationResult[];
|
|
1443
|
+
} | undefined;
|
|
1444
|
+
/** has the model-free deterministic baseline been run over the same corpus? */
|
|
1445
|
+
deterministicBaselineRun: boolean;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Decide whether Stage B may begin. Refuses by default and names every blocker.
|
|
1449
|
+
*
|
|
1450
|
+
* The deterministic-baseline requirement is the one most likely to be argued with, so it is stated
|
|
1451
|
+
* here: without it, a model result cannot distinguish "the LLM adds value" from "Stryker's built-in
|
|
1452
|
+
* operators happen to omit these edits and any tool producing them would have scored the same". That
|
|
1453
|
+
* distinction is the entire thesis, and it costs nothing to settle.
|
|
1454
|
+
*/
|
|
1455
|
+
declare function canStartStageB(inputs: StageBInputs): StageBReadiness;
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Layer 2 scenario-search contracts.
|
|
1459
|
+
*
|
|
1460
|
+
* The engine searches four lanes and records bounded counterexample evidence.
|
|
1461
|
+
*/
|
|
1462
|
+
declare const SCENARIO_LANES: readonly ["property", "value", "state", "failure"];
|
|
1463
|
+
type ScenarioLane = (typeof SCENARIO_LANES)[number];
|
|
1464
|
+
declare const SCENARIO_AUTHORITIES: readonly ["machine-sourced", "source-grounded", "corroborated", "code-inferred"];
|
|
1465
|
+
type ScenarioAuthority = (typeof SCENARIO_AUTHORITIES)[number];
|
|
1466
|
+
declare const SCENARIO_OUTCOMES: readonly ["confirmed-counterexample", "candidate-counterexample", "no-counterexample-found", "scenario-uncovered", "disagreement", "inconclusive", "not-applicable"];
|
|
1467
|
+
type ScenarioOutcome = (typeof SCENARIO_OUTCOMES)[number];
|
|
1468
|
+
declare const SCENARIO_SEARCH_STATES: readonly ["completed", "partial", "unavailable", "disabled", "disabled-by-tier", "not-run"];
|
|
1469
|
+
type ScenarioSearchState = (typeof SCENARIO_SEARCH_STATES)[number];
|
|
1470
|
+
declare const SCENARIO_SOURCE_KINDS: readonly ["contract", "schema", "documentation", "ticket", "test", "type", "example", "code", "trace"];
|
|
1471
|
+
type ScenarioSourceKind = (typeof SCENARIO_SOURCE_KINDS)[number];
|
|
1472
|
+
declare const SCENARIO_SOURCE_INDEPENDENCE: readonly ["independent", "derived-from-target", "unknown"];
|
|
1473
|
+
type ScenarioSourceIndependence = (typeof SCENARIO_SOURCE_INDEPENDENCE)[number];
|
|
1474
|
+
interface ScenarioSourceRef {
|
|
1475
|
+
kind: ScenarioSourceKind;
|
|
1476
|
+
repositoryPathOrExternalId: string;
|
|
1477
|
+
revisionOrTimestamp: string;
|
|
1478
|
+
startLine?: number;
|
|
1479
|
+
endLine?: number;
|
|
1480
|
+
contentDigest: string;
|
|
1481
|
+
independence: ScenarioSourceIndependence;
|
|
1482
|
+
/** Identifier of the deterministic policy rule that assigned authority. */
|
|
1483
|
+
authorityRule: string;
|
|
1484
|
+
}
|
|
1485
|
+
interface ScenarioInputDomain {
|
|
1486
|
+
/** Stable family name, such as `integer-range`, `schema-object`, or `operation-sequence`. */
|
|
1487
|
+
kind: string;
|
|
1488
|
+
/** Local source-derived explanation. It never belongs in the normalized artifact. */
|
|
1489
|
+
description: string;
|
|
1490
|
+
sourceRefs: ScenarioSourceRef[];
|
|
1491
|
+
}
|
|
1492
|
+
interface ScenarioOperationStep {
|
|
1493
|
+
operation: string;
|
|
1494
|
+
arguments: string[];
|
|
1495
|
+
expectedState?: string;
|
|
1496
|
+
}
|
|
1497
|
+
interface ScenarioExpectedObservation {
|
|
1498
|
+
kind: "equals" | "not-equals" | "throws" | "does-not-throw" | "resolves" | "rejects" | "predicate" | "state";
|
|
1499
|
+
subject: string;
|
|
1500
|
+
expected: string;
|
|
1501
|
+
}
|
|
1502
|
+
interface ScenarioIntentDraft {
|
|
1503
|
+
schema: "attest-scenario-intent/v1";
|
|
1504
|
+
id: string;
|
|
1505
|
+
lane: ScenarioLane;
|
|
1506
|
+
statement: string;
|
|
1507
|
+
authority: ScenarioAuthority;
|
|
1508
|
+
sourceRefs: ScenarioSourceRef[];
|
|
1509
|
+
targetSymbols: string[];
|
|
1510
|
+
setup: string[];
|
|
1511
|
+
generatedInputs: ScenarioInputDomain[];
|
|
1512
|
+
operations: ScenarioOperationStep[];
|
|
1513
|
+
expectedObservation: ScenarioExpectedObservation;
|
|
1514
|
+
cleanup: string[];
|
|
1515
|
+
forbiddenAssumptions: string[];
|
|
1516
|
+
}
|
|
1517
|
+
interface ScenarioIntent extends ScenarioIntentDraft {
|
|
1518
|
+
/** sha256 over every semantic field in ScenarioIntentDraft. */
|
|
1519
|
+
intentDigest: string;
|
|
1520
|
+
}
|
|
1521
|
+
interface ScenarioSummary {
|
|
1522
|
+
/** sha256 identity; source text and test text stay in the local sidecar. */
|
|
1523
|
+
scenarioId: string;
|
|
1524
|
+
lane: ScenarioLane;
|
|
1525
|
+
authority: ScenarioAuthority;
|
|
1526
|
+
outcome: ScenarioOutcome;
|
|
1527
|
+
targetDigest: string;
|
|
1528
|
+
intentDigest: string;
|
|
1529
|
+
sourceSetDigest: string;
|
|
1530
|
+
runs: number;
|
|
1531
|
+
replayed?: number;
|
|
1532
|
+
targetExecuted: boolean;
|
|
1533
|
+
assertionExecuted: boolean;
|
|
1534
|
+
runner: string;
|
|
1535
|
+
model: string | null;
|
|
1536
|
+
intentPromptVersion: string;
|
|
1537
|
+
testPromptVersion: string;
|
|
1538
|
+
toolVersion: string;
|
|
1539
|
+
containerDigest: string;
|
|
1540
|
+
}
|
|
1541
|
+
interface ScenarioSearchCounts {
|
|
1542
|
+
selectedTargets: number;
|
|
1543
|
+
measuredTargets: number;
|
|
1544
|
+
unmeasuredTargets: number;
|
|
1545
|
+
byOutcome: Record<ScenarioOutcome, number>;
|
|
1546
|
+
byLane: Record<ScenarioLane, number>;
|
|
1547
|
+
byAuthority: Record<ScenarioAuthority, number>;
|
|
1548
|
+
}
|
|
1549
|
+
interface ScenarioSearchRestrictions {
|
|
1550
|
+
containerRequired: true;
|
|
1551
|
+
network: "none";
|
|
1552
|
+
credentials: "none";
|
|
1553
|
+
productionWrites: "denied";
|
|
1554
|
+
}
|
|
1555
|
+
interface ScenarioSearchProvenance {
|
|
1556
|
+
engineVersion: string;
|
|
1557
|
+
policyDigest: string;
|
|
1558
|
+
evidencePackDigest: string;
|
|
1559
|
+
proofsDigest: string;
|
|
1560
|
+
containerImageDigest: string;
|
|
1561
|
+
runnerAdapterVersion: string;
|
|
1562
|
+
toolProtocolVersion: string;
|
|
1563
|
+
models: string[];
|
|
1564
|
+
llmCalls: number;
|
|
1565
|
+
cacheHits: number;
|
|
1566
|
+
inputTokens: number;
|
|
1567
|
+
outputTokens: number;
|
|
1568
|
+
reasoningTokens: number | null;
|
|
1569
|
+
costUsd: number | null;
|
|
1570
|
+
wallMs: number;
|
|
1571
|
+
restrictions: ScenarioSearchRestrictions;
|
|
1572
|
+
}
|
|
1573
|
+
type ScenarioSearchMeasuredBlock = ScenarioSearchProvenance & {
|
|
1574
|
+
state: "completed" | "partial";
|
|
1575
|
+
counts: ScenarioSearchCounts;
|
|
1576
|
+
summaries: ScenarioSummary[];
|
|
1577
|
+
};
|
|
1578
|
+
type ScenarioSearchBlock = ScenarioSearchMeasuredBlock | {
|
|
1579
|
+
state: Exclude<ScenarioSearchState, "completed" | "partial">;
|
|
1580
|
+
reason?: string;
|
|
1581
|
+
};
|
|
1582
|
+
declare function isScenarioLane(value: unknown): value is ScenarioLane;
|
|
1583
|
+
declare function isScenarioAuthority(value: unknown): value is ScenarioAuthority;
|
|
1584
|
+
declare function isScenarioOutcome(value: unknown): value is ScenarioOutcome;
|
|
1585
|
+
declare function isScenarioSearchState(value: unknown): value is ScenarioSearchState;
|
|
1586
|
+
/**
|
|
1587
|
+
* Copy an untrusted summary into the closed normalized shape and validate the evidence claims that
|
|
1588
|
+
* its outcome implies. Extra runtime fields are discarded, so source or test text cannot hitch a
|
|
1589
|
+
* ride through a structurally typed object.
|
|
1590
|
+
*/
|
|
1591
|
+
declare function sanitizeScenarioSummary(input: ScenarioSummary): ScenarioSummary;
|
|
1592
|
+
declare function deriveScenarioSearchCounts(selectedTargets: number, inputSummaries: readonly ScenarioSummary[]): {
|
|
1593
|
+
counts: ScenarioSearchCounts;
|
|
1594
|
+
summaries: ScenarioSummary[];
|
|
1595
|
+
};
|
|
1596
|
+
interface BuildScenarioSearchBlockInput extends ScenarioSearchProvenance {
|
|
1597
|
+
selectedTargets: number;
|
|
1598
|
+
summaries: readonly ScenarioSummary[];
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Build the only two measured block states. A run is completed exactly when every selected target
|
|
1602
|
+
* has a closed summary; otherwise it is visibly partial.
|
|
1603
|
+
*/
|
|
1604
|
+
declare function buildScenarioSearchBlock(input: BuildScenarioSearchBlockInput): ScenarioSearchMeasuredBlock;
|
|
1605
|
+
|
|
1606
|
+
declare const CI_PROPERTY_AUTHORITIES: readonly ["machine-sourced", "source-grounded"];
|
|
1607
|
+
type CiPropertyAuthority = (typeof CI_PROPERTY_AUTHORITIES)[number];
|
|
1608
|
+
declare const CI_PROPERTY_OUTCOMES: readonly ["pr-regression", "new-contract-counterexample", "source-grounded-test-gap", "pre-existing-counterexample", "possible-duplicate", "no-counterexample-found", "disagreement", "scenario-uncovered", "inconclusive", "not-applicable"];
|
|
1609
|
+
type CiPropertyOutcome = (typeof CI_PROPERTY_OUTCOMES)[number];
|
|
1610
|
+
declare const CI_PROPERTY_STATES: readonly ["completed", "partial", "unavailable", "disabled", "disabled-by-tier", "not-run"];
|
|
1611
|
+
type CiPropertyState = (typeof CI_PROPERTY_STATES)[number];
|
|
1612
|
+
declare const CI_RESIDUAL_STATES: readonly ["covered-existing", "proposed-closed", "open", "inconclusive"];
|
|
1613
|
+
type CiResidualState = (typeof CI_RESIDUAL_STATES)[number];
|
|
1614
|
+
declare const CI_PROPERTY_CAPS: readonly [1, 2, 4, 8];
|
|
1615
|
+
type CiPropertyCandidateCap = (typeof CI_PROPERTY_CAPS)[number];
|
|
1616
|
+
interface CiChangedRange {
|
|
1617
|
+
startLine: number;
|
|
1618
|
+
endLine: number;
|
|
1619
|
+
}
|
|
1620
|
+
interface CiResidualGapEntry {
|
|
1621
|
+
/** Stable digest over the carrier or unsupported-site identity. */
|
|
1622
|
+
gapIdentity: string;
|
|
1623
|
+
kind: "classic" | "deterministic" | "realistic" | "error-path" | "patch-revert" | "unsupported-site";
|
|
1624
|
+
headSha: string;
|
|
1625
|
+
mergeBaseSha: string;
|
|
1626
|
+
targetPath: string;
|
|
1627
|
+
targetSymbol: string;
|
|
1628
|
+
targetDigest: string;
|
|
1629
|
+
sourceDigest: string;
|
|
1630
|
+
changedRange: CiChangedRange;
|
|
1631
|
+
state: CiResidualState;
|
|
1632
|
+
mutantIdentity?: string;
|
|
1633
|
+
patchUnitId?: string;
|
|
1634
|
+
patchDigest?: string;
|
|
1635
|
+
candidateDigest?: string;
|
|
1636
|
+
candidateTestPath?: string;
|
|
1637
|
+
candidateTestName?: string;
|
|
1638
|
+
proofVerdict: "proven" | "rejected" | "not-attempted" | "not-applicable";
|
|
1639
|
+
wholeSuiteVerdict: "green" | "regressed" | "not-checked" | "not-applicable";
|
|
1640
|
+
producerVersion: string;
|
|
1641
|
+
}
|
|
1642
|
+
interface CiResidualGapLedger {
|
|
1643
|
+
schema: "attest-ci-residual-ledger/v1";
|
|
1644
|
+
headSha: string;
|
|
1645
|
+
mergeBaseSha: string;
|
|
1646
|
+
policyDigest: string;
|
|
1647
|
+
entries: CiResidualGapEntry[];
|
|
1648
|
+
ledgerDigest: string;
|
|
1649
|
+
}
|
|
1650
|
+
interface CiPropertySummary {
|
|
1651
|
+
findingId: string;
|
|
1652
|
+
outcome: CiPropertyOutcome;
|
|
1653
|
+
authority: CiPropertyAuthority;
|
|
1654
|
+
targetDigest: string;
|
|
1655
|
+
intentDigest: string;
|
|
1656
|
+
behaviourKey: string;
|
|
1657
|
+
sourceSetDigest: string;
|
|
1658
|
+
headReplays: number;
|
|
1659
|
+
comparisonReplays: number;
|
|
1660
|
+
targetExecuted: boolean;
|
|
1661
|
+
assertionExecuted: boolean;
|
|
1662
|
+
candidateDigest: string | null;
|
|
1663
|
+
}
|
|
1664
|
+
interface CiPropertyCounts {
|
|
1665
|
+
selectedTargets: number;
|
|
1666
|
+
validatedRules: number;
|
|
1667
|
+
executedRules: number;
|
|
1668
|
+
findings: number;
|
|
1669
|
+
duplicatesSuppressed: number;
|
|
1670
|
+
byOutcome: Record<CiPropertyOutcome, number>;
|
|
1671
|
+
}
|
|
1672
|
+
interface CiPropertyMeasuredBlock {
|
|
1673
|
+
state: "completed" | "partial";
|
|
1674
|
+
mode: "shadow" | "advisory";
|
|
1675
|
+
candidateCap: CiPropertyCandidateCap;
|
|
1676
|
+
counts: CiPropertyCounts;
|
|
1677
|
+
summaries: CiPropertySummary[];
|
|
1678
|
+
ledgerDigest: string;
|
|
1679
|
+
evidencePackDigest: string;
|
|
1680
|
+
proofsDigest: string;
|
|
1681
|
+
containerDigests: string[];
|
|
1682
|
+
models: string[];
|
|
1683
|
+
llmCalls: number;
|
|
1684
|
+
costUsd: number | null;
|
|
1685
|
+
wallMs: number;
|
|
1686
|
+
}
|
|
1687
|
+
type CiPropertySearchBlock = CiPropertyMeasuredBlock | {
|
|
1688
|
+
state: Exclude<CiPropertyState, "completed" | "partial">;
|
|
1689
|
+
reason?: string;
|
|
1690
|
+
};
|
|
1691
|
+
declare function ciPropertyLedgerDigest(ledger: Omit<CiResidualGapLedger, "ledgerDigest">): string;
|
|
1692
|
+
declare function validateCiResidualGapLedger(input: CiResidualGapLedger): CiResidualGapLedger;
|
|
1693
|
+
declare function isCiPropertyCandidateCap(value: unknown): value is CiPropertyCandidateCap;
|
|
1694
|
+
declare function sanitizeCiPropertySummary(input: CiPropertySummary): CiPropertySummary;
|
|
1695
|
+
interface BuildCiPropertyBlockInput {
|
|
1696
|
+
mode: "shadow" | "advisory";
|
|
1697
|
+
candidateCap: CiPropertyCandidateCap;
|
|
1698
|
+
selectedTargets: number;
|
|
1699
|
+
validatedRules: number;
|
|
1700
|
+
executedRules: number;
|
|
1701
|
+
duplicatesSuppressed: number;
|
|
1702
|
+
summaries: readonly CiPropertySummary[];
|
|
1703
|
+
expectedSummaries: number;
|
|
1704
|
+
ledgerDigest: string;
|
|
1705
|
+
evidencePackDigest: string;
|
|
1706
|
+
proofsDigest: string;
|
|
1707
|
+
containerDigests: readonly string[];
|
|
1708
|
+
models: readonly string[];
|
|
1709
|
+
llmCalls: number;
|
|
1710
|
+
costUsd: number | null;
|
|
1711
|
+
wallMs: number;
|
|
1712
|
+
}
|
|
1713
|
+
declare function buildCiPropertySearchBlock(input: BuildCiPropertyBlockInput): CiPropertyMeasuredBlock;
|
|
1714
|
+
|
|
1715
|
+
type FlakyPolicy = "quarantine" | "strict";
|
|
1716
|
+
/**
|
|
1717
|
+
* Layer-0 (diff-coverage) policy. `shortCircuit` defaults to FALSE: on a Layer-0 failure the
|
|
1718
|
+
* run still mutates the covered changed lines so the default advisory audience keeps its
|
|
1719
|
+
* mutation findings (a single uncovered line must not silently suppress all of Layer 1).
|
|
1720
|
+
* `shortCircuit: true` is the opt-in fail-fast — Layer 1 is skipped and every surface
|
|
1721
|
+
* (CLI, Markdown, artifact, certificate) discloses that mutation was skipped.
|
|
1722
|
+
*/
|
|
1723
|
+
interface DiffCoveragePolicy {
|
|
1724
|
+
shortCircuit: boolean;
|
|
1725
|
+
}
|
|
1726
|
+
interface DeterministicMutantsPolicy {
|
|
1727
|
+
/**
|
|
1728
|
+
* Production-candidate operators beyond Stryker's built-ins. Disabled until held-out
|
|
1729
|
+
* cross-project replay promotes the inventory.
|
|
1730
|
+
*/
|
|
1731
|
+
enabled: boolean;
|
|
1732
|
+
/** Maximum carried deterministic mutants per run; unselected candidates remain disclosed. */
|
|
1733
|
+
maxRun: number;
|
|
1734
|
+
}
|
|
1735
|
+
interface PerTestPolicy {
|
|
1736
|
+
enabled: boolean;
|
|
1737
|
+
/** Full-suite comparison sample; null until a held-out JavaScript benchmark freezes its size. */
|
|
1738
|
+
fullSuiteAuditSampleSize: number | null;
|
|
1739
|
+
/** Largest accepted mismatch fraction; null until benchmark-frozen. Both audit fields move together. */
|
|
1740
|
+
fullSuiteMismatchTolerance: number | null;
|
|
1741
|
+
}
|
|
1742
|
+
interface FixLoopPolicy {
|
|
1743
|
+
enabled: boolean;
|
|
1744
|
+
/** max survivor gaps to attempt a candidate killing test for */
|
|
1745
|
+
maxCandidates: number;
|
|
1746
|
+
/**
|
|
1747
|
+
* How many times each proof side (real / mutant) is re-run.
|
|
1748
|
+
*
|
|
1749
|
+
* The default is 2, not 1, and that is load-bearing rather than cautious: the executor ALTERNATES
|
|
1750
|
+
* which side runs first on each repetition, so a candidate that only "works" because of state the
|
|
1751
|
+
* previous run left behind cannot satisfy every repetition. With a single repetition there is
|
|
1752
|
+
* nothing to alternate and that protection never engages — the very defence would be inert by
|
|
1753
|
+
* default.
|
|
1754
|
+
*/
|
|
1755
|
+
proofRepetitions: number;
|
|
1756
|
+
/** Inline is shipping-default; deferred emits target-proven evidence for a separate same-PR lane. */
|
|
1757
|
+
suiteConfirmation?: "inline" | "deferred";
|
|
1758
|
+
/** Bounded literal non-secret values made available only inside fix proof containers. */
|
|
1759
|
+
syntheticEnvironment?: Record<string, string>;
|
|
1760
|
+
/**
|
|
1761
|
+
* Hard wall-clock budget for candidate GENERATION (model calls), in ms (default at the use
|
|
1762
|
+
* site: 12 min). Overrun ⇒ the block reports `unavailable: generation-budget-exceeded`.
|
|
1763
|
+
*/
|
|
1764
|
+
generationDeadlineMs?: number;
|
|
1765
|
+
/**
|
|
1766
|
+
* Hard wall-clock budget for the PROOF phase (targeted runs + the suite check), in ms
|
|
1767
|
+
* (default at the use site: 15 min). Overrun ⇒ unattempted candidates are reported
|
|
1768
|
+
* not-attempted and suite-unverified candidates are REJECTED — fail-closed, never
|
|
1769
|
+
* silently passed.
|
|
1770
|
+
*/
|
|
1771
|
+
proofDeadlineMs?: number;
|
|
1772
|
+
cacheDir?: string;
|
|
1773
|
+
}
|
|
1774
|
+
interface ErrorPathsPolicy {
|
|
1775
|
+
/** run the static handler scanner (advisory) */
|
|
1776
|
+
staticAnalysis: boolean;
|
|
1777
|
+
/** opted-in gate: a changed handler with an untested (survived/no-coverage) mutant fails the gate */
|
|
1778
|
+
failOnUntested: boolean;
|
|
1779
|
+
/** opted-in gate: any detected handler anti-pattern fails the gate */
|
|
1780
|
+
failOnAntiPattern: boolean;
|
|
1781
|
+
/** force a handler mutant when a changed handler has no usable built-in Stryker mutant */
|
|
1782
|
+
forceHandlerMutation: boolean;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Which model runs each Layer-1 task.
|
|
1786
|
+
*
|
|
1787
|
+
* NEVER a credential — one project endpoint and key come from the environment
|
|
1788
|
+
* (`ATTEST_MODEL_ENDPOINT` / `ATTEST_MODEL_API_KEY`); only the model NAME differs between entries.
|
|
1789
|
+
* Policy is checked into the customer's repository, so a key here would be committed and shared.
|
|
1790
|
+
*
|
|
1791
|
+
* Per TASK rather than per run, because the tasks are not alike: equivalence triage is a judgement
|
|
1792
|
+
* call over a small context, proposal generation is creative work over a whole file, and test
|
|
1793
|
+
* generation must produce code that compiles and runs. `default` covers any task not named.
|
|
1794
|
+
*/
|
|
1795
|
+
interface ModelChoicePolicy {
|
|
1796
|
+
/** the local Claude CLI, or a model hosted on the configured project endpoint */
|
|
1797
|
+
provider: "claude-cli" | "hosted";
|
|
1798
|
+
/** model name as the project knows it (required for `hosted`) */
|
|
1799
|
+
model?: string;
|
|
1800
|
+
/** `default` (ATTEST_MODEL_ENDPOINT) or `alt` (ATTEST_MODEL_ENDPOINT_ALT) */
|
|
1801
|
+
endpoint?: "default" | "alt";
|
|
1802
|
+
/** USD per 1M tokens, so run spend is reported rather than fabricated */
|
|
1803
|
+
pricePerMTokens?: {
|
|
1804
|
+
input: number;
|
|
1805
|
+
output: number;
|
|
1806
|
+
};
|
|
1807
|
+
temperature?: number;
|
|
1808
|
+
/** reasoning effort, where the model supports it — benchmarked as its own axis */
|
|
1809
|
+
/**
|
|
1810
|
+
* "xhigh" is OpenAI's own top tier (none|low|medium|high|xhigh); "ultra" belongs to other
|
|
1811
|
+
* harnesses' vocabularies. Both are accepted because the value passes through VERBATIM to the
|
|
1812
|
+
* provider — mapping one to the other would make the recorded identity (model@effort, baked
|
|
1813
|
+
* into every cache key and triage record) claim an effort that was never actually requested.
|
|
1814
|
+
*/
|
|
1815
|
+
effort?: "low" | "medium" | "high" | "xhigh" | "ultra";
|
|
1816
|
+
/**
|
|
1817
|
+
* Generated-token ceiling; reasoning models need headroom for hidden reasoning.
|
|
1818
|
+
*
|
|
1819
|
+
* ```yaml
|
|
1820
|
+
* classicMutation:
|
|
1821
|
+
* models:
|
|
1822
|
+
* fixLoop:
|
|
1823
|
+
* maxCompletionTokens: 8000
|
|
1824
|
+
* ```
|
|
1825
|
+
*
|
|
1826
|
+
* The fix loop wants 8000 or more. Its prompt now carries the enclosing function, the route to
|
|
1827
|
+
* it, and the resolved shapes of every type in both signatures, so the test it asks for is
|
|
1828
|
+
* correspondingly longer. A reply that hits the ceiling is TRUNCATED, and a truncated reply
|
|
1829
|
+
* truncates identically on retry — it is not retried, and the gap reports no candidate. Provider
|
|
1830
|
+
* defaults differ sharply here (2000 on one, 32000 on another), so a ceiling that never bound
|
|
1831
|
+
* one model can silently bind the next.
|
|
1832
|
+
*/
|
|
1833
|
+
maxCompletionTokens?: number;
|
|
1834
|
+
/**
|
|
1835
|
+
* How long one model call may take, in ms. Default 240_000.
|
|
1836
|
+
*
|
|
1837
|
+
* Measured, not guessed: with the current prompt this model answers a fix-loop call in 40–50s,
|
|
1838
|
+
* and the slowest gap observed on a real repository took 238s — two seconds under the old
|
|
1839
|
+
* hardcoded ceiling. The ceiling was reachable and nothing could move it, because it lived in
|
|
1840
|
+
* the provider source and no policy or environment path led to it.
|
|
1841
|
+
*
|
|
1842
|
+
* Bounded at 290_000. Node's own fetch abandons a request that has produced no response for
|
|
1843
|
+
* ~300s regardless of what we ask for (measured: three calls died at 301.0s against a 15-minute
|
|
1844
|
+
* setting, `UND_ERR_HEADERS_TIMEOUT`). Staying below it keeps our own abort the one that fires,
|
|
1845
|
+
* and a larger number here would read as patience we cannot deliver.
|
|
1846
|
+
*/
|
|
1847
|
+
timeoutMs?: number;
|
|
1848
|
+
}
|
|
1849
|
+
type ModelsPolicy = Partial<Record<"default" | "triage" | "counterfactual" | "fixLoop" | "naming" | "layer2Intent" | "layer2Test" | "layer2IntentFallback" | "layer2TestFallback" | "ciPropertyIntent" | "ciPropertyTest" | "ciPropertyIntentFallback" | "ciPropertyTestFallback", ModelChoicePolicy>>;
|
|
1850
|
+
interface ClassicMutationPolicy {
|
|
1851
|
+
deterministicMutants: DeterministicMutantsPolicy;
|
|
1852
|
+
perTest: PerTestPolicy;
|
|
1853
|
+
fixLoop: FixLoopPolicy;
|
|
1854
|
+
errorPaths: ErrorPathsPolicy;
|
|
1855
|
+
/** per-task model selection; absent ⇒ the local Claude CLI, as before */
|
|
1856
|
+
models?: ModelsPolicy;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Source-grounded Layer 2 search. This remains a research-only, advisory mechanism until the
|
|
1860
|
+
* frozen natural-fault benchmark and overnight shadow both pass.
|
|
1861
|
+
*/
|
|
1862
|
+
interface ScenarioSearchPolicy {
|
|
1863
|
+
enabled: boolean;
|
|
1864
|
+
/** Only the research profile exists before benchmark promotion. */
|
|
1865
|
+
profile: "research";
|
|
1866
|
+
lanes: ScenarioLane[];
|
|
1867
|
+
sourceModes: ScenarioAuthority[];
|
|
1868
|
+
maxTargets: number;
|
|
1869
|
+
maxCandidatesPerTarget: number;
|
|
1870
|
+
maxAgentTurnsPerTarget: number;
|
|
1871
|
+
maxModelCalls: number;
|
|
1872
|
+
totalDeadlineMs: number;
|
|
1873
|
+
executionDeadlineMs: number;
|
|
1874
|
+
replayRepetitions: number;
|
|
1875
|
+
/** Required true in v1: generated code is never executed on the host. */
|
|
1876
|
+
requireContainer: true;
|
|
1877
|
+
/** Optional user-owned cache directory override. */
|
|
1878
|
+
cacheDir?: string;
|
|
1879
|
+
/** Required false in v1: Layer 2 cannot affect a gate before promotion. */
|
|
1880
|
+
failOnCounterexample: false;
|
|
1881
|
+
}
|
|
1882
|
+
/** Bounded pull-request property search. Disabled until benchmark and shadow promotion. */
|
|
1883
|
+
interface CiPropertySearchPolicy {
|
|
1884
|
+
enabled: boolean;
|
|
1885
|
+
mode: "shadow" | "advisory";
|
|
1886
|
+
authorities: CiPropertyAuthority[];
|
|
1887
|
+
maxTargets: number;
|
|
1888
|
+
candidateCap: CiPropertyCandidateCap;
|
|
1889
|
+
maxFindings: number;
|
|
1890
|
+
claimPoolSize: number;
|
|
1891
|
+
propertyRuns: number;
|
|
1892
|
+
replayRepetitions: number;
|
|
1893
|
+
setupRepairAttempts: number;
|
|
1894
|
+
totalDeadlineMs: number;
|
|
1895
|
+
perCandidateDeadlineMs: number;
|
|
1896
|
+
maxEvidenceSources: number;
|
|
1897
|
+
maxEvidenceBytes: number;
|
|
1898
|
+
requireContainer: true;
|
|
1899
|
+
failOnFinding: false;
|
|
1900
|
+
cacheDir?: string;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Reverse-patch verification: revert a hunk of the pull request's OWN diff and re-run the suite.
|
|
1904
|
+
* A suite that stays green proves no test pins that change — a gap the mutation score cannot see,
|
|
1905
|
+
* because there is no mutant involved, only the author's real edit.
|
|
1906
|
+
*
|
|
1907
|
+
* BETA, and off by default. It is advisory in the strongest sense: it never changes the mutation
|
|
1908
|
+
* score, never moves a gate, and cannot fail a build. What it costs is wall time, and the honest
|
|
1909
|
+
* number is stated rather than hidden — measured on a seven-item cohort of real pull requests, a
|
|
1910
|
+
* caught change adds about 0.4x the suite's own runtime and a reported gap about 2.2x, because a
|
|
1911
|
+
* gap must be proven by running the complete suite twice.
|
|
1912
|
+
*
|
|
1913
|
+
* NOT tier-gated, unlike `scenarioSearch` and `ciPropertySearch`, which {@link researchTier}
|
|
1914
|
+
* forces to `disabled-by-tier` at tier 0. Those two spend model budget and a tier-0 run has none;
|
|
1915
|
+
* this lane spends only the customer's own test runner, so gating it by tier would withhold a
|
|
1916
|
+
* capability that costs nothing to grant. Stated here rather than left as an absence, because the
|
|
1917
|
+
* asymmetry is otherwise only visible by noticing which member is missing from ResearchTier.
|
|
1918
|
+
*
|
|
1919
|
+
* Beta limits, both deliberate: jest and vitest only (the comparison needs a per-test report),
|
|
1920
|
+
* and single-package runs only (the lane binds one baseline and one working directory, so a
|
|
1921
|
+
* multi-package run has nothing coherent to compare against and is refused by name).
|
|
1922
|
+
*/
|
|
1923
|
+
interface PatchRevertPolicy {
|
|
1924
|
+
enabled: boolean;
|
|
1925
|
+
/**
|
|
1926
|
+
* Run the covering test files in ONE runner invocation to decide the common "a test catches
|
|
1927
|
+
* this" case without a whole-suite run.
|
|
1928
|
+
*
|
|
1929
|
+
* On by default WHEN the lane is enabled, because it is what makes the cost bearable: measured
|
|
1930
|
+
* across the cohort it took the caught case from 0.96x of a suite to 0.41x, and on the two
|
|
1931
|
+
* realistic suite sizes (35s and 46s) to 0.05x. It cannot change a verdict — the screen may only
|
|
1932
|
+
* conclude "a test caught this", and every other outcome runs the complete suite regardless.
|
|
1933
|
+
*/
|
|
1934
|
+
batchedImpactScreen: boolean;
|
|
1935
|
+
}
|
|
1936
|
+
interface ResearchPolicy {
|
|
1937
|
+
scenarioSearch: ScenarioSearchPolicy;
|
|
1938
|
+
invariant: CiPropertySearchPolicy;
|
|
1939
|
+
}
|
|
1940
|
+
/** Classic-mutation cost controls (the sampling cap + static-mutant handling). */
|
|
1941
|
+
interface MutationPolicy {
|
|
1942
|
+
/**
|
|
1943
|
+
* Budget for classic mutants per run; null = uncapped (every mutant in the changed ranges runs
|
|
1944
|
+
* — today's behavior). When set, the engine enumerates every eligible mutant, then runs a
|
|
1945
|
+
* seeded line-first round-robin sample of this size: one mutant per changed line before any
|
|
1946
|
+
* line gets a second, so breadth over changed lines is maximal by construction. The full
|
|
1947
|
+
* eligible count, the sample, the seed, and line breadth are all disclosed in the artifact's
|
|
1948
|
+
* classicSampling block — a capped run can never present as a full one. mutantsPlanned/Run keep
|
|
1949
|
+
* describing the EXECUTED set (the server recomputes them from mutant records, so they cannot
|
|
1950
|
+
* mean anything the records don't contain); whether the §4.3 sampling-fraction floor should
|
|
1951
|
+
* read eligible-set instead is a frozen-plan amendment, deliberately not smuggled in here.
|
|
1952
|
+
*/
|
|
1953
|
+
maxMutantsPerRun: number | null;
|
|
1954
|
+
/**
|
|
1955
|
+
* Skip "static" mutants (evaluated at module load; each needs a fresh process + full-suite
|
|
1956
|
+
* run — Stryker measures 6-18% of mutants consuming 40-79% of run time). Skipped statics come
|
|
1957
|
+
* back as skipped-by-cap in the accounting, so the exclusion is disclosed, never silent.
|
|
1958
|
+
*/
|
|
1959
|
+
ignoreStatic: boolean;
|
|
1960
|
+
/** Confirmation repetitions; null means uncertain mutation outcomes are excluded, never credited. */
|
|
1961
|
+
recheckRepetitions: number | null;
|
|
1962
|
+
/** Multiplier for the baseline-derived timeout factor; configured with recheckRepetitions. */
|
|
1963
|
+
recheckTimeoutFactorMultiplier: number | null;
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* How several survivors in one place are presented as ONE finding.
|
|
1967
|
+
*
|
|
1968
|
+
* Declared here, in core, because it is a policy value the engine reads — the engine depends on
|
|
1969
|
+
* core, never the reverse, so the closed set has to live on this side of that edge.
|
|
1970
|
+
*
|
|
1971
|
+
* - `line` — group by source line. Wrong in both directions: two operands of one `if` on
|
|
1972
|
+
* separate lines render as two gaps, while two unrelated statements sharing a
|
|
1973
|
+
* line render as one. Retained ONLY because the hosted control plane never
|
|
1974
|
+
* receives source and so cannot do better.
|
|
1975
|
+
* - `structural` — group by the smallest enclosing STATEMENT, with containment capped so a
|
|
1976
|
+
* whole-function survivor cannot absorb everything inside it. A fact about the
|
|
1977
|
+
* syntax tree, so no merge can be wrong. Fixes both of line's errors. THE DEFAULT.
|
|
1978
|
+
*
|
|
1979
|
+
* TWO STRATEGIES WERE REMOVED, both on measurement rather than taste:
|
|
1980
|
+
*
|
|
1981
|
+
* - `data-flow` merged clusters sharing a resolved binding inside one function. On 189 real
|
|
1982
|
+
* survivors across 17 files it produced **2 merges beyond structural**, in 2 files. A setting
|
|
1983
|
+
* that changes nothing is worse than an absent one, because someone eventually enables it and
|
|
1984
|
+
* wonders why their output is identical. The scope resolver it was built on REMAINS — see
|
|
1985
|
+
* `engine-stryker/src/scope.ts`; the `wrong-variable` mutator depends on it to avoid substituting
|
|
1986
|
+
* a name that is not in scope, and nothing else can tell two same-named variables apart.
|
|
1987
|
+
* - `semantic` asked a model to merge. gpt-5.6-terra at `xhigh` also managed **2 merges** on the
|
|
1988
|
+
* same corpus — a mechanical analyser and the best available model independently agreeing there is
|
|
1989
|
+
* nothing to merge beyond the syntax tree. What the model IS good at is naming the gap, which is
|
|
1990
|
+
* now {@link FindingsPolicy.naming} and never touches the grouping.
|
|
1991
|
+
*
|
|
1992
|
+
* Every strategy keeps each mutant individually in the artifact and displays each cluster's member
|
|
1993
|
+
* count, so no signed number depends on this setting.
|
|
1994
|
+
*/
|
|
1995
|
+
declare const CLUSTER_STRATEGIES: readonly ["line", "structural"];
|
|
1996
|
+
type ClusterStrategy = (typeof CLUSTER_STRATEGIES)[number];
|
|
1997
|
+
/** Narrow an untrusted value to a strategy. Lives beside the set it guards so the two cannot drift. */
|
|
1998
|
+
declare function isClusterStrategy(v: unknown): v is ClusterStrategy;
|
|
1999
|
+
interface FindingsPolicy {
|
|
2000
|
+
clustering: ClusterStrategy;
|
|
2001
|
+
/**
|
|
2002
|
+
* Ask a model to NAME each test gap in plain English. Off by default — it costs one call per file.
|
|
2003
|
+
*
|
|
2004
|
+
* The model labels; it never groups. That distinction is the whole feature: `clustering` decides
|
|
2005
|
+
* how many gaps there are and stays mechanical, while naming only adds a sentence to each one, so
|
|
2006
|
+
* a bad reply costs a missing label and can never change a count.
|
|
2007
|
+
*
|
|
2008
|
+
* Measured on 63 real gaps from node-cron: gpt-5.6-terra at `xhigh` named 60 of them accurately.
|
|
2009
|
+
* Asked instead to MERGE gaps, the same model managed 2 merges beyond structural across 4 files —
|
|
2010
|
+
* which is why grouping-by-model was dropped and naming kept.
|
|
2011
|
+
*
|
|
2012
|
+
* Requires tier >= 1, like every other model-backed feature. The names are prose and stay
|
|
2013
|
+
* CUSTOMER-SIDE: they reach the pull-request comment the CLI posts and never the control plane.
|
|
2014
|
+
*/
|
|
2015
|
+
naming: boolean;
|
|
2016
|
+
}
|
|
2017
|
+
/** Proof-sandbox provisioning for fix-loop worktrees. */
|
|
2018
|
+
interface SandboxPolicy {
|
|
2019
|
+
/**
|
|
2020
|
+
* Repo-relative gitignored files to mirror into proof sandboxes (e.g. "packages/api/.env").
|
|
2021
|
+
* Root-level .env conventions are auto-linked without configuration; this list is for nested
|
|
2022
|
+
* paths only. Same-host ephemeral copies, wiped on dispose; content never egresses (the tier
|
|
2023
|
+
* schema has no field for it) and never enters model prompts. Explicit by design so security
|
|
2024
|
+
* review sees exactly which files proof runs may read.
|
|
2025
|
+
*/
|
|
2026
|
+
envFiles: string[];
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Identity and admission rules for the caller-prepared test environment.
|
|
2030
|
+
*
|
|
2031
|
+
* Callers invoke Abloh after their normal setup. Sandboxed HEAD/fault proofs never guess how to
|
|
2032
|
+
* reconstruct that setup: the trusted base policy must declare the exact install directory,
|
|
2033
|
+
* install command, and any setup commands. Missing or failing proof preparation is reported as
|
|
2034
|
+
* incomplete while host baseline, coverage, and classic mutation remain usable.
|
|
2035
|
+
*/
|
|
2036
|
+
interface EnvironmentPolicy {
|
|
2037
|
+
/**
|
|
2038
|
+
* Immutable OCI image used to prepare the test environment for adversarial proofs.
|
|
2039
|
+
* Omission uses Abloh's reviewed, digest-pinned Node image; a mutable tag is never accepted.
|
|
2040
|
+
*/
|
|
2041
|
+
runtimeImage?: string;
|
|
2042
|
+
/** Repository-relative directory in which the sandbox dependency install runs. */
|
|
2043
|
+
installDirectory?: string;
|
|
2044
|
+
/** Exact direct dependency-install command for sandbox proofs; no shell evaluation. */
|
|
2045
|
+
installCommand?: string;
|
|
2046
|
+
/** Optional local-only service presets. Every proof phase receives fresh isolated instances. */
|
|
2047
|
+
services: Array<{
|
|
2048
|
+
kind: "postgres" | "redis";
|
|
2049
|
+
image: string;
|
|
2050
|
+
}>;
|
|
2051
|
+
/** Environment-variable names that must be present. Values are used by tests but never recorded. */
|
|
2052
|
+
requiredVariables: string[];
|
|
2053
|
+
/** Additional repo-relative regular files whose bytes identify the prepared environment. */
|
|
2054
|
+
identityFiles: string[];
|
|
2055
|
+
/**
|
|
2056
|
+
* Optional direct commands run after the declared dependency install in the proof image.
|
|
2057
|
+
* Each string is parsed as argv and is never evaluated by a shell.
|
|
2058
|
+
*/
|
|
2059
|
+
setupCommands: string[];
|
|
2060
|
+
/**
|
|
2061
|
+
* Repo-relative files or directories setup may create or change. Their resulting bytes, modes,
|
|
2062
|
+
* missing state, and contained symlinks are bound into the environment contract.
|
|
2063
|
+
*/
|
|
2064
|
+
generatedFiles: string[];
|
|
2065
|
+
/** Optional single, literal test command. Shell composition and inline environment values are refused. */
|
|
2066
|
+
testCommand?: string;
|
|
2067
|
+
}
|
|
2068
|
+
interface AttestPolicy {
|
|
2069
|
+
/** minimum score (of changed code) to pass the gate */
|
|
2070
|
+
threshold: number;
|
|
2071
|
+
/** advisory (neutral check) until true */
|
|
2072
|
+
enforce: boolean;
|
|
2073
|
+
/** data-flow tier (§4.1) — 0 = no LLM, 1 = in-CI triage, 2 = snippet egress */
|
|
2074
|
+
tier: 0 | 1 | 2;
|
|
2075
|
+
floor: FloorPolicy;
|
|
2076
|
+
mutation: MutationPolicy;
|
|
2077
|
+
sandbox: SandboxPolicy;
|
|
2078
|
+
/** Repository-relative package selected by `abloh init`; absent means the repository root. */
|
|
2079
|
+
target: {
|
|
2080
|
+
directory?: string;
|
|
2081
|
+
};
|
|
2082
|
+
environment: EnvironmentPolicy;
|
|
2083
|
+
/** Layer-0 diff-coverage behavior */
|
|
2084
|
+
diffCoverage: DiffCoveragePolicy;
|
|
2085
|
+
flaky: FlakyPolicy;
|
|
2086
|
+
/** paths where "no new untriaged survivors" applies (optional rule) */
|
|
2087
|
+
flaggedPaths: string[];
|
|
2088
|
+
/**
|
|
2089
|
+
* Multi-package gate aggregation. Default worst-of-packages (a weak package cannot hide in
|
|
2090
|
+
* the pooled score). "standalone" is the raw-path escape hatch for a self-hosted server that
|
|
2091
|
+
* does not yet verify per-package evidence — decided BEFORE measurement, so the artifact
|
|
2092
|
+
* never claims a rule it also gated on.
|
|
2093
|
+
*/
|
|
2094
|
+
scoreAggregation: "standalone" | "worst-of-packages";
|
|
2095
|
+
/** certificate approvers; consumed by the control plane during issuance */
|
|
2096
|
+
approvers: string[];
|
|
2097
|
+
/** Classic-mutation extension policy: deterministic mutants, per-test, fix loop, error paths. */
|
|
2098
|
+
classicMutation: ClassicMutationPolicy;
|
|
2099
|
+
patchRevert: PatchRevertPolicy;
|
|
2100
|
+
research: ResearchPolicy;
|
|
2101
|
+
/** how survivors in one place are presented as one finding — presentation only, never a count */
|
|
2102
|
+
findings: FindingsPolicy;
|
|
2103
|
+
}
|
|
2104
|
+
declare const DEFAULT_CLASSIC_MUTATION: ClassicMutationPolicy;
|
|
2105
|
+
declare const DEFAULT_MECHANISMS: {
|
|
2106
|
+
scenarioSearch: ScenarioSearchPolicy;
|
|
2107
|
+
ciPropertySearch: CiPropertySearchPolicy;
|
|
2108
|
+
patchRevert: PatchRevertPolicy;
|
|
2109
|
+
};
|
|
2110
|
+
declare const DEFAULT_DIFF_COVERAGE: DiffCoveragePolicy;
|
|
2111
|
+
declare const DEFAULT_MUTATION: MutationPolicy;
|
|
2112
|
+
declare const DEFAULT_SANDBOX: SandboxPolicy;
|
|
2113
|
+
/** Reviewed multi-platform Node 22 base used only when a repository has not pinned its own image. */
|
|
2114
|
+
declare const DEFAULT_ENVIRONMENT_IMAGE_DIGEST = "6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3";
|
|
2115
|
+
declare const DEFAULT_ENVIRONMENT_IMAGE_REF = "node:22.23.1-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3";
|
|
2116
|
+
declare const DEFAULT_ENVIRONMENT: EnvironmentPolicy;
|
|
2117
|
+
/**
|
|
2118
|
+
* `structural` by default, not `line`.
|
|
2119
|
+
*
|
|
2120
|
+
* The line key is not a conservative baseline — it is actively wrong in both directions, and one of
|
|
2121
|
+
* those directions (two operands of one guard shown as two separate gaps) inflates what we report
|
|
2122
|
+
* about the customer. `structural` is a syntax-tree fact with no way for a merge to be incorrect,
|
|
2123
|
+
* so it is the honest default. The two strategies that CAN merge wrongly are opt-in.
|
|
2124
|
+
*/
|
|
2125
|
+
declare const DEFAULT_FINDINGS: FindingsPolicy;
|
|
2126
|
+
declare const DEFAULT_POLICY: AttestPolicy;
|
|
2127
|
+
/** Validate an explicit CLI/action test command using the same rules as committed policy. */
|
|
2128
|
+
declare function validatePreparedTestCommand(value: unknown, label?: string): string;
|
|
2129
|
+
/** Parse the validated literal-command subset into the exact argv that engines must execute. */
|
|
2130
|
+
declare function parsePreparedTestCommand(value: unknown, label?: string): string[];
|
|
2131
|
+
/** Parse an engine-generated command string whose data arguments were already shell-quoted. */
|
|
2132
|
+
declare function parseLiteralArguments(command: string, label?: string): string[];
|
|
2133
|
+
/** Hard bound on generated/literal command argv length — preflight surfaces it up front. */
|
|
2134
|
+
declare const MAX_LITERAL_ARGUMENTS = 128;
|
|
2135
|
+
/** Hard byte bound on a whole generated/literal command line. */
|
|
2136
|
+
declare const MAX_LITERAL_COMMAND_BYTES: number;
|
|
2137
|
+
/**
|
|
2138
|
+
* The resolved state of each Layer-1 feature after tier gating.
|
|
2139
|
+
*/
|
|
2140
|
+
interface ClassicMutationTier {
|
|
2141
|
+
fixLoop: "enabled" | "disabled" | "disabled-by-tier";
|
|
2142
|
+
perTest: "enabled" | "disabled";
|
|
2143
|
+
errorPathStatic: "enabled" | "disabled";
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* `effectiveTier` is the tier the RUN actually operates at — the `--tier` override when one was
|
|
2147
|
+
* passed, else the policy's. It must be threaded in explicitly: reading `policy.tier` here would
|
|
2148
|
+
* let `--tier 0` emit a Tier-0 artifact while the model-backed extensions still ran (and, inversely,
|
|
2149
|
+
* suppress features a `--tier 1` override enabled). The artifact's tier and the features that ran
|
|
2150
|
+
* must always describe the same run.
|
|
2151
|
+
*/
|
|
2152
|
+
declare function classicMutationTier(policy: AttestPolicy, effectiveTier?: 0 | 1 | 2): ClassicMutationTier;
|
|
2153
|
+
/**
|
|
2154
|
+
* The resolved state of source-grounded scenario search after tier gating. It is model-backed and
|
|
2155
|
+
* is forced `disabled-by-tier` at tier 0. Same
|
|
2156
|
+
* `effectiveTier` threading rule as {@link classicMutationTier}: the artifact's tier and the features that
|
|
2157
|
+
* ran must always describe the same run.
|
|
2158
|
+
*/
|
|
2159
|
+
interface ResearchTier {
|
|
2160
|
+
scenarioSearch: "enabled" | "disabled" | "disabled-by-tier";
|
|
2161
|
+
ciPropertySearch: "enabled" | "disabled" | "disabled-by-tier";
|
|
2162
|
+
}
|
|
2163
|
+
declare function researchTier(policy: AttestPolicy, effectiveTier?: 0 | 1 | 2): ResearchTier;
|
|
2164
|
+
/**
|
|
2165
|
+
* Validate a parsed policy document and merge it over defaults. Fail-closed: throws
|
|
2166
|
+
* `invalid <path>: …` on any unknown key, wrong type, or out-of-range value.
|
|
2167
|
+
*/
|
|
2168
|
+
declare function validatePolicy(raw: unknown, path: string): AttestPolicy;
|
|
2169
|
+
/** Load and validate a policy file over defaults. Missing file ⇒ defaults. */
|
|
2170
|
+
declare function loadPolicy(path: string): AttestPolicy;
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* worst-of-packages gate derivation — ONE implementation shared by the CLI's local gate and
|
|
2174
|
+
* the server's recompute (draft.ts hard-rejects any mismatch between them, so two copies would
|
|
2175
|
+
* turn drift into a customer-facing 400 outage).
|
|
2176
|
+
*/
|
|
2177
|
+
|
|
2178
|
+
interface WorstOfRow {
|
|
2179
|
+
directory: string;
|
|
2180
|
+
l0State: "completed" | "cannot-attest" | "not-applicable" | "not-run";
|
|
2181
|
+
l0Counts: ReturnType<typeof countsFromLines> | null;
|
|
2182
|
+
mutationState: "completed" | "cannot-attest" | "not-applicable" | "not-run";
|
|
2183
|
+
mutationCounts: Counts | null;
|
|
2184
|
+
confirmedEquivalent: number;
|
|
2185
|
+
scoreable: boolean;
|
|
2186
|
+
}
|
|
2187
|
+
interface WorstOfOutcome {
|
|
2188
|
+
gate: GateResult;
|
|
2189
|
+
/** null = the pooled gate decided; otherwise the directory of the deciding package. */
|
|
2190
|
+
decisivePackage: string | null;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* overall = worst(pooled combined gate, per-package gates) — the POOLED gate stays a member of
|
|
2194
|
+
* the min so every existing forcing rule (flagged paths, error paths, anti-patterns) survives
|
|
2195
|
+
* (panel C1). Rows without scoreable evidence never vote (panel C2). Per-package Layer-1 score
|
|
2196
|
+
* is measured against the SAME global threshold with the same triage basis as the pooled gate.
|
|
2197
|
+
*/
|
|
2198
|
+
declare function deriveWorstOfGate(input: {
|
|
2199
|
+
rows: readonly WorstOfRow[];
|
|
2200
|
+
pooledGate: GateResult;
|
|
2201
|
+
threshold: number;
|
|
2202
|
+
tier: 0 | 1 | 2;
|
|
2203
|
+
useTriagedBasis: boolean;
|
|
2204
|
+
}): WorstOfOutcome;
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* Linear-time canonical-base64 validation.
|
|
2208
|
+
*
|
|
2209
|
+
* The obvious regular expression for this shape —
|
|
2210
|
+
* `^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$` — is a trap. The grouped
|
|
2211
|
+
* quantifier makes V8 record a backtracking frame per four-character group, so the engine
|
|
2212
|
+
* overflows its stack on long inputs and throws `RangeError: Maximum call stack size exceeded`
|
|
2213
|
+
* instead of returning false. That is not a slow refusal, it is a WRONG one: the caller was
|
|
2214
|
+
* validating untrusted output, the throw escapes the validator, and the surrounding code reports
|
|
2215
|
+
* an internal error rather than the refusal the input earned.
|
|
2216
|
+
*
|
|
2217
|
+
* Measured on ts-pattern (2026-08-04): the fix loop's artifact collector returned a few hundred
|
|
2218
|
+
* kilobytes of legitimate base64 and the regex overflowed, surfacing as
|
|
2219
|
+
* `restricted-environment-unavailable: Maximum call stack size exceeded` — a real repository made
|
|
2220
|
+
* unmeasurable by its own valid output.
|
|
2221
|
+
*
|
|
2222
|
+
* This scan accepts exactly what that expression accepts (length a multiple of four, alphabet
|
|
2223
|
+
* characters only, one or two `=` allowed at the very end) with no backtracking and no recursion,
|
|
2224
|
+
* so the answer depends on input length only through a single pass.
|
|
2225
|
+
*/
|
|
2226
|
+
/** Accepts iff `value` is canonical, correctly padded standard base64 (empty string included). */
|
|
2227
|
+
declare function isCanonicalBase64(value: string): boolean;
|
|
2228
|
+
|
|
2229
|
+
/**
|
|
2230
|
+
* Findings model — the customer-facing "what the suite missed" list. Survivors and
|
|
2231
|
+
* no-coverage mutants become gap findings; triage attaches a verdict + rationale ref.
|
|
2232
|
+
*/
|
|
2233
|
+
|
|
2234
|
+
type TriageVerdict = "likely-equivalent" | "real-gap" | "unclear";
|
|
2235
|
+
/**
|
|
2236
|
+
* Canonical closed reason-code enum (§4.1 egress schema) — the SINGLE source of truth. Both
|
|
2237
|
+
* @abloh/triage (the producer) and @abloh/api (the validator) import this tuple instead of
|
|
2238
|
+
* keeping private mirrors that can silently drift out of lockstep.
|
|
2239
|
+
*/
|
|
2240
|
+
declare const TRIAGE_REASON_CODES: readonly ["EQUIVALENT_NO_OBSERVABLE_EFFECT", "EQUIVALENT_DEAD_BRANCH", "EQUIVALENT_REDUNDANT_CONDITION", "GAP_MISSING_ASSERTION", "GAP_UNCOVERED_BRANCH", "GAP_BOUNDARY_UNTESTED", "GAP_ERROR_PATH_UNTESTED", "UNCLEAR_NEEDS_HUMAN"];
|
|
2241
|
+
type TriageReasonCode = (typeof TRIAGE_REASON_CODES)[number];
|
|
2242
|
+
/**
|
|
2243
|
+
* The reason code a MECHANICALLY forced error-path gap carries (Extension 5). It is assigned by the
|
|
2244
|
+
* engine when a survivor sits inside a changed error handler — never by the LLM — so such a gap is
|
|
2245
|
+
* a real gap by construction and must never be folded into the confirmed-equivalent count.
|
|
2246
|
+
*/
|
|
2247
|
+
declare const MECHANICAL_ERROR_PATH_REASON: TriageReasonCode;
|
|
2248
|
+
/** A model-written description may never exceed this. */
|
|
2249
|
+
declare const MAX_FINDING_DESCRIPTION_LEN = 200;
|
|
2250
|
+
/**
|
|
2251
|
+
* Accept a model-written description, or reject it entirely.
|
|
2252
|
+
*
|
|
2253
|
+
* Returns null when the sentence is unusable, and the caller then falls back to the
|
|
2254
|
+
* mechanical template. Deliberately all-or-nothing: truncating or stripping a leaking
|
|
2255
|
+
* sentence would produce a mangled half-quote that still carries source.
|
|
2256
|
+
*/
|
|
2257
|
+
declare function sanitizeFindingDescription(raw: unknown): string | null;
|
|
2258
|
+
/**
|
|
2259
|
+
* The evidence sentence: what we did, and that nothing objected.
|
|
2260
|
+
*
|
|
2261
|
+
* This is the half that makes the headline credible. "No test covers the boundary case" is a
|
|
2262
|
+
* claim; "we moved a comparison boundary and every test still passed" is the measurement
|
|
2263
|
+
* behind it. Stated as fact — never "if this breaks", because code does not break, people
|
|
2264
|
+
* edit it, and never a consequence we did not observe.
|
|
2265
|
+
*/
|
|
2266
|
+
declare function describeMutationEvidence(mutator: string | null | undefined, status?: string | null, location?: {
|
|
2267
|
+
readonly file?: unknown;
|
|
2268
|
+
readonly startLine?: unknown;
|
|
2269
|
+
} | null): string;
|
|
2270
|
+
/** Gloss for a reason code, or the bare token when it is not one we know. */
|
|
2271
|
+
declare function describeReasonCode(code: string | null | undefined): string | null;
|
|
2272
|
+
/**
|
|
2273
|
+
* The header for one finding: what the test suite failed to check, in one line.
|
|
2274
|
+
*
|
|
2275
|
+
* Deliberately NOT "Surviving mutant: LogicalOperator in <file>" — that is a taxonomy token
|
|
2276
|
+
* pasted into a sentence, and it is what this replaces.
|
|
2277
|
+
*
|
|
2278
|
+
* The classifier's own sentence wins when there is one, because it can name the specific condition
|
|
2279
|
+
* nothing checks in THIS codebase's terms — "refunds above the daily cap are never rejected" says
|
|
2280
|
+
* something a template cannot. The reason-code gloss is the fallback, and it is a fallback: there
|
|
2281
|
+
* are eight of them, so a queue built on glosses alone shows the same eight headers over and over
|
|
2282
|
+
* and a reader cannot tell two findings apart without opening both.
|
|
2283
|
+
*
|
|
2284
|
+
* NO file:line. It used to end in one, which meant every surface that rendered this — the run
|
|
2285
|
+
* page's card, the queue row, the detail panel — printed the location twice, once inside the header
|
|
2286
|
+
* and again in the location field directly beneath it. Where a finding is belongs to the location
|
|
2287
|
+
* field; this sentence is what is wrong.
|
|
2288
|
+
*/
|
|
2289
|
+
declare function describeFinding(finding: {
|
|
2290
|
+
readonly status?: unknown;
|
|
2291
|
+
readonly triage?: {
|
|
2292
|
+
readonly reasonCode?: unknown;
|
|
2293
|
+
readonly description?: unknown;
|
|
2294
|
+
} | null;
|
|
2295
|
+
}): string;
|
|
2296
|
+
interface TriageRecord {
|
|
2297
|
+
verdict: TriageVerdict;
|
|
2298
|
+
/** closed-enum reason code (§4.1 egress schema) */
|
|
2299
|
+
reasonCode: string;
|
|
2300
|
+
confidence: number;
|
|
2301
|
+
/** free-text rationale — archived customer-side, referenced by digest in Tier 1 */
|
|
2302
|
+
rationale?: string;
|
|
2303
|
+
/**
|
|
2304
|
+
* One egress-checked sentence: what the suite failed to check, in the codebase's own
|
|
2305
|
+
* terms. Unlike {@link rationale} this DOES leave the runner — it is bounded, stripped
|
|
2306
|
+
* of anything resembling code by {@link sanitizeFindingDescription}, and advisory: it
|
|
2307
|
+
* never affects verdict, score or gate, and never appears on a certificate.
|
|
2308
|
+
*/
|
|
2309
|
+
description?: string;
|
|
2310
|
+
/**
|
|
2311
|
+
* The consequence half of the same claim: what could change without a test failing.
|
|
2312
|
+
*
|
|
2313
|
+
* A SECOND bounded field rather than a longer {@link description}, because the two fail
|
|
2314
|
+
* independently — a fumbled consequence would otherwise overrun the length cap and take the
|
|
2315
|
+
* condition sentence down with it, which is exactly why p6 forbade explaining consequences at
|
|
2316
|
+
* all. Advisory on the same terms: never affects verdict, score, gate or certificate.
|
|
2317
|
+
*/
|
|
2318
|
+
impact?: string;
|
|
2319
|
+
/** overridden by the customer to non-equivalent? */
|
|
2320
|
+
overridden?: boolean;
|
|
2321
|
+
/**
|
|
2322
|
+
* Reasoning effort the classifier ran at, when one was configured.
|
|
2323
|
+
*
|
|
2324
|
+
* Part of the classifier IDENTITY, not telemetry: the same model and prompt score differently
|
|
2325
|
+
* per effort (measured: gpt-5.6-terra made zero wrong equivalence calls at `high` and two at
|
|
2326
|
+
* `medium`), so the registry pins it — and the server recomputes `triageValidated` from these
|
|
2327
|
+
* records, so without this field a validated run cannot be distinguished from an unvalidated one.
|
|
2328
|
+
*/
|
|
2329
|
+
effort?: string;
|
|
2330
|
+
/**
|
|
2331
|
+
* Assigned mechanically by the engine (Extension 5 error-path forcing), NOT by the LLM. A
|
|
2332
|
+
* mechanical disposition is always a real gap and can never be counted equivalent — see
|
|
2333
|
+
* {@link isConfirmedEquivalent}. It also marks a mutant that must skip LLM equivalence triage.
|
|
2334
|
+
*/
|
|
2335
|
+
mechanical?: boolean;
|
|
2336
|
+
/** model that produced the verdict (traceability) */
|
|
2337
|
+
modelId?: string;
|
|
2338
|
+
/** prompt template version that produced the verdict (traceability) */
|
|
2339
|
+
promptVersion?: string;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Build the mechanical (non-LLM) error-path disposition for a survivor inside a changed handler.
|
|
2343
|
+
* Real gap by construction; can never be confirmed equivalent and is never sent to the LLM.
|
|
2344
|
+
*/
|
|
2345
|
+
declare function mechanicalErrorPathDisposition(): TriageRecord;
|
|
2346
|
+
declare const FINDING_SEVERITIES: readonly ["low", "medium", "high", "critical"];
|
|
2347
|
+
type FindingSeverity = (typeof FINDING_SEVERITIES)[number];
|
|
2348
|
+
declare function findingSeverity(finding: {
|
|
2349
|
+
/** "survived" | "no-coverage" — a no-coverage mutant is one nothing executed */
|
|
2350
|
+
status?: string | null;
|
|
2351
|
+
/** how many tests execute this mutant; null when the engine did not report it */
|
|
2352
|
+
coveredBy?: number | null;
|
|
2353
|
+
triage?: {
|
|
2354
|
+
reasonCode?: unknown;
|
|
2355
|
+
confidence?: unknown;
|
|
2356
|
+
} | null;
|
|
2357
|
+
}): FindingSeverity;
|
|
2358
|
+
/**
|
|
2359
|
+
* What KIND of hole a mutation finding is — the word the work queue groups and filters on.
|
|
2360
|
+
*
|
|
2361
|
+
* This was `status === "no-coverage" ? "uncovered-line" : "surviving-mutant"`: two values for a
|
|
2362
|
+
* vocabulary of seven, so a queue that offered "error handling" as a filter could never return a
|
|
2363
|
+
* row, and the run page's "Error-handling issues" tile linked somewhere permanently empty.
|
|
2364
|
+
*
|
|
2365
|
+
* The reason code is the only thing on a finding that says what the hole IS rather than how it was
|
|
2366
|
+
* observed, so it decides — and only where it makes a claim this vocabulary can express. An
|
|
2367
|
+
* unexercised error path is its own kind of problem and gets its own word; every other reason code
|
|
2368
|
+
* describes an ordinary untested line, which is what the two observational kinds already say
|
|
2369
|
+
* accurately. Inventing a distinct kind per reason code would restate the reason code as a
|
|
2370
|
+
* category and split the queue into slices nobody asked for.
|
|
2371
|
+
*
|
|
2372
|
+
* Deliberately NOT mapped: GAP_MISSING_ASSERTION does not become "asserts-nothing". That kind
|
|
2373
|
+
* names a TEST that asserts nothing at all — a per-test verdict about a whole test — whereas a
|
|
2374
|
+
* missing-assertion gap says the tests covering one line do not check this particular behaviour.
|
|
2375
|
+
* Folding them together would make the filter return two unrelated things under one name.
|
|
2376
|
+
*/
|
|
2377
|
+
declare function findingKind(finding: {
|
|
2378
|
+
status?: string | null;
|
|
2379
|
+
triage?: {
|
|
2380
|
+
reasonCode?: unknown;
|
|
2381
|
+
} | null;
|
|
2382
|
+
}): "uncovered-line" | "surviving-mutant" | "error-handler";
|
|
2383
|
+
interface GapFinding {
|
|
2384
|
+
mutantId: string;
|
|
2385
|
+
file: string;
|
|
2386
|
+
startLine: number;
|
|
2387
|
+
endLine: number;
|
|
2388
|
+
mutator: string;
|
|
2389
|
+
replacement: string | null;
|
|
2390
|
+
status: "survived" | "no-coverage";
|
|
2391
|
+
coveredBy: number;
|
|
2392
|
+
triage?: TriageRecord;
|
|
2393
|
+
/**
|
|
2394
|
+
* Exact 1-based columns of the mutated span, and the source slice it replaced.
|
|
2395
|
+
*
|
|
2396
|
+
* TIER-2 ONLY, like `replacement` beside them. `FINDING_ALLOWED_FIELDS` in `apps/api/src/draft.ts`
|
|
2397
|
+
* is a closed allowlist of seven structural names and lists none of these, so at tier 0 and tier 1
|
|
2398
|
+
* the sanitizer drops them at the trust boundary and no signed digest can move. Tier 2 is the
|
|
2399
|
+
* customer's explicit choice to let Abloh hold the evidence itself, and admits exactly these four
|
|
2400
|
+
* through `TIER2_ADDITIONAL_FINDING_FIELDS` — see the note there for what still never egresses.
|
|
2401
|
+
*
|
|
2402
|
+
* They exist because a LINE cannot locate an AST node, and finding clustering needs to know
|
|
2403
|
+
* which expression a mutant sits in — the difference between "two operands of one guard" and
|
|
2404
|
+
* "two unrelated statements that share a line". `originalText` is what makes that location
|
|
2405
|
+
* VERIFIABLE rather than assumed: the engine report's column base cannot be confirmed from the
|
|
2406
|
+
* report itself, so the recorded slice is the oracle and a mismatch means refuse, not guess.
|
|
2407
|
+
*/
|
|
2408
|
+
startColumn?: number;
|
|
2409
|
+
endColumn?: number;
|
|
2410
|
+
originalText?: string;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* A survivor is a real gap unless triage confirms it equivalent and it isn't overridden. A
|
|
2414
|
+
* mechanical disposition (error-path forcing) is a real gap by construction and can never be
|
|
2415
|
+
* counted equivalent, even if some caller mislabels its verdict.
|
|
2416
|
+
*/
|
|
2417
|
+
declare function isConfirmedEquivalent(t: TriageRecord | undefined): boolean;
|
|
2418
|
+
/** Strip the free-text rationale before any egress — it stays archived customer-side. */
|
|
2419
|
+
declare function redactTriageForEgress(t: TriageRecord): Omit<TriageRecord, "rationale">;
|
|
2420
|
+
interface LineGroup {
|
|
2421
|
+
/** the first member's mutant id — anchors the existing `finding=<id>` deep-link contract */
|
|
2422
|
+
first: string;
|
|
2423
|
+
file: string;
|
|
2424
|
+
startLine: number;
|
|
2425
|
+
endLine: number;
|
|
2426
|
+
/** displayable mutator tokens, in first-seen order */
|
|
2427
|
+
mutators: string[];
|
|
2428
|
+
/** how many mutants this group represents — load-bearing, never decoration */
|
|
2429
|
+
count: number;
|
|
2430
|
+
/**
|
|
2431
|
+
* Triage reason codes on this line, tallied, most common first.
|
|
2432
|
+
*
|
|
2433
|
+
* This is what makes a grouped annotation say what the SUITE failed to check rather than
|
|
2434
|
+
* only what we changed. It needs no model call: each code was already decided per mutant
|
|
2435
|
+
* during triage, so this is arithmetic over labels that exist.
|
|
2436
|
+
*/
|
|
2437
|
+
/**
|
|
2438
|
+
* Triage reasons on this line, tallied, most common first — each carrying one model
|
|
2439
|
+
* sentence drawn from its OWN members.
|
|
2440
|
+
*
|
|
2441
|
+
* Per reason rather than per line: mutants sharing a reason code are mutations of the same
|
|
2442
|
+
* behaviour, so their sentences say near-identical things and picking one is close to free
|
|
2443
|
+
* of arbitrariness. Picking one for a whole 55-mutant line is not — it presents one
|
|
2444
|
+
* member's opinion as the summary of everything on that line.
|
|
2445
|
+
*/
|
|
2446
|
+
reasons: {
|
|
2447
|
+
code: string;
|
|
2448
|
+
count: number;
|
|
2449
|
+
description?: string;
|
|
2450
|
+
}[];
|
|
2451
|
+
/**
|
|
2452
|
+
* The model's own sentence, present ONLY when this group has exactly one member — the
|
|
2453
|
+
* unambiguous case, where it is simply the best text available.
|
|
2454
|
+
*/
|
|
2455
|
+
description?: string;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Group survivors by source LINE.
|
|
2459
|
+
*
|
|
2460
|
+
* This is the rule the HOSTED surfaces use, and it lives here because the control plane cannot use
|
|
2461
|
+
* anything better: grouping by what a mutant actually breaks needs the syntax tree, and source
|
|
2462
|
+
* never reaches Abloh — everything that touches source runs in the customer's CI. Customer-side,
|
|
2463
|
+
* `@abloh/engine-stryker`'s `clusterMutants` does the real thing and calls this as its `line`
|
|
2464
|
+
* strategy.
|
|
2465
|
+
*
|
|
2466
|
+
* It lives in ONE place because it had already drifted into three. `structuralFindings`
|
|
2467
|
+
* (`apps/api/src/github-delivery.ts`) grouped correctly; `buildResolvedEvidence`
|
|
2468
|
+
* (`apps/api/src/v1-live.ts`) emitted one finding per mutant and so re-created the exact defect
|
|
2469
|
+
* grouping was introduced to remove — the live Cosmos delivery stacked six byte-identical
|
|
2470
|
+
* "Surviving mutant" annotations on one line, six of something and nothing about what. The App
|
|
2471
|
+
* documents that it accepts `count`/`mutators` precisely so that cannot happen; one of the two
|
|
2472
|
+
* senders simply never set them.
|
|
2473
|
+
*
|
|
2474
|
+
* Confirmed-equivalent survivors are excluded by the CALLER, not here: the verdict shape differs
|
|
2475
|
+
* between the artifact and the run record, and folding that in would give this function a second
|
|
2476
|
+
* job it could get wrong quietly.
|
|
2477
|
+
*/
|
|
2478
|
+
declare function groupFindingsByLine(findings: readonly {
|
|
2479
|
+
mutantId?: unknown;
|
|
2480
|
+
file?: unknown;
|
|
2481
|
+
startLine?: unknown;
|
|
2482
|
+
endLine?: unknown;
|
|
2483
|
+
mutator?: unknown;
|
|
2484
|
+
triage?: {
|
|
2485
|
+
reasonCode?: unknown;
|
|
2486
|
+
description?: unknown;
|
|
2487
|
+
} | null;
|
|
2488
|
+
}[]): LineGroup[];
|
|
2489
|
+
declare function toGapFindings(mutants: NormalizedMutant[]): GapFinding[];
|
|
2490
|
+
|
|
2491
|
+
/**
|
|
2492
|
+
* A NAME FOR A GAP THAT OUTLIVES THE RUN THAT FOUND IT.
|
|
2493
|
+
*
|
|
2494
|
+
* Every existing identifier for a mutant is deliberately run-scoped. `NormalizedMutant.id` says so
|
|
2495
|
+
* on its own doc comment — unique within a run and nothing more. {@link mutantIdentity} is stable
|
|
2496
|
+
* across runs but only at the same commit: it includes the start and end lines, because the caches
|
|
2497
|
+
* it was built for compare two measurements of identical source and must not collide two mutants
|
|
2498
|
+
* that differ only in span.
|
|
2499
|
+
*
|
|
2500
|
+
* A ledger needs the opposite trade. It follows one gap across commits, and the commonest thing a
|
|
2501
|
+
* developer does between two runs is move the code: add an import, extract a helper, reformat. All
|
|
2502
|
+
* of that shifts every line below it, and a key containing a line number renames every gap in the
|
|
2503
|
+
* file — so the ledger would close them all as fixed and open a fresh set, on a commit that fixed
|
|
2504
|
+
* nothing.
|
|
2505
|
+
*
|
|
2506
|
+
* WHAT IS AVAILABLE. Not much, on purpose. Below tier 2 a finding egresses only `file`, `mutator`,
|
|
2507
|
+
* `status`, `coveredBy` and the line span — the mutated source text stays on the customer's runner,
|
|
2508
|
+
* which is the whole point of the boundary. So the key is built from the two fields that survive an
|
|
2509
|
+
* edit (`file`, `mutator`) plus an ORDINAL: which same-kind gap in that file this is, counting down
|
|
2510
|
+
* the file.
|
|
2511
|
+
*
|
|
2512
|
+
* WHAT THAT COSTS. The ordinal is stable while the set of same-mutator gaps in a file keeps its
|
|
2513
|
+
* order and size, which covers the case the ledger exists for — a developer writes a test and the
|
|
2514
|
+
* source does not move at all. It is NOT stable when a new gap of the same kind appears earlier in
|
|
2515
|
+
* the same file: everything below it shifts by one, and the ledger reads that as the old rows
|
|
2516
|
+
* closing and new ones opening. That is a real limitation and it is bounded — one file, one mutator
|
|
2517
|
+
* kind — and it fails toward noticing too much rather than too little, which is the safe direction
|
|
2518
|
+
* for a list of things nobody has tested.
|
|
2519
|
+
*/
|
|
2520
|
+
/** The fields a gap key is built from. Deliberately the tier-0 subset — no source text. */
|
|
2521
|
+
interface GapKeyInput {
|
|
2522
|
+
file: string;
|
|
2523
|
+
mutator: string;
|
|
2524
|
+
startLine: number;
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Stable keys for one run's gaps, in the order the gaps were given.
|
|
2528
|
+
*
|
|
2529
|
+
* Ordinals are assigned per (file, mutator) by line order, NOT by the order the engine reported —
|
|
2530
|
+
* a report's order depends on which mutants were sampled into the run, so keying on it would make
|
|
2531
|
+
* a gap's name depend on its siblings.
|
|
2532
|
+
*/
|
|
2533
|
+
declare function gapKeys(gaps: readonly GapKeyInput[]): string[];
|
|
2534
|
+
|
|
2535
|
+
/**
|
|
2536
|
+
* Operator severity classification — "operator visibility" (Move 1).
|
|
2537
|
+
*
|
|
2538
|
+
* Every mutant carries a `mutator` name (Stryker's `mutatorName`, or one of our own
|
|
2539
|
+
* custom-operator names). This maps that name to a SEVERITY CLASS so a survivor's
|
|
2540
|
+
* significance is visible in the report: a surviving BlockStatement deletion ("a whole
|
|
2541
|
+
* code block could be removed and no test noticed") is far more alarming than a surviving
|
|
2542
|
+
* StringLiteral flip. Pure, deterministic, no I/O — safe to run anywhere, and it is
|
|
2543
|
+
* PRESENTATIONAL ONLY: it never enters the normalized schema, the artifact, or the signed
|
|
2544
|
+
* certificate, so it cannot affect scores or the two-layer evidence contract.
|
|
2545
|
+
*/
|
|
2546
|
+
type OperatorClass = "structural" | "operator" | "custom";
|
|
2547
|
+
/**
|
|
2548
|
+
* Classify a mutator name into a severity class. Unknown names fall back to "operator":
|
|
2549
|
+
* Stryker's remaining defaults (Arithmetic/Boolean/String/Unary/Update/Equality/Logical/
|
|
2550
|
+
* Regex/Assignment) are all light operator/value flips, so "operator" is the safe default.
|
|
2551
|
+
*/
|
|
2552
|
+
declare function operatorClass(mutator: string): OperatorClass;
|
|
2553
|
+
/** Tally mutator names by severity class (deterministic key order). */
|
|
2554
|
+
declare function operatorClassBreakdown(mutators: string[]): Record<OperatorClass, number>;
|
|
2555
|
+
/**
|
|
2556
|
+
* A short human phrase for a gap-severity breakdown, e.g. "2 structural, 1 operator".
|
|
2557
|
+
* Omits zero classes; returns "none" for an all-zero breakdown. Order: structural first
|
|
2558
|
+
* (the alarming ones lead), then custom, then operator.
|
|
2559
|
+
*/
|
|
2560
|
+
declare function operatorClassSummary(mutators: string[]): string;
|
|
2561
|
+
|
|
2562
|
+
/**
|
|
2563
|
+
* Shared Layer-1 gate overrides (Extension 5 — opted-in error-path enforcement).
|
|
2564
|
+
*
|
|
2565
|
+
* This runs identically in the CLI (which computes the gate from raw results) and in the API
|
|
2566
|
+
* (which recomputes the gate server-side from submitted evidence), so both reach the same verdict.
|
|
2567
|
+
*
|
|
2568
|
+
* Rule: an opted-in error-path finding may turn a PASS into a FAIL, but must NEVER replace an
|
|
2569
|
+
* existing `cannot-attest`. Insufficient evidence (cannot-attest) outranks a policy fail — a caller
|
|
2570
|
+
* must not be able to convert "we could not attest" into a plain "fail" (or vice-versa) by toggling
|
|
2571
|
+
* an error-path flag.
|
|
2572
|
+
*/
|
|
2573
|
+
|
|
2574
|
+
interface Layer1GateInput {
|
|
2575
|
+
/** the mutation gate already computed (pass | fail | cannot-attest) */
|
|
2576
|
+
baseGate: GateResult;
|
|
2577
|
+
/** opted-in: a changed handler with an untested (survived/no-coverage) mutant fails the gate */
|
|
2578
|
+
failOnUntested: boolean;
|
|
2579
|
+
/** opted-in: any detected handler anti-pattern fails the gate */
|
|
2580
|
+
failOnAntiPattern: boolean;
|
|
2581
|
+
/** count of untested changed-handler mutants (survived/no-coverage inside a changed handler) */
|
|
2582
|
+
untestedHandlerMutantCount: number;
|
|
2583
|
+
/**
|
|
2584
|
+
* Number of detected handler anti-patterns (empty-catch / catch-all-abort / todo-in-handler).
|
|
2585
|
+
* A COUNT, not the findings themselves, so both callers can satisfy this contract identically:
|
|
2586
|
+
* the CLI holds the full findings locally, while the API only ever sees the sanitized aggregate
|
|
2587
|
+
* (anti-pattern findings egress as counts by kind). The rule needs nothing more than the count.
|
|
2588
|
+
*/
|
|
2589
|
+
antiPatternCount: number;
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Apply the opted-in error-path gate overrides on top of the base mutation gate.
|
|
2593
|
+
* `cannot-attest` is terminal and returned unchanged; otherwise, if an enabled rule matches, the
|
|
2594
|
+
* gate becomes `fail` with an appended reason. When no rule matches, the base gate is returned
|
|
2595
|
+
* verbatim (object identity preserved) so callers can detect "no override applied".
|
|
2596
|
+
*/
|
|
2597
|
+
declare function applyLayer1Gate(input: Layer1GateInput): GateResult;
|
|
2598
|
+
|
|
2599
|
+
type TestVerdictKind = "asserts-nothing" | "duplicate-effectiveness";
|
|
2600
|
+
interface TestVerdict {
|
|
2601
|
+
/** canonical `<file>::<fullName>` — the NAME stays local; egress carries only its digest */
|
|
2602
|
+
test: string;
|
|
2603
|
+
kind: TestVerdictKind;
|
|
2604
|
+
coveredCount: number;
|
|
2605
|
+
creditedKillCount: number;
|
|
2606
|
+
/** stable digest of the duplicate group (duplicate-effectiveness only) */
|
|
2607
|
+
duplicateGroupDigest?: string;
|
|
2608
|
+
}
|
|
2609
|
+
interface TestAnalysisInput {
|
|
2610
|
+
/** post-quarantine mutants carrying canonical coveredByTests/killedByTests + final status */
|
|
2611
|
+
mutants: readonly NormalizedMutant[];
|
|
2612
|
+
/** canonical identities of tests the §4.9 baseline judged flaky */
|
|
2613
|
+
flakyTests: ReadonlySet<string>;
|
|
2614
|
+
/**
|
|
2615
|
+
* Whether killer/coverage attribution was fully resolvable (the report carried a complete test
|
|
2616
|
+
* dictionary and no ambiguous ids). When false, zero-credit covering tests are `unassessed`.
|
|
2617
|
+
*/
|
|
2618
|
+
attributionComplete: boolean;
|
|
2619
|
+
}
|
|
2620
|
+
interface TestAnalysisResult {
|
|
2621
|
+
verdicts: TestVerdict[];
|
|
2622
|
+
/** covering tests that could not be assessed because attribution was incomplete */
|
|
2623
|
+
unassessed: string[];
|
|
2624
|
+
attribution: "complete" | "partial";
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Analyze per-test effectiveness. Pure: same input → same output, sorted deterministically.
|
|
2628
|
+
*/
|
|
2629
|
+
declare function analyzeTestFindings(input: TestAnalysisInput): TestAnalysisResult;
|
|
2630
|
+
/** sha256 of a canonical test identity — the egress-safe reference (the name itself stays local). */
|
|
2631
|
+
declare function testIdentityDigest(canonical: string): string;
|
|
2632
|
+
|
|
2633
|
+
/** Closed, policy-owned vocabulary for CI property obligations. */
|
|
2634
|
+
declare const CI_PROPERTY_FAMILIES: readonly ["round-trip", "idempotence", "order-preservation", "monotonicity", "normalization-equivalence", "partition-recombine", "serialization-preservation", "acceptance-boundary", "state-transition", "error-containment", "recovery-continuity", "non-mutation", "cache-coherence", "representation-boundary", "resource-lifecycle", "conservation"];
|
|
2635
|
+
type CiPropertyFamily = (typeof CI_PROPERTY_FAMILIES)[number];
|
|
2636
|
+
declare const CI_OBLIGATION_SIGNAL_AUTHORITIES: readonly ["explicit-contract", "mechanical-risk"];
|
|
2637
|
+
type CiObligationSignalAuthority = (typeof CI_OBLIGATION_SIGNAL_AUTHORITIES)[number];
|
|
2638
|
+
/**
|
|
2639
|
+
* Deterministic source analysis may point the model at a risk, but never grants that risk source
|
|
2640
|
+
* authority. A rule still has to cite behaviour-specific, pre-cutoff evidence.
|
|
2641
|
+
*/
|
|
2642
|
+
interface CiObligationSignal {
|
|
2643
|
+
family: CiPropertyFamily;
|
|
2644
|
+
trigger: string;
|
|
2645
|
+
authority: CiObligationSignalAuthority;
|
|
2646
|
+
path: string;
|
|
2647
|
+
startLine: number;
|
|
2648
|
+
endLine: number;
|
|
2649
|
+
targetSymbols: string[];
|
|
2650
|
+
searchTerms: string[];
|
|
2651
|
+
strength: 1 | 2 | 3;
|
|
2652
|
+
/**
|
|
2653
|
+
* Which mutation operator survived, for a signal derived from a surviving mutant. The operator
|
|
2654
|
+
* NAME is engine vocabulary; the mutated and original expressions are deliberately absent,
|
|
2655
|
+
* because both are source text from the revision under review. Carrying them in prose — which
|
|
2656
|
+
* this signal did until 2026-08-06 — handed the model the code it is supposed to be judging
|
|
2657
|
+
* without ever appearing in a type.
|
|
2658
|
+
*/
|
|
2659
|
+
mutator?: string;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
/**
|
|
2663
|
+
* Turn surviving mutants into obligation signals the intent stage can actually read.
|
|
2664
|
+
*
|
|
2665
|
+
* The intent prompt used to receive `openGapKinds` — a deduped list of kind strings such as
|
|
2666
|
+
* `["classic"]` — while the ledger held, unhashed and in memory, the surviving mutant's operator,
|
|
2667
|
+
* original text, replacement, and exact line. A surviving mutant is the one input in the whole
|
|
2668
|
+
* system that is a PROVEN-undetected behaviour delta: the repository's own suite demonstrably
|
|
2669
|
+
* cannot see it, so a property aimed at it cannot be vacuous about the suite the way a
|
|
2670
|
+
* documentation-shaped property can. Reducing it to a kind string threw away precisely the part
|
|
2671
|
+
* a rule could be aimed with.
|
|
2672
|
+
*
|
|
2673
|
+
* Authority is untouched by construction: these ride the existing `mechanical-risk` channel,
|
|
2674
|
+
* whose prompt contract already states that risk signals direct WHERE to search and never
|
|
2675
|
+
* establish expected behaviour — a rule still has to cite behaviour-specific, pre-cutoff
|
|
2676
|
+
* evidence. Strength mirrors the operator-severity classes: a surviving structural or custom
|
|
2677
|
+
* operator (a deleted block, a removed guard) outranks a surviving value flip.
|
|
2678
|
+
*/
|
|
2679
|
+
declare function survivingMutantObligationSignals(entries: readonly CiResidualGapEntry[]): CiObligationSignal[];
|
|
2680
|
+
declare function ciPropertyMutatorFamilies(mutantIdentity: string | undefined): readonly CiPropertyFamily[];
|
|
2681
|
+
interface CiPropertyTargetCandidate {
|
|
2682
|
+
targetDigest: string;
|
|
2683
|
+
path: string;
|
|
2684
|
+
symbol: string;
|
|
2685
|
+
changeKind: "addition" | "deletion" | "replacement";
|
|
2686
|
+
runnerSupported: boolean;
|
|
2687
|
+
hasIndependentEvidence: boolean;
|
|
2688
|
+
publiclyReachable: boolean;
|
|
2689
|
+
excludedChange: boolean;
|
|
2690
|
+
generatedOrBinary: boolean;
|
|
2691
|
+
requiresForbiddenExternalResource: boolean;
|
|
2692
|
+
sourceConflict: boolean;
|
|
2693
|
+
allKnownLayer1GapsClosed: boolean;
|
|
2694
|
+
sourceIntroducesDifferentRule: boolean;
|
|
2695
|
+
openLayer1Residuals: number;
|
|
2696
|
+
mutationSiteAbsent: boolean;
|
|
2697
|
+
changedBranchHasDirectTest: boolean;
|
|
2698
|
+
supportedPropertyFamilies: number;
|
|
2699
|
+
estimatedSetupCost: number;
|
|
2700
|
+
}
|
|
2701
|
+
interface CiPropertyTargetDecision {
|
|
2702
|
+
target: CiPropertyTargetCandidate;
|
|
2703
|
+
selected: boolean;
|
|
2704
|
+
reasons: string[];
|
|
2705
|
+
}
|
|
2706
|
+
interface CiPropertyTargetPlan {
|
|
2707
|
+
selected: CiPropertyTargetCandidate[];
|
|
2708
|
+
decisions: CiPropertyTargetDecision[];
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Union Layer 1 and independently sourced targets before applying the fixed CI budget. A source
|
|
2712
|
+
* target may refine an existing Layer 1 target, but neither target family can erase the other.
|
|
2713
|
+
*/
|
|
2714
|
+
declare function buildCiPropertyTargetUniverse(input: {
|
|
2715
|
+
layer1Targets: readonly CiPropertyTargetCandidate[];
|
|
2716
|
+
sourceTargets: readonly CiPropertyTargetCandidate[];
|
|
2717
|
+
}): CiPropertyTargetCandidate[];
|
|
2718
|
+
/** Deterministic, bounded selector. It receives no fault label or model score. */
|
|
2719
|
+
declare function selectCiPropertyTargets(input: readonly CiPropertyTargetCandidate[], maxTargets?: number): CiPropertyTargetPlan;
|
|
2720
|
+
interface CiPropertyRuleCandidate {
|
|
2721
|
+
intentDigest: string;
|
|
2722
|
+
authority: CiPropertyAuthority;
|
|
2723
|
+
targetDigest: string;
|
|
2724
|
+
propertyFamily: string;
|
|
2725
|
+
operationSequence: string[];
|
|
2726
|
+
observation: {
|
|
2727
|
+
kind: string;
|
|
2728
|
+
subject: string;
|
|
2729
|
+
expected: string;
|
|
2730
|
+
};
|
|
2731
|
+
inputDomainKinds: string[];
|
|
2732
|
+
sourceSetDigest: string;
|
|
2733
|
+
openLayer1Match: "exact" | "indirect" | "none";
|
|
2734
|
+
templateSupported: boolean;
|
|
2735
|
+
setupDependencyCount: number;
|
|
2736
|
+
/** Behaviour-specific frozen evidence, not merely target-level evidence. */
|
|
2737
|
+
evidenceSpecificity: 0 | 1 | 2 | 3;
|
|
2738
|
+
/** Strength of a deterministic generic risk trigger matching this family. */
|
|
2739
|
+
riskTriggerStrength: 0 | 1 | 2 | 3;
|
|
2740
|
+
/** Confidence that schemas, types, examples, or fixtures constrain generated inputs. */
|
|
2741
|
+
inputValidityConfidence: 0 | 1 | 2 | 3;
|
|
2742
|
+
/** Order supplied by the model before deterministic policy ranking. */
|
|
2743
|
+
modelPriority: number;
|
|
2744
|
+
}
|
|
2745
|
+
interface RankedCiPropertyRule extends CiPropertyRuleCandidate {
|
|
2746
|
+
behaviourKey: string;
|
|
2747
|
+
}
|
|
2748
|
+
interface CiPropertyRuleDrop {
|
|
2749
|
+
intentDigest: string;
|
|
2750
|
+
reason: "duplicate-intent" | "duplicate-behaviour" | "closed-classicMutation" | "pool-cap";
|
|
2751
|
+
}
|
|
2752
|
+
declare function ciPropertyBehaviourKey(rule: Pick<CiPropertyRuleCandidate, "targetDigest" | "propertyFamily" | "operationSequence" | "observation" | "inputDomainKinds" | "sourceSetDigest">): string;
|
|
2753
|
+
declare function rankCiPropertyRules(input: readonly CiPropertyRuleCandidate[], options?: {
|
|
2754
|
+
closedBehaviourKeys?: ReadonlySet<string>;
|
|
2755
|
+
poolSize?: number;
|
|
2756
|
+
}): {
|
|
2757
|
+
ranked: RankedCiPropertyRule[];
|
|
2758
|
+
dropped: CiPropertyRuleDrop[];
|
|
2759
|
+
};
|
|
2760
|
+
|
|
2761
|
+
/**
|
|
2762
|
+
* Environment boundary between Abloh's control plane and untrusted repository code.
|
|
2763
|
+
*
|
|
2764
|
+
* Customer tests receive only the small portable runtime base plus variables explicitly named in
|
|
2765
|
+
* committed policy. Engine credentials are reserved and can never be re-admitted by policy.
|
|
2766
|
+
*/
|
|
2767
|
+
declare function isReservedCustomerVariable(name: string): boolean;
|
|
2768
|
+
declare function customerEnvironmentNames(required: readonly string[]): string[];
|
|
2769
|
+
declare function buildCustomerProcessEnvironment(source?: NodeJS.ProcessEnv, required?: readonly string[], overrides?: Readonly<Record<string, string>>): Record<string, string>;
|
|
2770
|
+
declare function containsEngineCredential(environment: Readonly<Record<string, string | undefined>>): boolean;
|
|
2771
|
+
|
|
2772
|
+
export { type AssayResult, type AssayVerdict, type AttestPolicy, type AttestResultsV2, type AttributionQuality, type BuildCiPropertyBlockInput, type BuildScenarioSearchBlockInput, CI_OBLIGATION_SIGNAL_AUTHORITIES, CI_PROPERTY_AUTHORITIES, CI_PROPERTY_CAPS, CI_PROPERTY_FAMILIES, CI_PROPERTY_OUTCOMES, CI_PROPERTY_STATES, CI_RESIDUAL_STATES, CLUSTER_STRATEGIES, type CellObservation, type ChangedStructuralCounts, type CiChangedRange, type CiObligationSignal, type CiObligationSignalAuthority, type CiPropertyAuthority, type CiPropertyCandidateCap, type CiPropertyCounts, type CiPropertyFamily, type CiPropertyMeasuredBlock, type CiPropertyOutcome, type CiPropertyRuleCandidate, type CiPropertyRuleDrop, type CiPropertySearchBlock, type CiPropertySearchPolicy, type CiPropertyState, type CiPropertySummary, type CiPropertyTargetCandidate, type CiPropertyTargetDecision, type CiPropertyTargetPlan, type CiResidualGapEntry, type CiResidualGapLedger, type CiResidualState, type ClassicMutationPolicy, type ClassicMutationTier, type ClassicSamplingBlock, type ClusterStrategy, type CombinedGate, type ConditionalPowerResult, type Counts, type CoverageProvider, DEFAULT_CLASSIC_MUTATION, DEFAULT_DIFF_COVERAGE, DEFAULT_ENVIRONMENT, DEFAULT_ENVIRONMENT_IMAGE_DIGEST, DEFAULT_ENVIRONMENT_IMAGE_REF, DEFAULT_FINDINGS, DEFAULT_FLOOR, DEFAULT_MECHANISMS, DEFAULT_MUTATION, DEFAULT_POLICY, DEFAULT_SANDBOX, DIFF_COVERAGE_THRESHOLD, type DecisiveLayer, type DetectionCell, type DeterministicMutantsBlock, type DeterministicMutantsPolicy, type DiffCoverageCounts, type DiffCoverageLine, type DiffCoverageLineState, type DiffCoverageNotApplicableReason, type DiffCoverageNotRunReason, type DiffCoveragePolicy, type DiffCoverageResult, DiffScopeError, EXCLUDE_RE, EXPECTED_ORACLE_CELL, type EnvironmentPolicy, type ErrorHandlersBlock, type ErrorPathsPolicy, type EvidenceFloor, type EvidenceProfile, type ExecutionAxis, type ExperimentManifest, FINDING_SEVERITIES, type FeatureState, type FileScope, type FileStructural, type FindingSeverity, type FindingsPolicy, type FixLoopBlock, type FixLoopPolicy, type FixProofSummary, type FixProofVerdict, type FlakyPolicy, type FloorPolicy, type GapFinding, type GapKeyInput, type GateResult, type GateStatus, HANDOFF_EVIDENCE_KEYS_V1, HANDOFF_EVIDENCE_KEYS_V2, HANDOFF_TARGET_KEYS, type HandlerAntiPatternKind, type HandlerFinding, type HeadroomResult, type HeadroomVerdict, type HistoricalPair, LAYER0_EXECUTION, LAYER0_SCOPE_DISCLOSURE, LAYER1_EXECUTION, type Layer1GateInput, type LineGroup, MAX_EXPANDED_LINES, MAX_FINDING_DESCRIPTION_LEN, MAX_LITERAL_ARGUMENTS, MAX_LITERAL_COMMAND_BYTES, MAX_MUTANT_ROSTER, MAX_SCOPE_RANGES, MECHANICAL_ERROR_PATH_REASON, MIN_CELL_REPEATS, MUTANT_STATUSES, MUTATION_SCOPE_SURFACE, type ManifestModel, type ManifestValidation, type ModelChoicePolicy, type ModelsPolicy, type MutantRosterEntry, type MutantStatus, type MutationExecution, type MutationMode, type MutationNotRunReason, type MutationPolicy, type MutationScopeKind, type MutationSkipReason, type NormalizedCoverage, type NormalizedMutant, type OperatorClass, type OracleObservation, PROVIDER_RAW_FORMAT, PYTHON_EXCLUDE_RE, PYTHON_SOURCE_RE, type PackageEvidence, type PairIsolation, type PatchRevertPolicy, type PerTestBlock, type PerTestFinding, type PerTestFindingKind, type PerTestPolicy, type ProposedMutant, type QualificationResult, type QualificationVerdict, type QualifiedOmission, REALISTIC_CATEGORIES, RUNNER_CAPABILITIES, type RankedCiPropertyRule, type RawCoverageFormat, type RawCoverageFormatName, type RealisticCategory, type ResearchPolicy, type ResearchTier, type RunnerCapability, type RunnerEcosystem, SCENARIO_AUTHORITIES, SCENARIO_LANES, SCENARIO_OUTCOMES, SCENARIO_SEARCH_STATES, SCENARIO_SOURCE_INDEPENDENCE, SCENARIO_SOURCE_KINDS, SOURCE_RE, type SandboxPolicy, type ScenarioAuthority, type ScenarioExpectedObservation, type ScenarioInputDomain, type ScenarioIntent, type ScenarioIntentDraft, type ScenarioLane, type ScenarioOperationStep, type ScenarioOutcome, type ScenarioSearchBlock, type ScenarioSearchCounts, type ScenarioSearchMeasuredBlock, type ScenarioSearchPolicy, type ScenarioSearchProvenance, type ScenarioSearchRestrictions, type ScenarioSearchState, type ScenarioSourceIndependence, type ScenarioSourceKind, type ScenarioSourceRef, type ScenarioSummary, type ScopeOptions, type ScopeResult, type ScoreInput, type Scores, type StageBInputs, type StageBReadiness, type StructuralCoverage, type StructuralSite, type SuiteOutcome, TARGET_ARTIFACT_KEYS, TARGET_SELECTION_KINDS, TRIAGE_REASON_CODES, type TargetSelectionKind, type TestAnalysisInput, type TestAnalysisResult, type TestVerdict, type TestVerdictKind, type TriageReasonCode, type TriageRecord, type TriageVerdict, type WorstOfOutcome, type WorstOfRow, analyzeTestFindings, applyLayer1Gate, assertNever, buildCiPropertySearchBlock, buildCiPropertyTargetUniverse, buildCustomerProcessEnvironment, buildScenarioSearchBlock, canStartStageB, capability, changedStructuralCoverage, ciPropertyBehaviourKey, ciPropertyLedgerDigest, ciPropertyMutatorFamilies, classicHeadroom, classicMutationExecution, classicMutationTier, classifyDiffCoverage, combineLayerGates, computeScope, computeScores, conditionalPower, containsEngineCredential, countsFromLines, coveredLinesToScope, customerEnvironmentNames, denominatorOf, deriveScenarioSearchCounts, deriveWorstOfGate, describeFinding, describeMutationEvidence, describeReasonCode, diffCoverageExecution, emptyCounts, errorCountOf, evaluateAssay, evaluateDiffCoverageGate, evaluateFloor, evaluateGate, expandScopeLines, findingKind, findingSeverity, gapKeys, groupFindingsByLine, isCanonicalBase64, isCiPropertyCandidateCap, isClusterStrategy, isConfirmedEquivalent, isNonExecutableLine, isReservedCustomerVariable, isScenarioAuthority, isScenarioLane, isScenarioOutcome, isScenarioSearchState, jsRunners, knownRunner, layer0EnabledRunners, loadPolicy, markNonExecutable, mechanicalErrorPathDisposition, missingScopedFiles, mutantIdentity, operatorClass, operatorClassBreakdown, operatorClassSummary, parseLiteralArguments, parsePreparedTestCommand, parseUnifiedDiff, qualifyCorpus, qualifyHistoricalPair, rankCiPropertyRules, recognizedProvidersFor, redactTriageForEgress, researchTier, sanitizeCiPropertySummary, sanitizeFindingDescription, sanitizeScenarioSummary, selectCiPropertyTargets, stableOutcome, strykerPluginRunners, survivingMutantObligationSignals, tallyCounts, testIdentityDigest, toGapFindings, uncoveredCount, validateCiResidualGapLedger, validateDiffCoverageLines, validateManifest, validatePolicy, validatePreparedTestCommand, wilsonInterval };
|