@bilalimamoglu/sift 0.3.1 → 0.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/core/exec.ts
2
2
  import { spawn } from "child_process";
3
3
  import { constants as osConstants } from "os";
4
- import pc2 from "picocolors";
4
+ import pc3 from "picocolors";
5
5
 
6
6
  // src/constants.ts
7
7
  import os from "os";
@@ -61,7 +61,7 @@ function evaluateGate(args) {
61
61
 
62
62
  // src/core/testStatusDecision.ts
63
63
  import { z } from "zod";
64
- var TEST_STATUS_DIAGNOSE_JSON_CONTRACT = '{"status":"ok|insufficient","diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","dominant_blocker_bucket_index":number|null,"provider_used":boolean,"provider_confidence":number|null,"provider_failed":boolean,"raw_slice_used":boolean,"raw_slice_strategy":"none|bucket_evidence|traceback_window|head_tail","resolved_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_subset_available":boolean,"main_buckets":[{"bucket_index":number,"label":string,"count":number,"root_cause":string,"evidence":string[],"bucket_confidence":number,"root_cause_confidence":number,"dominant":boolean,"secondary_visible_despite_blocker":boolean,"mini_diff":{"added_paths"?:number,"removed_models"?:number,"changed_task_mappings"?:number}|null}],"read_targets":[{"file":string,"line":number|null,"why":string,"bucket_index":number,"context_hint":{"start_line":number|null,"end_line":number|null,"search_hint":string|null}}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string},"resolved_tests"?:string[],"remaining_tests"?:string[]}';
64
+ var TEST_STATUS_DIAGNOSE_JSON_CONTRACT = '{"status":"ok|insufficient","diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","primary_suspect_kind":"test|app_code|config|environment|tooling|unknown","confidence_reason":string,"dominant_blocker_bucket_index":number|null,"provider_used":boolean,"provider_confidence":number|null,"provider_failed":boolean,"raw_slice_used":boolean,"raw_slice_strategy":"none|bucket_evidence|traceback_window|head_tail","resolved_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_subset_available":boolean,"main_buckets":[{"bucket_index":number,"label":string,"count":number,"root_cause":string,"suspect_kind":"test|app_code|config|environment|tooling|unknown","fix_hint":string,"evidence":string[],"bucket_confidence":number,"root_cause_confidence":number,"dominant":boolean,"secondary_visible_despite_blocker":boolean,"mini_diff":{"added_paths"?:number,"removed_models"?:number,"changed_task_mappings"?:number}|null}],"read_targets":[{"file":string,"line":number|null,"why":string,"bucket_index":number,"context_hint":{"start_line":number|null,"end_line":number|null,"search_hint":string|null}}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string},"resolved_tests"?:string[],"remaining_tests"?:string[]}';
65
65
  var TEST_STATUS_PROVIDER_SUPPLEMENT_JSON_CONTRACT = '{"diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","provider_confidence":number|null,"bucket_supplements":[{"label":string,"count":number,"root_cause":string,"anchor":{"file":string|null,"line":number|null,"search_hint":string|null},"fix_hint":string|null,"confidence":number}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string}}';
66
66
  var nextBestActionSchema = z.object({
67
67
  code: z.enum([
@@ -103,6 +103,15 @@ var testStatusDiagnoseContractSchema = z.object({
103
103
  additional_source_read_likely_low_value: z.boolean(),
104
104
  read_raw_only_if: z.string().nullable(),
105
105
  decision: z.enum(["stop", "zoom", "read_source", "read_raw"]),
106
+ primary_suspect_kind: z.enum([
107
+ "test",
108
+ "app_code",
109
+ "config",
110
+ "environment",
111
+ "tooling",
112
+ "unknown"
113
+ ]),
114
+ confidence_reason: z.string().min(1),
106
115
  dominant_blocker_bucket_index: z.number().int().nullable(),
107
116
  provider_used: z.boolean(),
108
117
  provider_confidence: z.number().min(0).max(1).nullable(),
@@ -117,6 +126,15 @@ var testStatusDiagnoseContractSchema = z.object({
117
126
  label: z.string(),
118
127
  count: z.number().int(),
119
128
  root_cause: z.string(),
129
+ suspect_kind: z.enum([
130
+ "test",
131
+ "app_code",
132
+ "config",
133
+ "environment",
134
+ "tooling",
135
+ "unknown"
136
+ ]),
137
+ fix_hint: z.string().min(1),
120
138
  evidence: z.array(z.string()).max(2),
121
139
  bucket_confidence: z.number(),
122
140
  root_cause_confidence: z.number(),
@@ -166,6 +184,255 @@ var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.om
166
184
  function parseTestStatusProviderSupplement(input) {
167
185
  return testStatusProviderSupplementSchema.parse(JSON.parse(input));
168
186
  }
187
+ var extendedBucketSpecs = [
188
+ {
189
+ prefix: "service unavailable:",
190
+ type: "service_unavailable",
191
+ label: "service unavailable",
192
+ genericTitle: "Service unavailable failures",
193
+ defaultCoverage: "error",
194
+ rootCauseConfidence: 0.9,
195
+ dominantPriority: 2,
196
+ dominantBlocker: true,
197
+ why: "it contains the dependency service or API path that is unavailable in the test environment",
198
+ fix: "Restore the dependency service or test double before rerunning the full suite."
199
+ },
200
+ {
201
+ prefix: "db refused:",
202
+ type: "db_connection_failure",
203
+ label: "database connection",
204
+ genericTitle: "Database connection failures",
205
+ defaultCoverage: "error",
206
+ rootCauseConfidence: 0.9,
207
+ dominantPriority: 2,
208
+ dominantBlocker: true,
209
+ why: "it contains the database host, DSN, or startup path that is refusing connections",
210
+ fix: "Restore the test database connectivity before rerunning the full suite."
211
+ },
212
+ {
213
+ prefix: "auth bypass absent:",
214
+ type: "auth_bypass_absent",
215
+ label: "auth bypass missing",
216
+ genericTitle: "Auth bypass setup failures",
217
+ defaultCoverage: "error",
218
+ rootCauseConfidence: 0.86,
219
+ dominantPriority: 2,
220
+ dominantBlocker: true,
221
+ why: "it contains the auth bypass fixture or setup path that tests expected to be active",
222
+ fix: "Restore the test auth bypass fixture or mock before rerunning the full suite."
223
+ },
224
+ {
225
+ prefix: "snapshot mismatch:",
226
+ type: "snapshot_mismatch",
227
+ label: "snapshot mismatch",
228
+ genericTitle: "Snapshot mismatches",
229
+ defaultCoverage: "failed",
230
+ rootCauseConfidence: 0.84,
231
+ why: "it contains the failing snapshot expectation behind this bucket",
232
+ fix: "Update the snapshots if these output changes are intentional, then rerun the suite."
233
+ },
234
+ {
235
+ prefix: "timeout:",
236
+ type: "timeout_failure",
237
+ label: "timeout",
238
+ genericTitle: "Timeout failures",
239
+ defaultCoverage: "mixed",
240
+ rootCauseConfidence: 0.9,
241
+ why: "it contains the test or fixture that exceeded the timeout threshold",
242
+ fix: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning."
243
+ },
244
+ {
245
+ prefix: "permission:",
246
+ type: "permission_denied_failure",
247
+ label: "permission denied",
248
+ genericTitle: "Permission failures",
249
+ defaultCoverage: "error",
250
+ rootCauseConfidence: 0.85,
251
+ why: "it contains the file, socket, or port access that was denied",
252
+ fix: "Check file or port permissions in the CI environment before rerunning."
253
+ },
254
+ {
255
+ prefix: "async loop:",
256
+ type: "async_event_loop_failure",
257
+ label: "async event loop",
258
+ genericTitle: "Async event loop failures",
259
+ defaultCoverage: "mixed",
260
+ rootCauseConfidence: 0.88,
261
+ why: "it contains the async setup or coroutine that caused the event loop error",
262
+ fix: "Check event loop scope and pytest-asyncio configuration before rerunning."
263
+ },
264
+ {
265
+ prefix: "fixture teardown:",
266
+ type: "fixture_teardown_failure",
267
+ label: "fixture teardown",
268
+ genericTitle: "Fixture teardown failures",
269
+ defaultCoverage: "error",
270
+ rootCauseConfidence: 0.85,
271
+ why: "it contains the fixture teardown path that failed after the test body completed",
272
+ fix: "Inspect the teardown cleanup path and restore idempotent fixture cleanup before rerunning."
273
+ },
274
+ {
275
+ prefix: "db migration:",
276
+ type: "db_migration_failure",
277
+ label: "db migration",
278
+ genericTitle: "DB migration failures",
279
+ defaultCoverage: "error",
280
+ rootCauseConfidence: 0.9,
281
+ why: "it contains the migration or model definition behind the missing table or relation",
282
+ fix: "Run pending migrations or fix the expected model schema before rerunning."
283
+ },
284
+ {
285
+ prefix: "configuration:",
286
+ type: "configuration_error",
287
+ label: "configuration error",
288
+ genericTitle: "Configuration errors",
289
+ defaultCoverage: "error",
290
+ rootCauseConfidence: 0.95,
291
+ dominantPriority: 4,
292
+ dominantBlocker: true,
293
+ why: "it contains the pytest configuration or conftest setup error that blocks the run",
294
+ fix: "Fix the pytest configuration, CLI usage, or conftest import error before rerunning."
295
+ },
296
+ {
297
+ prefix: "xdist worker crash:",
298
+ type: "xdist_worker_crash",
299
+ label: "xdist worker crash",
300
+ genericTitle: "xdist worker crashes",
301
+ defaultCoverage: "error",
302
+ rootCauseConfidence: 0.92,
303
+ dominantPriority: 3,
304
+ why: "it contains the worker startup or shared-state path that crashed an xdist worker",
305
+ fix: "Check shared state, worker startup hooks, or resource contention between workers before rerunning."
306
+ },
307
+ {
308
+ prefix: "type error:",
309
+ type: "type_error_failure",
310
+ label: "type error",
311
+ genericTitle: "Type errors",
312
+ defaultCoverage: "mixed",
313
+ rootCauseConfidence: 0.8,
314
+ why: "it contains the call site or fixture value that triggered the type error",
315
+ fix: "Inspect the mismatched argument or object shape and rerun the full suite at standard."
316
+ },
317
+ {
318
+ prefix: "resource leak:",
319
+ type: "resource_leak_warning",
320
+ label: "resource leak",
321
+ genericTitle: "Resource leak warnings",
322
+ defaultCoverage: "mixed",
323
+ rootCauseConfidence: 0.74,
324
+ why: "it contains the warning source behind the leaked file, socket, or coroutine",
325
+ fix: "Close the leaked resource or suppress the warning only if the cleanup is intentional."
326
+ },
327
+ {
328
+ prefix: "django db access:",
329
+ type: "django_db_access_denied",
330
+ label: "django db access",
331
+ genericTitle: "Django DB access failures",
332
+ defaultCoverage: "error",
333
+ rootCauseConfidence: 0.95,
334
+ why: "it needs the @pytest.mark.django_db decorator or fixture permission to access the database",
335
+ fix: "Add @pytest.mark.django_db to the test or class before rerunning."
336
+ },
337
+ {
338
+ prefix: "network:",
339
+ type: "network_failure",
340
+ label: "network failure",
341
+ genericTitle: "Network failures",
342
+ defaultCoverage: "error",
343
+ rootCauseConfidence: 0.88,
344
+ dominantPriority: 2,
345
+ why: "it contains the host, URL, or TLS path behind the network failure",
346
+ fix: "Check DNS, outbound network access, retries, or TLS trust before rerunning."
347
+ },
348
+ {
349
+ prefix: "segfault:",
350
+ type: "subprocess_crash_segfault",
351
+ label: "segfault",
352
+ genericTitle: "Segfault crashes",
353
+ defaultCoverage: "mixed",
354
+ rootCauseConfidence: 0.8,
355
+ why: "it contains the subprocess or native extension path that crashed with SIGSEGV",
356
+ fix: "Inspect the native extension, subprocess boundary, or incompatible binary before rerunning."
357
+ },
358
+ {
359
+ prefix: "flaky:",
360
+ type: "flaky_test_detected",
361
+ label: "flaky test",
362
+ genericTitle: "Flaky test detections",
363
+ defaultCoverage: "mixed",
364
+ rootCauseConfidence: 0.72,
365
+ why: "it contains the rerun-prone test that behaved inconsistently across attempts",
366
+ fix: "Stabilize the nondeterministic test or fixture before relying on reruns."
367
+ },
368
+ {
369
+ prefix: "serialization:",
370
+ type: "serialization_encoding_failure",
371
+ label: "serialization or encoding",
372
+ genericTitle: "Serialization or encoding failures",
373
+ defaultCoverage: "mixed",
374
+ rootCauseConfidence: 0.78,
375
+ why: "it contains the serialization or decoding path behind the malformed payload",
376
+ fix: "Inspect the encoded payload, serializer, or fixture data before rerunning."
377
+ },
378
+ {
379
+ prefix: "file not found:",
380
+ type: "file_not_found_failure",
381
+ label: "file not found",
382
+ genericTitle: "Missing file failures",
383
+ defaultCoverage: "mixed",
384
+ rootCauseConfidence: 0.82,
385
+ why: "it contains the missing file path or fixture artifact required by the test",
386
+ fix: "Restore the missing file, fixture artifact, or working-directory assumption before rerunning."
387
+ },
388
+ {
389
+ prefix: "memory:",
390
+ type: "memory_error",
391
+ label: "memory error",
392
+ genericTitle: "Memory failures",
393
+ defaultCoverage: "mixed",
394
+ rootCauseConfidence: 0.78,
395
+ why: "it contains the allocation path that exhausted available memory",
396
+ fix: "Reduce memory pressure or investigate the large allocation before rerunning."
397
+ },
398
+ {
399
+ prefix: "deprecation as error:",
400
+ type: "deprecation_warning_as_error",
401
+ label: "deprecation as error",
402
+ genericTitle: "Deprecation warnings as errors",
403
+ defaultCoverage: "mixed",
404
+ rootCauseConfidence: 0.74,
405
+ why: "it contains the deprecated API or warning filter that is failing the test run",
406
+ fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
407
+ },
408
+ {
409
+ prefix: "assertion failed:",
410
+ type: "assertion_failure",
411
+ label: "assertion failure",
412
+ genericTitle: "Assertion failures",
413
+ defaultCoverage: "failed",
414
+ rootCauseConfidence: 0.76,
415
+ why: "it contains the expected-versus-actual assertion that failed inside the visible test",
416
+ fix: "Read the assertion diff or expectation and fix the code or expected value before rerunning."
417
+ },
418
+ {
419
+ prefix: "xfail strict:",
420
+ type: "xfail_strict_unexpected_pass",
421
+ label: "strict xfail unexpected pass",
422
+ genericTitle: "Strict xfail unexpected passes",
423
+ defaultCoverage: "failed",
424
+ rootCauseConfidence: 0.78,
425
+ why: "it contains the strict xfail case that unexpectedly passed",
426
+ fix: "Remove or update the strict xfail expectation if the test is now passing intentionally."
427
+ }
428
+ ];
429
+ function findExtendedBucketSpec(reason) {
430
+ return extendedBucketSpecs.find((spec) => reason.startsWith(spec.prefix)) ?? null;
431
+ }
432
+ function extractReasonDetail(reason, prefix) {
433
+ const detail = reason.slice(prefix.length).trim();
434
+ return detail.length > 0 ? detail : null;
435
+ }
169
436
  function formatCount(count, singular, plural = `${singular}s`) {
170
437
  return `${count} ${count === 1 ? singular : plural}`;
171
438
  }
@@ -219,6 +486,10 @@ function formatTargetSummary(summary) {
219
486
  return `count=${summary.count}; families=${families}`;
220
487
  }
221
488
  function classifyGenericBucketType(reason) {
489
+ const extended = findExtendedBucketSpec(reason);
490
+ if (extended) {
491
+ return extended.type;
492
+ }
222
493
  if (reason.startsWith("missing test env:")) {
223
494
  return "shared_environment_blocker";
224
495
  }
@@ -263,6 +534,10 @@ function classifyVisibleStatusForLabel(args) {
263
534
  return "unknown";
264
535
  }
265
536
  function inferCoverageFromReason(reason) {
537
+ const extended = findExtendedBucketSpec(reason);
538
+ if (extended) {
539
+ return extended.defaultCoverage;
540
+ }
266
541
  if (reason.startsWith("missing test env:") || reason.startsWith("fixture guard:") || reason.startsWith("service unavailable:") || reason.startsWith("db refused:") || reason.startsWith("auth bypass absent:") || reason.startsWith("missing module:")) {
267
542
  return "error";
268
543
  }
@@ -323,7 +598,13 @@ function buildGenericBuckets(analysis) {
323
598
  summaryLines: [],
324
599
  reason,
325
600
  count: 1,
326
- confidence: reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62,
601
+ confidence: (() => {
602
+ const extended = findExtendedBucketSpec(reason);
603
+ if (extended) {
604
+ return Math.max(0.72, Math.min(extended.rootCauseConfidence, 0.82));
605
+ }
606
+ return reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62;
607
+ })(),
327
608
  representativeItems: [item],
328
609
  entities: [],
329
610
  hint: void 0,
@@ -340,7 +621,7 @@ function buildGenericBuckets(analysis) {
340
621
  push(item.reason, item);
341
622
  }
342
623
  for (const bucket of grouped.values()) {
343
- const title = bucket.type === "assertion_failure" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures";
624
+ const title = findExtendedBucketSpec(bucket.reason)?.genericTitle ?? (bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures");
344
625
  bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
345
626
  bucket.summaryLines = [bucket.headline];
346
627
  bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
@@ -413,13 +694,13 @@ function inferFailureBucketCoverage(bucket, analysis) {
413
694
  }
414
695
  }
415
696
  const claimed = bucket.countClaimed ?? bucket.countVisible;
416
- if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure") {
697
+ if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch") {
417
698
  return {
418
699
  error,
419
700
  failed: Math.max(failed, claimed)
420
701
  };
421
702
  }
422
- if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
703
+ if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "permission_denied_failure" || bucket.type === "fixture_teardown_failure" || bucket.type === "db_migration_failure" || bucket.type === "configuration_error" || bucket.type === "xdist_worker_crash" || bucket.type === "django_db_access_denied" || bucket.type === "network_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
423
704
  return {
424
705
  error: Math.max(error, claimed),
425
706
  failed
@@ -494,6 +775,10 @@ function dominantBucketPriority(bucket) {
494
775
  if (bucket.reason.startsWith("missing test env:")) {
495
776
  return 5;
496
777
  }
778
+ const extended = findExtendedBucketSpec(bucket.reason);
779
+ if (extended?.dominantPriority !== void 0) {
780
+ return extended.dominantPriority;
781
+ }
497
782
  if (bucket.type === "shared_environment_blocker") {
498
783
  return 4;
499
784
  }
@@ -527,12 +812,16 @@ function prioritizeBuckets(buckets) {
527
812
  });
528
813
  }
529
814
  function isDominantBlockerType(type) {
530
- return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
815
+ return type === "shared_environment_blocker" || type === "configuration_error" || type === "import_dependency_failure" || type === "collection_failure";
531
816
  }
532
817
  function labelForBucket(bucket) {
533
818
  if (bucket.labelOverride) {
534
819
  return bucket.labelOverride;
535
820
  }
821
+ const extended = findExtendedBucketSpec(bucket.reason);
822
+ if (extended) {
823
+ return extended.label;
824
+ }
536
825
  if (bucket.reason.startsWith("missing test env:")) {
537
826
  return "missing test env";
538
827
  }
@@ -566,6 +855,9 @@ function labelForBucket(bucket) {
566
855
  if (bucket.type === "assertion_failure") {
567
856
  return "assertion failure";
568
857
  }
858
+ if (bucket.type === "snapshot_mismatch") {
859
+ return "snapshot mismatch";
860
+ }
569
861
  if (bucket.type === "collection_failure") {
570
862
  return "collection failure";
571
863
  }
@@ -584,6 +876,10 @@ function rootCauseConfidenceFor(bucket) {
584
876
  if (isUnknownBucket(bucket)) {
585
877
  return 0.52;
586
878
  }
879
+ const extended = findExtendedBucketSpec(bucket.reason);
880
+ if (extended) {
881
+ return extended.rootCauseConfidence;
882
+ }
587
883
  if (bucket.reason.startsWith("missing test env:") || bucket.reason.startsWith("missing module:") || bucket.reason.startsWith("db refused:") || bucket.reason.startsWith("service unavailable:") || bucket.reason.startsWith("auth bypass absent:")) {
588
884
  return 0.95;
589
885
  }
@@ -624,6 +920,10 @@ function buildReadTargetWhy(args) {
624
920
  if (envVar) {
625
921
  return `it contains the ${envVar} setup guard`;
626
922
  }
923
+ const extended = findExtendedBucketSpec(args.bucket.reason);
924
+ if (extended) {
925
+ return extended.why;
926
+ }
627
927
  if (args.bucket.reason.startsWith("fixture guard:")) {
628
928
  return "it contains the fixture/setup guard behind this bucket";
629
929
  }
@@ -654,6 +954,9 @@ function buildReadTargetWhy(args) {
654
954
  }
655
955
  return "it maps to the visible stale snapshot expectation";
656
956
  }
957
+ if (args.bucket.type === "snapshot_mismatch") {
958
+ return "it maps to the visible snapshot mismatch bucket";
959
+ }
657
960
  if (args.bucket.type === "import_dependency_failure") {
658
961
  return "it is the first visible failing module in this missing dependency bucket";
659
962
  }
@@ -665,11 +968,54 @@ function buildReadTargetWhy(args) {
665
968
  }
666
969
  return `it maps to the visible ${args.bucketLabel} bucket`;
667
970
  }
971
+ function buildExtendedBucketSearchHint(bucket, anchor) {
972
+ const extended = findExtendedBucketSpec(bucket.reason);
973
+ if (!extended) {
974
+ return null;
975
+ }
976
+ const detail = extractReasonDetail(bucket.reason, extended.prefix);
977
+ if (!detail) {
978
+ return anchor.label.split("::")[1]?.trim() ?? anchor.label ?? null;
979
+ }
980
+ if (extended.type === "timeout_failure") {
981
+ const duration = detail.match(/>\s*([0-9]+(?:\.[0-9]+)?s?)/i)?.[1];
982
+ return duration ?? anchor.label.split("::")[1]?.trim() ?? detail;
983
+ }
984
+ if (extended.type === "db_migration_failure") {
985
+ const relation = detail.match(/\b(?:relation|table)\s+([A-Za-z0-9_.-]+)/i)?.[1];
986
+ return relation ?? detail;
987
+ }
988
+ if (extended.type === "network_failure") {
989
+ const url = detail.match(/\bhttps?:\/\/[^\s)'"`]+/i)?.[0];
990
+ const host = detail.match(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/)?.[0];
991
+ return url ?? host ?? detail;
992
+ }
993
+ if (extended.type === "xdist_worker_crash") {
994
+ return detail.match(/\bgw\d+\b/)?.[0] ?? detail;
995
+ }
996
+ if (extended.type === "fixture_teardown_failure") {
997
+ return detail.replace(/^of\s+/i, "") || anchor.label;
998
+ }
999
+ if (extended.type === "file_not_found_failure") {
1000
+ const path4 = detail.match(/['"]([^'"]+)['"]/)?.[1];
1001
+ return path4 ?? detail;
1002
+ }
1003
+ if (extended.type === "permission_denied_failure") {
1004
+ const path4 = detail.match(/['"]([^'"]+)['"]/)?.[1];
1005
+ const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
1006
+ return path4 ?? (port ? `port ${port}` : detail);
1007
+ }
1008
+ return detail;
1009
+ }
668
1010
  function buildReadTargetSearchHint(bucket, anchor) {
669
1011
  const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
670
1012
  if (envVar) {
671
1013
  return envVar;
672
1014
  }
1015
+ const extendedHint = buildExtendedBucketSearchHint(bucket, anchor);
1016
+ if (extendedHint) {
1017
+ return extendedHint;
1018
+ }
673
1019
  if (bucket.type === "contract_snapshot_drift") {
674
1020
  return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
675
1021
  }
@@ -784,6 +1130,10 @@ function extractMiniDiff(input, bucket) {
784
1130
  };
785
1131
  }
786
1132
  function inferSupplementCoverageKind(args) {
1133
+ const extended = findExtendedBucketSpec(args.rootCause);
1134
+ if (extended?.defaultCoverage === "error" || extended?.defaultCoverage === "failed") {
1135
+ return extended.defaultCoverage;
1136
+ }
787
1137
  const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
788
1138
  if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
789
1139
  normalized
@@ -854,7 +1204,7 @@ function buildProviderSupplementBuckets(args) {
854
1204
  });
855
1205
  }
856
1206
  function pickUnknownAnchor(args) {
857
- const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : null;
1207
+ const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : args.analysis.visibleFailedItems[0];
858
1208
  if (fromStatusItems) {
859
1209
  return {
860
1210
  label: fromStatusItems.label,
@@ -891,12 +1241,14 @@ function buildUnknownBucket(args) {
891
1241
  const isError = args.kind === "error";
892
1242
  const label = isError ? "unknown setup blocker" : "unknown failure family";
893
1243
  const reason = isError ? "unknown setup blocker: setup failures share a repeated but unclassified pattern" : "unknown failure family: failing tests share a repeated but unclassified pattern";
1244
+ const firstConcreteSignal = anchor && anchor.reason !== reason && anchor.reason !== "setup failures share a repeated but unclassified pattern" && anchor.reason !== "failing tests share a repeated but unclassified pattern" ? `First concrete signal: ${anchor.reason}` : null;
894
1245
  return {
895
1246
  type: "unknown_failure",
896
1247
  headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
897
1248
  summaryLines: [
898
- `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`
899
- ],
1249
+ `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
1250
+ firstConcreteSignal
1251
+ ].filter((value) => Boolean(value)),
900
1252
  reason,
901
1253
  count: args.count,
902
1254
  confidence: 0.45,
@@ -1023,10 +1375,14 @@ function buildStandardAnchorText(target) {
1023
1375
  }
1024
1376
  return formatReadTargetLocation(target);
1025
1377
  }
1026
- function buildStandardFixText(args) {
1378
+ function resolveBucketFixHint(args) {
1027
1379
  if (args.bucket.hint) {
1028
1380
  return args.bucket.hint;
1029
1381
  }
1382
+ const extended = findExtendedBucketSpec(args.bucket.reason);
1383
+ if (extended) {
1384
+ return extended.fix;
1385
+ }
1030
1386
  const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
1031
1387
  if (envVar) {
1032
1388
  return `Set ${envVar} before rerunning the affected tests.`;
@@ -1056,6 +1412,9 @@ function buildStandardFixText(args) {
1056
1412
  if (args.bucket.type === "contract_snapshot_drift") {
1057
1413
  return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
1058
1414
  }
1415
+ if (args.bucket.type === "snapshot_mismatch") {
1416
+ return "Update the snapshots if these output changes are intentional, then rerun the full suite at standard.";
1417
+ }
1059
1418
  if (args.bucket.type === "assertion_failure") {
1060
1419
  return "Inspect the failing assertion and rerun the full suite at standard.";
1061
1420
  }
@@ -1065,13 +1424,75 @@ function buildStandardFixText(args) {
1065
1424
  if (args.bucket.type === "runtime_failure") {
1066
1425
  return `Fix the visible ${args.bucketLabel} and rerun the full suite at standard.`;
1067
1426
  }
1068
- return null;
1427
+ return "Inspect the first visible anchor for this bucket, apply the smallest fix that explains it, then rerun the full suite at standard.";
1428
+ }
1429
+ function deriveBucketSuspectKind(args) {
1430
+ if (args.bucket.type === "shared_environment_blocker" || args.bucket.type === "fixture_guard_failure" || args.bucket.type === "permission_denied_failure" || args.bucket.type === "django_db_access_denied" || args.bucket.type === "network_failure" || args.bucket.type === "service_unavailable" || args.bucket.type === "db_connection_failure" || args.bucket.type === "auth_bypass_absent" || args.bucket.type === "fixture_teardown_failure") {
1431
+ return "environment";
1432
+ }
1433
+ if (args.bucket.type === "configuration_error" || args.bucket.type === "db_migration_failure" || args.bucket.type === "import_dependency_failure" || args.bucket.type === "collection_failure" || args.bucket.type === "no_tests_collected" || args.bucket.type === "deprecation_warning_as_error" || args.bucket.type === "file_not_found_failure") {
1434
+ return "config";
1435
+ }
1436
+ if (args.bucket.type === "contract_snapshot_drift" || args.bucket.type === "snapshot_mismatch" || args.bucket.type === "flaky_test_detected" || args.bucket.type === "xfail_strict_unexpected_pass") {
1437
+ return "test";
1438
+ }
1439
+ if (args.bucket.type === "xdist_worker_crash" || args.bucket.type === "timeout_failure" || args.bucket.type === "async_event_loop_failure" || args.bucket.type === "subprocess_crash_segfault" || args.bucket.type === "memory_error" || args.bucket.type === "resource_leak_warning" || args.bucket.type === "interrupted_run") {
1440
+ return "tooling";
1441
+ }
1442
+ if (args.bucket.type === "unknown_failure") {
1443
+ return "unknown";
1444
+ }
1445
+ if (args.bucket.type === "assertion_failure" || args.bucket.type === "runtime_failure" || args.bucket.type === "type_error_failure" || args.bucket.type === "serialization_encoding_failure") {
1446
+ const file = args.readTarget?.file ?? "";
1447
+ if (file.startsWith("src/")) {
1448
+ return "app_code";
1449
+ }
1450
+ if (file.startsWith("test/") || file.startsWith("tests/")) {
1451
+ return "test";
1452
+ }
1453
+ return "unknown";
1454
+ }
1455
+ return "unknown";
1456
+ }
1457
+ function derivePrimarySuspectKind(args) {
1458
+ const primaryBucket = (args.dominantBlockerBucketIndex !== null ? args.mainBuckets.find((bucket) => bucket.bucket_index === args.dominantBlockerBucketIndex) : null) ?? args.mainBuckets[0];
1459
+ return primaryBucket?.suspect_kind ?? "unknown";
1460
+ }
1461
+ function buildConfidenceReason(args) {
1462
+ const primaryBucket = args.mainBuckets.find((bucket) => bucket.dominant) ?? args.mainBuckets[0];
1463
+ if (args.decision === "stop" && primaryBucket && args.primarySuspectKind !== "unknown") {
1464
+ return `Dominant blocker (${primaryBucket.label}) is anchored and actionable.`;
1465
+ }
1466
+ if (args.decision === "zoom") {
1467
+ return "Unknown or low-confidence buckets remain; one deeper sift pass is justified.";
1468
+ }
1469
+ if (args.decision === "read_source") {
1470
+ return "The bucket is identified, but source context is still needed to make the next fix clear.";
1471
+ }
1472
+ return "Heuristic signal is still insufficient; exact traceback lines are needed.";
1473
+ }
1474
+ function formatSuspectKindLabel(kind) {
1475
+ switch (kind) {
1476
+ case "test":
1477
+ return "test code";
1478
+ case "app_code":
1479
+ return "application code";
1480
+ case "config":
1481
+ return "test or project configuration";
1482
+ case "environment":
1483
+ return "environment setup";
1484
+ case "tooling":
1485
+ return "test runner or tooling";
1486
+ default:
1487
+ return "unknown";
1488
+ }
1069
1489
  }
1070
1490
  function buildStandardBucketSupport(args) {
1071
1491
  return {
1072
1492
  headline: args.bucket.summaryLines[0] ? `- ${args.bucket.summaryLines[0]}` : renderBucketHeadline(args.contractBucket),
1493
+ firstConcreteSignalText: args.bucket.source === "unknown" ? args.bucket.summaryLines[1] ?? null : null,
1073
1494
  anchorText: buildStandardAnchorText(args.readTarget),
1074
- fixText: buildStandardFixText({
1495
+ fixText: resolveBucketFixHint({
1075
1496
  bucket: args.bucket,
1076
1497
  bucketLabel: args.contractBucket.label
1077
1498
  })
@@ -1094,6 +1515,9 @@ function renderStandard(args) {
1094
1515
  )
1095
1516
  });
1096
1517
  lines.push(support.headline);
1518
+ if (support.firstConcreteSignalText) {
1519
+ lines.push(`- ${support.firstConcreteSignalText}`);
1520
+ }
1097
1521
  if (support.anchorText) {
1098
1522
  lines.push(`- Anchor: ${support.anchorText}`);
1099
1523
  }
@@ -1103,6 +1527,7 @@ function renderStandard(args) {
1103
1527
  }
1104
1528
  }
1105
1529
  lines.push(buildDecisionLine(args.contract));
1530
+ lines.push(`- Likely owner: ${formatSuspectKindLabel(args.contract.primary_suspect_kind)}`);
1106
1531
  lines.push(`- Next: ${args.contract.next_best_action.note}`);
1107
1532
  lines.push(buildStopSignal(args.contract));
1108
1533
  return lines.join("\n");
@@ -1190,29 +1615,49 @@ function buildTestStatusDiagnoseContract(args) {
1190
1615
  })[0] ?? null;
1191
1616
  const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
1192
1617
  const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
1193
- const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && hasConcreteCoverage && !hasUnknownBucket && (dominantBucket?.bucket.confidence ?? 0) >= 0.6;
1194
- const rawNeeded = buckets.length === 0 ? !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure) : !diagnosisComplete && !hasUnknownBucket && buckets.every((bucket) => bucket.confidence < 0.7);
1195
1618
  const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
1196
1619
  const readTargets = buildReadTargets({
1197
1620
  buckets,
1198
1621
  dominantBucketIndex: dominantBlockerBucketIndex
1199
1622
  });
1200
- const mainBuckets = buckets.map((bucket, index) => ({
1201
- bucket_index: index + 1,
1202
- label: labelForBucket(bucket),
1203
- count: bucket.count,
1204
- root_cause: bucket.reason,
1205
- evidence: buildBucketEvidence(bucket),
1206
- bucket_confidence: Number(bucket.confidence.toFixed(2)),
1207
- root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
1208
- dominant: dominantBucket?.index === index,
1209
- secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== index + 1,
1210
- mini_diff: extractMiniDiff(args.input, bucket)
1211
- }));
1623
+ const dominantBucketHasConcreteAnchor = dominantBucket !== null && (readTargets.some((target) => target.bucket_index === dominantBucket.index + 1 && target.file.length > 0) || dominantBucket.bucket.representativeItems.some((item) => item.anchor_kind !== "none"));
1624
+ const smallConcreteSuite = args.analysis.failed + args.analysis.errors <= 2 && residuals.remainingErrors === 0 && residuals.remainingFailed === 0 && buckets.length === 1 && !hasUnknownBucket && dominantBucket !== null && dominantBucketHasConcreteAnchor;
1625
+ const dominantConfidenceThreshold = smallConcreteSuite ? 0.55 : 0.6;
1626
+ const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && hasConcreteCoverage && !hasUnknownBucket && (dominantBucket?.bucket.confidence ?? 0) >= dominantConfidenceThreshold;
1627
+ const rawNeeded = buckets.length === 0 ? !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure) : !diagnosisComplete && !hasUnknownBucket && buckets.every((bucket) => bucket.confidence < 0.7);
1628
+ const mainBuckets = buckets.map((bucket, index) => {
1629
+ const bucketIndex = index + 1;
1630
+ const label = labelForBucket(bucket);
1631
+ const readTarget = readTargets.find((target) => target.bucket_index === bucketIndex);
1632
+ return {
1633
+ bucket_index: bucketIndex,
1634
+ label,
1635
+ count: bucket.count,
1636
+ root_cause: bucket.reason,
1637
+ suspect_kind: deriveBucketSuspectKind({
1638
+ bucket,
1639
+ readTarget
1640
+ }),
1641
+ fix_hint: resolveBucketFixHint({
1642
+ bucket,
1643
+ bucketLabel: label
1644
+ }),
1645
+ evidence: buildBucketEvidence(bucket),
1646
+ bucket_confidence: Number(bucket.confidence.toFixed(2)),
1647
+ root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
1648
+ dominant: dominantBucket?.index === index,
1649
+ secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== bucketIndex,
1650
+ mini_diff: extractMiniDiff(args.input, bucket)
1651
+ };
1652
+ });
1212
1653
  const resolvedTests = unique(args.resolvedTests ?? []);
1213
1654
  const remainingTests = unique(
1214
1655
  args.remainingTests ?? unique([...args.analysis.visibleErrorLabels, ...args.analysis.visibleFailedLabels])
1215
1656
  );
1657
+ const primarySuspectKind = derivePrimarySuspectKind({
1658
+ mainBuckets,
1659
+ dominantBlockerBucketIndex
1660
+ });
1216
1661
  let nextBestAction;
1217
1662
  if (args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0) {
1218
1663
  nextBestAction = {
@@ -1258,6 +1703,8 @@ function buildTestStatusDiagnoseContract(args) {
1258
1703
  additional_source_read_likely_low_value: diagnosisComplete && !rawNeeded,
1259
1704
  read_raw_only_if: rawNeeded ? "you still need exact traceback lines after focused or verbose detail" : null,
1260
1705
  dominant_blocker_bucket_index: dominantBlockerBucketIndex,
1706
+ primary_suspect_kind: primarySuspectKind,
1707
+ confidence_reason: "Unknown or low-confidence buckets remain; one deeper sift pass is justified.",
1261
1708
  provider_used: false,
1262
1709
  provider_confidence: null,
1263
1710
  provider_failed: false,
@@ -1289,9 +1736,16 @@ function buildTestStatusDiagnoseContract(args) {
1289
1736
  })
1290
1737
  }
1291
1738
  };
1739
+ const resolvedDecision = effectiveDecision ?? deriveDecision(mergedContractWithoutDecision);
1740
+ const resolvedConfidenceReason = buildConfidenceReason({
1741
+ decision: resolvedDecision,
1742
+ mainBuckets,
1743
+ primarySuspectKind: mergedContractWithoutDecision.primary_suspect_kind
1744
+ });
1292
1745
  const contract = testStatusDiagnoseContractSchema.parse({
1293
1746
  ...mergedContractWithoutDecision,
1294
- decision: effectiveDecision ?? deriveDecision(mergedContractWithoutDecision)
1747
+ confidence_reason: resolvedConfidenceReason,
1748
+ decision: resolvedDecision
1295
1749
  });
1296
1750
  return {
1297
1751
  contract,
@@ -1363,6 +1817,27 @@ function buildTestStatusAnalysisContext(args) {
1363
1817
  var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
1364
1818
  var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
1365
1819
  var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
1820
+ var RESOURCE_DESTROY_HEADER_PATTERN = /^#\s+.+\bwill be (destroyed|deleted|replaced)\b/i;
1821
+ var DESTROY_ERROR_PATTERN = /(instance cannot be destroyed|prevent_destroy|downtime|data loss)/i;
1822
+ var ACTION_DESTROY_PATTERN = /^-\s+destroy$/i;
1823
+ var TSC_CODE_LABELS = {
1824
+ TS1002: "syntax error",
1825
+ TS1005: "syntax error",
1826
+ TS2304: "cannot find name",
1827
+ TS2307: "cannot find module",
1828
+ TS2322: "type mismatch",
1829
+ TS2339: "missing property on type",
1830
+ TS2345: "argument type mismatch",
1831
+ TS2554: "wrong argument count",
1832
+ TS2741: "missing required property",
1833
+ TS2769: "no matching overload",
1834
+ TS5083: "config file error",
1835
+ TS6133: "declared but unused",
1836
+ TS7006: "implicit any",
1837
+ TS18003: "no inputs were found",
1838
+ TS18046: "unknown type",
1839
+ TS18048: "possibly undefined"
1840
+ };
1366
1841
  function collectEvidence(input, matcher, limit = 3) {
1367
1842
  return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
1368
1843
  }
@@ -1376,11 +1851,236 @@ function inferPackage(line) {
1376
1851
  function inferRemediation(pkg) {
1377
1852
  return `Upgrade ${pkg} to a patched version.`;
1378
1853
  }
1854
+ function parseCompactAuditVulnerability(line) {
1855
+ if (/^Severity:\s*/i.test(line)) {
1856
+ return null;
1857
+ }
1858
+ if (!/\b(critical|high)\b/i.test(line)) {
1859
+ return null;
1860
+ }
1861
+ const pkg = inferPackage(line);
1862
+ if (!pkg) {
1863
+ return null;
1864
+ }
1865
+ return {
1866
+ package: pkg,
1867
+ severity: inferSeverity(line),
1868
+ remediation: inferRemediation(pkg)
1869
+ };
1870
+ }
1871
+ function inferAuditPackageHeader(line) {
1872
+ const trimmed = line.trim();
1873
+ if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.includes(":") || /^node_modules\//i.test(trimmed)) {
1874
+ return null;
1875
+ }
1876
+ const match = trimmed.match(/^([@a-z0-9._/-]+)(?:\s{2,}|\s+(?:[<>=~^*]|\d))/i);
1877
+ return match?.[1] ?? null;
1878
+ }
1879
+ function collectAuditCriticalVulnerabilities(input) {
1880
+ const lines = input.split("\n");
1881
+ const vulnerabilities = [];
1882
+ const seen = /* @__PURE__ */ new Set();
1883
+ const pushVulnerability = (pkg, severity) => {
1884
+ const key = `${pkg}:${severity}`;
1885
+ if (seen.has(key)) {
1886
+ return;
1887
+ }
1888
+ seen.add(key);
1889
+ vulnerabilities.push({
1890
+ package: pkg,
1891
+ severity,
1892
+ remediation: inferRemediation(pkg)
1893
+ });
1894
+ };
1895
+ for (let index = 0; index < lines.length; index += 1) {
1896
+ const line = lines[index].trim();
1897
+ if (!line) {
1898
+ continue;
1899
+ }
1900
+ const compact = parseCompactAuditVulnerability(line);
1901
+ if (compact) {
1902
+ pushVulnerability(compact.package, compact.severity);
1903
+ continue;
1904
+ }
1905
+ const pkg = inferAuditPackageHeader(line);
1906
+ if (!pkg) {
1907
+ continue;
1908
+ }
1909
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 5); cursor += 1) {
1910
+ const candidate = lines[cursor].trim();
1911
+ if (!candidate) {
1912
+ continue;
1913
+ }
1914
+ const severityMatch = candidate.match(/^Severity:\s*(critical|high)\b/i);
1915
+ if (severityMatch) {
1916
+ pushVulnerability(pkg, severityMatch[1].toLowerCase());
1917
+ break;
1918
+ }
1919
+ if (inferAuditPackageHeader(candidate) || parseCompactAuditVulnerability(candidate)) {
1920
+ break;
1921
+ }
1922
+ }
1923
+ }
1924
+ return vulnerabilities;
1925
+ }
1379
1926
  function getCount(input, label) {
1380
1927
  const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
1381
1928
  const lastMatch = matches.at(-1);
1382
1929
  return lastMatch ? Number(lastMatch[1]) : 0;
1383
1930
  }
1931
+ function collectInfraRiskEvidence(input) {
1932
+ const lines = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1933
+ const evidence = [];
1934
+ const seen = /* @__PURE__ */ new Set();
1935
+ const pushMatches = (matcher, options) => {
1936
+ let added = 0;
1937
+ for (const line of lines) {
1938
+ if (!matcher.test(line)) {
1939
+ continue;
1940
+ }
1941
+ if (options?.exclude?.test(line)) {
1942
+ continue;
1943
+ }
1944
+ if (seen.has(line)) {
1945
+ continue;
1946
+ }
1947
+ evidence.push(line);
1948
+ seen.add(line);
1949
+ added += 1;
1950
+ if (options?.limit && added >= options.limit) {
1951
+ return;
1952
+ }
1953
+ if (evidence.length >= (options?.maxEvidence ?? 4)) {
1954
+ return;
1955
+ }
1956
+ }
1957
+ };
1958
+ pushMatches(/Plan:/i, {
1959
+ exclude: ZERO_DESTRUCTIVE_SUMMARY_PATTERN,
1960
+ limit: 1
1961
+ });
1962
+ if (evidence.length < 4) {
1963
+ pushMatches(RESOURCE_DESTROY_HEADER_PATTERN, { limit: 2 });
1964
+ }
1965
+ if (evidence.length < 4) {
1966
+ pushMatches(DESTROY_ERROR_PATTERN, { limit: 1 });
1967
+ }
1968
+ if (evidence.length < 4) {
1969
+ pushMatches(ACTION_DESTROY_PATTERN, { limit: 1 });
1970
+ }
1971
+ if (evidence.length < 4) {
1972
+ pushMatches(RISK_LINE_PATTERN, {
1973
+ exclude: /->\s+null$|\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i,
1974
+ maxEvidence: 4
1975
+ });
1976
+ }
1977
+ return evidence.slice(0, 4);
1978
+ }
1979
+ function collectInfraDestroyTargets(input) {
1980
+ const targets = [];
1981
+ const seen = /* @__PURE__ */ new Set();
1982
+ for (const line of input.split("\n").map((entry) => entry.trim())) {
1983
+ const match = line.match(/^#\s+(.+?)\s+will be (destroyed|deleted|replaced)\b/i);
1984
+ const target = match?.[1]?.trim();
1985
+ if (!target || seen.has(target)) {
1986
+ continue;
1987
+ }
1988
+ seen.add(target);
1989
+ targets.push(target);
1990
+ }
1991
+ return targets;
1992
+ }
1993
+ function inferInfraDestroyCount(input, destroyTargets) {
1994
+ const matches = [
1995
+ ...input.matchAll(/\b(\d+)\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/gi)
1996
+ ];
1997
+ const lastMatch = matches.at(-1);
1998
+ return lastMatch ? Number(lastMatch[1]) : destroyTargets.length;
1999
+ }
2000
+ function collectInfraBlockers(input) {
2001
+ const lines = input.split("\n");
2002
+ const blockers = [];
2003
+ const seen = /* @__PURE__ */ new Set();
2004
+ for (let index = 0; index < lines.length; index += 1) {
2005
+ const trimmed = lines[index]?.trim();
2006
+ const errorMatch = trimmed?.match(/^(?:[│|]\s*)?Error:\s+(.+)$/);
2007
+ if (!errorMatch) {
2008
+ continue;
2009
+ }
2010
+ const message = errorMatch[1].trim();
2011
+ const nearby = lines.slice(index, index + 8).join("\n");
2012
+ const preventDestroyTarget = nearby.match(/Resource\s+([^\s]+)\s+has lifecycle\.prevent_destroy set/i)?.[1] ?? null;
2013
+ const type = preventDestroyTarget ? "prevent_destroy" : "destroy_blocked";
2014
+ const key = `${type}:${preventDestroyTarget ?? ""}:${message}`;
2015
+ if (seen.has(key)) {
2016
+ continue;
2017
+ }
2018
+ seen.add(key);
2019
+ blockers.push({
2020
+ type,
2021
+ target: preventDestroyTarget,
2022
+ message
2023
+ });
2024
+ }
2025
+ return blockers;
2026
+ }
2027
+ function detectTestRunner(input) {
2028
+ if (/^\s*Test Files?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /^\s*Tests?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /^\s*Snapshots?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(input)) {
2029
+ return "vitest";
2030
+ }
2031
+ if (/^\s*Test Suites:\s+\d+\s+failed,\s+\d+\s+passed(?:,\s+\d+\s+total)?/m.test(input) || /^\s*Tests:\s+\d+\s+failed,\s+\d+\s+passed(?:,\s+\d+\s+total)?/m.test(input)) {
2032
+ return "jest";
2033
+ }
2034
+ if (/\bpytest\b/i.test(input) || /^\s*=+.*\b\d+\s+failed\b.*=+\s*$/m.test(input) || /\bcollected\s+\d+\s+items\b/i.test(input)) {
2035
+ return "pytest";
2036
+ }
2037
+ return "unknown";
2038
+ }
2039
+ function extractVitestLineCount(input, label, metric) {
2040
+ const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
2041
+ const lines = [...input.matchAll(matcher)];
2042
+ const line = lines.at(-1)?.[1];
2043
+ if (!line) {
2044
+ return null;
2045
+ }
2046
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
2047
+ return metricMatch ? Number(metricMatch[1]) : null;
2048
+ }
2049
+ function extractJestLineCount(input, label, metric) {
2050
+ const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
2051
+ const lines = [...input.matchAll(matcher)];
2052
+ const line = lines.at(-1)?.[1];
2053
+ if (!line) {
2054
+ return null;
2055
+ }
2056
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
2057
+ return metricMatch ? Number(metricMatch[1]) : null;
2058
+ }
2059
+ function extractTestStatusCounts(input, runner) {
2060
+ if (runner === "vitest") {
2061
+ return {
2062
+ passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
2063
+ failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
2064
+ errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
2065
+ skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
2066
+ snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
2067
+ };
2068
+ }
2069
+ if (runner === "jest") {
2070
+ return {
2071
+ passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
2072
+ failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
2073
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
2074
+ skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
2075
+ };
2076
+ }
2077
+ return {
2078
+ passed: getCount(input, "passed"),
2079
+ failed: getCount(input, "failed"),
2080
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
2081
+ skipped: getCount(input, "skipped")
2082
+ };
2083
+ }
1384
2084
  function formatCount2(count, singular, plural = `${singular}s`) {
1385
2085
  return `${count} ${count === 1 ? singular : plural}`;
1386
2086
  }
@@ -1401,6 +2101,21 @@ function collectUniqueMatches(input, matcher, limit = 6) {
1401
2101
  }
1402
2102
  return values;
1403
2103
  }
2104
+ function compactDisplayFile(file) {
2105
+ const normalized = file.replace(/\\/g, "/").trim();
2106
+ if (!normalized) {
2107
+ return file;
2108
+ }
2109
+ const looksAbsolute = normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized);
2110
+ if (!looksAbsolute && normalized.length <= 60) {
2111
+ return normalized;
2112
+ }
2113
+ const basename = normalized.split("/").at(-1);
2114
+ return basename && basename.length > 0 ? basename : normalized;
2115
+ }
2116
+ function formatDisplayedFiles(files, limit = 3) {
2117
+ return [...new Set([...files].map((file) => file.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right)).slice(0, limit).map((file) => compactDisplayFile(file));
2118
+ }
1404
2119
  function emptyAnchor() {
1405
2120
  return {
1406
2121
  file: null,
@@ -1413,7 +2128,8 @@ function normalizeAnchorFile(value) {
1413
2128
  return value.replace(/\\/g, "/").trim();
1414
2129
  }
1415
2130
  function inferFileFromLabel(label) {
1416
- const candidate = cleanFailureLabel(label).split("::")[0]?.trim();
2131
+ const cleaned = cleanFailureLabel(label);
2132
+ const candidate = cleaned.split("::")[0]?.split(" > ")[0]?.trim();
1417
2133
  if (!candidate) {
1418
2134
  return null;
1419
2135
  }
@@ -1468,6 +2184,15 @@ function parseObservedAnchor(line) {
1468
2184
  anchor_confidence: 0.92
1469
2185
  };
1470
2186
  }
2187
+ const vitestTraceback = normalized.match(/^\s*❯\s+([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?/);
2188
+ if (vitestTraceback) {
2189
+ return {
2190
+ file: normalizeAnchorFile(vitestTraceback[1]),
2191
+ line: Number(vitestTraceback[2]),
2192
+ anchor_kind: "traceback",
2193
+ anchor_confidence: 1
2194
+ };
2195
+ }
1471
2196
  return null;
1472
2197
  }
1473
2198
  function resolveAnchorForLabel(args) {
@@ -1484,15 +2209,27 @@ function isLowValueInternalReason(normalized) {
1484
2209
  ) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
1485
2210
  }
1486
2211
  function scoreFailureReason(reason) {
2212
+ if (reason.startsWith("configuration:")) {
2213
+ return 6;
2214
+ }
1487
2215
  if (reason.startsWith("missing test env:")) {
1488
2216
  return 6;
1489
2217
  }
1490
2218
  if (reason.startsWith("missing module:")) {
1491
2219
  return 5;
1492
2220
  }
2221
+ if (reason.startsWith("snapshot mismatch:")) {
2222
+ return 4;
2223
+ }
1493
2224
  if (reason.startsWith("assertion failed:")) {
1494
2225
  return 4;
1495
2226
  }
2227
+ if (reason.startsWith("timeout:") || reason.startsWith("async loop:") || reason.startsWith("django db access:") || reason.startsWith("db migration:")) {
2228
+ return 3;
2229
+ }
2230
+ if (reason.startsWith("permission:") || reason.startsWith("xdist worker crash:") || reason.startsWith("network:") || reason.startsWith("segfault:") || reason.startsWith("memory:") || reason.startsWith("type error:") || reason.startsWith("serialization:") || reason.startsWith("file not found:") || reason.startsWith("deprecation as error:") || reason.startsWith("xfail strict:") || reason.startsWith("resource leak:") || reason.startsWith("flaky:") || reason.startsWith("fixture teardown:")) {
2231
+ return 2;
2232
+ }
1496
2233
  if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
1497
2234
  return 3;
1498
2235
  }
@@ -1501,6 +2238,16 @@ function scoreFailureReason(reason) {
1501
2238
  }
1502
2239
  return 1;
1503
2240
  }
2241
+ function buildClassifiedReason(prefix, detail) {
2242
+ return `${prefix}: ${detail}`.slice(0, 120);
2243
+ }
2244
+ function buildExcerptDetail(value, fallback) {
2245
+ const trimmed = value.trim().replace(/\s+/g, " ");
2246
+ return trimmed.length > 0 ? trimmed : fallback;
2247
+ }
2248
+ function sharedBlockerThreshold(reason) {
2249
+ return reason.startsWith("configuration:") ? 1 : 3;
2250
+ }
1504
2251
  function extractEnvBlockerName(normalized) {
1505
2252
  const directMatch = normalized.match(
1506
2253
  /\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
@@ -1600,60 +2347,366 @@ function classifyFailureReason(line, options) {
1600
2347
  group: "authentication test setup failures"
1601
2348
  };
1602
2349
  }
1603
- const pythonMissingModule = normalized.match(
1604
- /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
2350
+ const snapshotMismatch = normalized.match(
2351
+ /((?:Error:\s*)?Snapshot\b.+\bmismatched\b[^$]*|Snapshot comparison failed[^$]*)/i
1605
2352
  );
1606
- if (pythonMissingModule) {
2353
+ if (snapshotMismatch) {
1607
2354
  return {
1608
- reason: `missing module: ${pythonMissingModule[1]}`,
1609
- group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2355
+ reason: buildClassifiedReason(
2356
+ "snapshot mismatch",
2357
+ buildExcerptDetail(snapshotMismatch[1] ?? normalized, "snapshot output changed")
2358
+ ),
2359
+ group: "snapshot mismatches"
1610
2360
  };
1611
2361
  }
1612
- const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
1613
- if (nodeMissingModule) {
2362
+ const timeoutFailure = normalized.match(
2363
+ /(Failed:\s*Timeout\s*>[^,;]+|asyncio\.exceptions\.TimeoutError:\s*.+|TimeoutError:\s*.+|(?:Test|Hook)\s+timed out in\s+\d+(?:\.\d+)?m?s[^$]*|(?:\[vitest-(?:worker|pool)\]:\s*)?Timeout[^$]*)$/i
2364
+ );
2365
+ if (timeoutFailure) {
1614
2366
  return {
1615
- reason: `missing module: ${nodeMissingModule[1]}`,
1616
- group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2367
+ reason: buildClassifiedReason(
2368
+ "timeout",
2369
+ buildExcerptDetail(timeoutFailure[1] ?? normalized, "test exceeded timeout threshold")
2370
+ ),
2371
+ group: "timeout failures"
1617
2372
  };
1618
2373
  }
1619
- const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
1620
- if (assertionFailure) {
2374
+ const asyncLoopFailure = normalized.match(
2375
+ /(Event loop is closed|no current event loop|coroutine .* was never awaited|coroutine was never awaited)/i
2376
+ );
2377
+ if (asyncLoopFailure) {
1621
2378
  return {
1622
- reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
1623
- group: "assertion failures"
2379
+ reason: buildClassifiedReason(
2380
+ "async loop",
2381
+ buildExcerptDetail(asyncLoopFailure[1] ?? normalized, "async event loop failure")
2382
+ ),
2383
+ group: "async event loop failures"
1624
2384
  };
1625
2385
  }
1626
- const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
1627
- if (genericError) {
1628
- const errorType = genericError[1];
2386
+ const permissionFailure = normalized.match(
2387
+ /(PermissionError:\s*\[Errno 13\][^$]*|Address already in use)/i
2388
+ );
2389
+ if (permissionFailure) {
1629
2390
  return {
1630
- reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
1631
- group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
2391
+ reason: buildClassifiedReason(
2392
+ "permission",
2393
+ buildExcerptDetail(permissionFailure[1] ?? normalized, "permission denied or resource locked")
2394
+ ),
2395
+ group: "permission or locked resource failures"
1632
2396
  };
1633
2397
  }
1634
- if (/ImportError while importing test module/i.test(normalized)) {
2398
+ const osDiskFullFailure = normalized.match(
2399
+ /(OSError:\s*\[Errno 28\][^$]*|No space left on device)/i
2400
+ );
2401
+ if (osDiskFullFailure) {
1635
2402
  return {
1636
- reason: "import error during collection",
1637
- group: "import/dependency errors during collection"
2403
+ reason: buildClassifiedReason(
2404
+ "configuration",
2405
+ `disk full (${buildExcerptDetail(
2406
+ osDiskFullFailure[1] ?? normalized,
2407
+ "No space left on device"
2408
+ )})`
2409
+ ),
2410
+ group: "test configuration failures"
1638
2411
  };
1639
2412
  }
1640
- if (!/[A-Za-z]/.test(normalized)) {
1641
- return null;
2413
+ const osPermissionFailure = normalized.match(/OSError:\s*\[Errno 13\][^$]*/i);
2414
+ if (osPermissionFailure) {
2415
+ return {
2416
+ reason: buildClassifiedReason(
2417
+ "permission",
2418
+ buildExcerptDetail(osPermissionFailure[0] ?? normalized, "permission denied")
2419
+ ),
2420
+ group: "permission or locked resource failures"
2421
+ };
1642
2422
  }
1643
- return {
1644
- reason: normalized.slice(0, 120),
1645
- group: options.duringCollection ? "collection/import errors" : "other failures"
1646
- };
1647
- }
1648
- function pushFocusedFailureItem(items, candidate) {
1649
- if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
1650
- return;
2423
+ const xdistWorkerCrash = normalized.match(
2424
+ /(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
2425
+ );
2426
+ if (xdistWorkerCrash) {
2427
+ return {
2428
+ reason: buildClassifiedReason(
2429
+ "xdist worker crash",
2430
+ buildExcerptDetail(xdistWorkerCrash[1] ?? normalized, "pytest-xdist worker crashed")
2431
+ ),
2432
+ group: "xdist worker crashes"
2433
+ };
1651
2434
  }
1652
- items.push(candidate);
1653
- }
1654
- function chooseStrongestFailureItems(items) {
1655
- const strongest = /* @__PURE__ */ new Map();
1656
- const order = [];
2435
+ if (/Worker terminated due to reaching memory limit/i.test(normalized)) {
2436
+ return {
2437
+ reason: "memory: Worker terminated due to reaching memory limit",
2438
+ group: "memory exhaustion failures"
2439
+ };
2440
+ }
2441
+ if (/Database access not allowed, use the ["']django_db["'] mark/i.test(normalized)) {
2442
+ return {
2443
+ reason: 'django db access: Database access not allowed, use the "django_db" mark',
2444
+ group: "django database marker failures"
2445
+ };
2446
+ }
2447
+ const networkFailure = normalized.match(
2448
+ /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable|ConnectionResetError[^,;]*|BrokenPipeError[^,;]*|HTTPError:\s*[45]\d\d[^,;]*)/i
2449
+ );
2450
+ if (networkFailure) {
2451
+ return {
2452
+ reason: buildClassifiedReason(
2453
+ "network",
2454
+ buildExcerptDetail(networkFailure[1] ?? normalized, "network dependency failure")
2455
+ ),
2456
+ group: "network dependency failures"
2457
+ };
2458
+ }
2459
+ const matcherAssertionFailure = normalized.match(
2460
+ /(expect\(received\)\.(?:toBe|toEqual|toStrictEqual|toMatchObject)\(expected\))/i
2461
+ );
2462
+ if (matcherAssertionFailure) {
2463
+ return {
2464
+ reason: `assertion failed: ${matcherAssertionFailure[1]}`.slice(0, 120),
2465
+ group: "assertion failures"
2466
+ };
2467
+ }
2468
+ const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
2469
+ if (relationMigration) {
2470
+ return {
2471
+ reason: buildClassifiedReason("db migration", `relation ${relationMigration[1]} does not exist`),
2472
+ group: "database migration or schema failures"
2473
+ };
2474
+ }
2475
+ const noSuchTable = normalized.match(/no such table(?::)?\s*([A-Za-z0-9_.-]+)/i);
2476
+ if (noSuchTable) {
2477
+ return {
2478
+ reason: buildClassifiedReason("db migration", `no such table ${noSuchTable[1]}`),
2479
+ group: "database migration or schema failures"
2480
+ };
2481
+ }
2482
+ if (/InconsistentMigrationHistory/i.test(normalized)) {
2483
+ return {
2484
+ reason: "db migration: InconsistentMigrationHistory",
2485
+ group: "database migration or schema failures"
2486
+ };
2487
+ }
2488
+ if (/(Segmentation fault|SIGSEGV|\bexit 139\b)/i.test(normalized)) {
2489
+ return {
2490
+ reason: buildClassifiedReason(
2491
+ "segfault",
2492
+ buildExcerptDetail(normalized, "subprocess crashed with SIGSEGV")
2493
+ ),
2494
+ group: "subprocess crash failures"
2495
+ };
2496
+ }
2497
+ if (/(MemoryError\b|\bexit 137\b|OOMKilled|OutOfMemory)/i.test(normalized)) {
2498
+ return {
2499
+ reason: buildClassifiedReason(
2500
+ "memory",
2501
+ buildExcerptDetail(normalized, "process exhausted available memory")
2502
+ ),
2503
+ group: "memory exhaustion failures"
2504
+ };
2505
+ }
2506
+ const propertySetterOverrideFailure = normalized.match(
2507
+ /AttributeError:\s*(property ['"][^'"]+['"] of ['"][^'"]+['"] object has no setter|can't set attribute|readonly attribute|read-only attribute)/i
2508
+ );
2509
+ if (propertySetterOverrideFailure) {
2510
+ return {
2511
+ reason: buildClassifiedReason(
2512
+ "configuration",
2513
+ `invalid test setup override (${buildExcerptDetail(
2514
+ `AttributeError: ${propertySetterOverrideFailure[1] ?? normalized}`,
2515
+ "AttributeError: can't set attribute"
2516
+ )})`
2517
+ ),
2518
+ group: "test configuration failures"
2519
+ };
2520
+ }
2521
+ const setupOverrideFailure = normalized.match(/\b(AttributeError|TypeError):\s*(.+)$/i);
2522
+ if (setupOverrideFailure && /(monkeypatch|patch|fixture|settings|conftest)/i.test(normalized)) {
2523
+ return {
2524
+ reason: buildClassifiedReason(
2525
+ "configuration",
2526
+ `invalid test setup override (${buildExcerptDetail(
2527
+ `${setupOverrideFailure[1]}: ${setupOverrideFailure[2] ?? ""}`,
2528
+ `${setupOverrideFailure[1]}`
2529
+ )})`
2530
+ ),
2531
+ group: "test configuration failures"
2532
+ };
2533
+ }
2534
+ const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
2535
+ if (typeErrorFailure) {
2536
+ return {
2537
+ reason: buildClassifiedReason(
2538
+ "type error",
2539
+ buildExcerptDetail(typeErrorFailure[1] ?? normalized, "TypeError")
2540
+ ),
2541
+ group: "type errors"
2542
+ };
2543
+ }
2544
+ const serializationFailure = normalized.match(
2545
+ /\b(UnicodeDecodeError|JSONDecodeError|PicklingError):\s*(.+)$/i
2546
+ );
2547
+ if (serializationFailure) {
2548
+ return {
2549
+ reason: buildClassifiedReason(
2550
+ "serialization",
2551
+ `${serializationFailure[1]}: ${buildExcerptDetail(serializationFailure[2] ?? "", serializationFailure[1] ?? "serialization failure")}`
2552
+ ),
2553
+ group: "serialization and encoding failures"
2554
+ };
2555
+ }
2556
+ const fileNotFoundFailure = normalized.match(/FileNotFoundError:\s*(.+)$/i);
2557
+ if (fileNotFoundFailure) {
2558
+ return {
2559
+ reason: buildClassifiedReason(
2560
+ "file not found",
2561
+ buildExcerptDetail(fileNotFoundFailure[1] ?? normalized, "missing file during test execution")
2562
+ ),
2563
+ group: "missing file failures"
2564
+ };
2565
+ }
2566
+ const deprecationFailure = normalized.match(
2567
+ /\b(DeprecationWarning|FutureWarning|PytestRemovedIn9Warning):\s*(.+)$/i
2568
+ );
2569
+ if (deprecationFailure) {
2570
+ return {
2571
+ reason: buildClassifiedReason(
2572
+ "deprecation as error",
2573
+ `${deprecationFailure[1]}: ${buildExcerptDetail(deprecationFailure[2] ?? "", deprecationFailure[1] ?? "warning treated as error")}`
2574
+ ),
2575
+ group: "warnings treated as errors"
2576
+ };
2577
+ }
2578
+ const strictXfail = normalized.match(/XPASS\(strict\)\s*:?\s*(.+)?$/i);
2579
+ if (strictXfail) {
2580
+ return {
2581
+ reason: buildClassifiedReason(
2582
+ "xfail strict",
2583
+ buildExcerptDetail(strictXfail[1] ?? normalized, "strict xfail unexpectedly passed")
2584
+ ),
2585
+ group: "strict xfail expectation failures"
2586
+ };
2587
+ }
2588
+ const resourceLeak = normalized.match(
2589
+ /(PytestUnraisableExceptionWarning[^,;]*|ResourceWarning:\s*unclosed[^,;]*)/i
2590
+ );
2591
+ if (resourceLeak) {
2592
+ return {
2593
+ reason: buildClassifiedReason(
2594
+ "resource leak",
2595
+ buildExcerptDetail(resourceLeak[1] ?? normalized, "resource leak warning promoted to failure")
2596
+ ),
2597
+ group: "resource leak warnings"
2598
+ };
2599
+ }
2600
+ const flakyFailure = normalized.match(/\b(RERUN|[0-9]+\s+rerun|Flaky test passed)\b[^$]*/i);
2601
+ if (flakyFailure) {
2602
+ return {
2603
+ reason: buildClassifiedReason(
2604
+ "flaky",
2605
+ buildExcerptDetail(flakyFailure[0] ?? normalized, "test required reruns before passing")
2606
+ ),
2607
+ group: "flaky test detections"
2608
+ };
2609
+ }
2610
+ const teardownFailure = normalized.match(/ERROR at teardown of\s+(.+)$/i);
2611
+ if (teardownFailure) {
2612
+ return {
2613
+ reason: buildClassifiedReason(
2614
+ "fixture teardown",
2615
+ buildExcerptDetail(teardownFailure[1] ?? normalized, "fixture teardown failed")
2616
+ ),
2617
+ group: "fixture teardown failures"
2618
+ };
2619
+ }
2620
+ const configurationFailure = normalized.match(
2621
+ /(INTERNALERROR>.+|ConftestImportFailure[^,;]*|UsageError:\s*.+|ERROR:\s*usage:\s*.+|pytest:\s*error:\s*.+|Cannot use import statement outside a module[^$]*|Named export.+not found.+CommonJS[^$]*|failed to load config from.+|localStorage is not available[^$]*|No test suite found in file.+|No test found in suite.+)$/i
2622
+ );
2623
+ if (configurationFailure) {
2624
+ return {
2625
+ reason: buildClassifiedReason(
2626
+ "configuration",
2627
+ buildExcerptDetail(configurationFailure[1] ?? normalized, "test configuration error")
2628
+ ),
2629
+ group: "test configuration failures"
2630
+ };
2631
+ }
2632
+ const pythonMissingModule = normalized.match(
2633
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
2634
+ );
2635
+ if (pythonMissingModule) {
2636
+ return {
2637
+ reason: `missing module: ${pythonMissingModule[1]}`,
2638
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2639
+ };
2640
+ }
2641
+ const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
2642
+ if (nodeMissingModule) {
2643
+ return {
2644
+ reason: `missing module: ${nodeMissingModule[1]}`,
2645
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2646
+ };
2647
+ }
2648
+ const importResolutionFailure = normalized.match(/Failed to resolve import ['"]([^'"]+)['"]/i);
2649
+ if (importResolutionFailure) {
2650
+ return {
2651
+ reason: `missing module: ${importResolutionFailure[1]}`,
2652
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2653
+ };
2654
+ }
2655
+ const esmModuleFailure = normalized.match(/ERR_MODULE_NOT_FOUND[^'"`]*['"`]([^'"`]+)['"`]/i) ?? normalized.match(/Cannot find package ['"`]([^'"`]+)['"`]/i);
2656
+ if (esmModuleFailure) {
2657
+ return {
2658
+ reason: `missing module: ${esmModuleFailure[1]}`,
2659
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2660
+ };
2661
+ }
2662
+ const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
2663
+ if (assertionFailure) {
2664
+ return {
2665
+ reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
2666
+ group: "assertion failures"
2667
+ };
2668
+ }
2669
+ const vitestUnhandled = normalized.match(/Vitest caught\s+\d+\s+unhandled errors?/i);
2670
+ if (vitestUnhandled) {
2671
+ return {
2672
+ reason: `RuntimeError: ${buildExcerptDetail(vitestUnhandled[0] ?? normalized, "Vitest caught unhandled errors")}`.slice(
2673
+ 0,
2674
+ 120
2675
+ ),
2676
+ group: "runtime failures"
2677
+ };
2678
+ }
2679
+ const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
2680
+ if (genericError) {
2681
+ const errorType = genericError[1];
2682
+ return {
2683
+ reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
2684
+ group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
2685
+ };
2686
+ }
2687
+ if (/ImportError while importing test module/i.test(normalized)) {
2688
+ return {
2689
+ reason: "import error during collection",
2690
+ group: "import/dependency errors during collection"
2691
+ };
2692
+ }
2693
+ if (!/[A-Za-z]/.test(normalized)) {
2694
+ return null;
2695
+ }
2696
+ return {
2697
+ reason: normalized.slice(0, 120),
2698
+ group: options.duringCollection ? "collection/import errors" : "other failures"
2699
+ };
2700
+ }
2701
+ function pushFocusedFailureItem(items, candidate) {
2702
+ if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
2703
+ return;
2704
+ }
2705
+ items.push(candidate);
2706
+ }
2707
+ function chooseStrongestFailureItems(items) {
2708
+ const strongest = /* @__PURE__ */ new Map();
2709
+ const order = [];
1657
2710
  for (const item of items) {
1658
2711
  const existing = strongest.get(item.label);
1659
2712
  if (!existing) {
@@ -1667,6 +2720,125 @@ function chooseStrongestFailureItems(items) {
1667
2720
  }
1668
2721
  return order.map((label) => strongest.get(label));
1669
2722
  }
2723
+ function extractJsTestFile(value) {
2724
+ const match = value.match(/([A-Za-z0-9_./-]+\.(?:test|spec)\.[cm]?[jt]sx?)/i);
2725
+ return match ? normalizeAnchorFile(match[1]) : null;
2726
+ }
2727
+ function normalizeJsFailureLabel(label) {
2728
+ return cleanFailureLabel(label).replace(/^[❯×]\s*/, "").replace(/\s+\[[^\]]+\]\s*$/, "").replace(/\s+/g, " ").trim();
2729
+ }
2730
+ function classifyFailureLines(args) {
2731
+ let observedAnchor = null;
2732
+ let strongest = null;
2733
+ for (const line of args.lines) {
2734
+ observedAnchor = parseObservedAnchor(line) ?? observedAnchor;
2735
+ const classification = classifyFailureReason(line, {
2736
+ duringCollection: args.duringCollection
2737
+ });
2738
+ if (!classification) {
2739
+ continue;
2740
+ }
2741
+ const score = scoreFailureReason(classification.reason);
2742
+ if (!strongest || score > strongest.score) {
2743
+ strongest = {
2744
+ classification,
2745
+ score,
2746
+ observedAnchor: parseObservedAnchor(line) ?? observedAnchor
2747
+ };
2748
+ }
2749
+ }
2750
+ if (!strongest) {
2751
+ return null;
2752
+ }
2753
+ return {
2754
+ classification: strongest.classification,
2755
+ observedAnchor: strongest.observedAnchor ?? observedAnchor
2756
+ };
2757
+ }
2758
+ function collectJsFailureBlocks(input) {
2759
+ const blocks = [];
2760
+ let current = null;
2761
+ let section = null;
2762
+ let currentFile = null;
2763
+ const flushCurrent = () => {
2764
+ if (!current) {
2765
+ return;
2766
+ }
2767
+ blocks.push(current);
2768
+ current = null;
2769
+ };
2770
+ for (const rawLine of input.split("\n")) {
2771
+ const line = rawLine.trimEnd();
2772
+ const trimmed = line.trim();
2773
+ if (/⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(line)) {
2774
+ flushCurrent();
2775
+ section = "failed_tests";
2776
+ continue;
2777
+ }
2778
+ if (/⎯{2,}\s+Failed Suites?\s+\d+\s+⎯{2,}/.test(line)) {
2779
+ flushCurrent();
2780
+ section = "failed_suites";
2781
+ continue;
2782
+ }
2783
+ if (section && /^⎯{2,}.+⎯{2,}\s*$/.test(line)) {
2784
+ flushCurrent();
2785
+ section = null;
2786
+ continue;
2787
+ }
2788
+ const progress = line.match(
2789
+ /^(.+?\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>.+?)?)\s+(FAILED|ERROR)\s+\[[^\]]+\]\s*$/
2790
+ );
2791
+ if (progress) {
2792
+ flushCurrent();
2793
+ const label = normalizeJsFailureLabel(progress[1]);
2794
+ current = {
2795
+ label,
2796
+ status: progress[2] === "ERROR" ? "error" : "failed",
2797
+ detailLines: []
2798
+ };
2799
+ currentFile = extractJsTestFile(label);
2800
+ continue;
2801
+ }
2802
+ const failHeader = line.match(/^\s*FAIL\s+(.+)$/);
2803
+ if (failHeader) {
2804
+ const label = normalizeJsFailureLabel(failHeader[1]);
2805
+ if (extractJsTestFile(label)) {
2806
+ flushCurrent();
2807
+ current = {
2808
+ label,
2809
+ status: section === "failed_suites" || !label.includes(" > ") ? "error" : "failed",
2810
+ detailLines: []
2811
+ };
2812
+ currentFile = extractJsTestFile(label);
2813
+ continue;
2814
+ }
2815
+ }
2816
+ const failedTest = line.match(/^\s*×\s+(.+)$/);
2817
+ if (failedTest && (section === "failed_tests" || extractJsTestFile(failedTest[1]))) {
2818
+ flushCurrent();
2819
+ const candidate = normalizeJsFailureLabel(failedTest[1]);
2820
+ const file = extractJsTestFile(candidate) ?? currentFile;
2821
+ const label = file && !extractJsTestFile(candidate) ? `${file} > ${candidate}` : candidate;
2822
+ current = {
2823
+ label,
2824
+ status: "failed",
2825
+ detailLines: []
2826
+ };
2827
+ currentFile = extractJsTestFile(label) ?? currentFile;
2828
+ continue;
2829
+ }
2830
+ if (/^\s*(?:Tests?|Snapshots?|Test Files?|Test Suites?)\b/.test(line)) {
2831
+ flushCurrent();
2832
+ section = null;
2833
+ continue;
2834
+ }
2835
+ if (current && trimmed.length > 0) {
2836
+ current.detailLines.push(line);
2837
+ }
2838
+ }
2839
+ flushCurrent();
2840
+ return blocks;
2841
+ }
1670
2842
  function collectCollectionFailureItems(input) {
1671
2843
  const items = [];
1672
2844
  const lines = input.split("\n");
@@ -1674,6 +2846,24 @@ function collectCollectionFailureItems(input) {
1674
2846
  let pendingGenericReason = null;
1675
2847
  let currentAnchor = null;
1676
2848
  for (const line of lines) {
2849
+ const standaloneCollectionLabel = line.match(/No test suite found in file\s+(.+)$/i)?.[1] ?? line.match(/No test found in suite\s+(.+)$/i)?.[1] ?? line.match(/failed to load config from\s+(.+)$/i)?.[1];
2850
+ if (standaloneCollectionLabel) {
2851
+ const classification2 = classifyFailureReason(line, {
2852
+ duringCollection: true
2853
+ });
2854
+ if (classification2) {
2855
+ pushFocusedFailureItem(items, {
2856
+ label: cleanFailureLabel(standaloneCollectionLabel),
2857
+ reason: classification2.reason,
2858
+ group: classification2.group,
2859
+ ...resolveAnchorForLabel({
2860
+ label: cleanFailureLabel(standaloneCollectionLabel),
2861
+ observedAnchor: parseObservedAnchor(line)
2862
+ })
2863
+ });
2864
+ }
2865
+ continue;
2866
+ }
1677
2867
  const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
1678
2868
  if (collecting) {
1679
2869
  if (currentLabel && pendingGenericReason) {
@@ -1762,6 +2952,24 @@ function collectInlineFailureItems(input) {
1762
2952
  })
1763
2953
  });
1764
2954
  }
2955
+ for (const block of collectJsFailureBlocks(input)) {
2956
+ const resolved = classifyFailureLines({
2957
+ lines: block.detailLines,
2958
+ duringCollection: block.status === "error"
2959
+ });
2960
+ if (!resolved) {
2961
+ continue;
2962
+ }
2963
+ pushFocusedFailureItem(items, {
2964
+ label: block.label,
2965
+ reason: resolved.classification.reason,
2966
+ group: resolved.classification.group,
2967
+ ...resolveAnchorForLabel({
2968
+ label: block.label,
2969
+ observedAnchor: resolved.observedAnchor
2970
+ })
2971
+ });
2972
+ }
1765
2973
  return items;
1766
2974
  }
1767
2975
  function collectInlineFailureItemsWithStatus(input) {
@@ -1796,16 +3004,42 @@ function collectInlineFailureItemsWithStatus(input) {
1796
3004
  })
1797
3005
  });
1798
3006
  }
3007
+ for (const block of collectJsFailureBlocks(input)) {
3008
+ const resolved = classifyFailureLines({
3009
+ lines: block.detailLines,
3010
+ duringCollection: block.status === "error"
3011
+ });
3012
+ if (!resolved) {
3013
+ continue;
3014
+ }
3015
+ items.push({
3016
+ label: block.label,
3017
+ reason: resolved.classification.reason,
3018
+ group: resolved.classification.group,
3019
+ status: block.status,
3020
+ ...resolveAnchorForLabel({
3021
+ label: block.label,
3022
+ observedAnchor: resolved.observedAnchor
3023
+ })
3024
+ });
3025
+ }
1799
3026
  return items;
1800
3027
  }
1801
3028
  function collectStandaloneErrorClassifications(input) {
1802
3029
  const classifications = [];
1803
3030
  for (const line of input.split("\n")) {
3031
+ const trimmed = line.trim();
3032
+ if (!trimmed) {
3033
+ continue;
3034
+ }
1804
3035
  const standalone = line.match(/^\s*E\s+(.+)$/);
1805
- if (!standalone) {
3036
+ const candidate = standalone?.[1] ?? (/^(INTERNALERROR>|ConftestImportFailure\b|UsageError:|ERROR:\s*usage:|pytest:\s*error:)/i.test(
3037
+ trimmed
3038
+ ) ? trimmed : null);
3039
+ if (!candidate) {
1806
3040
  continue;
1807
3041
  }
1808
- const classification = classifyFailureReason(standalone[1], {
3042
+ const classification = classifyFailureReason(candidate, {
1809
3043
  duringCollection: false
1810
3044
  });
1811
3045
  if (!classification || classification.reason === "import error during collection") {
@@ -1921,6 +3155,9 @@ function collectFailureLabels(input) {
1921
3155
  pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
1922
3156
  }
1923
3157
  }
3158
+ for (const block of collectJsFailureBlocks(input)) {
3159
+ pushLabel(block.label, block.status);
3160
+ }
1924
3161
  return labels;
1925
3162
  }
1926
3163
  function classifyBucketTypeFromReason(reason) {
@@ -1930,6 +3167,60 @@ function classifyBucketTypeFromReason(reason) {
1930
3167
  if (reason.startsWith("fixture guard:")) {
1931
3168
  return "fixture_guard_failure";
1932
3169
  }
3170
+ if (reason.startsWith("timeout:")) {
3171
+ return "timeout_failure";
3172
+ }
3173
+ if (reason.startsWith("permission:")) {
3174
+ return "permission_denied_failure";
3175
+ }
3176
+ if (reason.startsWith("async loop:")) {
3177
+ return "async_event_loop_failure";
3178
+ }
3179
+ if (reason.startsWith("fixture teardown:")) {
3180
+ return "fixture_teardown_failure";
3181
+ }
3182
+ if (reason.startsWith("db migration:")) {
3183
+ return "db_migration_failure";
3184
+ }
3185
+ if (reason.startsWith("configuration:")) {
3186
+ return "configuration_error";
3187
+ }
3188
+ if (reason.startsWith("xdist worker crash:")) {
3189
+ return "xdist_worker_crash";
3190
+ }
3191
+ if (reason.startsWith("type error:")) {
3192
+ return "type_error_failure";
3193
+ }
3194
+ if (reason.startsWith("resource leak:")) {
3195
+ return "resource_leak_warning";
3196
+ }
3197
+ if (reason.startsWith("django db access:")) {
3198
+ return "django_db_access_denied";
3199
+ }
3200
+ if (reason.startsWith("network:")) {
3201
+ return "network_failure";
3202
+ }
3203
+ if (reason.startsWith("segfault:")) {
3204
+ return "subprocess_crash_segfault";
3205
+ }
3206
+ if (reason.startsWith("flaky:")) {
3207
+ return "flaky_test_detected";
3208
+ }
3209
+ if (reason.startsWith("serialization:")) {
3210
+ return "serialization_encoding_failure";
3211
+ }
3212
+ if (reason.startsWith("file not found:")) {
3213
+ return "file_not_found_failure";
3214
+ }
3215
+ if (reason.startsWith("memory:")) {
3216
+ return "memory_error";
3217
+ }
3218
+ if (reason.startsWith("deprecation as error:")) {
3219
+ return "deprecation_warning_as_error";
3220
+ }
3221
+ if (reason.startsWith("xfail strict:")) {
3222
+ return "xfail_strict_unexpected_pass";
3223
+ }
1933
3224
  if (reason.startsWith("service unavailable:")) {
1934
3225
  return "service_unavailable";
1935
3226
  }
@@ -1939,6 +3230,9 @@ function classifyBucketTypeFromReason(reason) {
1939
3230
  if (reason.startsWith("auth bypass absent:")) {
1940
3231
  return "auth_bypass_absent";
1941
3232
  }
3233
+ if (reason.startsWith("snapshot mismatch:")) {
3234
+ return "snapshot_mismatch";
3235
+ }
1942
3236
  if (reason.startsWith("missing module:")) {
1943
3237
  return "import_dependency_failure";
1944
3238
  }
@@ -1951,9 +3245,6 @@ function classifyBucketTypeFromReason(reason) {
1951
3245
  return "unknown_failure";
1952
3246
  }
1953
3247
  function synthesizeSharedBlockerBucket(args) {
1954
- if (args.errors === 0) {
1955
- return null;
1956
- }
1957
3248
  const visibleReasonGroups = /* @__PURE__ */ new Map();
1958
3249
  for (const item of args.visibleErrorItems) {
1959
3250
  const entry = visibleReasonGroups.get(item.reason);
@@ -1968,7 +3259,7 @@ function synthesizeSharedBlockerBucket(args) {
1968
3259
  items: [item]
1969
3260
  });
1970
3261
  }
1971
- const top = [...visibleReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
3262
+ const top = [...visibleReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
1972
3263
  const standaloneReasonGroups = /* @__PURE__ */ new Map();
1973
3264
  for (const classification of collectStandaloneErrorClassifications(args.input)) {
1974
3265
  const entry = standaloneReasonGroups.get(classification.reason);
@@ -1981,7 +3272,7 @@ function synthesizeSharedBlockerBucket(args) {
1981
3272
  group: classification.group
1982
3273
  });
1983
3274
  }
1984
- const standaloneTop = [...standaloneReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
3275
+ const standaloneTop = [...standaloneReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
1985
3276
  const visibleTopReason = top?.[0];
1986
3277
  const visibleTopStats = top?.[1];
1987
3278
  const standaloneTopReason = standaloneTop?.[0];
@@ -2020,6 +3311,12 @@ function synthesizeSharedBlockerBucket(args) {
2020
3311
  let hint;
2021
3312
  if (envVar) {
2022
3313
  hint = `Set ${envVar} (or pass --pgtest-dsn) before rerunning DB-isolated tests.`;
3314
+ } else if (effectiveReason.startsWith("configuration:")) {
3315
+ hint = "Fix the pytest configuration or conftest import error before rerunning the suite.";
3316
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
3317
+ hint = "Check shared state, worker startup, or resource contention between xdist workers before rerunning.";
3318
+ } else if (effectiveReason.startsWith("network:")) {
3319
+ hint = "Restore DNS, TLS, or outbound network access for the affected dependency before rerunning.";
2023
3320
  } else if (effectiveReason.startsWith("fixture guard:")) {
2024
3321
  hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
2025
3322
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -2034,6 +3331,12 @@ function synthesizeSharedBlockerBucket(args) {
2034
3331
  let headline;
2035
3332
  if (envVar) {
2036
3333
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors require ${envVar} for DB-isolated tests.`;
3334
+ } else if (effectiveReason.startsWith("configuration:")) {
3335
+ headline = `Shared blocker: ${atLeastPrefix}${countText} visible failure${countText === 1 ? "" : "s"} are caused by a pytest configuration error.`;
3336
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
3337
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by xdist worker crashes.`;
3338
+ } else if (effectiveReason.startsWith("network:")) {
3339
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by a network dependency failure.`;
2037
3340
  } else if (effectiveReason.startsWith("fixture guard:")) {
2038
3341
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
2039
3342
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -2064,11 +3367,17 @@ function synthesizeSharedBlockerBucket(args) {
2064
3367
  };
2065
3368
  }
2066
3369
  function synthesizeImportDependencyBucket(args) {
2067
- if (args.errors === 0) {
2068
- return null;
2069
- }
2070
- const importItems = args.visibleErrorItems.filter((item) => item.reason.startsWith("missing module:"));
2071
- if (importItems.length < 2) {
3370
+ const visibleImportItems = args.visibleErrorItems.filter(
3371
+ (item) => item.reason.startsWith("missing module:")
3372
+ );
3373
+ const inlineImportItems = chooseStrongestFailureItems(
3374
+ args.inlineItems.filter((item) => item.reason.startsWith("missing module:"))
3375
+ );
3376
+ const importItems = visibleImportItems.length > 0 ? visibleImportItems : inlineImportItems.map((item) => ({
3377
+ ...item,
3378
+ status: "failed"
3379
+ }));
3380
+ if (importItems.length === 0) {
2072
3381
  return null;
2073
3382
  }
2074
3383
  const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
@@ -2079,7 +3388,7 @@ function synthesizeImportDependencyBucket(args) {
2079
3388
  )
2080
3389
  ).slice(0, 6);
2081
3390
  const headlineCount = countClaimed ?? importItems.length;
2082
- const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible errors are caused by missing dependencies during test collection.`;
3391
+ const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible failure${headlineCount === 1 ? "" : "s"} are caused by missing dependencies during test collection.`;
2083
3392
  const summaryLines = [headline];
2084
3393
  if (modules.length > 0) {
2085
3394
  summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
@@ -2089,7 +3398,7 @@ function synthesizeImportDependencyBucket(args) {
2089
3398
  headline,
2090
3399
  countVisible: importItems.length,
2091
3400
  countClaimed,
2092
- reason: "missing dependencies during test collection",
3401
+ reason: modules.length === 1 ? `missing module: ${modules[0]}` : "missing dependencies during test collection",
2093
3402
  representativeItems: importItems.slice(0, 4).map((item) => ({
2094
3403
  label: item.label,
2095
3404
  reason: item.reason,
@@ -2108,7 +3417,7 @@ function synthesizeImportDependencyBucket(args) {
2108
3417
  };
2109
3418
  }
2110
3419
  function isContractDriftLabel(label) {
2111
- return /(freeze|snapshot|contract|manifest|openapi|golden)/i.test(label);
3420
+ return /(freeze|contract|manifest|openapi|golden)/i.test(label);
2112
3421
  }
2113
3422
  function looksLikeTaskKey(value) {
2114
3423
  return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
@@ -2239,23 +3548,81 @@ function synthesizeContractDriftBucket(args) {
2239
3548
  overflowLabel: "changed entities"
2240
3549
  };
2241
3550
  }
3551
+ function synthesizeSnapshotMismatchBucket(args) {
3552
+ const snapshotItems = chooseStrongestFailureItems(
3553
+ args.inlineItems.filter((item) => item.reason.startsWith("snapshot mismatch:"))
3554
+ );
3555
+ if (snapshotItems.length === 0) {
3556
+ return null;
3557
+ }
3558
+ const countClaimed = args.snapshotFailures && args.snapshotFailures >= snapshotItems.length ? args.snapshotFailures : void 0;
3559
+ const countText = countClaimed ?? snapshotItems.length;
3560
+ const summaryLines = [
3561
+ `Snapshot mismatches: ${formatCount2(countText, "snapshot expectation")} ${countText === 1 ? "is" : "are"} out of date with current output.`
3562
+ ];
3563
+ return {
3564
+ type: "snapshot_mismatch",
3565
+ headline: summaryLines[0],
3566
+ countVisible: snapshotItems.length,
3567
+ countClaimed,
3568
+ reason: "snapshot mismatch: snapshot expectations differ from current output",
3569
+ representativeItems: snapshotItems.slice(0, 4),
3570
+ entities: snapshotItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
3571
+ hint: "Update the snapshots if these output changes are intentional.",
3572
+ confidence: countClaimed ? 0.92 : 0.8,
3573
+ summaryLines,
3574
+ overflowCount: Math.max((countClaimed ?? snapshotItems.length) - Math.min(snapshotItems.length, 4), 0),
3575
+ overflowLabel: "snapshot failures"
3576
+ };
3577
+ }
3578
+ function synthesizeTimeoutBucket(args) {
3579
+ const timeoutItems = chooseStrongestFailureItems(
3580
+ args.inlineItems.filter((item) => item.reason.startsWith("timeout:"))
3581
+ );
3582
+ if (timeoutItems.length === 0) {
3583
+ return null;
3584
+ }
3585
+ const summaryLines = [
3586
+ `Timeout failures: ${formatCount2(timeoutItems.length, "test")} exceeded the configured timeout threshold.`
3587
+ ];
3588
+ return {
3589
+ type: "timeout_failure",
3590
+ headline: summaryLines[0],
3591
+ countVisible: timeoutItems.length,
3592
+ countClaimed: timeoutItems.length,
3593
+ reason: timeoutItems.length === 1 ? timeoutItems[0].reason : "timeout: tests exceeded the configured timeout threshold",
3594
+ representativeItems: timeoutItems.slice(0, 4),
3595
+ entities: timeoutItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
3596
+ hint: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning.",
3597
+ confidence: 0.84,
3598
+ summaryLines,
3599
+ overflowCount: Math.max(timeoutItems.length - Math.min(timeoutItems.length, 4), 0),
3600
+ overflowLabel: "timeout failures"
3601
+ };
3602
+ }
2242
3603
  function analyzeTestStatus(input) {
2243
- const passed = getCount(input, "passed");
2244
- const failed = getCount(input, "failed");
2245
- const errors = Math.max(getCount(input, "errors"), getCount(input, "error"));
2246
- const skipped = getCount(input, "skipped");
3604
+ const runner = detectTestRunner(input);
3605
+ const counts = extractTestStatusCounts(input, runner);
3606
+ const passed = counts.passed;
3607
+ const failed = counts.failed;
3608
+ const errors = counts.errors;
3609
+ const skipped = counts.skipped;
2247
3610
  const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
2248
- const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
3611
+ const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input) || /No test suite found in file/i.test(input) || /No test found in suite/i.test(input);
2249
3612
  const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
2250
3613
  const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
2251
3614
  const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
3615
+ const statusItems = collectInlineFailureItemsWithStatus(input);
2252
3616
  const visibleErrorItems = chooseStrongestStatusFailureItems([
2253
3617
  ...collectionItems.map((item) => ({
2254
3618
  ...item,
2255
3619
  status: "error"
2256
3620
  })),
2257
- ...collectInlineFailureItemsWithStatus(input).filter((item) => item.status === "error")
3621
+ ...statusItems.filter((item) => item.status === "error")
2258
3622
  ]);
3623
+ const visibleFailedItems = chooseStrongestStatusFailureItems(
3624
+ statusItems.filter((item) => item.status === "failed")
3625
+ );
2259
3626
  const labels = collectFailureLabels(input);
2260
3627
  const visibleErrorLabels = labels.filter((item) => item.status === "error").map((item) => item.label);
2261
3628
  const visibleFailedLabels = labels.filter((item) => item.status === "failed").map((item) => item.label);
@@ -2272,7 +3639,8 @@ function analyzeTestStatus(input) {
2272
3639
  if (!sharedBlocker) {
2273
3640
  const importDependencyBucket = synthesizeImportDependencyBucket({
2274
3641
  errors,
2275
- visibleErrorItems
3642
+ visibleErrorItems,
3643
+ inlineItems
2276
3644
  });
2277
3645
  if (importDependencyBucket) {
2278
3646
  buckets.push(importDependencyBucket);
@@ -2285,11 +3653,26 @@ function analyzeTestStatus(input) {
2285
3653
  if (contractDrift) {
2286
3654
  buckets.push(contractDrift);
2287
3655
  }
3656
+ const snapshotMismatch = synthesizeSnapshotMismatchBucket({
3657
+ inlineItems,
3658
+ snapshotFailures: counts.snapshotFailures
3659
+ });
3660
+ if (snapshotMismatch) {
3661
+ buckets.push(snapshotMismatch);
3662
+ }
3663
+ const timeoutBucket = synthesizeTimeoutBucket({
3664
+ inlineItems
3665
+ });
3666
+ if (timeoutBucket) {
3667
+ buckets.push(timeoutBucket);
3668
+ }
2288
3669
  return {
3670
+ runner,
2289
3671
  passed,
2290
3672
  failed,
2291
3673
  errors,
2292
3674
  skipped,
3675
+ snapshotFailures: counts.snapshotFailures,
2293
3676
  noTestsCollected,
2294
3677
  interrupted,
2295
3678
  collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
@@ -2298,6 +3681,7 @@ function analyzeTestStatus(input) {
2298
3681
  visibleErrorLabels,
2299
3682
  visibleFailedLabels,
2300
3683
  visibleErrorItems,
3684
+ visibleFailedItems,
2301
3685
  buckets
2302
3686
  };
2303
3687
  }
@@ -2319,114 +3703,666 @@ function testStatusHeuristic(input, detail = "standard") {
2319
3703
  if (detail === "focused") {
2320
3704
  return decision.focusedText;
2321
3705
  }
2322
- return decision.standardText;
3706
+ return decision.standardText;
3707
+ }
3708
+ return [
3709
+ "- Tests did not complete.",
3710
+ `- ${formatCount2(analysis.collectionErrorCount, "error")} occurred during collection.`,
3711
+ ...summarizeRepeatedTestCauses(input, {
3712
+ duringCollection: true
3713
+ })
3714
+ ].join("\n");
3715
+ }
3716
+ if (analysis.noTestsCollected) {
3717
+ return ["- Tests did not run.", "- Collected 0 items."].join("\n");
3718
+ }
3719
+ if (analysis.interrupted && analysis.failed === 0 && analysis.errors === 0) {
3720
+ return "- Test run was interrupted.";
3721
+ }
3722
+ if (analysis.failed === 0 && analysis.errors === 0 && analysis.passed > 0) {
3723
+ const details = [formatCount2(analysis.passed, "test")];
3724
+ if (analysis.skipped > 0) {
3725
+ details.push(formatCount2(analysis.skipped, "skip"));
3726
+ }
3727
+ return ["- Tests passed.", `- ${details.join(", ")}.`].join("\n");
3728
+ }
3729
+ if (analysis.failed > 0 || analysis.errors > 0 || analysis.inlineItems.length > 0 || analysis.buckets.length > 0) {
3730
+ const decision = buildTestStatusDiagnoseContract({
3731
+ input,
3732
+ analysis
3733
+ });
3734
+ if (detail === "verbose") {
3735
+ return decision.verboseText;
3736
+ }
3737
+ if (detail === "focused") {
3738
+ return decision.focusedText;
3739
+ }
3740
+ return decision.standardText;
3741
+ }
3742
+ return null;
3743
+ }
3744
+ function auditCriticalHeuristic(input) {
3745
+ if (/\bfound\s+0\s+vulnerabilities\b/i.test(input) || /\b0\s+vulnerabilities\b/i.test(input)) {
3746
+ return JSON.stringify(
3747
+ {
3748
+ status: "ok",
3749
+ vulnerabilities: [],
3750
+ summary: "No high or critical vulnerabilities found in the provided input."
3751
+ },
3752
+ null,
3753
+ 2
3754
+ );
3755
+ }
3756
+ const vulnerabilities = collectAuditCriticalVulnerabilities(input);
3757
+ if (vulnerabilities.length === 0) {
3758
+ return null;
3759
+ }
3760
+ const firstVulnerability = vulnerabilities[0];
3761
+ return JSON.stringify(
3762
+ {
3763
+ status: "ok",
3764
+ vulnerabilities,
3765
+ summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
3766
+ },
3767
+ null,
3768
+ 2
3769
+ );
3770
+ }
3771
+ function infraRiskHeuristic(input) {
3772
+ const destroyTargets = collectInfraDestroyTargets(input);
3773
+ const blockers = collectInfraBlockers(input);
3774
+ const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
3775
+ const riskEvidence = collectInfraRiskEvidence(input);
3776
+ if (riskEvidence.length > 0) {
3777
+ return JSON.stringify(
3778
+ {
3779
+ verdict: "fail",
3780
+ reason: "Destructive or clearly risky infrastructure change signals are present.",
3781
+ evidence: riskEvidence,
3782
+ destroy_count: inferInfraDestroyCount(input, destroyTargets),
3783
+ destroy_targets: destroyTargets,
3784
+ blockers
3785
+ },
3786
+ null,
3787
+ 2
3788
+ );
3789
+ }
3790
+ if (zeroDestructiveEvidence.length > 0) {
3791
+ return JSON.stringify(
3792
+ {
3793
+ verdict: "pass",
3794
+ reason: "The provided input explicitly indicates zero destructive changes.",
3795
+ evidence: zeroDestructiveEvidence,
3796
+ destroy_count: 0,
3797
+ destroy_targets: [],
3798
+ blockers: []
3799
+ },
3800
+ null,
3801
+ 2
3802
+ );
3803
+ }
3804
+ const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
3805
+ if (safeEvidence.length > 0) {
3806
+ return JSON.stringify(
3807
+ {
3808
+ verdict: "pass",
3809
+ reason: "The provided input explicitly indicates no risky infrastructure changes.",
3810
+ evidence: safeEvidence,
3811
+ destroy_count: 0,
3812
+ destroy_targets: [],
3813
+ blockers: []
3814
+ },
3815
+ null,
3816
+ 2
3817
+ );
3818
+ }
3819
+ return null;
3820
+ }
3821
+ function parseTscErrors(input) {
3822
+ const diagnostics = [];
3823
+ for (const rawLine of input.split("\n")) {
3824
+ const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").trimEnd();
3825
+ if (!line.trim()) {
3826
+ continue;
3827
+ }
3828
+ let match = line.match(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
3829
+ if (match) {
3830
+ diagnostics.push({
3831
+ file: match[1].replace(/\\/g, "/").trim(),
3832
+ line: Number(match[2]),
3833
+ column: Number(match[3]),
3834
+ code: match[4],
3835
+ message: match[5].trim()
3836
+ });
3837
+ continue;
3838
+ }
3839
+ match = line.match(/^(.+):(\d+):(\d+)\s+-\s+error\s+(TS\d+):\s+(.+)$/);
3840
+ if (match) {
3841
+ diagnostics.push({
3842
+ file: match[1].replace(/\\/g, "/").trim(),
3843
+ line: Number(match[2]),
3844
+ column: Number(match[3]),
3845
+ code: match[4],
3846
+ message: match[5].trim()
3847
+ });
3848
+ continue;
3849
+ }
3850
+ match = line.match(/^\s*error\s+(TS\d+):\s+(.+)$/);
3851
+ if (match) {
3852
+ diagnostics.push({
3853
+ file: null,
3854
+ line: null,
3855
+ column: null,
3856
+ code: match[1],
3857
+ message: match[2].trim()
3858
+ });
3859
+ }
3860
+ }
3861
+ return diagnostics;
3862
+ }
3863
+ function extractTscSummary(input) {
3864
+ const matches = [
3865
+ ...input.matchAll(/\bFound\s+(\d+)\s+errors?\b(?:\s+in\s+(\d+)\s+files?)?\.?/gi)
3866
+ ];
3867
+ const summary = matches.at(-1);
3868
+ if (!summary) {
3869
+ return null;
3870
+ }
3871
+ return {
3872
+ errorCount: Number(summary[1]),
3873
+ fileCount: summary[2] ? Number(summary[2]) : null
3874
+ };
3875
+ }
3876
+ function formatTscGroup(args) {
3877
+ const label = TSC_CODE_LABELS[args.code];
3878
+ const displayFiles = formatDisplayedFiles(args.files);
3879
+ let line = `- ${args.code}`;
3880
+ if (label) {
3881
+ line += ` (${label})`;
3882
+ }
3883
+ line += `: ${formatCount2(args.count, "occurrence")}`;
3884
+ if (displayFiles.length > 0) {
3885
+ line += ` across ${displayFiles.join(", ")}`;
3886
+ }
3887
+ return `${line}.`;
3888
+ }
3889
+ function typecheckSummaryHeuristic(input) {
3890
+ if (input.trim().length === 0) {
3891
+ return null;
3892
+ }
3893
+ const diagnostics = parseTscErrors(input);
3894
+ const summary = extractTscSummary(input);
3895
+ const hasTscSignal = diagnostics.length > 0 || summary !== null || /\berror\s+TS\d+:/m.test(input);
3896
+ if (!hasTscSignal) {
3897
+ return null;
3898
+ }
3899
+ if (summary?.errorCount === 0) {
3900
+ return "No type errors.";
3901
+ }
3902
+ if (diagnostics.length === 0 && summary === null) {
3903
+ return null;
3904
+ }
3905
+ const errorCount = summary?.errorCount ?? diagnostics.length;
3906
+ const allFiles = new Set(
3907
+ diagnostics.map((diagnostic) => diagnostic.file).filter((file) => Boolean(file))
3908
+ );
3909
+ const fileCount = summary?.fileCount ?? (allFiles.size > 0 ? allFiles.size : null);
3910
+ const groups = /* @__PURE__ */ new Map();
3911
+ for (const diagnostic of diagnostics) {
3912
+ const group = groups.get(diagnostic.code) ?? {
3913
+ count: 0,
3914
+ files: /* @__PURE__ */ new Set()
3915
+ };
3916
+ group.count += 1;
3917
+ if (diagnostic.file) {
3918
+ group.files.add(diagnostic.file);
3919
+ }
3920
+ groups.set(diagnostic.code, group);
3921
+ }
3922
+ const bullets = [
3923
+ `- Typecheck failed: ${formatCount2(errorCount, "error")}${fileCount ? ` in ${formatCount2(fileCount, "file")}` : ""}.`
3924
+ ];
3925
+ const sortedGroups = [...groups.entries()].map(([code, group]) => ({
3926
+ code,
3927
+ count: group.count,
3928
+ files: group.files
3929
+ })).sort((left, right) => right.count - left.count || left.code.localeCompare(right.code));
3930
+ for (const group of sortedGroups.slice(0, 3)) {
3931
+ bullets.push(formatTscGroup(group));
3932
+ }
3933
+ if (sortedGroups.length > 3) {
3934
+ const overflowFiles = /* @__PURE__ */ new Set();
3935
+ for (const group of sortedGroups.slice(3)) {
3936
+ for (const file of group.files) {
3937
+ overflowFiles.add(file);
3938
+ }
3939
+ }
3940
+ let overflow = `- ${formatCount2(sortedGroups.length - 3, "more error code")}`;
3941
+ if (overflowFiles.size > 0) {
3942
+ overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
3943
+ }
3944
+ bullets.push(`${overflow}.`);
3945
+ }
3946
+ return bullets.join("\n");
3947
+ }
3948
+ function looksLikeEslintFileHeader(line) {
3949
+ if (line.trim().length === 0 || line.trim() !== line) {
3950
+ return false;
3951
+ }
3952
+ if (/^\s*[✖×x]\s+\d+\s+problems?\b/i.test(line) || /potentially\s+fixable/i.test(line) || /^\d+\s+problems?\b/i.test(line)) {
3953
+ return false;
3954
+ }
3955
+ const normalized = line.replace(/\\/g, "/");
3956
+ const pathLike = normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../") || /^[A-Za-z]:\//.test(normalized) || /^[A-Za-z0-9_.-]+\//.test(normalized);
3957
+ return pathLike && /\.[A-Za-z0-9]+$/.test(normalized);
3958
+ }
3959
+ function normalizeEslintRule(rule, message) {
3960
+ if (rule && rule.trim().length > 0) {
3961
+ return rule.trim();
3962
+ }
3963
+ if (/parsing error/i.test(message)) {
3964
+ return "parsing error";
3965
+ }
3966
+ if (/fatal/i.test(message)) {
3967
+ return "fatal error";
3968
+ }
3969
+ return "unclassified lint error";
3970
+ }
3971
+ function parseEslintStylish(input) {
3972
+ const violations = [];
3973
+ let currentFile = null;
3974
+ for (const rawLine of input.split("\n")) {
3975
+ const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").replace(/\r$/, "");
3976
+ if (looksLikeEslintFileHeader(line.trim())) {
3977
+ currentFile = line.trim().replace(/\\/g, "/");
3978
+ continue;
3979
+ }
3980
+ let match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)\s*$/);
3981
+ if (match) {
3982
+ violations.push({
3983
+ file: currentFile ?? "(unknown file)",
3984
+ line: Number(match[1]),
3985
+ column: Number(match[2]),
3986
+ severity: match[3],
3987
+ message: match[4].trim(),
3988
+ rule: normalizeEslintRule(match[5], match[4])
3989
+ });
3990
+ continue;
3991
+ }
3992
+ match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s*$/);
3993
+ if (match) {
3994
+ violations.push({
3995
+ file: currentFile ?? "(unknown file)",
3996
+ line: Number(match[1]),
3997
+ column: Number(match[2]),
3998
+ severity: match[3],
3999
+ message: match[4].trim(),
4000
+ rule: normalizeEslintRule(null, match[4])
4001
+ });
4002
+ }
4003
+ }
4004
+ return violations;
4005
+ }
4006
+ function extractEslintSummary(input) {
4007
+ const summaryMatches = [
4008
+ ...input.matchAll(
4009
+ /^\s*[✖×x]?\s*(\d+)\s+problems?\s+\((\d+)\s+errors?,\s+(\d+)\s+warnings?\)/gim
4010
+ )
4011
+ ];
4012
+ const summary = summaryMatches.at(-1);
4013
+ if (!summary) {
4014
+ return null;
4015
+ }
4016
+ const fixableMatch = input.match(
4017
+ /(\d+)\s+errors?\s+and\s+(\d+)\s+warnings?\s+(?:are|is)\s+potentially\s+fixable/i
4018
+ );
4019
+ return {
4020
+ problems: Number(summary[1]),
4021
+ errors: Number(summary[2]),
4022
+ warnings: Number(summary[3]),
4023
+ fixableProblems: fixableMatch ? Number(fixableMatch[1]) + Number(fixableMatch[2]) : null
4024
+ };
4025
+ }
4026
+ function formatLintGroup(args) {
4027
+ const totalErrors = args.errors;
4028
+ const totalWarnings = args.warnings;
4029
+ const displayFiles = formatDisplayedFiles(args.files);
4030
+ let detail = "";
4031
+ if (totalErrors > 0 && totalWarnings > 0) {
4032
+ detail = `${formatCount2(totalErrors, "error")}, ${formatCount2(totalWarnings, "warning")}`;
4033
+ } else if (totalErrors > 0) {
4034
+ detail = formatCount2(totalErrors, "error");
4035
+ } else {
4036
+ detail = formatCount2(totalWarnings, "warning");
4037
+ }
4038
+ let line = `- ${args.rule}: ${detail}`;
4039
+ if (displayFiles.length > 0) {
4040
+ line += ` across ${displayFiles.join(", ")}`;
4041
+ }
4042
+ return `${line}.`;
4043
+ }
4044
+ function lintFailuresHeuristic(input) {
4045
+ const trimmed = input.trim();
4046
+ if (trimmed.length === 0 || trimmed.startsWith("[") || trimmed.startsWith("{")) {
4047
+ return null;
4048
+ }
4049
+ const summary = extractEslintSummary(input);
4050
+ const violations = parseEslintStylish(input);
4051
+ if (summary === null && violations.length === 0) {
4052
+ return null;
4053
+ }
4054
+ if (summary?.problems === 0) {
4055
+ return "No lint failures.";
4056
+ }
4057
+ const problems = summary?.problems ?? violations.length;
4058
+ const errors = summary?.errors ?? countPattern(input, /^\s*\d+:\d+\s+error\b/gm);
4059
+ const warnings = summary?.warnings ?? countPattern(input, /^\s*\d+:\d+\s+warning\b/gm);
4060
+ const bullets = [];
4061
+ if (errors > 0) {
4062
+ let headline = `- Lint failed: ${formatCount2(problems, "problem")} (${formatCount2(errors, "error")}, ${formatCount2(warnings, "warning")}).`;
4063
+ if ((summary?.fixableProblems ?? 0) > 0) {
4064
+ headline += ` ${formatCount2(summary.fixableProblems, "problem")} potentially fixable with --fix.`;
4065
+ }
4066
+ bullets.push(headline);
4067
+ } else {
4068
+ bullets.push(`- No lint errors visible: ${formatCount2(warnings, "warning")}.`);
4069
+ }
4070
+ const groups = /* @__PURE__ */ new Map();
4071
+ for (const violation of violations) {
4072
+ const group = groups.get(violation.rule) ?? {
4073
+ errors: 0,
4074
+ warnings: 0,
4075
+ files: /* @__PURE__ */ new Set()
4076
+ };
4077
+ if (violation.severity === "error") {
4078
+ group.errors += 1;
4079
+ } else {
4080
+ group.warnings += 1;
4081
+ }
4082
+ group.files.add(violation.file);
4083
+ groups.set(violation.rule, group);
4084
+ }
4085
+ const sortedGroups = [...groups.entries()].map(([rule, group]) => ({
4086
+ rule,
4087
+ errors: group.errors,
4088
+ warnings: group.warnings,
4089
+ total: group.errors + group.warnings,
4090
+ files: group.files
4091
+ })).sort((left, right) => {
4092
+ const leftHasErrors = left.errors > 0 ? 1 : 0;
4093
+ const rightHasErrors = right.errors > 0 ? 1 : 0;
4094
+ return rightHasErrors - leftHasErrors || right.total - left.total || left.rule.localeCompare(right.rule);
4095
+ });
4096
+ for (const group of sortedGroups.slice(0, 3)) {
4097
+ bullets.push(formatLintGroup(group));
4098
+ }
4099
+ if (sortedGroups.length > 3) {
4100
+ const overflowFiles = /* @__PURE__ */ new Set();
4101
+ for (const group of sortedGroups.slice(3)) {
4102
+ for (const file of group.files) {
4103
+ overflowFiles.add(file);
4104
+ }
2323
4105
  }
2324
- return [
2325
- "- Tests did not complete.",
2326
- `- ${formatCount2(analysis.collectionErrorCount, "error")} occurred during collection.`,
2327
- ...summarizeRepeatedTestCauses(input, {
2328
- duringCollection: true
2329
- })
2330
- ].join("\n");
4106
+ let overflow = `- ${formatCount2(sortedGroups.length - 3, "more rule")}`;
4107
+ if (overflowFiles.size > 0) {
4108
+ overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
4109
+ }
4110
+ bullets.push(`${overflow}.`);
2331
4111
  }
2332
- if (analysis.noTestsCollected) {
2333
- return ["- Tests did not run.", "- Collected 0 items."].join("\n");
4112
+ return bullets.join("\n");
4113
+ }
4114
+ function stripAnsiText(input) {
4115
+ return input.replace(/\u001b\[[0-9;]*m/g, "");
4116
+ }
4117
+ function normalizeBuildPath(file) {
4118
+ return file.replace(/\\/g, "/").replace(/^\.\//, "").trim();
4119
+ }
4120
+ function trimTrailingSentencePunctuation(input) {
4121
+ return input.replace(/[.:]+$/, "").trim();
4122
+ }
4123
+ function containsKnownBuildFailureSignal(input) {
4124
+ return /^ERROR in /m.test(input) || /^(?:[✘✗]\s*)?\[ERROR\]\s+/m.test(input) || /^error(?:\[E\d+\])?:\s+/m.test(input) || /^.+?\.go:\d+:\d+:\s+\S+/m.test(input) || /^.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm):\d+:\d+:\s*error:\s+/m.test(input) || /\berror\s+TS\d+:/m.test(input) || /^\s*npm ERR!/m.test(input) || /\bERR_PNPM_/m.test(input) || /^\s*error Command failed/m.test(input);
4125
+ }
4126
+ function detectExplicitBuildSuccess(input) {
4127
+ if (containsKnownBuildFailureSignal(input)) {
4128
+ return false;
2334
4129
  }
2335
- if (analysis.interrupted && analysis.failed === 0 && analysis.errors === 0) {
2336
- return "- Test run was interrupted.";
4130
+ return /\bcompiled successfully\b/i.test(input) || /^\s*Build succeeded\.?\s*$/im.test(input) || /\bcompiled with 0 errors?\b/i.test(input);
4131
+ }
4132
+ function inferBuildFailureCategory(message) {
4133
+ if (/module not found|can't resolve|could not resolve|cannot find module|no required module provides package/i.test(
4134
+ message
4135
+ )) {
4136
+ return "module-resolution";
2337
4137
  }
2338
- if (analysis.failed === 0 && analysis.errors === 0 && analysis.passed > 0) {
2339
- const details = [formatCount2(analysis.passed, "test")];
2340
- if (analysis.skipped > 0) {
2341
- details.push(formatCount2(analysis.skipped, "skip"));
2342
- }
2343
- return ["- Tests passed.", `- ${details.join(", ")}.`].join("\n");
4138
+ if (/no matching export|does not provide an export named|missing export/i.test(message)) {
4139
+ return "missing-export";
2344
4140
  }
2345
- if (analysis.failed > 0 || analysis.errors > 0 || analysis.inlineItems.length > 0 || analysis.buckets.length > 0) {
2346
- const decision = buildTestStatusDiagnoseContract({
2347
- input,
2348
- analysis
2349
- });
2350
- if (detail === "verbose") {
2351
- return decision.verboseText;
4141
+ if (/cannot find name|cannot find value|not found in this scope|undefined:|undeclared identifier/i.test(
4142
+ message
4143
+ )) {
4144
+ return "undefined-identifier";
4145
+ }
4146
+ if (/syntax error|unexpected token|expected ['"`;)]|expected .* after expression/i.test(message)) {
4147
+ return "syntax";
4148
+ }
4149
+ if (/\bTS\d+\b/.test(message) || /type .* is not assignable|type error|no matching overload/i.test(message)) {
4150
+ return "type";
4151
+ }
4152
+ return "generic";
4153
+ }
4154
+ function buildFailureSuggestion(category) {
4155
+ switch (category) {
4156
+ case "module-resolution":
4157
+ return "Install the missing package or fix the import path.";
4158
+ case "missing-export":
4159
+ return "Check the export name in the source module.";
4160
+ case "undefined-identifier":
4161
+ return "Define or import the missing identifier.";
4162
+ case "syntax":
4163
+ return "Fix the syntax error at the indicated location.";
4164
+ case "type":
4165
+ return "Fix the type error at the indicated location.";
4166
+ case "wrapper":
4167
+ return "Check the underlying build tool output above.";
4168
+ default:
4169
+ return "Fix the first reported error and rebuild.";
4170
+ }
4171
+ }
4172
+ function formatBuildFailureOutput(match) {
4173
+ const message = trimTrailingSentencePunctuation(match.message);
4174
+ const suggestion = buildFailureSuggestion(match.category);
4175
+ const displayFile = match.file ? compactDisplayFile(match.file) : null;
4176
+ if (displayFile && match.line !== null) {
4177
+ return `Build failed: ${message} in ${displayFile}:${match.line}. Fix: ${suggestion}`;
4178
+ }
4179
+ if (displayFile) {
4180
+ return `Build failed: ${message} in ${displayFile}. Fix: ${suggestion}`;
4181
+ }
4182
+ return `Build failed: ${message}. Fix: ${suggestion}`;
4183
+ }
4184
+ function extractWebpackBuildFailure(input) {
4185
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
4186
+ for (let index = 0; index < lines.length; index += 1) {
4187
+ const match = lines[index]?.match(/^ERROR in (.+?)(?:\s+(\d+):(\d+))?$/);
4188
+ if (!match) {
4189
+ continue;
2352
4190
  }
2353
- if (detail === "focused") {
2354
- return decision.focusedText;
4191
+ const candidates = [];
4192
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
4193
+ const candidate = lines[cursor]?.trim();
4194
+ if (!candidate) {
4195
+ continue;
4196
+ }
4197
+ if (/^ERROR in /.test(candidate) || /compiled with \d+ errors?/i.test(candidate)) {
4198
+ break;
4199
+ }
4200
+ if (/^(?:>|\|)|^\d+\s+\|/.test(candidate)) {
4201
+ continue;
4202
+ }
4203
+ candidates.push(candidate);
2355
4204
  }
2356
- return decision.standardText;
4205
+ let message = "Compilation error";
4206
+ if (candidates.length > 0) {
4207
+ const preferred = candidates.find(
4208
+ (candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate) && inferBuildFailureCategory(candidate) !== "generic"
4209
+ ) ?? candidates.find(
4210
+ (candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate)
4211
+ ) ?? candidates[0];
4212
+ message = preferred ?? message;
4213
+ }
4214
+ return {
4215
+ message,
4216
+ file: normalizeBuildPath(match[1]),
4217
+ line: match[2] ? Number(match[2]) : null,
4218
+ column: match[3] ? Number(match[3]) : null,
4219
+ category: inferBuildFailureCategory(message)
4220
+ };
2357
4221
  }
2358
4222
  return null;
2359
4223
  }
2360
- function auditCriticalHeuristic(input) {
2361
- const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
2362
- if (!/\b(critical|high)\b/i.test(line)) {
2363
- return null;
4224
+ function extractViteImportAnalysisBuildFailure(input) {
4225
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trim());
4226
+ for (const line of lines) {
4227
+ const match = line.match(
4228
+ /^\[plugin:vite:import-analysis\]\s+Failed to resolve import\s+"([^"]+)"\s+from\s+"([^"]+)"/i
4229
+ );
4230
+ if (!match) {
4231
+ continue;
2364
4232
  }
2365
- const pkg = inferPackage(line);
2366
- if (!pkg) {
2367
- return null;
4233
+ return {
4234
+ message: `Failed to resolve import "${match[1]}"`,
4235
+ file: normalizeBuildPath(match[2]),
4236
+ line: null,
4237
+ column: null,
4238
+ category: "module-resolution"
4239
+ };
4240
+ }
4241
+ return null;
4242
+ }
4243
+ function extractEsbuildBuildFailure(input) {
4244
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
4245
+ for (let index = 0; index < lines.length; index += 1) {
4246
+ const match = lines[index]?.match(/^(?:[✘✗]\s*)?\[ERROR\]\s*(.+)$/);
4247
+ if (!match) {
4248
+ continue;
4249
+ }
4250
+ const message = match[1].replace(/^\[vite\]\s*/i, "").trim();
4251
+ let file = null;
4252
+ let line = null;
4253
+ let column = null;
4254
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
4255
+ const locationMatch = lines[cursor]?.trim().match(/^(.+?):(\d+):(\d+):$/);
4256
+ if (!locationMatch) {
4257
+ continue;
4258
+ }
4259
+ file = normalizeBuildPath(locationMatch[1]);
4260
+ line = Number(locationMatch[2]);
4261
+ column = Number(locationMatch[3]);
4262
+ break;
2368
4263
  }
2369
4264
  return {
2370
- package: pkg,
2371
- severity: inferSeverity(line),
2372
- remediation: inferRemediation(pkg)
4265
+ message,
4266
+ file,
4267
+ line,
4268
+ column,
4269
+ category: inferBuildFailureCategory(message)
2373
4270
  };
2374
- }).filter((item) => item !== null);
2375
- if (vulnerabilities.length === 0) {
2376
- return null;
2377
4271
  }
2378
- const firstVulnerability = vulnerabilities[0];
2379
- return JSON.stringify(
2380
- {
2381
- status: "ok",
2382
- vulnerabilities,
2383
- summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
2384
- },
2385
- null,
2386
- 2
2387
- );
4272
+ return null;
2388
4273
  }
2389
- function infraRiskHeuristic(input) {
2390
- const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
2391
- const riskEvidence = input.split("\n").map((line) => line.trim()).filter(
2392
- (line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
2393
- ).slice(0, 3);
2394
- if (riskEvidence.length > 0) {
2395
- return JSON.stringify(
2396
- {
2397
- verdict: "fail",
2398
- reason: "Destructive or clearly risky infrastructure change signals are present.",
2399
- evidence: riskEvidence
2400
- },
2401
- null,
2402
- 2
2403
- );
4274
+ function extractCargoBuildFailure(input) {
4275
+ if (!/^error(?:\[E\d+\])?:\s+/m.test(input) || !(/^\s*-->\s+/m.test(input) || /could not compile/i.test(input))) {
4276
+ return null;
2404
4277
  }
2405
- if (zeroDestructiveEvidence.length > 0) {
2406
- return JSON.stringify(
2407
- {
2408
- verdict: "pass",
2409
- reason: "The provided input explicitly indicates zero destructive changes.",
2410
- evidence: zeroDestructiveEvidence
2411
- },
2412
- null,
2413
- 2
2414
- );
4278
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
4279
+ for (let index = 0; index < lines.length; index += 1) {
4280
+ const match = lines[index]?.match(/^error(?:\[(E\d+)\])?:\s+(.+)$/);
4281
+ if (!match) {
4282
+ continue;
4283
+ }
4284
+ const code = match[1];
4285
+ const locationMatch = lines.slice(index + 1, index + 7).join("\n").match(/^\s*-->\s+(.+?):(\d+):(\d+)/m);
4286
+ return {
4287
+ message: code ? `${code}: ${match[2].trim()}` : match[2].trim(),
4288
+ file: locationMatch ? normalizeBuildPath(locationMatch[1]) : null,
4289
+ line: locationMatch ? Number(locationMatch[2]) : null,
4290
+ column: locationMatch ? Number(locationMatch[3]) : null,
4291
+ category: inferBuildFailureCategory(match[2])
4292
+ };
2415
4293
  }
2416
- const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
2417
- if (safeEvidence.length > 0) {
2418
- return JSON.stringify(
2419
- {
2420
- verdict: "pass",
2421
- reason: "The provided input explicitly indicates no risky infrastructure changes.",
2422
- evidence: safeEvidence
2423
- },
2424
- null,
2425
- 2
4294
+ return null;
4295
+ }
4296
+ function extractCompilerStyleBuildFailure(input) {
4297
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
4298
+ for (const rawLine of lines) {
4299
+ let match = rawLine.match(
4300
+ /^(.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm)):([0-9]+):([0-9]+):\s*error:\s+(.+)$/
2426
4301
  );
4302
+ if (match) {
4303
+ return {
4304
+ message: match[4].trim(),
4305
+ file: normalizeBuildPath(match[1]),
4306
+ line: Number(match[2]),
4307
+ column: Number(match[3]),
4308
+ category: inferBuildFailureCategory(match[4])
4309
+ };
4310
+ }
4311
+ match = rawLine.match(/^(.+?\.go):([0-9]+):([0-9]+):\s+(.+)$/);
4312
+ if (match && !/^\s*warning:/i.test(match[4])) {
4313
+ return {
4314
+ message: match[4].trim(),
4315
+ file: normalizeBuildPath(match[1]),
4316
+ line: Number(match[2]),
4317
+ column: Number(match[3]),
4318
+ category: inferBuildFailureCategory(match[4])
4319
+ };
4320
+ }
2427
4321
  }
2428
4322
  return null;
2429
4323
  }
4324
+ function extractTscBuildFailure(input) {
4325
+ const diagnostics = parseTscErrors(input);
4326
+ const first = diagnostics[0];
4327
+ if (!first) {
4328
+ return null;
4329
+ }
4330
+ return {
4331
+ message: `${first.code}: ${first.message}`,
4332
+ file: first.file,
4333
+ line: first.line,
4334
+ column: first.column,
4335
+ category: inferBuildFailureCategory(`${first.code}: ${first.message}`)
4336
+ };
4337
+ }
4338
+ function extractWrapperBuildFailure(input) {
4339
+ if (!/^\s*npm ERR!|\bERR_PNPM_|^\s*error Command failed/m.test(input)) {
4340
+ return null;
4341
+ }
4342
+ const npmCommandMatch = input.match(/^\s*npm ERR!\s+.*?\bbuild:\s+`([^`]+)`/m);
4343
+ const genericCommandMatch = input.match(/^\s*.+?\s+build:\s+`([^`]+)`/m);
4344
+ const command = npmCommandMatch?.[1] ?? genericCommandMatch?.[1] ?? null;
4345
+ return {
4346
+ message: command ? `build script \`${command}\` failed` : "the build script failed",
4347
+ file: null,
4348
+ line: null,
4349
+ column: null,
4350
+ category: "wrapper"
4351
+ };
4352
+ }
4353
+ function buildFailureHeuristic(input) {
4354
+ if (input.trim().length === 0) {
4355
+ return null;
4356
+ }
4357
+ if (detectExplicitBuildSuccess(input)) {
4358
+ return "Build succeeded.";
4359
+ }
4360
+ const match = extractViteImportAnalysisBuildFailure(input) ?? extractWebpackBuildFailure(input) ?? extractEsbuildBuildFailure(input) ?? extractCargoBuildFailure(input) ?? extractCompilerStyleBuildFailure(input) ?? extractTscBuildFailure(input) ?? extractWrapperBuildFailure(input);
4361
+ if (!match) {
4362
+ return null;
4363
+ }
4364
+ return formatBuildFailureOutput(match);
4365
+ }
2430
4366
  function applyHeuristicPolicy(policyName, input, detail) {
2431
4367
  if (!policyName) {
2432
4368
  return null;
@@ -2440,6 +4376,15 @@ function applyHeuristicPolicy(policyName, input, detail) {
2440
4376
  if (policyName === "test-status") {
2441
4377
  return testStatusHeuristic(input, detail);
2442
4378
  }
4379
+ if (policyName === "typecheck-summary") {
4380
+ return typecheckSummaryHeuristic(input);
4381
+ }
4382
+ if (policyName === "lint-failures") {
4383
+ return lintFailuresHeuristic(input);
4384
+ }
4385
+ if (policyName === "build-failure") {
4386
+ return buildFailureHeuristic(input);
4387
+ }
2443
4388
  return null;
2444
4389
  }
2445
4390
 
@@ -2462,8 +4407,8 @@ function buildInsufficientSignalOutput(input) {
2462
4407
  } else {
2463
4408
  hint = "Hint: the captured output did not contain a clear answer for this preset.";
2464
4409
  }
2465
- return `${INSUFFICIENT_SIGNAL_TEXT}
2466
- ${hint}`;
4410
+ const presetSuggestion = input.recognizedRunner && input.recognizedRunner !== "unknown" && input.presetName !== "test-status" ? `Hint: captured output looks like ${input.recognizedRunner} test output; try --preset test-status.` : null;
4411
+ return [INSUFFICIENT_SIGNAL_TEXT, hint, presetSuggestion].filter((value) => Boolean(value)).join("\n");
2467
4412
  }
2468
4413
 
2469
4414
  // src/core/run.ts
@@ -3127,6 +5072,138 @@ function escapeRegExp(value) {
3127
5072
  function unique2(values) {
3128
5073
  return [...new Set(values)];
3129
5074
  }
5075
+ var genericBucketSearchTerms = /* @__PURE__ */ new Set([
5076
+ "runtimeerror",
5077
+ "typeerror",
5078
+ "error",
5079
+ "exception",
5080
+ "failed",
5081
+ "failure",
5082
+ "visible failure",
5083
+ "failing tests",
5084
+ "setup failures",
5085
+ "runtime failure",
5086
+ "assertion failed",
5087
+ "network",
5088
+ "permission",
5089
+ "configuration"
5090
+ ]);
5091
+ function normalizeSearchTerm(value) {
5092
+ return value.replace(/^['"`]+|['"`]+$/g, "").trim();
5093
+ }
5094
+ function isHighSignalSearchTerm(term) {
5095
+ const normalized = normalizeSearchTerm(term);
5096
+ if (normalized.length < 4) {
5097
+ return false;
5098
+ }
5099
+ const lower = normalized.toLowerCase();
5100
+ if (genericBucketSearchTerms.has(lower)) {
5101
+ return false;
5102
+ }
5103
+ if (/^(runtime|type|assertion|network|permission|configuration)\b/i.test(normalized)) {
5104
+ return false;
5105
+ }
5106
+ return true;
5107
+ }
5108
+ function scoreSearchTerm(term) {
5109
+ const normalized = normalizeSearchTerm(term);
5110
+ let score = normalized.length;
5111
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(normalized)) {
5112
+ score += 80;
5113
+ }
5114
+ if (/^TS\d+$/.test(normalized)) {
5115
+ score += 70;
5116
+ }
5117
+ if (/^[45]\d\d\b/.test(normalized) || /\bHTTPError:\s*[45]\d\d\b/i.test(normalized)) {
5118
+ score += 60;
5119
+ }
5120
+ if (normalized.includes("/") || normalized.includes("\\")) {
5121
+ score += 50;
5122
+ }
5123
+ if (/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/.test(normalized)) {
5124
+ score += 40;
5125
+ }
5126
+ if (/['"`]/.test(term)) {
5127
+ score += 30;
5128
+ }
5129
+ if (normalized.includes("::")) {
5130
+ score += 25;
5131
+ }
5132
+ return score;
5133
+ }
5134
+ function collectCandidateSearchTerms(value) {
5135
+ const candidates = [];
5136
+ const normalized = value.trim();
5137
+ if (!normalized) {
5138
+ return candidates;
5139
+ }
5140
+ for (const match of normalized.matchAll(/['"`]([^'"`]{4,})['"`]/g)) {
5141
+ candidates.push(match[1]);
5142
+ }
5143
+ for (const match of normalized.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) {
5144
+ candidates.push(match[0]);
5145
+ }
5146
+ for (const match of normalized.matchAll(/\bTS\d+\b/g)) {
5147
+ candidates.push(match[0]);
5148
+ }
5149
+ for (const match of normalized.matchAll(/\bHTTPError:\s*[45]\d\d\b/gi)) {
5150
+ candidates.push(match[0]);
5151
+ }
5152
+ for (const match of normalized.matchAll(/\/[A-Za-z0-9_./:{}-]{4,}/g)) {
5153
+ candidates.push(match[0]);
5154
+ }
5155
+ for (const match of normalized.matchAll(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g)) {
5156
+ candidates.push(match[0]);
5157
+ }
5158
+ for (const match of normalized.matchAll(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/g)) {
5159
+ candidates.push(match[0]);
5160
+ }
5161
+ const detail = normalized.split(":").slice(1).join(":").trim();
5162
+ if (detail.length >= 8) {
5163
+ candidates.push(detail);
5164
+ }
5165
+ return candidates;
5166
+ }
5167
+ function extractBucketSearchTerms(args) {
5168
+ const sources = [
5169
+ args.bucket.root_cause,
5170
+ ...args.bucket.evidence,
5171
+ ...args.readTargets.filter((target) => target.bucket_index === args.bucket.bucket_index).flatMap((target) => [target.context_hint.search_hint ?? "", target.file])
5172
+ ];
5173
+ const prioritized = unique2(
5174
+ sources.flatMap((value) => collectCandidateSearchTerms(value)).filter(isHighSignalSearchTerm)
5175
+ ).sort((left, right) => {
5176
+ const delta = scoreSearchTerm(right) - scoreSearchTerm(left);
5177
+ if (delta !== 0) {
5178
+ return delta;
5179
+ }
5180
+ return left.localeCompare(right);
5181
+ });
5182
+ if (prioritized.length > 0) {
5183
+ return prioritized.slice(0, 6);
5184
+ }
5185
+ const fallbackTerms = unique2(
5186
+ [...args.bucket.evidence, args.bucket.root_cause].flatMap((value) => value.split(/->|:/).map((part) => normalizeSearchTerm(part))).filter(isHighSignalSearchTerm)
5187
+ );
5188
+ return fallbackTerms.slice(0, 4);
5189
+ }
5190
+ function clusterIndexes(indexes, maxGap = 12) {
5191
+ if (indexes.length === 0) {
5192
+ return [];
5193
+ }
5194
+ const clusters = [];
5195
+ let currentCluster = [indexes[0]];
5196
+ for (const index of indexes.slice(1)) {
5197
+ if (index - currentCluster[currentCluster.length - 1] <= maxGap) {
5198
+ currentCluster.push(index);
5199
+ continue;
5200
+ }
5201
+ clusters.push(currentCluster);
5202
+ currentCluster = [index];
5203
+ }
5204
+ clusters.push(currentCluster);
5205
+ return clusters;
5206
+ }
3130
5207
  function buildLineWindows(args) {
3131
5208
  const selected = /* @__PURE__ */ new Set();
3132
5209
  for (const index of args.indexes) {
@@ -3142,6 +5219,12 @@ function buildLineWindows(args) {
3142
5219
  }
3143
5220
  return [...selected].sort((left, right) => left - right).map((index) => args.lines[index]);
3144
5221
  }
5222
+ function buildPriorityLineGroup(args) {
5223
+ return unique2([
5224
+ ...args.indexes.map((index) => args.lines[index]).filter(Boolean),
5225
+ ...buildLineWindows(args)
5226
+ ]);
5227
+ }
3145
5228
  function collapseSelectedLines(args) {
3146
5229
  if (args.lines.length === 0) {
3147
5230
  return args.fallback();
@@ -3290,15 +5373,16 @@ function buildTestStatusRawSlice(args) {
3290
5373
  ) ? index : -1
3291
5374
  ).filter((index) => index >= 0);
3292
5375
  const bucketGroups = args.contract.main_buckets.map((bucket) => {
3293
- const bucketTerms = unique2(
3294
- [bucket.root_cause, ...bucket.evidence].map((value) => value.split(":").at(-1)?.trim() ?? value.trim()).filter((value) => value.length >= 4)
3295
- );
5376
+ const bucketTerms = extractBucketSearchTerms({
5377
+ bucket,
5378
+ readTargets: args.contract.read_targets
5379
+ });
3296
5380
  const indexes = lines.map(
3297
5381
  (line, index) => bucketTerms.some((term) => new RegExp(escapeRegExp(term), "i").test(line)) ? index : -1
3298
5382
  ).filter((index) => index >= 0);
3299
5383
  return unique2([
3300
5384
  ...indexes.map((index) => lines[index]).filter(Boolean),
3301
- ...buildLineWindows({
5385
+ ...buildPriorityLineGroup({
3302
5386
  lines,
3303
5387
  indexes,
3304
5388
  radius: 2,
@@ -3306,26 +5390,55 @@ function buildTestStatusRawSlice(args) {
3306
5390
  })
3307
5391
  ]);
3308
5392
  });
3309
- const targetGroups = args.contract.read_targets.map(
3310
- (target) => buildLineWindows({
5393
+ const targetGroups = args.contract.read_targets.flatMap((target) => {
5394
+ const searchHintIndexes = findSearchHintIndexes({
3311
5395
  lines,
3312
- indexes: unique2([
3313
- ...findReadTargetIndexes({
3314
- lines,
3315
- file: target.file,
3316
- line: target.line,
3317
- contextHint: target.context_hint
3318
- }),
3319
- ...findSearchHintIndexes({
3320
- lines,
3321
- searchHint: target.context_hint.search_hint
3322
- })
3323
- ]),
3324
- radius: target.line === null ? 1 : 2,
3325
- maxLines: target.line === null ? 6 : 8
5396
+ searchHint: target.context_hint.search_hint
5397
+ });
5398
+ const fileIndexes = findReadTargetIndexes({
5399
+ lines,
5400
+ file: target.file,
5401
+ line: target.line,
5402
+ contextHint: target.context_hint
5403
+ });
5404
+ const radius = target.line === null ? 1 : 2;
5405
+ const maxLines = target.line === null ? 6 : 8;
5406
+ const groups = [
5407
+ searchHintIndexes.length > 0 ? buildPriorityLineGroup({
5408
+ lines,
5409
+ indexes: searchHintIndexes,
5410
+ radius,
5411
+ maxLines
5412
+ }) : null,
5413
+ fileIndexes.length > 0 ? buildPriorityLineGroup({
5414
+ lines,
5415
+ indexes: fileIndexes,
5416
+ radius,
5417
+ maxLines
5418
+ }) : null
5419
+ ].filter((group) => group !== null && group.length > 0);
5420
+ if (groups.length > 0) {
5421
+ return groups;
5422
+ }
5423
+ return [
5424
+ buildPriorityLineGroup({
5425
+ lines,
5426
+ indexes: unique2([...searchHintIndexes, ...fileIndexes]),
5427
+ radius,
5428
+ maxLines
5429
+ })
5430
+ ];
5431
+ });
5432
+ const failureHeaderIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) ? index : -1).filter((index) => index >= 0);
5433
+ const failureIndexes = (failureHeaderIndexes.length > 0 ? failureHeaderIndexes : lines.map((line, index) => /^E\s/.test(line) ? index : -1).filter((index) => index >= 0)).filter((index) => index >= 0);
5434
+ const failureHeaderGroups = clusterIndexes(failureIndexes).slice(0, 8).map(
5435
+ (cluster) => buildPriorityLineGroup({
5436
+ lines,
5437
+ indexes: cluster,
5438
+ radius: 1,
5439
+ maxLines: 8
3326
5440
  })
3327
- );
3328
- const failureIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) || /^E\s/.test(line) ? index : -1).filter((index) => index >= 0);
5441
+ ).filter((group) => group.length > 0);
3329
5442
  const selected = collapseSelectedLineGroups({
3330
5443
  groups: [
3331
5444
  ...targetGroups,
@@ -3339,12 +5452,14 @@ function buildTestStatusRawSlice(args) {
3339
5452
  })
3340
5453
  ]),
3341
5454
  ...bucketGroups,
3342
- buildLineWindows({
3343
- lines,
3344
- indexes: failureIndexes,
3345
- radius: 1,
3346
- maxLines: 24
3347
- })
5455
+ ...failureHeaderGroups.length > 0 ? failureHeaderGroups : [
5456
+ buildLineWindows({
5457
+ lines,
5458
+ indexes: failureIndexes,
5459
+ radius: 1,
5460
+ maxLines: 24
5461
+ })
5462
+ ]
3348
5463
  ],
3349
5464
  maxInputChars: args.config.maxInputChars,
3350
5465
  fallback: () => truncateInput(args.input, {
@@ -3485,7 +5600,8 @@ function withInsufficientHint(args) {
3485
5600
  return buildInsufficientSignalOutput({
3486
5601
  presetName: args.request.presetName,
3487
5602
  originalLength: args.prepared.meta.originalLength,
3488
- truncatedApplied: args.prepared.meta.truncatedApplied
5603
+ truncatedApplied: args.prepared.meta.truncatedApplied,
5604
+ recognizedRunner: detectTestRunner(args.prepared.redacted)
3489
5605
  });
3490
5606
  }
3491
5607
  async function generateWithRetry(args) {
@@ -3545,6 +5661,38 @@ function renderTestStatusDecisionOutput(args) {
3545
5661
  return args.decision.standardText;
3546
5662
  }
3547
5663
  function buildTestStatusProviderFailureDecision(args) {
5664
+ const concreteReadTarget = args.baseDecision.contract.read_targets.find(
5665
+ (target) => Boolean(target.file)
5666
+ );
5667
+ const hasUnknownBucket = args.baseDecision.contract.main_buckets.some(
5668
+ (bucket) => bucket.root_cause.startsWith("unknown ")
5669
+ );
5670
+ if (concreteReadTarget && !hasUnknownBucket) {
5671
+ return buildTestStatusDiagnoseContract({
5672
+ input: args.input,
5673
+ analysis: args.analysis,
5674
+ resolvedTests: args.baseDecision.contract.resolved_tests,
5675
+ remainingTests: args.baseDecision.contract.remaining_tests,
5676
+ contractOverrides: {
5677
+ ...args.baseDecision.contract,
5678
+ diagnosis_complete: false,
5679
+ raw_needed: false,
5680
+ additional_source_read_likely_low_value: false,
5681
+ read_raw_only_if: null,
5682
+ decision: "read_source",
5683
+ provider_used: true,
5684
+ provider_confidence: null,
5685
+ provider_failed: true,
5686
+ raw_slice_used: args.rawSliceUsed,
5687
+ raw_slice_strategy: args.rawSliceStrategy,
5688
+ next_best_action: {
5689
+ code: "read_source_for_bucket",
5690
+ bucket_index: args.baseDecision.contract.dominant_blocker_bucket_index ?? concreteReadTarget.bucket_index,
5691
+ note: `Provider follow-up failed (${args.reason}). The heuristic anchor is concrete enough to inspect source for the current bucket before reading raw traceback.`
5692
+ }
5693
+ }
5694
+ });
5695
+ }
3548
5696
  const shouldZoomFirst = args.request.detail !== "verbose";
3549
5697
  return buildTestStatusDiagnoseContract({
3550
5698
  input: args.input,
@@ -3571,7 +5719,7 @@ function buildTestStatusProviderFailureDecision(args) {
3571
5719
  }
3572
5720
  });
3573
5721
  }
3574
- async function runSift(request) {
5722
+ async function runSiftCore(request, recorder) {
3575
5723
  const prepared = prepareInput(request.stdin, request.config.input);
3576
5724
  const heuristicInput = prepared.redacted;
3577
5725
  const heuristicInputTruncated = false;
@@ -3657,6 +5805,7 @@ async function runSift(request) {
3657
5805
  finalOutput
3658
5806
  });
3659
5807
  }
5808
+ recorder?.heuristic();
3660
5809
  return finalOutput;
3661
5810
  }
3662
5811
  if (testStatusDecision && testStatusAnalysis) {
@@ -3756,6 +5905,7 @@ async function runSift(request) {
3756
5905
  providerInputChars: providerPrepared2.truncated.length,
3757
5906
  providerOutputChars: result.text.length
3758
5907
  });
5908
+ recorder?.provider(result.usage);
3759
5909
  return finalOutput;
3760
5910
  } catch (error) {
3761
5911
  const reason = error instanceof Error ? error.message : "unknown_error";
@@ -3790,6 +5940,7 @@ async function runSift(request) {
3790
5940
  rawSliceChars: rawSlice.text.length,
3791
5941
  providerInputChars: providerPrepared2.truncated.length
3792
5942
  });
5943
+ recorder?.fallback();
3793
5944
  return finalOutput;
3794
5945
  }
3795
5946
  }
@@ -3846,6 +5997,7 @@ async function runSift(request) {
3846
5997
  })) {
3847
5998
  throw new Error("Model output rejected by quality gate");
3848
5999
  }
6000
+ recorder?.provider(result.usage);
3849
6001
  return withInsufficientHint({
3850
6002
  output: normalizeOutput(result.text, providerPrompt.responseMode),
3851
6003
  request,
@@ -3853,6 +6005,7 @@ async function runSift(request) {
3853
6005
  });
3854
6006
  } catch (error) {
3855
6007
  const reason = error instanceof Error ? error.message : "unknown_error";
6008
+ recorder?.fallback();
3856
6009
  return withInsufficientHint({
3857
6010
  output: buildFallbackOutput({
3858
6011
  format: request.format,
@@ -3866,6 +6019,72 @@ async function runSift(request) {
3866
6019
  });
3867
6020
  }
3868
6021
  }
6022
+ async function runSift(request) {
6023
+ return runSiftCore(request);
6024
+ }
6025
+ async function runSiftWithStats(request) {
6026
+ if (request.dryRun) {
6027
+ return {
6028
+ output: await runSiftCore(request),
6029
+ stats: null
6030
+ };
6031
+ }
6032
+ const startedAt = Date.now();
6033
+ let layer = "fallback";
6034
+ let providerCalled = false;
6035
+ let totalTokens = null;
6036
+ const output = await runSiftCore(request, {
6037
+ heuristic() {
6038
+ layer = "heuristic";
6039
+ providerCalled = false;
6040
+ totalTokens = null;
6041
+ },
6042
+ provider(usage) {
6043
+ layer = "provider";
6044
+ providerCalled = true;
6045
+ totalTokens = usage?.totalTokens ?? null;
6046
+ },
6047
+ fallback() {
6048
+ layer = "fallback";
6049
+ providerCalled = true;
6050
+ totalTokens = null;
6051
+ }
6052
+ });
6053
+ return {
6054
+ output,
6055
+ stats: {
6056
+ layer,
6057
+ providerCalled,
6058
+ totalTokens,
6059
+ durationMs: Date.now() - startedAt,
6060
+ presetName: request.presetName
6061
+ }
6062
+ };
6063
+ }
6064
+
6065
+ // src/core/stats.ts
6066
+ import pc2 from "picocolors";
6067
+ function formatDuration(durationMs) {
6068
+ return durationMs >= 1e3 ? `${(durationMs / 1e3).toFixed(1)}s` : `${durationMs}ms`;
6069
+ }
6070
+ function formatStatsFooter(stats) {
6071
+ const duration = formatDuration(stats.durationMs);
6072
+ if (stats.layer === "heuristic") {
6073
+ return `[sift: heuristic \u2022 LLM skipped \u2022 summary ${duration}]`;
6074
+ }
6075
+ if (stats.layer === "provider") {
6076
+ const tokenSegment = stats.totalTokens !== null ? ` \u2022 ${stats.totalTokens} tokens` : "";
6077
+ return `[sift: provider \u2022 LLM used${tokenSegment} \u2022 summary ${duration}]`;
6078
+ }
6079
+ return `[sift: fallback \u2022 provider failed \u2022 summary ${duration}]`;
6080
+ }
6081
+ function emitStatsFooter(args) {
6082
+ if (args.quiet || !args.stats || !process.stderr.isTTY) {
6083
+ return;
6084
+ }
6085
+ process.stderr.write(`${pc2.dim(formatStatsFooter(args.stats))}
6086
+ `);
6087
+ }
3869
6088
 
3870
6089
  // src/core/testStatusState.ts
3871
6090
  import fs from "fs";
@@ -3875,10 +6094,29 @@ var detailSchema = z2.enum(["standard", "focused", "verbose"]);
3875
6094
  var failureBucketTypeSchema = z2.enum([
3876
6095
  "shared_environment_blocker",
3877
6096
  "fixture_guard_failure",
6097
+ "timeout_failure",
6098
+ "permission_denied_failure",
6099
+ "async_event_loop_failure",
6100
+ "fixture_teardown_failure",
6101
+ "db_migration_failure",
6102
+ "configuration_error",
6103
+ "xdist_worker_crash",
6104
+ "type_error_failure",
6105
+ "resource_leak_warning",
6106
+ "django_db_access_denied",
6107
+ "network_failure",
6108
+ "subprocess_crash_segfault",
6109
+ "flaky_test_detected",
6110
+ "serialization_encoding_failure",
6111
+ "file_not_found_failure",
6112
+ "memory_error",
6113
+ "deprecation_warning_as_error",
6114
+ "xfail_strict_unexpected_pass",
3878
6115
  "service_unavailable",
3879
6116
  "db_connection_failure",
3880
6117
  "auth_bypass_absent",
3881
6118
  "contract_snapshot_drift",
6119
+ "snapshot_mismatch",
3882
6120
  "import_dependency_failure",
3883
6121
  "collection_failure",
3884
6122
  "assertion_failure",
@@ -4548,7 +6786,7 @@ async function runExec(request) {
4548
6786
  const previousCachedRun = shouldCacheTestStatusBase ? tryReadCachedTestStatusRun() : null;
4549
6787
  if (request.config.runtime.verbose) {
4550
6788
  process.stderr.write(
4551
- `${pc2.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
6789
+ `${pc3.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
4552
6790
  `
4553
6791
  );
4554
6792
  }
@@ -4577,7 +6815,7 @@ async function runExec(request) {
4577
6815
  }
4578
6816
  bypassed = true;
4579
6817
  if (request.config.runtime.verbose) {
4580
- process.stderr.write(`${pc2.dim("sift")} bypass=interactive-prompt
6818
+ process.stderr.write(`${pc3.dim("sift")} bypass=interactive-prompt
4581
6819
  `);
4582
6820
  }
4583
6821
  process.stderr.write(capture.render());
@@ -4606,15 +6844,16 @@ async function runExec(request) {
4606
6844
  const shouldCacheTestStatus = shouldCacheTestStatusBase && !useWatchFlow;
4607
6845
  if (request.config.runtime.verbose) {
4608
6846
  process.stderr.write(
4609
- `${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
6847
+ `${pc3.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
4610
6848
  `
4611
6849
  );
4612
6850
  }
4613
6851
  if (autoWatchDetected) {
4614
- process.stderr.write(`${pc2.dim("sift")} auto-watch=detected
6852
+ process.stderr.write(`${pc3.dim("sift")} auto-watch=detected
4615
6853
  `);
4616
6854
  }
4617
6855
  if (!bypassed) {
6856
+ const reductionStartedAt = Date.now();
4618
6857
  if (request.showRaw && capturedOutput.length > 0) {
4619
6858
  process.stderr.write(capturedOutput);
4620
6859
  if (!capturedOutput.endsWith("\n")) {
@@ -4629,12 +6868,22 @@ async function runExec(request) {
4629
6868
  if (execSuccessShortcut && !request.dryRun) {
4630
6869
  if (request.config.runtime.verbose) {
4631
6870
  process.stderr.write(
4632
- `${pc2.dim("sift")} exec_shortcut=${request.presetName}
6871
+ `${pc3.dim("sift")} exec_shortcut=${request.presetName}
4633
6872
  `
4634
6873
  );
4635
6874
  }
4636
6875
  process.stdout.write(`${execSuccessShortcut}
4637
6876
  `);
6877
+ emitStatsFooter({
6878
+ stats: {
6879
+ layer: "heuristic",
6880
+ providerCalled: false,
6881
+ totalTokens: null,
6882
+ durationMs: Date.now() - reductionStartedAt,
6883
+ presetName: request.presetName
6884
+ },
6885
+ quiet: Boolean(request.quiet)
6886
+ });
4638
6887
  return exitCode;
4639
6888
  }
4640
6889
  if (useWatchFlow) {
@@ -4647,7 +6896,8 @@ async function runExec(request) {
4647
6896
  presetName: request.presetName,
4648
6897
  originalLength: capture.getTotalChars(),
4649
6898
  truncatedApplied: capture.wasTruncated(),
4650
- exitCode
6899
+ exitCode,
6900
+ recognizedRunner: detectTestRunner(capturedOutput)
4651
6901
  });
4652
6902
  }
4653
6903
  process.stdout.write(`${output2}
@@ -4675,7 +6925,7 @@ async function runExec(request) {
4675
6925
  previous: previousCachedRun,
4676
6926
  current: currentCachedRun
4677
6927
  }) : null;
4678
- let output = await runSift({
6928
+ const result = await runSiftWithStats({
4679
6929
  ...request,
4680
6930
  stdin: capturedOutput,
4681
6931
  analysisContext: request.skipCacheWrite && request.presetName === "test-status" ? [
@@ -4694,13 +6944,15 @@ async function runExec(request) {
4694
6944
  )
4695
6945
  } : request.testStatusContext
4696
6946
  });
6947
+ let output = result.output;
4697
6948
  if (shouldCacheTestStatus) {
4698
6949
  if (isInsufficientSignalOutput(output)) {
4699
6950
  output = buildInsufficientSignalOutput({
4700
6951
  presetName: request.presetName,
4701
6952
  originalLength: capture.getTotalChars(),
4702
6953
  truncatedApplied: capture.wasTruncated(),
4703
- exitCode
6954
+ exitCode,
6955
+ recognizedRunner: detectTestRunner(capturedOutput)
4704
6956
  });
4705
6957
  }
4706
6958
  if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
@@ -4733,7 +6985,7 @@ ${output}`;
4733
6985
  } catch (error) {
4734
6986
  if (request.config.runtime.verbose) {
4735
6987
  const reason = error instanceof Error ? error.message : "unknown_error";
4736
- process.stderr.write(`${pc2.dim("sift")} cache_write=failed reason=${reason}
6988
+ process.stderr.write(`${pc3.dim("sift")} cache_write=failed reason=${reason}
4737
6989
  `);
4738
6990
  }
4739
6991
  }
@@ -4743,11 +6995,16 @@ ${output}`;
4743
6995
  presetName: request.presetName,
4744
6996
  originalLength: capture.getTotalChars(),
4745
6997
  truncatedApplied: capture.wasTruncated(),
4746
- exitCode
6998
+ exitCode,
6999
+ recognizedRunner: detectTestRunner(capturedOutput)
4747
7000
  });
4748
7001
  }
4749
7002
  process.stdout.write(`${output}
4750
7003
  `);
7004
+ emitStatsFooter({
7005
+ stats: result.stats,
7006
+ quiet: Boolean(request.quiet)
7007
+ });
4751
7008
  if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
4752
7009
  presetName: request.presetName,
4753
7010
  output
@@ -5107,5 +7364,6 @@ export {
5107
7364
  normalizeChildExitCode,
5108
7365
  resolveConfig,
5109
7366
  runExec,
5110
- runSift
7367
+ runSift,
7368
+ runSiftWithStats
5111
7369
  };