@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/dist/index.js ADDED
@@ -0,0 +1,4288 @@
1
+ // src/schema.ts
2
+ var MUTANT_STATUSES = [
3
+ "killed",
4
+ "timeout",
5
+ "survived",
6
+ "no-coverage",
7
+ "runtime-error",
8
+ "build-error",
9
+ "skipped-by-cap"
10
+ ];
11
+ function emptyCounts() {
12
+ return {
13
+ killed: 0,
14
+ timeout: 0,
15
+ survived: 0,
16
+ "no-coverage": 0,
17
+ "runtime-error": 0,
18
+ "build-error": 0,
19
+ "skipped-by-cap": 0
20
+ };
21
+ }
22
+ function mutantIdentity(m) {
23
+ return [
24
+ m.file,
25
+ m.startLine,
26
+ m.endLine,
27
+ m.startColumn ?? "",
28
+ m.endColumn ?? "",
29
+ m.mutator,
30
+ m.originalText ?? "",
31
+ m.replacement ?? ""
32
+ ].join("\0");
33
+ }
34
+ var MUTATION_SCOPE_SURFACE = {
35
+ original: "full",
36
+ "covered-only": "covered-only",
37
+ // mutation ran on the covered subset PLUS forced error-handler mutants: still not the full diff,
38
+ // and the honest label for a reader deciding how much the score covers is the reduced one
39
+ "covered-plus-error-handlers": "covered-only",
40
+ "error-handlers-only": "error-handlers-only"
41
+ };
42
+ var REALISTIC_CATEGORIES = [
43
+ "missing-await",
44
+ "wrong-variable",
45
+ "argument-order",
46
+ "exception-swallow",
47
+ "off-by-one",
48
+ "wrong-constant"
49
+ ];
50
+ var MAX_MUTANT_ROSTER = 2e4;
51
+
52
+ // src/execution-axis.ts
53
+ var LAYER0_EXECUTION = {
54
+ completed: "completed",
55
+ // we could not trust the coverage we collected — "not measured", never "nothing was covered"
56
+ "cannot-attest": "unavailable",
57
+ "not-applicable": "not-applicable",
58
+ "not-run": "skipped"
59
+ };
60
+ var LAYER1_EXECUTION = {
61
+ completed: "completed",
62
+ skipped: "skipped",
63
+ "not-run": "unavailable"
64
+ };
65
+ var lookup = (table, state) => table[state];
66
+ function diffCoverageExecution(state) {
67
+ if (state === void 0 || state === null || state === "") return "unavailable";
68
+ return lookup(LAYER0_EXECUTION, state) ?? "unavailable";
69
+ }
70
+ function classicMutationExecution(state) {
71
+ if (state === void 0 || state === null || state === "") return "completed";
72
+ return lookup(LAYER1_EXECUTION, state) ?? "unavailable";
73
+ }
74
+ function assertNever(value, context) {
75
+ throw new Error(`${context}: unhandled variant ${JSON.stringify(value)}`);
76
+ }
77
+
78
+ // src/diff-scope.ts
79
+ import { execFile } from "child_process";
80
+ import { promisify } from "util";
81
+ var pExecFile = promisify(execFile);
82
+ var SOURCE_RE = /\.(ts|tsx|js|mjs|cjs|jsx)$/;
83
+ var PYTHON_SOURCE_RE = /\.py$/;
84
+ var PYTHON_EXCLUDE_RE = /((^|\/)tests?\/|(^|\/)test_[^\/]*\.py$|_test\.py$|(^|\/)conftest\.py$|(^|\/)(build|dist|docs?|examples?|benchmarks?|scripts?)\/|(^|\/)\.|site-packages\/|(^|\/)venv\/|\.egg-info\/|(^|\/)setup\.py$)/i;
85
+ var EXCLUDE_RE = /(\.d\.ts$|\.test\.|\.spec\.|\.test-d\.|\.min\.|__tests__|__mocks__|^(tests?|e2e|scripts|docs?|examples?|benchmarks?|website|fixtures?|build|tools?)\/|(^|\/)tests?\.[mc]?[jt]sx?$|(^|\/)\.|\.config\.|\.bench\.|(^|\/)vitest[^\/]*\.[mc]?[jt]s$|jest\.setup)/i;
86
+ function isUnambiguouslyCommentOnly(content) {
87
+ let rest = content.trimStart();
88
+ if (rest.startsWith("//")) return true;
89
+ while (rest.startsWith("/*")) {
90
+ const close = rest.indexOf("*/", 2);
91
+ if (close < 0) return true;
92
+ rest = rest.slice(close + 2).trimStart();
93
+ if (rest === "" || rest.startsWith("//")) return true;
94
+ }
95
+ return false;
96
+ }
97
+ var DiffScopeError = class extends Error {
98
+ constructor(message, cause) {
99
+ super(message);
100
+ this.cause = cause;
101
+ this.name = "DiffScopeError";
102
+ }
103
+ cause;
104
+ };
105
+ function unquoteGitPath(s) {
106
+ if (s.length < 2 || s[0] !== '"' || s[s.length - 1] !== '"') return null;
107
+ const inner = s.slice(1, -1);
108
+ const simple = { t: 9, n: 10, r: 13, '"': 34, "\\": 92, a: 7, b: 8, f: 12, v: 11 };
109
+ const bytes = [];
110
+ for (let k = 0; k < inner.length; k++) {
111
+ const ch = inner[k];
112
+ if (ch !== "\\") {
113
+ const codePoint = inner.codePointAt(k);
114
+ const literal = String.fromCodePoint(codePoint);
115
+ for (const b of Buffer.from(literal, "utf8")) bytes.push(b);
116
+ if (codePoint > 65535) k++;
117
+ continue;
118
+ }
119
+ const next = inner[k + 1];
120
+ if (next === void 0) return null;
121
+ if (next >= "0" && next <= "7") {
122
+ let oct = "";
123
+ let m = k + 1;
124
+ while (m < inner.length && oct.length < 3 && inner[m] >= "0" && inner[m] <= "7") {
125
+ oct += inner[m];
126
+ m++;
127
+ }
128
+ bytes.push(parseInt(oct, 8) & 255);
129
+ k = m - 1;
130
+ } else if (next in simple) {
131
+ bytes.push(simple[next]);
132
+ k++;
133
+ } else {
134
+ return null;
135
+ }
136
+ }
137
+ return Buffer.from(bytes).toString("utf8");
138
+ }
139
+ function parseUnifiedDiff(diffText, opts = {}) {
140
+ const sourceRe = opts.sourceRe ?? SOURCE_RE;
141
+ const excludeRe = opts.excludeRe ?? EXCLUDE_RE;
142
+ const dropComments = opts.dropComments ?? false;
143
+ const side = opts.side ?? "new";
144
+ const headerPrefix = side === "old" ? "--- " : "+++ ";
145
+ const headerPathPrefix = side === "old" ? "--- a/" : "+++ b/";
146
+ const sidePrefix = side === "old" ? "a/" : "b/";
147
+ const bodyMark = side === "old" ? "-" : "+";
148
+ const scopes = /* @__PURE__ */ new Map();
149
+ const inspectionScopes = /* @__PURE__ */ new Map();
150
+ const excluded = [];
151
+ const deletions = [];
152
+ const excludedSeen = /* @__PURE__ */ new Set();
153
+ let currentFile = null;
154
+ let pendingOldPath = null;
155
+ let deletedFile = null;
156
+ const lines = diffText.split("\n");
157
+ for (let i = 0; i < lines.length; i++) {
158
+ const line = lines[i];
159
+ if (/^(?:old mode|new mode) 160000$/u.test(line) || /^index [0-9a-f.]+ 160000$/u.test(line)) {
160
+ throw new DiffScopeError(
161
+ "submodule revision changed in this range: measuring submodule diffs is not supported"
162
+ );
163
+ }
164
+ if (side === "new" && line.startsWith("--- ")) {
165
+ pendingOldPath = null;
166
+ if (line.startsWith("--- a/")) pendingOldPath = line.slice("--- a/".length);
167
+ else if (line.startsWith('--- "')) {
168
+ const decoded = unquoteGitPath(line.slice(4));
169
+ if (decoded && decoded.startsWith("a/")) pendingOldPath = decoded.slice(2);
170
+ }
171
+ continue;
172
+ }
173
+ if (line.startsWith(headerPrefix)) {
174
+ let newPath = null;
175
+ if (line.startsWith(headerPathPrefix)) newPath = line.slice(headerPathPrefix.length);
176
+ else if (line.startsWith(`${headerPrefix}"`)) {
177
+ const decoded = unquoteGitPath(line.slice(4));
178
+ if (decoded && decoded.startsWith(sidePrefix)) newPath = decoded.slice(2);
179
+ }
180
+ if (newPath === null) {
181
+ currentFile = null;
182
+ deletedFile = side === "new" && line.startsWith(`${headerPrefix}/dev/null`) && pendingOldPath !== null && sourceRe.test(pendingOldPath) && !excludeRe.test(pendingOldPath) ? pendingOldPath : null;
183
+ pendingOldPath = null;
184
+ continue;
185
+ }
186
+ deletedFile = null;
187
+ pendingOldPath = null;
188
+ currentFile = newPath;
189
+ const reason = !sourceRe.test(currentFile) ? "not-source" : excludeRe.test(currentFile) ? "excluded-pattern" : null;
190
+ if (reason !== null) {
191
+ if (!excludedSeen.has(currentFile)) {
192
+ excludedSeen.add(currentFile);
193
+ excluded.push({ file: currentFile, reason });
194
+ }
195
+ currentFile = null;
196
+ }
197
+ continue;
198
+ }
199
+ if (currentFile === null && deletedFile !== null && line.startsWith("@@")) {
200
+ const om = line.match(/@@ -(\d+)(?:,(\d+))?/);
201
+ if (om) {
202
+ deletions.push({
203
+ file: deletedFile,
204
+ oldStart: Number(om[1]),
205
+ oldLines: om[2] === void 0 ? 1 : Number(om[2])
206
+ });
207
+ }
208
+ continue;
209
+ }
210
+ if (currentFile && line.startsWith("@@")) {
211
+ const m = line.match(side === "old" ? /@@ -(\d+)(?:,(\d+))?/ : /\+(\d+)(?:,(\d+))?/);
212
+ if (!m) continue;
213
+ const newStart = Number(m[1]);
214
+ const count = m[2] === void 0 ? 1 : Number(m[2]);
215
+ if (count === 0) {
216
+ if (side === "new" && currentFile) {
217
+ const om = line.match(/@@ -(\d+)(?:,(\d+))?/);
218
+ if (om) {
219
+ deletions.push({
220
+ file: currentFile,
221
+ oldStart: Number(om[1]),
222
+ oldLines: om[2] === void 0 ? 1 : Number(om[2])
223
+ });
224
+ }
225
+ }
226
+ continue;
227
+ }
228
+ const added = [];
229
+ const inspected = [];
230
+ let ln = newStart;
231
+ let j = i + 1;
232
+ for (; j < lines.length; j++) {
233
+ const body = lines[j];
234
+ if (body.startsWith("\\")) continue;
235
+ if (!body.startsWith("+") && !body.startsWith("-")) break;
236
+ if (body.startsWith(bodyMark)) {
237
+ const content = body.slice(1);
238
+ inspected.push(ln);
239
+ if (!(dropComments && isUnambiguouslyCommentOnly(content))) added.push(ln);
240
+ ln++;
241
+ }
242
+ }
243
+ i = j - 1;
244
+ addRanges(scopes, currentFile, added);
245
+ addRanges(inspectionScopes, currentFile, inspected);
246
+ }
247
+ }
248
+ const toScopes = (m) => [...m.entries()].map(([file, ranges]) => ({
249
+ file,
250
+ ranges,
251
+ lines: ranges.reduce((a, [s, e]) => a + (e - s + 1), 0)
252
+ }));
253
+ const out = toScopes(scopes);
254
+ return {
255
+ scopes: out,
256
+ totalLines: out.reduce((a, f) => a + f.lines, 0),
257
+ excluded,
258
+ inspectionScopes: toScopes(inspectionScopes),
259
+ deletions
260
+ };
261
+ }
262
+ function addRanges(target, file, ns) {
263
+ if (ns.length === 0) return;
264
+ const ranges = target.get(file) ?? [];
265
+ let s = ns[0];
266
+ let prev = ns[0];
267
+ for (const n of ns.slice(1)) {
268
+ if (n === prev + 1) {
269
+ prev = n;
270
+ continue;
271
+ }
272
+ ranges.push([s, prev]);
273
+ s = n;
274
+ prev = n;
275
+ }
276
+ ranges.push([s, prev]);
277
+ target.set(file, ranges);
278
+ }
279
+ async function computeScope(cwd, base, head = "HEAD", opts = {}) {
280
+ let leftSide = base;
281
+ if (opts.againstWorkingTree) {
282
+ try {
283
+ const { stdout: mb } = await pExecFile("git", ["merge-base", base, "HEAD"], {
284
+ cwd,
285
+ maxBuffer: 128 * 1024 * 1024,
286
+ timeout: 6e4,
287
+ killSignal: "SIGKILL"
288
+ });
289
+ leftSide = mb.trim();
290
+ if (leftSide === "") throw new Error("empty merge-base output");
291
+ } catch (err) {
292
+ const detail = err instanceof Error ? err.message.trim().split("\n")[0] : String(err);
293
+ throw new DiffScopeError(`git merge-base ${base} HEAD failed in ${cwd}: ${detail}`, err);
294
+ }
295
+ }
296
+ const range = opts.againstWorkingTree ? leftSide : `${base}...${head}`;
297
+ const args = ["-c", "core.quotepath=false", "diff", "--ignore-submodules=none", range, "-U0", "--no-color"];
298
+ if (opts.pathspec) args.push("--", opts.pathspec);
299
+ let stdout;
300
+ try {
301
+ ({ stdout } = await pExecFile("git", args, { cwd, maxBuffer: 128 * 1024 * 1024, timeout: 6e4, killSignal: "SIGKILL" }));
302
+ } catch (err) {
303
+ const detail = err instanceof Error ? err.message.trim().split("\n")[0] : String(err);
304
+ throw new DiffScopeError(`git diff ${range} failed in ${cwd}: ${detail}`, err);
305
+ }
306
+ return parseUnifiedDiff(stdout, opts);
307
+ }
308
+
309
+ // src/diff-coverage.ts
310
+ var LAYER0_SCOPE_DISCLOSURE = "scope of claim: one aggregate coverage run at the measured commit \u2014 no per-test attribution, no repetition, and no claim that any test asserts on these lines";
311
+ function changedStructuralCoverage(scope, structural) {
312
+ if (structural === null) return null;
313
+ const inScope = /* @__PURE__ */ new Map();
314
+ for (const { file, line } of expandScopeLines(scope)) {
315
+ const lines = inScope.get(file) ?? /* @__PURE__ */ new Set();
316
+ lines.add(line);
317
+ inScope.set(file, lines);
318
+ }
319
+ let fnTotal = 0;
320
+ let fnInvoked = 0;
321
+ let brTotal = 0;
322
+ let brTaken = 0;
323
+ for (const [file, lines] of inScope) {
324
+ const rec = structural.get(file);
325
+ if (!rec) continue;
326
+ for (const s of rec.functions) {
327
+ if (!lines.has(s.line)) continue;
328
+ fnTotal++;
329
+ if (s.hits > 0) fnInvoked++;
330
+ }
331
+ for (const s of rec.branches) {
332
+ if (!lines.has(s.line)) continue;
333
+ brTotal++;
334
+ if (s.hits > 0) brTaken++;
335
+ }
336
+ }
337
+ return { functions: { total: fnTotal, invoked: fnInvoked }, branches: { total: brTotal, taken: brTaken } };
338
+ }
339
+ var MAX_EXPANDED_LINES = 1e6;
340
+ var MAX_SCOPE_RANGES = 1e5;
341
+ function expandScopeLines(scope) {
342
+ const byFile = /* @__PURE__ */ new Map();
343
+ let rangeCount = 0;
344
+ for (const fs of scope) {
345
+ for (const [start, end] of fs.ranges) {
346
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) {
347
+ throw new Error(`expandScopeLines: invalid range [${start}, ${end}] for ${fs.file}`);
348
+ }
349
+ if (end - start + 1 > MAX_EXPANDED_LINES) {
350
+ throw new Error(`expandScopeLines: more than ${MAX_EXPANDED_LINES} changed lines`);
351
+ }
352
+ rangeCount++;
353
+ if (rangeCount > MAX_SCOPE_RANGES) {
354
+ throw new Error(`expandScopeLines: more than ${MAX_SCOPE_RANGES} scope ranges`);
355
+ }
356
+ const ranges = byFile.get(fs.file) ?? [];
357
+ ranges.push([start, end]);
358
+ byFile.set(fs.file, ranges);
359
+ }
360
+ }
361
+ const mergedByFile = /* @__PURE__ */ new Map();
362
+ let uniqueTotal = 0;
363
+ for (const [file, ranges] of byFile) {
364
+ ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
365
+ const merged = [];
366
+ for (const [start, end] of ranges) {
367
+ const last = merged[merged.length - 1];
368
+ if (last && start <= last[1] + 1) {
369
+ if (end > last[1]) last[1] = end;
370
+ } else {
371
+ merged.push([start, end]);
372
+ }
373
+ }
374
+ for (const [start, end] of merged) {
375
+ uniqueTotal += end - start + 1;
376
+ if (uniqueTotal > MAX_EXPANDED_LINES) {
377
+ throw new Error(`expandScopeLines: more than ${MAX_EXPANDED_LINES} changed lines`);
378
+ }
379
+ }
380
+ mergedByFile.set(file, merged);
381
+ }
382
+ const out = [];
383
+ for (const file of [...mergedByFile.keys()].sort()) {
384
+ for (const [start, end] of mergedByFile.get(file)) {
385
+ for (let line = start; line <= end; line++) out.push({ file, line });
386
+ }
387
+ }
388
+ return out;
389
+ }
390
+ function missingScopedFiles(scope, coverage) {
391
+ const files = [...new Set(scope.map((s) => s.file))].sort();
392
+ return files.filter((f) => !coverage.has(f));
393
+ }
394
+ function classifyDiffCoverage(scope, coverage) {
395
+ const lines = expandScopeLines(scope).map(({ file, line }) => {
396
+ const hits = coverage.get(file)?.get(line);
397
+ const state = hits === void 0 ? "not-instrumented" : hits > 0 ? "covered" : "uncovered";
398
+ return { file, line, state };
399
+ });
400
+ return { lines, counts: countsFromLines(lines) };
401
+ }
402
+ function isNonExecutableLine(text, file = "source.ts", line = 1) {
403
+ const t = text.trim();
404
+ if (t === "") return true;
405
+ if (file.endsWith(".py")) return t.startsWith("#") || isPureStructuralPunctuation(t);
406
+ if (line === 1 && t.startsWith("#!")) return true;
407
+ const stripped = stripLeadingJsComments(text, false).rest.trim();
408
+ if (stripped === "") return true;
409
+ return isPureStructuralPunctuation(stripped);
410
+ }
411
+ function isPureStructuralPunctuation(text) {
412
+ return /^[()[\]{};,]+$/.test(text);
413
+ }
414
+ function stripLeadingJsComments(text, startsInsideBlock) {
415
+ let rest = text;
416
+ let insideBlock = startsInsideBlock;
417
+ for (; ; ) {
418
+ if (insideBlock) {
419
+ const close2 = rest.indexOf("*/");
420
+ if (close2 < 0) return { rest: "", insideBlock: true };
421
+ rest = rest.slice(close2 + 2);
422
+ insideBlock = false;
423
+ }
424
+ rest = rest.trimStart();
425
+ if (rest.startsWith("//")) return { rest: "", insideBlock: false };
426
+ if (!rest.startsWith("/*")) return { rest, insideBlock: false };
427
+ const close = rest.indexOf("*/", 2);
428
+ if (close < 0) return { rest: "", insideBlock: true };
429
+ rest = rest.slice(close + 2);
430
+ }
431
+ }
432
+ function opensTrailingJsBlockComment(text) {
433
+ let quote = null;
434
+ let escaped = false;
435
+ for (let i = 0; i < text.length; i++) {
436
+ const char = text[i];
437
+ if (quote !== null) {
438
+ if (escaped) {
439
+ escaped = false;
440
+ } else if (char === "\\") {
441
+ escaped = true;
442
+ } else if (char === quote) {
443
+ quote = null;
444
+ }
445
+ continue;
446
+ }
447
+ if (char === "'" || char === '"' || char === "`") {
448
+ quote = char;
449
+ continue;
450
+ }
451
+ if (char === "/" && text[i + 1] === "/") return false;
452
+ if (char === "/" && text[i + 1] === "*") {
453
+ const close = text.indexOf("*/", i + 2);
454
+ if (close < 0) return true;
455
+ i = close + 1;
456
+ }
457
+ }
458
+ return false;
459
+ }
460
+ function moduleSyntaxTokens(source) {
461
+ const text = source.join("\n");
462
+ const lineStarts = [0];
463
+ for (let index2 = 0; index2 < text.length; index2++) {
464
+ if (text[index2] === "\n") lineStarts.push(index2 + 1);
465
+ }
466
+ const lineOf = (offset) => {
467
+ let low = 0;
468
+ let high = lineStarts.length;
469
+ while (low + 1 < high) {
470
+ const middle = Math.floor((low + high) / 2);
471
+ if (lineStarts[middle] <= offset) low = middle;
472
+ else high = middle;
473
+ }
474
+ return low + 1;
475
+ };
476
+ const skipQuoted = (start, quote) => {
477
+ let index2 = start + 1;
478
+ while (index2 < text.length) {
479
+ if (text[index2] === "\\") index2 += 2;
480
+ else if (text[index2] === quote) return index2 + 1;
481
+ else index2 += 1;
482
+ }
483
+ return text.length;
484
+ };
485
+ const skipBlockComment = (start) => {
486
+ const close = text.indexOf("*/", start + 2);
487
+ return close < 0 ? text.length : close + 2;
488
+ };
489
+ const skipTemplateExpression = (start) => {
490
+ let depth2 = 1;
491
+ let index2 = start;
492
+ while (index2 < text.length && depth2 > 0) {
493
+ const char = text[index2];
494
+ if (char === "'" || char === '"') {
495
+ index2 = skipQuoted(index2, char);
496
+ } else if (char === "`") {
497
+ index2 = skipTemplate(index2);
498
+ } else if (char === "/" && text[index2 + 1] === "*") {
499
+ index2 = skipBlockComment(index2);
500
+ } else if (char === "/" && text[index2 + 1] === "/") {
501
+ const newline = text.indexOf("\n", index2 + 2);
502
+ index2 = newline < 0 ? text.length : newline + 1;
503
+ } else {
504
+ if (char === "{") depth2 += 1;
505
+ else if (char === "}") depth2 -= 1;
506
+ index2 += 1;
507
+ }
508
+ }
509
+ return index2;
510
+ };
511
+ const skipTemplate = (start) => {
512
+ let index2 = start + 1;
513
+ while (index2 < text.length) {
514
+ if (text[index2] === "\\") {
515
+ index2 += 2;
516
+ } else if (text[index2] === "`") {
517
+ return index2 + 1;
518
+ } else if (text[index2] === "$" && text[index2 + 1] === "{") {
519
+ index2 = skipTemplateExpression(index2 + 2);
520
+ } else {
521
+ index2 += 1;
522
+ }
523
+ }
524
+ return text.length;
525
+ };
526
+ const raw = [];
527
+ const regexPrefix = /* @__PURE__ */ new Set([
528
+ "(",
529
+ "[",
530
+ "{",
531
+ "=",
532
+ ",",
533
+ ":",
534
+ ";",
535
+ "!",
536
+ "?",
537
+ "+",
538
+ "-",
539
+ "*",
540
+ "%",
541
+ "&",
542
+ "|",
543
+ "^",
544
+ "~",
545
+ "return",
546
+ "case",
547
+ "throw",
548
+ "yield",
549
+ "await",
550
+ "typeof",
551
+ "void",
552
+ "delete",
553
+ "instanceof",
554
+ "in",
555
+ "of"
556
+ ]);
557
+ let index = 0;
558
+ while (index < text.length) {
559
+ const char = text[index];
560
+ if (index === 0 && char === "#" && text[index + 1] === "!") {
561
+ const newline = text.indexOf("\n", index + 2);
562
+ index = newline < 0 ? text.length : newline + 1;
563
+ continue;
564
+ }
565
+ if (/\s/u.test(char)) {
566
+ index += 1;
567
+ continue;
568
+ }
569
+ if (char === "/" && text[index + 1] === "/") {
570
+ const newline = text.indexOf("\n", index + 2);
571
+ index = newline < 0 ? text.length : newline + 1;
572
+ continue;
573
+ }
574
+ if (char === "/" && text[index + 1] === "*") {
575
+ index = skipBlockComment(index);
576
+ continue;
577
+ }
578
+ const start = index;
579
+ let kind;
580
+ let value;
581
+ if (char === "'" || char === '"') {
582
+ kind = "string";
583
+ index = skipQuoted(index, char);
584
+ value = "string";
585
+ } else if (char === "`") {
586
+ kind = "punctuation";
587
+ index = skipTemplate(index);
588
+ value = "template";
589
+ } else if (char === "/" && (raw.length === 0 || regexPrefix.has(raw[raw.length - 1].value))) {
590
+ let cursor = index + 1;
591
+ let inClass = false;
592
+ let closed = false;
593
+ while (cursor < text.length && text[cursor] !== "\n") {
594
+ if (text[cursor] === "\\") cursor += 2;
595
+ else if (text[cursor] === "[") {
596
+ inClass = true;
597
+ cursor += 1;
598
+ } else if (text[cursor] === "]") {
599
+ inClass = false;
600
+ cursor += 1;
601
+ } else if (text[cursor] === "/" && !inClass) {
602
+ cursor += 1;
603
+ while (cursor < text.length && /[A-Za-z]/u.test(text[cursor])) cursor += 1;
604
+ closed = true;
605
+ break;
606
+ } else cursor += 1;
607
+ }
608
+ kind = "punctuation";
609
+ if (closed) {
610
+ index = cursor;
611
+ value = "regex";
612
+ } else {
613
+ index += 1;
614
+ value = "/";
615
+ }
616
+ } else if (/[A-Za-z_$]/u.test(char)) {
617
+ kind = "identifier";
618
+ index += 1;
619
+ while (index < text.length && /[A-Za-z0-9_$]/u.test(text[index])) index += 1;
620
+ value = text.slice(start, index);
621
+ } else {
622
+ kind = "punctuation";
623
+ value = char;
624
+ index += 1;
625
+ }
626
+ raw.push({
627
+ kind,
628
+ value,
629
+ startLine: lineOf(start),
630
+ endLine: lineOf(Math.max(start, index - 1))
631
+ });
632
+ }
633
+ let depth = 0;
634
+ return raw.map((token) => {
635
+ const withDepth = { ...token, depthBefore: depth };
636
+ if (token.value === "{") depth += 1;
637
+ else if (token.value === "}") depth = Math.max(0, depth - 1);
638
+ return withDepth;
639
+ });
640
+ }
641
+ function matchingModuleDelimiter(tokens, openIndex, open, close) {
642
+ if (tokens[openIndex]?.value !== open) return null;
643
+ let depth = 0;
644
+ for (let index = openIndex; index < tokens.length; index++) {
645
+ if (tokens[index].value === open) depth += 1;
646
+ else if (tokens[index].value === close && --depth === 0) return index;
647
+ }
648
+ return null;
649
+ }
650
+ function finishStaticModuleSource(tokens, sourceIndex) {
651
+ if (tokens[sourceIndex]?.kind !== "string") return null;
652
+ let end = sourceIndex;
653
+ let next = sourceIndex + 1;
654
+ if (tokens[next]?.value === "with" || tokens[next]?.value === "assert") {
655
+ const close = matchingModuleDelimiter(tokens, next + 1, "{", "}");
656
+ if (close === null) return null;
657
+ end = close;
658
+ next = close + 1;
659
+ }
660
+ if (tokens[next]?.value === ";") end = next;
661
+ return end;
662
+ }
663
+ function parseStaticImport(tokens, start) {
664
+ let index = start + 1;
665
+ const first = tokens[index];
666
+ if (first === void 0 || first.value === "(" || first.value === ".") return null;
667
+ if (first.kind === "string") return finishStaticModuleSource(tokens, index);
668
+ if (tokens[index]?.value === "type") index += 1;
669
+ const clauseStart = tokens[index];
670
+ if (clauseStart === void 0) return null;
671
+ if (clauseStart.kind === "identifier") {
672
+ index += 1;
673
+ if (tokens[index]?.value === ",") index += 1;
674
+ }
675
+ if (tokens[index]?.value === "{") {
676
+ const close = matchingModuleDelimiter(tokens, index, "{", "}");
677
+ if (close === null) return null;
678
+ index = close + 1;
679
+ } else if (tokens[index]?.value === "*") {
680
+ if (tokens[index + 1]?.value !== "as" || tokens[index + 2]?.kind !== "identifier") return null;
681
+ index += 3;
682
+ } else if (tokens[index - 1]?.kind !== "identifier") {
683
+ return null;
684
+ }
685
+ if (tokens[index]?.value !== "from" || tokens[index + 1]?.kind !== "string") return null;
686
+ return finishStaticModuleSource(tokens, index + 1);
687
+ }
688
+ function parseExportTypeDeclaration(tokens, start) {
689
+ let braces = 0;
690
+ let brackets = 0;
691
+ let parentheses = 0;
692
+ let sawEquals = false;
693
+ for (let index = start + 2; index < tokens.length; index++) {
694
+ const value = tokens[index].value;
695
+ if (value === "{") braces += 1;
696
+ else if (value === "}") braces = Math.max(0, braces - 1);
697
+ else if (value === "[") brackets += 1;
698
+ else if (value === "]") brackets = Math.max(0, brackets - 1);
699
+ else if (value === "(") parentheses += 1;
700
+ else if (value === ")") parentheses = Math.max(0, parentheses - 1);
701
+ else if (value === "=" && braces === 0 && brackets === 0 && parentheses === 0) sawEquals = true;
702
+ else if (value === ";" && braces === 0 && brackets === 0 && parentheses === 0) {
703
+ return sawEquals ? index : null;
704
+ }
705
+ }
706
+ return null;
707
+ }
708
+ function parseExportInterface(tokens, start) {
709
+ let open = start + 2;
710
+ while (open < tokens.length && tokens[open].value !== "{") {
711
+ if (tokens[open].value === ";") return null;
712
+ open += 1;
713
+ }
714
+ const close = matchingModuleDelimiter(tokens, open, "{", "}");
715
+ if (close === null) return null;
716
+ return tokens[close + 1]?.value === ";" ? close + 1 : close;
717
+ }
718
+ function parseStaticExport(tokens, start) {
719
+ let index = start + 1;
720
+ if (tokens[index]?.value === "type") {
721
+ if (tokens[index + 1]?.value !== "{" && tokens[index + 1]?.value !== "*") {
722
+ return parseExportTypeDeclaration(tokens, start);
723
+ }
724
+ index += 1;
725
+ } else if (tokens[index]?.value === "interface") {
726
+ return parseExportInterface(tokens, start);
727
+ }
728
+ if (tokens[index]?.value === "{") {
729
+ const close = matchingModuleDelimiter(tokens, index, "{", "}");
730
+ if (close === null) return null;
731
+ index = close + 1;
732
+ if (tokens[index]?.value === "from") {
733
+ if (tokens[index + 1]?.kind !== "string") return null;
734
+ return finishStaticModuleSource(tokens, index + 1);
735
+ }
736
+ return tokens[index]?.value === ";" ? index : close;
737
+ }
738
+ if (tokens[index]?.value !== "*") return null;
739
+ index += 1;
740
+ if (tokens[index]?.value === "as") {
741
+ if (tokens[index + 1]?.kind !== "identifier") return null;
742
+ index += 2;
743
+ }
744
+ if (tokens[index]?.value !== "from" || tokens[index + 1]?.kind !== "string") return null;
745
+ return finishStaticModuleSource(tokens, index + 1);
746
+ }
747
+ function definitelyDeclarativeModuleLines(source) {
748
+ const tokens = moduleSyntaxTokens(source);
749
+ const owned = /* @__PURE__ */ new Set();
750
+ let activeLine = -1;
751
+ let lineHasUnownedToken = false;
752
+ for (let index = 0; index < tokens.length; index++) {
753
+ const token = tokens[index];
754
+ if (token.startLine !== activeLine) {
755
+ activeLine = token.startLine;
756
+ lineHasUnownedToken = false;
757
+ }
758
+ if (owned.has(index)) continue;
759
+ if (lineHasUnownedToken || token.value !== "import" && token.value !== "export" || token.depthBefore !== 0) {
760
+ lineHasUnownedToken = true;
761
+ continue;
762
+ }
763
+ const previous = tokens[index - 1];
764
+ if (previous !== void 0 && previous.endLine < token.startLine && previous.value !== ";" && previous.value !== "}" && !owned.has(index - 1)) {
765
+ lineHasUnownedToken = true;
766
+ continue;
767
+ }
768
+ const end = token.value === "import" ? parseStaticImport(tokens, index) : parseStaticExport(tokens, index);
769
+ if (end === null) {
770
+ lineHasUnownedToken = true;
771
+ continue;
772
+ }
773
+ for (let ownedIndex = index; ownedIndex <= end; ownedIndex++) owned.add(ownedIndex);
774
+ }
775
+ const byLine = /* @__PURE__ */ new Map();
776
+ for (let index = 0; index < tokens.length; index++) {
777
+ const token = tokens[index];
778
+ for (let line = token.startLine; line <= token.endLine; line++) {
779
+ const indexes = byLine.get(line) ?? [];
780
+ indexes.push(index);
781
+ byLine.set(line, indexes);
782
+ }
783
+ }
784
+ const out = /* @__PURE__ */ new Set();
785
+ for (const [line, indexes] of byLine) {
786
+ if (indexes.length > 0 && indexes.every((index) => owned.has(index))) out.add(line);
787
+ }
788
+ return out;
789
+ }
790
+ function definitelyNonExecutableLines(file, source) {
791
+ const out = /* @__PURE__ */ new Set();
792
+ if (file.endsWith(".py")) {
793
+ for (let i = 0; i < source.length; i++) {
794
+ if (isNonExecutableLine(source[i], file, i + 1)) out.add(i + 1);
795
+ }
796
+ return out;
797
+ }
798
+ const declarativeModuleLines = definitelyDeclarativeModuleLines(source);
799
+ let insideBlock = false;
800
+ let insideTemplate = false;
801
+ for (let i = 0; i < source.length; i++) {
802
+ const t = source[i].trim();
803
+ if (insideTemplate) {
804
+ if (hasOddUnescapedBackticks(source[i])) insideTemplate = false;
805
+ continue;
806
+ }
807
+ if (i === 0 && t.startsWith("#!")) {
808
+ out.add(1);
809
+ continue;
810
+ }
811
+ const stripped = stripLeadingJsComments(source[i], insideBlock);
812
+ insideBlock = stripped.insideBlock;
813
+ const rest = stripped.rest.trim();
814
+ if (rest === "" || isPureStructuralPunctuation(rest) || declarativeModuleLines.has(i + 1) || // Plain function/class declaration OPENERS (no default-parameter expressions, which execute
815
+ // per call). vitest 4's AST-aware mapping emits no statement for these lines, so without
816
+ // this rescue every changed declaration line reads as a false gap. Excluding them cannot
817
+ // hide execution: the body lines still demand their own evidence.
818
+ /^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*[A-Za-z0-9_$]*\s*\([^)=]*\)\s*\{$/u.test(rest) || /^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+[A-Za-z0-9_$]+(?:\s+extends\s+[A-Za-z0-9_$.]+)?\s*\{$/u.test(rest)) {
819
+ out.add(i + 1);
820
+ }
821
+ if (!insideBlock && rest !== "" && opensTrailingJsBlockComment(rest)) insideBlock = true;
822
+ if (rest !== "" && hasOddUnescapedBackticks(rest)) insideTemplate = true;
823
+ }
824
+ return out;
825
+ }
826
+ function hasOddUnescapedBackticks(text) {
827
+ let count = 0;
828
+ for (let i = 0; i < text.length; i++) {
829
+ if (text[i] !== "`") continue;
830
+ let slashes = 0;
831
+ for (let j = i - 1; j >= 0 && text[j] === "\\"; j--) slashes++;
832
+ if (slashes % 2 === 0) count++;
833
+ }
834
+ return count % 2 === 1;
835
+ }
836
+ function markNonExecutable(lines, sourceOf) {
837
+ const cache = /* @__PURE__ */ new Map();
838
+ const nonExecutable = /* @__PURE__ */ new Map();
839
+ let marked = 0;
840
+ const out = lines.map((l) => {
841
+ if (l.state !== "not-instrumented") return l;
842
+ if (!cache.has(l.file)) cache.set(l.file, sourceOf(l.file));
843
+ const src = cache.get(l.file);
844
+ if (src === null || src === void 0) return l;
845
+ if (!nonExecutable.has(l.file)) nonExecutable.set(l.file, definitelyNonExecutableLines(l.file, src));
846
+ if (!nonExecutable.get(l.file).has(l.line)) return l;
847
+ marked++;
848
+ return { ...l, state: "not-executable" };
849
+ });
850
+ return { lines: out, marked };
851
+ }
852
+ function countsFromLines(lines) {
853
+ let covered = 0;
854
+ let uncovered = 0;
855
+ let notInstrumented = 0;
856
+ let notExecutable = 0;
857
+ for (const l of lines) {
858
+ if (l.state === "covered") covered++;
859
+ else if (l.state === "uncovered") uncovered++;
860
+ else if (l.state === "not-executable") notExecutable++;
861
+ else notInstrumented++;
862
+ }
863
+ return {
864
+ changed: lines.length - notExecutable,
865
+ covered,
866
+ uncovered,
867
+ notInstrumented,
868
+ ...notExecutable > 0 ? { notExecutable } : {}
869
+ };
870
+ }
871
+ function uncoveredCount(counts) {
872
+ return counts.uncovered + counts.notInstrumented;
873
+ }
874
+ function coveredLinesToScope(lines) {
875
+ const byFile = /* @__PURE__ */ new Map();
876
+ for (const l of lines) {
877
+ if (l.state !== "covered") continue;
878
+ const arr = byFile.get(l.file) ?? [];
879
+ arr.push(l.line);
880
+ byFile.set(l.file, arr);
881
+ }
882
+ const out = [];
883
+ for (const file of [...byFile.keys()].sort()) {
884
+ const nums = [...new Set(byFile.get(file))].sort((a, b) => a - b);
885
+ const ranges = [];
886
+ let s = nums[0];
887
+ let prev = nums[0];
888
+ for (const n of nums.slice(1)) {
889
+ if (n === prev + 1) {
890
+ prev = n;
891
+ continue;
892
+ }
893
+ ranges.push([s, prev]);
894
+ s = n;
895
+ prev = n;
896
+ }
897
+ ranges.push([s, prev]);
898
+ out.push({ file, ranges, lines: ranges.reduce((a, [rs, re]) => a + (re - rs + 1), 0) });
899
+ }
900
+ return out;
901
+ }
902
+ function validateDiffCoverageLines(scope, lines) {
903
+ const expected = expandScopeLines(scope);
904
+ const expectedKeys = new Set(expected.map((e) => `${e.file} ${e.line}`));
905
+ if (lines.length !== expected.length) {
906
+ throw new Error(`diffCoverage lines: expected ${expected.length} entries (one per changed line), got ${lines.length}`);
907
+ }
908
+ const seen = /* @__PURE__ */ new Set();
909
+ for (const l of lines) {
910
+ if (!Number.isInteger(l.line) || l.line < 1) throw new Error(`diffCoverage lines: invalid line ${JSON.stringify(l.line)}`);
911
+ if (l.state !== "covered" && l.state !== "uncovered" && l.state !== "not-instrumented" && l.state !== "not-executable") {
912
+ throw new Error(`diffCoverage lines: invalid state ${JSON.stringify(l.state)}`);
913
+ }
914
+ const key = `${l.file} ${l.line}`;
915
+ if (!expectedKeys.has(key)) throw new Error(`diffCoverage lines: entry ${l.file}:${l.line} is not in the changed-line scope`);
916
+ if (seen.has(key)) throw new Error(`diffCoverage lines: duplicate entry ${l.file}:${l.line}`);
917
+ seen.add(key);
918
+ }
919
+ return countsFromLines(lines);
920
+ }
921
+
922
+ // src/score.ts
923
+ function tallyCounts(mutants) {
924
+ const counts = emptyCounts();
925
+ for (const m of mutants) counts[m.status]++;
926
+ return counts;
927
+ }
928
+ function denominatorOf(counts) {
929
+ return counts.killed + counts.timeout + counts.survived + counts["no-coverage"];
930
+ }
931
+ function errorCountOf(counts) {
932
+ return counts["runtime-error"] + counts["build-error"];
933
+ }
934
+ function pct(n, d) {
935
+ return d > 0 ? Number((100 * n / d).toFixed(1)) : null;
936
+ }
937
+ function computeScores({ counts, confirmedEquivalent = 0, tier = 1, triageValidated = false }) {
938
+ const killedish = counts.killed + counts.timeout;
939
+ const denom = denominatorOf(counts);
940
+ const rawScore = pct(killedish, denom);
941
+ const triagedScore = tier === 0 ? null : pct(killedish, denom - confirmedEquivalent);
942
+ return {
943
+ rawScore,
944
+ triagedScore,
945
+ denominator: denom,
946
+ errorCount: errorCountOf(counts),
947
+ confirmedEquivalent,
948
+ triageValidated
949
+ };
950
+ }
951
+ var DEFAULT_FLOOR = {
952
+ minMutantsExecuted: 10,
953
+ maxErrorRate: 0.2,
954
+ minSamplingFraction: 0.5
955
+ };
956
+ function evaluateFloor(counts, mutantsPlanned, mutantsRun, policy = DEFAULT_FLOOR) {
957
+ const denom = denominatorOf(counts);
958
+ const errRate = mutantsPlanned > 0 ? errorCountOf(counts) / mutantsPlanned : 1;
959
+ const sampFrac = mutantsPlanned > 0 ? mutantsRun / mutantsPlanned : 0;
960
+ const minMutantsExecuted = denom >= policy.minMutantsExecuted;
961
+ const maxErrorRate = errRate <= policy.maxErrorRate;
962
+ const minSamplingFraction = sampFrac >= policy.minSamplingFraction;
963
+ return {
964
+ minMutantsExecuted,
965
+ maxErrorRate,
966
+ minSamplingFraction,
967
+ passed: minMutantsExecuted && maxErrorRate && minSamplingFraction
968
+ };
969
+ }
970
+ function evaluateGate(scores, floor, opts) {
971
+ const { threshold, tier = 1 } = opts;
972
+ if (!floor.passed) {
973
+ const which = [
974
+ !floor.minMutantsExecuted && "too few mutants executed",
975
+ !floor.maxErrorRate && "error rate too high",
976
+ !floor.minSamplingFraction && "sampling fraction too low"
977
+ ].filter(Boolean).join("; ");
978
+ return { status: "cannot-attest", score: null, threshold, reason: `evidence floor failed: ${which}` };
979
+ }
980
+ const useTriaged = tier !== 0 && scores.triageValidated;
981
+ const score = useTriaged ? scores.triagedScore ?? scores.rawScore : scores.rawScore;
982
+ if (score === null) return { status: "cannot-attest", score: null, threshold, reason: "no scoreable mutants" };
983
+ const basis = useTriaged ? "triaged score" : "score";
984
+ return score >= threshold ? { status: "pass", score, threshold, reason: `${basis} ${score}% \u2265 threshold ${threshold}%` } : { status: "fail", score, threshold, reason: `${basis} ${score}% < threshold ${threshold}%` };
985
+ }
986
+ var DIFF_COVERAGE_THRESHOLD = 100;
987
+ function evaluateDiffCoverageGate(dc) {
988
+ if (dc.state === "completed") {
989
+ const c = dc.counts;
990
+ if (c.changed === 0) return null;
991
+ const missing = uncoveredCount(c);
992
+ const rawScore = pct(c.covered, c.changed);
993
+ if (missing === 0) {
994
+ return { status: "pass", score: rawScore, threshold: DIFF_COVERAGE_THRESHOLD, reason: `all ${c.changed} changed line(s) covered` };
995
+ }
996
+ const score = rawScore !== null && rawScore >= 100 ? 99.9 : rawScore;
997
+ return {
998
+ status: "fail",
999
+ score,
1000
+ threshold: DIFF_COVERAGE_THRESHOLD,
1001
+ reason: `${missing} of ${c.changed} changed lines have no test executing them`
1002
+ };
1003
+ }
1004
+ if (dc.state === "cannot-attest") {
1005
+ return { status: "cannot-attest", score: null, threshold: DIFF_COVERAGE_THRESHOLD, reason: dc.reason };
1006
+ }
1007
+ return null;
1008
+ }
1009
+ function combineLayerGates(diffCoverage, _diffCoverageGate, mutationGate, opts) {
1010
+ const { threshold } = opts;
1011
+ const diffCoverageGate = evaluateDiffCoverageGate(diffCoverage);
1012
+ if (diffCoverage.state === "completed") {
1013
+ if (diffCoverageGate && diffCoverageGate.status === "fail") {
1014
+ return { overall: diffCoverageGate, decisiveLayer: "layer-0" };
1015
+ }
1016
+ } else if (diffCoverage.state === "cannot-attest") {
1017
+ return {
1018
+ overall: diffCoverageGate ?? { status: "cannot-attest", score: null, threshold, reason: diffCoverage.reason },
1019
+ decisiveLayer: "layer-0"
1020
+ };
1021
+ } else if (diffCoverage.state === "not-applicable" && (diffCoverage.reason === "empty-scope" || diffCoverage.reason === "no-executable-lines" || diffCoverage.reason === "deletion-only")) {
1022
+ const reason = diffCoverage.reason === "no-executable-lines" ? "no executable changed lines" : diffCoverage.reason === "deletion-only" ? "deletion-only change \u2014 no new line to execute and no mutation scope" : "no scoreable mutants";
1023
+ return { overall: { status: "cannot-attest", score: null, threshold, reason }, decisiveLayer: "none" };
1024
+ } else if (diffCoverage.state === "not-run") {
1025
+ return { overall: { status: "cannot-attest", score: null, threshold, reason: "no scoreable mutants" }, decisiveLayer: "none" };
1026
+ }
1027
+ if (mutationGate) return { overall: mutationGate, decisiveLayer: "layer-1" };
1028
+ return { overall: { status: "cannot-attest", score: null, threshold, reason: "no scoreable mutants" }, decisiveLayer: "none" };
1029
+ }
1030
+
1031
+ // src/headroom.ts
1032
+ function zFor(confidence) {
1033
+ if (confidence >= 0.99) return 2.5758;
1034
+ if (confidence >= 0.95) return 1.96;
1035
+ if (confidence >= 0.9) return 1.6449;
1036
+ return 1.96;
1037
+ }
1038
+ function wilsonInterval(successes, n, confidence = 0.95) {
1039
+ if (n <= 0) return { lower: 0, upper: 1 };
1040
+ const z = zFor(confidence);
1041
+ const p = successes / n;
1042
+ const z2 = z * z;
1043
+ const denom = 1 + z2 / n;
1044
+ const centre = p + z2 / (2 * n);
1045
+ const spread = z * Math.sqrt(p * (1 - p) / n + z2 / (4 * n * n));
1046
+ return {
1047
+ lower: Math.max(0, (centre - spread) / denom),
1048
+ upper: Math.min(1, (centre + spread) / denom)
1049
+ };
1050
+ }
1051
+ var pct2 = (x) => Number((100 * x).toFixed(1));
1052
+ function classicHeadroom(omissions, opts) {
1053
+ const confidence = opts.confidence ?? 0.95;
1054
+ const total = omissions.length;
1055
+ const classicMissed = omissions.filter((o) => !o.classicDetected).length;
1056
+ if (total === 0) {
1057
+ return {
1058
+ total: 0,
1059
+ classicMissed: 0,
1060
+ ceilingPct: null,
1061
+ lowerPct: null,
1062
+ upperPct: null,
1063
+ confidence,
1064
+ marginPct: opts.marginPct,
1065
+ // An empty corpus is not a pass. Treating "no evidence" as "proceed" would let the programme
1066
+ // start on the strength of having measured nothing at all.
1067
+ verdict: "inconclusive",
1068
+ reason: "no qualified omissions in the frozen corpus \u2014 nothing to bound"
1069
+ };
1070
+ }
1071
+ const { lower, upper } = wilsonInterval(classicMissed, total, confidence);
1072
+ const ceilingPct = pct2(classicMissed / total);
1073
+ const lowerPct = pct2(lower);
1074
+ const upperPct = pct2(upper);
1075
+ if (upperPct < opts.marginPct) {
1076
+ return {
1077
+ total,
1078
+ classicMissed,
1079
+ ceilingPct,
1080
+ lowerPct,
1081
+ upperPct,
1082
+ confidence,
1083
+ marginPct: opts.marginPct,
1084
+ verdict: "stop",
1085
+ reason: `classic already detects ${total - classicMissed} of ${total} qualified omissions, so the maximum possible realistic-only rate is ${ceilingPct}% (upper bound ${upperPct}%), below the declared margin of ${opts.marginPct}% \u2014 no model can clear it; stop before Stage B`
1086
+ };
1087
+ }
1088
+ if (lowerPct >= opts.marginPct) {
1089
+ return {
1090
+ total,
1091
+ classicMissed,
1092
+ ceilingPct,
1093
+ lowerPct,
1094
+ upperPct,
1095
+ confidence,
1096
+ marginPct: opts.marginPct,
1097
+ verdict: "proceed",
1098
+ reason: `classic misses ${classicMissed} of ${total} qualified omissions; the ceiling is ${ceilingPct}% (lower bound ${lowerPct}%), at or above the declared margin of ${opts.marginPct}%`
1099
+ };
1100
+ }
1101
+ return {
1102
+ total,
1103
+ classicMissed,
1104
+ ceilingPct,
1105
+ lowerPct,
1106
+ upperPct,
1107
+ confidence,
1108
+ marginPct: opts.marginPct,
1109
+ verdict: "inconclusive",
1110
+ reason: `ceiling ${ceilingPct}% with a ${Math.round(confidence * 100)}% interval of ${lowerPct}\u2013${upperPct}% straddles the declared margin of ${opts.marginPct}% \u2014 ${total} qualified omissions cannot settle it; enlarge the corpus rather than reading the point estimate`
1111
+ };
1112
+ }
1113
+ function conditionalPower(informativeN, opts) {
1114
+ const confidence = opts.confidence ?? 0.95;
1115
+ const margin = opts.marginPct / 100;
1116
+ if (informativeN <= 0) {
1117
+ return {
1118
+ informativeN: 0,
1119
+ marginPct: opts.marginPct,
1120
+ confidence,
1121
+ detectionsNeeded: null,
1122
+ bestPossibleLowerPct: 0,
1123
+ verdict: "underpowered",
1124
+ reason: "classic missed nothing \u2014 there are no omissions on which the method could add anything"
1125
+ };
1126
+ }
1127
+ const bestPossibleLowerPct = pct2(wilsonInterval(informativeN, informativeN, confidence).lower);
1128
+ let detectionsNeeded = null;
1129
+ for (let k = 0; k <= informativeN; k++) {
1130
+ if (wilsonInterval(k, informativeN, confidence).lower >= margin) {
1131
+ detectionsNeeded = k;
1132
+ break;
1133
+ }
1134
+ }
1135
+ if (detectionsNeeded === null) {
1136
+ return {
1137
+ informativeN,
1138
+ marginPct: opts.marginPct,
1139
+ confidence,
1140
+ detectionsNeeded: null,
1141
+ bestPossibleLowerPct,
1142
+ verdict: "underpowered",
1143
+ reason: `with ${informativeN} classic-missed omission(s), even a perfect ${informativeN}/${informativeN} result has a lower bound of ${bestPossibleLowerPct}%, below the declared margin of ${opts.marginPct}% \u2014 the bar is unreachable by construction; enlarge the corpus or lower the margin BEFORE running`
1144
+ };
1145
+ }
1146
+ if (detectionsNeeded >= informativeN) {
1147
+ return {
1148
+ informativeN,
1149
+ marginPct: opts.marginPct,
1150
+ confidence,
1151
+ detectionsNeeded,
1152
+ bestPossibleLowerPct,
1153
+ verdict: "underpowered",
1154
+ reason: `only a perfect ${informativeN}/${informativeN} clears the ${opts.marginPct}% margin, so a single miss \u2014 from one flaky item \u2014 reads as failure for a method that works; there is no power against any realistic effect`
1155
+ };
1156
+ }
1157
+ return {
1158
+ informativeN,
1159
+ marginPct: opts.marginPct,
1160
+ confidence,
1161
+ detectionsNeeded,
1162
+ bestPossibleLowerPct,
1163
+ verdict: "adequate",
1164
+ reason: `${detectionsNeeded} of ${informativeN} classic-missed omissions would clear the ${opts.marginPct}% margin at ${Math.round(confidence * 100)}% confidence, with room to miss ${informativeN - detectionsNeeded}`
1165
+ };
1166
+ }
1167
+
1168
+ // src/historical-pairs.ts
1169
+ function stableOutcome(cell) {
1170
+ if (cell.outcomes.length === 0) return null;
1171
+ const first = cell.outcomes[0];
1172
+ return cell.outcomes.every((o) => o === first) ? first : null;
1173
+ }
1174
+ var MIN_CELL_REPEATS = 3;
1175
+ function qualifyHistoricalPair(pair, opts = {}) {
1176
+ const minRepeats = opts.minRepeats ?? MIN_CELL_REPEATS;
1177
+ const reasons = [];
1178
+ if (pair.isolation.differingTestHunks < 1) {
1179
+ reasons.push("strong and weak do not differ by any test hunk \u2014 there is no regression delta to measure");
1180
+ }
1181
+ if (pair.isolation.strongDigestExcludingHunk !== pair.isolation.weakDigestExcludingHunk) {
1182
+ reasons.push(
1183
+ "everything outside the differing test hunk must be byte-identical (production files, helpers, fixtures, config, dependencies, every other test) \u2014 the digests differ"
1184
+ );
1185
+ }
1186
+ const cells = [
1187
+ // [label, cell, required outcome, is this the cell that must fail by the intended assertion]
1188
+ ["fixed code under the STRONG suite", pair.fixedUnderStrong, "pass", false],
1189
+ ["fixed code under the WEAK suite", pair.fixedUnderWeak, "pass", false],
1190
+ ["the historical fault under the STRONG suite", pair.faultUnderStrong, "fail", true],
1191
+ ["the historical fault under the WEAK suite", pair.faultUnderWeak, "pass", false]
1192
+ ];
1193
+ for (const [label, cell, required, needsAssertion] of cells) {
1194
+ if (cell.outcomes.length < minRepeats) {
1195
+ reasons.push(`${label}: ${cell.outcomes.length} repeat(s), need at least ${minRepeats} to rule out flakiness`);
1196
+ continue;
1197
+ }
1198
+ const stable = stableOutcome(cell);
1199
+ if (stable === null) {
1200
+ reasons.push(`${label}: repeats disagree (${cell.outcomes.join(", ")}) \u2014 a flaky cell cannot support a verdict`);
1201
+ continue;
1202
+ }
1203
+ if (stable === "error") {
1204
+ reasons.push(`${label}: harness error, which is not evidence about the tests`);
1205
+ continue;
1206
+ }
1207
+ if (stable !== required) {
1208
+ if (label.includes("fault") && label.includes("WEAK") && stable === "fail") {
1209
+ reasons.push(
1210
+ "the historical fault still FAILS under the weak suite \u2014 removing that test did not create the claimed omission, so this item proves nothing"
1211
+ );
1212
+ } else if (label.includes("fault") && label.includes("STRONG") && stable === "pass") {
1213
+ reasons.push(
1214
+ "the historical fault PASSES under the strong suite \u2014 the regression test does not actually catch the bug it shipped with"
1215
+ );
1216
+ } else {
1217
+ reasons.push(`${label}: expected ${required}, observed ${stable}`);
1218
+ }
1219
+ continue;
1220
+ }
1221
+ if (needsAssertion && cell.failedByIntendedAssertion !== true) {
1222
+ reasons.push(
1223
+ "the historical fault fails under strong, but NOT by the intended assertion \u2014 a timeout, build error or unrelated flaky test is the harness noticing damage, which any mutation achieves"
1224
+ );
1225
+ }
1226
+ }
1227
+ return { id: pair.id, verdict: reasons.length === 0 ? "qualified" : "rejected", reasons };
1228
+ }
1229
+ var EXPECTED_ORACLE_CELL = "strong-fail-weak-pass";
1230
+ function evaluateAssay(observations) {
1231
+ const failures = [];
1232
+ for (const o of observations) {
1233
+ if (!o.reachedExecution) {
1234
+ failures.push({
1235
+ pairId: o.pairId,
1236
+ reason: "the oracle mutant never reached execution \u2014 a structural gate rejected the real bug itself"
1237
+ });
1238
+ continue;
1239
+ }
1240
+ if (o.observed !== EXPECTED_ORACLE_CELL) {
1241
+ failures.push({
1242
+ pairId: o.pairId,
1243
+ reason: `the real historical bug landed in ${o.observed}, not ${EXPECTED_ORACLE_CELL} \u2014 the pipeline cannot detect a known fault`
1244
+ });
1245
+ continue;
1246
+ }
1247
+ if (!o.attributedToIntendedAssertion) {
1248
+ failures.push({
1249
+ pairId: o.pairId,
1250
+ reason: "the oracle mutant reached the expected cell, but the failure was not attributed to the intended assertion"
1251
+ });
1252
+ }
1253
+ }
1254
+ if (observations.length === 0) {
1255
+ return { verdict: "invalid", checked: 0, failures: [], reason: "no oracle observations \u2014 the positive control never ran" };
1256
+ }
1257
+ return failures.length === 0 ? {
1258
+ verdict: "valid",
1259
+ checked: observations.length,
1260
+ failures: [],
1261
+ reason: `all ${observations.length} oracle mutants landed in ${EXPECTED_ORACLE_CELL}, attributed to the intended assertion`
1262
+ } : {
1263
+ verdict: "invalid",
1264
+ checked: observations.length,
1265
+ failures,
1266
+ reason: `${failures.length} of ${observations.length} oracle mutants did not behave as a perfect proposal must \u2014 the assay cannot measure any model until this is fixed`
1267
+ };
1268
+ }
1269
+ function qualifyCorpus(pairs, opts = {}) {
1270
+ const results = pairs.map((p) => qualifyHistoricalPair(p, opts));
1271
+ return {
1272
+ qualified: results.filter((r) => r.verdict === "qualified"),
1273
+ rejected: results.filter((r) => r.verdict === "rejected")
1274
+ };
1275
+ }
1276
+
1277
+ // src/experiment-manifest.ts
1278
+ var PLACEHOLDER = /^(tbd|todo|xxx|\?+|n\/?a|placeholder|fixme)$/i;
1279
+ function blank(v) {
1280
+ if (v === null || v === void 0) return true;
1281
+ if (typeof v === "string") return v.trim().length === 0 || PLACEHOLDER.test(v.trim());
1282
+ if (Array.isArray(v)) return v.length === 0;
1283
+ return false;
1284
+ }
1285
+ var HEX64 = /^[0-9a-f]{64}$/;
1286
+ function validateManifest(m) {
1287
+ const problems = [];
1288
+ if (!m) return { frozen: false, problems: ["no manifest \u2014 nothing has been preregistered"] };
1289
+ if (m.schema !== "attest-benchmark-manifest/v1") problems.push('schema must be "attest-benchmark-manifest/v1"');
1290
+ if (blank(m.frozenAt)) problems.push("frozenAt is required \u2014 a manifest with no freeze date cannot be shown to predate the data");
1291
+ for (const [field, v] of [
1292
+ ["corpusHash", m.corpusHash],
1293
+ ["analysisCodeHash", m.analysisCodeHash],
1294
+ ["promptHash", m.promptHash]
1295
+ ]) {
1296
+ if (blank(v)) problems.push(`${field} is required \u2014 without it nobody can prove the experiment ran on what it claims`);
1297
+ else if (!HEX64.test(String(v))) problems.push(`${field} must be a 64-character hex digest`);
1298
+ }
1299
+ if (blank(m.models)) problems.push("models must be enumerated \u2014 'the strong ones' is not a preregistration");
1300
+ else {
1301
+ m.models.forEach((mm, i) => {
1302
+ if (blank(mm?.id)) problems.push(`models[${i}].id is required (the exact identifier sent on the wire)`);
1303
+ if (!mm?.provider) problems.push(`models[${i}].provider is required so the cost basis is unambiguous`);
1304
+ });
1305
+ }
1306
+ const positives = [
1307
+ ["corpusClusters", m.corpusClusters],
1308
+ ["drawsPerCell", m.drawsPerCell],
1309
+ ["minimumUsefulMarginPct", m.minimumUsefulMarginPct],
1310
+ ["stageBValidOutputFraction", m.stageBValidOutputFraction],
1311
+ ["reviewerRealismThresholdPct", m.reviewerRealismThresholdPct],
1312
+ ["proposalSlotsPerOmission", m.proposalSlotsPerOmission]
1313
+ ];
1314
+ for (const [field, v] of positives) {
1315
+ if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
1316
+ problems.push(`${field} must be a positive number, decided before any data exists`);
1317
+ }
1318
+ }
1319
+ if (typeof m.heldOutClusters !== "number" || !Number.isFinite(m.heldOutClusters) || m.heldOutClusters < 0) {
1320
+ problems.push("heldOutClusters must be a number (0 is allowed, but only if stated deliberately)");
1321
+ }
1322
+ if (typeof m.confidence !== "number" || !(m.confidence > 0.5 && m.confidence < 1)) {
1323
+ problems.push("confidence must be between 0.5 and 1 (e.g. 0.95)");
1324
+ }
1325
+ if (typeof m.stageBValidOutputFraction === "number" && m.stageBValidOutputFraction > 1) {
1326
+ problems.push("stageBValidOutputFraction is a fraction of 1, not a percentage");
1327
+ }
1328
+ if (!m.multiplicityProcedure) {
1329
+ problems.push("multiplicityProcedure is required \u2014 comparing six models is a family, and ignoring that inflates false positives");
1330
+ }
1331
+ if (blank(m.topSelectionRule)) problems.push("topSelectionRule must be a stated procedure, not a decision made after screening");
1332
+ if (blank(m.categoryQuotas) || Object.keys(m.categoryQuotas ?? {}).length === 0) {
1333
+ problems.push("categoryQuotas are required \u2014 without them category is confounded with repository");
1334
+ }
1335
+ if (blank(m.exclusionOrder)) {
1336
+ problems.push("exclusionOrder is required \u2014 replacing an inconvenient item after seeing its result is the most direct way to fake a finding");
1337
+ }
1338
+ if (blank(m.stopRules)) problems.push("stopRules must be written down before they can be applied honestly");
1339
+ return { frozen: problems.length === 0, problems };
1340
+ }
1341
+ function canStartStageB(inputs) {
1342
+ const blockers = [];
1343
+ const manifest = validateManifest(inputs.manifest);
1344
+ if (!manifest.frozen) {
1345
+ blockers.push(...manifest.problems.map((p) => `manifest: ${p}`));
1346
+ }
1347
+ if (!inputs.headroom) {
1348
+ blockers.push("classic-headroom has not been computed \u2014 it is free and determines whether the corpus can measure anything");
1349
+ }
1350
+ if (!inputs.power) {
1351
+ blockers.push("conditional power has not been computed \u2014 nobody has checked the declared margin is testable on this corpus");
1352
+ } else if (inputs.power.verdict === "underpowered") {
1353
+ blockers.push(`underpowered: ${inputs.power.reason}`);
1354
+ }
1355
+ if (!inputs.assay) {
1356
+ blockers.push("the perfect-proposer positive control has not been run \u2014 the instrument is unverified");
1357
+ } else if (inputs.assay.verdict === "invalid") {
1358
+ blockers.push(`assay invalid: ${inputs.assay.reason}`);
1359
+ }
1360
+ if (!inputs.qualification) {
1361
+ blockers.push("historical pairs have not been qualified \u2014 the corpus is unverified");
1362
+ } else if (inputs.qualification.qualified.length === 0) {
1363
+ blockers.push("no historical pair survived qualification \u2014 there is nothing to measure against");
1364
+ }
1365
+ if (!inputs.deterministicBaselineRun) {
1366
+ blockers.push(
1367
+ "the deterministic operator baseline has not been run \u2014 without it a model win cannot be distinguished from Stryker's operator set simply being incomplete"
1368
+ );
1369
+ }
1370
+ return { ready: blockers.length === 0, blockers };
1371
+ }
1372
+
1373
+ // src/policy.ts
1374
+ import { readFileSync, existsSync, realpathSync } from "fs";
1375
+ import { dirname } from "path";
1376
+ import { homedir } from "os";
1377
+ import { parse as parseYaml } from "yaml";
1378
+
1379
+ // src/ci-property-search.ts
1380
+ import { createHash } from "crypto";
1381
+ var CI_PROPERTY_AUTHORITIES = [
1382
+ "machine-sourced",
1383
+ "source-grounded"
1384
+ ];
1385
+ var CI_PROPERTY_OUTCOMES = [
1386
+ "pr-regression",
1387
+ "new-contract-counterexample",
1388
+ "source-grounded-test-gap",
1389
+ "pre-existing-counterexample",
1390
+ "possible-duplicate",
1391
+ "no-counterexample-found",
1392
+ "disagreement",
1393
+ "scenario-uncovered",
1394
+ "inconclusive",
1395
+ "not-applicable"
1396
+ ];
1397
+ var CI_PROPERTY_STATES = [
1398
+ "completed",
1399
+ "partial",
1400
+ "unavailable",
1401
+ "disabled",
1402
+ "disabled-by-tier",
1403
+ "not-run"
1404
+ ];
1405
+ var CI_RESIDUAL_STATES = [
1406
+ "covered-existing",
1407
+ "proposed-closed",
1408
+ "open",
1409
+ "inconclusive"
1410
+ ];
1411
+ var CI_PROPERTY_CAPS = [1, 2, 4, 8];
1412
+ var HEX642 = /^[a-f0-9]{64}$/;
1413
+ var SHA40 = /^[a-f0-9]{40}$/;
1414
+ var CONTROL = /[\u0000-\u001f\u007f]/;
1415
+ function sha256(value) {
1416
+ return createHash("sha256").update(value, "utf8").digest("hex");
1417
+ }
1418
+ function boundedIdentity(name, value) {
1419
+ if (value.length === 0 || value.length > 500 || CONTROL.test(value)) {
1420
+ throw new Error(`${name} must be a bounded printable identity`);
1421
+ }
1422
+ }
1423
+ function opaqueIdentity(name, value) {
1424
+ if (value.length === 0 || value.length > 1e4) {
1425
+ throw new Error(`${name} must be a bounded non-empty identity`);
1426
+ }
1427
+ }
1428
+ function digest(name, value) {
1429
+ if (!HEX642.test(value)) throw new Error(`${name} must be a lowercase sha256 digest`);
1430
+ }
1431
+ function nonNegativeInteger(name, value) {
1432
+ if (!Number.isInteger(value) || value < 0) {
1433
+ throw new Error(`${name} must be a non-negative integer`);
1434
+ }
1435
+ }
1436
+ function canonical(value) {
1437
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1438
+ if (value && typeof value === "object") {
1439
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
1440
+ }
1441
+ return JSON.stringify(value);
1442
+ }
1443
+ function ciPropertyLedgerDigest(ledger) {
1444
+ return sha256(canonical(ledger));
1445
+ }
1446
+ function validateCiResidualGapLedger(input) {
1447
+ if (input.schema !== "attest-ci-residual-ledger/v1") {
1448
+ throw new Error("CI residual ledger has an unknown schema");
1449
+ }
1450
+ if (!SHA40.test(input.headSha) || !SHA40.test(input.mergeBaseSha)) {
1451
+ throw new Error("CI residual ledger requires full lowercase HEAD and merge-base SHAs");
1452
+ }
1453
+ digest("policyDigest", input.policyDigest);
1454
+ const identities = /* @__PURE__ */ new Set();
1455
+ for (const [index, entry] of input.entries.entries()) {
1456
+ boundedIdentity(`entries[${index}].gapIdentity`, entry.gapIdentity);
1457
+ boundedIdentity(`entries[${index}].targetPath`, entry.targetPath);
1458
+ boundedIdentity(`entries[${index}].targetSymbol`, entry.targetSymbol);
1459
+ boundedIdentity(`entries[${index}].producerVersion`, entry.producerVersion);
1460
+ if (entry.headSha !== input.headSha || entry.mergeBaseSha !== input.mergeBaseSha) {
1461
+ throw new Error("every CI residual entry must belong to the ledger revisions");
1462
+ }
1463
+ for (const [name, value] of [
1464
+ ["targetDigest", entry.targetDigest],
1465
+ ["sourceDigest", entry.sourceDigest]
1466
+ ]) {
1467
+ digest(`entries[${index}].${name}`, value);
1468
+ }
1469
+ if (!Number.isInteger(entry.changedRange.startLine) || !Number.isInteger(entry.changedRange.endLine) || entry.changedRange.startLine < 1 || entry.changedRange.endLine < entry.changedRange.startLine) {
1470
+ throw new Error(`entries[${index}].changedRange is invalid`);
1471
+ }
1472
+ if (identities.has(entry.gapIdentity)) {
1473
+ throw new Error(`duplicate CI residual identity '${entry.gapIdentity}'`);
1474
+ }
1475
+ identities.add(entry.gapIdentity);
1476
+ if (entry.kind === "patch-revert") {
1477
+ if (!entry.patchUnitId || !entry.patchDigest) {
1478
+ throw new Error("patch-revert residuals require unit and patch identities");
1479
+ }
1480
+ boundedIdentity("patchUnitId", entry.patchUnitId);
1481
+ digest("patchDigest", entry.patchDigest);
1482
+ } else if (entry.kind !== "unsupported-site") {
1483
+ if (!entry.mutantIdentity) {
1484
+ throw new Error(`${entry.kind} residuals require mutantIdentity`);
1485
+ }
1486
+ opaqueIdentity("mutantIdentity", entry.mutantIdentity);
1487
+ }
1488
+ if (entry.state === "proposed-closed") {
1489
+ if (entry.proofVerdict !== "proven" || entry.wholeSuiteVerdict !== "green" || !entry.candidateDigest || !entry.candidateTestPath || !entry.candidateTestName) {
1490
+ throw new Error(
1491
+ "proposed-closed requires a proven, whole-suite-green candidate with local identity"
1492
+ );
1493
+ }
1494
+ digest("candidateDigest", entry.candidateDigest);
1495
+ boundedIdentity("candidateTestPath", entry.candidateTestPath);
1496
+ boundedIdentity("candidateTestName", entry.candidateTestName);
1497
+ }
1498
+ }
1499
+ const expected = ciPropertyLedgerDigest({
1500
+ schema: input.schema,
1501
+ headSha: input.headSha,
1502
+ mergeBaseSha: input.mergeBaseSha,
1503
+ policyDigest: input.policyDigest,
1504
+ entries: input.entries
1505
+ });
1506
+ if (input.ledgerDigest !== expected) {
1507
+ throw new Error("CI residual ledger digest does not bind the ordered entries");
1508
+ }
1509
+ return {
1510
+ ...input,
1511
+ entries: input.entries.map((entry) => ({
1512
+ ...entry,
1513
+ changedRange: { ...entry.changedRange }
1514
+ }))
1515
+ };
1516
+ }
1517
+ function isCiPropertyCandidateCap(value) {
1518
+ return CI_PROPERTY_CAPS.includes(value);
1519
+ }
1520
+ function sanitizeCiPropertySummary(input) {
1521
+ if (!CI_PROPERTY_OUTCOMES.includes(input.outcome)) {
1522
+ throw new Error("CI property summary has an unknown outcome");
1523
+ }
1524
+ if (!CI_PROPERTY_AUTHORITIES.includes(input.authority)) {
1525
+ throw new Error("CI property summary has an unknown authority");
1526
+ }
1527
+ for (const [name, value] of [
1528
+ ["findingId", input.findingId],
1529
+ ["targetDigest", input.targetDigest],
1530
+ ["intentDigest", input.intentDigest],
1531
+ ["behaviourKey", input.behaviourKey],
1532
+ ["sourceSetDigest", input.sourceSetDigest]
1533
+ ]) {
1534
+ digest(name, value);
1535
+ }
1536
+ if (input.candidateDigest !== null) digest("candidateDigest", input.candidateDigest);
1537
+ nonNegativeInteger("headReplays", input.headReplays);
1538
+ nonNegativeInteger("comparisonReplays", input.comparisonReplays);
1539
+ const differentialFinding = input.outcome === "pr-regression" || input.outcome === "source-grounded-test-gap";
1540
+ if (differentialFinding) {
1541
+ if (!input.targetExecuted || !input.assertionExecuted || input.headReplays !== 3 || input.comparisonReplays !== 3 || input.candidateDigest === null) {
1542
+ throw new Error("a CI property finding requires target, assertion, candidate, and 3/3 replay proof");
1543
+ }
1544
+ }
1545
+ if (input.outcome === "new-contract-counterexample" && (!input.targetExecuted || !input.assertionExecuted || input.headReplays !== 3 || input.comparisonReplays !== 0 || input.candidateDigest === null)) {
1546
+ throw new Error(
1547
+ "a new-contract counterexample requires a 3/3 HEAD proof and an absent comparison target"
1548
+ );
1549
+ }
1550
+ if (input.outcome === "no-counterexample-found" && (!input.targetExecuted || !input.assertionExecuted)) {
1551
+ throw new Error("no-counterexample-found requires target and assertion execution");
1552
+ }
1553
+ return {
1554
+ findingId: input.findingId,
1555
+ outcome: input.outcome,
1556
+ authority: input.authority,
1557
+ targetDigest: input.targetDigest,
1558
+ intentDigest: input.intentDigest,
1559
+ behaviourKey: input.behaviourKey,
1560
+ sourceSetDigest: input.sourceSetDigest,
1561
+ headReplays: input.headReplays,
1562
+ comparisonReplays: input.comparisonReplays,
1563
+ targetExecuted: input.targetExecuted,
1564
+ assertionExecuted: input.assertionExecuted,
1565
+ candidateDigest: input.candidateDigest
1566
+ };
1567
+ }
1568
+ function buildCiPropertySearchBlock(input) {
1569
+ if (!isCiPropertyCandidateCap(input.candidateCap)) {
1570
+ throw new Error("candidateCap must be one of 1, 2, 4, or 8");
1571
+ }
1572
+ for (const [name, value] of [
1573
+ ["selectedTargets", input.selectedTargets],
1574
+ ["validatedRules", input.validatedRules],
1575
+ ["executedRules", input.executedRules],
1576
+ ["duplicatesSuppressed", input.duplicatesSuppressed],
1577
+ ["expectedSummaries", input.expectedSummaries],
1578
+ ["llmCalls", input.llmCalls],
1579
+ ["wallMs", input.wallMs]
1580
+ ]) {
1581
+ nonNegativeInteger(name, value);
1582
+ }
1583
+ if (input.selectedTargets > 2) throw new Error("CI property search may select at most two targets");
1584
+ if (input.executedRules > input.validatedRules) {
1585
+ throw new Error("executedRules cannot exceed validatedRules");
1586
+ }
1587
+ if (input.executedRules > input.selectedTargets * input.candidateCap) {
1588
+ throw new Error("executedRules exceeds the target and candidate caps");
1589
+ }
1590
+ if (input.costUsd !== null && (!Number.isFinite(input.costUsd) || input.costUsd < 0)) {
1591
+ throw new Error("costUsd must be null or a non-negative finite number");
1592
+ }
1593
+ for (const [name, value] of [
1594
+ ["ledgerDigest", input.ledgerDigest],
1595
+ ["evidencePackDigest", input.evidencePackDigest],
1596
+ ["proofsDigest", input.proofsDigest]
1597
+ ]) {
1598
+ digest(name, value);
1599
+ }
1600
+ const containerDigests = [...new Set(input.containerDigests)];
1601
+ for (const value of containerDigests) digest("containerDigest", value);
1602
+ for (const model of input.models) boundedIdentity("model", model);
1603
+ const summaries = input.summaries.map(sanitizeCiPropertySummary);
1604
+ const ids = new Set(summaries.map((summary) => summary.findingId));
1605
+ if (ids.size !== summaries.length) {
1606
+ throw new Error("CI property summary identities must be unique");
1607
+ }
1608
+ if (summaries.length > input.expectedSummaries) {
1609
+ throw new Error("CI property summaries exceed expected measured rules");
1610
+ }
1611
+ const byOutcome = Object.fromEntries(
1612
+ CI_PROPERTY_OUTCOMES.map((outcome) => [outcome, 0])
1613
+ );
1614
+ for (const summary of summaries) byOutcome[summary.outcome] += 1;
1615
+ const findings = byOutcome["pr-regression"] + byOutcome["new-contract-counterexample"] + byOutcome["source-grounded-test-gap"];
1616
+ if (findings > 2) throw new Error("CI property output may contain at most two findings");
1617
+ return {
1618
+ state: summaries.length === input.expectedSummaries ? "completed" : "partial",
1619
+ mode: input.mode,
1620
+ candidateCap: input.candidateCap,
1621
+ counts: {
1622
+ selectedTargets: input.selectedTargets,
1623
+ validatedRules: input.validatedRules,
1624
+ executedRules: input.executedRules,
1625
+ findings,
1626
+ duplicatesSuppressed: input.duplicatesSuppressed,
1627
+ byOutcome
1628
+ },
1629
+ summaries,
1630
+ ledgerDigest: input.ledgerDigest,
1631
+ evidencePackDigest: input.evidencePackDigest,
1632
+ proofsDigest: input.proofsDigest,
1633
+ containerDigests,
1634
+ models: [...input.models],
1635
+ llmCalls: input.llmCalls,
1636
+ costUsd: input.costUsd,
1637
+ wallMs: input.wallMs
1638
+ };
1639
+ }
1640
+
1641
+ // src/policy.ts
1642
+ var CLUSTER_STRATEGIES = ["line", "structural"];
1643
+ function isClusterStrategy(v) {
1644
+ return typeof v === "string" && CLUSTER_STRATEGIES.includes(v);
1645
+ }
1646
+ var DEFAULT_CLASSIC_MUTATION = {
1647
+ /**
1648
+ * Triage runs on gpt-5.6-terra at `high`.
1649
+ *
1650
+ * Benchmarked, not chosen: at `high` it made zero wrong equivalence calls and at `medium` it made
1651
+ * two — which is why the effort is pinned here rather than left to the provider default. The
1652
+ * effort is part of the classifier IDENTITY (it is baked into every cache key and triage record),
1653
+ * so a run at a different effort is a different classifier, not the same one tuned.
1654
+ *
1655
+ * Only tier 1+ ever reaches this; tier 0 makes no model calls at all.
1656
+ */
1657
+ models: {
1658
+ triage: { provider: "hosted", model: "gpt-5.6-terra", effort: "high" },
1659
+ // The fix-loop generation benchmark was run with Sol at xhigh; pin that exact model identity.
1660
+ fixLoop: { provider: "hosted", model: "gpt-5.6-sol", effort: "xhigh" },
1661
+ /**
1662
+ * Gap NAMING runs terra at `xhigh`.
1663
+ *
1664
+ * Measured on 65 real surviving mutants across 4 node-cron files: 65 of 65 named, with zero
1665
+ * vague nouns and zero names joining two behaviours with "and" — the two failure patterns
1666
+ * Kenneth identified when grading an earlier run by hand.
1667
+ *
1668
+ * Effort matters more here than elsewhere and the evidence is partly indirect. On the older
1669
+ * clustering prompt terra scored 88% at `high` and 95% at `xhigh`; at `xhigh` it also declined a
1670
+ * merge it had got WRONG at `high` (it had labelled `if (s.endsWith('Z')) return 0` as "parsing
1671
+ * nonzero offsets" — the opposite branch) and split it into two correct names instead. That is
1672
+ * the model catching its own error class, which is the behaviour worth paying for.
1673
+ *
1674
+ * HONEST LIMIT: the naming-only prompt was never run at `high`, so `xhigh` is proven adequate,
1675
+ * not proven necessary — some of the 88%→65/65 gain belongs to the prompt rather than the
1676
+ * effort. It costs roughly 2x latency (52-93s per file against 13-39s). Kenneth pinned xhigh
1677
+ * deliberately with that tradeoff stated.
1678
+ */
1679
+ naming: { provider: "hosted", model: "gpt-5.6-terra", effort: "xhigh" },
1680
+ layer2Intent: { provider: "hosted", model: "gpt-5.6-sol", effort: "high" },
1681
+ layer2Test: { provider: "hosted", model: "gpt-5.6-sol", effort: "high" },
1682
+ // CI invariance is one Terra-XHIGH mechanism: both model tasks share the same frozen identity.
1683
+ //
1684
+ // Measured 2026-08-05, four arms x three runs on ts-pattern through the production path, the
1685
+ // only axis varying being this default:
1686
+ //
1687
+ // arm real verdicts rules validated (avg) stage time (avg)
1688
+ // terra-high 1 / 3 1.33 98 s
1689
+ // terra-xhigh 3 / 3 2.33 123 s
1690
+ // sol-high 3 / 3 3.67 175 s
1691
+ // sol-xhigh 0 / 3 1.33 204 s (2 runs hit the ceiling)
1692
+ //
1693
+ // terra-high — what shipped before this line changed — produced a usable verdict in one run of
1694
+ // three: once inconclusive, once validating no rule at all, so there was nothing to execute.
1695
+ // sol-xhigh is worse still, blowing perCandidateDeadlineMs twice and returning inconclusive on
1696
+ // the run that did finish. Both remaining arms went 3/3.
1697
+ //
1698
+ // sol-high yields 57% more rules and terra-xhigh runs 42% faster. At the shipped
1699
+ // `candidateCap: 1` every rule past the first is DISCARDED, so sol-high's yield is currently
1700
+ // unrealisable while its latency is paid on every pull request — hence terra-xhigh. Re-run the
1701
+ // sweep before trusting that ordering at a raised cap, where pool size is the whole point.
1702
+ //
1703
+ // RE-RUN at cap 8 on the v7 prompts (2026-08-05 evening, planted-regression fixture, 3 runs
1704
+ // per arm):
1705
+ //
1706
+ // arm proposed/kept executed findings deadline hits
1707
+ // terra-xhigh 2, 5, 4 2, 2, 4 1 1 of 3
1708
+ // sol-high 8, 8, 8 7, 6, 4 0 3 of 3
1709
+ //
1710
+ // The prediction inverted. sol-high's yield advantage became full 8-rule pools and produced
1711
+ // nothing — its extra rules were passing properties of unmeasured strength, and every one of
1712
+ // its runs burned into the 600 s stage deadline. terra-xhigh proposed less and produced the
1713
+ // lane's FIRST model-generated pr-regression (counterexample [null,null], 3/3 head, 3/3
1714
+ // base). At N=3 per arm that is directional rather than decisive, but there is currently no
1715
+ // axis on which sol-high wins: findings terra, latency terra, container cost terra. Default
1716
+ // stays terra-xhigh; re-examine only with wide-measurement data across several repositories.
1717
+ ciPropertyIntent: { provider: "hosted", model: "gpt-5.6-terra", effort: "xhigh" },
1718
+ ciPropertyTest: { provider: "hosted", model: "gpt-5.6-terra", effort: "xhigh" }
1719
+ },
1720
+ deterministicMutants: { enabled: false, maxRun: 25 },
1721
+ perTest: { enabled: true, fullSuiteAuditSampleSize: null, fullSuiteMismatchTolerance: null },
1722
+ fixLoop: { enabled: true, maxCandidates: 10, proofRepetitions: 2, suiteConfirmation: "inline", syntheticEnvironment: {} },
1723
+ errorPaths: { staticAnalysis: true, failOnUntested: false, failOnAntiPattern: false, forceHandlerMutation: false }
1724
+ };
1725
+ function cloneClassicMutation() {
1726
+ return {
1727
+ // deep-copied per task, or a policy file setting `models.triage.effort` would write through to
1728
+ // the module-level default and change the classifier identity for every later load
1729
+ ...DEFAULT_CLASSIC_MUTATION.models ? { models: Object.fromEntries(Object.entries(DEFAULT_CLASSIC_MUTATION.models).map(([task, m]) => [task, { ...m }])) } : {},
1730
+ deterministicMutants: { ...DEFAULT_CLASSIC_MUTATION.deterministicMutants },
1731
+ perTest: { ...DEFAULT_CLASSIC_MUTATION.perTest },
1732
+ fixLoop: {
1733
+ ...DEFAULT_CLASSIC_MUTATION.fixLoop,
1734
+ syntheticEnvironment: { ...DEFAULT_CLASSIC_MUTATION.fixLoop.syntheticEnvironment ?? {} }
1735
+ },
1736
+ errorPaths: { ...DEFAULT_CLASSIC_MUTATION.errorPaths }
1737
+ };
1738
+ }
1739
+ var DEFAULT_MECHANISMS = {
1740
+ scenarioSearch: {
1741
+ enabled: false,
1742
+ profile: "research",
1743
+ lanes: ["property"],
1744
+ sourceModes: ["machine-sourced", "source-grounded", "corroborated", "code-inferred"],
1745
+ maxTargets: 5,
1746
+ maxCandidatesPerTarget: 5,
1747
+ maxAgentTurnsPerTarget: 100,
1748
+ maxModelCalls: 100,
1749
+ totalDeadlineMs: 36e5,
1750
+ executionDeadlineMs: 12e4,
1751
+ replayRepetitions: 3,
1752
+ requireContainer: true,
1753
+ failOnCounterexample: false
1754
+ },
1755
+ ciPropertySearch: {
1756
+ // The bounded CI path is customer-facing and advisory: source-grounded model proposals are
1757
+ // credited only after immutable HEAD/base replay, and never change mutation score or gate.
1758
+ //
1759
+ // PARKED 2026-08-06. Off by default because the lane produced zero findings on the frozen
1760
+ // cohort of real suite-evading regressions across three configurations, and every finding it
1761
+ // produced on a planted fixture traced back to information leaked into its own inputs. The
1762
+ // CLI parks it ahead of this setting either way; this makes the shipped configuration say the
1763
+ // same thing, so nobody reads `true` here and concludes the lane is live.
1764
+ enabled: false,
1765
+ mode: "advisory",
1766
+ authorities: ["machine-sourced", "source-grounded"],
1767
+ maxTargets: 2,
1768
+ // 8, raised from 1 (2026-08-05), which makes it equal to `claimPoolSize` — every rule the
1769
+ // intent stage validates now runs, and nothing is generated then discarded.
1770
+ //
1771
+ // The raise was expected to be expensive and is not, because the assumption behind the old
1772
+ // value turned out to be wrong. Cap 1 was chosen to avoid paying for 8 candidates. Measured
1773
+ // across 9 intent calls on ts-pattern, the stage asked for 8 rules and returned 2, 3, 2, 2, 2,
1774
+ // 0, 1, 3, 2 — never more than 3. The fixture repo had been overriding this to 8 the whole
1775
+ // time, so every one of those runs already ran uncapped, and across all of them there is not a
1776
+ // single `dropped-pool-cap` or `unexecuted-policy-stop` record. The cap has never truncated
1777
+ // anything.
1778
+ //
1779
+ // So the cost of this line is the difference between running one validated rule and running
1780
+ // the two or three that exist: roughly 1.5-2x the stage, not 8x, and comfortably inside
1781
+ // `totalDeadlineMs`. Batching and container pooling were designed to make cap 8 affordable and
1782
+ // are not needed at this pool size; revisit them if intent yield ever climbs past about 4.
1783
+ candidateCap: 8,
1784
+ maxFindings: 2,
1785
+ claimPoolSize: 8,
1786
+ propertyRuns: 200,
1787
+ replayRepetitions: 3,
1788
+ setupRepairAttempts: 1,
1789
+ totalDeadlineMs: 6e5,
1790
+ // 180 s, raised from 90 s (2026-08-05). The intent call is the reasoning stage — it reads the
1791
+ // evidence pack and returns the whole rule pool in one response — and at the pinned
1792
+ // gpt-5.6-terra high-effort configuration it did not fit: measured on ts-pattern it burned the
1793
+ // entire 90 s window across two attempts and reported failureKind "timeout" with zero
1794
+ // validated rules, so the stage could never execute a single rule. The 90 s budget and the
1795
+ // high-effort model pin were never reconciled with each other.
1796
+ //
1797
+ // Knock-on to know: this same field is the container process timeout, so a hung property run
1798
+ // now takes 180 s rather than 90 s to fail. It remains bounded by totalDeadlineMs.
1799
+ perCandidateDeadlineMs: 18e4,
1800
+ maxEvidenceSources: 24,
1801
+ maxEvidenceBytes: 262144,
1802
+ requireContainer: true,
1803
+ failOnFinding: false
1804
+ },
1805
+ patchRevert: {
1806
+ // BETA, opt-in. A repository turns it on in abloh.yml:
1807
+ //
1808
+ // research:
1809
+ // patchRevert:
1810
+ // enabled: true
1811
+ //
1812
+ // Off here rather than on-with-a-kill-switch because the lane spends real CI wall time on
1813
+ // every pull request, and a verification tool that silently lengthens someone's build is the
1814
+ // wrong default however good its findings are.
1815
+ enabled: false,
1816
+ batchedImpactScreen: true
1817
+ }
1818
+ };
1819
+ function cloneResearch() {
1820
+ return {
1821
+ scenarioSearch: {
1822
+ ...DEFAULT_MECHANISMS.scenarioSearch,
1823
+ lanes: [...DEFAULT_MECHANISMS.scenarioSearch.lanes],
1824
+ sourceModes: [...DEFAULT_MECHANISMS.scenarioSearch.sourceModes]
1825
+ },
1826
+ invariant: {
1827
+ ...DEFAULT_MECHANISMS.ciPropertySearch,
1828
+ authorities: [...DEFAULT_MECHANISMS.ciPropertySearch.authorities]
1829
+ }
1830
+ };
1831
+ }
1832
+ function clonePatchRevert() {
1833
+ return { ...DEFAULT_MECHANISMS.patchRevert };
1834
+ }
1835
+ var DEFAULT_DIFF_COVERAGE = { shortCircuit: false };
1836
+ var DEFAULT_MUTATION = {
1837
+ // uncapped by default: a silent sample would change what every existing run measures; capping
1838
+ // is an explicit, disclosed policy choice per repo
1839
+ maxMutantsPerRun: null,
1840
+ ignoreStatic: true,
1841
+ recheckRepetitions: null,
1842
+ recheckTimeoutFactorMultiplier: null
1843
+ };
1844
+ var DEFAULT_SANDBOX = { envFiles: [] };
1845
+ var DEFAULT_ENVIRONMENT_IMAGE_DIGEST = "6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3";
1846
+ var DEFAULT_ENVIRONMENT_IMAGE_REF = `node:22.23.1-bookworm-slim@sha256:${DEFAULT_ENVIRONMENT_IMAGE_DIGEST}`;
1847
+ var DEFAULT_ENVIRONMENT = {
1848
+ services: [],
1849
+ requiredVariables: [],
1850
+ identityFiles: [],
1851
+ setupCommands: [],
1852
+ generatedFiles: []
1853
+ };
1854
+ var DEFAULT_FINDINGS = { clustering: "structural", naming: false };
1855
+ var DEFAULT_POLICY = {
1856
+ threshold: 70,
1857
+ enforce: false,
1858
+ /*
1859
+ * Tier 2 is the default posture: Abloh retains the mutated source span and the model's own
1860
+ * output alongside the structural result.
1861
+ *
1862
+ * IT WAS 0, THEN 1, AND IS NOW 2. Each step widened what a run with no tier in its policy does.
1863
+ * At 1 the triage model ran in the customer's own CI under their credentials and only structure
1864
+ * reached Abloh; at 2 the mutated span (`originalText`), the classifier's rationale and proven
1865
+ * test bodies reach Abloh as well, as digest-bound sidecars.
1866
+ *
1867
+ * WHAT THAT COMMITS A CUSTOMER TO, stated plainly because a default nobody chose is still a
1868
+ * choice somebody made for them: a customer who never writes a tier into their policy has their
1869
+ * source snippets stored by Abloh. This used to require a deliberate act by an administrator and
1870
+ * no longer does. Lowering it is still one setting away — the tier is per-organization and the
1871
+ * sanitizer decides at ingest, so a run measured at a lower tier stays structural forever.
1872
+ */
1873
+ tier: 2,
1874
+ floor: DEFAULT_FLOOR,
1875
+ mutation: DEFAULT_MUTATION,
1876
+ sandbox: DEFAULT_SANDBOX,
1877
+ target: {},
1878
+ environment: DEFAULT_ENVIRONMENT,
1879
+ diffCoverage: DEFAULT_DIFF_COVERAGE,
1880
+ flaky: "quarantine",
1881
+ flaggedPaths: [],
1882
+ scoreAggregation: "worst-of-packages",
1883
+ approvers: [],
1884
+ classicMutation: DEFAULT_CLASSIC_MUTATION,
1885
+ patchRevert: DEFAULT_MECHANISMS.patchRevert,
1886
+ research: { scenarioSearch: DEFAULT_MECHANISMS.scenarioSearch, invariant: DEFAULT_MECHANISMS.ciPropertySearch },
1887
+ findings: DEFAULT_FINDINGS
1888
+ };
1889
+ var KNOWN_KEYS = ["threshold", "enforce", "tier", "floor", "mutation", "sandbox", "target", "environment", "diffCoverage", "flaky", "flaggedPaths", "approvers", "classicMutation", "patchRevert", "findings", "scoreAggregation"];
1890
+ var RESEARCH_KEY_ALLOWED = () => process.env.ABLOH_RESEARCH_POLICY === "1";
1891
+ var KNOWN_TARGET_KEYS = ["directory"];
1892
+ var KNOWN_FINDINGS_KEYS = ["clustering", "naming"];
1893
+ var KNOWN_MUTATION_KEYS = ["maxMutantsPerRun", "ignoreStatic", "recheckRepetitions", "recheckTimeoutFactorMultiplier"];
1894
+ var KNOWN_SANDBOX_KEYS = ["envFiles"];
1895
+ var KNOWN_ENVIRONMENT_KEYS = [
1896
+ "runtimeImage",
1897
+ "installDirectory",
1898
+ "installCommand",
1899
+ "services",
1900
+ "requiredVariables",
1901
+ "identityFiles",
1902
+ "setupCommands",
1903
+ "generatedFiles",
1904
+ "testCommand"
1905
+ ];
1906
+ var KNOWN_CLASSIC_MUTATION_KEYS = ["deterministicMutants", "perTest", "fixLoop", "errorPaths", "models"];
1907
+ var KNOWN_DETERMINISTIC_KEYS = ["enabled", "maxRun"];
1908
+ var KNOWN_PERTEST_KEYS = ["enabled", "fullSuiteAuditSampleSize", "fullSuiteMismatchTolerance"];
1909
+ var KNOWN_FIXLOOP_KEYS = ["enabled", "maxCandidates", "proofRepetitions", "suiteConfirmation", "syntheticEnvironment", "generationDeadlineMs", "proofDeadlineMs", "cacheDir"];
1910
+ var KNOWN_ERRORPATHS_KEYS = ["staticAnalysis", "failOnUntested", "failOnAntiPattern", "forceHandlerMutation"];
1911
+ var KNOWN_FLOOR_KEYS = ["minMutantsExecuted", "maxErrorRate", "minSamplingFraction"];
1912
+ var KNOWN_DIFF_COVERAGE_KEYS = ["shortCircuit"];
1913
+ var KNOWN_RESEARCH_KEYS = ["scenarioSearch", "invariant"];
1914
+ var KNOWN_SCENARIO_SEARCH_KEYS = [
1915
+ "enabled",
1916
+ "profile",
1917
+ "lanes",
1918
+ "sourceModes",
1919
+ "maxTargets",
1920
+ "maxCandidatesPerTarget",
1921
+ "maxAgentTurnsPerTarget",
1922
+ "maxModelCalls",
1923
+ "totalDeadlineMs",
1924
+ "executionDeadlineMs",
1925
+ "replayRepetitions",
1926
+ "requireContainer",
1927
+ "cacheDir",
1928
+ "failOnCounterexample"
1929
+ ];
1930
+ var REQUIRED_ENABLED_SCENARIO_SEARCH_KEYS = [
1931
+ "profile",
1932
+ "lanes",
1933
+ "sourceModes",
1934
+ "maxTargets",
1935
+ "maxCandidatesPerTarget",
1936
+ "maxAgentTurnsPerTarget",
1937
+ "maxModelCalls",
1938
+ "totalDeadlineMs",
1939
+ "executionDeadlineMs",
1940
+ "replayRepetitions",
1941
+ "requireContainer"
1942
+ ];
1943
+ var KNOWN_SCENARIO_LANES = ["property", "value", "state", "failure"];
1944
+ var KNOWN_SCENARIO_AUTHORITIES = [
1945
+ "machine-sourced",
1946
+ "source-grounded",
1947
+ "corroborated",
1948
+ "code-inferred"
1949
+ ];
1950
+ var KNOWN_PATCH_REVERT_KEYS = ["enabled", "batchedImpactScreen"];
1951
+ var KNOWN_CI_PROPERTY_KEYS = [
1952
+ "enabled",
1953
+ "mode",
1954
+ "authorities",
1955
+ "maxTargets",
1956
+ "candidateCap",
1957
+ "maxFindings",
1958
+ "claimPoolSize",
1959
+ "propertyRuns",
1960
+ "replayRepetitions",
1961
+ "setupRepairAttempts",
1962
+ "totalDeadlineMs",
1963
+ "perCandidateDeadlineMs",
1964
+ "maxEvidenceSources",
1965
+ "maxEvidenceBytes",
1966
+ "requireContainer",
1967
+ "failOnFinding",
1968
+ "cacheDir"
1969
+ ];
1970
+ var REQUIRED_ENABLED_CI_PROPERTY_KEYS = [
1971
+ "mode",
1972
+ "authorities",
1973
+ "maxTargets",
1974
+ "candidateCap",
1975
+ "maxFindings",
1976
+ "claimPoolSize",
1977
+ "propertyRuns",
1978
+ "replayRepetitions",
1979
+ "setupRepairAttempts",
1980
+ "totalDeadlineMs",
1981
+ "perCandidateDeadlineMs",
1982
+ "maxEvidenceSources",
1983
+ "maxEvidenceBytes",
1984
+ "requireContainer",
1985
+ "failOnFinding"
1986
+ ];
1987
+ function isPlainObject(v) {
1988
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1989
+ }
1990
+ function fail(path, msg) {
1991
+ throw new Error(`invalid ${path}: ${msg}`);
1992
+ }
1993
+ function checkNumber(path, key, v, min, max) {
1994
+ if (typeof v !== "number" || !Number.isFinite(v)) fail(path, `${key} must be a number, got ${JSON.stringify(v)}`);
1995
+ if (v < min || v > max) fail(path, `${key} must be between ${min} and ${max}, got ${v}`);
1996
+ return v;
1997
+ }
1998
+ function checkStringArray(path, key, v) {
1999
+ if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
2000
+ fail(path, `${key} must be an array of strings, got ${JSON.stringify(v)}`);
2001
+ }
2002
+ return v;
2003
+ }
2004
+ function preparedTestCommandProblem(value) {
2005
+ if (typeof value !== "string" || value.trim().length === 0) return "must be a non-empty string";
2006
+ if (Buffer.byteLength(value, "utf8") > 4096) return "must be at most 4096 UTF-8 bytes";
2007
+ if (/[\0\r\n]/u.test(value)) return "must be one line without NUL bytes";
2008
+ if (/[;&|<>`]/u.test(value) || /\$\(/u.test(value)) {
2009
+ return "must be one literal command without shell composition, redirection, or substitution; put complex setup in the repository's normal test script";
2010
+ }
2011
+ if (/^\s*[A-Za-z_][A-Za-z0-9_]*=/u.test(value)) {
2012
+ return "must not contain inline environment assignments; declare required variable names under environment.requiredVariables and supply values through the caller environment";
2013
+ }
2014
+ if (/[\$*?\[\]~]/u.test(value)) {
2015
+ return "must not contain shell expansion characters ($, *, ?, [, ], or ~); use a repository script with literal arguments";
2016
+ }
2017
+ return null;
2018
+ }
2019
+ function immutableImageProblem(value) {
2020
+ if (typeof value !== "string" || value.length === 0) return "must be a non-empty string";
2021
+ if (value.length > 384) return "must be at most 384 characters";
2022
+ if (!/^[a-z0-9][a-z0-9._:/-]*@sha256:[a-f0-9]{64}$/u.test(value)) {
2023
+ return "must be a lowercase immutable OCI reference ending in @sha256:<64 hex>";
2024
+ }
2025
+ return null;
2026
+ }
2027
+ function validatePreparedTestCommand(value, label = "test command") {
2028
+ const problem = preparedTestCommandProblem(value);
2029
+ if (problem) throw new Error(`${label} ${problem}`);
2030
+ return value.trim();
2031
+ }
2032
+ function parsePreparedTestCommand(value, label = "test command") {
2033
+ const command = validatePreparedTestCommand(value, label);
2034
+ return parseLiteralArguments(command, label);
2035
+ }
2036
+ function parseLiteralArguments(command, label = "command") {
2037
+ if (Buffer.byteLength(command, "utf8") > MAX_LITERAL_COMMAND_BYTES || /[\0\r\n]/u.test(command)) {
2038
+ throw new Error(`${label} must be one bounded line without NUL bytes`);
2039
+ }
2040
+ const argv = [];
2041
+ let token = "";
2042
+ let quote = null;
2043
+ let started = false;
2044
+ for (let index = 0; index < command.length; index += 1) {
2045
+ const char = command[index];
2046
+ if (quote === "single") {
2047
+ if (char === "'") quote = null;
2048
+ else token += char;
2049
+ started = true;
2050
+ continue;
2051
+ }
2052
+ if (quote === "double") {
2053
+ if (char === '"') {
2054
+ quote = null;
2055
+ } else if (char === "\\") {
2056
+ const next = command[index + 1];
2057
+ if (next === '"' || next === "\\") {
2058
+ token += next;
2059
+ index += 1;
2060
+ } else {
2061
+ token += "\\";
2062
+ }
2063
+ } else {
2064
+ token += char;
2065
+ }
2066
+ started = true;
2067
+ continue;
2068
+ }
2069
+ if (char === "\\") {
2070
+ const next = command[index + 1];
2071
+ if (next === void 0) throw new Error(`${label} contains an unterminated escape`);
2072
+ token += next;
2073
+ index += 1;
2074
+ started = true;
2075
+ } else if (char === "'") {
2076
+ quote = "single";
2077
+ started = true;
2078
+ } else if (char === '"') {
2079
+ quote = "double";
2080
+ started = true;
2081
+ } else if (/\s/u.test(char)) {
2082
+ if (started) {
2083
+ argv.push(token);
2084
+ token = "";
2085
+ started = false;
2086
+ }
2087
+ } else {
2088
+ token += char;
2089
+ started = true;
2090
+ }
2091
+ }
2092
+ if (quote !== null) throw new Error(`${label} contains an unterminated quote`);
2093
+ if (started) argv.push(token);
2094
+ if (argv.length === 0 || argv.length > MAX_LITERAL_ARGUMENTS || argv.some((entry) => entry.length > 4096)) {
2095
+ throw new Error(`${label} must contain 1 to 128 bounded arguments`);
2096
+ }
2097
+ return argv;
2098
+ }
2099
+ var MAX_LITERAL_ARGUMENTS = 128;
2100
+ var MAX_LITERAL_COMMAND_BYTES = 256 * 1024;
2101
+ function checkBool(path, key, v) {
2102
+ if (typeof v !== "boolean") fail(path, `${key} must be true or false, got ${JSON.stringify(v)}`);
2103
+ return v;
2104
+ }
2105
+ function checkOptCacheDir(path, key, v) {
2106
+ if (typeof v !== "string" || v.length === 0) fail(path, `${key} must be a non-empty string path, got ${JSON.stringify(v)}`);
2107
+ if (!v.startsWith("/")) fail(path, `${key} must be an ABSOLUTE path (a workspace-relative cache can be seeded by the repo), got ${JSON.stringify(v)}`);
2108
+ if (v.includes("..")) fail(path, `${key} must not contain '..', got ${JSON.stringify(v)}`);
2109
+ const home = homedir();
2110
+ if (!home || !(v === home || v.startsWith(`${home}/`))) {
2111
+ fail(path, `${key} must live under the invoking user's home directory (${home || "unknown"}) \u2014 these caches hold generated code that is later executed, so a shared or world-writable path is refused; got ${JSON.stringify(v)}`);
2112
+ }
2113
+ try {
2114
+ const realHome = realpathSync(home);
2115
+ let probe = v;
2116
+ while (!existsSync(probe)) {
2117
+ const parent = dirname(probe);
2118
+ if (parent === probe) break;
2119
+ probe = parent;
2120
+ }
2121
+ const real = realpathSync(probe);
2122
+ if (!(real === realHome || real.startsWith(`${realHome}/`))) {
2123
+ fail(path, `${key} resolves outside the user's home (via ${real}) \u2014 a symlinked cache is the same exposure as a shared one`);
2124
+ }
2125
+ } catch {
2126
+ fail(path, `${key} could not be resolved on this filesystem, so its ownership cannot be established`);
2127
+ }
2128
+ return v;
2129
+ }
2130
+ function checkIntRange(path, key, v, min, max) {
2131
+ if (typeof v !== "number" || !Number.isInteger(v) || v < min || v > max) {
2132
+ fail(path, `${key} must be an integer between ${min} and ${max}, got ${JSON.stringify(v)}`);
2133
+ }
2134
+ return v;
2135
+ }
2136
+ function checkNestedKeys(path, label, obj, known) {
2137
+ const unknown = Object.keys(obj).filter((k) => !known.includes(k));
2138
+ if (unknown.length > 0) fail(path, `unknown ${label} key(s): ${unknown.join(", ")} (known: ${known.join(", ")})`);
2139
+ }
2140
+ function validateClassicMutation(path, raw, out) {
2141
+ if (!isPlainObject(raw)) fail(path, `classicMutation must be a mapping, got ${JSON.stringify(raw)}`);
2142
+ checkNestedKeys(path, "classicMutation", raw, KNOWN_CLASSIC_MUTATION_KEYS);
2143
+ if ("deterministicMutants" in raw) {
2144
+ const d = raw.deterministicMutants;
2145
+ if (!isPlainObject(d)) fail(path, `classicMutation.deterministicMutants must be a mapping, got ${JSON.stringify(d)}`);
2146
+ checkNestedKeys(path, "classicMutation.deterministicMutants", d, KNOWN_DETERMINISTIC_KEYS);
2147
+ if ("enabled" in d) {
2148
+ out.deterministicMutants.enabled = checkBool(path, "classicMutation.deterministicMutants.enabled", d.enabled);
2149
+ }
2150
+ if ("maxRun" in d) {
2151
+ out.deterministicMutants.maxRun = checkIntRange(path, "classicMutation.deterministicMutants.maxRun", d.maxRun, 0, 500);
2152
+ }
2153
+ }
2154
+ if ("perTest" in raw) {
2155
+ const p = raw.perTest;
2156
+ if (!isPlainObject(p)) fail(path, `classicMutation.perTest must be a mapping, got ${JSON.stringify(p)}`);
2157
+ checkNestedKeys(path, "classicMutation.perTest", p, KNOWN_PERTEST_KEYS);
2158
+ if ("enabled" in p) out.perTest.enabled = checkBool(path, "classicMutation.perTest.enabled", p.enabled);
2159
+ if ("fullSuiteAuditSampleSize" in p) {
2160
+ out.perTest.fullSuiteAuditSampleSize = p.fullSuiteAuditSampleSize === null ? null : checkIntRange(path, "classicMutation.perTest.fullSuiteAuditSampleSize", p.fullSuiteAuditSampleSize, 1, 1e4);
2161
+ }
2162
+ if ("fullSuiteMismatchTolerance" in p) {
2163
+ out.perTest.fullSuiteMismatchTolerance = p.fullSuiteMismatchTolerance === null ? null : checkNumber(path, "classicMutation.perTest.fullSuiteMismatchTolerance", p.fullSuiteMismatchTolerance, 0, 1);
2164
+ }
2165
+ if (out.perTest.fullSuiteAuditSampleSize === null !== (out.perTest.fullSuiteMismatchTolerance === null)) {
2166
+ fail(path, "classicMutation.perTest full-suite audit sample size and mismatch tolerance must be configured together");
2167
+ }
2168
+ }
2169
+ if ("fixLoop" in raw) {
2170
+ const f = raw.fixLoop;
2171
+ if (!isPlainObject(f)) fail(path, `classicMutation.fixLoop must be a mapping, got ${JSON.stringify(f)}`);
2172
+ checkNestedKeys(path, "classicMutation.fixLoop", f, KNOWN_FIXLOOP_KEYS);
2173
+ if ("enabled" in f) out.fixLoop.enabled = checkBool(path, "classicMutation.fixLoop.enabled", f.enabled);
2174
+ if ("maxCandidates" in f) out.fixLoop.maxCandidates = checkIntRange(path, "classicMutation.fixLoop.maxCandidates", f.maxCandidates, 0, 200);
2175
+ if ("proofRepetitions" in f) out.fixLoop.proofRepetitions = checkIntRange(path, "classicMutation.fixLoop.proofRepetitions", f.proofRepetitions, 1, 10);
2176
+ if ("suiteConfirmation" in f) {
2177
+ if (f.suiteConfirmation !== "inline" && f.suiteConfirmation !== "deferred") {
2178
+ fail(path, "classicMutation.fixLoop.suiteConfirmation must be 'inline' or 'deferred'");
2179
+ }
2180
+ out.fixLoop.suiteConfirmation = f.suiteConfirmation;
2181
+ }
2182
+ if ("syntheticEnvironment" in f) {
2183
+ if (!isPlainObject(f.syntheticEnvironment)) {
2184
+ fail(path, "classicMutation.fixLoop.syntheticEnvironment must be a mapping of literal strings");
2185
+ }
2186
+ const entries = Object.entries(f.syntheticEnvironment);
2187
+ if (entries.length > 16) fail(path, "classicMutation.fixLoop.syntheticEnvironment exceeds the 16-value cap");
2188
+ const environment = {};
2189
+ for (const [key, value] of entries) {
2190
+ if (!/^ATTEST_TEST_[A-Z0-9_]{1,64}$/u.test(key) || /(SECRET|TOKEN|PASSWORD|CREDENTIAL|PRIVATE|ACCESS_KEY|API_KEY)/u.test(key) || typeof value !== "string" || value.length > 512 || /[\u0000-\u001f\u007f]/u.test(value)) {
2191
+ fail(path, "classicMutation.fixLoop.syntheticEnvironment requires bounded non-secret ATTEST_TEST_ string literals");
2192
+ }
2193
+ environment[key] = value;
2194
+ }
2195
+ out.fixLoop.syntheticEnvironment = environment;
2196
+ }
2197
+ if ("generationDeadlineMs" in f) out.fixLoop.generationDeadlineMs = checkIntRange(path, "classicMutation.fixLoop.generationDeadlineMs", f.generationDeadlineMs, 6e4, 72e5);
2198
+ if ("proofDeadlineMs" in f) out.fixLoop.proofDeadlineMs = checkIntRange(path, "classicMutation.fixLoop.proofDeadlineMs", f.proofDeadlineMs, 6e4, 72e5);
2199
+ if ("cacheDir" in f) out.fixLoop.cacheDir = checkOptCacheDir(path, "classicMutation.fixLoop.cacheDir", f.cacheDir);
2200
+ }
2201
+ if ("errorPaths" in raw) {
2202
+ const e = raw.errorPaths;
2203
+ if (!isPlainObject(e)) fail(path, `classicMutation.errorPaths must be a mapping, got ${JSON.stringify(e)}`);
2204
+ checkNestedKeys(path, "classicMutation.errorPaths", e, KNOWN_ERRORPATHS_KEYS);
2205
+ if ("staticAnalysis" in e) out.errorPaths.staticAnalysis = checkBool(path, "classicMutation.errorPaths.staticAnalysis", e.staticAnalysis);
2206
+ if ("failOnUntested" in e) out.errorPaths.failOnUntested = checkBool(path, "classicMutation.errorPaths.failOnUntested", e.failOnUntested);
2207
+ if ("failOnAntiPattern" in e) out.errorPaths.failOnAntiPattern = checkBool(path, "classicMutation.errorPaths.failOnAntiPattern", e.failOnAntiPattern);
2208
+ if ("forceHandlerMutation" in e) out.errorPaths.forceHandlerMutation = checkBool(path, "classicMutation.errorPaths.forceHandlerMutation", e.forceHandlerMutation);
2209
+ }
2210
+ if ("models" in raw) {
2211
+ fail(
2212
+ path,
2213
+ "classicMutation.models is not allowed \u2014 the models are fixed by the service, because a project must not choose the model that judges it"
2214
+ );
2215
+ }
2216
+ if (out.errorPaths.failOnAntiPattern && !out.errorPaths.staticAnalysis) {
2217
+ fail(path, "classicMutation.errorPaths.failOnAntiPattern requires staticAnalysis: true \u2014 anti-patterns are only detected by the scanner, so this rule could never fire");
2218
+ }
2219
+ if (out.errorPaths.failOnUntested && !out.errorPaths.staticAnalysis && !out.errorPaths.forceHandlerMutation) {
2220
+ fail(
2221
+ path,
2222
+ "classicMutation.errorPaths.failOnUntested requires staticAnalysis: true or forceHandlerMutation: true \u2014 without either, no handler mutant is ever identified and this rule could never fire"
2223
+ );
2224
+ }
2225
+ }
2226
+ function validateResearch(path, raw, out) {
2227
+ if (!isPlainObject(raw)) fail(path, `research must be a mapping, got ${JSON.stringify(raw)}`);
2228
+ checkNestedKeys(path, "research", raw, KNOWN_RESEARCH_KEYS);
2229
+ if ("scenarioSearch" in raw) {
2230
+ const s = raw.scenarioSearch;
2231
+ if (!isPlainObject(s)) {
2232
+ fail(path, `research.scenarioSearch must be a mapping, got ${JSON.stringify(s)}`);
2233
+ }
2234
+ checkNestedKeys(
2235
+ path,
2236
+ "research.scenarioSearch",
2237
+ s,
2238
+ KNOWN_SCENARIO_SEARCH_KEYS
2239
+ );
2240
+ if ("enabled" in s) {
2241
+ out.scenarioSearch.enabled = checkBool(
2242
+ path,
2243
+ "research.scenarioSearch.enabled",
2244
+ s.enabled
2245
+ );
2246
+ }
2247
+ if ("profile" in s) {
2248
+ if (s.profile !== "research") {
2249
+ fail(
2250
+ path,
2251
+ `research.scenarioSearch.profile must be 'research' before benchmark promotion, got ${JSON.stringify(s.profile)}`
2252
+ );
2253
+ }
2254
+ out.scenarioSearch.profile = s.profile;
2255
+ }
2256
+ if ("lanes" in s) {
2257
+ if (!Array.isArray(s.lanes) || s.lanes.length === 0 || s.lanes.some(
2258
+ (lane) => typeof lane !== "string" || !KNOWN_SCENARIO_LANES.includes(lane)
2259
+ )) {
2260
+ fail(
2261
+ path,
2262
+ `research.scenarioSearch.lanes must be a non-empty subset of ${KNOWN_SCENARIO_LANES.join(", ")}, got ${JSON.stringify(s.lanes)}`
2263
+ );
2264
+ }
2265
+ if (new Set(s.lanes).size !== s.lanes.length) {
2266
+ fail(path, "research.scenarioSearch.lanes must not contain duplicates");
2267
+ }
2268
+ const selected = new Set(s.lanes);
2269
+ out.scenarioSearch.lanes = KNOWN_SCENARIO_LANES.filter(
2270
+ (lane) => selected.has(lane)
2271
+ );
2272
+ }
2273
+ if ("sourceModes" in s) {
2274
+ if (!Array.isArray(s.sourceModes) || s.sourceModes.length === 0 || s.sourceModes.some(
2275
+ (mode) => typeof mode !== "string" || !KNOWN_SCENARIO_AUTHORITIES.includes(mode)
2276
+ )) {
2277
+ fail(
2278
+ path,
2279
+ `research.scenarioSearch.sourceModes must be a non-empty subset of ${KNOWN_SCENARIO_AUTHORITIES.join(", ")}, got ${JSON.stringify(s.sourceModes)}`
2280
+ );
2281
+ }
2282
+ if (new Set(s.sourceModes).size !== s.sourceModes.length) {
2283
+ fail(
2284
+ path,
2285
+ "research.scenarioSearch.sourceModes must not contain duplicates"
2286
+ );
2287
+ }
2288
+ const selected = new Set(s.sourceModes);
2289
+ out.scenarioSearch.sourceModes = KNOWN_SCENARIO_AUTHORITIES.filter(
2290
+ (mode) => selected.has(mode)
2291
+ );
2292
+ }
2293
+ if ("maxTargets" in s) {
2294
+ out.scenarioSearch.maxTargets = checkIntRange(
2295
+ path,
2296
+ "research.scenarioSearch.maxTargets",
2297
+ s.maxTargets,
2298
+ 1,
2299
+ 100
2300
+ );
2301
+ }
2302
+ if ("maxCandidatesPerTarget" in s) {
2303
+ out.scenarioSearch.maxCandidatesPerTarget = checkIntRange(
2304
+ path,
2305
+ "research.scenarioSearch.maxCandidatesPerTarget",
2306
+ s.maxCandidatesPerTarget,
2307
+ 1,
2308
+ 100
2309
+ );
2310
+ }
2311
+ if ("maxAgentTurnsPerTarget" in s) {
2312
+ out.scenarioSearch.maxAgentTurnsPerTarget = checkIntRange(
2313
+ path,
2314
+ "research.scenarioSearch.maxAgentTurnsPerTarget",
2315
+ s.maxAgentTurnsPerTarget,
2316
+ 1,
2317
+ 200
2318
+ );
2319
+ }
2320
+ if ("maxModelCalls" in s) {
2321
+ out.scenarioSearch.maxModelCalls = checkIntRange(
2322
+ path,
2323
+ "research.scenarioSearch.maxModelCalls",
2324
+ s.maxModelCalls,
2325
+ 1,
2326
+ 1e3
2327
+ );
2328
+ }
2329
+ if ("totalDeadlineMs" in s) {
2330
+ out.scenarioSearch.totalDeadlineMs = checkIntRange(
2331
+ path,
2332
+ "research.scenarioSearch.totalDeadlineMs",
2333
+ s.totalDeadlineMs,
2334
+ 6e4,
2335
+ 72e5
2336
+ );
2337
+ }
2338
+ if ("executionDeadlineMs" in s) {
2339
+ out.scenarioSearch.executionDeadlineMs = checkIntRange(
2340
+ path,
2341
+ "research.scenarioSearch.executionDeadlineMs",
2342
+ s.executionDeadlineMs,
2343
+ 1e3,
2344
+ 6e5
2345
+ );
2346
+ }
2347
+ if ("replayRepetitions" in s) {
2348
+ out.scenarioSearch.replayRepetitions = checkIntRange(
2349
+ path,
2350
+ "research.scenarioSearch.replayRepetitions",
2351
+ s.replayRepetitions,
2352
+ 1,
2353
+ 10
2354
+ );
2355
+ }
2356
+ if ("requireContainer" in s) {
2357
+ if (s.requireContainer !== true) {
2358
+ fail(
2359
+ path,
2360
+ "research.scenarioSearch.requireContainer must be true \u2014 generated tests never execute on the host"
2361
+ );
2362
+ }
2363
+ out.scenarioSearch.requireContainer = true;
2364
+ }
2365
+ if ("cacheDir" in s) {
2366
+ out.scenarioSearch.cacheDir = checkOptCacheDir(
2367
+ path,
2368
+ "research.scenarioSearch.cacheDir",
2369
+ s.cacheDir
2370
+ );
2371
+ }
2372
+ if ("failOnCounterexample" in s) {
2373
+ if (s.failOnCounterexample !== false) {
2374
+ fail(
2375
+ path,
2376
+ "research.scenarioSearch.failOnCounterexample must remain false before benchmark and shadow promotion"
2377
+ );
2378
+ }
2379
+ out.scenarioSearch.failOnCounterexample = false;
2380
+ }
2381
+ if (out.scenarioSearch.enabled) {
2382
+ const missing = REQUIRED_ENABLED_SCENARIO_SEARCH_KEYS.filter(
2383
+ (key) => !(key in s)
2384
+ );
2385
+ if (missing.length > 0) {
2386
+ fail(
2387
+ path,
2388
+ `research.scenarioSearch enabled runs require explicit research limits: ${missing.join(", ")}`
2389
+ );
2390
+ }
2391
+ }
2392
+ if (out.scenarioSearch.executionDeadlineMs > out.scenarioSearch.totalDeadlineMs) {
2393
+ fail(
2394
+ path,
2395
+ "research.scenarioSearch.executionDeadlineMs cannot exceed totalDeadlineMs"
2396
+ );
2397
+ }
2398
+ }
2399
+ if ("invariant" in raw) {
2400
+ const c = raw.invariant;
2401
+ if (!isPlainObject(c)) {
2402
+ fail(path, `research.invariant must be a mapping, got ${JSON.stringify(c)}`);
2403
+ }
2404
+ checkNestedKeys(
2405
+ path,
2406
+ "research.invariant",
2407
+ c,
2408
+ KNOWN_CI_PROPERTY_KEYS
2409
+ );
2410
+ if ("enabled" in c) {
2411
+ out.invariant.enabled = checkBool(
2412
+ path,
2413
+ "research.invariant.enabled",
2414
+ c.enabled
2415
+ );
2416
+ }
2417
+ if ("mode" in c) {
2418
+ if (c.mode !== "shadow" && c.mode !== "advisory") {
2419
+ fail(
2420
+ path,
2421
+ `research.invariant.mode must be shadow or advisory, got ${JSON.stringify(c.mode)}`
2422
+ );
2423
+ }
2424
+ out.invariant.mode = c.mode;
2425
+ }
2426
+ if ("authorities" in c) {
2427
+ if (!Array.isArray(c.authorities) || c.authorities.length === 0 || c.authorities.some(
2428
+ (authority) => typeof authority !== "string" || !CI_PROPERTY_AUTHORITIES.includes(authority)
2429
+ )) {
2430
+ fail(
2431
+ path,
2432
+ `research.invariant.authorities must be a non-empty subset of ${CI_PROPERTY_AUTHORITIES.join(", ")}, got ${JSON.stringify(c.authorities)}`
2433
+ );
2434
+ }
2435
+ if (new Set(c.authorities).size !== c.authorities.length) {
2436
+ fail(path, "research.invariant.authorities must not contain duplicates");
2437
+ }
2438
+ const selected = new Set(c.authorities);
2439
+ out.invariant.authorities = CI_PROPERTY_AUTHORITIES.filter(
2440
+ (authority) => selected.has(authority)
2441
+ );
2442
+ }
2443
+ if ("maxTargets" in c) {
2444
+ out.invariant.maxTargets = checkIntRange(
2445
+ path,
2446
+ "research.invariant.maxTargets",
2447
+ c.maxTargets,
2448
+ 1,
2449
+ 2
2450
+ );
2451
+ }
2452
+ if ("candidateCap" in c) {
2453
+ if (!CI_PROPERTY_CAPS.includes(c.candidateCap)) {
2454
+ fail(
2455
+ path,
2456
+ `research.invariant.candidateCap must be one of ${CI_PROPERTY_CAPS.join(", ")}, got ${JSON.stringify(c.candidateCap)}`
2457
+ );
2458
+ }
2459
+ out.invariant.candidateCap = c.candidateCap;
2460
+ }
2461
+ if ("maxFindings" in c) {
2462
+ out.invariant.maxFindings = checkIntRange(
2463
+ path,
2464
+ "research.invariant.maxFindings",
2465
+ c.maxFindings,
2466
+ 1,
2467
+ 2
2468
+ );
2469
+ }
2470
+ if ("claimPoolSize" in c) {
2471
+ const size = checkIntRange(
2472
+ path,
2473
+ "research.invariant.claimPoolSize",
2474
+ c.claimPoolSize,
2475
+ 8,
2476
+ 8
2477
+ );
2478
+ out.invariant.claimPoolSize = size;
2479
+ }
2480
+ if ("propertyRuns" in c) {
2481
+ out.invariant.propertyRuns = checkIntRange(
2482
+ path,
2483
+ "research.invariant.propertyRuns",
2484
+ c.propertyRuns,
2485
+ 200,
2486
+ 200
2487
+ );
2488
+ }
2489
+ if ("replayRepetitions" in c) {
2490
+ out.invariant.replayRepetitions = checkIntRange(
2491
+ path,
2492
+ "research.invariant.replayRepetitions",
2493
+ c.replayRepetitions,
2494
+ 3,
2495
+ 3
2496
+ );
2497
+ }
2498
+ if ("setupRepairAttempts" in c) {
2499
+ out.invariant.setupRepairAttempts = checkIntRange(
2500
+ path,
2501
+ "research.invariant.setupRepairAttempts",
2502
+ c.setupRepairAttempts,
2503
+ 0,
2504
+ 1
2505
+ );
2506
+ }
2507
+ if ("totalDeadlineMs" in c) {
2508
+ out.invariant.totalDeadlineMs = checkIntRange(
2509
+ path,
2510
+ "research.invariant.totalDeadlineMs",
2511
+ c.totalDeadlineMs,
2512
+ 6e4,
2513
+ 6e5
2514
+ );
2515
+ }
2516
+ if ("perCandidateDeadlineMs" in c) {
2517
+ out.invariant.perCandidateDeadlineMs = checkIntRange(
2518
+ path,
2519
+ "research.invariant.perCandidateDeadlineMs",
2520
+ c.perCandidateDeadlineMs,
2521
+ 1e3,
2522
+ 18e4
2523
+ );
2524
+ }
2525
+ if ("maxEvidenceSources" in c) {
2526
+ out.invariant.maxEvidenceSources = checkIntRange(
2527
+ path,
2528
+ "research.invariant.maxEvidenceSources",
2529
+ c.maxEvidenceSources,
2530
+ 1,
2531
+ 24
2532
+ );
2533
+ }
2534
+ if ("maxEvidenceBytes" in c) {
2535
+ out.invariant.maxEvidenceBytes = checkIntRange(
2536
+ path,
2537
+ "research.invariant.maxEvidenceBytes",
2538
+ c.maxEvidenceBytes,
2539
+ 1024,
2540
+ 262144
2541
+ );
2542
+ }
2543
+ if ("requireContainer" in c) {
2544
+ if (c.requireContainer !== true) {
2545
+ fail(
2546
+ path,
2547
+ "research.invariant.requireContainer must be true \u2014 generated tests never execute on the host"
2548
+ );
2549
+ }
2550
+ out.invariant.requireContainer = true;
2551
+ }
2552
+ if ("failOnFinding" in c) {
2553
+ if (c.failOnFinding !== false) {
2554
+ fail(
2555
+ path,
2556
+ "research.invariant.failOnFinding must remain false in version 1"
2557
+ );
2558
+ }
2559
+ out.invariant.failOnFinding = false;
2560
+ }
2561
+ if ("cacheDir" in c) {
2562
+ out.invariant.cacheDir = checkOptCacheDir(
2563
+ path,
2564
+ "research.invariant.cacheDir",
2565
+ c.cacheDir
2566
+ );
2567
+ }
2568
+ if (out.invariant.enabled) {
2569
+ const missing = REQUIRED_ENABLED_CI_PROPERTY_KEYS.filter(
2570
+ (key) => !(key in c)
2571
+ );
2572
+ if (missing.length > 0) {
2573
+ fail(
2574
+ path,
2575
+ `research.invariant enabled runs require explicit CI limits: ${missing.join(", ")}`
2576
+ );
2577
+ }
2578
+ }
2579
+ if (out.invariant.perCandidateDeadlineMs > out.invariant.totalDeadlineMs) {
2580
+ fail(
2581
+ path,
2582
+ "research.invariant.perCandidateDeadlineMs cannot exceed totalDeadlineMs"
2583
+ );
2584
+ }
2585
+ if (out.invariant.candidateCap > out.invariant.claimPoolSize) {
2586
+ fail(
2587
+ path,
2588
+ "research.invariant.candidateCap cannot exceed claimPoolSize"
2589
+ );
2590
+ }
2591
+ }
2592
+ }
2593
+ function validatePatchRevert(path, raw, out) {
2594
+ if (!isPlainObject(raw)) {
2595
+ fail(path, `patchRevert must be a mapping, got ${JSON.stringify(raw)}`);
2596
+ }
2597
+ checkNestedKeys(path, "patchRevert", raw, KNOWN_PATCH_REVERT_KEYS);
2598
+ if ("enabled" in raw) {
2599
+ out.enabled = checkBool(path, "patchRevert.enabled", raw.enabled);
2600
+ }
2601
+ if ("batchedImpactScreen" in raw) {
2602
+ out.batchedImpactScreen = checkBool(path, "patchRevert.batchedImpactScreen", raw.batchedImpactScreen);
2603
+ }
2604
+ }
2605
+ function classicMutationTier(policy, effectiveTier = policy.tier) {
2606
+ const tier0 = effectiveTier === 0;
2607
+ return {
2608
+ fixLoop: tier0 ? "disabled-by-tier" : policy.classicMutation.fixLoop.enabled ? "enabled" : "disabled",
2609
+ perTest: policy.classicMutation.perTest.enabled ? "enabled" : "disabled",
2610
+ errorPathStatic: policy.classicMutation.errorPaths.staticAnalysis ? "enabled" : "disabled"
2611
+ };
2612
+ }
2613
+ function researchTier(policy, effectiveTier = policy.tier) {
2614
+ const tier0 = effectiveTier === 0;
2615
+ return {
2616
+ scenarioSearch: tier0 ? "disabled-by-tier" : policy.research.scenarioSearch.enabled ? "enabled" : "disabled",
2617
+ ciPropertySearch: tier0 ? "disabled-by-tier" : policy.research.invariant.enabled ? "enabled" : "disabled"
2618
+ };
2619
+ }
2620
+ function validatePolicy(raw, path) {
2621
+ const policy = {
2622
+ ...DEFAULT_POLICY,
2623
+ floor: { ...DEFAULT_POLICY.floor },
2624
+ mutation: { ...DEFAULT_POLICY.mutation },
2625
+ sandbox: { envFiles: [...DEFAULT_POLICY.sandbox.envFiles] },
2626
+ target: { ...DEFAULT_POLICY.target },
2627
+ environment: {
2628
+ ...DEFAULT_POLICY.environment,
2629
+ services: DEFAULT_POLICY.environment.services.map((service) => ({ ...service })),
2630
+ requiredVariables: [...DEFAULT_POLICY.environment.requiredVariables],
2631
+ identityFiles: [...DEFAULT_POLICY.environment.identityFiles],
2632
+ setupCommands: [...DEFAULT_POLICY.environment.setupCommands],
2633
+ generatedFiles: [...DEFAULT_POLICY.environment.generatedFiles]
2634
+ },
2635
+ diffCoverage: { ...DEFAULT_POLICY.diffCoverage },
2636
+ flaggedPaths: [],
2637
+ scoreAggregation: DEFAULT_POLICY.scoreAggregation,
2638
+ approvers: [],
2639
+ classicMutation: cloneClassicMutation(),
2640
+ patchRevert: clonePatchRevert(),
2641
+ research: cloneResearch(),
2642
+ findings: { ...DEFAULT_POLICY.findings }
2643
+ };
2644
+ if (raw === null || raw === void 0) return policy;
2645
+ if (!isPlainObject(raw)) fail(path, `policy must be a mapping of keys to values, got ${Array.isArray(raw) ? "an array" : `a ${typeof raw}`}`);
2646
+ const permitted = RESEARCH_KEY_ALLOWED() ? [...KNOWN_KEYS, "research"] : KNOWN_KEYS;
2647
+ const unknown = Object.keys(raw).filter((k) => !permitted.includes(k));
2648
+ if (unknown.length > 0) fail(path, `unknown key(s): ${unknown.join(", ")} (known: ${KNOWN_KEYS.join(", ")})`);
2649
+ if ("threshold" in raw) policy.threshold = checkNumber(path, "threshold", raw.threshold, 0, 100);
2650
+ if ("enforce" in raw) {
2651
+ if (typeof raw.enforce !== "boolean") fail(path, `enforce must be true or false, got ${JSON.stringify(raw.enforce)}`);
2652
+ policy.enforce = raw.enforce;
2653
+ }
2654
+ if ("tier" in raw) {
2655
+ if (raw.tier !== 0 && raw.tier !== 1 && raw.tier !== 2) fail(path, `tier must be 0, 1, or 2, got ${JSON.stringify(raw.tier)}`);
2656
+ policy.tier = raw.tier;
2657
+ }
2658
+ if ("floor" in raw) {
2659
+ const floor = raw.floor;
2660
+ if (!isPlainObject(floor)) fail(path, `floor must be a mapping, got ${JSON.stringify(floor)}`);
2661
+ const unknownFloor = Object.keys(floor).filter((k) => !KNOWN_FLOOR_KEYS.includes(k));
2662
+ if (unknownFloor.length > 0) fail(path, `unknown floor key(s): ${unknownFloor.join(", ")} (known: ${KNOWN_FLOOR_KEYS.join(", ")})`);
2663
+ if ("minMutantsExecuted" in floor) {
2664
+ const v = floor.minMutantsExecuted;
2665
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 1) {
2666
+ fail(path, `floor.minMutantsExecuted must be an integer >= 1, got ${JSON.stringify(v)}`);
2667
+ }
2668
+ policy.floor.minMutantsExecuted = v;
2669
+ }
2670
+ if ("maxErrorRate" in floor) policy.floor.maxErrorRate = checkNumber(path, "floor.maxErrorRate", floor.maxErrorRate, 0, 1);
2671
+ if ("minSamplingFraction" in floor) {
2672
+ policy.floor.minSamplingFraction = checkNumber(path, "floor.minSamplingFraction", floor.minSamplingFraction, 0, 1);
2673
+ }
2674
+ }
2675
+ if ("mutation" in raw) {
2676
+ const mu = raw.mutation;
2677
+ if (!isPlainObject(mu)) fail(path, `mutation must be a mapping, got ${JSON.stringify(mu)}`);
2678
+ const unknownMu = Object.keys(mu).filter((k) => !KNOWN_MUTATION_KEYS.includes(k));
2679
+ if (unknownMu.length > 0) fail(path, `unknown mutation key(s): ${unknownMu.join(", ")} (known: ${KNOWN_MUTATION_KEYS.join(", ")})`);
2680
+ if ("maxMutantsPerRun" in mu) {
2681
+ policy.mutation.maxMutantsPerRun = mu.maxMutantsPerRun === null ? null : checkIntRange(path, "mutation.maxMutantsPerRun", mu.maxMutantsPerRun, 1, 1e6);
2682
+ }
2683
+ if ("ignoreStatic" in mu) policy.mutation.ignoreStatic = checkBool(path, "mutation.ignoreStatic", mu.ignoreStatic);
2684
+ if ("recheckRepetitions" in mu) {
2685
+ policy.mutation.recheckRepetitions = mu.recheckRepetitions === null ? null : checkIntRange(path, "mutation.recheckRepetitions", mu.recheckRepetitions, 2, 10);
2686
+ }
2687
+ if ("recheckTimeoutFactorMultiplier" in mu) {
2688
+ policy.mutation.recheckTimeoutFactorMultiplier = mu.recheckTimeoutFactorMultiplier === null ? null : checkNumber(path, "mutation.recheckTimeoutFactorMultiplier", mu.recheckTimeoutFactorMultiplier, 1.01, 20);
2689
+ }
2690
+ if (policy.mutation.recheckRepetitions === null !== (policy.mutation.recheckTimeoutFactorMultiplier === null)) {
2691
+ fail(path, "mutation recheck repetitions and timeout-factor multiplier must be configured together");
2692
+ }
2693
+ }
2694
+ if ("sandbox" in raw) {
2695
+ const sb = raw.sandbox;
2696
+ if (!isPlainObject(sb)) fail(path, `sandbox must be a mapping, got ${JSON.stringify(sb)}`);
2697
+ const unknownSb = Object.keys(sb).filter((k) => !KNOWN_SANDBOX_KEYS.includes(k));
2698
+ if (unknownSb.length > 0) fail(path, `unknown sandbox key(s): ${unknownSb.join(", ")} (known: ${KNOWN_SANDBOX_KEYS.join(", ")})`);
2699
+ if ("envFiles" in sb) {
2700
+ const v = sb.envFiles;
2701
+ if (!Array.isArray(v) || v.some((x) => typeof x !== "string" || x.length === 0)) {
2702
+ fail(path, `sandbox.envFiles must be an array of non-empty strings, got ${JSON.stringify(v)}`);
2703
+ }
2704
+ for (const f of v) {
2705
+ if (f.startsWith("/") || f.split(/[\\/]+/).some((seg) => seg === "..")) {
2706
+ fail(path, `sandbox.envFiles entries must be repo-relative without "..": ${JSON.stringify(f)}`);
2707
+ }
2708
+ }
2709
+ policy.sandbox.envFiles = [...v];
2710
+ }
2711
+ }
2712
+ if ("target" in raw) {
2713
+ const target = raw.target;
2714
+ if (!isPlainObject(target)) fail(path, `target must be a mapping, got ${JSON.stringify(target)}`);
2715
+ checkNestedKeys(path, "target", target, KNOWN_TARGET_KEYS);
2716
+ if ("directory" in target) {
2717
+ const value = target.directory;
2718
+ if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\") || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
2719
+ fail(path, `target.directory must be a normalized repo-relative path, got ${JSON.stringify(value)}`);
2720
+ }
2721
+ policy.target.directory = value;
2722
+ }
2723
+ }
2724
+ if ("environment" in raw) {
2725
+ const environment = raw.environment;
2726
+ if (!isPlainObject(environment)) {
2727
+ fail(path, `environment must be a mapping, got ${JSON.stringify(environment)}`);
2728
+ }
2729
+ checkNestedKeys(path, "environment", environment, KNOWN_ENVIRONMENT_KEYS);
2730
+ if ("runtimeImage" in environment) {
2731
+ const problem = immutableImageProblem(environment.runtimeImage);
2732
+ if (problem) fail(path, `environment.runtimeImage ${problem}`);
2733
+ policy.environment.runtimeImage = environment.runtimeImage;
2734
+ }
2735
+ if ("installDirectory" in environment) {
2736
+ const value = environment.installDirectory;
2737
+ if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\") || value !== "." && value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
2738
+ fail(path, `environment.installDirectory must be '.' or a normalized repo-relative path, got ${JSON.stringify(value)}`);
2739
+ }
2740
+ policy.environment.installDirectory = value;
2741
+ }
2742
+ if ("installCommand" in environment) {
2743
+ const problem = preparedTestCommandProblem(environment.installCommand);
2744
+ if (problem) fail(path, `environment.installCommand ${problem}`);
2745
+ policy.environment.installCommand = environment.installCommand.trim();
2746
+ }
2747
+ if ("services" in environment) {
2748
+ if (!Array.isArray(environment.services) || environment.services.length > 2) {
2749
+ fail(path, "environment.services must be an array containing at most PostgreSQL and Redis");
2750
+ }
2751
+ const services = [];
2752
+ for (const [index, service] of environment.services.entries()) {
2753
+ if (!isPlainObject(service)) {
2754
+ fail(path, `environment.services[${index}] must be a mapping`);
2755
+ }
2756
+ checkNestedKeys(path, `environment.services[${index}]`, service, ["kind", "image"]);
2757
+ if (service.kind !== "postgres" && service.kind !== "redis") {
2758
+ fail(path, `environment.services[${index}].kind must be 'postgres' or 'redis'`);
2759
+ }
2760
+ const problem = immutableImageProblem(service.image);
2761
+ if (problem) fail(path, `environment.services[${index}].image ${problem}`);
2762
+ services.push({ kind: service.kind, image: service.image });
2763
+ }
2764
+ if (new Set(services.map((service) => service.kind)).size !== services.length) {
2765
+ fail(path, "environment.services may declare each service kind only once");
2766
+ }
2767
+ policy.environment.services = services.sort((left, right) => left.kind.localeCompare(right.kind));
2768
+ }
2769
+ if ("requiredVariables" in environment) {
2770
+ const values = checkStringArray(
2771
+ path,
2772
+ "environment.requiredVariables",
2773
+ environment.requiredVariables
2774
+ );
2775
+ if (values.length > 64 || values.some((value) => !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(value))) {
2776
+ fail(
2777
+ path,
2778
+ "environment.requiredVariables must contain at most 64 portable environment-variable names"
2779
+ );
2780
+ }
2781
+ const reserved = values.filter((value) => value.toUpperCase().startsWith("ABLOH_") || value.toUpperCase().startsWith("ATTEST_") || [
2782
+ "ANTHROPIC_API_KEY",
2783
+ "AWS_ACCESS_KEY_ID",
2784
+ "AWS_SECRET_ACCESS_KEY",
2785
+ "AWS_SESSION_TOKEN",
2786
+ "GH_TOKEN",
2787
+ "GITHUB_TOKEN",
2788
+ "OPENAI_API_KEY"
2789
+ ].includes(value.toUpperCase()));
2790
+ if (reserved.length > 0) {
2791
+ fail(path, `environment.requiredVariables contains reserved engine credential name(s): ${reserved.join(", ")}`);
2792
+ }
2793
+ policy.environment.requiredVariables = [...new Set(values)].sort();
2794
+ }
2795
+ if ("identityFiles" in environment) {
2796
+ const values = checkStringArray(path, "environment.identityFiles", environment.identityFiles);
2797
+ if (values.length > 64) {
2798
+ fail(path, "environment.identityFiles must contain at most 64 paths");
2799
+ }
2800
+ for (const value of values) {
2801
+ if (value.length === 0 || value.startsWith("/") || value.includes("\\") || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
2802
+ fail(
2803
+ path,
2804
+ `environment.identityFiles entries must be normalized repo-relative paths: ${JSON.stringify(value)}`
2805
+ );
2806
+ }
2807
+ }
2808
+ policy.environment.identityFiles = [...new Set(values)].sort();
2809
+ }
2810
+ if ("setupCommands" in environment) {
2811
+ const values = checkStringArray(path, "environment.setupCommands", environment.setupCommands);
2812
+ if (values.length > 8) {
2813
+ fail(path, "environment.setupCommands must contain at most 8 commands");
2814
+ }
2815
+ for (const value of values) {
2816
+ const problem = preparedTestCommandProblem(value);
2817
+ if (problem) fail(path, `environment.setupCommands entry ${problem}`);
2818
+ }
2819
+ policy.environment.setupCommands = values.map((value) => value.trim());
2820
+ }
2821
+ if ("generatedFiles" in environment) {
2822
+ const values = checkStringArray(path, "environment.generatedFiles", environment.generatedFiles);
2823
+ if (values.length > 128) {
2824
+ fail(path, "environment.generatedFiles must contain at most 128 paths");
2825
+ }
2826
+ for (const value of values) {
2827
+ if (value.length === 0 || value.startsWith("/") || value.includes("\\") || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
2828
+ fail(
2829
+ path,
2830
+ `environment.generatedFiles entries must be normalized repo-relative paths: ${JSON.stringify(value)}`
2831
+ );
2832
+ }
2833
+ }
2834
+ policy.environment.generatedFiles = [...new Set(values)].sort();
2835
+ }
2836
+ if ("testCommand" in environment) {
2837
+ const problem = preparedTestCommandProblem(environment.testCommand);
2838
+ if (problem) fail(path, `environment.testCommand ${problem}`);
2839
+ policy.environment.testCommand = environment.testCommand.trim();
2840
+ }
2841
+ }
2842
+ if ("diffCoverage" in raw) {
2843
+ const dc = raw.diffCoverage;
2844
+ if (!isPlainObject(dc)) fail(path, `diffCoverage must be a mapping, got ${JSON.stringify(dc)}`);
2845
+ const unknownDc = Object.keys(dc).filter((k) => !KNOWN_DIFF_COVERAGE_KEYS.includes(k));
2846
+ if (unknownDc.length > 0) {
2847
+ fail(path, `unknown diffCoverage key(s): ${unknownDc.join(", ")} (known: ${KNOWN_DIFF_COVERAGE_KEYS.join(", ")})`);
2848
+ }
2849
+ if ("shortCircuit" in dc) {
2850
+ if (typeof dc.shortCircuit !== "boolean") fail(path, `diffCoverage.shortCircuit must be true or false, got ${JSON.stringify(dc.shortCircuit)}`);
2851
+ policy.diffCoverage.shortCircuit = dc.shortCircuit;
2852
+ }
2853
+ }
2854
+ if ("flaky" in raw) {
2855
+ if (raw.flaky !== "quarantine" && raw.flaky !== "strict") {
2856
+ fail(path, `flaky must be "quarantine" or "strict", got ${JSON.stringify(raw.flaky)}`);
2857
+ }
2858
+ policy.flaky = raw.flaky;
2859
+ }
2860
+ if ("flaggedPaths" in raw) policy.flaggedPaths = checkStringArray(path, "flaggedPaths", raw.flaggedPaths);
2861
+ if ("scoreAggregation" in raw) {
2862
+ if (raw.scoreAggregation !== "standalone" && raw.scoreAggregation !== "worst-of-packages") {
2863
+ fail(path, `scoreAggregation must be "standalone" or "worst-of-packages", got ${JSON.stringify(raw.scoreAggregation)}`);
2864
+ }
2865
+ policy.scoreAggregation = raw.scoreAggregation;
2866
+ }
2867
+ if ("approvers" in raw) policy.approvers = checkStringArray(path, "approvers", raw.approvers);
2868
+ if ("classicMutation" in raw) validateClassicMutation(path, raw.classicMutation, policy.classicMutation);
2869
+ if ("patchRevert" in raw) validatePatchRevert(path, raw.patchRevert, policy.patchRevert);
2870
+ if ("research" in raw && RESEARCH_KEY_ALLOWED()) validateResearch(path, raw.research, policy.research);
2871
+ if ("findings" in raw) {
2872
+ const f = raw.findings;
2873
+ if (!isPlainObject(f)) fail(path, `findings must be a mapping, got ${JSON.stringify(f)}`);
2874
+ const unknownF = Object.keys(f).filter((k) => !KNOWN_FINDINGS_KEYS.includes(k));
2875
+ if (unknownF.length > 0) {
2876
+ fail(path, `unknown findings key(s): ${unknownF.join(", ")} (known: ${KNOWN_FINDINGS_KEYS.join(", ")})`);
2877
+ }
2878
+ if ("clustering" in f) {
2879
+ if (!CLUSTER_STRATEGIES.includes(f.clustering)) {
2880
+ fail(path, `findings.clustering must be one of ${CLUSTER_STRATEGIES.join(", ")}, got ${JSON.stringify(f.clustering)}`);
2881
+ }
2882
+ policy.findings.clustering = f.clustering;
2883
+ }
2884
+ if ("naming" in f) {
2885
+ if (typeof f.naming !== "boolean") {
2886
+ fail(path, `findings.naming must be true or false, got ${JSON.stringify(f.naming)}`);
2887
+ }
2888
+ policy.findings.naming = f.naming;
2889
+ }
2890
+ }
2891
+ return policy;
2892
+ }
2893
+ function loadPolicy(path) {
2894
+ if (!existsSync(path)) return validatePolicy(null, path);
2895
+ let raw;
2896
+ try {
2897
+ raw = parseYaml(readFileSync(path, "utf8"));
2898
+ } catch (err) {
2899
+ throw new Error(`invalid ${path}: ${err instanceof Error ? err.message : String(err)}`);
2900
+ }
2901
+ return validatePolicy(raw, path);
2902
+ }
2903
+
2904
+ // src/worst-of.ts
2905
+ var GATE_SEVERITY = { pass: 0, fail: 1, "cannot-attest": 2 };
2906
+ function deriveWorstOfGate(input) {
2907
+ let worst = {
2908
+ status: input.pooledGate.status,
2909
+ score: input.pooledGate.score,
2910
+ reason: input.pooledGate.reason ?? "pooled gate",
2911
+ directory: null
2912
+ };
2913
+ for (const row of input.rows) {
2914
+ let status;
2915
+ let score = null;
2916
+ let reason;
2917
+ if (row.l0State === "cannot-attest" || row.mutationState === "cannot-attest") {
2918
+ status = "cannot-attest";
2919
+ reason = `package ${row.directory}: evidence could not be attested`;
2920
+ } else if (!row.scoreable) {
2921
+ continue;
2922
+ } else {
2923
+ const scores = computeScores({
2924
+ counts: row.mutationCounts,
2925
+ confirmedEquivalent: row.confirmedEquivalent,
2926
+ tier: input.tier
2927
+ });
2928
+ score = input.useTriagedBasis && scores.triagedScore !== null ? scores.triagedScore : scores.rawScore;
2929
+ const l0Fails = row.l0Counts !== null && row.l0Counts.uncovered + row.l0Counts.notInstrumented > 0;
2930
+ if (l0Fails) {
2931
+ status = "fail";
2932
+ reason = `package ${row.directory}: ${row.l0Counts.uncovered + row.l0Counts.notInstrumented} changed line(s) unexecuted`;
2933
+ } else if (score !== null && score < input.threshold) {
2934
+ status = "fail";
2935
+ reason = `package ${row.directory}: score ${score}% below threshold ${input.threshold}%`;
2936
+ } else {
2937
+ status = "pass";
2938
+ reason = `package ${row.directory}: passed`;
2939
+ }
2940
+ }
2941
+ if (GATE_SEVERITY[status] > GATE_SEVERITY[worst.status]) {
2942
+ worst = { status, score, reason, directory: row.directory };
2943
+ }
2944
+ }
2945
+ return {
2946
+ gate: {
2947
+ status: worst.status,
2948
+ score: worst.score,
2949
+ threshold: input.threshold,
2950
+ reason: worst.reason
2951
+ },
2952
+ decisivePackage: worst.directory
2953
+ };
2954
+ }
2955
+
2956
+ // src/base64.ts
2957
+ function isCanonicalBase64(value) {
2958
+ if (value.length % 4 !== 0) return false;
2959
+ let padding = 0;
2960
+ while (padding < 2 && padding < value.length && value.charCodeAt(value.length - 1 - padding) === 61) {
2961
+ padding += 1;
2962
+ }
2963
+ const body = value.length - padding;
2964
+ for (let index = 0; index < body; index += 1) {
2965
+ const code = value.charCodeAt(index);
2966
+ const alphabet = code >= 65 && code <= 90 || // A-Z
2967
+ code >= 97 && code <= 122 || // a-z
2968
+ code >= 48 && code <= 57 || // 0-9
2969
+ code === 43 || // +
2970
+ code === 47;
2971
+ if (!alphabet) return false;
2972
+ }
2973
+ return true;
2974
+ }
2975
+
2976
+ // src/findings.ts
2977
+ var TRIAGE_REASON_CODES = [
2978
+ "EQUIVALENT_NO_OBSERVABLE_EFFECT",
2979
+ "EQUIVALENT_DEAD_BRANCH",
2980
+ "EQUIVALENT_REDUNDANT_CONDITION",
2981
+ "GAP_MISSING_ASSERTION",
2982
+ "GAP_UNCOVERED_BRANCH",
2983
+ "GAP_BOUNDARY_UNTESTED",
2984
+ "GAP_ERROR_PATH_UNTESTED",
2985
+ "UNCLEAR_NEEDS_HUMAN"
2986
+ ];
2987
+ var MECHANICAL_ERROR_PATH_REASON = "GAP_ERROR_PATH_UNTESTED";
2988
+ var REASON_GLOSS = {
2989
+ /* Each value is a COMPLETE headline, not a fragment: it is used as the leading
2990
+ clause of the finding title, so "No test checks this — the boundary case is not
2991
+ tested" (two ways of saying the same thing) is the failure mode to avoid. */
2992
+ GAP_MISSING_ASSERTION: "No test asserts on the result",
2993
+ GAP_UNCOVERED_BRANCH: "No test exercises this branch",
2994
+ GAP_BOUNDARY_UNTESTED: "No test covers the boundary case",
2995
+ GAP_ERROR_PATH_UNTESTED: "No test drives this error path",
2996
+ UNCLEAR_NEEDS_HUMAN: "Needs a human look",
2997
+ /* The equivalent codes are reasons a survivor is NOT a gap, so they must not read
2998
+ as a missing test. Rendered so a customer reviewing an override sees the stated
2999
+ reason in English. */
3000
+ EQUIVALENT_NO_OBSERVABLE_EFFECT: "Likely equivalent \u2014 no observable effect",
3001
+ EQUIVALENT_DEAD_BRANCH: "Likely equivalent \u2014 the branch cannot be reached",
3002
+ EQUIVALENT_REDUNDANT_CONDITION: "Likely equivalent \u2014 the condition is redundant"
3003
+ };
3004
+ var MUTATOR_GLOSS = {
3005
+ ArithmeticOperator: "swapped an arithmetic operator (e.g. + to -)",
3006
+ ArrayDeclaration: "emptied an array literal",
3007
+ AssignmentOperator: "swapped a compound assignment (e.g. += to -=)",
3008
+ BlockStatement: "removed an entire block body",
3009
+ BooleanLiteral: "inverted a boolean",
3010
+ ConditionalExpression: "forced a branch condition to a constant",
3011
+ EqualityOperator: "moved a comparison boundary (e.g. < to <=)",
3012
+ LogicalOperator: "swapped && and ||",
3013
+ MethodExpression: "dropped or replaced a method call",
3014
+ ObjectLiteral: "emptied an object literal",
3015
+ OptionalChaining: "toggled optional chaining",
3016
+ Regex: "altered a regular expression",
3017
+ StringLiteral: "changed a string literal",
3018
+ UnaryOperator: "flipped a unary operator (e.g. +x to -x)",
3019
+ UpdateOperator: "flipped an increment/decrement",
3020
+ ArrowFunction: "replaced an arrow-function body with undefined",
3021
+ ReturnValue: "replaced a return value with a plausible wrong value",
3022
+ GuardRemoval: "deleted a whole guard clause",
3023
+ ThrowRemoval: "deleted a throw statement"
3024
+ };
3025
+ var MAX_FINDING_DESCRIPTION_LEN = 200;
3026
+ var CODE_SYNTAX = /[`{}]|=>|::/u;
3027
+ var PRINTABLE_ASCII = /^[\x20-\x7E]*$/u;
3028
+ var ENGLISH_APOSTROPHE = /(?<=[A-Za-z])'(?=[A-Za-z])|(?<=s)'(?![A-Za-z])/gu;
3029
+ var ANY_QUOTE = /["']/u;
3030
+ function normalizePunctuation(text) {
3031
+ return text.replace(/[\u2010-\u2015]/gu, "-").replace(/[\u2018\u2019\u201A\u201B]/gu, "'").replace(/[\u201C-\u201F]/gu, '"').replace(/\u2026/gu, "...").replace(/[\u00A0\u2007\u202F]/gu, " ");
3032
+ }
3033
+ function sanitizeFindingDescription(raw) {
3034
+ if (typeof raw !== "string") return null;
3035
+ const text = normalizePunctuation(raw.trim().replace(/\s+/gu, " "));
3036
+ if (text.length === 0 || text.length > MAX_FINDING_DESCRIPTION_LEN) return null;
3037
+ if (!PRINTABLE_ASCII.test(text)) return null;
3038
+ if (CODE_SYNTAX.test(text)) return null;
3039
+ if (ANY_QUOTE.test(text.replace(ENGLISH_APOSTROPHE, ""))) return null;
3040
+ return text;
3041
+ }
3042
+ function describeMutationEvidence(mutator, status, location) {
3043
+ const file = location && typeof location.file === "string" ? location.file : null;
3044
+ const line = location && typeof location.startLine === "number" ? `:${location.startLine}` : "";
3045
+ const at = file ? ` at ${file}${line}` : "";
3046
+ if (status === "no-coverage") {
3047
+ return `Nothing in your suite executes this line${at}, so no mutation could be measured here.`;
3048
+ }
3049
+ const known = typeof mutator === "string" ? MUTATOR_GLOSS[mutator] : void 0;
3050
+ if (known) return `We ${known}${at} and every test still passed.`;
3051
+ const named = typeof mutator === "string" && mutator.length > 0 ? mutator : "a mutation";
3052
+ return `We planted ${named}${at} and every test still passed.`;
3053
+ }
3054
+ function describeReasonCode(code) {
3055
+ if (typeof code !== "string" || code.length === 0) return null;
3056
+ return REASON_GLOSS[code] ?? code;
3057
+ }
3058
+ function describeFinding(finding) {
3059
+ const triage = finding.triage && typeof finding.triage === "object" ? finding.triage : null;
3060
+ const sentence = sanitizeFindingDescription(triage?.description);
3061
+ if (sentence) return sentence;
3062
+ if (finding.status === "no-coverage") return "No test runs this line";
3063
+ const rawCode = triage ? triage.reasonCode : null;
3064
+ return describeReasonCode(typeof rawCode === "string" ? rawCode : null) ?? "No test checks this line";
3065
+ }
3066
+ function mechanicalErrorPathDisposition() {
3067
+ return { verdict: "real-gap", reasonCode: MECHANICAL_ERROR_PATH_REASON, confidence: 1, mechanical: true };
3068
+ }
3069
+ var FINDING_SEVERITIES = ["low", "medium", "high", "critical"];
3070
+ var SEVERITY_BY_REASON = {
3071
+ /* Nothing exercises the failure path. The one people find in production. */
3072
+ GAP_ERROR_PATH_UNTESTED: 3,
3073
+ /* A whole branch never runs, so every behaviour behind it is unmeasured. */
3074
+ GAP_UNCOVERED_BRANCH: 2,
3075
+ /* The path runs, but only in the middle of its range — off-by-ones live here. */
3076
+ GAP_BOUNDARY_UNTESTED: 2,
3077
+ /* The code ran and the test watched it happen without checking the result. */
3078
+ GAP_MISSING_ASSERTION: 1,
3079
+ /* The classifier declined to call it. A person has to look; that is not the same as urgent. */
3080
+ UNCLEAR_NEEDS_HUMAN: 1,
3081
+ /* Only reachable when a customer has overridden an equivalence verdict — it is on the list
3082
+ because they put it there, so it is listed, quietly. */
3083
+ EQUIVALENT_NO_OBSERVABLE_EFFECT: 0,
3084
+ EQUIVALENT_DEAD_BRANCH: 0,
3085
+ EQUIVALENT_REDUNDANT_CONDITION: 0
3086
+ };
3087
+ var UNSURE_BELOW = 0.5;
3088
+ function findingSeverity(finding) {
3089
+ const reasonCode = typeof finding.triage?.reasonCode === "string" ? finding.triage.reasonCode : null;
3090
+ const confidence = typeof finding.triage?.confidence === "number" && Number.isFinite(finding.triage.confidence) ? finding.triage.confidence : null;
3091
+ let score = reasonCode === null ? 1 : SEVERITY_BY_REASON[reasonCode] ?? 1;
3092
+ if (finding.status === "no-coverage" || finding.coveredBy === 0) score += 1;
3093
+ if (confidence !== null && confidence < UNSURE_BELOW) score -= 1;
3094
+ const clamped = Math.max(0, Math.min(FINDING_SEVERITIES.length - 1, score));
3095
+ return FINDING_SEVERITIES[clamped];
3096
+ }
3097
+ function findingKind(finding) {
3098
+ const reasonCode = typeof finding.triage?.reasonCode === "string" ? finding.triage.reasonCode : null;
3099
+ if (reasonCode === MECHANICAL_ERROR_PATH_REASON) return "error-handler";
3100
+ return finding.status === "no-coverage" ? "uncovered-line" : "surviving-mutant";
3101
+ }
3102
+ function isConfirmedEquivalent(t) {
3103
+ return !!t && !t.mechanical && t.verdict === "likely-equivalent" && !t.overridden;
3104
+ }
3105
+ function redactTriageForEgress(t) {
3106
+ const { rationale: _rationale, ...rest } = t;
3107
+ return rest;
3108
+ }
3109
+ var MUTATOR_TOKEN_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/u;
3110
+ function groupFindingsByLine(findings) {
3111
+ const groups = /* @__PURE__ */ new Map();
3112
+ const reasonTallies = /* @__PURE__ */ new Map();
3113
+ const soleDescription = /* @__PURE__ */ new Map();
3114
+ const reasonDescription = /* @__PURE__ */ new Map();
3115
+ for (const f of findings) {
3116
+ if (typeof f.file !== "string" || typeof f.startLine !== "number") continue;
3117
+ const endLine = typeof f.endLine === "number" ? f.endLine : f.startLine;
3118
+ const key = `${f.file} ${f.startLine} ${endLine}`;
3119
+ const mutator = typeof f.mutator === "string" && MUTATOR_TOKEN_RE.test(f.mutator) ? f.mutator : null;
3120
+ const rawCode = f.triage && typeof f.triage === "object" ? f.triage.reasonCode : null;
3121
+ const reasonCode = typeof rawCode === "string" && MUTATOR_TOKEN_RE.test(rawCode) ? rawCode : null;
3122
+ const group = groups.get(key);
3123
+ if (group) {
3124
+ group.count += 1;
3125
+ if (mutator) group.mutators.push(mutator);
3126
+ } else {
3127
+ groups.set(key, {
3128
+ first: typeof f.mutantId === "string" ? f.mutantId : String(groups.size),
3129
+ file: f.file,
3130
+ startLine: f.startLine,
3131
+ endLine,
3132
+ mutators: mutator ? [mutator] : [],
3133
+ count: 1,
3134
+ reasons: []
3135
+ });
3136
+ }
3137
+ const sentence = sanitizeFindingDescription(
3138
+ f.triage && typeof f.triage === "object" ? f.triage.description : null
3139
+ );
3140
+ if (reasonCode) {
3141
+ const tally = reasonTallies.get(key) ?? /* @__PURE__ */ new Map();
3142
+ tally.set(reasonCode, (tally.get(reasonCode) ?? 0) + 1);
3143
+ reasonTallies.set(key, tally);
3144
+ const reasonKey = `${key}\0${reasonCode}`;
3145
+ if (sentence && !reasonDescription.has(reasonKey)) {
3146
+ reasonDescription.set(reasonKey, sentence);
3147
+ }
3148
+ }
3149
+ if (!group && sentence) soleDescription.set(key, sentence);
3150
+ }
3151
+ for (const [key, group] of groups) {
3152
+ group.reasons = [...reasonTallies.get(key) ?? /* @__PURE__ */ new Map()].sort(([an, ac], [bn, bc]) => bc - ac || an.localeCompare(bn)).map(([code, count]) => {
3153
+ const description = reasonDescription.get(`${key}\0${code}`);
3154
+ return description ? { code, count, description } : { code, count };
3155
+ });
3156
+ const sentence = soleDescription.get(key);
3157
+ if (group.count === 1 && sentence) group.description = sentence;
3158
+ }
3159
+ return [...groups.values()];
3160
+ }
3161
+ function toGapFindings(mutants) {
3162
+ return mutants.filter((m) => m.status === "survived" || m.status === "no-coverage").map((m) => ({
3163
+ mutantId: m.id,
3164
+ file: m.file,
3165
+ startLine: m.startLine,
3166
+ endLine: m.endLine,
3167
+ mutator: m.mutator,
3168
+ replacement: m.replacement,
3169
+ status: m.status,
3170
+ coveredBy: m.coveredBy,
3171
+ // carried through, not dropped: absent on the Python engine and on historical artifacts,
3172
+ // where clustering degrades to the line key and says so rather than guessing a span
3173
+ ...m.startColumn !== void 0 ? { startColumn: m.startColumn } : {},
3174
+ ...m.endColumn !== void 0 ? { endColumn: m.endColumn } : {},
3175
+ ...m.originalText !== void 0 ? { originalText: m.originalText } : {}
3176
+ }));
3177
+ }
3178
+
3179
+ // src/gap-identity.ts
3180
+ function gapKeys(gaps) {
3181
+ const ordered = gaps.map((gap, index) => ({ gap, index })).sort(
3182
+ (a, b) => a.gap.file !== b.gap.file ? a.gap.file < b.gap.file ? -1 : 1 : a.gap.mutator !== b.gap.mutator ? a.gap.mutator < b.gap.mutator ? -1 : 1 : a.gap.startLine !== b.gap.startLine ? a.gap.startLine - b.gap.startLine : (
3183
+ /* Same file, same mutator, same line: two mutants of one kind on one line. Their
3184
+ relative order is arbitrary but must be DETERMINISTIC, so fall back to the order
3185
+ they arrived in rather than leaving it to the sort's stability guarantees. */
3186
+ a.index - b.index
3187
+ )
3188
+ );
3189
+ const seen = /* @__PURE__ */ new Map();
3190
+ const keys = new Array(gaps.length);
3191
+ for (const { gap, index } of ordered) {
3192
+ const bucket = JSON.stringify([gap.file, gap.mutator]);
3193
+ const ordinal = seen.get(bucket) ?? 0;
3194
+ seen.set(bucket, ordinal + 1);
3195
+ keys[index] = JSON.stringify([gap.file, gap.mutator, ordinal]);
3196
+ }
3197
+ return keys;
3198
+ }
3199
+
3200
+ // src/operators.ts
3201
+ var STRUCTURAL = /* @__PURE__ */ new Set([
3202
+ "BlockStatement",
3203
+ // removes an entire statement-block body
3204
+ "ConditionalExpression",
3205
+ // forces a guard/branch to true|false
3206
+ "MethodExpression",
3207
+ // drops a method call (e.g. .trim(), .filter())
3208
+ "ArrayDeclaration",
3209
+ // empties an array literal → []
3210
+ "ObjectLiteral",
3211
+ // empties an object literal → {}
3212
+ "ArrowFunction",
3213
+ // replaces an arrow body with undefined
3214
+ "OptionalChaining"
3215
+ // toggles optional chaining
3216
+ ]);
3217
+ var CUSTOM = /* @__PURE__ */ new Set([
3218
+ "ReturnValue",
3219
+ // replace a return value with a type-plausible wrong value
3220
+ "GuardRemoval",
3221
+ // delete a whole `if (cond) return/throw ...` guard
3222
+ "ThrowRemoval"
3223
+ // delete a `throw ...;` statement
3224
+ ]);
3225
+ function operatorClass(mutator) {
3226
+ if (CUSTOM.has(mutator)) return "custom";
3227
+ if (STRUCTURAL.has(mutator)) return "structural";
3228
+ return "operator";
3229
+ }
3230
+ function operatorClassBreakdown(mutators) {
3231
+ const out = { structural: 0, operator: 0, custom: 0 };
3232
+ for (const m of mutators) out[operatorClass(m)]++;
3233
+ return out;
3234
+ }
3235
+ function operatorClassSummary(mutators) {
3236
+ const b = operatorClassBreakdown(mutators);
3237
+ const parts = [];
3238
+ if (b.structural) parts.push(`${b.structural} structural`);
3239
+ if (b.custom) parts.push(`${b.custom} custom`);
3240
+ if (b.operator) parts.push(`${b.operator} operator`);
3241
+ return parts.length ? parts.join(", ") : "none";
3242
+ }
3243
+
3244
+ // src/classic-mutation-gate.ts
3245
+ function applyLayer1Gate(input) {
3246
+ const { baseGate } = input;
3247
+ if (baseGate.status === "cannot-attest") return baseGate;
3248
+ const reasons = [];
3249
+ if (input.failOnUntested && input.untestedHandlerMutantCount > 0) {
3250
+ reasons.push(`${input.untestedHandlerMutantCount} untested changed error-handler mutant(s)`);
3251
+ }
3252
+ if (input.failOnAntiPattern && input.antiPatternCount > 0) {
3253
+ reasons.push(`${input.antiPatternCount} error-handler anti-pattern(s)`);
3254
+ }
3255
+ if (reasons.length === 0) return baseGate;
3256
+ return {
3257
+ ...baseGate,
3258
+ status: "fail",
3259
+ reason: `${baseGate.reason ? baseGate.reason + "; " : ""}error-path policy: ${reasons.join(", ")}`
3260
+ };
3261
+ }
3262
+
3263
+ // src/test-findings.ts
3264
+ import { createHash as createHash2 } from "crypto";
3265
+ var USABLE = /* @__PURE__ */ new Set(["killed", "timeout", "survived", "no-coverage"]);
3266
+ function digest2(parts) {
3267
+ const h = createHash2("sha256");
3268
+ for (const p of parts) h.update(p + "\n");
3269
+ return h.digest("hex");
3270
+ }
3271
+ function analyzeTestFindings(input) {
3272
+ const { flakyTests, attributionComplete } = input;
3273
+ const covered = /* @__PURE__ */ new Map();
3274
+ const credited = /* @__PURE__ */ new Map();
3275
+ const ensure = (map, key) => {
3276
+ let s = map.get(key);
3277
+ if (!s) {
3278
+ s = /* @__PURE__ */ new Set();
3279
+ map.set(key, s);
3280
+ }
3281
+ return s;
3282
+ };
3283
+ for (const m of input.mutants) {
3284
+ if (!USABLE.has(m.status)) continue;
3285
+ for (const t of m.coveredByTests ?? []) {
3286
+ if (flakyTests.has(t)) continue;
3287
+ ensure(covered, t).add(m.id);
3288
+ }
3289
+ if (m.status === "killed" || m.status === "timeout") {
3290
+ for (const t of m.killedByTests ?? []) {
3291
+ if (flakyTests.has(t)) continue;
3292
+ ensure(credited, t).add(m.id);
3293
+ }
3294
+ }
3295
+ }
3296
+ const verdicts = [];
3297
+ const unassessed = [];
3298
+ for (const [test, coveredSet] of covered) {
3299
+ const creditedSet = credited.get(test);
3300
+ const creditedCount = creditedSet ? creditedSet.size : 0;
3301
+ if (coveredSet.size === 0 || creditedCount > 0) continue;
3302
+ if (!attributionComplete) {
3303
+ unassessed.push(test);
3304
+ continue;
3305
+ }
3306
+ verdicts.push({ test, kind: "asserts-nothing", coveredCount: coveredSet.size, creditedKillCount: 0 });
3307
+ }
3308
+ if (attributionComplete) {
3309
+ const groups = /* @__PURE__ */ new Map();
3310
+ const fingerprints = /* @__PURE__ */ new Map();
3311
+ for (const [test, coveredSet] of covered) {
3312
+ if (coveredSet.size === 0) continue;
3313
+ const creditedSet = credited.get(test) ?? /* @__PURE__ */ new Set();
3314
+ const coveredSorted = [...coveredSet].sort();
3315
+ const killedSorted = [...creditedSet].sort();
3316
+ const key = digest2(["cov", ...coveredSorted, "kill", ...killedSorted]);
3317
+ (groups.get(key) ?? groups.set(key, []).get(key)).push(test);
3318
+ fingerprints.set(key, { covered: coveredSorted, killed: killedSorted });
3319
+ }
3320
+ for (const [key, members] of groups) {
3321
+ if (members.length < 2) continue;
3322
+ const fp = fingerprints.get(key);
3323
+ for (const test of members) {
3324
+ verdicts.push({
3325
+ test,
3326
+ kind: "duplicate-effectiveness",
3327
+ coveredCount: fp.covered.length,
3328
+ creditedKillCount: fp.killed.length,
3329
+ duplicateGroupDigest: key
3330
+ });
3331
+ }
3332
+ }
3333
+ }
3334
+ verdicts.sort((a, b) => a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : a.test < b.test ? -1 : a.test > b.test ? 1 : 0);
3335
+ unassessed.sort();
3336
+ return { verdicts, unassessed, attribution: attributionComplete ? "complete" : "partial" };
3337
+ }
3338
+ function testIdentityDigest(canonical3) {
3339
+ return createHash2("sha256").update(canonical3).digest("hex");
3340
+ }
3341
+
3342
+ // src/scenario-search.ts
3343
+ var SCENARIO_LANES = ["property", "value", "state", "failure"];
3344
+ var SCENARIO_AUTHORITIES = [
3345
+ "machine-sourced",
3346
+ "source-grounded",
3347
+ "corroborated",
3348
+ "code-inferred"
3349
+ ];
3350
+ var SCENARIO_OUTCOMES = [
3351
+ "confirmed-counterexample",
3352
+ "candidate-counterexample",
3353
+ "no-counterexample-found",
3354
+ "scenario-uncovered",
3355
+ "disagreement",
3356
+ "inconclusive",
3357
+ "not-applicable"
3358
+ ];
3359
+ var SCENARIO_SEARCH_STATES = [
3360
+ "completed",
3361
+ "partial",
3362
+ "unavailable",
3363
+ "disabled",
3364
+ "disabled-by-tier",
3365
+ "not-run"
3366
+ ];
3367
+ var SCENARIO_SOURCE_KINDS = [
3368
+ "contract",
3369
+ "schema",
3370
+ "documentation",
3371
+ "ticket",
3372
+ "test",
3373
+ "type",
3374
+ "example",
3375
+ "code",
3376
+ "trace"
3377
+ ];
3378
+ var SCENARIO_SOURCE_INDEPENDENCE = [
3379
+ "independent",
3380
+ "derived-from-target",
3381
+ "unknown"
3382
+ ];
3383
+ var HEX_64 = /^[a-f0-9]{64}$/;
3384
+ var MAX_IDENTITY_LENGTH = 200;
3385
+ function enumGuard(values, value) {
3386
+ return typeof value === "string" && values.includes(value);
3387
+ }
3388
+ function isScenarioLane(value) {
3389
+ return enumGuard(SCENARIO_LANES, value);
3390
+ }
3391
+ function isScenarioAuthority(value) {
3392
+ return enumGuard(SCENARIO_AUTHORITIES, value);
3393
+ }
3394
+ function isScenarioOutcome(value) {
3395
+ return enumGuard(SCENARIO_OUTCOMES, value);
3396
+ }
3397
+ function isScenarioSearchState(value) {
3398
+ return enumGuard(SCENARIO_SEARCH_STATES, value);
3399
+ }
3400
+ function assertNonNegativeInteger(name, value) {
3401
+ if (!Number.isInteger(value) || value < 0) {
3402
+ throw new Error(`${name} must be a non-negative integer`);
3403
+ }
3404
+ }
3405
+ function assertIdentity(name, value) {
3406
+ if (value.length === 0 || value.length > MAX_IDENTITY_LENGTH || /[\u0000-\u001f\u007f]/.test(value)) {
3407
+ throw new Error(`${name} must be a bounded printable identity`);
3408
+ }
3409
+ }
3410
+ function assertDigest(name, value) {
3411
+ if (!HEX_64.test(value)) throw new Error(`${name} must be a lowercase 64-hex sha256`);
3412
+ }
3413
+ function emptyRecord(keys) {
3414
+ return Object.fromEntries(keys.map((key) => [key, 0]));
3415
+ }
3416
+ function sanitizeScenarioSummary(input) {
3417
+ if (!isScenarioLane(input.lane)) throw new Error("scenario summary has an unknown lane");
3418
+ if (!isScenarioAuthority(input.authority)) {
3419
+ throw new Error("scenario summary has an unknown authority");
3420
+ }
3421
+ if (!isScenarioOutcome(input.outcome)) throw new Error("scenario summary has an unknown outcome");
3422
+ for (const [name, value] of [
3423
+ ["scenarioId", input.scenarioId],
3424
+ ["targetDigest", input.targetDigest],
3425
+ ["intentDigest", input.intentDigest],
3426
+ ["sourceSetDigest", input.sourceSetDigest],
3427
+ ["containerDigest", input.containerDigest]
3428
+ ]) {
3429
+ assertDigest(name, value);
3430
+ }
3431
+ for (const [name, value] of [
3432
+ ["runner", input.runner],
3433
+ ["intentPromptVersion", input.intentPromptVersion],
3434
+ ["testPromptVersion", input.testPromptVersion],
3435
+ ["toolVersion", input.toolVersion]
3436
+ ]) {
3437
+ assertIdentity(name, value);
3438
+ }
3439
+ if (input.model !== null) assertIdentity("model", input.model);
3440
+ assertNonNegativeInteger("runs", input.runs);
3441
+ if (input.replayed !== void 0) assertNonNegativeInteger("replayed", input.replayed);
3442
+ const replayedCounterexample = input.outcome === "confirmed-counterexample" || input.outcome === "candidate-counterexample";
3443
+ if (replayedCounterexample) {
3444
+ if (input.runs === 0 || (input.replayed ?? 0) === 0) {
3445
+ throw new Error("a counterexample requires an executed search and a plain replay");
3446
+ }
3447
+ if (!input.targetExecuted || !input.assertionExecuted) {
3448
+ throw new Error("a counterexample requires both target and assertion execution");
3449
+ }
3450
+ }
3451
+ if (input.outcome === "confirmed-counterexample" && input.authority !== "machine-sourced" && input.authority !== "source-grounded") {
3452
+ throw new Error("a confirmed counterexample requires sourced authority");
3453
+ }
3454
+ if (input.outcome === "candidate-counterexample" && input.authority !== "corroborated" && input.authority !== "code-inferred") {
3455
+ throw new Error("a candidate counterexample requires corroborated or code-inferred authority");
3456
+ }
3457
+ if (input.outcome === "no-counterexample-found") {
3458
+ if (input.runs === 0 || !input.targetExecuted || !input.assertionExecuted) {
3459
+ throw new Error("no-counterexample-found requires an executed target and assertion");
3460
+ }
3461
+ }
3462
+ return {
3463
+ scenarioId: input.scenarioId,
3464
+ lane: input.lane,
3465
+ authority: input.authority,
3466
+ outcome: input.outcome,
3467
+ targetDigest: input.targetDigest,
3468
+ intentDigest: input.intentDigest,
3469
+ sourceSetDigest: input.sourceSetDigest,
3470
+ runs: input.runs,
3471
+ ...input.replayed === void 0 ? {} : { replayed: input.replayed },
3472
+ targetExecuted: input.targetExecuted,
3473
+ assertionExecuted: input.assertionExecuted,
3474
+ runner: input.runner,
3475
+ model: input.model,
3476
+ intentPromptVersion: input.intentPromptVersion,
3477
+ testPromptVersion: input.testPromptVersion,
3478
+ toolVersion: input.toolVersion,
3479
+ containerDigest: input.containerDigest
3480
+ };
3481
+ }
3482
+ function deriveScenarioSearchCounts(selectedTargets, inputSummaries) {
3483
+ assertNonNegativeInteger("selectedTargets", selectedTargets);
3484
+ const summaries = inputSummaries.map(sanitizeScenarioSummary);
3485
+ if (summaries.length > selectedTargets) {
3486
+ throw new Error("measured scenario summaries cannot exceed selected targets");
3487
+ }
3488
+ const ids = new Set(summaries.map((summary) => summary.scenarioId));
3489
+ if (ids.size !== summaries.length) throw new Error("scenario summary ids must be unique");
3490
+ const byOutcome = emptyRecord(SCENARIO_OUTCOMES);
3491
+ const byLane = emptyRecord(SCENARIO_LANES);
3492
+ const byAuthority = emptyRecord(SCENARIO_AUTHORITIES);
3493
+ for (const summary of summaries) {
3494
+ byOutcome[summary.outcome] += 1;
3495
+ byLane[summary.lane] += 1;
3496
+ byAuthority[summary.authority] += 1;
3497
+ }
3498
+ return {
3499
+ counts: {
3500
+ selectedTargets,
3501
+ measuredTargets: summaries.length,
3502
+ unmeasuredTargets: selectedTargets - summaries.length,
3503
+ byOutcome,
3504
+ byLane,
3505
+ byAuthority
3506
+ },
3507
+ summaries
3508
+ };
3509
+ }
3510
+ function buildScenarioSearchBlock(input) {
3511
+ for (const [name, value] of [
3512
+ ["policyDigest", input.policyDigest],
3513
+ ["evidencePackDigest", input.evidencePackDigest],
3514
+ ["proofsDigest", input.proofsDigest],
3515
+ ["containerImageDigest", input.containerImageDigest]
3516
+ ]) {
3517
+ assertDigest(name, value);
3518
+ }
3519
+ for (const [name, value] of [
3520
+ ["engineVersion", input.engineVersion],
3521
+ ["runnerAdapterVersion", input.runnerAdapterVersion],
3522
+ ["toolProtocolVersion", input.toolProtocolVersion]
3523
+ ]) {
3524
+ assertIdentity(name, value);
3525
+ }
3526
+ for (const model of input.models) assertIdentity("model", model);
3527
+ for (const [name, value] of [
3528
+ ["llmCalls", input.llmCalls],
3529
+ ["cacheHits", input.cacheHits],
3530
+ ["inputTokens", input.inputTokens],
3531
+ ["outputTokens", input.outputTokens],
3532
+ ["wallMs", input.wallMs]
3533
+ ]) {
3534
+ assertNonNegativeInteger(name, value);
3535
+ }
3536
+ if (input.reasoningTokens !== null) {
3537
+ assertNonNegativeInteger("reasoningTokens", input.reasoningTokens);
3538
+ }
3539
+ if (input.costUsd !== null && (!Number.isFinite(input.costUsd) || input.costUsd < 0)) {
3540
+ throw new Error("costUsd must be null or a non-negative finite number");
3541
+ }
3542
+ if (input.restrictions.containerRequired !== true || input.restrictions.network !== "none" || input.restrictions.credentials !== "none" || input.restrictions.productionWrites !== "denied") {
3543
+ throw new Error("scenario search restrictions may not be weakened");
3544
+ }
3545
+ const { counts, summaries } = deriveScenarioSearchCounts(
3546
+ input.selectedTargets,
3547
+ input.summaries
3548
+ );
3549
+ return {
3550
+ state: counts.unmeasuredTargets === 0 ? "completed" : "partial",
3551
+ engineVersion: input.engineVersion,
3552
+ policyDigest: input.policyDigest,
3553
+ evidencePackDigest: input.evidencePackDigest,
3554
+ proofsDigest: input.proofsDigest,
3555
+ containerImageDigest: input.containerImageDigest,
3556
+ runnerAdapterVersion: input.runnerAdapterVersion,
3557
+ toolProtocolVersion: input.toolProtocolVersion,
3558
+ models: [...input.models],
3559
+ llmCalls: input.llmCalls,
3560
+ cacheHits: input.cacheHits,
3561
+ inputTokens: input.inputTokens,
3562
+ outputTokens: input.outputTokens,
3563
+ reasoningTokens: input.reasoningTokens,
3564
+ costUsd: input.costUsd,
3565
+ wallMs: input.wallMs,
3566
+ restrictions: {
3567
+ containerRequired: true,
3568
+ network: "none",
3569
+ credentials: "none",
3570
+ productionWrites: "denied"
3571
+ },
3572
+ counts,
3573
+ summaries
3574
+ };
3575
+ }
3576
+
3577
+ // src/ci-property-ranking.ts
3578
+ import { createHash as createHash3 } from "crypto";
3579
+ var FAMILIES_BY_MUTATOR = {
3580
+ // Value and arithmetic flips: a quantity comes out wrong, so ask what the quantity conserves.
3581
+ ArithmeticOperator: ["conservation", "monotonicity"],
3582
+ AssignmentOperator: ["conservation", "monotonicity"],
3583
+ UnaryOperator: ["monotonicity", "conservation"],
3584
+ UpdateOperator: ["monotonicity", "conservation"],
3585
+ // Branch and guard flips: a decision goes the wrong way at some input, so ask where the
3586
+ // accept/reject line sits.
3587
+ BooleanLiteral: ["acceptance-boundary"],
3588
+ ConditionalExpression: ["acceptance-boundary", "error-containment"],
3589
+ EqualityOperator: ["acceptance-boundary", "monotonicity"],
3590
+ LogicalOperator: ["acceptance-boundary"],
3591
+ GuardRemoval: ["acceptance-boundary", "error-containment"],
3592
+ // Text and pattern flips: what changes is a representation, not a decision.
3593
+ Regex: ["normalization-equivalence", "acceptance-boundary"],
3594
+ StringLiteral: ["normalization-equivalence", "serialization-preservation"],
3595
+ // Dropped calls and emptied literals: something the code builds up is lost.
3596
+ MethodExpression: ["normalization-equivalence", "idempotence", "order-preservation"],
3597
+ ArrayDeclaration: ["conservation", "order-preservation", "partition-recombine"],
3598
+ ObjectLiteral: ["serialization-preservation", "round-trip"],
3599
+ ReturnValue: ["round-trip", "representation-boundary"],
3600
+ // Whole constructs removed: the alarming ones. Effects show up as state left behind.
3601
+ BlockStatement: ["state-transition", "non-mutation", "resource-lifecycle"],
3602
+ ArrowFunction: ["state-transition", "round-trip"],
3603
+ OptionalChaining: ["error-containment"],
3604
+ ThrowRemoval: ["error-containment", "recovery-continuity"]
3605
+ };
3606
+ function signalText(value, limit) {
3607
+ return value.replace(/\s+/gu, " ").trim().slice(0, limit);
3608
+ }
3609
+ function survivingMutantObligationSignals(entries) {
3610
+ const signals = [];
3611
+ for (const entry of entries) {
3612
+ if (entry.state !== "open" || entry.mutantIdentity === void 0) continue;
3613
+ const parts = entry.mutantIdentity.split("\0");
3614
+ const mutator = parts[5];
3615
+ if (mutator === void 0) continue;
3616
+ const families = FAMILIES_BY_MUTATOR[mutator];
3617
+ if (families === void 0 || families.length === 0) continue;
3618
+ const siteLine = Number(parts[1]);
3619
+ const startLine = Number.isInteger(siteLine) && siteLine >= 1 ? siteLine : entry.changedRange.startLine;
3620
+ const siteEnd = Number(parts[2]);
3621
+ const endLine = Number.isInteger(siteEnd) && siteEnd >= startLine ? siteEnd : startLine;
3622
+ signals.push({
3623
+ // One signal per mutant, aimed at the FIRST family — the map orders each operator's
3624
+ // families by fit, and one aimed signal beats three diluted ones under the 24-signal cap.
3625
+ family: families[0],
3626
+ trigger: signalText(
3627
+ `a ${mutator} mutation at this location survived the repository's own test suite, so its behaviour there is not pinned by any existing test`,
3628
+ 200
3629
+ ),
3630
+ authority: "mechanical-risk",
3631
+ path: entry.targetPath,
3632
+ startLine,
3633
+ endLine,
3634
+ targetSymbols: [signalText(entry.targetSymbol, 500)],
3635
+ searchTerms: [signalText(entry.targetSymbol, 100), signalText(mutator, 100)],
3636
+ // Survival outranks static analysis. A surviving mutant is a DEMONSTRATED behaviour delta
3637
+ // the suite cannot see; a structural hint is a guess that one might exist there — yet
3638
+ // structural families claim strength 3 while operator-class mutants claimed 2, so the
3639
+ // proven signal sorted below the guesses and could be evicted by the 24-signal cap.
3640
+ strength: 3,
3641
+ mutator: signalText(mutator, 100)
3642
+ });
3643
+ }
3644
+ return signals.sort(
3645
+ (left, right) => right.strength - left.strength || left.path.localeCompare(right.path) || left.startLine - right.startLine
3646
+ );
3647
+ }
3648
+ function ciPropertyMutatorFamilies(mutantIdentity2) {
3649
+ if (mutantIdentity2 === void 0) return [];
3650
+ const mutator = mutantIdentity2.split("\0")[5];
3651
+ return mutator === void 0 ? [] : FAMILIES_BY_MUTATOR[mutator] ?? [];
3652
+ }
3653
+ var HEX643 = /^[a-f0-9]{64}$/;
3654
+ function sha2562(value) {
3655
+ return createHash3("sha256").update(value, "utf8").digest("hex");
3656
+ }
3657
+ function canonical2(value) {
3658
+ if (Array.isArray(value)) return `[${value.map(canonical2).join(",")}]`;
3659
+ if (value && typeof value === "object") {
3660
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonical2(item)}`).join(",")}}`;
3661
+ }
3662
+ return JSON.stringify(value);
3663
+ }
3664
+ function mergeTargetCandidates(classicMutation, sourced) {
3665
+ if (classicMutation.path !== sourced.path || classicMutation.symbol !== sourced.symbol || classicMutation.changeKind !== sourced.changeKind) {
3666
+ throw new Error(
3667
+ `conflicting CI property target identity '${classicMutation.targetDigest}'`
3668
+ );
3669
+ }
3670
+ return {
3671
+ ...classicMutation,
3672
+ runnerSupported: classicMutation.runnerSupported && sourced.runnerSupported,
3673
+ hasIndependentEvidence: classicMutation.hasIndependentEvidence || sourced.hasIndependentEvidence,
3674
+ publiclyReachable: classicMutation.publiclyReachable && sourced.publiclyReachable,
3675
+ excludedChange: classicMutation.excludedChange || sourced.excludedChange,
3676
+ generatedOrBinary: classicMutation.generatedOrBinary || sourced.generatedOrBinary,
3677
+ requiresForbiddenExternalResource: classicMutation.requiresForbiddenExternalResource || sourced.requiresForbiddenExternalResource,
3678
+ sourceConflict: classicMutation.sourceConflict || sourced.sourceConflict,
3679
+ allKnownLayer1GapsClosed: classicMutation.allKnownLayer1GapsClosed || sourced.allKnownLayer1GapsClosed,
3680
+ sourceIntroducesDifferentRule: classicMutation.sourceIntroducesDifferentRule || sourced.sourceIntroducesDifferentRule,
3681
+ openLayer1Residuals: Math.max(
3682
+ classicMutation.openLayer1Residuals,
3683
+ sourced.openLayer1Residuals
3684
+ ),
3685
+ mutationSiteAbsent: classicMutation.mutationSiteAbsent || sourced.mutationSiteAbsent,
3686
+ changedBranchHasDirectTest: classicMutation.changedBranchHasDirectTest || sourced.changedBranchHasDirectTest,
3687
+ supportedPropertyFamilies: Math.max(
3688
+ classicMutation.supportedPropertyFamilies,
3689
+ sourced.supportedPropertyFamilies
3690
+ ),
3691
+ estimatedSetupCost: Math.max(
3692
+ classicMutation.estimatedSetupCost,
3693
+ sourced.estimatedSetupCost
3694
+ )
3695
+ };
3696
+ }
3697
+ function buildCiPropertyTargetUniverse(input) {
3698
+ const universe = /* @__PURE__ */ new Map();
3699
+ const add = (target, source) => {
3700
+ const existing = universe.get(target.targetDigest);
3701
+ if (existing === void 0) {
3702
+ universe.set(target.targetDigest, target);
3703
+ return;
3704
+ }
3705
+ if (source === "classicMutation") {
3706
+ throw new Error(`duplicate Layer 1 CI property target '${target.targetDigest}'`);
3707
+ }
3708
+ universe.set(target.targetDigest, mergeTargetCandidates(existing, target));
3709
+ };
3710
+ for (const target of input.layer1Targets) add(target, "classicMutation");
3711
+ const sourced = /* @__PURE__ */ new Set();
3712
+ for (const target of input.sourceTargets) {
3713
+ if (sourced.has(target.targetDigest)) {
3714
+ throw new Error(
3715
+ `duplicate independently sourced CI property target '${target.targetDigest}'`
3716
+ );
3717
+ }
3718
+ sourced.add(target.targetDigest);
3719
+ add(target, "independent-source");
3720
+ }
3721
+ return [...universe.values()].sort(
3722
+ (left, right) => left.targetDigest.localeCompare(right.targetDigest)
3723
+ );
3724
+ }
3725
+ function targetRejections(target) {
3726
+ const reasons = [];
3727
+ if (!HEX643.test(target.targetDigest)) reasons.push("invalid-target-digest");
3728
+ if (!target.runnerSupported) reasons.push("unsupported-runner");
3729
+ if (!target.hasIndependentEvidence) reasons.push("no-independent-evidence");
3730
+ if (!target.publiclyReachable) reasons.push("not-publicly-reachable");
3731
+ if (target.excludedChange) reasons.push("test-config-vendor-or-build-only");
3732
+ if (target.generatedOrBinary) reasons.push("generated-or-binary");
3733
+ if (target.requiresForbiddenExternalResource) reasons.push("forbidden-external-resource");
3734
+ if (target.sourceConflict) reasons.push("source-disagreement");
3735
+ if (target.allKnownLayer1GapsClosed && !target.sourceIntroducesDifferentRule) {
3736
+ reasons.push("all-known-gaps-closed");
3737
+ }
3738
+ if (!Number.isInteger(target.openLayer1Residuals) || target.openLayer1Residuals < 0) {
3739
+ reasons.push("invalid-open-residual-count");
3740
+ }
3741
+ if (!Number.isInteger(target.supportedPropertyFamilies) || target.supportedPropertyFamilies < 0) {
3742
+ reasons.push("invalid-property-family-count");
3743
+ }
3744
+ if (!Number.isFinite(target.estimatedSetupCost) || target.estimatedSetupCost < 0) {
3745
+ reasons.push("invalid-setup-cost");
3746
+ }
3747
+ return reasons;
3748
+ }
3749
+ function compareTargets(left, right) {
3750
+ const flags = [
3751
+ (target) => target.hasIndependentEvidence,
3752
+ (target) => target.openLayer1Residuals > 0,
3753
+ (target) => target.mutationSiteAbsent,
3754
+ (target) => target.publiclyReachable,
3755
+ (target) => !target.changedBranchHasDirectTest
3756
+ ];
3757
+ for (const flag of flags) {
3758
+ const difference = Number(flag(right)) - Number(flag(left));
3759
+ if (difference !== 0) return difference;
3760
+ }
3761
+ if (right.supportedPropertyFamilies !== left.supportedPropertyFamilies) {
3762
+ return right.supportedPropertyFamilies - left.supportedPropertyFamilies;
3763
+ }
3764
+ if (left.estimatedSetupCost !== right.estimatedSetupCost) {
3765
+ return left.estimatedSetupCost - right.estimatedSetupCost;
3766
+ }
3767
+ return left.targetDigest.localeCompare(right.targetDigest);
3768
+ }
3769
+ function selectCiPropertyTargets(input, maxTargets = 2) {
3770
+ if (!Number.isInteger(maxTargets) || maxTargets < 1 || maxTargets > 2) {
3771
+ throw new Error("CI property maxTargets must be one or two");
3772
+ }
3773
+ const seen = /* @__PURE__ */ new Set();
3774
+ for (const target of input) {
3775
+ if (seen.has(target.targetDigest)) {
3776
+ throw new Error(`duplicate CI property target '${target.targetDigest}'`);
3777
+ }
3778
+ seen.add(target.targetDigest);
3779
+ }
3780
+ const reasons = new Map(
3781
+ input.map((target) => [target.targetDigest, targetRejections(target)])
3782
+ );
3783
+ const eligible = input.filter((target) => reasons.get(target.targetDigest).length === 0).sort(compareTargets);
3784
+ const selected = eligible.slice(0, maxTargets);
3785
+ const selectedIds = new Set(selected.map((target) => target.targetDigest));
3786
+ return {
3787
+ selected,
3788
+ decisions: input.map((target) => {
3789
+ const targetReasons = [...reasons.get(target.targetDigest)];
3790
+ const chosen = selectedIds.has(target.targetDigest);
3791
+ if (!chosen && targetReasons.length === 0) targetReasons.push("target-budget");
3792
+ return { target, selected: chosen, reasons: targetReasons };
3793
+ }).sort(
3794
+ (left, right) => left.target.targetDigest.localeCompare(right.target.targetDigest)
3795
+ )
3796
+ };
3797
+ }
3798
+ function ciPropertyBehaviourKey(rule) {
3799
+ return sha2562(
3800
+ canonical2({
3801
+ targetDigest: rule.targetDigest,
3802
+ propertyFamily: rule.propertyFamily.trim().toLowerCase(),
3803
+ operationSequence: rule.operationSequence.map((step) => step.trim()),
3804
+ observation: {
3805
+ kind: rule.observation.kind.trim().toLowerCase(),
3806
+ subject: rule.observation.subject.trim(),
3807
+ expected: rule.observation.expected.trim()
3808
+ },
3809
+ inputDomainKinds: [...new Set(rule.inputDomainKinds.map((kind) => kind.trim().toLowerCase()))].sort(),
3810
+ sourceSetDigest: rule.sourceSetDigest
3811
+ })
3812
+ );
3813
+ }
3814
+ function compareRules(left, right) {
3815
+ const authority = { "machine-sourced": 0, "source-grounded": 1 };
3816
+ if (authority[left.authority] !== authority[right.authority]) {
3817
+ return authority[left.authority] - authority[right.authority];
3818
+ }
3819
+ const match = { exact: 0, indirect: 1, none: 2 };
3820
+ if (match[left.openLayer1Match] !== match[right.openLayer1Match]) {
3821
+ return match[left.openLayer1Match] - match[right.openLayer1Match];
3822
+ }
3823
+ if (left.riskTriggerStrength !== right.riskTriggerStrength) {
3824
+ return right.riskTriggerStrength - left.riskTriggerStrength;
3825
+ }
3826
+ if (left.evidenceSpecificity !== right.evidenceSpecificity) {
3827
+ return right.evidenceSpecificity - left.evidenceSpecificity;
3828
+ }
3829
+ if (left.inputValidityConfidence !== right.inputValidityConfidence) {
3830
+ return right.inputValidityConfidence - left.inputValidityConfidence;
3831
+ }
3832
+ if (left.templateSupported !== right.templateSupported) {
3833
+ return Number(right.templateSupported) - Number(left.templateSupported);
3834
+ }
3835
+ if (left.setupDependencyCount !== right.setupDependencyCount) {
3836
+ return left.setupDependencyCount - right.setupDependencyCount;
3837
+ }
3838
+ if (left.modelPriority !== right.modelPriority) {
3839
+ return left.modelPriority - right.modelPriority;
3840
+ }
3841
+ return left.intentDigest.localeCompare(right.intentDigest);
3842
+ }
3843
+ function rankCiPropertyRules(input, options = {}) {
3844
+ const poolSize = options.poolSize ?? 8;
3845
+ if (!Number.isInteger(poolSize) || poolSize < 1 || poolSize > 8) {
3846
+ throw new Error("CI property rule pool must contain one to eight rules");
3847
+ }
3848
+ const closed = options.closedBehaviourKeys ?? /* @__PURE__ */ new Set();
3849
+ const intentIds = /* @__PURE__ */ new Set();
3850
+ const behaviourIds = /* @__PURE__ */ new Set();
3851
+ const accepted = [];
3852
+ const dropped = [];
3853
+ for (const rule of input) {
3854
+ if (!HEX643.test(rule.intentDigest) || !HEX643.test(rule.targetDigest) || !HEX643.test(rule.sourceSetDigest)) {
3855
+ throw new Error("CI property rules require sha256 intent, target, and source-set identities");
3856
+ }
3857
+ if (!Number.isInteger(rule.setupDependencyCount) || rule.setupDependencyCount < 0) {
3858
+ throw new Error("setupDependencyCount must be a non-negative integer");
3859
+ }
3860
+ for (const [name, value] of [
3861
+ ["evidenceSpecificity", rule.evidenceSpecificity],
3862
+ ["riskTriggerStrength", rule.riskTriggerStrength],
3863
+ ["inputValidityConfidence", rule.inputValidityConfidence]
3864
+ ]) {
3865
+ if (!Number.isInteger(value) || value < 0 || value > 3) {
3866
+ throw new Error(`${name} must be an integer from zero to three`);
3867
+ }
3868
+ }
3869
+ if (!Number.isInteger(rule.modelPriority) || rule.modelPriority < 0) {
3870
+ throw new Error("modelPriority must be a non-negative integer");
3871
+ }
3872
+ if (intentIds.has(rule.intentDigest)) {
3873
+ dropped.push({ intentDigest: rule.intentDigest, reason: "duplicate-intent" });
3874
+ continue;
3875
+ }
3876
+ intentIds.add(rule.intentDigest);
3877
+ const behaviourKey = ciPropertyBehaviourKey(rule);
3878
+ if (closed.has(behaviourKey)) {
3879
+ dropped.push({ intentDigest: rule.intentDigest, reason: "closed-classicMutation" });
3880
+ continue;
3881
+ }
3882
+ if (behaviourIds.has(behaviourKey)) {
3883
+ dropped.push({ intentDigest: rule.intentDigest, reason: "duplicate-behaviour" });
3884
+ continue;
3885
+ }
3886
+ behaviourIds.add(behaviourKey);
3887
+ accepted.push({ ...rule, behaviourKey });
3888
+ }
3889
+ accepted.sort(compareRules);
3890
+ const firstByFamily = [];
3891
+ const repeatedFamilies = [];
3892
+ const families = /* @__PURE__ */ new Set();
3893
+ for (const rule of accepted) {
3894
+ if (families.has(rule.propertyFamily)) repeatedFamilies.push(rule);
3895
+ else {
3896
+ families.add(rule.propertyFamily);
3897
+ firstByFamily.push(rule);
3898
+ }
3899
+ }
3900
+ const ordered = [...firstByFamily, ...repeatedFamilies];
3901
+ for (const rule of ordered.slice(poolSize)) {
3902
+ dropped.push({ intentDigest: rule.intentDigest, reason: "pool-cap" });
3903
+ }
3904
+ return { ranked: ordered.slice(0, poolSize), dropped };
3905
+ }
3906
+
3907
+ // src/ci-property-obligations.ts
3908
+ var CI_PROPERTY_FAMILIES = [
3909
+ "round-trip",
3910
+ "idempotence",
3911
+ "order-preservation",
3912
+ "monotonicity",
3913
+ "normalization-equivalence",
3914
+ "partition-recombine",
3915
+ "serialization-preservation",
3916
+ "acceptance-boundary",
3917
+ "state-transition",
3918
+ "error-containment",
3919
+ "recovery-continuity",
3920
+ "non-mutation",
3921
+ "cache-coherence",
3922
+ "representation-boundary",
3923
+ "resource-lifecycle",
3924
+ "conservation"
3925
+ ];
3926
+ var CI_OBLIGATION_SIGNAL_AUTHORITIES = [
3927
+ "explicit-contract",
3928
+ "mechanical-risk"
3929
+ ];
3930
+
3931
+ // src/customer-environment.ts
3932
+ import { homedir as homedir2 } from "os";
3933
+ import { isAbsolute, join } from "path";
3934
+ var PORTABLE_RUNTIME_NAMES = /* @__PURE__ */ new Set([
3935
+ "CI",
3936
+ "HOME",
3937
+ "LANG",
3938
+ "LC_ALL",
3939
+ "LC_CTYPE",
3940
+ "LOGNAME",
3941
+ "NODE_ENV",
3942
+ "NO_COLOR",
3943
+ "PATH",
3944
+ "PATHEXT",
3945
+ "SHELL",
3946
+ "SYSTEMROOT",
3947
+ "TEMP",
3948
+ "TERM",
3949
+ "TMP",
3950
+ "TMPDIR",
3951
+ "TZ",
3952
+ "USER"
3953
+ ]);
3954
+ var RESERVED_EXACT = /* @__PURE__ */ new Set([
3955
+ "ANTHROPIC_API_KEY",
3956
+ "AWS_ACCESS_KEY_ID",
3957
+ "AWS_SECRET_ACCESS_KEY",
3958
+ "AWS_SESSION_TOKEN",
3959
+ "GH_TOKEN",
3960
+ "GITHUB_TOKEN",
3961
+ "OPENAI_API_KEY",
3962
+ // These are engine-enforced package-manager safety controls. A repository must not be able to
3963
+ // re-enable Corepack downloads or package-registry access by listing one in abloh.yml.
3964
+ "COREPACK_ENABLE_AUTO_PIN",
3965
+ "COREPACK_ENABLE_NETWORK",
3966
+ "COREPACK_DEFAULT_TO_LATEST",
3967
+ "COREPACK_HOME",
3968
+ "BUN_OPTIONS",
3969
+ "DO_NOT_TRACK",
3970
+ "NPM_CONFIG_AUDIT",
3971
+ "NPM_CONFIG_FUND",
3972
+ "NPM_CONFIG_OFFLINE",
3973
+ "NPM_CONFIG_UPDATE_NOTIFIER",
3974
+ // Node preload and module-resolution flags execute before the requested tool. In particular,
3975
+ // `--preserve-symlinks` makes pnpm's flat compatibility links outrank packages' declared nested
3976
+ // dependencies; customer-controlled preload flags would also execute arbitrary code inside the
3977
+ // engine process. Neither belongs in a measured customer environment.
3978
+ "NODE_OPTIONS",
3979
+ "PNPM_DISABLE_SELF_UPDATE_CHECK",
3980
+ "YARN_ENABLE_NETWORK",
3981
+ "YARN_ENABLE_TELEMETRY",
3982
+ // Yarn 4 rejects `YARN_OFFLINE` as an unknown legacy configuration key. Keep it reserved so a
3983
+ // repository cannot reintroduce it; `YARN_ENABLE_NETWORK=0` is the supported network boundary.
3984
+ "YARN_OFFLINE"
3985
+ ]);
3986
+ function packageManagerSafetyEnvironment(source) {
3987
+ const home = source.HOME ?? homedir2();
3988
+ const corepackHome = source.COREPACK_HOME ?? join(home, ".cache", "node", "corepack");
3989
+ if (!isAbsolute(corepackHome)) {
3990
+ throw new Error("COREPACK_HOME must be absolute so delegated package-manager bytes can be bound");
3991
+ }
3992
+ return {
3993
+ BUN_OPTIONS: "--no-install",
3994
+ COREPACK_ENABLE_AUTO_PIN: "0",
3995
+ COREPACK_ENABLE_NETWORK: "0",
3996
+ COREPACK_DEFAULT_TO_LATEST: "0",
3997
+ COREPACK_HOME: corepackHome,
3998
+ DO_NOT_TRACK: "1",
3999
+ NPM_CONFIG_AUDIT: "false",
4000
+ NPM_CONFIG_FUND: "false",
4001
+ NPM_CONFIG_OFFLINE: "true",
4002
+ NPM_CONFIG_UPDATE_NOTIFIER: "false",
4003
+ PNPM_DISABLE_SELF_UPDATE_CHECK: "true",
4004
+ // pnpm >=11 inspects node_modules before running scripts and refuses entries it did not
4005
+ // create — including Abloh's staged coverage-provider overlay. Measurement consumes the
4006
+ // prepared tree as-is; re-verification (like re-installation) is never part of a run.
4007
+ npm_config_verify_deps_before_run: "false",
4008
+ YARN_ENABLE_NETWORK: "0",
4009
+ YARN_ENABLE_TELEMETRY: "0"
4010
+ };
4011
+ }
4012
+ function isReservedCustomerVariable(name) {
4013
+ const upper = name.toUpperCase();
4014
+ return upper.startsWith("ABLOH_") || upper.startsWith("ATTEST_") || RESERVED_EXACT.has(upper);
4015
+ }
4016
+ function customerEnvironmentNames(required) {
4017
+ const names = new Set(PORTABLE_RUNTIME_NAMES);
4018
+ for (const name of required) {
4019
+ if (isReservedCustomerVariable(name)) {
4020
+ throw new Error(`environment variable ${name} is reserved for Abloh and cannot be exposed to repository code`);
4021
+ }
4022
+ names.add(name);
4023
+ }
4024
+ return [...names].sort();
4025
+ }
4026
+ function buildCustomerProcessEnvironment(source = process.env, required = [], overrides = {}) {
4027
+ const missing = required.filter((name) => source[name] === void 0);
4028
+ if (missing.length > 0) {
4029
+ throw new Error(`prepared test environment is missing required variable(s): ${missing.join(", ")}`);
4030
+ }
4031
+ const output = {};
4032
+ for (const name of customerEnvironmentNames(required)) {
4033
+ const value = source[name];
4034
+ if (value !== void 0) output[name] = value;
4035
+ }
4036
+ for (const [name, value] of Object.entries(overrides)) {
4037
+ if (isReservedCustomerVariable(name)) {
4038
+ throw new Error(`environment override ${name} is reserved for Abloh`);
4039
+ }
4040
+ output[name] = value;
4041
+ }
4042
+ Object.assign(output, packageManagerSafetyEnvironment(source));
4043
+ return output;
4044
+ }
4045
+ function containsEngineCredential(environment) {
4046
+ return Object.keys(environment).some(isReservedCustomerVariable);
4047
+ }
4048
+
4049
+ // src/capability-registry.ts
4050
+ var PROVIDER_RAW_FORMAT = {
4051
+ v8: "istanbul-coverage-final-v1",
4052
+ babel: "istanbul-coverage-final-v1",
4053
+ c8: "istanbul-coverage-final-v1",
4054
+ "bun-lcov": "lcov-v1",
4055
+ "coverage.py": "coverage-py-json-v1"
4056
+ };
4057
+ var RUNNER_CAPABILITIES = [
4058
+ { runner: "jest", ecosystem: "js", layer0Providers: ["v8", "babel"], layer0Enabled: true, mutation: "stryker-plugin", perTestDrivable: true },
4059
+ { runner: "vitest", ecosystem: "js", layer0Providers: ["v8"], layer0Enabled: true, mutation: "stryker-plugin", perTestDrivable: true },
4060
+ { runner: "mocha", ecosystem: "js", layer0Providers: ["c8"], layer0Enabled: true, mutation: "stryker-plugin", perTestDrivable: true },
4061
+ { runner: "node-test", ecosystem: "js", layer0Providers: ["c8"], layer0Enabled: true, mutation: "whole-suite-command", perTestDrivable: true },
4062
+ { runner: "ava", ecosystem: "js", layer0Providers: ["c8"], layer0Enabled: true, mutation: "whole-suite-command", perTestDrivable: true },
4063
+ { runner: "tap", ecosystem: "js", layer0Providers: ["c8"], layer0Enabled: true, mutation: "stryker-plugin", perTestDrivable: true },
4064
+ { runner: "jasmine", ecosystem: "js", layer0Providers: ["c8"], layer0Enabled: true, mutation: "stryker-plugin", perTestDrivable: true },
4065
+ // Layer 0 disabled: bun 1.3.14 reported an unexecuted line as covered and emits no per-function
4066
+ // records to correct with. "bun-lcov" stays recognized so historical artifacts validate.
4067
+ { runner: "bun", ecosystem: "js", layer0Providers: ["bun-lcov"], layer0Enabled: false, mutation: "whole-suite-command", perTestDrivable: true },
4068
+ { runner: "pytest", ecosystem: "python", layer0Providers: ["coverage.py"], layer0Enabled: true, mutation: "cosmic-ray", perTestDrivable: false }
4069
+ ];
4070
+ var BY_RUNNER = new Map(RUNNER_CAPABILITIES.map((c) => [c.runner, c]));
4071
+ function capability(runner) {
4072
+ const found = BY_RUNNER.get(runner);
4073
+ if (found === void 0) throw new Error(`unknown runner in capability registry: ${runner}`);
4074
+ return found;
4075
+ }
4076
+ function knownRunner(runner) {
4077
+ return BY_RUNNER.has(runner);
4078
+ }
4079
+ function jsRunners() {
4080
+ return RUNNER_CAPABILITIES.filter((c) => c.ecosystem === "js").map((c) => c.runner);
4081
+ }
4082
+ function layer0EnabledRunners(ecosystem) {
4083
+ return RUNNER_CAPABILITIES.filter((c) => c.layer0Enabled && (ecosystem === void 0 || c.ecosystem === ecosystem)).map((c) => c.runner);
4084
+ }
4085
+ function strykerPluginRunners() {
4086
+ return RUNNER_CAPABILITIES.filter((c) => c.mutation === "stryker-plugin").map((c) => c.runner);
4087
+ }
4088
+ function recognizedProvidersFor(runner) {
4089
+ return new Set(capability(runner).layer0Providers);
4090
+ }
4091
+ var TARGET_ARTIFACT_KEYS = [
4092
+ "repo",
4093
+ "baseSha",
4094
+ "sha",
4095
+ "runner",
4096
+ "directory",
4097
+ "targetSelection"
4098
+ ];
4099
+ var HANDOFF_TARGET_KEYS = ["baseSha", "sha", "runner"];
4100
+ var HANDOFF_EVIDENCE_KEYS_V1 = [
4101
+ "schema",
4102
+ "engine",
4103
+ "target",
4104
+ "scope",
4105
+ "diffCoverage",
4106
+ "rawCoverageDigest",
4107
+ "rawCoverageFormat",
4108
+ "mutationExecution",
4109
+ "mutationScope",
4110
+ "tier",
4111
+ "mutantsPlanned",
4112
+ "mutantsRun",
4113
+ "counts",
4114
+ "scores",
4115
+ "floor",
4116
+ "gate",
4117
+ "baseline",
4118
+ "findingCount",
4119
+ "findings",
4120
+ "policy",
4121
+ "rationalesDigest",
4122
+ "rawReportDigest",
4123
+ "skipBaseline"
4124
+ ];
4125
+ var HANDOFF_EVIDENCE_KEYS_V2 = [
4126
+ ...HANDOFF_EVIDENCE_KEYS_V1,
4127
+ "evidenceProfile",
4128
+ "packages",
4129
+ "mutantRoster",
4130
+ // Reverse-patch verification (opt-in beta): an advisory verdict, its cost, and bounded gap
4131
+ // locations. Advisory on both sides of the wire — no score, floor, gate or certificate
4132
+ // computation reads it.
4133
+ "patchRevert"
4134
+ ];
4135
+ var TARGET_SELECTION_KINDS = [
4136
+ "declared-policy",
4137
+ "declared-cli",
4138
+ "auto-diff",
4139
+ "auto-diff-multi",
4140
+ "repository-root"
4141
+ ];
4142
+ export {
4143
+ CI_OBLIGATION_SIGNAL_AUTHORITIES,
4144
+ CI_PROPERTY_AUTHORITIES,
4145
+ CI_PROPERTY_CAPS,
4146
+ CI_PROPERTY_FAMILIES,
4147
+ CI_PROPERTY_OUTCOMES,
4148
+ CI_PROPERTY_STATES,
4149
+ CI_RESIDUAL_STATES,
4150
+ CLUSTER_STRATEGIES,
4151
+ DEFAULT_CLASSIC_MUTATION,
4152
+ DEFAULT_DIFF_COVERAGE,
4153
+ DEFAULT_ENVIRONMENT,
4154
+ DEFAULT_ENVIRONMENT_IMAGE_DIGEST,
4155
+ DEFAULT_ENVIRONMENT_IMAGE_REF,
4156
+ DEFAULT_FINDINGS,
4157
+ DEFAULT_FLOOR,
4158
+ DEFAULT_MECHANISMS,
4159
+ DEFAULT_MUTATION,
4160
+ DEFAULT_POLICY,
4161
+ DEFAULT_SANDBOX,
4162
+ DIFF_COVERAGE_THRESHOLD,
4163
+ DiffScopeError,
4164
+ EXCLUDE_RE,
4165
+ EXPECTED_ORACLE_CELL,
4166
+ FINDING_SEVERITIES,
4167
+ HANDOFF_EVIDENCE_KEYS_V1,
4168
+ HANDOFF_EVIDENCE_KEYS_V2,
4169
+ HANDOFF_TARGET_KEYS,
4170
+ LAYER0_EXECUTION,
4171
+ LAYER0_SCOPE_DISCLOSURE,
4172
+ LAYER1_EXECUTION,
4173
+ MAX_EXPANDED_LINES,
4174
+ MAX_FINDING_DESCRIPTION_LEN,
4175
+ MAX_LITERAL_ARGUMENTS,
4176
+ MAX_LITERAL_COMMAND_BYTES,
4177
+ MAX_MUTANT_ROSTER,
4178
+ MAX_SCOPE_RANGES,
4179
+ MECHANICAL_ERROR_PATH_REASON,
4180
+ MIN_CELL_REPEATS,
4181
+ MUTANT_STATUSES,
4182
+ MUTATION_SCOPE_SURFACE,
4183
+ PROVIDER_RAW_FORMAT,
4184
+ PYTHON_EXCLUDE_RE,
4185
+ PYTHON_SOURCE_RE,
4186
+ REALISTIC_CATEGORIES,
4187
+ RUNNER_CAPABILITIES,
4188
+ SCENARIO_AUTHORITIES,
4189
+ SCENARIO_LANES,
4190
+ SCENARIO_OUTCOMES,
4191
+ SCENARIO_SEARCH_STATES,
4192
+ SCENARIO_SOURCE_INDEPENDENCE,
4193
+ SCENARIO_SOURCE_KINDS,
4194
+ SOURCE_RE,
4195
+ TARGET_ARTIFACT_KEYS,
4196
+ TARGET_SELECTION_KINDS,
4197
+ TRIAGE_REASON_CODES,
4198
+ analyzeTestFindings,
4199
+ applyLayer1Gate,
4200
+ assertNever,
4201
+ buildCiPropertySearchBlock,
4202
+ buildCiPropertyTargetUniverse,
4203
+ buildCustomerProcessEnvironment,
4204
+ buildScenarioSearchBlock,
4205
+ canStartStageB,
4206
+ capability,
4207
+ changedStructuralCoverage,
4208
+ ciPropertyBehaviourKey,
4209
+ ciPropertyLedgerDigest,
4210
+ ciPropertyMutatorFamilies,
4211
+ classicHeadroom,
4212
+ classicMutationExecution,
4213
+ classicMutationTier,
4214
+ classifyDiffCoverage,
4215
+ combineLayerGates,
4216
+ computeScope,
4217
+ computeScores,
4218
+ conditionalPower,
4219
+ containsEngineCredential,
4220
+ countsFromLines,
4221
+ coveredLinesToScope,
4222
+ customerEnvironmentNames,
4223
+ denominatorOf,
4224
+ deriveScenarioSearchCounts,
4225
+ deriveWorstOfGate,
4226
+ describeFinding,
4227
+ describeMutationEvidence,
4228
+ describeReasonCode,
4229
+ diffCoverageExecution,
4230
+ emptyCounts,
4231
+ errorCountOf,
4232
+ evaluateAssay,
4233
+ evaluateDiffCoverageGate,
4234
+ evaluateFloor,
4235
+ evaluateGate,
4236
+ expandScopeLines,
4237
+ findingKind,
4238
+ findingSeverity,
4239
+ gapKeys,
4240
+ groupFindingsByLine,
4241
+ isCanonicalBase64,
4242
+ isCiPropertyCandidateCap,
4243
+ isClusterStrategy,
4244
+ isConfirmedEquivalent,
4245
+ isNonExecutableLine,
4246
+ isReservedCustomerVariable,
4247
+ isScenarioAuthority,
4248
+ isScenarioLane,
4249
+ isScenarioOutcome,
4250
+ isScenarioSearchState,
4251
+ jsRunners,
4252
+ knownRunner,
4253
+ layer0EnabledRunners,
4254
+ loadPolicy,
4255
+ markNonExecutable,
4256
+ mechanicalErrorPathDisposition,
4257
+ missingScopedFiles,
4258
+ mutantIdentity,
4259
+ operatorClass,
4260
+ operatorClassBreakdown,
4261
+ operatorClassSummary,
4262
+ parseLiteralArguments,
4263
+ parsePreparedTestCommand,
4264
+ parseUnifiedDiff,
4265
+ qualifyCorpus,
4266
+ qualifyHistoricalPair,
4267
+ rankCiPropertyRules,
4268
+ recognizedProvidersFor,
4269
+ redactTriageForEgress,
4270
+ researchTier,
4271
+ sanitizeCiPropertySummary,
4272
+ sanitizeFindingDescription,
4273
+ sanitizeScenarioSummary,
4274
+ selectCiPropertyTargets,
4275
+ stableOutcome,
4276
+ strykerPluginRunners,
4277
+ survivingMutantObligationSignals,
4278
+ tallyCounts,
4279
+ testIdentityDigest,
4280
+ toGapFindings,
4281
+ uncoveredCount,
4282
+ validateCiResidualGapLedger,
4283
+ validateDiffCoverageLines,
4284
+ validateManifest,
4285
+ validatePolicy,
4286
+ validatePreparedTestCommand,
4287
+ wilsonInterval
4288
+ };