@bilalimamoglu/sift 0.3.0 → 0.3.2

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
@@ -62,7 +62,7 @@ function evaluateGate(args) {
62
62
  // src/core/testStatusDecision.ts
63
63
  import { z } from "zod";
64
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[]}';
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,"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string}}';
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([
68
68
  "fix_dominant_blocker",
@@ -80,6 +80,20 @@ var testStatusProviderSupplementSchema = z.object({
80
80
  read_raw_only_if: z.string().nullable(),
81
81
  decision: z.enum(["stop", "zoom", "read_source", "read_raw"]),
82
82
  provider_confidence: z.number().min(0).max(1).nullable(),
83
+ bucket_supplements: z.array(
84
+ z.object({
85
+ label: z.string().min(1),
86
+ count: z.number().int().positive(),
87
+ root_cause: z.string().min(1),
88
+ anchor: z.object({
89
+ file: z.string().nullable(),
90
+ line: z.number().int().nullable(),
91
+ search_hint: z.string().nullable()
92
+ }),
93
+ fix_hint: z.string().nullable(),
94
+ confidence: z.number().min(0).max(1)
95
+ })
96
+ ).max(2),
83
97
  next_best_action: nextBestActionSchema
84
98
  });
85
99
  var testStatusDiagnoseContractSchema = z.object({
@@ -152,6 +166,209 @@ var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.om
152
166
  function parseTestStatusProviderSupplement(input) {
153
167
  return testStatusProviderSupplementSchema.parse(JSON.parse(input));
154
168
  }
169
+ var extendedBucketSpecs = [
170
+ {
171
+ prefix: "snapshot mismatch:",
172
+ type: "snapshot_mismatch",
173
+ label: "snapshot mismatch",
174
+ genericTitle: "Snapshot mismatches",
175
+ defaultCoverage: "failed",
176
+ rootCauseConfidence: 0.84,
177
+ why: "it contains the failing snapshot expectation behind this bucket",
178
+ fix: "Update the snapshots if these output changes are intentional, then rerun the suite."
179
+ },
180
+ {
181
+ prefix: "timeout:",
182
+ type: "timeout_failure",
183
+ label: "timeout",
184
+ genericTitle: "Timeout failures",
185
+ defaultCoverage: "mixed",
186
+ rootCauseConfidence: 0.9,
187
+ why: "it contains the test or fixture that exceeded the timeout threshold",
188
+ fix: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning."
189
+ },
190
+ {
191
+ prefix: "permission:",
192
+ type: "permission_denied_failure",
193
+ label: "permission denied",
194
+ genericTitle: "Permission failures",
195
+ defaultCoverage: "error",
196
+ rootCauseConfidence: 0.85,
197
+ why: "it contains the file, socket, or port access that was denied",
198
+ fix: "Check file or port permissions in the CI environment before rerunning."
199
+ },
200
+ {
201
+ prefix: "async loop:",
202
+ type: "async_event_loop_failure",
203
+ label: "async event loop",
204
+ genericTitle: "Async event loop failures",
205
+ defaultCoverage: "mixed",
206
+ rootCauseConfidence: 0.88,
207
+ why: "it contains the async setup or coroutine that caused the event loop error",
208
+ fix: "Check event loop scope and pytest-asyncio configuration before rerunning."
209
+ },
210
+ {
211
+ prefix: "fixture teardown:",
212
+ type: "fixture_teardown_failure",
213
+ label: "fixture teardown",
214
+ genericTitle: "Fixture teardown failures",
215
+ defaultCoverage: "error",
216
+ rootCauseConfidence: 0.85,
217
+ why: "it contains the fixture teardown path that failed after the test body completed",
218
+ fix: "Inspect the teardown cleanup path and restore idempotent fixture cleanup before rerunning."
219
+ },
220
+ {
221
+ prefix: "db migration:",
222
+ type: "db_migration_failure",
223
+ label: "db migration",
224
+ genericTitle: "DB migration failures",
225
+ defaultCoverage: "error",
226
+ rootCauseConfidence: 0.9,
227
+ why: "it contains the migration or model definition behind the missing table or relation",
228
+ fix: "Run pending migrations or fix the expected model schema before rerunning."
229
+ },
230
+ {
231
+ prefix: "configuration:",
232
+ type: "configuration_error",
233
+ label: "configuration error",
234
+ genericTitle: "Configuration errors",
235
+ defaultCoverage: "error",
236
+ rootCauseConfidence: 0.95,
237
+ dominantPriority: 4,
238
+ dominantBlocker: true,
239
+ why: "it contains the pytest configuration or conftest setup error that blocks the run",
240
+ fix: "Fix the pytest configuration, CLI usage, or conftest import error before rerunning."
241
+ },
242
+ {
243
+ prefix: "xdist worker crash:",
244
+ type: "xdist_worker_crash",
245
+ label: "xdist worker crash",
246
+ genericTitle: "xdist worker crashes",
247
+ defaultCoverage: "error",
248
+ rootCauseConfidence: 0.92,
249
+ dominantPriority: 3,
250
+ why: "it contains the worker startup or shared-state path that crashed an xdist worker",
251
+ fix: "Check shared state, worker startup hooks, or resource contention between workers before rerunning."
252
+ },
253
+ {
254
+ prefix: "type error:",
255
+ type: "type_error_failure",
256
+ label: "type error",
257
+ genericTitle: "Type errors",
258
+ defaultCoverage: "mixed",
259
+ rootCauseConfidence: 0.8,
260
+ why: "it contains the call site or fixture value that triggered the type error",
261
+ fix: "Inspect the mismatched argument or object shape and rerun the full suite at standard."
262
+ },
263
+ {
264
+ prefix: "resource leak:",
265
+ type: "resource_leak_warning",
266
+ label: "resource leak",
267
+ genericTitle: "Resource leak warnings",
268
+ defaultCoverage: "mixed",
269
+ rootCauseConfidence: 0.74,
270
+ why: "it contains the warning source behind the leaked file, socket, or coroutine",
271
+ fix: "Close the leaked resource or suppress the warning only if the cleanup is intentional."
272
+ },
273
+ {
274
+ prefix: "django db access:",
275
+ type: "django_db_access_denied",
276
+ label: "django db access",
277
+ genericTitle: "Django DB access failures",
278
+ defaultCoverage: "error",
279
+ rootCauseConfidence: 0.95,
280
+ why: "it needs the @pytest.mark.django_db decorator or fixture permission to access the database",
281
+ fix: "Add @pytest.mark.django_db to the test or class before rerunning."
282
+ },
283
+ {
284
+ prefix: "network:",
285
+ type: "network_failure",
286
+ label: "network failure",
287
+ genericTitle: "Network failures",
288
+ defaultCoverage: "error",
289
+ rootCauseConfidence: 0.88,
290
+ dominantPriority: 2,
291
+ why: "it contains the host, URL, or TLS path behind the network failure",
292
+ fix: "Check DNS, outbound network access, retries, or TLS trust before rerunning."
293
+ },
294
+ {
295
+ prefix: "segfault:",
296
+ type: "subprocess_crash_segfault",
297
+ label: "segfault",
298
+ genericTitle: "Segfault crashes",
299
+ defaultCoverage: "mixed",
300
+ rootCauseConfidence: 0.8,
301
+ why: "it contains the subprocess or native extension path that crashed with SIGSEGV",
302
+ fix: "Inspect the native extension, subprocess boundary, or incompatible binary before rerunning."
303
+ },
304
+ {
305
+ prefix: "flaky:",
306
+ type: "flaky_test_detected",
307
+ label: "flaky test",
308
+ genericTitle: "Flaky test detections",
309
+ defaultCoverage: "mixed",
310
+ rootCauseConfidence: 0.72,
311
+ why: "it contains the rerun-prone test that behaved inconsistently across attempts",
312
+ fix: "Stabilize the nondeterministic test or fixture before relying on reruns."
313
+ },
314
+ {
315
+ prefix: "serialization:",
316
+ type: "serialization_encoding_failure",
317
+ label: "serialization or encoding",
318
+ genericTitle: "Serialization or encoding failures",
319
+ defaultCoverage: "mixed",
320
+ rootCauseConfidence: 0.78,
321
+ why: "it contains the serialization or decoding path behind the malformed payload",
322
+ fix: "Inspect the encoded payload, serializer, or fixture data before rerunning."
323
+ },
324
+ {
325
+ prefix: "file not found:",
326
+ type: "file_not_found_failure",
327
+ label: "file not found",
328
+ genericTitle: "Missing file failures",
329
+ defaultCoverage: "mixed",
330
+ rootCauseConfidence: 0.82,
331
+ why: "it contains the missing file path or fixture artifact required by the test",
332
+ fix: "Restore the missing file, fixture artifact, or working-directory assumption before rerunning."
333
+ },
334
+ {
335
+ prefix: "memory:",
336
+ type: "memory_error",
337
+ label: "memory error",
338
+ genericTitle: "Memory failures",
339
+ defaultCoverage: "mixed",
340
+ rootCauseConfidence: 0.78,
341
+ why: "it contains the allocation path that exhausted available memory",
342
+ fix: "Reduce memory pressure or investigate the large allocation before rerunning."
343
+ },
344
+ {
345
+ prefix: "deprecation as error:",
346
+ type: "deprecation_warning_as_error",
347
+ label: "deprecation as error",
348
+ genericTitle: "Deprecation warnings as errors",
349
+ defaultCoverage: "mixed",
350
+ rootCauseConfidence: 0.74,
351
+ why: "it contains the deprecated API or warning filter that is failing the test run",
352
+ fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
353
+ },
354
+ {
355
+ prefix: "xfail strict:",
356
+ type: "xfail_strict_unexpected_pass",
357
+ label: "strict xfail unexpected pass",
358
+ genericTitle: "Strict xfail unexpected passes",
359
+ defaultCoverage: "failed",
360
+ rootCauseConfidence: 0.78,
361
+ why: "it contains the strict xfail case that unexpectedly passed",
362
+ fix: "Remove or update the strict xfail expectation if the test is now passing intentionally."
363
+ }
364
+ ];
365
+ function findExtendedBucketSpec(reason) {
366
+ return extendedBucketSpecs.find((spec) => reason.startsWith(spec.prefix)) ?? null;
367
+ }
368
+ function extractReasonDetail(reason, prefix) {
369
+ const detail = reason.slice(prefix.length).trim();
370
+ return detail.length > 0 ? detail : null;
371
+ }
155
372
  function formatCount(count, singular, plural = `${singular}s`) {
156
373
  return `${count} ${count === 1 ? singular : plural}`;
157
374
  }
@@ -205,6 +422,10 @@ function formatTargetSummary(summary) {
205
422
  return `count=${summary.count}; families=${families}`;
206
423
  }
207
424
  function classifyGenericBucketType(reason) {
425
+ const extended = findExtendedBucketSpec(reason);
426
+ if (extended) {
427
+ return extended.type;
428
+ }
208
429
  if (reason.startsWith("missing test env:")) {
209
430
  return "shared_environment_blocker";
210
431
  }
@@ -231,14 +452,77 @@ function classifyGenericBucketType(reason) {
231
452
  }
232
453
  return "unknown_failure";
233
454
  }
455
+ function isUnknownBucket(bucket) {
456
+ return bucket.source === "unknown" || bucket.reason.startsWith("unknown ");
457
+ }
458
+ function classifyVisibleStatusForLabel(args) {
459
+ const isError = args.errorLabels.has(args.label);
460
+ const isFailed = args.failedLabels.has(args.label);
461
+ if (isError && isFailed) {
462
+ return "mixed";
463
+ }
464
+ if (isError) {
465
+ return "error";
466
+ }
467
+ if (isFailed) {
468
+ return "failed";
469
+ }
470
+ return "unknown";
471
+ }
472
+ function inferCoverageFromReason(reason) {
473
+ const extended = findExtendedBucketSpec(reason);
474
+ if (extended) {
475
+ return extended.defaultCoverage;
476
+ }
477
+ 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:")) {
478
+ return "error";
479
+ }
480
+ if (reason.startsWith("assertion failed:")) {
481
+ return "failed";
482
+ }
483
+ return "mixed";
484
+ }
485
+ function buildCoverageCounts(args) {
486
+ if (args.coverageKind === "error") {
487
+ return {
488
+ error: args.count,
489
+ failed: 0
490
+ };
491
+ }
492
+ if (args.coverageKind === "failed") {
493
+ return {
494
+ error: 0,
495
+ failed: args.count
496
+ };
497
+ }
498
+ return {
499
+ error: 0,
500
+ failed: 0
501
+ };
502
+ }
234
503
  function buildGenericBuckets(analysis) {
235
504
  const buckets = [];
236
505
  const grouped = /* @__PURE__ */ new Map();
506
+ const errorLabels = new Set(analysis.visibleErrorLabels);
507
+ const failedLabels = new Set(analysis.visibleFailedLabels);
237
508
  const push = (reason, item) => {
238
- const key = `${classifyGenericBucketType(reason)}:${reason}`;
509
+ const coverageKind = (() => {
510
+ const status = classifyVisibleStatusForLabel({
511
+ label: item.label,
512
+ errorLabels,
513
+ failedLabels
514
+ });
515
+ return status === "unknown" ? inferCoverageFromReason(reason) : status;
516
+ })();
517
+ const key = `${classifyGenericBucketType(reason)}:${coverageKind}:${reason}`;
239
518
  const existing = grouped.get(key);
240
519
  if (existing) {
241
520
  existing.count += 1;
521
+ if (coverageKind === "error") {
522
+ existing.coverage.error += 1;
523
+ } else if (coverageKind === "failed") {
524
+ existing.coverage.failed += 1;
525
+ }
242
526
  if (!existing.representativeItems.some((entry) => entry.label === item.label) && existing.representativeItems.length < 6) {
243
527
  existing.representativeItems.push(item);
244
528
  }
@@ -250,19 +534,30 @@ function buildGenericBuckets(analysis) {
250
534
  summaryLines: [],
251
535
  reason,
252
536
  count: 1,
253
- confidence: reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62,
537
+ confidence: (() => {
538
+ const extended = findExtendedBucketSpec(reason);
539
+ if (extended) {
540
+ return Math.max(0.72, Math.min(extended.rootCauseConfidence, 0.82));
541
+ }
542
+ return reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62;
543
+ })(),
254
544
  representativeItems: [item],
255
545
  entities: [],
256
546
  hint: void 0,
257
547
  overflowCount: 0,
258
- overflowLabel: "failing tests/modules"
548
+ overflowLabel: "failing tests/modules",
549
+ coverage: buildCoverageCounts({
550
+ count: 1,
551
+ coverageKind
552
+ }),
553
+ source: "heuristic"
259
554
  });
260
555
  };
261
556
  for (const item of [...analysis.collectionItems, ...analysis.inlineItems]) {
262
557
  push(item.reason, item);
263
558
  }
264
559
  for (const bucket of grouped.values()) {
265
- 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";
560
+ 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");
266
561
  bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
267
562
  bucket.summaryLines = [bucket.headline];
268
563
  bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
@@ -308,10 +603,51 @@ function mergeBucketDetails(existing, incoming) {
308
603
  incoming.overflowCount,
309
604
  count - representativeItems.length
310
605
  ),
311
- overflowLabel: existing.overflowLabel || incoming.overflowLabel
606
+ overflowLabel: existing.overflowLabel || incoming.overflowLabel,
607
+ labelOverride: existing.labelOverride ?? incoming.labelOverride,
608
+ coverage: {
609
+ error: Math.max(existing.coverage.error, incoming.coverage.error),
610
+ failed: Math.max(existing.coverage.failed, incoming.coverage.failed)
611
+ },
612
+ source: existing.source
613
+ };
614
+ }
615
+ function inferFailureBucketCoverage(bucket, analysis) {
616
+ const errorLabels = new Set(analysis.visibleErrorLabels);
617
+ const failedLabels = new Set(analysis.visibleFailedLabels);
618
+ let error = 0;
619
+ let failed = 0;
620
+ for (const item of bucket.representativeItems) {
621
+ const status = classifyVisibleStatusForLabel({
622
+ label: item.label,
623
+ errorLabels,
624
+ failedLabels
625
+ });
626
+ if (status === "error") {
627
+ error += 1;
628
+ } else if (status === "failed") {
629
+ failed += 1;
630
+ }
631
+ }
632
+ const claimed = bucket.countClaimed ?? bucket.countVisible;
633
+ if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch") {
634
+ return {
635
+ error,
636
+ failed: Math.max(failed, claimed)
637
+ };
638
+ }
639
+ 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") {
640
+ return {
641
+ error: Math.max(error, claimed),
642
+ failed
643
+ };
644
+ }
645
+ return {
646
+ error,
647
+ failed
312
648
  };
313
649
  }
314
- function mergeBuckets(analysis) {
650
+ function mergeBuckets(analysis, extraBuckets = []) {
315
651
  const mergedByIdentity = /* @__PURE__ */ new Map();
316
652
  const merged = [];
317
653
  const pushBucket = (bucket) => {
@@ -340,7 +676,9 @@ function mergeBuckets(analysis) {
340
676
  entities: [...bucket2.entities],
341
677
  hint: bucket2.hint,
342
678
  overflowCount: bucket2.overflowCount,
343
- overflowLabel: bucket2.overflowLabel
679
+ overflowLabel: bucket2.overflowLabel,
680
+ coverage: inferFailureBucketCoverage(bucket2, analysis),
681
+ source: "heuristic"
344
682
  }))) {
345
683
  pushBucket(bucket);
346
684
  }
@@ -364,12 +702,19 @@ function mergeBuckets(analysis) {
364
702
  coveredLabels.add(item.label);
365
703
  }
366
704
  }
705
+ for (const bucket of extraBuckets) {
706
+ pushBucket(bucket);
707
+ }
367
708
  return merged;
368
709
  }
369
710
  function dominantBucketPriority(bucket) {
370
711
  if (bucket.reason.startsWith("missing test env:")) {
371
712
  return 5;
372
713
  }
714
+ const extended = findExtendedBucketSpec(bucket.reason);
715
+ if (extended?.dominantPriority !== void 0) {
716
+ return extended.dominantPriority;
717
+ }
373
718
  if (bucket.type === "shared_environment_blocker") {
374
719
  return 4;
375
720
  }
@@ -379,6 +724,9 @@ function dominantBucketPriority(bucket) {
379
724
  if (bucket.type === "collection_failure") {
380
725
  return 2;
381
726
  }
727
+ if (isUnknownBucket(bucket)) {
728
+ return 2;
729
+ }
382
730
  if (bucket.type === "contract_snapshot_drift") {
383
731
  return 1;
384
732
  }
@@ -400,9 +748,16 @@ function prioritizeBuckets(buckets) {
400
748
  });
401
749
  }
402
750
  function isDominantBlockerType(type) {
403
- return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
751
+ return type === "shared_environment_blocker" || type === "configuration_error" || type === "import_dependency_failure" || type === "collection_failure";
404
752
  }
405
753
  function labelForBucket(bucket) {
754
+ if (bucket.labelOverride) {
755
+ return bucket.labelOverride;
756
+ }
757
+ const extended = findExtendedBucketSpec(bucket.reason);
758
+ if (extended) {
759
+ return extended.label;
760
+ }
406
761
  if (bucket.reason.startsWith("missing test env:")) {
407
762
  return "missing test env";
408
763
  }
@@ -436,21 +791,40 @@ function labelForBucket(bucket) {
436
791
  if (bucket.type === "assertion_failure") {
437
792
  return "assertion failure";
438
793
  }
794
+ if (bucket.type === "snapshot_mismatch") {
795
+ return "snapshot mismatch";
796
+ }
439
797
  if (bucket.type === "collection_failure") {
440
798
  return "collection failure";
441
799
  }
442
800
  if (bucket.type === "runtime_failure") {
443
801
  return "runtime failure";
444
802
  }
803
+ if (bucket.reason.startsWith("unknown setup blocker:")) {
804
+ return "unknown setup blocker";
805
+ }
806
+ if (bucket.reason.startsWith("unknown failure family:")) {
807
+ return "unknown failure family";
808
+ }
445
809
  return "unknown failure";
446
810
  }
447
811
  function rootCauseConfidenceFor(bucket) {
812
+ if (isUnknownBucket(bucket)) {
813
+ return 0.52;
814
+ }
815
+ const extended = findExtendedBucketSpec(bucket.reason);
816
+ if (extended) {
817
+ return extended.rootCauseConfidence;
818
+ }
448
819
  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:")) {
449
820
  return 0.95;
450
821
  }
451
822
  if (bucket.type === "contract_snapshot_drift") {
452
823
  return bucket.entities.length > 0 ? 0.92 : 0.76;
453
824
  }
825
+ if (bucket.source === "provider") {
826
+ return Math.max(0.6, Math.min(bucket.confidence, 0.82));
827
+ }
454
828
  return Math.max(0.6, Math.min(bucket.confidence, 0.88));
455
829
  }
456
830
  function buildBucketEvidence(bucket) {
@@ -482,6 +856,10 @@ function buildReadTargetWhy(args) {
482
856
  if (envVar) {
483
857
  return `it contains the ${envVar} setup guard`;
484
858
  }
859
+ const extended = findExtendedBucketSpec(args.bucket.reason);
860
+ if (extended) {
861
+ return extended.why;
862
+ }
485
863
  if (args.bucket.reason.startsWith("fixture guard:")) {
486
864
  return "it contains the fixture/setup guard behind this bucket";
487
865
  }
@@ -494,6 +872,12 @@ function buildReadTargetWhy(args) {
494
872
  if (args.bucket.reason.startsWith("auth bypass absent:")) {
495
873
  return "it contains the auth bypass setup behind this bucket";
496
874
  }
875
+ if (args.bucket.reason.startsWith("unknown setup blocker:")) {
876
+ return "it is the first anchored setup failure in this unknown bucket";
877
+ }
878
+ if (args.bucket.reason.startsWith("unknown failure family:")) {
879
+ return "it is the first anchored failing test in this unknown bucket";
880
+ }
497
881
  if (args.bucket.type === "contract_snapshot_drift") {
498
882
  if (args.bucketLabel === "route drift") {
499
883
  return "it maps to the visible route drift bucket";
@@ -506,6 +890,9 @@ function buildReadTargetWhy(args) {
506
890
  }
507
891
  return "it maps to the visible stale snapshot expectation";
508
892
  }
893
+ if (args.bucket.type === "snapshot_mismatch") {
894
+ return "it maps to the visible snapshot mismatch bucket";
895
+ }
509
896
  if (args.bucket.type === "import_dependency_failure") {
510
897
  return "it is the first visible failing module in this missing dependency bucket";
511
898
  }
@@ -517,11 +904,54 @@ function buildReadTargetWhy(args) {
517
904
  }
518
905
  return `it maps to the visible ${args.bucketLabel} bucket`;
519
906
  }
907
+ function buildExtendedBucketSearchHint(bucket, anchor) {
908
+ const extended = findExtendedBucketSpec(bucket.reason);
909
+ if (!extended) {
910
+ return null;
911
+ }
912
+ const detail = extractReasonDetail(bucket.reason, extended.prefix);
913
+ if (!detail) {
914
+ return anchor.label.split("::")[1]?.trim() ?? anchor.label ?? null;
915
+ }
916
+ if (extended.type === "timeout_failure") {
917
+ const duration = detail.match(/>\s*([0-9]+(?:\.[0-9]+)?s?)/i)?.[1];
918
+ return duration ?? anchor.label.split("::")[1]?.trim() ?? detail;
919
+ }
920
+ if (extended.type === "db_migration_failure") {
921
+ const relation = detail.match(/\b(?:relation|table)\s+([A-Za-z0-9_.-]+)/i)?.[1];
922
+ return relation ?? detail;
923
+ }
924
+ if (extended.type === "network_failure") {
925
+ const url = detail.match(/\bhttps?:\/\/[^\s)'"`]+/i)?.[0];
926
+ const host = detail.match(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/)?.[0];
927
+ return url ?? host ?? detail;
928
+ }
929
+ if (extended.type === "xdist_worker_crash") {
930
+ return detail.match(/\bgw\d+\b/)?.[0] ?? detail;
931
+ }
932
+ if (extended.type === "fixture_teardown_failure") {
933
+ return detail.replace(/^of\s+/i, "") || anchor.label;
934
+ }
935
+ if (extended.type === "file_not_found_failure") {
936
+ const path4 = detail.match(/['"]([^'"]+)['"]/)?.[1];
937
+ return path4 ?? detail;
938
+ }
939
+ if (extended.type === "permission_denied_failure") {
940
+ const path4 = detail.match(/['"]([^'"]+)['"]/)?.[1];
941
+ const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
942
+ return path4 ?? (port ? `port ${port}` : detail);
943
+ }
944
+ return detail;
945
+ }
520
946
  function buildReadTargetSearchHint(bucket, anchor) {
521
947
  const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
522
948
  if (envVar) {
523
949
  return envVar;
524
950
  }
951
+ const extendedHint = buildExtendedBucketSearchHint(bucket, anchor);
952
+ if (extendedHint) {
953
+ return extendedHint;
954
+ }
525
955
  if (bucket.type === "contract_snapshot_drift") {
526
956
  return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
527
957
  }
@@ -543,6 +973,9 @@ function buildReadTargetSearchHint(bucket, anchor) {
543
973
  if (assertionText) {
544
974
  return assertionText;
545
975
  }
976
+ if (bucket.reason.startsWith("unknown ")) {
977
+ return anchor.reason;
978
+ }
546
979
  const fallbackLabel = anchor.label.split("::")[1]?.trim();
547
980
  return fallbackLabel || null;
548
981
  }
@@ -602,6 +1035,12 @@ function buildConcreteNextNote(args) {
602
1035
  if (args.nextBestAction.code === "read_source_for_bucket") {
603
1036
  return lead;
604
1037
  }
1038
+ if (args.nextBestAction.code === "insufficient_signal") {
1039
+ if (args.nextBestAction.note.startsWith("Provider follow-up failed")) {
1040
+ return args.nextBestAction.note;
1041
+ }
1042
+ return `${lead} Then take one deeper sift pass before raw traceback.`;
1043
+ }
605
1044
  return args.nextBestAction.note;
606
1045
  }
607
1046
  function extractMiniDiff(input, bucket) {
@@ -626,6 +1065,156 @@ function extractMiniDiff(input, bucket) {
626
1065
  ...changedTaskMappings > 0 ? { changed_task_mappings: changedTaskMappings } : {}
627
1066
  };
628
1067
  }
1068
+ function inferSupplementCoverageKind(args) {
1069
+ const extended = findExtendedBucketSpec(args.rootCause);
1070
+ if (extended?.defaultCoverage === "error" || extended?.defaultCoverage === "failed") {
1071
+ return extended.defaultCoverage;
1072
+ }
1073
+ const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
1074
+ if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
1075
+ normalized
1076
+ )) {
1077
+ return "error";
1078
+ }
1079
+ if (/snapshot|contract|drift|assertion|expected|actual|golden/.test(normalized)) {
1080
+ return "failed";
1081
+ }
1082
+ if (args.remainingErrors > 0 && args.remainingFailed === 0) {
1083
+ return "error";
1084
+ }
1085
+ return "failed";
1086
+ }
1087
+ function buildProviderSupplementBuckets(args) {
1088
+ let remainingErrors = args.remainingErrors;
1089
+ let remainingFailed = args.remainingFailed;
1090
+ return args.supplements.flatMap((supplement) => {
1091
+ const coverageKind = inferSupplementCoverageKind({
1092
+ label: supplement.label,
1093
+ rootCause: supplement.root_cause,
1094
+ remainingErrors,
1095
+ remainingFailed
1096
+ });
1097
+ const budget = coverageKind === "error" ? remainingErrors : remainingFailed;
1098
+ const count = Math.max(0, Math.min(supplement.count, budget));
1099
+ if (count === 0) {
1100
+ return [];
1101
+ }
1102
+ if (coverageKind === "error") {
1103
+ remainingErrors -= count;
1104
+ } else {
1105
+ remainingFailed -= count;
1106
+ }
1107
+ const representativeLabel = supplement.anchor.file ?? `${supplement.label} supplement`;
1108
+ const representativeItem = {
1109
+ label: representativeLabel,
1110
+ reason: supplement.root_cause,
1111
+ group: supplement.label,
1112
+ file: supplement.anchor.file,
1113
+ line: supplement.anchor.line,
1114
+ anchor_kind: supplement.anchor.file && supplement.anchor.line !== null ? "traceback" : supplement.anchor.file ? "test_label" : supplement.anchor.search_hint ? "entity" : "none",
1115
+ anchor_confidence: Math.max(0.4, Math.min(supplement.confidence, 0.82))
1116
+ };
1117
+ return [
1118
+ {
1119
+ type: classifyGenericBucketType(supplement.root_cause),
1120
+ headline: `${supplement.label}: ${formatCount(count, "visible failure")} share ${supplement.root_cause}.`,
1121
+ summaryLines: [
1122
+ `${supplement.label}: ${formatCount(count, "visible failure")} share ${supplement.root_cause}.`
1123
+ ],
1124
+ reason: supplement.root_cause,
1125
+ count,
1126
+ confidence: Math.max(0.4, Math.min(supplement.confidence, 0.82)),
1127
+ representativeItems: [representativeItem],
1128
+ entities: supplement.anchor.search_hint ? [supplement.anchor.search_hint] : [],
1129
+ hint: supplement.fix_hint ?? void 0,
1130
+ overflowCount: Math.max(count - 1, 0),
1131
+ overflowLabel: "failing tests/modules",
1132
+ labelOverride: supplement.label,
1133
+ coverage: buildCoverageCounts({
1134
+ count,
1135
+ coverageKind
1136
+ }),
1137
+ source: "provider"
1138
+ }
1139
+ ];
1140
+ });
1141
+ }
1142
+ function pickUnknownAnchor(args) {
1143
+ const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : null;
1144
+ if (fromStatusItems) {
1145
+ return {
1146
+ label: fromStatusItems.label,
1147
+ reason: fromStatusItems.reason,
1148
+ group: fromStatusItems.group,
1149
+ file: fromStatusItems.file,
1150
+ line: fromStatusItems.line,
1151
+ anchor_kind: fromStatusItems.anchor_kind,
1152
+ anchor_confidence: fromStatusItems.anchor_confidence
1153
+ };
1154
+ }
1155
+ const label = args.kind === "error" ? args.analysis.visibleErrorLabels[0] : args.analysis.visibleFailedLabels[0];
1156
+ if (label) {
1157
+ const normalizedLabel = normalizeTestId(label);
1158
+ const fileMatch = normalizedLabel.match(/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)\b/);
1159
+ const file = fileMatch?.[1] ?? normalizedLabel.split("::")[0] ?? null;
1160
+ return {
1161
+ label,
1162
+ reason: args.kind === "error" ? "setup failures share a repeated but unclassified pattern" : "failing tests share a repeated but unclassified pattern",
1163
+ group: args.kind === "error" ? "unknown setup blocker" : "unknown failure family",
1164
+ file: file && file !== label ? file : null,
1165
+ line: null,
1166
+ anchor_kind: file && file !== label ? "test_label" : "none",
1167
+ anchor_confidence: file && file !== label ? 0.6 : 0
1168
+ };
1169
+ }
1170
+ return null;
1171
+ }
1172
+ function buildUnknownBucket(args) {
1173
+ if (args.count <= 0) {
1174
+ return null;
1175
+ }
1176
+ const anchor = pickUnknownAnchor(args);
1177
+ const isError = args.kind === "error";
1178
+ const label = isError ? "unknown setup blocker" : "unknown failure family";
1179
+ 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";
1180
+ return {
1181
+ type: "unknown_failure",
1182
+ headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
1183
+ summaryLines: [
1184
+ `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`
1185
+ ],
1186
+ reason,
1187
+ count: args.count,
1188
+ confidence: 0.45,
1189
+ representativeItems: anchor ? [anchor] : [],
1190
+ entities: [],
1191
+ hint: isError ? "Take one deeper sift pass or inspect the first anchored setup failure." : "Take one deeper sift pass or inspect the first anchored failing test.",
1192
+ overflowCount: Math.max(args.count - (anchor ? 1 : 0), 0),
1193
+ overflowLabel: "failing tests/modules",
1194
+ labelOverride: label,
1195
+ coverage: buildCoverageCounts({
1196
+ count: args.count,
1197
+ coverageKind: isError ? "error" : "failed"
1198
+ }),
1199
+ source: "unknown"
1200
+ };
1201
+ }
1202
+ function buildCoverageResiduals(args) {
1203
+ const covered = args.buckets.reduce(
1204
+ (totals, bucket) => ({
1205
+ error: totals.error + bucket.coverage.error,
1206
+ failed: totals.failed + bucket.coverage.failed
1207
+ }),
1208
+ {
1209
+ error: 0,
1210
+ failed: 0
1211
+ }
1212
+ );
1213
+ return {
1214
+ remainingErrors: Math.max(args.analysis.errors - Math.min(args.analysis.errors, covered.error), 0),
1215
+ remainingFailed: Math.max(args.analysis.failed - Math.min(args.analysis.failed, covered.failed), 0)
1216
+ };
1217
+ }
629
1218
  function buildOutcomeLines(analysis) {
630
1219
  if (analysis.noTestsCollected) {
631
1220
  return ["- Tests did not run.", "- Collected 0 items."];
@@ -724,6 +1313,10 @@ function buildStandardFixText(args) {
724
1313
  if (args.bucket.hint) {
725
1314
  return args.bucket.hint;
726
1315
  }
1316
+ const extended = findExtendedBucketSpec(args.bucket.reason);
1317
+ if (extended) {
1318
+ return extended.fix;
1319
+ }
727
1320
  const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
728
1321
  if (envVar) {
729
1322
  return `Set ${envVar} before rerunning the affected tests.`;
@@ -744,9 +1337,18 @@ function buildStandardFixText(args) {
744
1337
  if (args.bucket.reason.startsWith("auth bypass absent:")) {
745
1338
  return "Restore the test auth bypass setup and rerun the full suite at standard.";
746
1339
  }
1340
+ if (args.bucket.reason.startsWith("unknown setup blocker:")) {
1341
+ return "Take one deeper sift pass or inspect the first anchored setup failure before rerunning.";
1342
+ }
1343
+ if (args.bucket.reason.startsWith("unknown failure family:")) {
1344
+ return "Take one deeper sift pass or inspect the first anchored failing test before rerunning.";
1345
+ }
747
1346
  if (args.bucket.type === "contract_snapshot_drift") {
748
1347
  return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
749
1348
  }
1349
+ if (args.bucket.type === "snapshot_mismatch") {
1350
+ return "Update the snapshots if these output changes are intentional, then rerun the full suite at standard.";
1351
+ }
750
1352
  if (args.bucket.type === "assertion_failure") {
751
1353
  return "Inspect the failing assertion and rerun the full suite at standard.";
752
1354
  }
@@ -840,7 +1442,35 @@ function renderVerbose(args) {
840
1442
  return lines.join("\n");
841
1443
  }
842
1444
  function buildTestStatusDiagnoseContract(args) {
843
- const buckets = prioritizeBuckets(mergeBuckets(args.analysis)).slice(0, 3);
1445
+ const heuristicBuckets = mergeBuckets(args.analysis);
1446
+ const preUnknownSimpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && heuristicBuckets.length === 0 && (args.providerBucketSupplements?.length ?? 0) === 0;
1447
+ const heuristicResiduals = buildCoverageResiduals({
1448
+ analysis: args.analysis,
1449
+ buckets: heuristicBuckets
1450
+ });
1451
+ const providerSupplementBuckets = buildProviderSupplementBuckets({
1452
+ supplements: args.providerBucketSupplements ?? [],
1453
+ remainingErrors: heuristicResiduals.remainingErrors,
1454
+ remainingFailed: heuristicResiduals.remainingFailed
1455
+ });
1456
+ const combinedBuckets = mergeBuckets(args.analysis, providerSupplementBuckets);
1457
+ const residuals = buildCoverageResiduals({
1458
+ analysis: args.analysis,
1459
+ buckets: combinedBuckets
1460
+ });
1461
+ const unknownBuckets = preUnknownSimpleCollectionFailure ? [] : [
1462
+ buildUnknownBucket({
1463
+ analysis: args.analysis,
1464
+ kind: "error",
1465
+ count: residuals.remainingErrors
1466
+ }),
1467
+ buildUnknownBucket({
1468
+ analysis: args.analysis,
1469
+ kind: "failed",
1470
+ count: residuals.remainingFailed
1471
+ })
1472
+ ].filter((bucket) => Boolean(bucket));
1473
+ const buckets = prioritizeBuckets([...combinedBuckets, ...unknownBuckets]).slice(0, 3);
844
1474
  const simpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && buckets.length === 0;
845
1475
  const dominantBucket = buckets.map((bucket, index) => ({
846
1476
  bucket,
@@ -851,8 +1481,10 @@ function buildTestStatusDiagnoseContract(args) {
851
1481
  }
852
1482
  return right.bucket.confidence - left.bucket.confidence;
853
1483
  })[0] ?? null;
854
- const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && (dominantBucket?.bucket.confidence ?? 0) >= 0.7;
855
- const rawNeeded = buckets.length > 0 ? buckets.every((bucket) => bucket.confidence < 0.7) : !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure);
1484
+ const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
1485
+ const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
1486
+ 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;
1487
+ 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);
856
1488
  const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
857
1489
  const readTargets = buildReadTargets({
858
1490
  buckets,
@@ -887,6 +1519,12 @@ function buildTestStatusDiagnoseContract(args) {
887
1519
  bucket_index: null,
888
1520
  note: "Inspect the collection traceback or setup code next; the run failed before tests executed."
889
1521
  };
1522
+ } else if (hasUnknownBucket) {
1523
+ nextBestAction = {
1524
+ code: "insufficient_signal",
1525
+ bucket_index: dominantBucket ? dominantBucket.index + 1 : null,
1526
+ note: "Take one deeper sift pass or inspect the first anchored failure before falling back to raw traceback."
1527
+ };
890
1528
  } else if (!diagnosisComplete) {
891
1529
  nextBestAction = {
892
1530
  code: rawNeeded ? "read_raw_for_exact_traceback" : "insufficient_signal",
@@ -924,11 +1562,15 @@ function buildTestStatusDiagnoseContract(args) {
924
1562
  read_targets: readTargets,
925
1563
  next_best_action: nextBestAction
926
1564
  };
1565
+ const effectiveDiagnosisComplete = Boolean(args.contractOverrides?.diagnosis_complete ?? diagnosisComplete) && !hasUnknownBucket;
1566
+ const requestedDecision = args.contractOverrides?.decision;
1567
+ const effectiveDecision = hasUnknownBucket && requestedDecision && (requestedDecision === "stop" || requestedDecision === "read_source") ? "zoom" : requestedDecision;
927
1568
  const effectiveNextBestAction = args.contractOverrides?.next_best_action ?? baseContract.next_best_action;
928
1569
  const mergedContractWithoutDecision = {
929
1570
  ...baseContract,
930
1571
  ...args.contractOverrides,
931
- status: args.contractOverrides?.diagnosis_complete ?? diagnosisComplete ? "ok" : "insufficient",
1572
+ diagnosis_complete: effectiveDiagnosisComplete,
1573
+ status: effectiveDiagnosisComplete ? "ok" : "insufficient",
932
1574
  next_best_action: {
933
1575
  ...effectiveNextBestAction,
934
1576
  note: buildConcreteNextNote({
@@ -942,7 +1584,7 @@ function buildTestStatusDiagnoseContract(args) {
942
1584
  };
943
1585
  const contract = testStatusDiagnoseContractSchema.parse({
944
1586
  ...mergedContractWithoutDecision,
945
- decision: args.contractOverrides?.decision ?? deriveDecision(mergedContractWithoutDecision)
1587
+ decision: effectiveDecision ?? deriveDecision(mergedContractWithoutDecision)
946
1588
  });
947
1589
  return {
948
1590
  contract,
@@ -1032,6 +1674,63 @@ function getCount(input, label) {
1032
1674
  const lastMatch = matches.at(-1);
1033
1675
  return lastMatch ? Number(lastMatch[1]) : 0;
1034
1676
  }
1677
+ function detectTestRunner(input) {
1678
+ 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)) {
1679
+ return "vitest";
1680
+ }
1681
+ 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)) {
1682
+ return "jest";
1683
+ }
1684
+ 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)) {
1685
+ return "pytest";
1686
+ }
1687
+ return "unknown";
1688
+ }
1689
+ function extractVitestLineCount(input, label, metric) {
1690
+ const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
1691
+ const lines = [...input.matchAll(matcher)];
1692
+ const line = lines.at(-1)?.[1];
1693
+ if (!line) {
1694
+ return null;
1695
+ }
1696
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
1697
+ return metricMatch ? Number(metricMatch[1]) : null;
1698
+ }
1699
+ function extractJestLineCount(input, label, metric) {
1700
+ const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
1701
+ const lines = [...input.matchAll(matcher)];
1702
+ const line = lines.at(-1)?.[1];
1703
+ if (!line) {
1704
+ return null;
1705
+ }
1706
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
1707
+ return metricMatch ? Number(metricMatch[1]) : null;
1708
+ }
1709
+ function extractTestStatusCounts(input, runner) {
1710
+ if (runner === "vitest") {
1711
+ return {
1712
+ passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
1713
+ failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
1714
+ errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
1715
+ skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
1716
+ snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
1717
+ };
1718
+ }
1719
+ if (runner === "jest") {
1720
+ return {
1721
+ passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
1722
+ failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
1723
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
1724
+ skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
1725
+ };
1726
+ }
1727
+ return {
1728
+ passed: getCount(input, "passed"),
1729
+ failed: getCount(input, "failed"),
1730
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
1731
+ skipped: getCount(input, "skipped")
1732
+ };
1733
+ }
1035
1734
  function formatCount2(count, singular, plural = `${singular}s`) {
1036
1735
  return `${count} ${count === 1 ? singular : plural}`;
1037
1736
  }
@@ -1064,7 +1763,8 @@ function normalizeAnchorFile(value) {
1064
1763
  return value.replace(/\\/g, "/").trim();
1065
1764
  }
1066
1765
  function inferFileFromLabel(label) {
1067
- const candidate = cleanFailureLabel(label).split("::")[0]?.trim();
1766
+ const cleaned = cleanFailureLabel(label);
1767
+ const candidate = cleaned.split("::")[0]?.split(" > ")[0]?.trim();
1068
1768
  if (!candidate) {
1069
1769
  return null;
1070
1770
  }
@@ -1119,6 +1819,15 @@ function parseObservedAnchor(line) {
1119
1819
  anchor_confidence: 0.92
1120
1820
  };
1121
1821
  }
1822
+ const vitestTraceback = normalized.match(/^\s*❯\s+([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?/);
1823
+ if (vitestTraceback) {
1824
+ return {
1825
+ file: normalizeAnchorFile(vitestTraceback[1]),
1826
+ line: Number(vitestTraceback[2]),
1827
+ anchor_kind: "traceback",
1828
+ anchor_confidence: 1
1829
+ };
1830
+ }
1122
1831
  return null;
1123
1832
  }
1124
1833
  function resolveAnchorForLabel(args) {
@@ -1135,15 +1844,27 @@ function isLowValueInternalReason(normalized) {
1135
1844
  ) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
1136
1845
  }
1137
1846
  function scoreFailureReason(reason) {
1847
+ if (reason.startsWith("configuration:")) {
1848
+ return 6;
1849
+ }
1138
1850
  if (reason.startsWith("missing test env:")) {
1139
1851
  return 6;
1140
1852
  }
1141
1853
  if (reason.startsWith("missing module:")) {
1142
1854
  return 5;
1143
1855
  }
1856
+ if (reason.startsWith("snapshot mismatch:")) {
1857
+ return 4;
1858
+ }
1144
1859
  if (reason.startsWith("assertion failed:")) {
1145
1860
  return 4;
1146
1861
  }
1862
+ if (reason.startsWith("timeout:") || reason.startsWith("async loop:") || reason.startsWith("django db access:") || reason.startsWith("db migration:")) {
1863
+ return 3;
1864
+ }
1865
+ 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:")) {
1866
+ return 2;
1867
+ }
1147
1868
  if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
1148
1869
  return 3;
1149
1870
  }
@@ -1152,6 +1873,16 @@ function scoreFailureReason(reason) {
1152
1873
  }
1153
1874
  return 1;
1154
1875
  }
1876
+ function buildClassifiedReason(prefix, detail) {
1877
+ return `${prefix}: ${detail}`.slice(0, 120);
1878
+ }
1879
+ function buildExcerptDetail(value, fallback) {
1880
+ const trimmed = value.trim().replace(/\s+/g, " ");
1881
+ return trimmed.length > 0 ? trimmed : fallback;
1882
+ }
1883
+ function sharedBlockerThreshold(reason) {
1884
+ return reason.startsWith("configuration:") ? 1 : 3;
1885
+ }
1155
1886
  function extractEnvBlockerName(normalized) {
1156
1887
  const directMatch = normalized.match(
1157
1888
  /\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
@@ -1162,7 +1893,25 @@ function extractEnvBlockerName(normalized) {
1162
1893
  const fallbackMatch = normalized.match(
1163
1894
  /\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]*DB-isolated tests)/
1164
1895
  );
1165
- return fallbackMatch?.[1] ?? null;
1896
+ if (fallbackMatch) {
1897
+ return fallbackMatch[1];
1898
+ }
1899
+ const leadingEnvMatch = normalized.match(
1900
+ /\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]{0,80}\b(?:is\s+)?(?:missing|unset|not set|not configured|required)\b)/
1901
+ );
1902
+ if (leadingEnvMatch) {
1903
+ return leadingEnvMatch[1];
1904
+ }
1905
+ const trailingEnvMatch = normalized.match(
1906
+ /\b(?:missing|unset|not set|not configured|required)\b[^.\n]{0,80}\b([A-Z][A-Z0-9_]{2,})\b/
1907
+ );
1908
+ if (trailingEnvMatch) {
1909
+ return trailingEnvMatch[1];
1910
+ }
1911
+ const validationEnvMatch = normalized.match(
1912
+ /\bValidationError\b[^.\n]{0,120}\b([A-Z][A-Z0-9_]{2,})\b/
1913
+ );
1914
+ return validationEnvMatch?.[1] ?? null;
1166
1915
  }
1167
1916
  function classifyFailureReason(line, options) {
1168
1917
  const normalized = line.trim().replace(/^[A-Z]\s+/, "");
@@ -1183,7 +1932,7 @@ function classifyFailureReason(line, options) {
1183
1932
  };
1184
1933
  }
1185
1934
  const missingEnv = normalized.match(
1186
- /\b(?:environment variable|env(?:ironment)? var(?:iable)?|Missing required env(?:ironment)? variable)\s+([A-Z][A-Z0-9_]{2,})\b/i
1935
+ /\b(?:environment variable|env(?:ironment)? var(?:iable)?|missing required env(?:ironment)? variable)\s+([A-Z][A-Z0-9_]{2,})\b/
1187
1936
  );
1188
1937
  if (missingEnv) {
1189
1938
  return {
@@ -1215,6 +1964,12 @@ function classifyFailureReason(line, options) {
1215
1964
  group: "database connectivity failures"
1216
1965
  };
1217
1966
  }
1967
+ if (/(ECONNREFUSED|ConnectionRefusedError|connection refused)/i.test(normalized)) {
1968
+ return {
1969
+ reason: "service unavailable: dependency connection was refused",
1970
+ group: "service availability failures"
1971
+ };
1972
+ }
1218
1973
  if (/(503\b|service unavailable|temporarily unavailable)/i.test(normalized)) {
1219
1974
  return {
1220
1975
  reason: "service unavailable: dependency service is unavailable",
@@ -1227,6 +1982,226 @@ function classifyFailureReason(line, options) {
1227
1982
  group: "authentication test setup failures"
1228
1983
  };
1229
1984
  }
1985
+ const snapshotMismatch = normalized.match(
1986
+ /((?:Error:\s*)?Snapshot\b.+\bmismatched\b[^$]*|Snapshot comparison failed[^$]*)/i
1987
+ );
1988
+ if (snapshotMismatch) {
1989
+ return {
1990
+ reason: buildClassifiedReason(
1991
+ "snapshot mismatch",
1992
+ buildExcerptDetail(snapshotMismatch[1] ?? normalized, "snapshot output changed")
1993
+ ),
1994
+ group: "snapshot mismatches"
1995
+ };
1996
+ }
1997
+ const timeoutFailure = normalized.match(
1998
+ /(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
1999
+ );
2000
+ if (timeoutFailure) {
2001
+ return {
2002
+ reason: buildClassifiedReason(
2003
+ "timeout",
2004
+ buildExcerptDetail(timeoutFailure[1] ?? normalized, "test exceeded timeout threshold")
2005
+ ),
2006
+ group: "timeout failures"
2007
+ };
2008
+ }
2009
+ const asyncLoopFailure = normalized.match(
2010
+ /(Event loop is closed|no current event loop|coroutine .* was never awaited|coroutine was never awaited)/i
2011
+ );
2012
+ if (asyncLoopFailure) {
2013
+ return {
2014
+ reason: buildClassifiedReason(
2015
+ "async loop",
2016
+ buildExcerptDetail(asyncLoopFailure[1] ?? normalized, "async event loop failure")
2017
+ ),
2018
+ group: "async event loop failures"
2019
+ };
2020
+ }
2021
+ const permissionFailure = normalized.match(
2022
+ /(PermissionError:\s*\[Errno 13\][^$]*|Address already in use)/i
2023
+ );
2024
+ if (permissionFailure) {
2025
+ return {
2026
+ reason: buildClassifiedReason(
2027
+ "permission",
2028
+ buildExcerptDetail(permissionFailure[1] ?? normalized, "permission denied or resource locked")
2029
+ ),
2030
+ group: "permission or locked resource failures"
2031
+ };
2032
+ }
2033
+ const xdistWorkerCrash = normalized.match(
2034
+ /(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
2035
+ );
2036
+ if (xdistWorkerCrash) {
2037
+ return {
2038
+ reason: buildClassifiedReason(
2039
+ "xdist worker crash",
2040
+ buildExcerptDetail(xdistWorkerCrash[1] ?? normalized, "pytest-xdist worker crashed")
2041
+ ),
2042
+ group: "xdist worker crashes"
2043
+ };
2044
+ }
2045
+ if (/Worker terminated due to reaching memory limit/i.test(normalized)) {
2046
+ return {
2047
+ reason: "memory: Worker terminated due to reaching memory limit",
2048
+ group: "memory exhaustion failures"
2049
+ };
2050
+ }
2051
+ if (/Database access not allowed, use the ["']django_db["'] mark/i.test(normalized)) {
2052
+ return {
2053
+ reason: 'django db access: Database access not allowed, use the "django_db" mark',
2054
+ group: "django database marker failures"
2055
+ };
2056
+ }
2057
+ const networkFailure = normalized.match(
2058
+ /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable)/i
2059
+ );
2060
+ if (networkFailure) {
2061
+ return {
2062
+ reason: buildClassifiedReason(
2063
+ "network",
2064
+ buildExcerptDetail(networkFailure[1] ?? normalized, "network dependency failure")
2065
+ ),
2066
+ group: "network dependency failures"
2067
+ };
2068
+ }
2069
+ const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
2070
+ if (relationMigration) {
2071
+ return {
2072
+ reason: buildClassifiedReason("db migration", `relation ${relationMigration[1]} does not exist`),
2073
+ group: "database migration or schema failures"
2074
+ };
2075
+ }
2076
+ const noSuchTable = normalized.match(/no such table(?::)?\s*([A-Za-z0-9_.-]+)/i);
2077
+ if (noSuchTable) {
2078
+ return {
2079
+ reason: buildClassifiedReason("db migration", `no such table ${noSuchTable[1]}`),
2080
+ group: "database migration or schema failures"
2081
+ };
2082
+ }
2083
+ if (/InconsistentMigrationHistory/i.test(normalized)) {
2084
+ return {
2085
+ reason: "db migration: InconsistentMigrationHistory",
2086
+ group: "database migration or schema failures"
2087
+ };
2088
+ }
2089
+ if (/(Segmentation fault|SIGSEGV|\bexit 139\b)/i.test(normalized)) {
2090
+ return {
2091
+ reason: buildClassifiedReason(
2092
+ "segfault",
2093
+ buildExcerptDetail(normalized, "subprocess crashed with SIGSEGV")
2094
+ ),
2095
+ group: "subprocess crash failures"
2096
+ };
2097
+ }
2098
+ if (/(MemoryError\b|\bexit 137\b|OOMKilled|OutOfMemory)/i.test(normalized)) {
2099
+ return {
2100
+ reason: buildClassifiedReason(
2101
+ "memory",
2102
+ buildExcerptDetail(normalized, "process exhausted available memory")
2103
+ ),
2104
+ group: "memory exhaustion failures"
2105
+ };
2106
+ }
2107
+ const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
2108
+ if (typeErrorFailure) {
2109
+ return {
2110
+ reason: buildClassifiedReason(
2111
+ "type error",
2112
+ buildExcerptDetail(typeErrorFailure[1] ?? normalized, "TypeError")
2113
+ ),
2114
+ group: "type errors"
2115
+ };
2116
+ }
2117
+ const serializationFailure = normalized.match(
2118
+ /\b(UnicodeDecodeError|JSONDecodeError|PicklingError):\s*(.+)$/i
2119
+ );
2120
+ if (serializationFailure) {
2121
+ return {
2122
+ reason: buildClassifiedReason(
2123
+ "serialization",
2124
+ `${serializationFailure[1]}: ${buildExcerptDetail(serializationFailure[2] ?? "", serializationFailure[1] ?? "serialization failure")}`
2125
+ ),
2126
+ group: "serialization and encoding failures"
2127
+ };
2128
+ }
2129
+ const fileNotFoundFailure = normalized.match(/FileNotFoundError:\s*(.+)$/i);
2130
+ if (fileNotFoundFailure) {
2131
+ return {
2132
+ reason: buildClassifiedReason(
2133
+ "file not found",
2134
+ buildExcerptDetail(fileNotFoundFailure[1] ?? normalized, "missing file during test execution")
2135
+ ),
2136
+ group: "missing file failures"
2137
+ };
2138
+ }
2139
+ const deprecationFailure = normalized.match(
2140
+ /\b(DeprecationWarning|FutureWarning|PytestRemovedIn9Warning):\s*(.+)$/i
2141
+ );
2142
+ if (deprecationFailure) {
2143
+ return {
2144
+ reason: buildClassifiedReason(
2145
+ "deprecation as error",
2146
+ `${deprecationFailure[1]}: ${buildExcerptDetail(deprecationFailure[2] ?? "", deprecationFailure[1] ?? "warning treated as error")}`
2147
+ ),
2148
+ group: "warnings treated as errors"
2149
+ };
2150
+ }
2151
+ const strictXfail = normalized.match(/XPASS\(strict\)\s*:?\s*(.+)?$/i);
2152
+ if (strictXfail) {
2153
+ return {
2154
+ reason: buildClassifiedReason(
2155
+ "xfail strict",
2156
+ buildExcerptDetail(strictXfail[1] ?? normalized, "strict xfail unexpectedly passed")
2157
+ ),
2158
+ group: "strict xfail expectation failures"
2159
+ };
2160
+ }
2161
+ const resourceLeak = normalized.match(
2162
+ /(PytestUnraisableExceptionWarning[^,;]*|ResourceWarning:\s*unclosed[^,;]*)/i
2163
+ );
2164
+ if (resourceLeak) {
2165
+ return {
2166
+ reason: buildClassifiedReason(
2167
+ "resource leak",
2168
+ buildExcerptDetail(resourceLeak[1] ?? normalized, "resource leak warning promoted to failure")
2169
+ ),
2170
+ group: "resource leak warnings"
2171
+ };
2172
+ }
2173
+ const flakyFailure = normalized.match(/\b(RERUN|[0-9]+\s+rerun|Flaky test passed)\b[^$]*/i);
2174
+ if (flakyFailure) {
2175
+ return {
2176
+ reason: buildClassifiedReason(
2177
+ "flaky",
2178
+ buildExcerptDetail(flakyFailure[0] ?? normalized, "test required reruns before passing")
2179
+ ),
2180
+ group: "flaky test detections"
2181
+ };
2182
+ }
2183
+ const teardownFailure = normalized.match(/ERROR at teardown of\s+(.+)$/i);
2184
+ if (teardownFailure) {
2185
+ return {
2186
+ reason: buildClassifiedReason(
2187
+ "fixture teardown",
2188
+ buildExcerptDetail(teardownFailure[1] ?? normalized, "fixture teardown failed")
2189
+ ),
2190
+ group: "fixture teardown failures"
2191
+ };
2192
+ }
2193
+ const configurationFailure = normalized.match(
2194
+ /(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
2195
+ );
2196
+ if (configurationFailure) {
2197
+ return {
2198
+ reason: buildClassifiedReason(
2199
+ "configuration",
2200
+ buildExcerptDetail(configurationFailure[1] ?? normalized, "test configuration error")
2201
+ ),
2202
+ group: "test configuration failures"
2203
+ };
2204
+ }
1230
2205
  const pythonMissingModule = normalized.match(
1231
2206
  /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
1232
2207
  );
@@ -1243,6 +2218,20 @@ function classifyFailureReason(line, options) {
1243
2218
  group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
1244
2219
  };
1245
2220
  }
2221
+ const importResolutionFailure = normalized.match(/Failed to resolve import ['"]([^'"]+)['"]/i);
2222
+ if (importResolutionFailure) {
2223
+ return {
2224
+ reason: `missing module: ${importResolutionFailure[1]}`,
2225
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2226
+ };
2227
+ }
2228
+ const esmModuleFailure = normalized.match(/ERR_MODULE_NOT_FOUND[^'"`]*['"`]([^'"`]+)['"`]/i) ?? normalized.match(/Cannot find package ['"`]([^'"`]+)['"`]/i);
2229
+ if (esmModuleFailure) {
2230
+ return {
2231
+ reason: `missing module: ${esmModuleFailure[1]}`,
2232
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2233
+ };
2234
+ }
1246
2235
  const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
1247
2236
  if (assertionFailure) {
1248
2237
  return {
@@ -1250,6 +2239,16 @@ function classifyFailureReason(line, options) {
1250
2239
  group: "assertion failures"
1251
2240
  };
1252
2241
  }
2242
+ const vitestUnhandled = normalized.match(/Vitest caught\s+\d+\s+unhandled errors?/i);
2243
+ if (vitestUnhandled) {
2244
+ return {
2245
+ reason: `RuntimeError: ${buildExcerptDetail(vitestUnhandled[0] ?? normalized, "Vitest caught unhandled errors")}`.slice(
2246
+ 0,
2247
+ 120
2248
+ ),
2249
+ group: "runtime failures"
2250
+ };
2251
+ }
1253
2252
  const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
1254
2253
  if (genericError) {
1255
2254
  const errorType = genericError[1];
@@ -1294,6 +2293,125 @@ function chooseStrongestFailureItems(items) {
1294
2293
  }
1295
2294
  return order.map((label) => strongest.get(label));
1296
2295
  }
2296
+ function extractJsTestFile(value) {
2297
+ const match = value.match(/([A-Za-z0-9_./-]+\.(?:test|spec)\.[cm]?[jt]sx?)/i);
2298
+ return match ? normalizeAnchorFile(match[1]) : null;
2299
+ }
2300
+ function normalizeJsFailureLabel(label) {
2301
+ return cleanFailureLabel(label).replace(/^[❯×]\s*/, "").replace(/\s+\[[^\]]+\]\s*$/, "").replace(/\s+/g, " ").trim();
2302
+ }
2303
+ function classifyFailureLines(args) {
2304
+ let observedAnchor = null;
2305
+ let strongest = null;
2306
+ for (const line of args.lines) {
2307
+ observedAnchor = parseObservedAnchor(line) ?? observedAnchor;
2308
+ const classification = classifyFailureReason(line, {
2309
+ duringCollection: args.duringCollection
2310
+ });
2311
+ if (!classification) {
2312
+ continue;
2313
+ }
2314
+ const score = scoreFailureReason(classification.reason);
2315
+ if (!strongest || score > strongest.score) {
2316
+ strongest = {
2317
+ classification,
2318
+ score,
2319
+ observedAnchor: parseObservedAnchor(line) ?? observedAnchor
2320
+ };
2321
+ }
2322
+ }
2323
+ if (!strongest) {
2324
+ return null;
2325
+ }
2326
+ return {
2327
+ classification: strongest.classification,
2328
+ observedAnchor: strongest.observedAnchor ?? observedAnchor
2329
+ };
2330
+ }
2331
+ function collectJsFailureBlocks(input) {
2332
+ const blocks = [];
2333
+ let current = null;
2334
+ let section = null;
2335
+ let currentFile = null;
2336
+ const flushCurrent = () => {
2337
+ if (!current) {
2338
+ return;
2339
+ }
2340
+ blocks.push(current);
2341
+ current = null;
2342
+ };
2343
+ for (const rawLine of input.split("\n")) {
2344
+ const line = rawLine.trimEnd();
2345
+ const trimmed = line.trim();
2346
+ if (/⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(line)) {
2347
+ flushCurrent();
2348
+ section = "failed_tests";
2349
+ continue;
2350
+ }
2351
+ if (/⎯{2,}\s+Failed Suites?\s+\d+\s+⎯{2,}/.test(line)) {
2352
+ flushCurrent();
2353
+ section = "failed_suites";
2354
+ continue;
2355
+ }
2356
+ if (section && /^⎯{2,}.+⎯{2,}\s*$/.test(line)) {
2357
+ flushCurrent();
2358
+ section = null;
2359
+ continue;
2360
+ }
2361
+ const progress = line.match(
2362
+ /^(.+?\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>.+?)?)\s+(FAILED|ERROR)\s+\[[^\]]+\]\s*$/
2363
+ );
2364
+ if (progress) {
2365
+ flushCurrent();
2366
+ const label = normalizeJsFailureLabel(progress[1]);
2367
+ current = {
2368
+ label,
2369
+ status: progress[2] === "ERROR" ? "error" : "failed",
2370
+ detailLines: []
2371
+ };
2372
+ currentFile = extractJsTestFile(label);
2373
+ continue;
2374
+ }
2375
+ const failHeader = line.match(/^\s*FAIL\s+(.+)$/);
2376
+ if (failHeader) {
2377
+ const label = normalizeJsFailureLabel(failHeader[1]);
2378
+ if (extractJsTestFile(label)) {
2379
+ flushCurrent();
2380
+ current = {
2381
+ label,
2382
+ status: section === "failed_suites" || !label.includes(" > ") ? "error" : "failed",
2383
+ detailLines: []
2384
+ };
2385
+ currentFile = extractJsTestFile(label);
2386
+ continue;
2387
+ }
2388
+ }
2389
+ const failedTest = line.match(/^\s*×\s+(.+)$/);
2390
+ if (failedTest && (section === "failed_tests" || extractJsTestFile(failedTest[1]))) {
2391
+ flushCurrent();
2392
+ const candidate = normalizeJsFailureLabel(failedTest[1]);
2393
+ const file = extractJsTestFile(candidate) ?? currentFile;
2394
+ const label = file && !extractJsTestFile(candidate) ? `${file} > ${candidate}` : candidate;
2395
+ current = {
2396
+ label,
2397
+ status: "failed",
2398
+ detailLines: []
2399
+ };
2400
+ currentFile = extractJsTestFile(label) ?? currentFile;
2401
+ continue;
2402
+ }
2403
+ if (/^\s*(?:Tests?|Snapshots?|Test Files?|Test Suites?)\b/.test(line)) {
2404
+ flushCurrent();
2405
+ section = null;
2406
+ continue;
2407
+ }
2408
+ if (current && trimmed.length > 0) {
2409
+ current.detailLines.push(line);
2410
+ }
2411
+ }
2412
+ flushCurrent();
2413
+ return blocks;
2414
+ }
1297
2415
  function collectCollectionFailureItems(input) {
1298
2416
  const items = [];
1299
2417
  const lines = input.split("\n");
@@ -1301,6 +2419,24 @@ function collectCollectionFailureItems(input) {
1301
2419
  let pendingGenericReason = null;
1302
2420
  let currentAnchor = null;
1303
2421
  for (const line of lines) {
2422
+ 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];
2423
+ if (standaloneCollectionLabel) {
2424
+ const classification2 = classifyFailureReason(line, {
2425
+ duringCollection: true
2426
+ });
2427
+ if (classification2) {
2428
+ pushFocusedFailureItem(items, {
2429
+ label: cleanFailureLabel(standaloneCollectionLabel),
2430
+ reason: classification2.reason,
2431
+ group: classification2.group,
2432
+ ...resolveAnchorForLabel({
2433
+ label: cleanFailureLabel(standaloneCollectionLabel),
2434
+ observedAnchor: parseObservedAnchor(line)
2435
+ })
2436
+ });
2437
+ }
2438
+ continue;
2439
+ }
1304
2440
  const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
1305
2441
  if (collecting) {
1306
2442
  if (currentLabel && pendingGenericReason) {
@@ -1389,6 +2525,24 @@ function collectInlineFailureItems(input) {
1389
2525
  })
1390
2526
  });
1391
2527
  }
2528
+ for (const block of collectJsFailureBlocks(input)) {
2529
+ const resolved = classifyFailureLines({
2530
+ lines: block.detailLines,
2531
+ duringCollection: block.status === "error"
2532
+ });
2533
+ if (!resolved) {
2534
+ continue;
2535
+ }
2536
+ pushFocusedFailureItem(items, {
2537
+ label: block.label,
2538
+ reason: resolved.classification.reason,
2539
+ group: resolved.classification.group,
2540
+ ...resolveAnchorForLabel({
2541
+ label: block.label,
2542
+ observedAnchor: resolved.observedAnchor
2543
+ })
2544
+ });
2545
+ }
1392
2546
  return items;
1393
2547
  }
1394
2548
  function collectInlineFailureItemsWithStatus(input) {
@@ -1423,16 +2577,42 @@ function collectInlineFailureItemsWithStatus(input) {
1423
2577
  })
1424
2578
  });
1425
2579
  }
2580
+ for (const block of collectJsFailureBlocks(input)) {
2581
+ const resolved = classifyFailureLines({
2582
+ lines: block.detailLines,
2583
+ duringCollection: block.status === "error"
2584
+ });
2585
+ if (!resolved) {
2586
+ continue;
2587
+ }
2588
+ items.push({
2589
+ label: block.label,
2590
+ reason: resolved.classification.reason,
2591
+ group: resolved.classification.group,
2592
+ status: block.status,
2593
+ ...resolveAnchorForLabel({
2594
+ label: block.label,
2595
+ observedAnchor: resolved.observedAnchor
2596
+ })
2597
+ });
2598
+ }
1426
2599
  return items;
1427
2600
  }
1428
2601
  function collectStandaloneErrorClassifications(input) {
1429
2602
  const classifications = [];
1430
2603
  for (const line of input.split("\n")) {
2604
+ const trimmed = line.trim();
2605
+ if (!trimmed) {
2606
+ continue;
2607
+ }
1431
2608
  const standalone = line.match(/^\s*E\s+(.+)$/);
1432
- if (!standalone) {
2609
+ const candidate = standalone?.[1] ?? (/^(INTERNALERROR>|ConftestImportFailure\b|UsageError:|ERROR:\s*usage:|pytest:\s*error:)/i.test(
2610
+ trimmed
2611
+ ) ? trimmed : null);
2612
+ if (!candidate) {
1433
2613
  continue;
1434
2614
  }
1435
- const classification = classifyFailureReason(standalone[1], {
2615
+ const classification = classifyFailureReason(candidate, {
1436
2616
  duringCollection: false
1437
2617
  });
1438
2618
  if (!classification || classification.reason === "import error during collection") {
@@ -1548,6 +2728,9 @@ function collectFailureLabels(input) {
1548
2728
  pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
1549
2729
  }
1550
2730
  }
2731
+ for (const block of collectJsFailureBlocks(input)) {
2732
+ pushLabel(block.label, block.status);
2733
+ }
1551
2734
  return labels;
1552
2735
  }
1553
2736
  function classifyBucketTypeFromReason(reason) {
@@ -1557,6 +2740,60 @@ function classifyBucketTypeFromReason(reason) {
1557
2740
  if (reason.startsWith("fixture guard:")) {
1558
2741
  return "fixture_guard_failure";
1559
2742
  }
2743
+ if (reason.startsWith("timeout:")) {
2744
+ return "timeout_failure";
2745
+ }
2746
+ if (reason.startsWith("permission:")) {
2747
+ return "permission_denied_failure";
2748
+ }
2749
+ if (reason.startsWith("async loop:")) {
2750
+ return "async_event_loop_failure";
2751
+ }
2752
+ if (reason.startsWith("fixture teardown:")) {
2753
+ return "fixture_teardown_failure";
2754
+ }
2755
+ if (reason.startsWith("db migration:")) {
2756
+ return "db_migration_failure";
2757
+ }
2758
+ if (reason.startsWith("configuration:")) {
2759
+ return "configuration_error";
2760
+ }
2761
+ if (reason.startsWith("xdist worker crash:")) {
2762
+ return "xdist_worker_crash";
2763
+ }
2764
+ if (reason.startsWith("type error:")) {
2765
+ return "type_error_failure";
2766
+ }
2767
+ if (reason.startsWith("resource leak:")) {
2768
+ return "resource_leak_warning";
2769
+ }
2770
+ if (reason.startsWith("django db access:")) {
2771
+ return "django_db_access_denied";
2772
+ }
2773
+ if (reason.startsWith("network:")) {
2774
+ return "network_failure";
2775
+ }
2776
+ if (reason.startsWith("segfault:")) {
2777
+ return "subprocess_crash_segfault";
2778
+ }
2779
+ if (reason.startsWith("flaky:")) {
2780
+ return "flaky_test_detected";
2781
+ }
2782
+ if (reason.startsWith("serialization:")) {
2783
+ return "serialization_encoding_failure";
2784
+ }
2785
+ if (reason.startsWith("file not found:")) {
2786
+ return "file_not_found_failure";
2787
+ }
2788
+ if (reason.startsWith("memory:")) {
2789
+ return "memory_error";
2790
+ }
2791
+ if (reason.startsWith("deprecation as error:")) {
2792
+ return "deprecation_warning_as_error";
2793
+ }
2794
+ if (reason.startsWith("xfail strict:")) {
2795
+ return "xfail_strict_unexpected_pass";
2796
+ }
1560
2797
  if (reason.startsWith("service unavailable:")) {
1561
2798
  return "service_unavailable";
1562
2799
  }
@@ -1566,6 +2803,9 @@ function classifyBucketTypeFromReason(reason) {
1566
2803
  if (reason.startsWith("auth bypass absent:")) {
1567
2804
  return "auth_bypass_absent";
1568
2805
  }
2806
+ if (reason.startsWith("snapshot mismatch:")) {
2807
+ return "snapshot_mismatch";
2808
+ }
1569
2809
  if (reason.startsWith("missing module:")) {
1570
2810
  return "import_dependency_failure";
1571
2811
  }
@@ -1578,9 +2818,6 @@ function classifyBucketTypeFromReason(reason) {
1578
2818
  return "unknown_failure";
1579
2819
  }
1580
2820
  function synthesizeSharedBlockerBucket(args) {
1581
- if (args.errors === 0) {
1582
- return null;
1583
- }
1584
2821
  const visibleReasonGroups = /* @__PURE__ */ new Map();
1585
2822
  for (const item of args.visibleErrorItems) {
1586
2823
  const entry = visibleReasonGroups.get(item.reason);
@@ -1595,7 +2832,7 @@ function synthesizeSharedBlockerBucket(args) {
1595
2832
  items: [item]
1596
2833
  });
1597
2834
  }
1598
- const top = [...visibleReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
2835
+ const top = [...visibleReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
1599
2836
  const standaloneReasonGroups = /* @__PURE__ */ new Map();
1600
2837
  for (const classification of collectStandaloneErrorClassifications(args.input)) {
1601
2838
  const entry = standaloneReasonGroups.get(classification.reason);
@@ -1608,7 +2845,7 @@ function synthesizeSharedBlockerBucket(args) {
1608
2845
  group: classification.group
1609
2846
  });
1610
2847
  }
1611
- const standaloneTop = [...standaloneReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
2848
+ const standaloneTop = [...standaloneReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
1612
2849
  const visibleTopReason = top?.[0];
1613
2850
  const visibleTopStats = top?.[1];
1614
2851
  const standaloneTopReason = standaloneTop?.[0];
@@ -1647,6 +2884,12 @@ function synthesizeSharedBlockerBucket(args) {
1647
2884
  let hint;
1648
2885
  if (envVar) {
1649
2886
  hint = `Set ${envVar} (or pass --pgtest-dsn) before rerunning DB-isolated tests.`;
2887
+ } else if (effectiveReason.startsWith("configuration:")) {
2888
+ hint = "Fix the pytest configuration or conftest import error before rerunning the suite.";
2889
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
2890
+ hint = "Check shared state, worker startup, or resource contention between xdist workers before rerunning.";
2891
+ } else if (effectiveReason.startsWith("network:")) {
2892
+ hint = "Restore DNS, TLS, or outbound network access for the affected dependency before rerunning.";
1650
2893
  } else if (effectiveReason.startsWith("fixture guard:")) {
1651
2894
  hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
1652
2895
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -1661,6 +2904,12 @@ function synthesizeSharedBlockerBucket(args) {
1661
2904
  let headline;
1662
2905
  if (envVar) {
1663
2906
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors require ${envVar} for DB-isolated tests.`;
2907
+ } else if (effectiveReason.startsWith("configuration:")) {
2908
+ headline = `Shared blocker: ${atLeastPrefix}${countText} visible failure${countText === 1 ? "" : "s"} are caused by a pytest configuration error.`;
2909
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
2910
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by xdist worker crashes.`;
2911
+ } else if (effectiveReason.startsWith("network:")) {
2912
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by a network dependency failure.`;
1664
2913
  } else if (effectiveReason.startsWith("fixture guard:")) {
1665
2914
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
1666
2915
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -1691,22 +2940,28 @@ function synthesizeSharedBlockerBucket(args) {
1691
2940
  };
1692
2941
  }
1693
2942
  function synthesizeImportDependencyBucket(args) {
1694
- if (args.errors === 0) {
1695
- return null;
1696
- }
1697
- const importItems = args.visibleErrorItems.filter((item) => item.reason.startsWith("missing module:"));
1698
- if (importItems.length < 2) {
2943
+ const visibleImportItems = args.visibleErrorItems.filter(
2944
+ (item) => item.reason.startsWith("missing module:")
2945
+ );
2946
+ const inlineImportItems = chooseStrongestFailureItems(
2947
+ args.inlineItems.filter((item) => item.reason.startsWith("missing module:"))
2948
+ );
2949
+ const importItems = visibleImportItems.length > 0 ? visibleImportItems : inlineImportItems.map((item) => ({
2950
+ ...item,
2951
+ status: "failed"
2952
+ }));
2953
+ if (importItems.length === 0) {
1699
2954
  return null;
1700
2955
  }
1701
2956
  const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
1702
- const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >= 3 && args.errors >= importItems.length ? args.errors : void 0;
2957
+ const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >= 2 && args.errors >= importItems.length ? args.errors : void 0;
1703
2958
  const modules = Array.from(
1704
2959
  new Set(
1705
2960
  importItems.map((item) => item.reason.replace("missing module:", "").trim()).filter(Boolean)
1706
2961
  )
1707
2962
  ).slice(0, 6);
1708
2963
  const headlineCount = countClaimed ?? importItems.length;
1709
- 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.`;
2964
+ 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.`;
1710
2965
  const summaryLines = [headline];
1711
2966
  if (modules.length > 0) {
1712
2967
  summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
@@ -1716,7 +2971,7 @@ function synthesizeImportDependencyBucket(args) {
1716
2971
  headline,
1717
2972
  countVisible: importItems.length,
1718
2973
  countClaimed,
1719
- reason: "missing dependencies during test collection",
2974
+ reason: modules.length === 1 ? `missing module: ${modules[0]}` : "missing dependencies during test collection",
1720
2975
  representativeItems: importItems.slice(0, 4).map((item) => ({
1721
2976
  label: item.label,
1722
2977
  reason: item.reason,
@@ -1735,7 +2990,7 @@ function synthesizeImportDependencyBucket(args) {
1735
2990
  };
1736
2991
  }
1737
2992
  function isContractDriftLabel(label) {
1738
- return /(freeze|snapshot|contract|manifest|openapi)/i.test(label);
2993
+ return /(freeze|contract|manifest|openapi|golden)/i.test(label);
1739
2994
  }
1740
2995
  function looksLikeTaskKey(value) {
1741
2996
  return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
@@ -1866,13 +3121,67 @@ function synthesizeContractDriftBucket(args) {
1866
3121
  overflowLabel: "changed entities"
1867
3122
  };
1868
3123
  }
3124
+ function synthesizeSnapshotMismatchBucket(args) {
3125
+ const snapshotItems = chooseStrongestFailureItems(
3126
+ args.inlineItems.filter((item) => item.reason.startsWith("snapshot mismatch:"))
3127
+ );
3128
+ if (snapshotItems.length === 0) {
3129
+ return null;
3130
+ }
3131
+ const countClaimed = args.snapshotFailures && args.snapshotFailures >= snapshotItems.length ? args.snapshotFailures : void 0;
3132
+ const countText = countClaimed ?? snapshotItems.length;
3133
+ const summaryLines = [
3134
+ `Snapshot mismatches: ${formatCount2(countText, "snapshot expectation")} ${countText === 1 ? "is" : "are"} out of date with current output.`
3135
+ ];
3136
+ return {
3137
+ type: "snapshot_mismatch",
3138
+ headline: summaryLines[0],
3139
+ countVisible: snapshotItems.length,
3140
+ countClaimed,
3141
+ reason: "snapshot mismatch: snapshot expectations differ from current output",
3142
+ representativeItems: snapshotItems.slice(0, 4),
3143
+ entities: snapshotItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
3144
+ hint: "Update the snapshots if these output changes are intentional.",
3145
+ confidence: countClaimed ? 0.92 : 0.8,
3146
+ summaryLines,
3147
+ overflowCount: Math.max((countClaimed ?? snapshotItems.length) - Math.min(snapshotItems.length, 4), 0),
3148
+ overflowLabel: "snapshot failures"
3149
+ };
3150
+ }
3151
+ function synthesizeTimeoutBucket(args) {
3152
+ const timeoutItems = chooseStrongestFailureItems(
3153
+ args.inlineItems.filter((item) => item.reason.startsWith("timeout:"))
3154
+ );
3155
+ if (timeoutItems.length === 0) {
3156
+ return null;
3157
+ }
3158
+ const summaryLines = [
3159
+ `Timeout failures: ${formatCount2(timeoutItems.length, "test")} exceeded the configured timeout threshold.`
3160
+ ];
3161
+ return {
3162
+ type: "timeout_failure",
3163
+ headline: summaryLines[0],
3164
+ countVisible: timeoutItems.length,
3165
+ countClaimed: timeoutItems.length,
3166
+ reason: timeoutItems.length === 1 ? timeoutItems[0].reason : "timeout: tests exceeded the configured timeout threshold",
3167
+ representativeItems: timeoutItems.slice(0, 4),
3168
+ entities: timeoutItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
3169
+ hint: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning.",
3170
+ confidence: 0.84,
3171
+ summaryLines,
3172
+ overflowCount: Math.max(timeoutItems.length - Math.min(timeoutItems.length, 4), 0),
3173
+ overflowLabel: "timeout failures"
3174
+ };
3175
+ }
1869
3176
  function analyzeTestStatus(input) {
1870
- const passed = getCount(input, "passed");
1871
- const failed = getCount(input, "failed");
1872
- const errors = Math.max(getCount(input, "errors"), getCount(input, "error"));
1873
- const skipped = getCount(input, "skipped");
3177
+ const runner = detectTestRunner(input);
3178
+ const counts = extractTestStatusCounts(input, runner);
3179
+ const passed = counts.passed;
3180
+ const failed = counts.failed;
3181
+ const errors = counts.errors;
3182
+ const skipped = counts.skipped;
1874
3183
  const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
1875
- const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
3184
+ 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);
1876
3185
  const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
1877
3186
  const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
1878
3187
  const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
@@ -1899,7 +3208,8 @@ function analyzeTestStatus(input) {
1899
3208
  if (!sharedBlocker) {
1900
3209
  const importDependencyBucket = synthesizeImportDependencyBucket({
1901
3210
  errors,
1902
- visibleErrorItems
3211
+ visibleErrorItems,
3212
+ inlineItems
1903
3213
  });
1904
3214
  if (importDependencyBucket) {
1905
3215
  buckets.push(importDependencyBucket);
@@ -1912,11 +3222,26 @@ function analyzeTestStatus(input) {
1912
3222
  if (contractDrift) {
1913
3223
  buckets.push(contractDrift);
1914
3224
  }
3225
+ const snapshotMismatch = synthesizeSnapshotMismatchBucket({
3226
+ inlineItems,
3227
+ snapshotFailures: counts.snapshotFailures
3228
+ });
3229
+ if (snapshotMismatch) {
3230
+ buckets.push(snapshotMismatch);
3231
+ }
3232
+ const timeoutBucket = synthesizeTimeoutBucket({
3233
+ inlineItems
3234
+ });
3235
+ if (timeoutBucket) {
3236
+ buckets.push(timeoutBucket);
3237
+ }
1915
3238
  return {
3239
+ runner,
1916
3240
  passed,
1917
3241
  failed,
1918
3242
  errors,
1919
3243
  skipped,
3244
+ snapshotFailures: counts.snapshotFailures,
1920
3245
  noTestsCollected,
1921
3246
  interrupted,
1922
3247
  collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
@@ -2224,10 +3549,11 @@ async function buildOpenAICompatibleError(response) {
2224
3549
  return new Error(detail);
2225
3550
  }
2226
3551
  var OpenAICompatibleProvider = class {
2227
- name = "openai-compatible";
3552
+ name;
2228
3553
  baseUrl;
2229
3554
  apiKey;
2230
3555
  constructor(options) {
3556
+ this.name = options.name ?? "openai-compatible";
2231
3557
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
2232
3558
  this.apiKey = options.apiKey;
2233
3559
  }
@@ -2303,6 +3629,13 @@ function createProvider(config) {
2303
3629
  apiKey: config.provider.apiKey
2304
3630
  });
2305
3631
  }
3632
+ if (config.provider.provider === "openrouter") {
3633
+ return new OpenAICompatibleProvider({
3634
+ baseUrl: config.provider.baseUrl,
3635
+ apiKey: config.provider.apiKey,
3636
+ name: "openrouter"
3637
+ });
3638
+ }
2306
3639
  throw new Error(`Unsupported provider: ${config.provider.provider}`);
2307
3640
  }
2308
3641
 
@@ -2475,9 +3808,12 @@ function resolvePromptPolicy(args) {
2475
3808
  "Return only valid JSON.",
2476
3809
  `Use this exact contract: ${args.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT}.`,
2477
3810
  "Treat the heuristic context as extraction guidance, but do not invent hidden failures.",
2478
- "Use the heuristic extract as the bucket truth unless the visible command output clearly disproves it.",
3811
+ "Use the heuristic extract as the base bucket truth unless the visible command output clearly disproves it.",
3812
+ "If some visible failure or error families remain unexplained, add at most 2 bucket_supplements for the residual families only.",
3813
+ "Do not rewrite or delete heuristic buckets; only supplement missing residual coverage.",
3814
+ "Keep bucket_supplement counts within the unexplained residual failures or errors.",
2479
3815
  "Identify the dominant blocker, remaining visible failure buckets, the decision, and the next best action.",
2480
- "Set diagnosis_complete to true only when the visible output is already sufficient to stop and act.",
3816
+ "Set diagnosis_complete to true only when the visible output is already sufficient to stop and act and no unknown residual family remains.",
2481
3817
  "Set raw_needed to true only when exact traceback lines are still required.",
2482
3818
  "Set provider_confidence to a number between 0 and 1, or null only when confidence cannot be estimated."
2483
3819
  ] : [
@@ -2994,6 +4330,7 @@ function buildGenericRawSlice(args) {
2994
4330
 
2995
4331
  // src/core/run.ts
2996
4332
  var RETRY_DELAY_MS = 300;
4333
+ var PROVIDER_PENDING_NOTICE_DELAY_MS = 150;
2997
4334
  function estimateTokenCount(text) {
2998
4335
  return Math.max(1, Math.ceil(text.length / 4));
2999
4336
  }
@@ -3014,6 +4351,8 @@ function logVerboseTestStatusTelemetry(args) {
3014
4351
  `${pc.dim("sift")} diagnosis_complete_at_layer=${getDiagnosisCompleteAtLayer(args.contract)}`,
3015
4352
  `${pc.dim("sift")} heuristic_short_circuit=${!args.contract.provider_used && args.contract.diagnosis_complete && !args.contract.raw_needed && !args.contract.provider_failed}`,
3016
4353
  `${pc.dim("sift")} raw_input_chars=${args.request.stdin.length}`,
4354
+ `${pc.dim("sift")} heuristic_input_chars=${args.heuristicInputChars}`,
4355
+ `${pc.dim("sift")} heuristic_input_truncated=${args.heuristicInputTruncated}`,
3017
4356
  `${pc.dim("sift")} prepared_input_chars=${args.prepared.meta.finalLength}`,
3018
4357
  `${pc.dim("sift")} raw_slice_chars=${args.rawSliceChars ?? 0}`,
3019
4358
  `${pc.dim("sift")} provider_input_chars=${args.providerInputChars ?? 0}`,
@@ -3055,6 +4394,7 @@ function buildDryRunOutput(args) {
3055
4394
  responseMode: args.responseMode,
3056
4395
  policy: args.request.policyName ?? null,
3057
4396
  heuristicOutput: args.heuristicOutput ?? null,
4397
+ heuristicInput: args.heuristicInput,
3058
4398
  input: {
3059
4399
  originalLength: args.prepared.meta.originalLength,
3060
4400
  finalLength: args.prepared.meta.finalLength,
@@ -3071,6 +4411,25 @@ function buildDryRunOutput(args) {
3071
4411
  async function delay(ms) {
3072
4412
  await new Promise((resolve) => setTimeout(resolve, ms));
3073
4413
  }
4414
+ function startProviderPendingNotice() {
4415
+ if (!process.stderr.isTTY) {
4416
+ return () => {
4417
+ };
4418
+ }
4419
+ const message = "sift waiting for provider...";
4420
+ let shown = false;
4421
+ const timer = setTimeout(() => {
4422
+ shown = true;
4423
+ process.stderr.write(`${message}\r`);
4424
+ }, PROVIDER_PENDING_NOTICE_DELAY_MS);
4425
+ return () => {
4426
+ clearTimeout(timer);
4427
+ if (!shown) {
4428
+ return;
4429
+ }
4430
+ process.stderr.write(`\r${" ".repeat(message.length)}\r`);
4431
+ };
4432
+ }
3074
4433
  function withInsufficientHint(args) {
3075
4434
  if (!isInsufficientSignalOutput(args.output)) {
3076
4435
  return args.output;
@@ -3091,22 +4450,27 @@ async function generateWithRetry(args) {
3091
4450
  responseMode: args.responseMode,
3092
4451
  jsonResponseFormat: args.request.config.provider.jsonResponseFormat
3093
4452
  });
4453
+ const stopPendingNotice = startProviderPendingNotice();
3094
4454
  try {
3095
- return await generate();
3096
- } catch (error) {
3097
- const reason = error instanceof Error ? error.message : "unknown_error";
3098
- if (!isRetriableReason(reason)) {
3099
- throw error;
3100
- }
3101
- if (args.request.config.runtime.verbose) {
3102
- process.stderr.write(
3103
- `${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
4455
+ try {
4456
+ return await generate();
4457
+ } catch (error) {
4458
+ const reason = error instanceof Error ? error.message : "unknown_error";
4459
+ if (!isRetriableReason(reason)) {
4460
+ throw error;
4461
+ }
4462
+ if (args.request.config.runtime.verbose) {
4463
+ process.stderr.write(
4464
+ `${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
3104
4465
  `
3105
- );
4466
+ );
4467
+ }
4468
+ await delay(RETRY_DELAY_MS);
3106
4469
  }
3107
- await delay(RETRY_DELAY_MS);
4470
+ return await generate();
4471
+ } finally {
4472
+ stopPendingNotice();
3108
4473
  }
3109
- return generate();
3110
4474
  }
3111
4475
  function hasRecognizableTestStatusSignal(input) {
3112
4476
  const analysis = analyzeTestStatus(input);
@@ -3161,11 +4525,22 @@ function buildTestStatusProviderFailureDecision(args) {
3161
4525
  }
3162
4526
  async function runSift(request) {
3163
4527
  const prepared = prepareInput(request.stdin, request.config.input);
4528
+ const heuristicInput = prepared.redacted;
4529
+ const heuristicInputTruncated = false;
4530
+ const heuristicPrepared = {
4531
+ ...prepared,
4532
+ truncated: heuristicInput,
4533
+ meta: {
4534
+ ...prepared.meta,
4535
+ finalLength: heuristicInput.length,
4536
+ truncatedApplied: heuristicInputTruncated
4537
+ }
4538
+ };
3164
4539
  const provider = createProvider(request.config);
3165
- const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(prepared.truncated);
3166
- const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(prepared.truncated) : null;
4540
+ const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(heuristicInput);
4541
+ const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(heuristicInput) : null;
3167
4542
  const testStatusDecision = hasTestStatusSignal && testStatusAnalysis ? buildTestStatusDiagnoseContract({
3168
- input: prepared.truncated,
4543
+ input: heuristicInput,
3169
4544
  analysis: testStatusAnalysis,
3170
4545
  resolvedTests: request.testStatusContext?.resolvedTests,
3171
4546
  remainingTests: request.testStatusContext?.remainingTests
@@ -3180,7 +4555,7 @@ async function runSift(request) {
3180
4555
  `
3181
4556
  );
3182
4557
  }
3183
- const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, prepared.truncated, request.detail);
4558
+ const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, heuristicInput, request.detail);
3184
4559
  if (heuristicOutput) {
3185
4560
  if (request.config.runtime.verbose) {
3186
4561
  process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
@@ -3190,7 +4565,7 @@ async function runSift(request) {
3190
4565
  question: request.question,
3191
4566
  format: request.format,
3192
4567
  goal: request.goal,
3193
- input: prepared.truncated,
4568
+ input: heuristicInput,
3194
4569
  detail: request.detail,
3195
4570
  policyName: request.policyName,
3196
4571
  outputContract: request.policyName === "test-status" && request.goal === "diagnose" && request.format === "json" ? request.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT : request.outputContract,
@@ -3210,6 +4585,11 @@ async function runSift(request) {
3210
4585
  prompt: heuristicPrompt.prompt,
3211
4586
  responseMode: heuristicPrompt.responseMode,
3212
4587
  prepared,
4588
+ heuristicInput: {
4589
+ length: heuristicInput.length,
4590
+ truncatedApplied: heuristicInputTruncated,
4591
+ strategy: "full-redacted"
4592
+ },
3213
4593
  heuristicOutput,
3214
4594
  strategy: "heuristic"
3215
4595
  });
@@ -3223,6 +4603,8 @@ async function runSift(request) {
3223
4603
  logVerboseTestStatusTelemetry({
3224
4604
  request,
3225
4605
  prepared,
4606
+ heuristicInputChars: heuristicInput.length,
4607
+ heuristicInputTruncated,
3226
4608
  contract: testStatusDecision.contract,
3227
4609
  finalOutput
3228
4610
  });
@@ -3274,6 +4656,11 @@ async function runSift(request) {
3274
4656
  prompt: prompt.prompt,
3275
4657
  responseMode: prompt.responseMode,
3276
4658
  prepared: providerPrepared2,
4659
+ heuristicInput: {
4660
+ length: heuristicInput.length,
4661
+ truncatedApplied: heuristicInputTruncated,
4662
+ strategy: "full-redacted"
4663
+ },
3277
4664
  heuristicOutput: testStatusHeuristicOutput,
3278
4665
  strategy: "hybrid"
3279
4666
  });
@@ -3287,10 +4674,11 @@ async function runSift(request) {
3287
4674
  });
3288
4675
  const supplement = parseTestStatusProviderSupplement(result.text);
3289
4676
  const mergedDecision = buildTestStatusDiagnoseContract({
3290
- input: prepared.truncated,
4677
+ input: heuristicInput,
3291
4678
  analysis: testStatusAnalysis,
3292
4679
  resolvedTests: request.testStatusContext?.resolvedTests,
3293
4680
  remainingTests: request.testStatusContext?.remainingTests,
4681
+ providerBucketSupplements: supplement.bucket_supplements,
3294
4682
  contractOverrides: {
3295
4683
  diagnosis_complete: supplement.diagnosis_complete,
3296
4684
  raw_needed: supplement.raw_needed,
@@ -3312,6 +4700,8 @@ async function runSift(request) {
3312
4700
  logVerboseTestStatusTelemetry({
3313
4701
  request,
3314
4702
  prepared,
4703
+ heuristicInputChars: heuristicInput.length,
4704
+ heuristicInputTruncated,
3315
4705
  contract: mergedDecision.contract,
3316
4706
  finalOutput,
3317
4707
  rawSliceChars: rawSlice.text.length,
@@ -3324,7 +4714,7 @@ async function runSift(request) {
3324
4714
  const failureDecision = buildTestStatusProviderFailureDecision({
3325
4715
  request,
3326
4716
  baseDecision: testStatusDecision,
3327
- input: prepared.truncated,
4717
+ input: heuristicInput,
3328
4718
  analysis: testStatusAnalysis,
3329
4719
  reason,
3330
4720
  rawSliceUsed: rawSlice.used,
@@ -3345,6 +4735,8 @@ async function runSift(request) {
3345
4735
  logVerboseTestStatusTelemetry({
3346
4736
  request,
3347
4737
  prepared,
4738
+ heuristicInputChars: heuristicInput.length,
4739
+ heuristicInputTruncated,
3348
4740
  contract: failureDecision.contract,
3349
4741
  finalOutput,
3350
4742
  rawSliceChars: rawSlice.text.length,
@@ -3383,6 +4775,11 @@ async function runSift(request) {
3383
4775
  prompt: providerPrompt.prompt,
3384
4776
  responseMode: providerPrompt.responseMode,
3385
4777
  prepared: providerPrepared,
4778
+ heuristicInput: {
4779
+ length: heuristicInput.length,
4780
+ truncatedApplied: heuristicInputTruncated,
4781
+ strategy: "full-redacted"
4782
+ },
3386
4783
  heuristicOutput: testStatusDecision ? testStatusHeuristicOutput : null,
3387
4784
  strategy: testStatusDecision ? "hybrid" : "provider"
3388
4785
  });
@@ -3430,10 +4827,29 @@ var detailSchema = z2.enum(["standard", "focused", "verbose"]);
3430
4827
  var failureBucketTypeSchema = z2.enum([
3431
4828
  "shared_environment_blocker",
3432
4829
  "fixture_guard_failure",
4830
+ "timeout_failure",
4831
+ "permission_denied_failure",
4832
+ "async_event_loop_failure",
4833
+ "fixture_teardown_failure",
4834
+ "db_migration_failure",
4835
+ "configuration_error",
4836
+ "xdist_worker_crash",
4837
+ "type_error_failure",
4838
+ "resource_leak_warning",
4839
+ "django_db_access_denied",
4840
+ "network_failure",
4841
+ "subprocess_crash_segfault",
4842
+ "flaky_test_detected",
4843
+ "serialization_encoding_failure",
4844
+ "file_not_found_failure",
4845
+ "memory_error",
4846
+ "deprecation_warning_as_error",
4847
+ "xfail_strict_unexpected_pass",
3433
4848
  "service_unavailable",
3434
4849
  "db_connection_failure",
3435
4850
  "auth_bypass_absent",
3436
4851
  "contract_snapshot_drift",
4852
+ "snapshot_mismatch",
3437
4853
  "import_dependency_failure",
3438
4854
  "collection_failure",
3439
4855
  "assertion_failure",
@@ -4419,13 +5835,16 @@ var OPENAI_COMPATIBLE_BASE_URL_ENV = [
4419
5835
  { prefix: "https://api.together.xyz/", envName: "TOGETHER_API_KEY" },
4420
5836
  { prefix: "https://api.groq.com/openai/", envName: "GROQ_API_KEY" }
4421
5837
  ];
5838
+ var NATIVE_PROVIDER_API_KEY_ENV = {
5839
+ openai: "OPENAI_API_KEY",
5840
+ openrouter: "OPENROUTER_API_KEY"
5841
+ };
4422
5842
  var PROVIDER_API_KEY_ENV = {
4423
5843
  anthropic: "ANTHROPIC_API_KEY",
4424
5844
  claude: "ANTHROPIC_API_KEY",
4425
5845
  groq: "GROQ_API_KEY",
4426
- openai: "OPENAI_API_KEY",
4427
- openrouter: "OPENROUTER_API_KEY",
4428
- together: "TOGETHER_API_KEY"
5846
+ together: "TOGETHER_API_KEY",
5847
+ ...NATIVE_PROVIDER_API_KEY_ENV
4429
5848
  };
4430
5849
  function normalizeBaseUrl(baseUrl) {
4431
5850
  if (!baseUrl) {
@@ -4463,7 +5882,11 @@ function resolveProviderApiKey(provider, baseUrl, env) {
4463
5882
 
4464
5883
  // src/config/schema.ts
4465
5884
  import { z as z3 } from "zod";
4466
- var providerNameSchema = z3.enum(["openai", "openai-compatible"]);
5885
+ var providerNameSchema = z3.enum([
5886
+ "openai",
5887
+ "openai-compatible",
5888
+ "openrouter"
5889
+ ]);
4467
5890
  var outputFormatSchema = z3.enum([
4468
5891
  "brief",
4469
5892
  "bullets",
@@ -4492,6 +5915,15 @@ var providerConfigSchema = z3.object({
4492
5915
  temperature: z3.number().min(0).max(2),
4493
5916
  maxOutputTokens: z3.number().int().positive()
4494
5917
  });
5918
+ var providerProfileSchema = z3.object({
5919
+ model: z3.string().min(1).optional(),
5920
+ baseUrl: z3.string().url().optional(),
5921
+ apiKey: z3.string().optional()
5922
+ });
5923
+ var providerProfilesSchema = z3.object({
5924
+ openai: providerProfileSchema.optional(),
5925
+ openrouter: providerProfileSchema.optional()
5926
+ }).optional();
4495
5927
  var inputConfigSchema = z3.object({
4496
5928
  stripAnsi: z3.boolean(),
4497
5929
  redact: z3.boolean(),
@@ -4516,10 +5948,19 @@ var siftConfigSchema = z3.object({
4516
5948
  provider: providerConfigSchema,
4517
5949
  input: inputConfigSchema,
4518
5950
  runtime: runtimeConfigSchema,
4519
- presets: z3.record(presetDefinitionSchema)
5951
+ presets: z3.record(presetDefinitionSchema),
5952
+ providerProfiles: providerProfilesSchema
4520
5953
  });
4521
5954
 
4522
5955
  // src/config/resolve.ts
5956
+ var PROVIDER_DEFAULT_OVERRIDES = {
5957
+ openrouter: {
5958
+ provider: {
5959
+ model: "openrouter/free",
5960
+ baseUrl: "https://openrouter.ai/api/v1"
5961
+ }
5962
+ }
5963
+ };
4523
5964
  function isRecord(value) {
4524
5965
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4525
5966
  }
@@ -4582,13 +6023,32 @@ function buildCredentialEnvOverrides(env, context) {
4582
6023
  }
4583
6024
  };
4584
6025
  }
6026
+ function getBaseConfigForProvider(provider) {
6027
+ return mergeDefined(defaultConfig, provider ? PROVIDER_DEFAULT_OVERRIDES[provider] : {});
6028
+ }
6029
+ function resolveProvisionalProvider(args) {
6030
+ const provisional = mergeDefined(
6031
+ mergeDefined(
6032
+ mergeDefined(defaultConfig, args.fileConfig),
6033
+ args.nonCredentialEnvConfig
6034
+ ),
6035
+ stripApiKey(args.cliOverrides) ?? {}
6036
+ );
6037
+ return provisional.provider.provider;
6038
+ }
4585
6039
  function resolveConfig(options = {}) {
4586
6040
  const env = options.env ?? process.env;
4587
6041
  const fileConfig = loadRawConfig(options.configPath);
4588
6042
  const nonCredentialEnvConfig = buildNonCredentialEnvOverrides(env);
6043
+ const provisionalProvider = resolveProvisionalProvider({
6044
+ fileConfig,
6045
+ nonCredentialEnvConfig,
6046
+ cliOverrides: options.cliOverrides
6047
+ });
6048
+ const baseConfig = getBaseConfigForProvider(provisionalProvider);
4589
6049
  const contextConfig = mergeDefined(
4590
6050
  mergeDefined(
4591
- mergeDefined(defaultConfig, fileConfig),
6051
+ mergeDefined(baseConfig, fileConfig),
4592
6052
  nonCredentialEnvConfig
4593
6053
  ),
4594
6054
  stripApiKey(options.cliOverrides) ?? {}
@@ -4600,7 +6060,7 @@ function resolveConfig(options = {}) {
4600
6060
  const merged = mergeDefined(
4601
6061
  mergeDefined(
4602
6062
  mergeDefined(
4603
- mergeDefined(defaultConfig, fileConfig),
6063
+ mergeDefined(baseConfig, fileConfig),
4604
6064
  nonCredentialEnvConfig
4605
6065
  ),
4606
6066
  credentialEnvConfig