@bilalimamoglu/sift 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1876,7 +1876,7 @@ function showPreset(config, name, includeInternal = false) {
1876
1876
  }
1877
1877
 
1878
1878
  // src/core/escalate.ts
1879
- import pc3 from "picocolors";
1879
+ import pc4 from "picocolors";
1880
1880
 
1881
1881
  // src/core/insufficient.ts
1882
1882
  function isInsufficientSignalOutput(output) {
@@ -1897,8 +1897,8 @@ function buildInsufficientSignalOutput(input) {
1897
1897
  } else {
1898
1898
  hint = "Hint: the captured output did not contain a clear answer for this preset.";
1899
1899
  }
1900
- return `${INSUFFICIENT_SIGNAL_TEXT}
1901
- ${hint}`;
1900
+ const presetSuggestion = input.recognizedRunner && input.recognizedRunner !== "unknown" && input.presetName !== "test-status" ? `Hint: captured output looks like ${input.recognizedRunner} test output; try --preset test-status.` : null;
1901
+ return [INSUFFICIENT_SIGNAL_TEXT, hint, presetSuggestion].filter((value) => Boolean(value)).join("\n");
1902
1902
  }
1903
1903
 
1904
1904
  // src/core/run.ts
@@ -2124,7 +2124,7 @@ function createProvider(config) {
2124
2124
 
2125
2125
  // src/core/testStatusDecision.ts
2126
2126
  import { z as z2 } from "zod";
2127
- 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[]}';
2127
+ var TEST_STATUS_DIAGNOSE_JSON_CONTRACT = '{"status":"ok|insufficient","diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","primary_suspect_kind":"test|app_code|config|environment|tooling|unknown","confidence_reason":string,"dominant_blocker_bucket_index":number|null,"provider_used":boolean,"provider_confidence":number|null,"provider_failed":boolean,"raw_slice_used":boolean,"raw_slice_strategy":"none|bucket_evidence|traceback_window|head_tail","resolved_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_subset_available":boolean,"main_buckets":[{"bucket_index":number,"label":string,"count":number,"root_cause":string,"suspect_kind":"test|app_code|config|environment|tooling|unknown","fix_hint":string,"evidence":string[],"bucket_confidence":number,"root_cause_confidence":number,"dominant":boolean,"secondary_visible_despite_blocker":boolean,"mini_diff":{"added_paths"?:number,"removed_models"?:number,"changed_task_mappings"?:number}|null}],"read_targets":[{"file":string,"line":number|null,"why":string,"bucket_index":number,"context_hint":{"start_line":number|null,"end_line":number|null,"search_hint":string|null}}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string},"resolved_tests"?:string[],"remaining_tests"?:string[]}';
2128
2128
  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}}';
2129
2129
  var nextBestActionSchema = z2.object({
2130
2130
  code: z2.enum([
@@ -2166,6 +2166,15 @@ var testStatusDiagnoseContractSchema = z2.object({
2166
2166
  additional_source_read_likely_low_value: z2.boolean(),
2167
2167
  read_raw_only_if: z2.string().nullable(),
2168
2168
  decision: z2.enum(["stop", "zoom", "read_source", "read_raw"]),
2169
+ primary_suspect_kind: z2.enum([
2170
+ "test",
2171
+ "app_code",
2172
+ "config",
2173
+ "environment",
2174
+ "tooling",
2175
+ "unknown"
2176
+ ]),
2177
+ confidence_reason: z2.string().min(1),
2169
2178
  dominant_blocker_bucket_index: z2.number().int().nullable(),
2170
2179
  provider_used: z2.boolean(),
2171
2180
  provider_confidence: z2.number().min(0).max(1).nullable(),
@@ -2180,6 +2189,15 @@ var testStatusDiagnoseContractSchema = z2.object({
2180
2189
  label: z2.string(),
2181
2190
  count: z2.number().int(),
2182
2191
  root_cause: z2.string(),
2192
+ suspect_kind: z2.enum([
2193
+ "test",
2194
+ "app_code",
2195
+ "config",
2196
+ "environment",
2197
+ "tooling",
2198
+ "unknown"
2199
+ ]),
2200
+ fix_hint: z2.string().min(1),
2183
2201
  evidence: z2.array(z2.string()).max(2),
2184
2202
  bucket_confidence: z2.number(),
2185
2203
  root_cause_confidence: z2.number(),
@@ -2229,6 +2247,255 @@ var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.om
2229
2247
  function parseTestStatusProviderSupplement(input) {
2230
2248
  return testStatusProviderSupplementSchema.parse(JSON.parse(input));
2231
2249
  }
2250
+ var extendedBucketSpecs = [
2251
+ {
2252
+ prefix: "service unavailable:",
2253
+ type: "service_unavailable",
2254
+ label: "service unavailable",
2255
+ genericTitle: "Service unavailable failures",
2256
+ defaultCoverage: "error",
2257
+ rootCauseConfidence: 0.9,
2258
+ dominantPriority: 2,
2259
+ dominantBlocker: true,
2260
+ why: "it contains the dependency service or API path that is unavailable in the test environment",
2261
+ fix: "Restore the dependency service or test double before rerunning the full suite."
2262
+ },
2263
+ {
2264
+ prefix: "db refused:",
2265
+ type: "db_connection_failure",
2266
+ label: "database connection",
2267
+ genericTitle: "Database connection failures",
2268
+ defaultCoverage: "error",
2269
+ rootCauseConfidence: 0.9,
2270
+ dominantPriority: 2,
2271
+ dominantBlocker: true,
2272
+ why: "it contains the database host, DSN, or startup path that is refusing connections",
2273
+ fix: "Restore the test database connectivity before rerunning the full suite."
2274
+ },
2275
+ {
2276
+ prefix: "auth bypass absent:",
2277
+ type: "auth_bypass_absent",
2278
+ label: "auth bypass missing",
2279
+ genericTitle: "Auth bypass setup failures",
2280
+ defaultCoverage: "error",
2281
+ rootCauseConfidence: 0.86,
2282
+ dominantPriority: 2,
2283
+ dominantBlocker: true,
2284
+ why: "it contains the auth bypass fixture or setup path that tests expected to be active",
2285
+ fix: "Restore the test auth bypass fixture or mock before rerunning the full suite."
2286
+ },
2287
+ {
2288
+ prefix: "snapshot mismatch:",
2289
+ type: "snapshot_mismatch",
2290
+ label: "snapshot mismatch",
2291
+ genericTitle: "Snapshot mismatches",
2292
+ defaultCoverage: "failed",
2293
+ rootCauseConfidence: 0.84,
2294
+ why: "it contains the failing snapshot expectation behind this bucket",
2295
+ fix: "Update the snapshots if these output changes are intentional, then rerun the suite."
2296
+ },
2297
+ {
2298
+ prefix: "timeout:",
2299
+ type: "timeout_failure",
2300
+ label: "timeout",
2301
+ genericTitle: "Timeout failures",
2302
+ defaultCoverage: "mixed",
2303
+ rootCauseConfidence: 0.9,
2304
+ why: "it contains the test or fixture that exceeded the timeout threshold",
2305
+ fix: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning."
2306
+ },
2307
+ {
2308
+ prefix: "permission:",
2309
+ type: "permission_denied_failure",
2310
+ label: "permission denied",
2311
+ genericTitle: "Permission failures",
2312
+ defaultCoverage: "error",
2313
+ rootCauseConfidence: 0.85,
2314
+ why: "it contains the file, socket, or port access that was denied",
2315
+ fix: "Check file or port permissions in the CI environment before rerunning."
2316
+ },
2317
+ {
2318
+ prefix: "async loop:",
2319
+ type: "async_event_loop_failure",
2320
+ label: "async event loop",
2321
+ genericTitle: "Async event loop failures",
2322
+ defaultCoverage: "mixed",
2323
+ rootCauseConfidence: 0.88,
2324
+ why: "it contains the async setup or coroutine that caused the event loop error",
2325
+ fix: "Check event loop scope and pytest-asyncio configuration before rerunning."
2326
+ },
2327
+ {
2328
+ prefix: "fixture teardown:",
2329
+ type: "fixture_teardown_failure",
2330
+ label: "fixture teardown",
2331
+ genericTitle: "Fixture teardown failures",
2332
+ defaultCoverage: "error",
2333
+ rootCauseConfidence: 0.85,
2334
+ why: "it contains the fixture teardown path that failed after the test body completed",
2335
+ fix: "Inspect the teardown cleanup path and restore idempotent fixture cleanup before rerunning."
2336
+ },
2337
+ {
2338
+ prefix: "db migration:",
2339
+ type: "db_migration_failure",
2340
+ label: "db migration",
2341
+ genericTitle: "DB migration failures",
2342
+ defaultCoverage: "error",
2343
+ rootCauseConfidence: 0.9,
2344
+ why: "it contains the migration or model definition behind the missing table or relation",
2345
+ fix: "Run pending migrations or fix the expected model schema before rerunning."
2346
+ },
2347
+ {
2348
+ prefix: "configuration:",
2349
+ type: "configuration_error",
2350
+ label: "configuration error",
2351
+ genericTitle: "Configuration errors",
2352
+ defaultCoverage: "error",
2353
+ rootCauseConfidence: 0.95,
2354
+ dominantPriority: 4,
2355
+ dominantBlocker: true,
2356
+ why: "it contains the pytest configuration or conftest setup error that blocks the run",
2357
+ fix: "Fix the pytest configuration, CLI usage, or conftest import error before rerunning."
2358
+ },
2359
+ {
2360
+ prefix: "xdist worker crash:",
2361
+ type: "xdist_worker_crash",
2362
+ label: "xdist worker crash",
2363
+ genericTitle: "xdist worker crashes",
2364
+ defaultCoverage: "error",
2365
+ rootCauseConfidence: 0.92,
2366
+ dominantPriority: 3,
2367
+ why: "it contains the worker startup or shared-state path that crashed an xdist worker",
2368
+ fix: "Check shared state, worker startup hooks, or resource contention between workers before rerunning."
2369
+ },
2370
+ {
2371
+ prefix: "type error:",
2372
+ type: "type_error_failure",
2373
+ label: "type error",
2374
+ genericTitle: "Type errors",
2375
+ defaultCoverage: "mixed",
2376
+ rootCauseConfidence: 0.8,
2377
+ why: "it contains the call site or fixture value that triggered the type error",
2378
+ fix: "Inspect the mismatched argument or object shape and rerun the full suite at standard."
2379
+ },
2380
+ {
2381
+ prefix: "resource leak:",
2382
+ type: "resource_leak_warning",
2383
+ label: "resource leak",
2384
+ genericTitle: "Resource leak warnings",
2385
+ defaultCoverage: "mixed",
2386
+ rootCauseConfidence: 0.74,
2387
+ why: "it contains the warning source behind the leaked file, socket, or coroutine",
2388
+ fix: "Close the leaked resource or suppress the warning only if the cleanup is intentional."
2389
+ },
2390
+ {
2391
+ prefix: "django db access:",
2392
+ type: "django_db_access_denied",
2393
+ label: "django db access",
2394
+ genericTitle: "Django DB access failures",
2395
+ defaultCoverage: "error",
2396
+ rootCauseConfidence: 0.95,
2397
+ why: "it needs the @pytest.mark.django_db decorator or fixture permission to access the database",
2398
+ fix: "Add @pytest.mark.django_db to the test or class before rerunning."
2399
+ },
2400
+ {
2401
+ prefix: "network:",
2402
+ type: "network_failure",
2403
+ label: "network failure",
2404
+ genericTitle: "Network failures",
2405
+ defaultCoverage: "error",
2406
+ rootCauseConfidence: 0.88,
2407
+ dominantPriority: 2,
2408
+ why: "it contains the host, URL, or TLS path behind the network failure",
2409
+ fix: "Check DNS, outbound network access, retries, or TLS trust before rerunning."
2410
+ },
2411
+ {
2412
+ prefix: "segfault:",
2413
+ type: "subprocess_crash_segfault",
2414
+ label: "segfault",
2415
+ genericTitle: "Segfault crashes",
2416
+ defaultCoverage: "mixed",
2417
+ rootCauseConfidence: 0.8,
2418
+ why: "it contains the subprocess or native extension path that crashed with SIGSEGV",
2419
+ fix: "Inspect the native extension, subprocess boundary, or incompatible binary before rerunning."
2420
+ },
2421
+ {
2422
+ prefix: "flaky:",
2423
+ type: "flaky_test_detected",
2424
+ label: "flaky test",
2425
+ genericTitle: "Flaky test detections",
2426
+ defaultCoverage: "mixed",
2427
+ rootCauseConfidence: 0.72,
2428
+ why: "it contains the rerun-prone test that behaved inconsistently across attempts",
2429
+ fix: "Stabilize the nondeterministic test or fixture before relying on reruns."
2430
+ },
2431
+ {
2432
+ prefix: "serialization:",
2433
+ type: "serialization_encoding_failure",
2434
+ label: "serialization or encoding",
2435
+ genericTitle: "Serialization or encoding failures",
2436
+ defaultCoverage: "mixed",
2437
+ rootCauseConfidence: 0.78,
2438
+ why: "it contains the serialization or decoding path behind the malformed payload",
2439
+ fix: "Inspect the encoded payload, serializer, or fixture data before rerunning."
2440
+ },
2441
+ {
2442
+ prefix: "file not found:",
2443
+ type: "file_not_found_failure",
2444
+ label: "file not found",
2445
+ genericTitle: "Missing file failures",
2446
+ defaultCoverage: "mixed",
2447
+ rootCauseConfidence: 0.82,
2448
+ why: "it contains the missing file path or fixture artifact required by the test",
2449
+ fix: "Restore the missing file, fixture artifact, or working-directory assumption before rerunning."
2450
+ },
2451
+ {
2452
+ prefix: "memory:",
2453
+ type: "memory_error",
2454
+ label: "memory error",
2455
+ genericTitle: "Memory failures",
2456
+ defaultCoverage: "mixed",
2457
+ rootCauseConfidence: 0.78,
2458
+ why: "it contains the allocation path that exhausted available memory",
2459
+ fix: "Reduce memory pressure or investigate the large allocation before rerunning."
2460
+ },
2461
+ {
2462
+ prefix: "deprecation as error:",
2463
+ type: "deprecation_warning_as_error",
2464
+ label: "deprecation as error",
2465
+ genericTitle: "Deprecation warnings as errors",
2466
+ defaultCoverage: "mixed",
2467
+ rootCauseConfidence: 0.74,
2468
+ why: "it contains the deprecated API or warning filter that is failing the test run",
2469
+ fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
2470
+ },
2471
+ {
2472
+ prefix: "assertion failed:",
2473
+ type: "assertion_failure",
2474
+ label: "assertion failure",
2475
+ genericTitle: "Assertion failures",
2476
+ defaultCoverage: "failed",
2477
+ rootCauseConfidence: 0.76,
2478
+ why: "it contains the expected-versus-actual assertion that failed inside the visible test",
2479
+ fix: "Read the assertion diff or expectation and fix the code or expected value before rerunning."
2480
+ },
2481
+ {
2482
+ prefix: "xfail strict:",
2483
+ type: "xfail_strict_unexpected_pass",
2484
+ label: "strict xfail unexpected pass",
2485
+ genericTitle: "Strict xfail unexpected passes",
2486
+ defaultCoverage: "failed",
2487
+ rootCauseConfidence: 0.78,
2488
+ why: "it contains the strict xfail case that unexpectedly passed",
2489
+ fix: "Remove or update the strict xfail expectation if the test is now passing intentionally."
2490
+ }
2491
+ ];
2492
+ function findExtendedBucketSpec(reason) {
2493
+ return extendedBucketSpecs.find((spec) => reason.startsWith(spec.prefix)) ?? null;
2494
+ }
2495
+ function extractReasonDetail(reason, prefix) {
2496
+ const detail = reason.slice(prefix.length).trim();
2497
+ return detail.length > 0 ? detail : null;
2498
+ }
2232
2499
  function formatCount(count, singular, plural = `${singular}s`) {
2233
2500
  return `${count} ${count === 1 ? singular : plural}`;
2234
2501
  }
@@ -2282,6 +2549,10 @@ function formatTargetSummary(summary) {
2282
2549
  return `count=${summary.count}; families=${families}`;
2283
2550
  }
2284
2551
  function classifyGenericBucketType(reason) {
2552
+ const extended = findExtendedBucketSpec(reason);
2553
+ if (extended) {
2554
+ return extended.type;
2555
+ }
2285
2556
  if (reason.startsWith("missing test env:")) {
2286
2557
  return "shared_environment_blocker";
2287
2558
  }
@@ -2326,6 +2597,10 @@ function classifyVisibleStatusForLabel(args) {
2326
2597
  return "unknown";
2327
2598
  }
2328
2599
  function inferCoverageFromReason(reason) {
2600
+ const extended = findExtendedBucketSpec(reason);
2601
+ if (extended) {
2602
+ return extended.defaultCoverage;
2603
+ }
2329
2604
  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:")) {
2330
2605
  return "error";
2331
2606
  }
@@ -2386,7 +2661,13 @@ function buildGenericBuckets(analysis) {
2386
2661
  summaryLines: [],
2387
2662
  reason,
2388
2663
  count: 1,
2389
- confidence: reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62,
2664
+ confidence: (() => {
2665
+ const extended = findExtendedBucketSpec(reason);
2666
+ if (extended) {
2667
+ return Math.max(0.72, Math.min(extended.rootCauseConfidence, 0.82));
2668
+ }
2669
+ return reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62;
2670
+ })(),
2390
2671
  representativeItems: [item],
2391
2672
  entities: [],
2392
2673
  hint: void 0,
@@ -2403,7 +2684,7 @@ function buildGenericBuckets(analysis) {
2403
2684
  push(item.reason, item);
2404
2685
  }
2405
2686
  for (const bucket of grouped.values()) {
2406
- 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";
2687
+ 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");
2407
2688
  bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
2408
2689
  bucket.summaryLines = [bucket.headline];
2409
2690
  bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
@@ -2476,13 +2757,13 @@ function inferFailureBucketCoverage(bucket, analysis) {
2476
2757
  }
2477
2758
  }
2478
2759
  const claimed = bucket.countClaimed ?? bucket.countVisible;
2479
- if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure") {
2760
+ if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch") {
2480
2761
  return {
2481
2762
  error,
2482
2763
  failed: Math.max(failed, claimed)
2483
2764
  };
2484
2765
  }
2485
- if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
2766
+ 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") {
2486
2767
  return {
2487
2768
  error: Math.max(error, claimed),
2488
2769
  failed
@@ -2557,6 +2838,10 @@ function dominantBucketPriority(bucket) {
2557
2838
  if (bucket.reason.startsWith("missing test env:")) {
2558
2839
  return 5;
2559
2840
  }
2841
+ const extended = findExtendedBucketSpec(bucket.reason);
2842
+ if (extended?.dominantPriority !== void 0) {
2843
+ return extended.dominantPriority;
2844
+ }
2560
2845
  if (bucket.type === "shared_environment_blocker") {
2561
2846
  return 4;
2562
2847
  }
@@ -2590,12 +2875,16 @@ function prioritizeBuckets(buckets) {
2590
2875
  });
2591
2876
  }
2592
2877
  function isDominantBlockerType(type) {
2593
- return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
2878
+ return type === "shared_environment_blocker" || type === "configuration_error" || type === "import_dependency_failure" || type === "collection_failure";
2594
2879
  }
2595
2880
  function labelForBucket(bucket) {
2596
2881
  if (bucket.labelOverride) {
2597
2882
  return bucket.labelOverride;
2598
2883
  }
2884
+ const extended = findExtendedBucketSpec(bucket.reason);
2885
+ if (extended) {
2886
+ return extended.label;
2887
+ }
2599
2888
  if (bucket.reason.startsWith("missing test env:")) {
2600
2889
  return "missing test env";
2601
2890
  }
@@ -2629,6 +2918,9 @@ function labelForBucket(bucket) {
2629
2918
  if (bucket.type === "assertion_failure") {
2630
2919
  return "assertion failure";
2631
2920
  }
2921
+ if (bucket.type === "snapshot_mismatch") {
2922
+ return "snapshot mismatch";
2923
+ }
2632
2924
  if (bucket.type === "collection_failure") {
2633
2925
  return "collection failure";
2634
2926
  }
@@ -2647,6 +2939,10 @@ function rootCauseConfidenceFor(bucket) {
2647
2939
  if (isUnknownBucket(bucket)) {
2648
2940
  return 0.52;
2649
2941
  }
2942
+ const extended = findExtendedBucketSpec(bucket.reason);
2943
+ if (extended) {
2944
+ return extended.rootCauseConfidence;
2945
+ }
2650
2946
  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:")) {
2651
2947
  return 0.95;
2652
2948
  }
@@ -2687,6 +2983,10 @@ function buildReadTargetWhy(args) {
2687
2983
  if (envVar) {
2688
2984
  return `it contains the ${envVar} setup guard`;
2689
2985
  }
2986
+ const extended = findExtendedBucketSpec(args.bucket.reason);
2987
+ if (extended) {
2988
+ return extended.why;
2989
+ }
2690
2990
  if (args.bucket.reason.startsWith("fixture guard:")) {
2691
2991
  return "it contains the fixture/setup guard behind this bucket";
2692
2992
  }
@@ -2717,6 +3017,9 @@ function buildReadTargetWhy(args) {
2717
3017
  }
2718
3018
  return "it maps to the visible stale snapshot expectation";
2719
3019
  }
3020
+ if (args.bucket.type === "snapshot_mismatch") {
3021
+ return "it maps to the visible snapshot mismatch bucket";
3022
+ }
2720
3023
  if (args.bucket.type === "import_dependency_failure") {
2721
3024
  return "it is the first visible failing module in this missing dependency bucket";
2722
3025
  }
@@ -2728,11 +3031,54 @@ function buildReadTargetWhy(args) {
2728
3031
  }
2729
3032
  return `it maps to the visible ${args.bucketLabel} bucket`;
2730
3033
  }
3034
+ function buildExtendedBucketSearchHint(bucket, anchor) {
3035
+ const extended = findExtendedBucketSpec(bucket.reason);
3036
+ if (!extended) {
3037
+ return null;
3038
+ }
3039
+ const detail = extractReasonDetail(bucket.reason, extended.prefix);
3040
+ if (!detail) {
3041
+ return anchor.label.split("::")[1]?.trim() ?? anchor.label ?? null;
3042
+ }
3043
+ if (extended.type === "timeout_failure") {
3044
+ const duration = detail.match(/>\s*([0-9]+(?:\.[0-9]+)?s?)/i)?.[1];
3045
+ return duration ?? anchor.label.split("::")[1]?.trim() ?? detail;
3046
+ }
3047
+ if (extended.type === "db_migration_failure") {
3048
+ const relation = detail.match(/\b(?:relation|table)\s+([A-Za-z0-9_.-]+)/i)?.[1];
3049
+ return relation ?? detail;
3050
+ }
3051
+ if (extended.type === "network_failure") {
3052
+ const url = detail.match(/\bhttps?:\/\/[^\s)'"`]+/i)?.[0];
3053
+ const host = detail.match(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/)?.[0];
3054
+ return url ?? host ?? detail;
3055
+ }
3056
+ if (extended.type === "xdist_worker_crash") {
3057
+ return detail.match(/\bgw\d+\b/)?.[0] ?? detail;
3058
+ }
3059
+ if (extended.type === "fixture_teardown_failure") {
3060
+ return detail.replace(/^of\s+/i, "") || anchor.label;
3061
+ }
3062
+ if (extended.type === "file_not_found_failure") {
3063
+ const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
3064
+ return path8 ?? detail;
3065
+ }
3066
+ if (extended.type === "permission_denied_failure") {
3067
+ const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
3068
+ const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
3069
+ return path8 ?? (port ? `port ${port}` : detail);
3070
+ }
3071
+ return detail;
3072
+ }
2731
3073
  function buildReadTargetSearchHint(bucket, anchor) {
2732
3074
  const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
2733
3075
  if (envVar) {
2734
3076
  return envVar;
2735
3077
  }
3078
+ const extendedHint = buildExtendedBucketSearchHint(bucket, anchor);
3079
+ if (extendedHint) {
3080
+ return extendedHint;
3081
+ }
2736
3082
  if (bucket.type === "contract_snapshot_drift") {
2737
3083
  return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
2738
3084
  }
@@ -2847,6 +3193,10 @@ function extractMiniDiff(input, bucket) {
2847
3193
  };
2848
3194
  }
2849
3195
  function inferSupplementCoverageKind(args) {
3196
+ const extended = findExtendedBucketSpec(args.rootCause);
3197
+ if (extended?.defaultCoverage === "error" || extended?.defaultCoverage === "failed") {
3198
+ return extended.defaultCoverage;
3199
+ }
2850
3200
  const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
2851
3201
  if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
2852
3202
  normalized
@@ -2917,7 +3267,7 @@ function buildProviderSupplementBuckets(args) {
2917
3267
  });
2918
3268
  }
2919
3269
  function pickUnknownAnchor(args) {
2920
- const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : null;
3270
+ const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : args.analysis.visibleFailedItems[0];
2921
3271
  if (fromStatusItems) {
2922
3272
  return {
2923
3273
  label: fromStatusItems.label,
@@ -2954,12 +3304,14 @@ function buildUnknownBucket(args) {
2954
3304
  const isError = args.kind === "error";
2955
3305
  const label = isError ? "unknown setup blocker" : "unknown failure family";
2956
3306
  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";
3307
+ const firstConcreteSignal = anchor && anchor.reason !== reason && anchor.reason !== "setup failures share a repeated but unclassified pattern" && anchor.reason !== "failing tests share a repeated but unclassified pattern" ? `First concrete signal: ${anchor.reason}` : null;
2957
3308
  return {
2958
3309
  type: "unknown_failure",
2959
3310
  headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
2960
3311
  summaryLines: [
2961
- `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`
2962
- ],
3312
+ `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
3313
+ firstConcreteSignal
3314
+ ].filter((value) => Boolean(value)),
2963
3315
  reason,
2964
3316
  count: args.count,
2965
3317
  confidence: 0.45,
@@ -3086,10 +3438,14 @@ function buildStandardAnchorText(target) {
3086
3438
  }
3087
3439
  return formatReadTargetLocation(target);
3088
3440
  }
3089
- function buildStandardFixText(args) {
3441
+ function resolveBucketFixHint(args) {
3090
3442
  if (args.bucket.hint) {
3091
3443
  return args.bucket.hint;
3092
3444
  }
3445
+ const extended = findExtendedBucketSpec(args.bucket.reason);
3446
+ if (extended) {
3447
+ return extended.fix;
3448
+ }
3093
3449
  const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
3094
3450
  if (envVar) {
3095
3451
  return `Set ${envVar} before rerunning the affected tests.`;
@@ -3119,6 +3475,9 @@ function buildStandardFixText(args) {
3119
3475
  if (args.bucket.type === "contract_snapshot_drift") {
3120
3476
  return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
3121
3477
  }
3478
+ if (args.bucket.type === "snapshot_mismatch") {
3479
+ return "Update the snapshots if these output changes are intentional, then rerun the full suite at standard.";
3480
+ }
3122
3481
  if (args.bucket.type === "assertion_failure") {
3123
3482
  return "Inspect the failing assertion and rerun the full suite at standard.";
3124
3483
  }
@@ -3128,13 +3487,75 @@ function buildStandardFixText(args) {
3128
3487
  if (args.bucket.type === "runtime_failure") {
3129
3488
  return `Fix the visible ${args.bucketLabel} and rerun the full suite at standard.`;
3130
3489
  }
3131
- return null;
3490
+ return "Inspect the first visible anchor for this bucket, apply the smallest fix that explains it, then rerun the full suite at standard.";
3491
+ }
3492
+ function deriveBucketSuspectKind(args) {
3493
+ if (args.bucket.type === "shared_environment_blocker" || args.bucket.type === "fixture_guard_failure" || args.bucket.type === "permission_denied_failure" || args.bucket.type === "django_db_access_denied" || args.bucket.type === "network_failure" || args.bucket.type === "service_unavailable" || args.bucket.type === "db_connection_failure" || args.bucket.type === "auth_bypass_absent" || args.bucket.type === "fixture_teardown_failure") {
3494
+ return "environment";
3495
+ }
3496
+ if (args.bucket.type === "configuration_error" || args.bucket.type === "db_migration_failure" || args.bucket.type === "import_dependency_failure" || args.bucket.type === "collection_failure" || args.bucket.type === "no_tests_collected" || args.bucket.type === "deprecation_warning_as_error" || args.bucket.type === "file_not_found_failure") {
3497
+ return "config";
3498
+ }
3499
+ if (args.bucket.type === "contract_snapshot_drift" || args.bucket.type === "snapshot_mismatch" || args.bucket.type === "flaky_test_detected" || args.bucket.type === "xfail_strict_unexpected_pass") {
3500
+ return "test";
3501
+ }
3502
+ if (args.bucket.type === "xdist_worker_crash" || args.bucket.type === "timeout_failure" || args.bucket.type === "async_event_loop_failure" || args.bucket.type === "subprocess_crash_segfault" || args.bucket.type === "memory_error" || args.bucket.type === "resource_leak_warning" || args.bucket.type === "interrupted_run") {
3503
+ return "tooling";
3504
+ }
3505
+ if (args.bucket.type === "unknown_failure") {
3506
+ return "unknown";
3507
+ }
3508
+ if (args.bucket.type === "assertion_failure" || args.bucket.type === "runtime_failure" || args.bucket.type === "type_error_failure" || args.bucket.type === "serialization_encoding_failure") {
3509
+ const file = args.readTarget?.file ?? "";
3510
+ if (file.startsWith("src/")) {
3511
+ return "app_code";
3512
+ }
3513
+ if (file.startsWith("test/") || file.startsWith("tests/")) {
3514
+ return "test";
3515
+ }
3516
+ return "unknown";
3517
+ }
3518
+ return "unknown";
3519
+ }
3520
+ function derivePrimarySuspectKind(args) {
3521
+ const primaryBucket = (args.dominantBlockerBucketIndex !== null ? args.mainBuckets.find((bucket) => bucket.bucket_index === args.dominantBlockerBucketIndex) : null) ?? args.mainBuckets[0];
3522
+ return primaryBucket?.suspect_kind ?? "unknown";
3523
+ }
3524
+ function buildConfidenceReason(args) {
3525
+ const primaryBucket = args.mainBuckets.find((bucket) => bucket.dominant) ?? args.mainBuckets[0];
3526
+ if (args.decision === "stop" && primaryBucket && args.primarySuspectKind !== "unknown") {
3527
+ return `Dominant blocker (${primaryBucket.label}) is anchored and actionable.`;
3528
+ }
3529
+ if (args.decision === "zoom") {
3530
+ return "Unknown or low-confidence buckets remain; one deeper sift pass is justified.";
3531
+ }
3532
+ if (args.decision === "read_source") {
3533
+ return "The bucket is identified, but source context is still needed to make the next fix clear.";
3534
+ }
3535
+ return "Heuristic signal is still insufficient; exact traceback lines are needed.";
3536
+ }
3537
+ function formatSuspectKindLabel(kind) {
3538
+ switch (kind) {
3539
+ case "test":
3540
+ return "test code";
3541
+ case "app_code":
3542
+ return "application code";
3543
+ case "config":
3544
+ return "test or project configuration";
3545
+ case "environment":
3546
+ return "environment setup";
3547
+ case "tooling":
3548
+ return "test runner or tooling";
3549
+ default:
3550
+ return "unknown";
3551
+ }
3132
3552
  }
3133
3553
  function buildStandardBucketSupport(args) {
3134
3554
  return {
3135
3555
  headline: args.bucket.summaryLines[0] ? `- ${args.bucket.summaryLines[0]}` : renderBucketHeadline(args.contractBucket),
3556
+ firstConcreteSignalText: args.bucket.source === "unknown" ? args.bucket.summaryLines[1] ?? null : null,
3136
3557
  anchorText: buildStandardAnchorText(args.readTarget),
3137
- fixText: buildStandardFixText({
3558
+ fixText: resolveBucketFixHint({
3138
3559
  bucket: args.bucket,
3139
3560
  bucketLabel: args.contractBucket.label
3140
3561
  })
@@ -3157,6 +3578,9 @@ function renderStandard(args) {
3157
3578
  )
3158
3579
  });
3159
3580
  lines.push(support.headline);
3581
+ if (support.firstConcreteSignalText) {
3582
+ lines.push(`- ${support.firstConcreteSignalText}`);
3583
+ }
3160
3584
  if (support.anchorText) {
3161
3585
  lines.push(`- Anchor: ${support.anchorText}`);
3162
3586
  }
@@ -3166,6 +3590,7 @@ function renderStandard(args) {
3166
3590
  }
3167
3591
  }
3168
3592
  lines.push(buildDecisionLine(args.contract));
3593
+ lines.push(`- Likely owner: ${formatSuspectKindLabel(args.contract.primary_suspect_kind)}`);
3169
3594
  lines.push(`- Next: ${args.contract.next_best_action.note}`);
3170
3595
  lines.push(buildStopSignal(args.contract));
3171
3596
  return lines.join("\n");
@@ -3253,29 +3678,49 @@ function buildTestStatusDiagnoseContract(args) {
3253
3678
  })[0] ?? null;
3254
3679
  const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
3255
3680
  const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
3256
- 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;
3257
- 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);
3258
3681
  const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
3259
3682
  const readTargets = buildReadTargets({
3260
3683
  buckets,
3261
3684
  dominantBucketIndex: dominantBlockerBucketIndex
3262
3685
  });
3263
- const mainBuckets = buckets.map((bucket, index) => ({
3264
- bucket_index: index + 1,
3265
- label: labelForBucket(bucket),
3266
- count: bucket.count,
3267
- root_cause: bucket.reason,
3268
- evidence: buildBucketEvidence(bucket),
3269
- bucket_confidence: Number(bucket.confidence.toFixed(2)),
3270
- root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
3271
- dominant: dominantBucket?.index === index,
3272
- secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== index + 1,
3273
- mini_diff: extractMiniDiff(args.input, bucket)
3274
- }));
3686
+ const dominantBucketHasConcreteAnchor = dominantBucket !== null && (readTargets.some((target) => target.bucket_index === dominantBucket.index + 1 && target.file.length > 0) || dominantBucket.bucket.representativeItems.some((item) => item.anchor_kind !== "none"));
3687
+ const smallConcreteSuite = args.analysis.failed + args.analysis.errors <= 2 && residuals.remainingErrors === 0 && residuals.remainingFailed === 0 && buckets.length === 1 && !hasUnknownBucket && dominantBucket !== null && dominantBucketHasConcreteAnchor;
3688
+ const dominantConfidenceThreshold = smallConcreteSuite ? 0.55 : 0.6;
3689
+ const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && hasConcreteCoverage && !hasUnknownBucket && (dominantBucket?.bucket.confidence ?? 0) >= dominantConfidenceThreshold;
3690
+ 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);
3691
+ const mainBuckets = buckets.map((bucket, index) => {
3692
+ const bucketIndex = index + 1;
3693
+ const label = labelForBucket(bucket);
3694
+ const readTarget = readTargets.find((target) => target.bucket_index === bucketIndex);
3695
+ return {
3696
+ bucket_index: bucketIndex,
3697
+ label,
3698
+ count: bucket.count,
3699
+ root_cause: bucket.reason,
3700
+ suspect_kind: deriveBucketSuspectKind({
3701
+ bucket,
3702
+ readTarget
3703
+ }),
3704
+ fix_hint: resolveBucketFixHint({
3705
+ bucket,
3706
+ bucketLabel: label
3707
+ }),
3708
+ evidence: buildBucketEvidence(bucket),
3709
+ bucket_confidence: Number(bucket.confidence.toFixed(2)),
3710
+ root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
3711
+ dominant: dominantBucket?.index === index,
3712
+ secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== bucketIndex,
3713
+ mini_diff: extractMiniDiff(args.input, bucket)
3714
+ };
3715
+ });
3275
3716
  const resolvedTests = unique(args.resolvedTests ?? []);
3276
3717
  const remainingTests = unique(
3277
3718
  args.remainingTests ?? unique([...args.analysis.visibleErrorLabels, ...args.analysis.visibleFailedLabels])
3278
3719
  );
3720
+ const primarySuspectKind = derivePrimarySuspectKind({
3721
+ mainBuckets,
3722
+ dominantBlockerBucketIndex
3723
+ });
3279
3724
  let nextBestAction;
3280
3725
  if (args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0) {
3281
3726
  nextBestAction = {
@@ -3321,6 +3766,8 @@ function buildTestStatusDiagnoseContract(args) {
3321
3766
  additional_source_read_likely_low_value: diagnosisComplete && !rawNeeded,
3322
3767
  read_raw_only_if: rawNeeded ? "you still need exact traceback lines after focused or verbose detail" : null,
3323
3768
  dominant_blocker_bucket_index: dominantBlockerBucketIndex,
3769
+ primary_suspect_kind: primarySuspectKind,
3770
+ confidence_reason: "Unknown or low-confidence buckets remain; one deeper sift pass is justified.",
3324
3771
  provider_used: false,
3325
3772
  provider_confidence: null,
3326
3773
  provider_failed: false,
@@ -3352,9 +3799,16 @@ function buildTestStatusDiagnoseContract(args) {
3352
3799
  })
3353
3800
  }
3354
3801
  };
3802
+ const resolvedDecision = effectiveDecision ?? deriveDecision(mergedContractWithoutDecision);
3803
+ const resolvedConfidenceReason = buildConfidenceReason({
3804
+ decision: resolvedDecision,
3805
+ mainBuckets,
3806
+ primarySuspectKind: mergedContractWithoutDecision.primary_suspect_kind
3807
+ });
3355
3808
  const contract = testStatusDiagnoseContractSchema.parse({
3356
3809
  ...mergedContractWithoutDecision,
3357
- decision: effectiveDecision ?? deriveDecision(mergedContractWithoutDecision)
3810
+ confidence_reason: resolvedConfidenceReason,
3811
+ decision: resolvedDecision
3358
3812
  });
3359
3813
  return {
3360
3814
  contract,
@@ -3759,6 +4213,27 @@ function buildFallbackOutput(args) {
3759
4213
  var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
3760
4214
  var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
3761
4215
  var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
4216
+ var RESOURCE_DESTROY_HEADER_PATTERN = /^#\s+.+\bwill be (destroyed|deleted|replaced)\b/i;
4217
+ var DESTROY_ERROR_PATTERN = /(instance cannot be destroyed|prevent_destroy|downtime|data loss)/i;
4218
+ var ACTION_DESTROY_PATTERN = /^-\s+destroy$/i;
4219
+ var TSC_CODE_LABELS = {
4220
+ TS1002: "syntax error",
4221
+ TS1005: "syntax error",
4222
+ TS2304: "cannot find name",
4223
+ TS2307: "cannot find module",
4224
+ TS2322: "type mismatch",
4225
+ TS2339: "missing property on type",
4226
+ TS2345: "argument type mismatch",
4227
+ TS2554: "wrong argument count",
4228
+ TS2741: "missing required property",
4229
+ TS2769: "no matching overload",
4230
+ TS5083: "config file error",
4231
+ TS6133: "declared but unused",
4232
+ TS7006: "implicit any",
4233
+ TS18003: "no inputs were found",
4234
+ TS18046: "unknown type",
4235
+ TS18048: "possibly undefined"
4236
+ };
3762
4237
  function collectEvidence(input, matcher, limit = 3) {
3763
4238
  return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
3764
4239
  }
@@ -3772,11 +4247,236 @@ function inferPackage(line) {
3772
4247
  function inferRemediation(pkg2) {
3773
4248
  return `Upgrade ${pkg2} to a patched version.`;
3774
4249
  }
4250
+ function parseCompactAuditVulnerability(line) {
4251
+ if (/^Severity:\s*/i.test(line)) {
4252
+ return null;
4253
+ }
4254
+ if (!/\b(critical|high)\b/i.test(line)) {
4255
+ return null;
4256
+ }
4257
+ const pkg2 = inferPackage(line);
4258
+ if (!pkg2) {
4259
+ return null;
4260
+ }
4261
+ return {
4262
+ package: pkg2,
4263
+ severity: inferSeverity(line),
4264
+ remediation: inferRemediation(pkg2)
4265
+ };
4266
+ }
4267
+ function inferAuditPackageHeader(line) {
4268
+ const trimmed = line.trim();
4269
+ if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.includes(":") || /^node_modules\//i.test(trimmed)) {
4270
+ return null;
4271
+ }
4272
+ const match = trimmed.match(/^([@a-z0-9._/-]+)(?:\s{2,}|\s+(?:[<>=~^*]|\d))/i);
4273
+ return match?.[1] ?? null;
4274
+ }
4275
+ function collectAuditCriticalVulnerabilities(input) {
4276
+ const lines = input.split("\n");
4277
+ const vulnerabilities = [];
4278
+ const seen = /* @__PURE__ */ new Set();
4279
+ const pushVulnerability = (pkg2, severity) => {
4280
+ const key = `${pkg2}:${severity}`;
4281
+ if (seen.has(key)) {
4282
+ return;
4283
+ }
4284
+ seen.add(key);
4285
+ vulnerabilities.push({
4286
+ package: pkg2,
4287
+ severity,
4288
+ remediation: inferRemediation(pkg2)
4289
+ });
4290
+ };
4291
+ for (let index = 0; index < lines.length; index += 1) {
4292
+ const line = lines[index].trim();
4293
+ if (!line) {
4294
+ continue;
4295
+ }
4296
+ const compact = parseCompactAuditVulnerability(line);
4297
+ if (compact) {
4298
+ pushVulnerability(compact.package, compact.severity);
4299
+ continue;
4300
+ }
4301
+ const pkg2 = inferAuditPackageHeader(line);
4302
+ if (!pkg2) {
4303
+ continue;
4304
+ }
4305
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 5); cursor += 1) {
4306
+ const candidate = lines[cursor].trim();
4307
+ if (!candidate) {
4308
+ continue;
4309
+ }
4310
+ const severityMatch = candidate.match(/^Severity:\s*(critical|high)\b/i);
4311
+ if (severityMatch) {
4312
+ pushVulnerability(pkg2, severityMatch[1].toLowerCase());
4313
+ break;
4314
+ }
4315
+ if (inferAuditPackageHeader(candidate) || parseCompactAuditVulnerability(candidate)) {
4316
+ break;
4317
+ }
4318
+ }
4319
+ }
4320
+ return vulnerabilities;
4321
+ }
3775
4322
  function getCount(input, label) {
3776
4323
  const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
3777
4324
  const lastMatch = matches.at(-1);
3778
4325
  return lastMatch ? Number(lastMatch[1]) : 0;
3779
4326
  }
4327
+ function collectInfraRiskEvidence(input) {
4328
+ const lines = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
4329
+ const evidence = [];
4330
+ const seen = /* @__PURE__ */ new Set();
4331
+ const pushMatches = (matcher, options) => {
4332
+ let added = 0;
4333
+ for (const line of lines) {
4334
+ if (!matcher.test(line)) {
4335
+ continue;
4336
+ }
4337
+ if (options?.exclude?.test(line)) {
4338
+ continue;
4339
+ }
4340
+ if (seen.has(line)) {
4341
+ continue;
4342
+ }
4343
+ evidence.push(line);
4344
+ seen.add(line);
4345
+ added += 1;
4346
+ if (options?.limit && added >= options.limit) {
4347
+ return;
4348
+ }
4349
+ if (evidence.length >= (options?.maxEvidence ?? 4)) {
4350
+ return;
4351
+ }
4352
+ }
4353
+ };
4354
+ pushMatches(/Plan:/i, {
4355
+ exclude: ZERO_DESTRUCTIVE_SUMMARY_PATTERN,
4356
+ limit: 1
4357
+ });
4358
+ if (evidence.length < 4) {
4359
+ pushMatches(RESOURCE_DESTROY_HEADER_PATTERN, { limit: 2 });
4360
+ }
4361
+ if (evidence.length < 4) {
4362
+ pushMatches(DESTROY_ERROR_PATTERN, { limit: 1 });
4363
+ }
4364
+ if (evidence.length < 4) {
4365
+ pushMatches(ACTION_DESTROY_PATTERN, { limit: 1 });
4366
+ }
4367
+ if (evidence.length < 4) {
4368
+ pushMatches(RISK_LINE_PATTERN, {
4369
+ exclude: /->\s+null$|\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i,
4370
+ maxEvidence: 4
4371
+ });
4372
+ }
4373
+ return evidence.slice(0, 4);
4374
+ }
4375
+ function collectInfraDestroyTargets(input) {
4376
+ const targets = [];
4377
+ const seen = /* @__PURE__ */ new Set();
4378
+ for (const line of input.split("\n").map((entry) => entry.trim())) {
4379
+ const match = line.match(/^#\s+(.+?)\s+will be (destroyed|deleted|replaced)\b/i);
4380
+ const target = match?.[1]?.trim();
4381
+ if (!target || seen.has(target)) {
4382
+ continue;
4383
+ }
4384
+ seen.add(target);
4385
+ targets.push(target);
4386
+ }
4387
+ return targets;
4388
+ }
4389
+ function inferInfraDestroyCount(input, destroyTargets) {
4390
+ const matches = [
4391
+ ...input.matchAll(/\b(\d+)\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/gi)
4392
+ ];
4393
+ const lastMatch = matches.at(-1);
4394
+ return lastMatch ? Number(lastMatch[1]) : destroyTargets.length;
4395
+ }
4396
+ function collectInfraBlockers(input) {
4397
+ const lines = input.split("\n");
4398
+ const blockers = [];
4399
+ const seen = /* @__PURE__ */ new Set();
4400
+ for (let index = 0; index < lines.length; index += 1) {
4401
+ const trimmed = lines[index]?.trim();
4402
+ const errorMatch = trimmed?.match(/^(?:[│|]\s*)?Error:\s+(.+)$/);
4403
+ if (!errorMatch) {
4404
+ continue;
4405
+ }
4406
+ const message = errorMatch[1].trim();
4407
+ const nearby = lines.slice(index, index + 8).join("\n");
4408
+ const preventDestroyTarget = nearby.match(/Resource\s+([^\s]+)\s+has lifecycle\.prevent_destroy set/i)?.[1] ?? null;
4409
+ const type = preventDestroyTarget ? "prevent_destroy" : "destroy_blocked";
4410
+ const key = `${type}:${preventDestroyTarget ?? ""}:${message}`;
4411
+ if (seen.has(key)) {
4412
+ continue;
4413
+ }
4414
+ seen.add(key);
4415
+ blockers.push({
4416
+ type,
4417
+ target: preventDestroyTarget,
4418
+ message
4419
+ });
4420
+ }
4421
+ return blockers;
4422
+ }
4423
+ function detectTestRunner(input) {
4424
+ 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)) {
4425
+ return "vitest";
4426
+ }
4427
+ 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)) {
4428
+ return "jest";
4429
+ }
4430
+ 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)) {
4431
+ return "pytest";
4432
+ }
4433
+ return "unknown";
4434
+ }
4435
+ function extractVitestLineCount(input, label, metric) {
4436
+ const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
4437
+ const lines = [...input.matchAll(matcher)];
4438
+ const line = lines.at(-1)?.[1];
4439
+ if (!line) {
4440
+ return null;
4441
+ }
4442
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
4443
+ return metricMatch ? Number(metricMatch[1]) : null;
4444
+ }
4445
+ function extractJestLineCount(input, label, metric) {
4446
+ const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
4447
+ const lines = [...input.matchAll(matcher)];
4448
+ const line = lines.at(-1)?.[1];
4449
+ if (!line) {
4450
+ return null;
4451
+ }
4452
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
4453
+ return metricMatch ? Number(metricMatch[1]) : null;
4454
+ }
4455
+ function extractTestStatusCounts(input, runner) {
4456
+ if (runner === "vitest") {
4457
+ return {
4458
+ passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
4459
+ failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
4460
+ errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
4461
+ skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
4462
+ snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
4463
+ };
4464
+ }
4465
+ if (runner === "jest") {
4466
+ return {
4467
+ passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
4468
+ failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
4469
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
4470
+ skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
4471
+ };
4472
+ }
4473
+ return {
4474
+ passed: getCount(input, "passed"),
4475
+ failed: getCount(input, "failed"),
4476
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
4477
+ skipped: getCount(input, "skipped")
4478
+ };
4479
+ }
3780
4480
  function formatCount2(count, singular, plural = `${singular}s`) {
3781
4481
  return `${count} ${count === 1 ? singular : plural}`;
3782
4482
  }
@@ -3797,6 +4497,21 @@ function collectUniqueMatches(input, matcher, limit = 6) {
3797
4497
  }
3798
4498
  return values;
3799
4499
  }
4500
+ function compactDisplayFile(file) {
4501
+ const normalized = file.replace(/\\/g, "/").trim();
4502
+ if (!normalized) {
4503
+ return file;
4504
+ }
4505
+ const looksAbsolute = normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized);
4506
+ if (!looksAbsolute && normalized.length <= 60) {
4507
+ return normalized;
4508
+ }
4509
+ const basename = normalized.split("/").at(-1);
4510
+ return basename && basename.length > 0 ? basename : normalized;
4511
+ }
4512
+ function formatDisplayedFiles(files, limit = 3) {
4513
+ return [...new Set([...files].map((file) => file.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right)).slice(0, limit).map((file) => compactDisplayFile(file));
4514
+ }
3800
4515
  function emptyAnchor() {
3801
4516
  return {
3802
4517
  file: null,
@@ -3809,7 +4524,8 @@ function normalizeAnchorFile(value) {
3809
4524
  return value.replace(/\\/g, "/").trim();
3810
4525
  }
3811
4526
  function inferFileFromLabel(label) {
3812
- const candidate = cleanFailureLabel(label).split("::")[0]?.trim();
4527
+ const cleaned = cleanFailureLabel(label);
4528
+ const candidate = cleaned.split("::")[0]?.split(" > ")[0]?.trim();
3813
4529
  if (!candidate) {
3814
4530
  return null;
3815
4531
  }
@@ -3864,6 +4580,15 @@ function parseObservedAnchor(line) {
3864
4580
  anchor_confidence: 0.92
3865
4581
  };
3866
4582
  }
4583
+ const vitestTraceback = normalized.match(/^\s*❯\s+([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?/);
4584
+ if (vitestTraceback) {
4585
+ return {
4586
+ file: normalizeAnchorFile(vitestTraceback[1]),
4587
+ line: Number(vitestTraceback[2]),
4588
+ anchor_kind: "traceback",
4589
+ anchor_confidence: 1
4590
+ };
4591
+ }
3867
4592
  return null;
3868
4593
  }
3869
4594
  function resolveAnchorForLabel(args) {
@@ -3880,15 +4605,27 @@ function isLowValueInternalReason(normalized) {
3880
4605
  ) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
3881
4606
  }
3882
4607
  function scoreFailureReason(reason) {
4608
+ if (reason.startsWith("configuration:")) {
4609
+ return 6;
4610
+ }
3883
4611
  if (reason.startsWith("missing test env:")) {
3884
4612
  return 6;
3885
4613
  }
3886
4614
  if (reason.startsWith("missing module:")) {
3887
4615
  return 5;
3888
4616
  }
4617
+ if (reason.startsWith("snapshot mismatch:")) {
4618
+ return 4;
4619
+ }
3889
4620
  if (reason.startsWith("assertion failed:")) {
3890
4621
  return 4;
3891
4622
  }
4623
+ if (reason.startsWith("timeout:") || reason.startsWith("async loop:") || reason.startsWith("django db access:") || reason.startsWith("db migration:")) {
4624
+ return 3;
4625
+ }
4626
+ 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:")) {
4627
+ return 2;
4628
+ }
3892
4629
  if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
3893
4630
  return 3;
3894
4631
  }
@@ -3897,6 +4634,16 @@ function scoreFailureReason(reason) {
3897
4634
  }
3898
4635
  return 1;
3899
4636
  }
4637
+ function buildClassifiedReason(prefix, detail) {
4638
+ return `${prefix}: ${detail}`.slice(0, 120);
4639
+ }
4640
+ function buildExcerptDetail(value, fallback) {
4641
+ const trimmed = value.trim().replace(/\s+/g, " ");
4642
+ return trimmed.length > 0 ? trimmed : fallback;
4643
+ }
4644
+ function sharedBlockerThreshold(reason) {
4645
+ return reason.startsWith("configuration:") ? 1 : 3;
4646
+ }
3900
4647
  function extractEnvBlockerName(normalized) {
3901
4648
  const directMatch = normalized.match(
3902
4649
  /\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
@@ -3996,59 +4743,365 @@ function classifyFailureReason(line, options) {
3996
4743
  group: "authentication test setup failures"
3997
4744
  };
3998
4745
  }
3999
- const pythonMissingModule = normalized.match(
4000
- /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
4746
+ const snapshotMismatch = normalized.match(
4747
+ /((?:Error:\s*)?Snapshot\b.+\bmismatched\b[^$]*|Snapshot comparison failed[^$]*)/i
4001
4748
  );
4002
- if (pythonMissingModule) {
4749
+ if (snapshotMismatch) {
4003
4750
  return {
4004
- reason: `missing module: ${pythonMissingModule[1]}`,
4005
- group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
4751
+ reason: buildClassifiedReason(
4752
+ "snapshot mismatch",
4753
+ buildExcerptDetail(snapshotMismatch[1] ?? normalized, "snapshot output changed")
4754
+ ),
4755
+ group: "snapshot mismatches"
4006
4756
  };
4007
4757
  }
4008
- const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
4009
- if (nodeMissingModule) {
4758
+ const timeoutFailure = normalized.match(
4759
+ /(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
4760
+ );
4761
+ if (timeoutFailure) {
4010
4762
  return {
4011
- reason: `missing module: ${nodeMissingModule[1]}`,
4012
- group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
4763
+ reason: buildClassifiedReason(
4764
+ "timeout",
4765
+ buildExcerptDetail(timeoutFailure[1] ?? normalized, "test exceeded timeout threshold")
4766
+ ),
4767
+ group: "timeout failures"
4013
4768
  };
4014
4769
  }
4015
- const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
4016
- if (assertionFailure) {
4770
+ const asyncLoopFailure = normalized.match(
4771
+ /(Event loop is closed|no current event loop|coroutine .* was never awaited|coroutine was never awaited)/i
4772
+ );
4773
+ if (asyncLoopFailure) {
4017
4774
  return {
4018
- reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
4019
- group: "assertion failures"
4775
+ reason: buildClassifiedReason(
4776
+ "async loop",
4777
+ buildExcerptDetail(asyncLoopFailure[1] ?? normalized, "async event loop failure")
4778
+ ),
4779
+ group: "async event loop failures"
4020
4780
  };
4021
4781
  }
4022
- const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
4023
- if (genericError) {
4024
- const errorType = genericError[1];
4782
+ const permissionFailure = normalized.match(
4783
+ /(PermissionError:\s*\[Errno 13\][^$]*|Address already in use)/i
4784
+ );
4785
+ if (permissionFailure) {
4025
4786
  return {
4026
- reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
4027
- group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
4787
+ reason: buildClassifiedReason(
4788
+ "permission",
4789
+ buildExcerptDetail(permissionFailure[1] ?? normalized, "permission denied or resource locked")
4790
+ ),
4791
+ group: "permission or locked resource failures"
4028
4792
  };
4029
4793
  }
4030
- if (/ImportError while importing test module/i.test(normalized)) {
4794
+ const osDiskFullFailure = normalized.match(
4795
+ /(OSError:\s*\[Errno 28\][^$]*|No space left on device)/i
4796
+ );
4797
+ if (osDiskFullFailure) {
4031
4798
  return {
4032
- reason: "import error during collection",
4033
- group: "import/dependency errors during collection"
4799
+ reason: buildClassifiedReason(
4800
+ "configuration",
4801
+ `disk full (${buildExcerptDetail(
4802
+ osDiskFullFailure[1] ?? normalized,
4803
+ "No space left on device"
4804
+ )})`
4805
+ ),
4806
+ group: "test configuration failures"
4034
4807
  };
4035
4808
  }
4036
- if (!/[A-Za-z]/.test(normalized)) {
4037
- return null;
4809
+ const osPermissionFailure = normalized.match(/OSError:\s*\[Errno 13\][^$]*/i);
4810
+ if (osPermissionFailure) {
4811
+ return {
4812
+ reason: buildClassifiedReason(
4813
+ "permission",
4814
+ buildExcerptDetail(osPermissionFailure[0] ?? normalized, "permission denied")
4815
+ ),
4816
+ group: "permission or locked resource failures"
4817
+ };
4038
4818
  }
4039
- return {
4040
- reason: normalized.slice(0, 120),
4041
- group: options.duringCollection ? "collection/import errors" : "other failures"
4042
- };
4043
- }
4044
- function pushFocusedFailureItem(items, candidate) {
4045
- if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
4046
- return;
4819
+ const xdistWorkerCrash = normalized.match(
4820
+ /(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
4821
+ );
4822
+ if (xdistWorkerCrash) {
4823
+ return {
4824
+ reason: buildClassifiedReason(
4825
+ "xdist worker crash",
4826
+ buildExcerptDetail(xdistWorkerCrash[1] ?? normalized, "pytest-xdist worker crashed")
4827
+ ),
4828
+ group: "xdist worker crashes"
4829
+ };
4047
4830
  }
4048
- items.push(candidate);
4049
- }
4050
- function chooseStrongestFailureItems(items) {
4051
- const strongest = /* @__PURE__ */ new Map();
4831
+ if (/Worker terminated due to reaching memory limit/i.test(normalized)) {
4832
+ return {
4833
+ reason: "memory: Worker terminated due to reaching memory limit",
4834
+ group: "memory exhaustion failures"
4835
+ };
4836
+ }
4837
+ if (/Database access not allowed, use the ["']django_db["'] mark/i.test(normalized)) {
4838
+ return {
4839
+ reason: 'django db access: Database access not allowed, use the "django_db" mark',
4840
+ group: "django database marker failures"
4841
+ };
4842
+ }
4843
+ const networkFailure = normalized.match(
4844
+ /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable|ConnectionResetError[^,;]*|BrokenPipeError[^,;]*|HTTPError:\s*[45]\d\d[^,;]*)/i
4845
+ );
4846
+ if (networkFailure) {
4847
+ return {
4848
+ reason: buildClassifiedReason(
4849
+ "network",
4850
+ buildExcerptDetail(networkFailure[1] ?? normalized, "network dependency failure")
4851
+ ),
4852
+ group: "network dependency failures"
4853
+ };
4854
+ }
4855
+ const matcherAssertionFailure = normalized.match(
4856
+ /(expect\(received\)\.(?:toBe|toEqual|toStrictEqual|toMatchObject)\(expected\))/i
4857
+ );
4858
+ if (matcherAssertionFailure) {
4859
+ return {
4860
+ reason: `assertion failed: ${matcherAssertionFailure[1]}`.slice(0, 120),
4861
+ group: "assertion failures"
4862
+ };
4863
+ }
4864
+ const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
4865
+ if (relationMigration) {
4866
+ return {
4867
+ reason: buildClassifiedReason("db migration", `relation ${relationMigration[1]} does not exist`),
4868
+ group: "database migration or schema failures"
4869
+ };
4870
+ }
4871
+ const noSuchTable = normalized.match(/no such table(?::)?\s*([A-Za-z0-9_.-]+)/i);
4872
+ if (noSuchTable) {
4873
+ return {
4874
+ reason: buildClassifiedReason("db migration", `no such table ${noSuchTable[1]}`),
4875
+ group: "database migration or schema failures"
4876
+ };
4877
+ }
4878
+ if (/InconsistentMigrationHistory/i.test(normalized)) {
4879
+ return {
4880
+ reason: "db migration: InconsistentMigrationHistory",
4881
+ group: "database migration or schema failures"
4882
+ };
4883
+ }
4884
+ if (/(Segmentation fault|SIGSEGV|\bexit 139\b)/i.test(normalized)) {
4885
+ return {
4886
+ reason: buildClassifiedReason(
4887
+ "segfault",
4888
+ buildExcerptDetail(normalized, "subprocess crashed with SIGSEGV")
4889
+ ),
4890
+ group: "subprocess crash failures"
4891
+ };
4892
+ }
4893
+ if (/(MemoryError\b|\bexit 137\b|OOMKilled|OutOfMemory)/i.test(normalized)) {
4894
+ return {
4895
+ reason: buildClassifiedReason(
4896
+ "memory",
4897
+ buildExcerptDetail(normalized, "process exhausted available memory")
4898
+ ),
4899
+ group: "memory exhaustion failures"
4900
+ };
4901
+ }
4902
+ const propertySetterOverrideFailure = normalized.match(
4903
+ /AttributeError:\s*(property ['"][^'"]+['"] of ['"][^'"]+['"] object has no setter|can't set attribute|readonly attribute|read-only attribute)/i
4904
+ );
4905
+ if (propertySetterOverrideFailure) {
4906
+ return {
4907
+ reason: buildClassifiedReason(
4908
+ "configuration",
4909
+ `invalid test setup override (${buildExcerptDetail(
4910
+ `AttributeError: ${propertySetterOverrideFailure[1] ?? normalized}`,
4911
+ "AttributeError: can't set attribute"
4912
+ )})`
4913
+ ),
4914
+ group: "test configuration failures"
4915
+ };
4916
+ }
4917
+ const setupOverrideFailure = normalized.match(/\b(AttributeError|TypeError):\s*(.+)$/i);
4918
+ if (setupOverrideFailure && /(monkeypatch|patch|fixture|settings|conftest)/i.test(normalized)) {
4919
+ return {
4920
+ reason: buildClassifiedReason(
4921
+ "configuration",
4922
+ `invalid test setup override (${buildExcerptDetail(
4923
+ `${setupOverrideFailure[1]}: ${setupOverrideFailure[2] ?? ""}`,
4924
+ `${setupOverrideFailure[1]}`
4925
+ )})`
4926
+ ),
4927
+ group: "test configuration failures"
4928
+ };
4929
+ }
4930
+ const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
4931
+ if (typeErrorFailure) {
4932
+ return {
4933
+ reason: buildClassifiedReason(
4934
+ "type error",
4935
+ buildExcerptDetail(typeErrorFailure[1] ?? normalized, "TypeError")
4936
+ ),
4937
+ group: "type errors"
4938
+ };
4939
+ }
4940
+ const serializationFailure = normalized.match(
4941
+ /\b(UnicodeDecodeError|JSONDecodeError|PicklingError):\s*(.+)$/i
4942
+ );
4943
+ if (serializationFailure) {
4944
+ return {
4945
+ reason: buildClassifiedReason(
4946
+ "serialization",
4947
+ `${serializationFailure[1]}: ${buildExcerptDetail(serializationFailure[2] ?? "", serializationFailure[1] ?? "serialization failure")}`
4948
+ ),
4949
+ group: "serialization and encoding failures"
4950
+ };
4951
+ }
4952
+ const fileNotFoundFailure = normalized.match(/FileNotFoundError:\s*(.+)$/i);
4953
+ if (fileNotFoundFailure) {
4954
+ return {
4955
+ reason: buildClassifiedReason(
4956
+ "file not found",
4957
+ buildExcerptDetail(fileNotFoundFailure[1] ?? normalized, "missing file during test execution")
4958
+ ),
4959
+ group: "missing file failures"
4960
+ };
4961
+ }
4962
+ const deprecationFailure = normalized.match(
4963
+ /\b(DeprecationWarning|FutureWarning|PytestRemovedIn9Warning):\s*(.+)$/i
4964
+ );
4965
+ if (deprecationFailure) {
4966
+ return {
4967
+ reason: buildClassifiedReason(
4968
+ "deprecation as error",
4969
+ `${deprecationFailure[1]}: ${buildExcerptDetail(deprecationFailure[2] ?? "", deprecationFailure[1] ?? "warning treated as error")}`
4970
+ ),
4971
+ group: "warnings treated as errors"
4972
+ };
4973
+ }
4974
+ const strictXfail = normalized.match(/XPASS\(strict\)\s*:?\s*(.+)?$/i);
4975
+ if (strictXfail) {
4976
+ return {
4977
+ reason: buildClassifiedReason(
4978
+ "xfail strict",
4979
+ buildExcerptDetail(strictXfail[1] ?? normalized, "strict xfail unexpectedly passed")
4980
+ ),
4981
+ group: "strict xfail expectation failures"
4982
+ };
4983
+ }
4984
+ const resourceLeak = normalized.match(
4985
+ /(PytestUnraisableExceptionWarning[^,;]*|ResourceWarning:\s*unclosed[^,;]*)/i
4986
+ );
4987
+ if (resourceLeak) {
4988
+ return {
4989
+ reason: buildClassifiedReason(
4990
+ "resource leak",
4991
+ buildExcerptDetail(resourceLeak[1] ?? normalized, "resource leak warning promoted to failure")
4992
+ ),
4993
+ group: "resource leak warnings"
4994
+ };
4995
+ }
4996
+ const flakyFailure = normalized.match(/\b(RERUN|[0-9]+\s+rerun|Flaky test passed)\b[^$]*/i);
4997
+ if (flakyFailure) {
4998
+ return {
4999
+ reason: buildClassifiedReason(
5000
+ "flaky",
5001
+ buildExcerptDetail(flakyFailure[0] ?? normalized, "test required reruns before passing")
5002
+ ),
5003
+ group: "flaky test detections"
5004
+ };
5005
+ }
5006
+ const teardownFailure = normalized.match(/ERROR at teardown of\s+(.+)$/i);
5007
+ if (teardownFailure) {
5008
+ return {
5009
+ reason: buildClassifiedReason(
5010
+ "fixture teardown",
5011
+ buildExcerptDetail(teardownFailure[1] ?? normalized, "fixture teardown failed")
5012
+ ),
5013
+ group: "fixture teardown failures"
5014
+ };
5015
+ }
5016
+ const configurationFailure = normalized.match(
5017
+ /(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
5018
+ );
5019
+ if (configurationFailure) {
5020
+ return {
5021
+ reason: buildClassifiedReason(
5022
+ "configuration",
5023
+ buildExcerptDetail(configurationFailure[1] ?? normalized, "test configuration error")
5024
+ ),
5025
+ group: "test configuration failures"
5026
+ };
5027
+ }
5028
+ const pythonMissingModule = normalized.match(
5029
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
5030
+ );
5031
+ if (pythonMissingModule) {
5032
+ return {
5033
+ reason: `missing module: ${pythonMissingModule[1]}`,
5034
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
5035
+ };
5036
+ }
5037
+ const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
5038
+ if (nodeMissingModule) {
5039
+ return {
5040
+ reason: `missing module: ${nodeMissingModule[1]}`,
5041
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
5042
+ };
5043
+ }
5044
+ const importResolutionFailure = normalized.match(/Failed to resolve import ['"]([^'"]+)['"]/i);
5045
+ if (importResolutionFailure) {
5046
+ return {
5047
+ reason: `missing module: ${importResolutionFailure[1]}`,
5048
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
5049
+ };
5050
+ }
5051
+ const esmModuleFailure = normalized.match(/ERR_MODULE_NOT_FOUND[^'"`]*['"`]([^'"`]+)['"`]/i) ?? normalized.match(/Cannot find package ['"`]([^'"`]+)['"`]/i);
5052
+ if (esmModuleFailure) {
5053
+ return {
5054
+ reason: `missing module: ${esmModuleFailure[1]}`,
5055
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
5056
+ };
5057
+ }
5058
+ const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
5059
+ if (assertionFailure) {
5060
+ return {
5061
+ reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
5062
+ group: "assertion failures"
5063
+ };
5064
+ }
5065
+ const vitestUnhandled = normalized.match(/Vitest caught\s+\d+\s+unhandled errors?/i);
5066
+ if (vitestUnhandled) {
5067
+ return {
5068
+ reason: `RuntimeError: ${buildExcerptDetail(vitestUnhandled[0] ?? normalized, "Vitest caught unhandled errors")}`.slice(
5069
+ 0,
5070
+ 120
5071
+ ),
5072
+ group: "runtime failures"
5073
+ };
5074
+ }
5075
+ const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
5076
+ if (genericError) {
5077
+ const errorType = genericError[1];
5078
+ return {
5079
+ reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
5080
+ group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
5081
+ };
5082
+ }
5083
+ if (/ImportError while importing test module/i.test(normalized)) {
5084
+ return {
5085
+ reason: "import error during collection",
5086
+ group: "import/dependency errors during collection"
5087
+ };
5088
+ }
5089
+ if (!/[A-Za-z]/.test(normalized)) {
5090
+ return null;
5091
+ }
5092
+ return {
5093
+ reason: normalized.slice(0, 120),
5094
+ group: options.duringCollection ? "collection/import errors" : "other failures"
5095
+ };
5096
+ }
5097
+ function pushFocusedFailureItem(items, candidate) {
5098
+ if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
5099
+ return;
5100
+ }
5101
+ items.push(candidate);
5102
+ }
5103
+ function chooseStrongestFailureItems(items) {
5104
+ const strongest = /* @__PURE__ */ new Map();
4052
5105
  const order = [];
4053
5106
  for (const item of items) {
4054
5107
  const existing = strongest.get(item.label);
@@ -4063,6 +5116,125 @@ function chooseStrongestFailureItems(items) {
4063
5116
  }
4064
5117
  return order.map((label) => strongest.get(label));
4065
5118
  }
5119
+ function extractJsTestFile(value) {
5120
+ const match = value.match(/([A-Za-z0-9_./-]+\.(?:test|spec)\.[cm]?[jt]sx?)/i);
5121
+ return match ? normalizeAnchorFile(match[1]) : null;
5122
+ }
5123
+ function normalizeJsFailureLabel(label) {
5124
+ return cleanFailureLabel(label).replace(/^[❯×]\s*/, "").replace(/\s+\[[^\]]+\]\s*$/, "").replace(/\s+/g, " ").trim();
5125
+ }
5126
+ function classifyFailureLines(args) {
5127
+ let observedAnchor = null;
5128
+ let strongest = null;
5129
+ for (const line of args.lines) {
5130
+ observedAnchor = parseObservedAnchor(line) ?? observedAnchor;
5131
+ const classification = classifyFailureReason(line, {
5132
+ duringCollection: args.duringCollection
5133
+ });
5134
+ if (!classification) {
5135
+ continue;
5136
+ }
5137
+ const score = scoreFailureReason(classification.reason);
5138
+ if (!strongest || score > strongest.score) {
5139
+ strongest = {
5140
+ classification,
5141
+ score,
5142
+ observedAnchor: parseObservedAnchor(line) ?? observedAnchor
5143
+ };
5144
+ }
5145
+ }
5146
+ if (!strongest) {
5147
+ return null;
5148
+ }
5149
+ return {
5150
+ classification: strongest.classification,
5151
+ observedAnchor: strongest.observedAnchor ?? observedAnchor
5152
+ };
5153
+ }
5154
+ function collectJsFailureBlocks(input) {
5155
+ const blocks = [];
5156
+ let current = null;
5157
+ let section = null;
5158
+ let currentFile = null;
5159
+ const flushCurrent = () => {
5160
+ if (!current) {
5161
+ return;
5162
+ }
5163
+ blocks.push(current);
5164
+ current = null;
5165
+ };
5166
+ for (const rawLine of input.split("\n")) {
5167
+ const line = rawLine.trimEnd();
5168
+ const trimmed = line.trim();
5169
+ if (/⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(line)) {
5170
+ flushCurrent();
5171
+ section = "failed_tests";
5172
+ continue;
5173
+ }
5174
+ if (/⎯{2,}\s+Failed Suites?\s+\d+\s+⎯{2,}/.test(line)) {
5175
+ flushCurrent();
5176
+ section = "failed_suites";
5177
+ continue;
5178
+ }
5179
+ if (section && /^⎯{2,}.+⎯{2,}\s*$/.test(line)) {
5180
+ flushCurrent();
5181
+ section = null;
5182
+ continue;
5183
+ }
5184
+ const progress = line.match(
5185
+ /^(.+?\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>.+?)?)\s+(FAILED|ERROR)\s+\[[^\]]+\]\s*$/
5186
+ );
5187
+ if (progress) {
5188
+ flushCurrent();
5189
+ const label = normalizeJsFailureLabel(progress[1]);
5190
+ current = {
5191
+ label,
5192
+ status: progress[2] === "ERROR" ? "error" : "failed",
5193
+ detailLines: []
5194
+ };
5195
+ currentFile = extractJsTestFile(label);
5196
+ continue;
5197
+ }
5198
+ const failHeader = line.match(/^\s*FAIL\s+(.+)$/);
5199
+ if (failHeader) {
5200
+ const label = normalizeJsFailureLabel(failHeader[1]);
5201
+ if (extractJsTestFile(label)) {
5202
+ flushCurrent();
5203
+ current = {
5204
+ label,
5205
+ status: section === "failed_suites" || !label.includes(" > ") ? "error" : "failed",
5206
+ detailLines: []
5207
+ };
5208
+ currentFile = extractJsTestFile(label);
5209
+ continue;
5210
+ }
5211
+ }
5212
+ const failedTest = line.match(/^\s*×\s+(.+)$/);
5213
+ if (failedTest && (section === "failed_tests" || extractJsTestFile(failedTest[1]))) {
5214
+ flushCurrent();
5215
+ const candidate = normalizeJsFailureLabel(failedTest[1]);
5216
+ const file = extractJsTestFile(candidate) ?? currentFile;
5217
+ const label = file && !extractJsTestFile(candidate) ? `${file} > ${candidate}` : candidate;
5218
+ current = {
5219
+ label,
5220
+ status: "failed",
5221
+ detailLines: []
5222
+ };
5223
+ currentFile = extractJsTestFile(label) ?? currentFile;
5224
+ continue;
5225
+ }
5226
+ if (/^\s*(?:Tests?|Snapshots?|Test Files?|Test Suites?)\b/.test(line)) {
5227
+ flushCurrent();
5228
+ section = null;
5229
+ continue;
5230
+ }
5231
+ if (current && trimmed.length > 0) {
5232
+ current.detailLines.push(line);
5233
+ }
5234
+ }
5235
+ flushCurrent();
5236
+ return blocks;
5237
+ }
4066
5238
  function collectCollectionFailureItems(input) {
4067
5239
  const items = [];
4068
5240
  const lines = input.split("\n");
@@ -4070,6 +5242,24 @@ function collectCollectionFailureItems(input) {
4070
5242
  let pendingGenericReason = null;
4071
5243
  let currentAnchor = null;
4072
5244
  for (const line of lines) {
5245
+ 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];
5246
+ if (standaloneCollectionLabel) {
5247
+ const classification2 = classifyFailureReason(line, {
5248
+ duringCollection: true
5249
+ });
5250
+ if (classification2) {
5251
+ pushFocusedFailureItem(items, {
5252
+ label: cleanFailureLabel(standaloneCollectionLabel),
5253
+ reason: classification2.reason,
5254
+ group: classification2.group,
5255
+ ...resolveAnchorForLabel({
5256
+ label: cleanFailureLabel(standaloneCollectionLabel),
5257
+ observedAnchor: parseObservedAnchor(line)
5258
+ })
5259
+ });
5260
+ }
5261
+ continue;
5262
+ }
4073
5263
  const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
4074
5264
  if (collecting) {
4075
5265
  if (currentLabel && pendingGenericReason) {
@@ -4158,6 +5348,24 @@ function collectInlineFailureItems(input) {
4158
5348
  })
4159
5349
  });
4160
5350
  }
5351
+ for (const block of collectJsFailureBlocks(input)) {
5352
+ const resolved = classifyFailureLines({
5353
+ lines: block.detailLines,
5354
+ duringCollection: block.status === "error"
5355
+ });
5356
+ if (!resolved) {
5357
+ continue;
5358
+ }
5359
+ pushFocusedFailureItem(items, {
5360
+ label: block.label,
5361
+ reason: resolved.classification.reason,
5362
+ group: resolved.classification.group,
5363
+ ...resolveAnchorForLabel({
5364
+ label: block.label,
5365
+ observedAnchor: resolved.observedAnchor
5366
+ })
5367
+ });
5368
+ }
4161
5369
  return items;
4162
5370
  }
4163
5371
  function collectInlineFailureItemsWithStatus(input) {
@@ -4192,16 +5400,42 @@ function collectInlineFailureItemsWithStatus(input) {
4192
5400
  })
4193
5401
  });
4194
5402
  }
5403
+ for (const block of collectJsFailureBlocks(input)) {
5404
+ const resolved = classifyFailureLines({
5405
+ lines: block.detailLines,
5406
+ duringCollection: block.status === "error"
5407
+ });
5408
+ if (!resolved) {
5409
+ continue;
5410
+ }
5411
+ items.push({
5412
+ label: block.label,
5413
+ reason: resolved.classification.reason,
5414
+ group: resolved.classification.group,
5415
+ status: block.status,
5416
+ ...resolveAnchorForLabel({
5417
+ label: block.label,
5418
+ observedAnchor: resolved.observedAnchor
5419
+ })
5420
+ });
5421
+ }
4195
5422
  return items;
4196
5423
  }
4197
5424
  function collectStandaloneErrorClassifications(input) {
4198
5425
  const classifications = [];
4199
5426
  for (const line of input.split("\n")) {
5427
+ const trimmed = line.trim();
5428
+ if (!trimmed) {
5429
+ continue;
5430
+ }
4200
5431
  const standalone = line.match(/^\s*E\s+(.+)$/);
4201
- if (!standalone) {
5432
+ const candidate = standalone?.[1] ?? (/^(INTERNALERROR>|ConftestImportFailure\b|UsageError:|ERROR:\s*usage:|pytest:\s*error:)/i.test(
5433
+ trimmed
5434
+ ) ? trimmed : null);
5435
+ if (!candidate) {
4202
5436
  continue;
4203
5437
  }
4204
- const classification = classifyFailureReason(standalone[1], {
5438
+ const classification = classifyFailureReason(candidate, {
4205
5439
  duringCollection: false
4206
5440
  });
4207
5441
  if (!classification || classification.reason === "import error during collection") {
@@ -4317,6 +5551,9 @@ function collectFailureLabels(input) {
4317
5551
  pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
4318
5552
  }
4319
5553
  }
5554
+ for (const block of collectJsFailureBlocks(input)) {
5555
+ pushLabel(block.label, block.status);
5556
+ }
4320
5557
  return labels;
4321
5558
  }
4322
5559
  function classifyBucketTypeFromReason(reason) {
@@ -4326,6 +5563,60 @@ function classifyBucketTypeFromReason(reason) {
4326
5563
  if (reason.startsWith("fixture guard:")) {
4327
5564
  return "fixture_guard_failure";
4328
5565
  }
5566
+ if (reason.startsWith("timeout:")) {
5567
+ return "timeout_failure";
5568
+ }
5569
+ if (reason.startsWith("permission:")) {
5570
+ return "permission_denied_failure";
5571
+ }
5572
+ if (reason.startsWith("async loop:")) {
5573
+ return "async_event_loop_failure";
5574
+ }
5575
+ if (reason.startsWith("fixture teardown:")) {
5576
+ return "fixture_teardown_failure";
5577
+ }
5578
+ if (reason.startsWith("db migration:")) {
5579
+ return "db_migration_failure";
5580
+ }
5581
+ if (reason.startsWith("configuration:")) {
5582
+ return "configuration_error";
5583
+ }
5584
+ if (reason.startsWith("xdist worker crash:")) {
5585
+ return "xdist_worker_crash";
5586
+ }
5587
+ if (reason.startsWith("type error:")) {
5588
+ return "type_error_failure";
5589
+ }
5590
+ if (reason.startsWith("resource leak:")) {
5591
+ return "resource_leak_warning";
5592
+ }
5593
+ if (reason.startsWith("django db access:")) {
5594
+ return "django_db_access_denied";
5595
+ }
5596
+ if (reason.startsWith("network:")) {
5597
+ return "network_failure";
5598
+ }
5599
+ if (reason.startsWith("segfault:")) {
5600
+ return "subprocess_crash_segfault";
5601
+ }
5602
+ if (reason.startsWith("flaky:")) {
5603
+ return "flaky_test_detected";
5604
+ }
5605
+ if (reason.startsWith("serialization:")) {
5606
+ return "serialization_encoding_failure";
5607
+ }
5608
+ if (reason.startsWith("file not found:")) {
5609
+ return "file_not_found_failure";
5610
+ }
5611
+ if (reason.startsWith("memory:")) {
5612
+ return "memory_error";
5613
+ }
5614
+ if (reason.startsWith("deprecation as error:")) {
5615
+ return "deprecation_warning_as_error";
5616
+ }
5617
+ if (reason.startsWith("xfail strict:")) {
5618
+ return "xfail_strict_unexpected_pass";
5619
+ }
4329
5620
  if (reason.startsWith("service unavailable:")) {
4330
5621
  return "service_unavailable";
4331
5622
  }
@@ -4335,6 +5626,9 @@ function classifyBucketTypeFromReason(reason) {
4335
5626
  if (reason.startsWith("auth bypass absent:")) {
4336
5627
  return "auth_bypass_absent";
4337
5628
  }
5629
+ if (reason.startsWith("snapshot mismatch:")) {
5630
+ return "snapshot_mismatch";
5631
+ }
4338
5632
  if (reason.startsWith("missing module:")) {
4339
5633
  return "import_dependency_failure";
4340
5634
  }
@@ -4347,9 +5641,6 @@ function classifyBucketTypeFromReason(reason) {
4347
5641
  return "unknown_failure";
4348
5642
  }
4349
5643
  function synthesizeSharedBlockerBucket(args) {
4350
- if (args.errors === 0) {
4351
- return null;
4352
- }
4353
5644
  const visibleReasonGroups = /* @__PURE__ */ new Map();
4354
5645
  for (const item of args.visibleErrorItems) {
4355
5646
  const entry = visibleReasonGroups.get(item.reason);
@@ -4364,7 +5655,7 @@ function synthesizeSharedBlockerBucket(args) {
4364
5655
  items: [item]
4365
5656
  });
4366
5657
  }
4367
- const top = [...visibleReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
5658
+ const top = [...visibleReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
4368
5659
  const standaloneReasonGroups = /* @__PURE__ */ new Map();
4369
5660
  for (const classification of collectStandaloneErrorClassifications(args.input)) {
4370
5661
  const entry = standaloneReasonGroups.get(classification.reason);
@@ -4377,7 +5668,7 @@ function synthesizeSharedBlockerBucket(args) {
4377
5668
  group: classification.group
4378
5669
  });
4379
5670
  }
4380
- const standaloneTop = [...standaloneReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
5671
+ const standaloneTop = [...standaloneReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
4381
5672
  const visibleTopReason = top?.[0];
4382
5673
  const visibleTopStats = top?.[1];
4383
5674
  const standaloneTopReason = standaloneTop?.[0];
@@ -4416,6 +5707,12 @@ function synthesizeSharedBlockerBucket(args) {
4416
5707
  let hint;
4417
5708
  if (envVar) {
4418
5709
  hint = `Set ${envVar} (or pass --pgtest-dsn) before rerunning DB-isolated tests.`;
5710
+ } else if (effectiveReason.startsWith("configuration:")) {
5711
+ hint = "Fix the pytest configuration or conftest import error before rerunning the suite.";
5712
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
5713
+ hint = "Check shared state, worker startup, or resource contention between xdist workers before rerunning.";
5714
+ } else if (effectiveReason.startsWith("network:")) {
5715
+ hint = "Restore DNS, TLS, or outbound network access for the affected dependency before rerunning.";
4419
5716
  } else if (effectiveReason.startsWith("fixture guard:")) {
4420
5717
  hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
4421
5718
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -4430,6 +5727,12 @@ function synthesizeSharedBlockerBucket(args) {
4430
5727
  let headline;
4431
5728
  if (envVar) {
4432
5729
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors require ${envVar} for DB-isolated tests.`;
5730
+ } else if (effectiveReason.startsWith("configuration:")) {
5731
+ headline = `Shared blocker: ${atLeastPrefix}${countText} visible failure${countText === 1 ? "" : "s"} are caused by a pytest configuration error.`;
5732
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
5733
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by xdist worker crashes.`;
5734
+ } else if (effectiveReason.startsWith("network:")) {
5735
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by a network dependency failure.`;
4433
5736
  } else if (effectiveReason.startsWith("fixture guard:")) {
4434
5737
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
4435
5738
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -4460,11 +5763,17 @@ function synthesizeSharedBlockerBucket(args) {
4460
5763
  };
4461
5764
  }
4462
5765
  function synthesizeImportDependencyBucket(args) {
4463
- if (args.errors === 0) {
4464
- return null;
4465
- }
4466
- const importItems = args.visibleErrorItems.filter((item) => item.reason.startsWith("missing module:"));
4467
- if (importItems.length < 2) {
5766
+ const visibleImportItems = args.visibleErrorItems.filter(
5767
+ (item) => item.reason.startsWith("missing module:")
5768
+ );
5769
+ const inlineImportItems = chooseStrongestFailureItems(
5770
+ args.inlineItems.filter((item) => item.reason.startsWith("missing module:"))
5771
+ );
5772
+ const importItems = visibleImportItems.length > 0 ? visibleImportItems : inlineImportItems.map((item) => ({
5773
+ ...item,
5774
+ status: "failed"
5775
+ }));
5776
+ if (importItems.length === 0) {
4468
5777
  return null;
4469
5778
  }
4470
5779
  const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
@@ -4475,7 +5784,7 @@ function synthesizeImportDependencyBucket(args) {
4475
5784
  )
4476
5785
  ).slice(0, 6);
4477
5786
  const headlineCount = countClaimed ?? importItems.length;
4478
- 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.`;
5787
+ 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.`;
4479
5788
  const summaryLines = [headline];
4480
5789
  if (modules.length > 0) {
4481
5790
  summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
@@ -4485,7 +5794,7 @@ function synthesizeImportDependencyBucket(args) {
4485
5794
  headline,
4486
5795
  countVisible: importItems.length,
4487
5796
  countClaimed,
4488
- reason: "missing dependencies during test collection",
5797
+ reason: modules.length === 1 ? `missing module: ${modules[0]}` : "missing dependencies during test collection",
4489
5798
  representativeItems: importItems.slice(0, 4).map((item) => ({
4490
5799
  label: item.label,
4491
5800
  reason: item.reason,
@@ -4504,7 +5813,7 @@ function synthesizeImportDependencyBucket(args) {
4504
5813
  };
4505
5814
  }
4506
5815
  function isContractDriftLabel(label) {
4507
- return /(freeze|snapshot|contract|manifest|openapi|golden)/i.test(label);
5816
+ return /(freeze|contract|manifest|openapi|golden)/i.test(label);
4508
5817
  }
4509
5818
  function looksLikeTaskKey(value) {
4510
5819
  return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
@@ -4635,23 +5944,81 @@ function synthesizeContractDriftBucket(args) {
4635
5944
  overflowLabel: "changed entities"
4636
5945
  };
4637
5946
  }
5947
+ function synthesizeSnapshotMismatchBucket(args) {
5948
+ const snapshotItems = chooseStrongestFailureItems(
5949
+ args.inlineItems.filter((item) => item.reason.startsWith("snapshot mismatch:"))
5950
+ );
5951
+ if (snapshotItems.length === 0) {
5952
+ return null;
5953
+ }
5954
+ const countClaimed = args.snapshotFailures && args.snapshotFailures >= snapshotItems.length ? args.snapshotFailures : void 0;
5955
+ const countText = countClaimed ?? snapshotItems.length;
5956
+ const summaryLines = [
5957
+ `Snapshot mismatches: ${formatCount2(countText, "snapshot expectation")} ${countText === 1 ? "is" : "are"} out of date with current output.`
5958
+ ];
5959
+ return {
5960
+ type: "snapshot_mismatch",
5961
+ headline: summaryLines[0],
5962
+ countVisible: snapshotItems.length,
5963
+ countClaimed,
5964
+ reason: "snapshot mismatch: snapshot expectations differ from current output",
5965
+ representativeItems: snapshotItems.slice(0, 4),
5966
+ entities: snapshotItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
5967
+ hint: "Update the snapshots if these output changes are intentional.",
5968
+ confidence: countClaimed ? 0.92 : 0.8,
5969
+ summaryLines,
5970
+ overflowCount: Math.max((countClaimed ?? snapshotItems.length) - Math.min(snapshotItems.length, 4), 0),
5971
+ overflowLabel: "snapshot failures"
5972
+ };
5973
+ }
5974
+ function synthesizeTimeoutBucket(args) {
5975
+ const timeoutItems = chooseStrongestFailureItems(
5976
+ args.inlineItems.filter((item) => item.reason.startsWith("timeout:"))
5977
+ );
5978
+ if (timeoutItems.length === 0) {
5979
+ return null;
5980
+ }
5981
+ const summaryLines = [
5982
+ `Timeout failures: ${formatCount2(timeoutItems.length, "test")} exceeded the configured timeout threshold.`
5983
+ ];
5984
+ return {
5985
+ type: "timeout_failure",
5986
+ headline: summaryLines[0],
5987
+ countVisible: timeoutItems.length,
5988
+ countClaimed: timeoutItems.length,
5989
+ reason: timeoutItems.length === 1 ? timeoutItems[0].reason : "timeout: tests exceeded the configured timeout threshold",
5990
+ representativeItems: timeoutItems.slice(0, 4),
5991
+ entities: timeoutItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
5992
+ hint: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning.",
5993
+ confidence: 0.84,
5994
+ summaryLines,
5995
+ overflowCount: Math.max(timeoutItems.length - Math.min(timeoutItems.length, 4), 0),
5996
+ overflowLabel: "timeout failures"
5997
+ };
5998
+ }
4638
5999
  function analyzeTestStatus(input) {
4639
- const passed = getCount(input, "passed");
4640
- const failed = getCount(input, "failed");
4641
- const errors = Math.max(getCount(input, "errors"), getCount(input, "error"));
4642
- const skipped = getCount(input, "skipped");
6000
+ const runner = detectTestRunner(input);
6001
+ const counts = extractTestStatusCounts(input, runner);
6002
+ const passed = counts.passed;
6003
+ const failed = counts.failed;
6004
+ const errors = counts.errors;
6005
+ const skipped = counts.skipped;
4643
6006
  const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
4644
- const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
6007
+ 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);
4645
6008
  const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
4646
6009
  const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
4647
6010
  const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
6011
+ const statusItems = collectInlineFailureItemsWithStatus(input);
4648
6012
  const visibleErrorItems = chooseStrongestStatusFailureItems([
4649
6013
  ...collectionItems.map((item) => ({
4650
6014
  ...item,
4651
6015
  status: "error"
4652
6016
  })),
4653
- ...collectInlineFailureItemsWithStatus(input).filter((item) => item.status === "error")
6017
+ ...statusItems.filter((item) => item.status === "error")
4654
6018
  ]);
6019
+ const visibleFailedItems = chooseStrongestStatusFailureItems(
6020
+ statusItems.filter((item) => item.status === "failed")
6021
+ );
4655
6022
  const labels = collectFailureLabels(input);
4656
6023
  const visibleErrorLabels = labels.filter((item) => item.status === "error").map((item) => item.label);
4657
6024
  const visibleFailedLabels = labels.filter((item) => item.status === "failed").map((item) => item.label);
@@ -4668,7 +6035,8 @@ function analyzeTestStatus(input) {
4668
6035
  if (!sharedBlocker) {
4669
6036
  const importDependencyBucket = synthesizeImportDependencyBucket({
4670
6037
  errors,
4671
- visibleErrorItems
6038
+ visibleErrorItems,
6039
+ inlineItems
4672
6040
  });
4673
6041
  if (importDependencyBucket) {
4674
6042
  buckets.push(importDependencyBucket);
@@ -4681,11 +6049,26 @@ function analyzeTestStatus(input) {
4681
6049
  if (contractDrift) {
4682
6050
  buckets.push(contractDrift);
4683
6051
  }
6052
+ const snapshotMismatch = synthesizeSnapshotMismatchBucket({
6053
+ inlineItems,
6054
+ snapshotFailures: counts.snapshotFailures
6055
+ });
6056
+ if (snapshotMismatch) {
6057
+ buckets.push(snapshotMismatch);
6058
+ }
6059
+ const timeoutBucket = synthesizeTimeoutBucket({
6060
+ inlineItems
6061
+ });
6062
+ if (timeoutBucket) {
6063
+ buckets.push(timeoutBucket);
6064
+ }
4684
6065
  return {
6066
+ runner,
4685
6067
  passed,
4686
6068
  failed,
4687
6069
  errors,
4688
6070
  skipped,
6071
+ snapshotFailures: counts.snapshotFailures,
4689
6072
  noTestsCollected,
4690
6073
  interrupted,
4691
6074
  collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
@@ -4694,6 +6077,7 @@ function analyzeTestStatus(input) {
4694
6077
  visibleErrorLabels,
4695
6078
  visibleFailedLabels,
4696
6079
  visibleErrorItems,
6080
+ visibleFailedItems,
4697
6081
  buckets
4698
6082
  };
4699
6083
  }
@@ -4725,104 +6109,656 @@ function testStatusHeuristic(input, detail = "standard") {
4725
6109
  })
4726
6110
  ].join("\n");
4727
6111
  }
4728
- if (analysis.noTestsCollected) {
4729
- return ["- Tests did not run.", "- Collected 0 items."].join("\n");
6112
+ if (analysis.noTestsCollected) {
6113
+ return ["- Tests did not run.", "- Collected 0 items."].join("\n");
6114
+ }
6115
+ if (analysis.interrupted && analysis.failed === 0 && analysis.errors === 0) {
6116
+ return "- Test run was interrupted.";
6117
+ }
6118
+ if (analysis.failed === 0 && analysis.errors === 0 && analysis.passed > 0) {
6119
+ const details = [formatCount2(analysis.passed, "test")];
6120
+ if (analysis.skipped > 0) {
6121
+ details.push(formatCount2(analysis.skipped, "skip"));
6122
+ }
6123
+ return ["- Tests passed.", `- ${details.join(", ")}.`].join("\n");
6124
+ }
6125
+ if (analysis.failed > 0 || analysis.errors > 0 || analysis.inlineItems.length > 0 || analysis.buckets.length > 0) {
6126
+ const decision = buildTestStatusDiagnoseContract({
6127
+ input,
6128
+ analysis
6129
+ });
6130
+ if (detail === "verbose") {
6131
+ return decision.verboseText;
6132
+ }
6133
+ if (detail === "focused") {
6134
+ return decision.focusedText;
6135
+ }
6136
+ return decision.standardText;
6137
+ }
6138
+ return null;
6139
+ }
6140
+ function auditCriticalHeuristic(input) {
6141
+ if (/\bfound\s+0\s+vulnerabilities\b/i.test(input) || /\b0\s+vulnerabilities\b/i.test(input)) {
6142
+ return JSON.stringify(
6143
+ {
6144
+ status: "ok",
6145
+ vulnerabilities: [],
6146
+ summary: "No high or critical vulnerabilities found in the provided input."
6147
+ },
6148
+ null,
6149
+ 2
6150
+ );
6151
+ }
6152
+ const vulnerabilities = collectAuditCriticalVulnerabilities(input);
6153
+ if (vulnerabilities.length === 0) {
6154
+ return null;
6155
+ }
6156
+ const firstVulnerability = vulnerabilities[0];
6157
+ return JSON.stringify(
6158
+ {
6159
+ status: "ok",
6160
+ vulnerabilities,
6161
+ summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
6162
+ },
6163
+ null,
6164
+ 2
6165
+ );
6166
+ }
6167
+ function infraRiskHeuristic(input) {
6168
+ const destroyTargets = collectInfraDestroyTargets(input);
6169
+ const blockers = collectInfraBlockers(input);
6170
+ const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
6171
+ const riskEvidence = collectInfraRiskEvidence(input);
6172
+ if (riskEvidence.length > 0) {
6173
+ return JSON.stringify(
6174
+ {
6175
+ verdict: "fail",
6176
+ reason: "Destructive or clearly risky infrastructure change signals are present.",
6177
+ evidence: riskEvidence,
6178
+ destroy_count: inferInfraDestroyCount(input, destroyTargets),
6179
+ destroy_targets: destroyTargets,
6180
+ blockers
6181
+ },
6182
+ null,
6183
+ 2
6184
+ );
6185
+ }
6186
+ if (zeroDestructiveEvidence.length > 0) {
6187
+ return JSON.stringify(
6188
+ {
6189
+ verdict: "pass",
6190
+ reason: "The provided input explicitly indicates zero destructive changes.",
6191
+ evidence: zeroDestructiveEvidence,
6192
+ destroy_count: 0,
6193
+ destroy_targets: [],
6194
+ blockers: []
6195
+ },
6196
+ null,
6197
+ 2
6198
+ );
6199
+ }
6200
+ const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
6201
+ if (safeEvidence.length > 0) {
6202
+ return JSON.stringify(
6203
+ {
6204
+ verdict: "pass",
6205
+ reason: "The provided input explicitly indicates no risky infrastructure changes.",
6206
+ evidence: safeEvidence,
6207
+ destroy_count: 0,
6208
+ destroy_targets: [],
6209
+ blockers: []
6210
+ },
6211
+ null,
6212
+ 2
6213
+ );
6214
+ }
6215
+ return null;
6216
+ }
6217
+ function parseTscErrors(input) {
6218
+ const diagnostics = [];
6219
+ for (const rawLine of input.split("\n")) {
6220
+ const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").trimEnd();
6221
+ if (!line.trim()) {
6222
+ continue;
6223
+ }
6224
+ let match = line.match(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
6225
+ if (match) {
6226
+ diagnostics.push({
6227
+ file: match[1].replace(/\\/g, "/").trim(),
6228
+ line: Number(match[2]),
6229
+ column: Number(match[3]),
6230
+ code: match[4],
6231
+ message: match[5].trim()
6232
+ });
6233
+ continue;
6234
+ }
6235
+ match = line.match(/^(.+):(\d+):(\d+)\s+-\s+error\s+(TS\d+):\s+(.+)$/);
6236
+ if (match) {
6237
+ diagnostics.push({
6238
+ file: match[1].replace(/\\/g, "/").trim(),
6239
+ line: Number(match[2]),
6240
+ column: Number(match[3]),
6241
+ code: match[4],
6242
+ message: match[5].trim()
6243
+ });
6244
+ continue;
6245
+ }
6246
+ match = line.match(/^\s*error\s+(TS\d+):\s+(.+)$/);
6247
+ if (match) {
6248
+ diagnostics.push({
6249
+ file: null,
6250
+ line: null,
6251
+ column: null,
6252
+ code: match[1],
6253
+ message: match[2].trim()
6254
+ });
6255
+ }
6256
+ }
6257
+ return diagnostics;
6258
+ }
6259
+ function extractTscSummary(input) {
6260
+ const matches = [
6261
+ ...input.matchAll(/\bFound\s+(\d+)\s+errors?\b(?:\s+in\s+(\d+)\s+files?)?\.?/gi)
6262
+ ];
6263
+ const summary = matches.at(-1);
6264
+ if (!summary) {
6265
+ return null;
6266
+ }
6267
+ return {
6268
+ errorCount: Number(summary[1]),
6269
+ fileCount: summary[2] ? Number(summary[2]) : null
6270
+ };
6271
+ }
6272
+ function formatTscGroup(args) {
6273
+ const label = TSC_CODE_LABELS[args.code];
6274
+ const displayFiles = formatDisplayedFiles(args.files);
6275
+ let line = `- ${args.code}`;
6276
+ if (label) {
6277
+ line += ` (${label})`;
6278
+ }
6279
+ line += `: ${formatCount2(args.count, "occurrence")}`;
6280
+ if (displayFiles.length > 0) {
6281
+ line += ` across ${displayFiles.join(", ")}`;
6282
+ }
6283
+ return `${line}.`;
6284
+ }
6285
+ function typecheckSummaryHeuristic(input) {
6286
+ if (input.trim().length === 0) {
6287
+ return null;
6288
+ }
6289
+ const diagnostics = parseTscErrors(input);
6290
+ const summary = extractTscSummary(input);
6291
+ const hasTscSignal = diagnostics.length > 0 || summary !== null || /\berror\s+TS\d+:/m.test(input);
6292
+ if (!hasTscSignal) {
6293
+ return null;
6294
+ }
6295
+ if (summary?.errorCount === 0) {
6296
+ return "No type errors.";
6297
+ }
6298
+ if (diagnostics.length === 0 && summary === null) {
6299
+ return null;
6300
+ }
6301
+ const errorCount = summary?.errorCount ?? diagnostics.length;
6302
+ const allFiles = new Set(
6303
+ diagnostics.map((diagnostic) => diagnostic.file).filter((file) => Boolean(file))
6304
+ );
6305
+ const fileCount = summary?.fileCount ?? (allFiles.size > 0 ? allFiles.size : null);
6306
+ const groups = /* @__PURE__ */ new Map();
6307
+ for (const diagnostic of diagnostics) {
6308
+ const group = groups.get(diagnostic.code) ?? {
6309
+ count: 0,
6310
+ files: /* @__PURE__ */ new Set()
6311
+ };
6312
+ group.count += 1;
6313
+ if (diagnostic.file) {
6314
+ group.files.add(diagnostic.file);
6315
+ }
6316
+ groups.set(diagnostic.code, group);
6317
+ }
6318
+ const bullets = [
6319
+ `- Typecheck failed: ${formatCount2(errorCount, "error")}${fileCount ? ` in ${formatCount2(fileCount, "file")}` : ""}.`
6320
+ ];
6321
+ const sortedGroups = [...groups.entries()].map(([code, group]) => ({
6322
+ code,
6323
+ count: group.count,
6324
+ files: group.files
6325
+ })).sort((left, right) => right.count - left.count || left.code.localeCompare(right.code));
6326
+ for (const group of sortedGroups.slice(0, 3)) {
6327
+ bullets.push(formatTscGroup(group));
6328
+ }
6329
+ if (sortedGroups.length > 3) {
6330
+ const overflowFiles = /* @__PURE__ */ new Set();
6331
+ for (const group of sortedGroups.slice(3)) {
6332
+ for (const file of group.files) {
6333
+ overflowFiles.add(file);
6334
+ }
6335
+ }
6336
+ let overflow = `- ${formatCount2(sortedGroups.length - 3, "more error code")}`;
6337
+ if (overflowFiles.size > 0) {
6338
+ overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
6339
+ }
6340
+ bullets.push(`${overflow}.`);
6341
+ }
6342
+ return bullets.join("\n");
6343
+ }
6344
+ function looksLikeEslintFileHeader(line) {
6345
+ if (line.trim().length === 0 || line.trim() !== line) {
6346
+ return false;
6347
+ }
6348
+ if (/^\s*[✖×x]\s+\d+\s+problems?\b/i.test(line) || /potentially\s+fixable/i.test(line) || /^\d+\s+problems?\b/i.test(line)) {
6349
+ return false;
6350
+ }
6351
+ const normalized = line.replace(/\\/g, "/");
6352
+ const pathLike = normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../") || /^[A-Za-z]:\//.test(normalized) || /^[A-Za-z0-9_.-]+\//.test(normalized);
6353
+ return pathLike && /\.[A-Za-z0-9]+$/.test(normalized);
6354
+ }
6355
+ function normalizeEslintRule(rule, message) {
6356
+ if (rule && rule.trim().length > 0) {
6357
+ return rule.trim();
6358
+ }
6359
+ if (/parsing error/i.test(message)) {
6360
+ return "parsing error";
6361
+ }
6362
+ if (/fatal/i.test(message)) {
6363
+ return "fatal error";
6364
+ }
6365
+ return "unclassified lint error";
6366
+ }
6367
+ function parseEslintStylish(input) {
6368
+ const violations = [];
6369
+ let currentFile = null;
6370
+ for (const rawLine of input.split("\n")) {
6371
+ const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").replace(/\r$/, "");
6372
+ if (looksLikeEslintFileHeader(line.trim())) {
6373
+ currentFile = line.trim().replace(/\\/g, "/");
6374
+ continue;
6375
+ }
6376
+ let match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)\s*$/);
6377
+ if (match) {
6378
+ violations.push({
6379
+ file: currentFile ?? "(unknown file)",
6380
+ line: Number(match[1]),
6381
+ column: Number(match[2]),
6382
+ severity: match[3],
6383
+ message: match[4].trim(),
6384
+ rule: normalizeEslintRule(match[5], match[4])
6385
+ });
6386
+ continue;
6387
+ }
6388
+ match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s*$/);
6389
+ if (match) {
6390
+ violations.push({
6391
+ file: currentFile ?? "(unknown file)",
6392
+ line: Number(match[1]),
6393
+ column: Number(match[2]),
6394
+ severity: match[3],
6395
+ message: match[4].trim(),
6396
+ rule: normalizeEslintRule(null, match[4])
6397
+ });
6398
+ }
6399
+ }
6400
+ return violations;
6401
+ }
6402
+ function extractEslintSummary(input) {
6403
+ const summaryMatches = [
6404
+ ...input.matchAll(
6405
+ /^\s*[✖×x]?\s*(\d+)\s+problems?\s+\((\d+)\s+errors?,\s+(\d+)\s+warnings?\)/gim
6406
+ )
6407
+ ];
6408
+ const summary = summaryMatches.at(-1);
6409
+ if (!summary) {
6410
+ return null;
6411
+ }
6412
+ const fixableMatch = input.match(
6413
+ /(\d+)\s+errors?\s+and\s+(\d+)\s+warnings?\s+(?:are|is)\s+potentially\s+fixable/i
6414
+ );
6415
+ return {
6416
+ problems: Number(summary[1]),
6417
+ errors: Number(summary[2]),
6418
+ warnings: Number(summary[3]),
6419
+ fixableProblems: fixableMatch ? Number(fixableMatch[1]) + Number(fixableMatch[2]) : null
6420
+ };
6421
+ }
6422
+ function formatLintGroup(args) {
6423
+ const totalErrors = args.errors;
6424
+ const totalWarnings = args.warnings;
6425
+ const displayFiles = formatDisplayedFiles(args.files);
6426
+ let detail = "";
6427
+ if (totalErrors > 0 && totalWarnings > 0) {
6428
+ detail = `${formatCount2(totalErrors, "error")}, ${formatCount2(totalWarnings, "warning")}`;
6429
+ } else if (totalErrors > 0) {
6430
+ detail = formatCount2(totalErrors, "error");
6431
+ } else {
6432
+ detail = formatCount2(totalWarnings, "warning");
6433
+ }
6434
+ let line = `- ${args.rule}: ${detail}`;
6435
+ if (displayFiles.length > 0) {
6436
+ line += ` across ${displayFiles.join(", ")}`;
6437
+ }
6438
+ return `${line}.`;
6439
+ }
6440
+ function lintFailuresHeuristic(input) {
6441
+ const trimmed = input.trim();
6442
+ if (trimmed.length === 0 || trimmed.startsWith("[") || trimmed.startsWith("{")) {
6443
+ return null;
6444
+ }
6445
+ const summary = extractEslintSummary(input);
6446
+ const violations = parseEslintStylish(input);
6447
+ if (summary === null && violations.length === 0) {
6448
+ return null;
6449
+ }
6450
+ if (summary?.problems === 0) {
6451
+ return "No lint failures.";
6452
+ }
6453
+ const problems = summary?.problems ?? violations.length;
6454
+ const errors = summary?.errors ?? countPattern(input, /^\s*\d+:\d+\s+error\b/gm);
6455
+ const warnings = summary?.warnings ?? countPattern(input, /^\s*\d+:\d+\s+warning\b/gm);
6456
+ const bullets = [];
6457
+ if (errors > 0) {
6458
+ let headline = `- Lint failed: ${formatCount2(problems, "problem")} (${formatCount2(errors, "error")}, ${formatCount2(warnings, "warning")}).`;
6459
+ if ((summary?.fixableProblems ?? 0) > 0) {
6460
+ headline += ` ${formatCount2(summary.fixableProblems, "problem")} potentially fixable with --fix.`;
6461
+ }
6462
+ bullets.push(headline);
6463
+ } else {
6464
+ bullets.push(`- No lint errors visible: ${formatCount2(warnings, "warning")}.`);
6465
+ }
6466
+ const groups = /* @__PURE__ */ new Map();
6467
+ for (const violation of violations) {
6468
+ const group = groups.get(violation.rule) ?? {
6469
+ errors: 0,
6470
+ warnings: 0,
6471
+ files: /* @__PURE__ */ new Set()
6472
+ };
6473
+ if (violation.severity === "error") {
6474
+ group.errors += 1;
6475
+ } else {
6476
+ group.warnings += 1;
6477
+ }
6478
+ group.files.add(violation.file);
6479
+ groups.set(violation.rule, group);
6480
+ }
6481
+ const sortedGroups = [...groups.entries()].map(([rule, group]) => ({
6482
+ rule,
6483
+ errors: group.errors,
6484
+ warnings: group.warnings,
6485
+ total: group.errors + group.warnings,
6486
+ files: group.files
6487
+ })).sort((left, right) => {
6488
+ const leftHasErrors = left.errors > 0 ? 1 : 0;
6489
+ const rightHasErrors = right.errors > 0 ? 1 : 0;
6490
+ return rightHasErrors - leftHasErrors || right.total - left.total || left.rule.localeCompare(right.rule);
6491
+ });
6492
+ for (const group of sortedGroups.slice(0, 3)) {
6493
+ bullets.push(formatLintGroup(group));
6494
+ }
6495
+ if (sortedGroups.length > 3) {
6496
+ const overflowFiles = /* @__PURE__ */ new Set();
6497
+ for (const group of sortedGroups.slice(3)) {
6498
+ for (const file of group.files) {
6499
+ overflowFiles.add(file);
6500
+ }
6501
+ }
6502
+ let overflow = `- ${formatCount2(sortedGroups.length - 3, "more rule")}`;
6503
+ if (overflowFiles.size > 0) {
6504
+ overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
6505
+ }
6506
+ bullets.push(`${overflow}.`);
6507
+ }
6508
+ return bullets.join("\n");
6509
+ }
6510
+ function stripAnsiText(input) {
6511
+ return input.replace(/\u001b\[[0-9;]*m/g, "");
6512
+ }
6513
+ function normalizeBuildPath(file) {
6514
+ return file.replace(/\\/g, "/").replace(/^\.\//, "").trim();
6515
+ }
6516
+ function trimTrailingSentencePunctuation(input) {
6517
+ return input.replace(/[.:]+$/, "").trim();
6518
+ }
6519
+ function containsKnownBuildFailureSignal(input) {
6520
+ return /^ERROR in /m.test(input) || /^(?:[✘✗]\s*)?\[ERROR\]\s+/m.test(input) || /^error(?:\[E\d+\])?:\s+/m.test(input) || /^.+?\.go:\d+:\d+:\s+\S+/m.test(input) || /^.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm):\d+:\d+:\s*error:\s+/m.test(input) || /\berror\s+TS\d+:/m.test(input) || /^\s*npm ERR!/m.test(input) || /\bERR_PNPM_/m.test(input) || /^\s*error Command failed/m.test(input);
6521
+ }
6522
+ function detectExplicitBuildSuccess(input) {
6523
+ if (containsKnownBuildFailureSignal(input)) {
6524
+ return false;
6525
+ }
6526
+ return /\bcompiled successfully\b/i.test(input) || /^\s*Build succeeded\.?\s*$/im.test(input) || /\bcompiled with 0 errors?\b/i.test(input);
6527
+ }
6528
+ function inferBuildFailureCategory(message) {
6529
+ if (/module not found|can't resolve|could not resolve|cannot find module|no required module provides package/i.test(
6530
+ message
6531
+ )) {
6532
+ return "module-resolution";
4730
6533
  }
4731
- if (analysis.interrupted && analysis.failed === 0 && analysis.errors === 0) {
4732
- return "- Test run was interrupted.";
6534
+ if (/no matching export|does not provide an export named|missing export/i.test(message)) {
6535
+ return "missing-export";
4733
6536
  }
4734
- if (analysis.failed === 0 && analysis.errors === 0 && analysis.passed > 0) {
4735
- const details = [formatCount2(analysis.passed, "test")];
4736
- if (analysis.skipped > 0) {
4737
- details.push(formatCount2(analysis.skipped, "skip"));
6537
+ if (/cannot find name|cannot find value|not found in this scope|undefined:|undeclared identifier/i.test(
6538
+ message
6539
+ )) {
6540
+ return "undefined-identifier";
6541
+ }
6542
+ if (/syntax error|unexpected token|expected ['"`;)]|expected .* after expression/i.test(message)) {
6543
+ return "syntax";
6544
+ }
6545
+ if (/\bTS\d+\b/.test(message) || /type .* is not assignable|type error|no matching overload/i.test(message)) {
6546
+ return "type";
6547
+ }
6548
+ return "generic";
6549
+ }
6550
+ function buildFailureSuggestion(category) {
6551
+ switch (category) {
6552
+ case "module-resolution":
6553
+ return "Install the missing package or fix the import path.";
6554
+ case "missing-export":
6555
+ return "Check the export name in the source module.";
6556
+ case "undefined-identifier":
6557
+ return "Define or import the missing identifier.";
6558
+ case "syntax":
6559
+ return "Fix the syntax error at the indicated location.";
6560
+ case "type":
6561
+ return "Fix the type error at the indicated location.";
6562
+ case "wrapper":
6563
+ return "Check the underlying build tool output above.";
6564
+ default:
6565
+ return "Fix the first reported error and rebuild.";
6566
+ }
6567
+ }
6568
+ function formatBuildFailureOutput(match) {
6569
+ const message = trimTrailingSentencePunctuation(match.message);
6570
+ const suggestion = buildFailureSuggestion(match.category);
6571
+ const displayFile = match.file ? compactDisplayFile(match.file) : null;
6572
+ if (displayFile && match.line !== null) {
6573
+ return `Build failed: ${message} in ${displayFile}:${match.line}. Fix: ${suggestion}`;
6574
+ }
6575
+ if (displayFile) {
6576
+ return `Build failed: ${message} in ${displayFile}. Fix: ${suggestion}`;
6577
+ }
6578
+ return `Build failed: ${message}. Fix: ${suggestion}`;
6579
+ }
6580
+ function extractWebpackBuildFailure(input) {
6581
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
6582
+ for (let index = 0; index < lines.length; index += 1) {
6583
+ const match = lines[index]?.match(/^ERROR in (.+?)(?:\s+(\d+):(\d+))?$/);
6584
+ if (!match) {
6585
+ continue;
4738
6586
  }
4739
- return ["- Tests passed.", `- ${details.join(", ")}.`].join("\n");
4740
- }
4741
- if (analysis.failed > 0 || analysis.errors > 0 || analysis.inlineItems.length > 0 || analysis.buckets.length > 0) {
4742
- const decision = buildTestStatusDiagnoseContract({
4743
- input,
4744
- analysis
4745
- });
4746
- if (detail === "verbose") {
4747
- return decision.verboseText;
6587
+ const candidates = [];
6588
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
6589
+ const candidate = lines[cursor]?.trim();
6590
+ if (!candidate) {
6591
+ continue;
6592
+ }
6593
+ if (/^ERROR in /.test(candidate) || /compiled with \d+ errors?/i.test(candidate)) {
6594
+ break;
6595
+ }
6596
+ if (/^(?:>|\|)|^\d+\s+\|/.test(candidate)) {
6597
+ continue;
6598
+ }
6599
+ candidates.push(candidate);
4748
6600
  }
4749
- if (detail === "focused") {
4750
- return decision.focusedText;
6601
+ let message = "Compilation error";
6602
+ if (candidates.length > 0) {
6603
+ const preferred = candidates.find(
6604
+ (candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate) && inferBuildFailureCategory(candidate) !== "generic"
6605
+ ) ?? candidates.find(
6606
+ (candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate)
6607
+ ) ?? candidates[0];
6608
+ message = preferred ?? message;
4751
6609
  }
4752
- return decision.standardText;
6610
+ return {
6611
+ message,
6612
+ file: normalizeBuildPath(match[1]),
6613
+ line: match[2] ? Number(match[2]) : null,
6614
+ column: match[3] ? Number(match[3]) : null,
6615
+ category: inferBuildFailureCategory(message)
6616
+ };
4753
6617
  }
4754
6618
  return null;
4755
6619
  }
4756
- function auditCriticalHeuristic(input) {
4757
- const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
4758
- if (!/\b(critical|high)\b/i.test(line)) {
4759
- return null;
6620
+ function extractViteImportAnalysisBuildFailure(input) {
6621
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trim());
6622
+ for (const line of lines) {
6623
+ const match = line.match(
6624
+ /^\[plugin:vite:import-analysis\]\s+Failed to resolve import\s+"([^"]+)"\s+from\s+"([^"]+)"/i
6625
+ );
6626
+ if (!match) {
6627
+ continue;
4760
6628
  }
4761
- const pkg2 = inferPackage(line);
4762
- if (!pkg2) {
4763
- return null;
6629
+ return {
6630
+ message: `Failed to resolve import "${match[1]}"`,
6631
+ file: normalizeBuildPath(match[2]),
6632
+ line: null,
6633
+ column: null,
6634
+ category: "module-resolution"
6635
+ };
6636
+ }
6637
+ return null;
6638
+ }
6639
+ function extractEsbuildBuildFailure(input) {
6640
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
6641
+ for (let index = 0; index < lines.length; index += 1) {
6642
+ const match = lines[index]?.match(/^(?:[✘✗]\s*)?\[ERROR\]\s*(.+)$/);
6643
+ if (!match) {
6644
+ continue;
6645
+ }
6646
+ const message = match[1].replace(/^\[vite\]\s*/i, "").trim();
6647
+ let file = null;
6648
+ let line = null;
6649
+ let column = null;
6650
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
6651
+ const locationMatch = lines[cursor]?.trim().match(/^(.+?):(\d+):(\d+):$/);
6652
+ if (!locationMatch) {
6653
+ continue;
6654
+ }
6655
+ file = normalizeBuildPath(locationMatch[1]);
6656
+ line = Number(locationMatch[2]);
6657
+ column = Number(locationMatch[3]);
6658
+ break;
4764
6659
  }
4765
6660
  return {
4766
- package: pkg2,
4767
- severity: inferSeverity(line),
4768
- remediation: inferRemediation(pkg2)
6661
+ message,
6662
+ file,
6663
+ line,
6664
+ column,
6665
+ category: inferBuildFailureCategory(message)
4769
6666
  };
4770
- }).filter((item) => item !== null);
4771
- if (vulnerabilities.length === 0) {
4772
- return null;
4773
6667
  }
4774
- const firstVulnerability = vulnerabilities[0];
4775
- return JSON.stringify(
4776
- {
4777
- status: "ok",
4778
- vulnerabilities,
4779
- summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
4780
- },
4781
- null,
4782
- 2
4783
- );
6668
+ return null;
4784
6669
  }
4785
- function infraRiskHeuristic(input) {
4786
- const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
4787
- const riskEvidence = input.split("\n").map((line) => line.trim()).filter(
4788
- (line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
4789
- ).slice(0, 3);
4790
- if (riskEvidence.length > 0) {
4791
- return JSON.stringify(
4792
- {
4793
- verdict: "fail",
4794
- reason: "Destructive or clearly risky infrastructure change signals are present.",
4795
- evidence: riskEvidence
4796
- },
4797
- null,
4798
- 2
4799
- );
6670
+ function extractCargoBuildFailure(input) {
6671
+ if (!/^error(?:\[E\d+\])?:\s+/m.test(input) || !(/^\s*-->\s+/m.test(input) || /could not compile/i.test(input))) {
6672
+ return null;
4800
6673
  }
4801
- if (zeroDestructiveEvidence.length > 0) {
4802
- return JSON.stringify(
4803
- {
4804
- verdict: "pass",
4805
- reason: "The provided input explicitly indicates zero destructive changes.",
4806
- evidence: zeroDestructiveEvidence
4807
- },
4808
- null,
4809
- 2
4810
- );
6674
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
6675
+ for (let index = 0; index < lines.length; index += 1) {
6676
+ const match = lines[index]?.match(/^error(?:\[(E\d+)\])?:\s+(.+)$/);
6677
+ if (!match) {
6678
+ continue;
6679
+ }
6680
+ const code = match[1];
6681
+ const locationMatch = lines.slice(index + 1, index + 7).join("\n").match(/^\s*-->\s+(.+?):(\d+):(\d+)/m);
6682
+ return {
6683
+ message: code ? `${code}: ${match[2].trim()}` : match[2].trim(),
6684
+ file: locationMatch ? normalizeBuildPath(locationMatch[1]) : null,
6685
+ line: locationMatch ? Number(locationMatch[2]) : null,
6686
+ column: locationMatch ? Number(locationMatch[3]) : null,
6687
+ category: inferBuildFailureCategory(match[2])
6688
+ };
4811
6689
  }
4812
- const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
4813
- if (safeEvidence.length > 0) {
4814
- return JSON.stringify(
4815
- {
4816
- verdict: "pass",
4817
- reason: "The provided input explicitly indicates no risky infrastructure changes.",
4818
- evidence: safeEvidence
4819
- },
4820
- null,
4821
- 2
6690
+ return null;
6691
+ }
6692
+ function extractCompilerStyleBuildFailure(input) {
6693
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
6694
+ for (const rawLine of lines) {
6695
+ let match = rawLine.match(
6696
+ /^(.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm)):([0-9]+):([0-9]+):\s*error:\s+(.+)$/
4822
6697
  );
6698
+ if (match) {
6699
+ return {
6700
+ message: match[4].trim(),
6701
+ file: normalizeBuildPath(match[1]),
6702
+ line: Number(match[2]),
6703
+ column: Number(match[3]),
6704
+ category: inferBuildFailureCategory(match[4])
6705
+ };
6706
+ }
6707
+ match = rawLine.match(/^(.+?\.go):([0-9]+):([0-9]+):\s+(.+)$/);
6708
+ if (match && !/^\s*warning:/i.test(match[4])) {
6709
+ return {
6710
+ message: match[4].trim(),
6711
+ file: normalizeBuildPath(match[1]),
6712
+ line: Number(match[2]),
6713
+ column: Number(match[3]),
6714
+ category: inferBuildFailureCategory(match[4])
6715
+ };
6716
+ }
4823
6717
  }
4824
6718
  return null;
4825
6719
  }
6720
+ function extractTscBuildFailure(input) {
6721
+ const diagnostics = parseTscErrors(input);
6722
+ const first = diagnostics[0];
6723
+ if (!first) {
6724
+ return null;
6725
+ }
6726
+ return {
6727
+ message: `${first.code}: ${first.message}`,
6728
+ file: first.file,
6729
+ line: first.line,
6730
+ column: first.column,
6731
+ category: inferBuildFailureCategory(`${first.code}: ${first.message}`)
6732
+ };
6733
+ }
6734
+ function extractWrapperBuildFailure(input) {
6735
+ if (!/^\s*npm ERR!|\bERR_PNPM_|^\s*error Command failed/m.test(input)) {
6736
+ return null;
6737
+ }
6738
+ const npmCommandMatch = input.match(/^\s*npm ERR!\s+.*?\bbuild:\s+`([^`]+)`/m);
6739
+ const genericCommandMatch = input.match(/^\s*.+?\s+build:\s+`([^`]+)`/m);
6740
+ const command = npmCommandMatch?.[1] ?? genericCommandMatch?.[1] ?? null;
6741
+ return {
6742
+ message: command ? `build script \`${command}\` failed` : "the build script failed",
6743
+ file: null,
6744
+ line: null,
6745
+ column: null,
6746
+ category: "wrapper"
6747
+ };
6748
+ }
6749
+ function buildFailureHeuristic(input) {
6750
+ if (input.trim().length === 0) {
6751
+ return null;
6752
+ }
6753
+ if (detectExplicitBuildSuccess(input)) {
6754
+ return "Build succeeded.";
6755
+ }
6756
+ const match = extractViteImportAnalysisBuildFailure(input) ?? extractWebpackBuildFailure(input) ?? extractEsbuildBuildFailure(input) ?? extractCargoBuildFailure(input) ?? extractCompilerStyleBuildFailure(input) ?? extractTscBuildFailure(input) ?? extractWrapperBuildFailure(input);
6757
+ if (!match) {
6758
+ return null;
6759
+ }
6760
+ return formatBuildFailureOutput(match);
6761
+ }
4826
6762
  function applyHeuristicPolicy(policyName, input, detail) {
4827
6763
  if (!policyName) {
4828
6764
  return null;
@@ -4836,6 +6772,15 @@ function applyHeuristicPolicy(policyName, input, detail) {
4836
6772
  if (policyName === "test-status") {
4837
6773
  return testStatusHeuristic(input, detail);
4838
6774
  }
6775
+ if (policyName === "typecheck-summary") {
6776
+ return typecheckSummaryHeuristic(input);
6777
+ }
6778
+ if (policyName === "lint-failures") {
6779
+ return lintFailuresHeuristic(input);
6780
+ }
6781
+ if (policyName === "build-failure") {
6782
+ return buildFailureHeuristic(input);
6783
+ }
4839
6784
  return null;
4840
6785
  }
4841
6786
 
@@ -4946,6 +6891,138 @@ function escapeRegExp2(value) {
4946
6891
  function unique2(values) {
4947
6892
  return [...new Set(values)];
4948
6893
  }
6894
+ var genericBucketSearchTerms = /* @__PURE__ */ new Set([
6895
+ "runtimeerror",
6896
+ "typeerror",
6897
+ "error",
6898
+ "exception",
6899
+ "failed",
6900
+ "failure",
6901
+ "visible failure",
6902
+ "failing tests",
6903
+ "setup failures",
6904
+ "runtime failure",
6905
+ "assertion failed",
6906
+ "network",
6907
+ "permission",
6908
+ "configuration"
6909
+ ]);
6910
+ function normalizeSearchTerm(value) {
6911
+ return value.replace(/^['"`]+|['"`]+$/g, "").trim();
6912
+ }
6913
+ function isHighSignalSearchTerm(term) {
6914
+ const normalized = normalizeSearchTerm(term);
6915
+ if (normalized.length < 4) {
6916
+ return false;
6917
+ }
6918
+ const lower = normalized.toLowerCase();
6919
+ if (genericBucketSearchTerms.has(lower)) {
6920
+ return false;
6921
+ }
6922
+ if (/^(runtime|type|assertion|network|permission|configuration)\b/i.test(normalized)) {
6923
+ return false;
6924
+ }
6925
+ return true;
6926
+ }
6927
+ function scoreSearchTerm(term) {
6928
+ const normalized = normalizeSearchTerm(term);
6929
+ let score = normalized.length;
6930
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(normalized)) {
6931
+ score += 80;
6932
+ }
6933
+ if (/^TS\d+$/.test(normalized)) {
6934
+ score += 70;
6935
+ }
6936
+ if (/^[45]\d\d\b/.test(normalized) || /\bHTTPError:\s*[45]\d\d\b/i.test(normalized)) {
6937
+ score += 60;
6938
+ }
6939
+ if (normalized.includes("/") || normalized.includes("\\")) {
6940
+ score += 50;
6941
+ }
6942
+ if (/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/.test(normalized)) {
6943
+ score += 40;
6944
+ }
6945
+ if (/['"`]/.test(term)) {
6946
+ score += 30;
6947
+ }
6948
+ if (normalized.includes("::")) {
6949
+ score += 25;
6950
+ }
6951
+ return score;
6952
+ }
6953
+ function collectCandidateSearchTerms(value) {
6954
+ const candidates = [];
6955
+ const normalized = value.trim();
6956
+ if (!normalized) {
6957
+ return candidates;
6958
+ }
6959
+ for (const match of normalized.matchAll(/['"`]([^'"`]{4,})['"`]/g)) {
6960
+ candidates.push(match[1]);
6961
+ }
6962
+ for (const match of normalized.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) {
6963
+ candidates.push(match[0]);
6964
+ }
6965
+ for (const match of normalized.matchAll(/\bTS\d+\b/g)) {
6966
+ candidates.push(match[0]);
6967
+ }
6968
+ for (const match of normalized.matchAll(/\bHTTPError:\s*[45]\d\d\b/gi)) {
6969
+ candidates.push(match[0]);
6970
+ }
6971
+ for (const match of normalized.matchAll(/\/[A-Za-z0-9_./:{}-]{4,}/g)) {
6972
+ candidates.push(match[0]);
6973
+ }
6974
+ for (const match of normalized.matchAll(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g)) {
6975
+ candidates.push(match[0]);
6976
+ }
6977
+ for (const match of normalized.matchAll(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/g)) {
6978
+ candidates.push(match[0]);
6979
+ }
6980
+ const detail = normalized.split(":").slice(1).join(":").trim();
6981
+ if (detail.length >= 8) {
6982
+ candidates.push(detail);
6983
+ }
6984
+ return candidates;
6985
+ }
6986
+ function extractBucketSearchTerms(args) {
6987
+ const sources = [
6988
+ args.bucket.root_cause,
6989
+ ...args.bucket.evidence,
6990
+ ...args.readTargets.filter((target) => target.bucket_index === args.bucket.bucket_index).flatMap((target) => [target.context_hint.search_hint ?? "", target.file])
6991
+ ];
6992
+ const prioritized = unique2(
6993
+ sources.flatMap((value) => collectCandidateSearchTerms(value)).filter(isHighSignalSearchTerm)
6994
+ ).sort((left, right) => {
6995
+ const delta = scoreSearchTerm(right) - scoreSearchTerm(left);
6996
+ if (delta !== 0) {
6997
+ return delta;
6998
+ }
6999
+ return left.localeCompare(right);
7000
+ });
7001
+ if (prioritized.length > 0) {
7002
+ return prioritized.slice(0, 6);
7003
+ }
7004
+ const fallbackTerms = unique2(
7005
+ [...args.bucket.evidence, args.bucket.root_cause].flatMap((value) => value.split(/->|:/).map((part) => normalizeSearchTerm(part))).filter(isHighSignalSearchTerm)
7006
+ );
7007
+ return fallbackTerms.slice(0, 4);
7008
+ }
7009
+ function clusterIndexes(indexes, maxGap = 12) {
7010
+ if (indexes.length === 0) {
7011
+ return [];
7012
+ }
7013
+ const clusters = [];
7014
+ let currentCluster = [indexes[0]];
7015
+ for (const index of indexes.slice(1)) {
7016
+ if (index - currentCluster[currentCluster.length - 1] <= maxGap) {
7017
+ currentCluster.push(index);
7018
+ continue;
7019
+ }
7020
+ clusters.push(currentCluster);
7021
+ currentCluster = [index];
7022
+ }
7023
+ clusters.push(currentCluster);
7024
+ return clusters;
7025
+ }
4949
7026
  function buildLineWindows(args) {
4950
7027
  const selected = /* @__PURE__ */ new Set();
4951
7028
  for (const index of args.indexes) {
@@ -4961,6 +7038,12 @@ function buildLineWindows(args) {
4961
7038
  }
4962
7039
  return [...selected].sort((left, right) => left - right).map((index) => args.lines[index]);
4963
7040
  }
7041
+ function buildPriorityLineGroup(args) {
7042
+ return unique2([
7043
+ ...args.indexes.map((index) => args.lines[index]).filter(Boolean),
7044
+ ...buildLineWindows(args)
7045
+ ]);
7046
+ }
4964
7047
  function collapseSelectedLines(args) {
4965
7048
  if (args.lines.length === 0) {
4966
7049
  return args.fallback();
@@ -5109,15 +7192,16 @@ function buildTestStatusRawSlice(args) {
5109
7192
  ) ? index : -1
5110
7193
  ).filter((index) => index >= 0);
5111
7194
  const bucketGroups = args.contract.main_buckets.map((bucket) => {
5112
- const bucketTerms = unique2(
5113
- [bucket.root_cause, ...bucket.evidence].map((value) => value.split(":").at(-1)?.trim() ?? value.trim()).filter((value) => value.length >= 4)
5114
- );
7195
+ const bucketTerms = extractBucketSearchTerms({
7196
+ bucket,
7197
+ readTargets: args.contract.read_targets
7198
+ });
5115
7199
  const indexes = lines.map(
5116
7200
  (line, index) => bucketTerms.some((term) => new RegExp(escapeRegExp2(term), "i").test(line)) ? index : -1
5117
7201
  ).filter((index) => index >= 0);
5118
7202
  return unique2([
5119
7203
  ...indexes.map((index) => lines[index]).filter(Boolean),
5120
- ...buildLineWindows({
7204
+ ...buildPriorityLineGroup({
5121
7205
  lines,
5122
7206
  indexes,
5123
7207
  radius: 2,
@@ -5125,26 +7209,55 @@ function buildTestStatusRawSlice(args) {
5125
7209
  })
5126
7210
  ]);
5127
7211
  });
5128
- const targetGroups = args.contract.read_targets.map(
5129
- (target) => buildLineWindows({
7212
+ const targetGroups = args.contract.read_targets.flatMap((target) => {
7213
+ const searchHintIndexes = findSearchHintIndexes({
5130
7214
  lines,
5131
- indexes: unique2([
5132
- ...findReadTargetIndexes({
5133
- lines,
5134
- file: target.file,
5135
- line: target.line,
5136
- contextHint: target.context_hint
5137
- }),
5138
- ...findSearchHintIndexes({
5139
- lines,
5140
- searchHint: target.context_hint.search_hint
5141
- })
5142
- ]),
5143
- radius: target.line === null ? 1 : 2,
5144
- maxLines: target.line === null ? 6 : 8
7215
+ searchHint: target.context_hint.search_hint
7216
+ });
7217
+ const fileIndexes = findReadTargetIndexes({
7218
+ lines,
7219
+ file: target.file,
7220
+ line: target.line,
7221
+ contextHint: target.context_hint
7222
+ });
7223
+ const radius = target.line === null ? 1 : 2;
7224
+ const maxLines = target.line === null ? 6 : 8;
7225
+ const groups = [
7226
+ searchHintIndexes.length > 0 ? buildPriorityLineGroup({
7227
+ lines,
7228
+ indexes: searchHintIndexes,
7229
+ radius,
7230
+ maxLines
7231
+ }) : null,
7232
+ fileIndexes.length > 0 ? buildPriorityLineGroup({
7233
+ lines,
7234
+ indexes: fileIndexes,
7235
+ radius,
7236
+ maxLines
7237
+ }) : null
7238
+ ].filter((group) => group !== null && group.length > 0);
7239
+ if (groups.length > 0) {
7240
+ return groups;
7241
+ }
7242
+ return [
7243
+ buildPriorityLineGroup({
7244
+ lines,
7245
+ indexes: unique2([...searchHintIndexes, ...fileIndexes]),
7246
+ radius,
7247
+ maxLines
7248
+ })
7249
+ ];
7250
+ });
7251
+ const failureHeaderIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) ? index : -1).filter((index) => index >= 0);
7252
+ const failureIndexes = (failureHeaderIndexes.length > 0 ? failureHeaderIndexes : lines.map((line, index) => /^E\s/.test(line) ? index : -1).filter((index) => index >= 0)).filter((index) => index >= 0);
7253
+ const failureHeaderGroups = clusterIndexes(failureIndexes).slice(0, 8).map(
7254
+ (cluster) => buildPriorityLineGroup({
7255
+ lines,
7256
+ indexes: cluster,
7257
+ radius: 1,
7258
+ maxLines: 8
5145
7259
  })
5146
- );
5147
- const failureIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) || /^E\s/.test(line) ? index : -1).filter((index) => index >= 0);
7260
+ ).filter((group) => group.length > 0);
5148
7261
  const selected = collapseSelectedLineGroups({
5149
7262
  groups: [
5150
7263
  ...targetGroups,
@@ -5158,12 +7271,14 @@ function buildTestStatusRawSlice(args) {
5158
7271
  })
5159
7272
  ]),
5160
7273
  ...bucketGroups,
5161
- buildLineWindows({
5162
- lines,
5163
- indexes: failureIndexes,
5164
- radius: 1,
5165
- maxLines: 24
5166
- })
7274
+ ...failureHeaderGroups.length > 0 ? failureHeaderGroups : [
7275
+ buildLineWindows({
7276
+ lines,
7277
+ indexes: failureIndexes,
7278
+ radius: 1,
7279
+ maxLines: 24
7280
+ })
7281
+ ]
5167
7282
  ],
5168
7283
  maxInputChars: args.config.maxInputChars,
5169
7284
  fallback: () => truncateInput(args.input, {
@@ -5304,7 +7419,8 @@ function withInsufficientHint(args) {
5304
7419
  return buildInsufficientSignalOutput({
5305
7420
  presetName: args.request.presetName,
5306
7421
  originalLength: args.prepared.meta.originalLength,
5307
- truncatedApplied: args.prepared.meta.truncatedApplied
7422
+ truncatedApplied: args.prepared.meta.truncatedApplied,
7423
+ recognizedRunner: detectTestRunner(args.prepared.redacted)
5308
7424
  });
5309
7425
  }
5310
7426
  async function generateWithRetry(args) {
@@ -5364,6 +7480,38 @@ function renderTestStatusDecisionOutput(args) {
5364
7480
  return args.decision.standardText;
5365
7481
  }
5366
7482
  function buildTestStatusProviderFailureDecision(args) {
7483
+ const concreteReadTarget = args.baseDecision.contract.read_targets.find(
7484
+ (target) => Boolean(target.file)
7485
+ );
7486
+ const hasUnknownBucket = args.baseDecision.contract.main_buckets.some(
7487
+ (bucket) => bucket.root_cause.startsWith("unknown ")
7488
+ );
7489
+ if (concreteReadTarget && !hasUnknownBucket) {
7490
+ return buildTestStatusDiagnoseContract({
7491
+ input: args.input,
7492
+ analysis: args.analysis,
7493
+ resolvedTests: args.baseDecision.contract.resolved_tests,
7494
+ remainingTests: args.baseDecision.contract.remaining_tests,
7495
+ contractOverrides: {
7496
+ ...args.baseDecision.contract,
7497
+ diagnosis_complete: false,
7498
+ raw_needed: false,
7499
+ additional_source_read_likely_low_value: false,
7500
+ read_raw_only_if: null,
7501
+ decision: "read_source",
7502
+ provider_used: true,
7503
+ provider_confidence: null,
7504
+ provider_failed: true,
7505
+ raw_slice_used: args.rawSliceUsed,
7506
+ raw_slice_strategy: args.rawSliceStrategy,
7507
+ next_best_action: {
7508
+ code: "read_source_for_bucket",
7509
+ bucket_index: args.baseDecision.contract.dominant_blocker_bucket_index ?? concreteReadTarget.bucket_index,
7510
+ note: `Provider follow-up failed (${args.reason}). The heuristic anchor is concrete enough to inspect source for the current bucket before reading raw traceback.`
7511
+ }
7512
+ }
7513
+ });
7514
+ }
5367
7515
  const shouldZoomFirst = args.request.detail !== "verbose";
5368
7516
  return buildTestStatusDiagnoseContract({
5369
7517
  input: args.input,
@@ -5390,7 +7538,7 @@ function buildTestStatusProviderFailureDecision(args) {
5390
7538
  }
5391
7539
  });
5392
7540
  }
5393
- async function runSift(request) {
7541
+ async function runSiftCore(request, recorder) {
5394
7542
  const prepared = prepareInput(request.stdin, request.config.input);
5395
7543
  const heuristicInput = prepared.redacted;
5396
7544
  const heuristicInputTruncated = false;
@@ -5476,6 +7624,7 @@ async function runSift(request) {
5476
7624
  finalOutput
5477
7625
  });
5478
7626
  }
7627
+ recorder?.heuristic();
5479
7628
  return finalOutput;
5480
7629
  }
5481
7630
  if (testStatusDecision && testStatusAnalysis) {
@@ -5575,6 +7724,7 @@ async function runSift(request) {
5575
7724
  providerInputChars: providerPrepared2.truncated.length,
5576
7725
  providerOutputChars: result.text.length
5577
7726
  });
7727
+ recorder?.provider(result.usage);
5578
7728
  return finalOutput;
5579
7729
  } catch (error) {
5580
7730
  const reason = error instanceof Error ? error.message : "unknown_error";
@@ -5609,6 +7759,7 @@ async function runSift(request) {
5609
7759
  rawSliceChars: rawSlice.text.length,
5610
7760
  providerInputChars: providerPrepared2.truncated.length
5611
7761
  });
7762
+ recorder?.fallback();
5612
7763
  return finalOutput;
5613
7764
  }
5614
7765
  }
@@ -5665,6 +7816,7 @@ async function runSift(request) {
5665
7816
  })) {
5666
7817
  throw new Error("Model output rejected by quality gate");
5667
7818
  }
7819
+ recorder?.provider(result.usage);
5668
7820
  return withInsufficientHint({
5669
7821
  output: normalizeOutput(result.text, providerPrompt.responseMode),
5670
7822
  request,
@@ -5672,6 +7824,7 @@ async function runSift(request) {
5672
7824
  });
5673
7825
  } catch (error) {
5674
7826
  const reason = error instanceof Error ? error.message : "unknown_error";
7827
+ recorder?.fallback();
5675
7828
  return withInsufficientHint({
5676
7829
  output: buildFallbackOutput({
5677
7830
  format: request.format,
@@ -5685,6 +7838,72 @@ async function runSift(request) {
5685
7838
  });
5686
7839
  }
5687
7840
  }
7841
+ async function runSift(request) {
7842
+ return runSiftCore(request);
7843
+ }
7844
+ async function runSiftWithStats(request) {
7845
+ if (request.dryRun) {
7846
+ return {
7847
+ output: await runSiftCore(request),
7848
+ stats: null
7849
+ };
7850
+ }
7851
+ const startedAt = Date.now();
7852
+ let layer = "fallback";
7853
+ let providerCalled = false;
7854
+ let totalTokens = null;
7855
+ const output = await runSiftCore(request, {
7856
+ heuristic() {
7857
+ layer = "heuristic";
7858
+ providerCalled = false;
7859
+ totalTokens = null;
7860
+ },
7861
+ provider(usage) {
7862
+ layer = "provider";
7863
+ providerCalled = true;
7864
+ totalTokens = usage?.totalTokens ?? null;
7865
+ },
7866
+ fallback() {
7867
+ layer = "fallback";
7868
+ providerCalled = true;
7869
+ totalTokens = null;
7870
+ }
7871
+ });
7872
+ return {
7873
+ output,
7874
+ stats: {
7875
+ layer,
7876
+ providerCalled,
7877
+ totalTokens,
7878
+ durationMs: Date.now() - startedAt,
7879
+ presetName: request.presetName
7880
+ }
7881
+ };
7882
+ }
7883
+
7884
+ // src/core/stats.ts
7885
+ import pc3 from "picocolors";
7886
+ function formatDuration(durationMs) {
7887
+ return durationMs >= 1e3 ? `${(durationMs / 1e3).toFixed(1)}s` : `${durationMs}ms`;
7888
+ }
7889
+ function formatStatsFooter(stats) {
7890
+ const duration = formatDuration(stats.durationMs);
7891
+ if (stats.layer === "heuristic") {
7892
+ return `[sift: heuristic \u2022 LLM skipped \u2022 summary ${duration}]`;
7893
+ }
7894
+ if (stats.layer === "provider") {
7895
+ const tokenSegment = stats.totalTokens !== null ? ` \u2022 ${stats.totalTokens} tokens` : "";
7896
+ return `[sift: provider \u2022 LLM used${tokenSegment} \u2022 summary ${duration}]`;
7897
+ }
7898
+ return `[sift: fallback \u2022 provider failed \u2022 summary ${duration}]`;
7899
+ }
7900
+ function emitStatsFooter(args) {
7901
+ if (args.quiet || !args.stats || !process.stderr.isTTY) {
7902
+ return;
7903
+ }
7904
+ process.stderr.write(`${pc3.dim(formatStatsFooter(args.stats))}
7905
+ `);
7906
+ }
5688
7907
 
5689
7908
  // src/core/testStatusState.ts
5690
7909
  import fs5 from "fs";
@@ -5694,10 +7913,29 @@ var detailSchema = z3.enum(["standard", "focused", "verbose"]);
5694
7913
  var failureBucketTypeSchema = z3.enum([
5695
7914
  "shared_environment_blocker",
5696
7915
  "fixture_guard_failure",
7916
+ "timeout_failure",
7917
+ "permission_denied_failure",
7918
+ "async_event_loop_failure",
7919
+ "fixture_teardown_failure",
7920
+ "db_migration_failure",
7921
+ "configuration_error",
7922
+ "xdist_worker_crash",
7923
+ "type_error_failure",
7924
+ "resource_leak_warning",
7925
+ "django_db_access_denied",
7926
+ "network_failure",
7927
+ "subprocess_crash_segfault",
7928
+ "flaky_test_detected",
7929
+ "serialization_encoding_failure",
7930
+ "file_not_found_failure",
7931
+ "memory_error",
7932
+ "deprecation_warning_as_error",
7933
+ "xfail_strict_unexpected_pass",
5697
7934
  "service_unavailable",
5698
7935
  "db_connection_failure",
5699
7936
  "auth_bypass_absent",
5700
7937
  "contract_snapshot_drift",
7938
+ "snapshot_mismatch",
5701
7939
  "import_dependency_failure",
5702
7940
  "collection_failure",
5703
7941
  "assertion_failure",
@@ -6143,7 +8381,7 @@ async function runEscalate(request) {
6143
8381
  const detail = resolveEscalationDetail(state, request.detail, request.showRaw);
6144
8382
  if (request.verbose) {
6145
8383
  process.stderr.write(
6146
- `${pc3.dim("sift")} escalate detail=${detail} cached_detail=${state.detail} command=${state.commandPreview}
8384
+ `${pc4.dim("sift")} escalate detail=${detail} cached_detail=${state.detail} command=${state.commandPreview}
6147
8385
  `
6148
8386
  );
6149
8387
  }
@@ -6153,7 +8391,7 @@ async function runEscalate(request) {
6153
8391
  process.stderr.write("\n");
6154
8392
  }
6155
8393
  }
6156
- let output = await runSift({
8394
+ const result = await runSiftWithStats({
6157
8395
  question: request.question,
6158
8396
  format: request.format,
6159
8397
  goal: request.goal,
@@ -6170,6 +8408,7 @@ async function runEscalate(request) {
6170
8408
  remainingSubsetAvailable: Boolean(state.pytest?.subsetCapable) && (state.pytest?.failingNodeIds.length ?? 0) > 0
6171
8409
  }
6172
8410
  });
8411
+ let output = result.output;
6173
8412
  if (isInsufficientSignalOutput(output)) {
6174
8413
  output = buildInsufficientSignalOutput({
6175
8414
  presetName: "test-status",
@@ -6180,6 +8419,10 @@ async function runEscalate(request) {
6180
8419
  }
6181
8420
  process.stdout.write(`${output}
6182
8421
  `);
8422
+ emitStatsFooter({
8423
+ stats: result.stats,
8424
+ quiet: Boolean(request.quiet)
8425
+ });
6183
8426
  try {
6184
8427
  writeCachedTestStatusRun({
6185
8428
  ...state,
@@ -6188,7 +8431,7 @@ async function runEscalate(request) {
6188
8431
  } catch (error) {
6189
8432
  if (request.verbose) {
6190
8433
  const reason = error instanceof Error ? error.message : "unknown_error";
6191
- process.stderr.write(`${pc3.dim("sift")} cache_write=failed reason=${reason}
8434
+ process.stderr.write(`${pc4.dim("sift")} cache_write=failed reason=${reason}
6192
8435
  `);
6193
8436
  }
6194
8437
  }
@@ -6198,7 +8441,7 @@ async function runEscalate(request) {
6198
8441
  // src/core/exec.ts
6199
8442
  import { spawn } from "child_process";
6200
8443
  import { constants as osConstants } from "os";
6201
- import pc4 from "picocolors";
8444
+ import pc5 from "picocolors";
6202
8445
 
6203
8446
  // src/core/gate.ts
6204
8447
  var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
@@ -6528,7 +8771,7 @@ async function runExec(request) {
6528
8771
  const previousCachedRun = shouldCacheTestStatusBase ? tryReadCachedTestStatusRun() : null;
6529
8772
  if (request.config.runtime.verbose) {
6530
8773
  process.stderr.write(
6531
- `${pc4.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
8774
+ `${pc5.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
6532
8775
  `
6533
8776
  );
6534
8777
  }
@@ -6557,7 +8800,7 @@ async function runExec(request) {
6557
8800
  }
6558
8801
  bypassed = true;
6559
8802
  if (request.config.runtime.verbose) {
6560
- process.stderr.write(`${pc4.dim("sift")} bypass=interactive-prompt
8803
+ process.stderr.write(`${pc5.dim("sift")} bypass=interactive-prompt
6561
8804
  `);
6562
8805
  }
6563
8806
  process.stderr.write(capture.render());
@@ -6586,15 +8829,16 @@ async function runExec(request) {
6586
8829
  const shouldCacheTestStatus = shouldCacheTestStatusBase && !useWatchFlow;
6587
8830
  if (request.config.runtime.verbose) {
6588
8831
  process.stderr.write(
6589
- `${pc4.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
8832
+ `${pc5.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
6590
8833
  `
6591
8834
  );
6592
8835
  }
6593
8836
  if (autoWatchDetected) {
6594
- process.stderr.write(`${pc4.dim("sift")} auto-watch=detected
8837
+ process.stderr.write(`${pc5.dim("sift")} auto-watch=detected
6595
8838
  `);
6596
8839
  }
6597
8840
  if (!bypassed) {
8841
+ const reductionStartedAt = Date.now();
6598
8842
  if (request.showRaw && capturedOutput.length > 0) {
6599
8843
  process.stderr.write(capturedOutput);
6600
8844
  if (!capturedOutput.endsWith("\n")) {
@@ -6609,12 +8853,22 @@ async function runExec(request) {
6609
8853
  if (execSuccessShortcut && !request.dryRun) {
6610
8854
  if (request.config.runtime.verbose) {
6611
8855
  process.stderr.write(
6612
- `${pc4.dim("sift")} exec_shortcut=${request.presetName}
8856
+ `${pc5.dim("sift")} exec_shortcut=${request.presetName}
6613
8857
  `
6614
8858
  );
6615
8859
  }
6616
8860
  process.stdout.write(`${execSuccessShortcut}
6617
8861
  `);
8862
+ emitStatsFooter({
8863
+ stats: {
8864
+ layer: "heuristic",
8865
+ providerCalled: false,
8866
+ totalTokens: null,
8867
+ durationMs: Date.now() - reductionStartedAt,
8868
+ presetName: request.presetName
8869
+ },
8870
+ quiet: Boolean(request.quiet)
8871
+ });
6618
8872
  return exitCode;
6619
8873
  }
6620
8874
  if (useWatchFlow) {
@@ -6627,7 +8881,8 @@ async function runExec(request) {
6627
8881
  presetName: request.presetName,
6628
8882
  originalLength: capture.getTotalChars(),
6629
8883
  truncatedApplied: capture.wasTruncated(),
6630
- exitCode
8884
+ exitCode,
8885
+ recognizedRunner: detectTestRunner(capturedOutput)
6631
8886
  });
6632
8887
  }
6633
8888
  process.stdout.write(`${output2}
@@ -6655,7 +8910,7 @@ async function runExec(request) {
6655
8910
  previous: previousCachedRun,
6656
8911
  current: currentCachedRun
6657
8912
  }) : null;
6658
- let output = await runSift({
8913
+ const result = await runSiftWithStats({
6659
8914
  ...request,
6660
8915
  stdin: capturedOutput,
6661
8916
  analysisContext: request.skipCacheWrite && request.presetName === "test-status" ? [
@@ -6674,13 +8929,15 @@ async function runExec(request) {
6674
8929
  )
6675
8930
  } : request.testStatusContext
6676
8931
  });
8932
+ let output = result.output;
6677
8933
  if (shouldCacheTestStatus) {
6678
8934
  if (isInsufficientSignalOutput(output)) {
6679
8935
  output = buildInsufficientSignalOutput({
6680
8936
  presetName: request.presetName,
6681
8937
  originalLength: capture.getTotalChars(),
6682
8938
  truncatedApplied: capture.wasTruncated(),
6683
- exitCode
8939
+ exitCode,
8940
+ recognizedRunner: detectTestRunner(capturedOutput)
6684
8941
  });
6685
8942
  }
6686
8943
  if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
@@ -6713,7 +8970,7 @@ ${output}`;
6713
8970
  } catch (error) {
6714
8971
  if (request.config.runtime.verbose) {
6715
8972
  const reason = error instanceof Error ? error.message : "unknown_error";
6716
- process.stderr.write(`${pc4.dim("sift")} cache_write=failed reason=${reason}
8973
+ process.stderr.write(`${pc5.dim("sift")} cache_write=failed reason=${reason}
6717
8974
  `);
6718
8975
  }
6719
8976
  }
@@ -6723,11 +8980,16 @@ ${output}`;
6723
8980
  presetName: request.presetName,
6724
8981
  originalLength: capture.getTotalChars(),
6725
8982
  truncatedApplied: capture.wasTruncated(),
6726
- exitCode
8983
+ exitCode,
8984
+ recognizedRunner: detectTestRunner(capturedOutput)
6727
8985
  });
6728
8986
  }
6729
8987
  process.stdout.write(`${output}
6730
8988
  `);
8989
+ emitStatsFooter({
8990
+ stats: result.stats,
8991
+ quiet: Boolean(request.quiet)
8992
+ });
6731
8993
  if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
6732
8994
  presetName: request.presetName,
6733
8995
  output
@@ -6817,6 +9079,7 @@ var defaultCliDeps = {
6817
9079
  evaluateGate,
6818
9080
  readStdin,
6819
9081
  runSift,
9082
+ runSiftWithStats,
6820
9083
  runWatch,
6821
9084
  looksLikeWatchStream,
6822
9085
  getPreset
@@ -6925,7 +9188,7 @@ function applySharedOptions(command) {
6925
9188
  ).option(
6926
9189
  "--fail-on",
6927
9190
  "Fail with exit code 1 when a supported built-in preset produces a blocking result"
6928
- ).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
9191
+ ).option("--config <path>", "Path to config file").option("--quiet", "Suppress the stats footer on stderr").option("--verbose", "Enable verbose stderr logging");
6929
9192
  }
6930
9193
  function normalizeDetail(value) {
6931
9194
  if (value === void 0 || value === null || value === "") {
@@ -7024,21 +9287,25 @@ function createCliApp(args = {}) {
7024
9287
  stderr.write("\n");
7025
9288
  }
7026
9289
  }
7027
- const output = deps.looksLikeWatchStream(stdin) ? await deps.runWatch({
7028
- question: input.question,
7029
- format: input.format,
7030
- goal: input.goal,
7031
- stdin,
7032
- config,
7033
- dryRun: Boolean(input.options.dryRun),
7034
- showRaw: Boolean(input.options.showRaw),
7035
- includeTestIds: Boolean(input.options.includeTestIds),
7036
- detail: input.detail,
7037
- presetName: input.presetName,
7038
- policyName: input.policyName,
7039
- outputContract: input.outputContract,
7040
- fallbackJson: input.fallbackJson
7041
- }) : await deps.runSift({
9290
+ const isWatchStream = deps.looksLikeWatchStream(stdin);
9291
+ const result = isWatchStream ? {
9292
+ output: await deps.runWatch({
9293
+ question: input.question,
9294
+ format: input.format,
9295
+ goal: input.goal,
9296
+ stdin,
9297
+ config,
9298
+ dryRun: Boolean(input.options.dryRun),
9299
+ showRaw: Boolean(input.options.showRaw),
9300
+ includeTestIds: Boolean(input.options.includeTestIds),
9301
+ detail: input.detail,
9302
+ presetName: input.presetName,
9303
+ policyName: input.policyName,
9304
+ outputContract: input.outputContract,
9305
+ fallbackJson: input.fallbackJson
9306
+ }),
9307
+ stats: null
9308
+ } : await deps.runSiftWithStats({
7042
9309
  question: input.question,
7043
9310
  format: input.format,
7044
9311
  goal: input.goal,
@@ -7053,8 +9320,13 @@ function createCliApp(args = {}) {
7053
9320
  outputContract: input.outputContract,
7054
9321
  fallbackJson: input.fallbackJson
7055
9322
  });
9323
+ const output = result.output;
7056
9324
  stdout.write(`${output}
7057
9325
  `);
9326
+ emitStatsFooter({
9327
+ stats: result.stats,
9328
+ quiet: Boolean(input.options.quiet)
9329
+ });
7058
9330
  if (Boolean(input.options.failOn) && !Boolean(input.options.dryRun) && input.presetName && deps.evaluateGate({
7059
9331
  presetName: input.presetName,
7060
9332
  output
@@ -7084,6 +9356,7 @@ function createCliApp(args = {}) {
7084
9356
  dryRun: Boolean(input.options.dryRun),
7085
9357
  diff: input.diff,
7086
9358
  failOn: Boolean(input.options.failOn),
9359
+ quiet: Boolean(input.options.quiet),
7087
9360
  showRaw: Boolean(input.options.showRaw),
7088
9361
  includeTestIds: Boolean(input.options.includeTestIds),
7089
9362
  watch: Boolean(input.options.watch),
@@ -7263,6 +9536,7 @@ function createCliApp(args = {}) {
7263
9536
  outputContract: preset.outputContract,
7264
9537
  fallbackJson: preset.fallbackJson,
7265
9538
  detail: normalizeEscalateDetail(options.detail),
9539
+ quiet: Boolean(options.quiet),
7266
9540
  showRaw: Boolean(options.showRaw),
7267
9541
  verbose: Boolean(options.verbose)
7268
9542
  });