@bilalimamoglu/sift 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1262,7 +1262,8 @@ function renderInstructionBody() {
1262
1262
  "- After making or planning a fix, refresh the truth with `sift rerun` so the same full suite runs again at `standard` and shows what is resolved or still remaining.",
1263
1263
  "- The normal stop budget is `standard` first, then at most one zoom step before raw.",
1264
1264
  "- Only if more detail is still needed after `sift rerun`, use `sift rerun --remaining --detail focused`, then `sift rerun --remaining --detail verbose`, then `sift rerun --remaining --detail verbose --show-raw`.",
1265
- "- `sift rerun --remaining` currently supports only argv-mode `pytest ...` or `python -m pytest ...` runs; otherwise rerun a narrowed command manually with `sift exec --preset test-status -- <narrowed pytest command>`.",
1265
+ "- `sift rerun --remaining` narrows automatically for `pytest` and reruns the full original command for `vitest` and `jest` while keeping the diagnosis focused on what still fails.",
1266
+ "- For other runners, rerun a narrowed command manually with `sift exec --preset test-status -- <narrowed test command>` if you need a smaller surface.",
1266
1267
  "- Start with `standard` text. Use diagnose JSON only when automation or machine branching truly needs it.",
1267
1268
  "- If `standard` already shows bucket-level root cause, anchor, and fix lines, trust it and report from it directly.",
1268
1269
  "- In that case, do not re-verify the same bucket with raw pytest; at most do one targeted source read before you edit.",
@@ -1876,7 +1877,7 @@ function showPreset(config, name, includeInternal = false) {
1876
1877
  }
1877
1878
 
1878
1879
  // src/core/escalate.ts
1879
- import pc3 from "picocolors";
1880
+ import pc4 from "picocolors";
1880
1881
 
1881
1882
  // src/core/insufficient.ts
1882
1883
  function isInsufficientSignalOutput(output) {
@@ -1897,8 +1898,8 @@ function buildInsufficientSignalOutput(input) {
1897
1898
  } else {
1898
1899
  hint = "Hint: the captured output did not contain a clear answer for this preset.";
1899
1900
  }
1900
- return `${INSUFFICIENT_SIGNAL_TEXT}
1901
- ${hint}`;
1901
+ 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;
1902
+ return [INSUFFICIENT_SIGNAL_TEXT, hint, presetSuggestion].filter((value) => Boolean(value)).join("\n");
1902
1903
  }
1903
1904
 
1904
1905
  // src/core/run.ts
@@ -2124,7 +2125,125 @@ function createProvider(config) {
2124
2125
 
2125
2126
  // src/core/testStatusDecision.ts
2126
2127
  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[]}';
2128
+
2129
+ // src/core/testStatusTargets.ts
2130
+ function unique(values) {
2131
+ return [...new Set(values)];
2132
+ }
2133
+ function normalizeTestId(value) {
2134
+ return value.replace(/\\/g, "/").replace(/\s+/g, " ").trim();
2135
+ }
2136
+ function stripMatcherProse(value) {
2137
+ return value.replace(/\s+-\s+.*$/, "").trim();
2138
+ }
2139
+ function extractJsFile(value) {
2140
+ const match = value.match(/([A-Za-z0-9_./-]+\.(?:test|spec)\.[cm]?[jt]sx?)/i);
2141
+ return match ? normalizeTestId(match[1]) : null;
2142
+ }
2143
+ function normalizeFailingTarget(label, runner) {
2144
+ const normalized = normalizeTestId(label).replace(/^['"]|['"]$/g, "");
2145
+ if (runner === "pytest") {
2146
+ return stripMatcherProse(normalized);
2147
+ }
2148
+ if (runner === "vitest" || runner === "jest") {
2149
+ const compact = normalized.replace(/^FAIL\s+/i, "").replace(/^[❯×]\s*/, "").replace(/\s+\[[^\]]+\]\s*$/, "").trim();
2150
+ const file = extractJsFile(compact);
2151
+ if (!file) {
2152
+ return stripMatcherProse(compact);
2153
+ }
2154
+ const fileIndex = compact.indexOf(file);
2155
+ const suffix = compact.slice(fileIndex + file.length).trim();
2156
+ if (!suffix) {
2157
+ return file;
2158
+ }
2159
+ if (suffix.startsWith(">")) {
2160
+ const testName = stripMatcherProse(suffix.replace(/^>\s*/, ""));
2161
+ return testName.length > 0 ? `${file} > ${testName}` : file;
2162
+ }
2163
+ return file;
2164
+ }
2165
+ return normalized;
2166
+ }
2167
+ function extractFamilyPrefix(value) {
2168
+ const normalized = normalizeTestId(value);
2169
+ const filePart = normalized.split("::")[0]?.split(" > ")[0]?.trim() ?? normalized;
2170
+ const workflowMatch = filePart.match(/^(\.github\/workflows\/)/);
2171
+ if (workflowMatch) {
2172
+ return workflowMatch[1];
2173
+ }
2174
+ const testsMatch = filePart.match(/^((?:test|tests)\/[^/]+\/)/);
2175
+ if (testsMatch) {
2176
+ return testsMatch[1];
2177
+ }
2178
+ const srcMatch = filePart.match(/^(src\/[^/]+\/)/);
2179
+ if (srcMatch) {
2180
+ return srcMatch[1];
2181
+ }
2182
+ const configMatch = filePart.match(
2183
+ /^((?:[^/]+\/)*(?:package\.json|pytest\.ini|pyproject\.toml|tox\.ini|conftest\.py|(?:vitest|jest)\.config\.[^/]+|tsconfig(?:\.[^/]+)?\.json|[^/]*config[^/]*\.(?:json|ya?ml)))$/i
2184
+ );
2185
+ if (configMatch) {
2186
+ return configMatch[1];
2187
+ }
2188
+ const segments = filePart.replace(/^\/+/, "").split("/").filter(Boolean);
2189
+ if (segments.length >= 2) {
2190
+ return `${segments[0]}/${segments[1]}/`;
2191
+ }
2192
+ if (segments.length === 1) {
2193
+ return segments[0];
2194
+ }
2195
+ return "other";
2196
+ }
2197
+ function buildTestTargetSummary(values) {
2198
+ const uniqueValues = unique(values);
2199
+ const counts = /* @__PURE__ */ new Map();
2200
+ for (const value of uniqueValues) {
2201
+ const prefix = extractFamilyPrefix(value);
2202
+ counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
2203
+ }
2204
+ const families = [...counts.entries()].map(([prefix, count]) => ({
2205
+ prefix,
2206
+ count
2207
+ })).sort((left, right) => {
2208
+ if (right.count !== left.count) {
2209
+ return right.count - left.count;
2210
+ }
2211
+ return left.prefix.localeCompare(right.prefix);
2212
+ }).slice(0, 5);
2213
+ return {
2214
+ count: uniqueValues.length,
2215
+ families
2216
+ };
2217
+ }
2218
+ function formatTargetSummary(summary) {
2219
+ if (summary.count === 0) {
2220
+ return "count=0";
2221
+ }
2222
+ const families = summary.families.length > 0 ? summary.families.map((family) => `${family.prefix}${family.count}`).join(", ") : "none";
2223
+ return `count=${summary.count}; families=${families}`;
2224
+ }
2225
+ function joinFamilies(families) {
2226
+ if (families.length === 0) {
2227
+ return "";
2228
+ }
2229
+ if (families.length === 1) {
2230
+ return families[0];
2231
+ }
2232
+ if (families.length === 2) {
2233
+ return `${families[0]} and ${families[1]}`;
2234
+ }
2235
+ return `${families.slice(0, -1).join(", ")}, and ${families.at(-1)}`;
2236
+ }
2237
+ function describeTargetSummary(summary) {
2238
+ if (summary.count === 0 || summary.families.length === 0) {
2239
+ return null;
2240
+ }
2241
+ const families = summary.families.map((family) => `${family.prefix} (${family.count})`);
2242
+ return `across ${joinFamilies(families)}`;
2243
+ }
2244
+
2245
+ // src/core/testStatusDecision.ts
2246
+ 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","remaining_mode":"none|subset_rerun|full_rerun_diff","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
2247
  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
2248
  var nextBestActionSchema = z2.object({
2130
2249
  code: z2.enum([
@@ -2166,6 +2285,16 @@ var testStatusDiagnoseContractSchema = z2.object({
2166
2285
  additional_source_read_likely_low_value: z2.boolean(),
2167
2286
  read_raw_only_if: z2.string().nullable(),
2168
2287
  decision: z2.enum(["stop", "zoom", "read_source", "read_raw"]),
2288
+ remaining_mode: z2.enum(["none", "subset_rerun", "full_rerun_diff"]),
2289
+ primary_suspect_kind: z2.enum([
2290
+ "test",
2291
+ "app_code",
2292
+ "config",
2293
+ "environment",
2294
+ "tooling",
2295
+ "unknown"
2296
+ ]),
2297
+ confidence_reason: z2.string().min(1),
2169
2298
  dominant_blocker_bucket_index: z2.number().int().nullable(),
2170
2299
  provider_used: z2.boolean(),
2171
2300
  provider_confidence: z2.number().min(0).max(1).nullable(),
@@ -2180,6 +2309,15 @@ var testStatusDiagnoseContractSchema = z2.object({
2180
2309
  label: z2.string(),
2181
2310
  count: z2.number().int(),
2182
2311
  root_cause: z2.string(),
2312
+ suspect_kind: z2.enum([
2313
+ "test",
2314
+ "app_code",
2315
+ "config",
2316
+ "environment",
2317
+ "tooling",
2318
+ "unknown"
2319
+ ]),
2320
+ fix_hint: z2.string().min(1),
2183
2321
  evidence: z2.array(z2.string()).max(2),
2184
2322
  bucket_confidence: z2.number(),
2185
2323
  root_cause_confidence: z2.number(),
@@ -2230,6 +2368,42 @@ function parseTestStatusProviderSupplement(input) {
2230
2368
  return testStatusProviderSupplementSchema.parse(JSON.parse(input));
2231
2369
  }
2232
2370
  var extendedBucketSpecs = [
2371
+ {
2372
+ prefix: "service unavailable:",
2373
+ type: "service_unavailable",
2374
+ label: "service unavailable",
2375
+ genericTitle: "Service unavailable failures",
2376
+ defaultCoverage: "error",
2377
+ rootCauseConfidence: 0.9,
2378
+ dominantPriority: 2,
2379
+ dominantBlocker: true,
2380
+ why: "it contains the dependency service or API path that is unavailable in the test environment",
2381
+ fix: "Restore the dependency service or test double before rerunning the full suite."
2382
+ },
2383
+ {
2384
+ prefix: "db refused:",
2385
+ type: "db_connection_failure",
2386
+ label: "database connection",
2387
+ genericTitle: "Database connection failures",
2388
+ defaultCoverage: "error",
2389
+ rootCauseConfidence: 0.9,
2390
+ dominantPriority: 2,
2391
+ dominantBlocker: true,
2392
+ why: "it contains the database host, DSN, or startup path that is refusing connections",
2393
+ fix: "Restore the test database connectivity before rerunning the full suite."
2394
+ },
2395
+ {
2396
+ prefix: "auth bypass absent:",
2397
+ type: "auth_bypass_absent",
2398
+ label: "auth bypass missing",
2399
+ genericTitle: "Auth bypass setup failures",
2400
+ defaultCoverage: "error",
2401
+ rootCauseConfidence: 0.86,
2402
+ dominantPriority: 2,
2403
+ dominantBlocker: true,
2404
+ why: "it contains the auth bypass fixture or setup path that tests expected to be active",
2405
+ fix: "Restore the test auth bypass fixture or mock before rerunning the full suite."
2406
+ },
2233
2407
  {
2234
2408
  prefix: "snapshot mismatch:",
2235
2409
  type: "snapshot_mismatch",
@@ -2414,6 +2588,16 @@ var extendedBucketSpecs = [
2414
2588
  why: "it contains the deprecated API or warning filter that is failing the test run",
2415
2589
  fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
2416
2590
  },
2591
+ {
2592
+ prefix: "assertion failed:",
2593
+ type: "assertion_failure",
2594
+ label: "assertion failure",
2595
+ genericTitle: "Assertion failures",
2596
+ defaultCoverage: "failed",
2597
+ rootCauseConfidence: 0.76,
2598
+ why: "it contains the expected-versus-actual assertion that failed inside the visible test",
2599
+ fix: "Read the assertion diff or expectation and fix the code or expected value before rerunning."
2600
+ },
2417
2601
  {
2418
2602
  prefix: "xfail strict:",
2419
2603
  type: "xfail_strict_unexpected_pass",
@@ -2435,54 +2619,127 @@ function extractReasonDetail(reason, prefix) {
2435
2619
  function formatCount(count, singular, plural = `${singular}s`) {
2436
2620
  return `${count} ${count === 1 ? singular : plural}`;
2437
2621
  }
2438
- function unique(values) {
2622
+ function unique2(values) {
2439
2623
  return [...new Set(values)];
2440
2624
  }
2441
- function normalizeTestId(value) {
2625
+ function normalizeTestId2(value) {
2442
2626
  return value.replace(/\\/g, "/").trim();
2443
2627
  }
2444
- function extractTestFamilyPrefix(value) {
2445
- const normalized = normalizeTestId(value);
2446
- const testsMatch = normalized.match(/^(tests\/[^/]+\/)/);
2447
- if (testsMatch) {
2448
- return testsMatch[1];
2628
+ function normalizePathCandidate(value) {
2629
+ if (!value) {
2630
+ return null;
2449
2631
  }
2450
- const filePart = normalized.split("::")[0]?.trim() ?? "";
2451
- if (!filePart.includes("/")) {
2452
- return "other";
2632
+ let normalized = value.replace(/\\/g, "/").trim();
2633
+ normalized = normalized.replace(/^[("'`<\[]+/, "").replace(/[>"'`\]),:;]+$/, "");
2634
+ normalized = normalized.replace(/^<repo>\//, "").replace(/^\.\//, "");
2635
+ if (normalized.includes("::")) {
2636
+ normalized = normalized.split("::")[0]?.trim() ?? normalized;
2453
2637
  }
2454
- const segments = filePart.replace(/^\/+/, "").split("/").filter(Boolean);
2455
- if (segments.length === 0) {
2456
- return "other";
2638
+ if (normalized.startsWith("/") && !normalized.startsWith("/tmp/") && !normalized.startsWith("/var/tmp/")) {
2639
+ return null;
2640
+ }
2641
+ if (/^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(normalized)) {
2642
+ return normalized;
2643
+ }
2644
+ if (/^(?:src|test|tests)\/.+\.[A-Za-z0-9._-]+$/i.test(normalized)) {
2645
+ return normalized;
2457
2646
  }
2458
- return `${segments[0]}/`;
2647
+ if (/^(?:package\.json|pytest\.ini|pyproject\.toml|tox\.ini|(?:[A-Za-z0-9._/-]+\/)?conftest\.py)$/i.test(
2648
+ normalized
2649
+ )) {
2650
+ return normalized;
2651
+ }
2652
+ if (/^(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+$/i.test(normalized)) {
2653
+ return normalized;
2654
+ }
2655
+ if (/^(?:[A-Za-z0-9._/-]+\/)?tsconfig(?:\.[A-Za-z0-9_-]+)?\.json$/i.test(normalized)) {
2656
+ return normalized;
2657
+ }
2658
+ if (/^[A-Za-z0-9._/-]*config[A-Za-z0-9._/-]*\.(?:json|yml|yaml)$/i.test(normalized)) {
2659
+ return normalized;
2660
+ }
2661
+ return null;
2459
2662
  }
2460
- function buildTestTargetSummary(values) {
2461
- const counts = /* @__PURE__ */ new Map();
2462
- for (const value of values) {
2463
- const prefix = extractTestFamilyPrefix(value);
2464
- counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
2663
+ function addPathCandidatesFromText(target, text) {
2664
+ if (!text) {
2665
+ return;
2465
2666
  }
2466
- const families = [...counts.entries()].map(([prefix, count]) => ({
2467
- prefix,
2468
- count
2469
- })).sort((left, right) => {
2470
- if (right.count !== left.count) {
2471
- return right.count - left.count;
2667
+ const pattern = /(?:^|[\s("'`])((?:\.github\/workflows\/[A-Za-z0-9._/-]+\.(?:yml|yaml)|(?:src|test|tests)\/[A-Za-z0-9._/-]+\.[A-Za-z0-9._-]+|package\.json|pytest\.ini|pyproject\.toml|tox\.ini|(?:[A-Za-z0-9._/-]+\/)?conftest\.py|(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+|(?:[A-Za-z0-9._/-]+\/)?tsconfig(?:\.[A-Za-z0-9_-]+)?\.json|[A-Za-z0-9._/-]*config[A-Za-z0-9._/-]*\.(?:json|yml|yaml)))/g;
2668
+ for (const match of text.matchAll(pattern)) {
2669
+ const normalized = normalizePathCandidate(match[1] ?? null);
2670
+ if (normalized) {
2671
+ target.add(normalized);
2672
+ }
2673
+ }
2674
+ }
2675
+ function extractBucketPathCandidates(args) {
2676
+ const candidates = /* @__PURE__ */ new Set();
2677
+ const push = (value) => {
2678
+ const normalized = normalizePathCandidate(value);
2679
+ if (normalized) {
2680
+ candidates.add(normalized);
2472
2681
  }
2473
- return left.prefix.localeCompare(right.prefix);
2474
- }).slice(0, 5);
2475
- return {
2476
- count: values.length,
2477
- families
2478
2682
  };
2683
+ push(args.readTarget?.file);
2684
+ for (const item of args.bucket.representativeItems) {
2685
+ push(item.file);
2686
+ addPathCandidatesFromText(candidates, item.label);
2687
+ addPathCandidatesFromText(candidates, item.reason);
2688
+ }
2689
+ addPathCandidatesFromText(candidates, args.bucket.reason);
2690
+ addPathCandidatesFromText(candidates, args.bucket.headline);
2691
+ for (const line of args.bucket.summaryLines) {
2692
+ addPathCandidatesFromText(candidates, line);
2693
+ }
2694
+ return [...candidates];
2479
2695
  }
2480
- function formatTargetSummary(summary) {
2481
- if (summary.count === 0) {
2482
- return "count=0";
2696
+ function isConfigPathCandidate(path8) {
2697
+ return /^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(path8) || /^(?:package\.json|pytest\.ini|pyproject\.toml|tox\.ini|(?:[A-Za-z0-9._/-]+\/)?conftest\.py)$/i.test(
2698
+ path8
2699
+ ) || /^(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+$/i.test(path8) || /^(?:[A-Za-z0-9._/-]+\/)?tsconfig(?:\.[A-Za-z0-9_-]+)?\.json$/i.test(path8) || /^[A-Za-z0-9._/-]*config[A-Za-z0-9._/-]*\.(?:json|yml|yaml)$/i.test(path8);
2700
+ }
2701
+ function isAppPathCandidate(path8) {
2702
+ return path8.startsWith("src/");
2703
+ }
2704
+ function isTestPathCandidate(path8) {
2705
+ return path8.startsWith("test/") || path8.startsWith("tests/");
2706
+ }
2707
+ function looksLikeMatcherLiteralComparison(detail) {
2708
+ return /\bexpected\b[\s\S]*\bto (?:be|contain)\b/i.test(detail);
2709
+ }
2710
+ function looksLikeGoldenLiteralDrift(detail) {
2711
+ return /\\n/.test(detail) || /-\s+(?:Tests|Decision|Likely owner|Next|Stop signal)\b/.test(detail) || /\b(?:node-version|workflow_dispatch|run-name|matrix|registry-url)\b/i.test(detail);
2712
+ }
2713
+ function isGoldenOutputDriftBucket(bucket) {
2714
+ if (bucket.type !== "assertion_failure") {
2715
+ return false;
2483
2716
  }
2484
- const families = summary.families.length > 0 ? summary.families.map((family) => `${family.prefix}${family.count}`).join(", ") : "none";
2485
- return `count=${summary.count}; families=${families}`;
2717
+ const detail = extractReasonDetail(bucket.reason, "assertion failed:") ?? bucket.reason;
2718
+ if (!looksLikeMatcherLiteralComparison(detail)) {
2719
+ return false;
2720
+ }
2721
+ if (bucket.reason.startsWith("snapshot mismatch:")) {
2722
+ return false;
2723
+ }
2724
+ if (!looksLikeGoldenLiteralDrift(detail)) {
2725
+ return false;
2726
+ }
2727
+ const candidates = extractBucketPathCandidates({
2728
+ bucket
2729
+ });
2730
+ return candidates.some((candidate) => isConfigPathCandidate(candidate) || isTestPathCandidate(candidate));
2731
+ }
2732
+ function specializeBucket(bucket) {
2733
+ if (!isGoldenOutputDriftBucket(bucket)) {
2734
+ return bucket;
2735
+ }
2736
+ return {
2737
+ ...bucket,
2738
+ type: "golden_output_drift",
2739
+ reason: "golden output drift: expected literal or golden output no longer matches current output",
2740
+ labelOverride: "golden output drift",
2741
+ hint: "Update the expected literal or golden output if the new output is intentional; otherwise fix the generated output and rerun."
2742
+ };
2486
2743
  }
2487
2744
  function classifyGenericBucketType(reason) {
2488
2745
  const extended = findExtendedBucketSpec(reason);
@@ -2507,6 +2764,9 @@ function classifyGenericBucketType(reason) {
2507
2764
  if (reason.startsWith("missing module:")) {
2508
2765
  return "import_dependency_failure";
2509
2766
  }
2767
+ if (reason.startsWith("golden output drift:")) {
2768
+ return "golden_output_drift";
2769
+ }
2510
2770
  if (reason.startsWith("assertion failed:")) {
2511
2771
  return "assertion_failure";
2512
2772
  }
@@ -2659,7 +2919,7 @@ function mergeBucketDetails(existing, incoming) {
2659
2919
  count,
2660
2920
  confidence: Math.max(existing.confidence, incoming.confidence),
2661
2921
  representativeItems,
2662
- entities: unique([...existing.entities, ...incoming.entities]),
2922
+ entities: unique2([...existing.entities, ...incoming.entities]),
2663
2923
  hint: existing.hint ?? incoming.hint,
2664
2924
  overflowCount: Math.max(
2665
2925
  existing.overflowCount,
@@ -2851,6 +3111,9 @@ function labelForBucket(bucket) {
2851
3111
  if (bucket.type === "import_dependency_failure") {
2852
3112
  return "import dependency failure";
2853
3113
  }
3114
+ if (bucket.type === "golden_output_drift") {
3115
+ return "golden output drift";
3116
+ }
2854
3117
  if (bucket.type === "assertion_failure") {
2855
3118
  return "assertion failure";
2856
3119
  }
@@ -2885,6 +3148,9 @@ function rootCauseConfidenceFor(bucket) {
2885
3148
  if (bucket.type === "contract_snapshot_drift") {
2886
3149
  return bucket.entities.length > 0 ? 0.92 : 0.76;
2887
3150
  }
3151
+ if (bucket.type === "golden_output_drift") {
3152
+ return 0.78;
3153
+ }
2888
3154
  if (bucket.source === "provider") {
2889
3155
  return Math.max(0.6, Math.min(bucket.confidence, 0.82));
2890
3156
  }
@@ -2959,6 +3225,9 @@ function buildReadTargetWhy(args) {
2959
3225
  if (args.bucket.type === "import_dependency_failure") {
2960
3226
  return "it is the first visible failing module in this missing dependency bucket";
2961
3227
  }
3228
+ if (args.bucket.type === "golden_output_drift") {
3229
+ return "it is the first visible golden or literal drift anchor for this bucket";
3230
+ }
2962
3231
  if (args.bucket.type === "assertion_failure") {
2963
3232
  return "it is the first visible failing test in this bucket";
2964
3233
  }
@@ -3036,6 +3305,9 @@ function buildReadTargetSearchHint(bucket, anchor) {
3036
3305
  if (assertionText) {
3037
3306
  return assertionText;
3038
3307
  }
3308
+ if (bucket.type === "golden_output_drift") {
3309
+ return bucket.representativeItems.map((item) => item.reason.match(/^assertion failed:\s+(.+)$/)?.[1] ?? item.reason).find(Boolean) ?? anchor.label.split("::")[1]?.trim() ?? null;
3310
+ }
3039
3311
  if (bucket.reason.startsWith("unknown ")) {
3040
3312
  return anchor.reason;
3041
3313
  }
@@ -3090,18 +3362,36 @@ function buildConcreteNextNote(args) {
3090
3362
  }
3091
3363
  const lead = primaryTarget.context_hint.start_line !== null && primaryTarget.context_hint.end_line !== null ? `Read ${primaryTarget.file} lines ${primaryTarget.context_hint.start_line}-${primaryTarget.context_hint.end_line} first; ${primaryTarget.why}.` : primaryTarget.context_hint.search_hint ? `Search for ${primaryTarget.context_hint.search_hint} in ${primaryTarget.file} first; ${primaryTarget.why}.` : `Read ${formatReadTargetLocation(primaryTarget)} first; ${primaryTarget.why}.`;
3092
3364
  if (args.nextBestAction.code === "fix_dominant_blocker") {
3365
+ if (args.remainingMode === "subset_rerun") {
3366
+ return "Fix the remaining bucket first, then refresh the full-suite truth with sift rerun.";
3367
+ }
3368
+ if (args.remainingMode === "full_rerun_diff") {
3369
+ return "Fix the remaining bucket first. The cached full-suite baseline is still preserved; use sift rerun when you want to refresh it.";
3370
+ }
3093
3371
  if (args.nextBestAction.bucket_index === 1 && args.hasSecondaryVisibleBucket) {
3094
3372
  return "Fix bucket 1 first, then rerun the full suite at standard. Secondary buckets are already visible behind it.";
3095
3373
  }
3096
3374
  return `Fix bucket ${args.nextBestAction.bucket_index ?? 1} first, then rerun the full suite at standard.`;
3097
3375
  }
3098
3376
  if (args.nextBestAction.code === "read_source_for_bucket") {
3377
+ if (args.remainingMode === "subset_rerun") {
3378
+ return "Fix the remaining bucket first, then refresh the full-suite truth with sift rerun.";
3379
+ }
3380
+ if (args.remainingMode === "full_rerun_diff") {
3381
+ return "Fix the remaining bucket first. The cached full-suite baseline is still preserved; use sift rerun when you want to refresh it.";
3382
+ }
3099
3383
  return lead;
3100
3384
  }
3101
3385
  if (args.nextBestAction.code === "insufficient_signal") {
3102
- if (args.nextBestAction.note.startsWith("Provider follow-up failed")) {
3386
+ if (args.nextBestAction.note.startsWith("Provider follow-up")) {
3103
3387
  return args.nextBestAction.note;
3104
3388
  }
3389
+ if (args.remainingMode === "subset_rerun") {
3390
+ return "Fix the remaining bucket first, then refresh the full-suite truth with sift rerun.";
3391
+ }
3392
+ if (args.remainingMode === "full_rerun_diff") {
3393
+ return "Fix the remaining bucket first. The cached full-suite baseline is still preserved; use sift rerun when you want to refresh it.";
3394
+ }
3105
3395
  return `${lead} Then take one deeper sift pass before raw traceback.`;
3106
3396
  }
3107
3397
  return args.nextBestAction.note;
@@ -3110,13 +3400,13 @@ function extractMiniDiff(input, bucket) {
3110
3400
  if (bucket.type !== "contract_snapshot_drift") {
3111
3401
  return null;
3112
3402
  }
3113
- const addedPaths = unique(
3403
+ const addedPaths = unique2(
3114
3404
  [...input.matchAll(/[+-]\s+'(\/api\/[^']+)'/g)].map((match) => match[1])
3115
3405
  ).length;
3116
- const removedModels = unique(
3406
+ const removedModels = unique2(
3117
3407
  [...input.matchAll(/[+-]\s+'([A-Za-z0-9._/-]+-[A-Za-z0-9._-]+)'/g)].map((match) => match[1])
3118
3408
  ).length;
3119
- const changedTaskMappings = unique(
3409
+ const changedTaskMappings = unique2(
3120
3410
  [...input.matchAll(/[+-]\s+'([a-z]+(?:_[a-z0-9]+)+)'/g)].map((match) => match[1])
3121
3411
  ).length;
3122
3412
  if (addedPaths === 0 && removedModels === 0 && changedTaskMappings === 0) {
@@ -3203,7 +3493,7 @@ function buildProviderSupplementBuckets(args) {
3203
3493
  });
3204
3494
  }
3205
3495
  function pickUnknownAnchor(args) {
3206
- const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : null;
3496
+ const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : args.analysis.visibleFailedItems[0];
3207
3497
  if (fromStatusItems) {
3208
3498
  return {
3209
3499
  label: fromStatusItems.label,
@@ -3217,7 +3507,7 @@ function pickUnknownAnchor(args) {
3217
3507
  }
3218
3508
  const label = args.kind === "error" ? args.analysis.visibleErrorLabels[0] : args.analysis.visibleFailedLabels[0];
3219
3509
  if (label) {
3220
- const normalizedLabel = normalizeTestId(label);
3510
+ const normalizedLabel = normalizeTestId2(label);
3221
3511
  const fileMatch = normalizedLabel.match(/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)\b/);
3222
3512
  const file = fileMatch?.[1] ?? normalizedLabel.split("::")[0] ?? null;
3223
3513
  return {
@@ -3240,12 +3530,14 @@ function buildUnknownBucket(args) {
3240
3530
  const isError = args.kind === "error";
3241
3531
  const label = isError ? "unknown setup blocker" : "unknown failure family";
3242
3532
  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";
3533
+ 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;
3243
3534
  return {
3244
3535
  type: "unknown_failure",
3245
3536
  headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
3246
3537
  summaryLines: [
3247
- `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`
3248
- ],
3538
+ `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
3539
+ firstConcreteSignal
3540
+ ].filter((value) => Boolean(value)),
3249
3541
  reason,
3250
3542
  count: args.count,
3251
3543
  confidence: 0.45,
@@ -3343,16 +3635,29 @@ function buildDecisionLine(contract) {
3343
3635
  }
3344
3636
  return "- Decision: raw only if exact traceback is required.";
3345
3637
  }
3638
+ function buildRemainingPassLine(contract) {
3639
+ if (contract.remaining_mode === "subset_rerun") {
3640
+ return "- Remaining pass: showing only what is still failing from the cached baseline.";
3641
+ }
3642
+ if (contract.remaining_mode === "full_rerun_diff") {
3643
+ return "- Remaining pass: full rerun analyzed against the cached baseline because narrowed rerun is not available for this runner.";
3644
+ }
3645
+ return null;
3646
+ }
3346
3647
  function buildComparisonLines(contract) {
3347
3648
  const lines = [];
3649
+ const resolvedSummary = buildTestTargetSummary(contract.resolved_tests);
3650
+ const remainingSummary = buildTestTargetSummary(contract.remaining_tests);
3348
3651
  if (contract.resolved_tests.length > 0) {
3652
+ const summaryText = describeTargetSummary(resolvedSummary);
3349
3653
  lines.push(
3350
- `- Resolved in this rerun: ${formatCount(contract.resolved_tests.length, "test")} dropped out of the failing set.`
3654
+ `- Resolved in this rerun: ${formatCount(contract.resolved_tests.length, "test")} dropped out of the failing set${summaryText ? ` ${summaryText}` : ""}.`
3351
3655
  );
3352
3656
  }
3353
- if (contract.resolved_tests.length > 0 && contract.remaining_tests.length > 0) {
3657
+ if (contract.remaining_tests.length > 0 && (contract.resolved_tests.length > 0 || contract.remaining_mode !== "none")) {
3658
+ const summaryText = describeTargetSummary(remainingSummary);
3354
3659
  lines.push(
3355
- `- Remaining failing targets: ${formatCount(contract.remaining_tests.length, "test/module", "tests/modules")}.`
3660
+ `- Remaining failing targets: ${formatCount(contract.remaining_tests.length, "test/module", "tests/modules")}${summaryText ? ` ${summaryText}` : ""}.`
3356
3661
  );
3357
3662
  }
3358
3663
  return lines;
@@ -3372,7 +3677,7 @@ function buildStandardAnchorText(target) {
3372
3677
  }
3373
3678
  return formatReadTargetLocation(target);
3374
3679
  }
3375
- function buildStandardFixText(args) {
3680
+ function resolveBucketFixHint(args) {
3376
3681
  if (args.bucket.hint) {
3377
3682
  return args.bucket.hint;
3378
3683
  }
@@ -3421,13 +3726,96 @@ function buildStandardFixText(args) {
3421
3726
  if (args.bucket.type === "runtime_failure") {
3422
3727
  return `Fix the visible ${args.bucketLabel} and rerun the full suite at standard.`;
3423
3728
  }
3424
- return null;
3729
+ return "Inspect the first visible anchor for this bucket, apply the smallest fix that explains it, then rerun the full suite at standard.";
3730
+ }
3731
+ function deriveBucketSuspectKind(args) {
3732
+ const pathCandidates = extractBucketPathCandidates({
3733
+ bucket: args.bucket,
3734
+ readTarget: args.readTarget
3735
+ });
3736
+ const hasConfigCandidate = pathCandidates.some((candidate) => isConfigPathCandidate(candidate));
3737
+ const hasAppCandidate = pathCandidates.some((candidate) => isAppPathCandidate(candidate));
3738
+ const hasTestCandidate = pathCandidates.some((candidate) => isTestPathCandidate(candidate));
3739
+ 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") {
3740
+ return "environment";
3741
+ }
3742
+ 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") {
3743
+ return "config";
3744
+ }
3745
+ 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") {
3746
+ return "test";
3747
+ }
3748
+ if (args.bucket.type === "golden_output_drift") {
3749
+ if (hasConfigCandidate) {
3750
+ return "config";
3751
+ }
3752
+ if (hasAppCandidate) {
3753
+ return "app_code";
3754
+ }
3755
+ if (hasTestCandidate) {
3756
+ return "test";
3757
+ }
3758
+ return "unknown";
3759
+ }
3760
+ 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") {
3761
+ return "tooling";
3762
+ }
3763
+ if (args.bucket.type === "unknown_failure") {
3764
+ return "unknown";
3765
+ }
3766
+ if (args.bucket.type === "assertion_failure" || args.bucket.type === "runtime_failure" || args.bucket.type === "type_error_failure" || args.bucket.type === "serialization_encoding_failure") {
3767
+ if (hasConfigCandidate) {
3768
+ return "config";
3769
+ }
3770
+ if (hasAppCandidate) {
3771
+ return "app_code";
3772
+ }
3773
+ if (hasTestCandidate) {
3774
+ return "test";
3775
+ }
3776
+ return "unknown";
3777
+ }
3778
+ return "unknown";
3779
+ }
3780
+ function derivePrimarySuspectKind(args) {
3781
+ const primaryBucket = (args.dominantBlockerBucketIndex !== null ? args.mainBuckets.find((bucket) => bucket.bucket_index === args.dominantBlockerBucketIndex) : null) ?? args.mainBuckets[0];
3782
+ return primaryBucket?.suspect_kind ?? "unknown";
3783
+ }
3784
+ function buildConfidenceReason(args) {
3785
+ const primaryBucket = args.mainBuckets.find((bucket) => bucket.dominant) ?? args.mainBuckets[0];
3786
+ if (args.decision === "stop" && primaryBucket && args.primarySuspectKind !== "unknown") {
3787
+ return `Dominant blocker (${primaryBucket.label}) is anchored and actionable.`;
3788
+ }
3789
+ if (args.decision === "zoom") {
3790
+ return "Unknown or low-confidence buckets remain; one deeper sift pass is justified.";
3791
+ }
3792
+ if (args.decision === "read_source") {
3793
+ return "The bucket is identified, but source context is still needed to make the next fix clear.";
3794
+ }
3795
+ return "Heuristic signal is still insufficient; exact traceback lines are needed.";
3796
+ }
3797
+ function formatSuspectKindLabel(kind) {
3798
+ switch (kind) {
3799
+ case "test":
3800
+ return "test code";
3801
+ case "app_code":
3802
+ return "application code";
3803
+ case "config":
3804
+ return "test or project configuration";
3805
+ case "environment":
3806
+ return "environment setup";
3807
+ case "tooling":
3808
+ return "test runner or tooling";
3809
+ default:
3810
+ return "unknown";
3811
+ }
3425
3812
  }
3426
3813
  function buildStandardBucketSupport(args) {
3427
3814
  return {
3428
3815
  headline: args.bucket.summaryLines[0] ? `- ${args.bucket.summaryLines[0]}` : renderBucketHeadline(args.contractBucket),
3816
+ firstConcreteSignalText: args.bucket.source === "unknown" ? args.bucket.summaryLines[1] ?? null : null,
3429
3817
  anchorText: buildStandardAnchorText(args.readTarget),
3430
- fixText: buildStandardFixText({
3818
+ fixText: resolveBucketFixHint({
3431
3819
  bucket: args.bucket,
3432
3820
  bucketLabel: args.contractBucket.label
3433
3821
  })
@@ -3435,6 +3823,10 @@ function buildStandardBucketSupport(args) {
3435
3823
  }
3436
3824
  function renderStandard(args) {
3437
3825
  const lines = [...buildOutcomeLines(args.analysis), ...buildComparisonLines(args.contract)];
3826
+ const remainingPassLine = buildRemainingPassLine(args.contract);
3827
+ if (remainingPassLine) {
3828
+ lines.push(remainingPassLine);
3829
+ }
3438
3830
  if (args.contract.main_buckets.length > 0) {
3439
3831
  for (const bucket of args.contract.main_buckets.slice(0, 3)) {
3440
3832
  const rawBucket = args.buckets[bucket.bucket_index - 1];
@@ -3450,6 +3842,9 @@ function renderStandard(args) {
3450
3842
  )
3451
3843
  });
3452
3844
  lines.push(support.headline);
3845
+ if (support.firstConcreteSignalText) {
3846
+ lines.push(`- ${support.firstConcreteSignalText}`);
3847
+ }
3453
3848
  if (support.anchorText) {
3454
3849
  lines.push(`- Anchor: ${support.anchorText}`);
3455
3850
  }
@@ -3459,12 +3854,19 @@ function renderStandard(args) {
3459
3854
  }
3460
3855
  }
3461
3856
  lines.push(buildDecisionLine(args.contract));
3857
+ if (args.contract.main_buckets.length > 0 && args.contract.primary_suspect_kind !== "unknown") {
3858
+ lines.push(`- Likely owner: ${formatSuspectKindLabel(args.contract.primary_suspect_kind)}`);
3859
+ }
3462
3860
  lines.push(`- Next: ${args.contract.next_best_action.note}`);
3463
3861
  lines.push(buildStopSignal(args.contract));
3464
3862
  return lines.join("\n");
3465
3863
  }
3466
3864
  function renderFocused(args) {
3467
3865
  const lines = [...buildOutcomeLines(args.analysis), ...buildComparisonLines(args.contract)];
3866
+ const remainingPassLine = buildRemainingPassLine(args.contract);
3867
+ if (remainingPassLine) {
3868
+ lines.push(remainingPassLine);
3869
+ }
3468
3870
  for (const bucket of args.contract.main_buckets) {
3469
3871
  const rawBucket = args.buckets[bucket.bucket_index - 1];
3470
3872
  lines.push(
@@ -3484,6 +3886,10 @@ function renderFocused(args) {
3484
3886
  }
3485
3887
  function renderVerbose(args) {
3486
3888
  const lines = [...buildOutcomeLines(args.analysis), ...buildComparisonLines(args.contract)];
3889
+ const remainingPassLine = buildRemainingPassLine(args.contract);
3890
+ if (remainingPassLine) {
3891
+ lines.push(remainingPassLine);
3892
+ }
3487
3893
  for (const bucket of args.contract.main_buckets) {
3488
3894
  const rawBucket = args.buckets[bucket.bucket_index - 1];
3489
3895
  lines.push(
@@ -3533,7 +3939,9 @@ function buildTestStatusDiagnoseContract(args) {
3533
3939
  count: residuals.remainingFailed
3534
3940
  })
3535
3941
  ].filter((bucket) => Boolean(bucket));
3536
- const buckets = prioritizeBuckets([...combinedBuckets, ...unknownBuckets]).slice(0, 3);
3942
+ const buckets = prioritizeBuckets(
3943
+ [...combinedBuckets, ...unknownBuckets].map((bucket) => specializeBucket(bucket))
3944
+ ).slice(0, 3);
3537
3945
  const simpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && buckets.length === 0;
3538
3946
  const dominantBucket = buckets.map((bucket, index) => ({
3539
3947
  bucket,
@@ -3546,29 +3954,49 @@ function buildTestStatusDiagnoseContract(args) {
3546
3954
  })[0] ?? null;
3547
3955
  const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
3548
3956
  const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
3549
- 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;
3550
- 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);
3551
3957
  const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
3552
3958
  const readTargets = buildReadTargets({
3553
3959
  buckets,
3554
3960
  dominantBucketIndex: dominantBlockerBucketIndex
3555
3961
  });
3556
- const mainBuckets = buckets.map((bucket, index) => ({
3557
- bucket_index: index + 1,
3558
- label: labelForBucket(bucket),
3559
- count: bucket.count,
3560
- root_cause: bucket.reason,
3561
- evidence: buildBucketEvidence(bucket),
3562
- bucket_confidence: Number(bucket.confidence.toFixed(2)),
3563
- root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
3564
- dominant: dominantBucket?.index === index,
3565
- secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== index + 1,
3566
- mini_diff: extractMiniDiff(args.input, bucket)
3567
- }));
3568
- const resolvedTests = unique(args.resolvedTests ?? []);
3569
- const remainingTests = unique(
3570
- args.remainingTests ?? unique([...args.analysis.visibleErrorLabels, ...args.analysis.visibleFailedLabels])
3962
+ 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"));
3963
+ const smallConcreteSuite = args.analysis.failed + args.analysis.errors <= 2 && residuals.remainingErrors === 0 && residuals.remainingFailed === 0 && buckets.length === 1 && !hasUnknownBucket && dominantBucket !== null && dominantBucketHasConcreteAnchor;
3964
+ const dominantConfidenceThreshold = smallConcreteSuite ? 0.55 : 0.6;
3965
+ 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;
3966
+ 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);
3967
+ const mainBuckets = buckets.map((bucket, index) => {
3968
+ const bucketIndex = index + 1;
3969
+ const label = labelForBucket(bucket);
3970
+ const readTarget = readTargets.find((target) => target.bucket_index === bucketIndex);
3971
+ return {
3972
+ bucket_index: bucketIndex,
3973
+ label,
3974
+ count: bucket.count,
3975
+ root_cause: bucket.reason,
3976
+ suspect_kind: deriveBucketSuspectKind({
3977
+ bucket,
3978
+ readTarget
3979
+ }),
3980
+ fix_hint: resolveBucketFixHint({
3981
+ bucket,
3982
+ bucketLabel: label
3983
+ }),
3984
+ evidence: buildBucketEvidence(bucket),
3985
+ bucket_confidence: Number(bucket.confidence.toFixed(2)),
3986
+ root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
3987
+ dominant: dominantBucket?.index === index,
3988
+ secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== bucketIndex,
3989
+ mini_diff: extractMiniDiff(args.input, bucket)
3990
+ };
3991
+ });
3992
+ const resolvedTests = unique2(args.resolvedTests ?? []);
3993
+ const remainingTests = unique2(
3994
+ args.remainingTests ?? unique2([...args.analysis.visibleErrorLabels, ...args.analysis.visibleFailedLabels])
3571
3995
  );
3996
+ const primarySuspectKind = derivePrimarySuspectKind({
3997
+ mainBuckets,
3998
+ dominantBlockerBucketIndex
3999
+ });
3572
4000
  let nextBestAction;
3573
4001
  if (args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0) {
3574
4002
  nextBestAction = {
@@ -3613,7 +4041,10 @@ function buildTestStatusDiagnoseContract(args) {
3613
4041
  raw_needed: rawNeeded,
3614
4042
  additional_source_read_likely_low_value: diagnosisComplete && !rawNeeded,
3615
4043
  read_raw_only_if: rawNeeded ? "you still need exact traceback lines after focused or verbose detail" : null,
4044
+ remaining_mode: args.remainingMode ?? "none",
3616
4045
  dominant_blocker_bucket_index: dominantBlockerBucketIndex,
4046
+ primary_suspect_kind: primarySuspectKind,
4047
+ confidence_reason: "Unknown or low-confidence buckets remain; one deeper sift pass is justified.",
3617
4048
  provider_used: false,
3618
4049
  provider_confidence: null,
3619
4050
  provider_failed: false,
@@ -3641,13 +4072,21 @@ function buildTestStatusDiagnoseContract(args) {
3641
4072
  readTargets,
3642
4073
  hasSecondaryVisibleBucket: mainBuckets.some(
3643
4074
  (bucket) => bucket.secondary_visible_despite_blocker
3644
- )
4075
+ ),
4076
+ remainingMode: args.contractOverrides?.remaining_mode ?? baseContract.remaining_mode
3645
4077
  })
3646
4078
  }
3647
4079
  };
4080
+ const resolvedDecision = effectiveDecision ?? deriveDecision(mergedContractWithoutDecision);
4081
+ const resolvedConfidenceReason = buildConfidenceReason({
4082
+ decision: resolvedDecision,
4083
+ mainBuckets,
4084
+ primarySuspectKind: mergedContractWithoutDecision.primary_suspect_kind
4085
+ });
3648
4086
  const contract = testStatusDiagnoseContractSchema.parse({
3649
4087
  ...mergedContractWithoutDecision,
3650
- decision: effectiveDecision ?? deriveDecision(mergedContractWithoutDecision)
4088
+ confidence_reason: resolvedConfidenceReason,
4089
+ decision: resolvedDecision
3651
4090
  });
3652
4091
  return {
3653
4092
  contract,
@@ -3699,6 +4138,7 @@ function buildTestStatusAnalysisContext(args) {
3699
4138
  `- diagnosis_complete=${args.contract.diagnosis_complete}`,
3700
4139
  `- raw_needed=${args.contract.raw_needed}`,
3701
4140
  `- decision=${args.contract.decision}`,
4141
+ `- remaining_mode=${args.contract.remaining_mode}`,
3702
4142
  `- provider_used=${args.contract.provider_used}`,
3703
4143
  `- provider_failed=${args.contract.provider_failed}`,
3704
4144
  `- raw_slice_strategy=${args.contract.raw_slice_strategy}`,
@@ -4052,6 +4492,27 @@ function buildFallbackOutput(args) {
4052
4492
  var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
4053
4493
  var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
4054
4494
  var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
4495
+ var RESOURCE_DESTROY_HEADER_PATTERN = /^#\s+.+\bwill be (destroyed|deleted|replaced)\b/i;
4496
+ var DESTROY_ERROR_PATTERN = /(instance cannot be destroyed|prevent_destroy|downtime|data loss)/i;
4497
+ var ACTION_DESTROY_PATTERN = /^-\s+destroy$/i;
4498
+ var TSC_CODE_LABELS = {
4499
+ TS1002: "syntax error",
4500
+ TS1005: "syntax error",
4501
+ TS2304: "cannot find name",
4502
+ TS2307: "cannot find module",
4503
+ TS2322: "type mismatch",
4504
+ TS2339: "missing property on type",
4505
+ TS2345: "argument type mismatch",
4506
+ TS2554: "wrong argument count",
4507
+ TS2741: "missing required property",
4508
+ TS2769: "no matching overload",
4509
+ TS5083: "config file error",
4510
+ TS6133: "declared but unused",
4511
+ TS7006: "implicit any",
4512
+ TS18003: "no inputs were found",
4513
+ TS18046: "unknown type",
4514
+ TS18048: "possibly undefined"
4515
+ };
4055
4516
  function collectEvidence(input, matcher, limit = 3) {
4056
4517
  return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
4057
4518
  }
@@ -4065,63 +4526,231 @@ function inferPackage(line) {
4065
4526
  function inferRemediation(pkg2) {
4066
4527
  return `Upgrade ${pkg2} to a patched version.`;
4067
4528
  }
4068
- function getCount(input, label) {
4069
- const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
4070
- const lastMatch = matches.at(-1);
4071
- return lastMatch ? Number(lastMatch[1]) : 0;
4072
- }
4073
- function detectTestRunner(input) {
4074
- 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)) {
4075
- return "vitest";
4076
- }
4077
- 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)) {
4078
- return "jest";
4529
+ function parseCompactAuditVulnerability(line) {
4530
+ if (/^Severity:\s*/i.test(line)) {
4531
+ return null;
4079
4532
  }
4080
- 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)) {
4081
- return "pytest";
4533
+ if (!/\b(critical|high)\b/i.test(line)) {
4534
+ return null;
4082
4535
  }
4083
- return "unknown";
4084
- }
4085
- function extractVitestLineCount(input, label, metric) {
4086
- const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
4087
- const lines = [...input.matchAll(matcher)];
4088
- const line = lines.at(-1)?.[1];
4089
- if (!line) {
4536
+ const pkg2 = inferPackage(line);
4537
+ if (!pkg2) {
4090
4538
  return null;
4091
4539
  }
4092
- const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
4093
- return metricMatch ? Number(metricMatch[1]) : null;
4540
+ return {
4541
+ package: pkg2,
4542
+ severity: inferSeverity(line),
4543
+ remediation: inferRemediation(pkg2)
4544
+ };
4094
4545
  }
4095
- function extractJestLineCount(input, label, metric) {
4096
- const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
4097
- const lines = [...input.matchAll(matcher)];
4098
- const line = lines.at(-1)?.[1];
4099
- if (!line) {
4546
+ function inferAuditPackageHeader(line) {
4547
+ const trimmed = line.trim();
4548
+ if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.includes(":") || /^node_modules\//i.test(trimmed)) {
4100
4549
  return null;
4101
4550
  }
4102
- const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
4103
- return metricMatch ? Number(metricMatch[1]) : null;
4551
+ const match = trimmed.match(/^([@a-z0-9._/-]+)(?:\s{2,}|\s+(?:[<>=~^*]|\d))/i);
4552
+ return match?.[1] ?? null;
4104
4553
  }
4105
- function extractTestStatusCounts(input, runner) {
4106
- if (runner === "vitest") {
4107
- return {
4108
- passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
4109
- failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
4110
- errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
4111
- skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
4112
- snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
4113
- };
4114
- }
4115
- if (runner === "jest") {
4116
- return {
4117
- passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
4118
- failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
4119
- errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
4120
- skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
4121
- };
4122
- }
4123
- return {
4124
- passed: getCount(input, "passed"),
4554
+ function collectAuditCriticalVulnerabilities(input) {
4555
+ const lines = input.split("\n");
4556
+ const vulnerabilities = [];
4557
+ const seen = /* @__PURE__ */ new Set();
4558
+ const pushVulnerability = (pkg2, severity) => {
4559
+ const key = `${pkg2}:${severity}`;
4560
+ if (seen.has(key)) {
4561
+ return;
4562
+ }
4563
+ seen.add(key);
4564
+ vulnerabilities.push({
4565
+ package: pkg2,
4566
+ severity,
4567
+ remediation: inferRemediation(pkg2)
4568
+ });
4569
+ };
4570
+ for (let index = 0; index < lines.length; index += 1) {
4571
+ const line = lines[index].trim();
4572
+ if (!line) {
4573
+ continue;
4574
+ }
4575
+ const compact = parseCompactAuditVulnerability(line);
4576
+ if (compact) {
4577
+ pushVulnerability(compact.package, compact.severity);
4578
+ continue;
4579
+ }
4580
+ const pkg2 = inferAuditPackageHeader(line);
4581
+ if (!pkg2) {
4582
+ continue;
4583
+ }
4584
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 5); cursor += 1) {
4585
+ const candidate = lines[cursor].trim();
4586
+ if (!candidate) {
4587
+ continue;
4588
+ }
4589
+ const severityMatch = candidate.match(/^Severity:\s*(critical|high)\b/i);
4590
+ if (severityMatch) {
4591
+ pushVulnerability(pkg2, severityMatch[1].toLowerCase());
4592
+ break;
4593
+ }
4594
+ if (inferAuditPackageHeader(candidate) || parseCompactAuditVulnerability(candidate)) {
4595
+ break;
4596
+ }
4597
+ }
4598
+ }
4599
+ return vulnerabilities;
4600
+ }
4601
+ function getCount(input, label) {
4602
+ const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
4603
+ const lastMatch = matches.at(-1);
4604
+ return lastMatch ? Number(lastMatch[1]) : 0;
4605
+ }
4606
+ function collectInfraRiskEvidence(input) {
4607
+ const lines = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
4608
+ const evidence = [];
4609
+ const seen = /* @__PURE__ */ new Set();
4610
+ const pushMatches = (matcher, options) => {
4611
+ let added = 0;
4612
+ for (const line of lines) {
4613
+ if (!matcher.test(line)) {
4614
+ continue;
4615
+ }
4616
+ if (options?.exclude?.test(line)) {
4617
+ continue;
4618
+ }
4619
+ if (seen.has(line)) {
4620
+ continue;
4621
+ }
4622
+ evidence.push(line);
4623
+ seen.add(line);
4624
+ added += 1;
4625
+ if (options?.limit && added >= options.limit) {
4626
+ return;
4627
+ }
4628
+ if (evidence.length >= (options?.maxEvidence ?? 4)) {
4629
+ return;
4630
+ }
4631
+ }
4632
+ };
4633
+ pushMatches(/Plan:/i, {
4634
+ exclude: ZERO_DESTRUCTIVE_SUMMARY_PATTERN,
4635
+ limit: 1
4636
+ });
4637
+ if (evidence.length < 4) {
4638
+ pushMatches(RESOURCE_DESTROY_HEADER_PATTERN, { limit: 2 });
4639
+ }
4640
+ if (evidence.length < 4) {
4641
+ pushMatches(DESTROY_ERROR_PATTERN, { limit: 1 });
4642
+ }
4643
+ if (evidence.length < 4) {
4644
+ pushMatches(ACTION_DESTROY_PATTERN, { limit: 1 });
4645
+ }
4646
+ if (evidence.length < 4) {
4647
+ pushMatches(RISK_LINE_PATTERN, {
4648
+ exclude: /->\s+null$|\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i,
4649
+ maxEvidence: 4
4650
+ });
4651
+ }
4652
+ return evidence.slice(0, 4);
4653
+ }
4654
+ function collectInfraDestroyTargets(input) {
4655
+ const targets = [];
4656
+ const seen = /* @__PURE__ */ new Set();
4657
+ for (const line of input.split("\n").map((entry) => entry.trim())) {
4658
+ const match = line.match(/^#\s+(.+?)\s+will be (destroyed|deleted|replaced)\b/i);
4659
+ const target = match?.[1]?.trim();
4660
+ if (!target || seen.has(target)) {
4661
+ continue;
4662
+ }
4663
+ seen.add(target);
4664
+ targets.push(target);
4665
+ }
4666
+ return targets;
4667
+ }
4668
+ function inferInfraDestroyCount(input, destroyTargets) {
4669
+ const matches = [
4670
+ ...input.matchAll(/\b(\d+)\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/gi)
4671
+ ];
4672
+ const lastMatch = matches.at(-1);
4673
+ return lastMatch ? Number(lastMatch[1]) : destroyTargets.length;
4674
+ }
4675
+ function collectInfraBlockers(input) {
4676
+ const lines = input.split("\n");
4677
+ const blockers = [];
4678
+ const seen = /* @__PURE__ */ new Set();
4679
+ for (let index = 0; index < lines.length; index += 1) {
4680
+ const trimmed = lines[index]?.trim();
4681
+ const errorMatch = trimmed?.match(/^(?:[│|]\s*)?Error:\s+(.+)$/);
4682
+ if (!errorMatch) {
4683
+ continue;
4684
+ }
4685
+ const message = errorMatch[1].trim();
4686
+ const nearby = lines.slice(index, index + 8).join("\n");
4687
+ const preventDestroyTarget = nearby.match(/Resource\s+([^\s]+)\s+has lifecycle\.prevent_destroy set/i)?.[1] ?? null;
4688
+ const type = preventDestroyTarget ? "prevent_destroy" : "destroy_blocked";
4689
+ const key = `${type}:${preventDestroyTarget ?? ""}:${message}`;
4690
+ if (seen.has(key)) {
4691
+ continue;
4692
+ }
4693
+ seen.add(key);
4694
+ blockers.push({
4695
+ type,
4696
+ target: preventDestroyTarget,
4697
+ message
4698
+ });
4699
+ }
4700
+ return blockers;
4701
+ }
4702
+ function detectTestRunner(input) {
4703
+ 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)) {
4704
+ return "vitest";
4705
+ }
4706
+ 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)) {
4707
+ return "jest";
4708
+ }
4709
+ if (/\bpytest\b/i.test(input) || /^\s*(?:FAILED|ERROR)\s+[A-Za-z0-9_./-]+::[^\n]+$/m.test(input) || /^\s*=+.*\b\d+\s+failed\b.*=+\s*$/m.test(input) || /\bcollected\s+\d+\s+items\b/i.test(input)) {
4710
+ return "pytest";
4711
+ }
4712
+ return "unknown";
4713
+ }
4714
+ function extractVitestLineCount(input, label, metric) {
4715
+ const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
4716
+ const lines = [...input.matchAll(matcher)];
4717
+ const line = lines.at(-1)?.[1];
4718
+ if (!line) {
4719
+ return null;
4720
+ }
4721
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
4722
+ return metricMatch ? Number(metricMatch[1]) : null;
4723
+ }
4724
+ function extractJestLineCount(input, label, metric) {
4725
+ const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
4726
+ const lines = [...input.matchAll(matcher)];
4727
+ const line = lines.at(-1)?.[1];
4728
+ if (!line) {
4729
+ return null;
4730
+ }
4731
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
4732
+ return metricMatch ? Number(metricMatch[1]) : null;
4733
+ }
4734
+ function extractTestStatusCounts(input, runner) {
4735
+ if (runner === "vitest") {
4736
+ return {
4737
+ passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
4738
+ failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
4739
+ errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
4740
+ skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
4741
+ snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
4742
+ };
4743
+ }
4744
+ if (runner === "jest") {
4745
+ return {
4746
+ passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
4747
+ failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
4748
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
4749
+ skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
4750
+ };
4751
+ }
4752
+ return {
4753
+ passed: getCount(input, "passed"),
4125
4754
  failed: getCount(input, "failed"),
4126
4755
  errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
4127
4756
  skipped: getCount(input, "skipped")
@@ -4147,6 +4776,21 @@ function collectUniqueMatches(input, matcher, limit = 6) {
4147
4776
  }
4148
4777
  return values;
4149
4778
  }
4779
+ function compactDisplayFile(file) {
4780
+ const normalized = file.replace(/\\/g, "/").trim();
4781
+ if (!normalized) {
4782
+ return file;
4783
+ }
4784
+ const looksAbsolute = normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized);
4785
+ if (!looksAbsolute && normalized.length <= 60) {
4786
+ return normalized;
4787
+ }
4788
+ const basename = normalized.split("/").at(-1);
4789
+ return basename && basename.length > 0 ? basename : normalized;
4790
+ }
4791
+ function formatDisplayedFiles(files, limit = 3) {
4792
+ return [...new Set([...files].map((file) => file.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right)).slice(0, limit).map((file) => compactDisplayFile(file));
4793
+ }
4150
4794
  function emptyAnchor() {
4151
4795
  return {
4152
4796
  file: null,
@@ -4426,6 +5070,31 @@ function classifyFailureReason(line, options) {
4426
5070
  group: "permission or locked resource failures"
4427
5071
  };
4428
5072
  }
5073
+ const osDiskFullFailure = normalized.match(
5074
+ /(OSError:\s*\[Errno 28\][^$]*|No space left on device)/i
5075
+ );
5076
+ if (osDiskFullFailure) {
5077
+ return {
5078
+ reason: buildClassifiedReason(
5079
+ "configuration",
5080
+ `disk full (${buildExcerptDetail(
5081
+ osDiskFullFailure[1] ?? normalized,
5082
+ "No space left on device"
5083
+ )})`
5084
+ ),
5085
+ group: "test configuration failures"
5086
+ };
5087
+ }
5088
+ const osPermissionFailure = normalized.match(/OSError:\s*\[Errno 13\][^$]*/i);
5089
+ if (osPermissionFailure) {
5090
+ return {
5091
+ reason: buildClassifiedReason(
5092
+ "permission",
5093
+ buildExcerptDetail(osPermissionFailure[0] ?? normalized, "permission denied")
5094
+ ),
5095
+ group: "permission or locked resource failures"
5096
+ };
5097
+ }
4429
5098
  const xdistWorkerCrash = normalized.match(
4430
5099
  /(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
4431
5100
  );
@@ -4451,7 +5120,7 @@ function classifyFailureReason(line, options) {
4451
5120
  };
4452
5121
  }
4453
5122
  const networkFailure = normalized.match(
4454
- /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable)/i
5123
+ /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable|ConnectionResetError[^,;]*|BrokenPipeError[^,;]*|HTTPError:\s*[45]\d\d[^,;]*)/i
4455
5124
  );
4456
5125
  if (networkFailure) {
4457
5126
  return {
@@ -4462,6 +5131,15 @@ function classifyFailureReason(line, options) {
4462
5131
  group: "network dependency failures"
4463
5132
  };
4464
5133
  }
5134
+ const matcherAssertionFailure = normalized.match(
5135
+ /(expect\(received\)\.(?:toBe|toEqual|toStrictEqual|toMatchObject)\(expected\))/i
5136
+ );
5137
+ if (matcherAssertionFailure) {
5138
+ return {
5139
+ reason: `assertion failed: ${matcherAssertionFailure[1]}`.slice(0, 120),
5140
+ group: "assertion failures"
5141
+ };
5142
+ }
4465
5143
  const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
4466
5144
  if (relationMigration) {
4467
5145
  return {
@@ -4500,6 +5178,34 @@ function classifyFailureReason(line, options) {
4500
5178
  group: "memory exhaustion failures"
4501
5179
  };
4502
5180
  }
5181
+ const propertySetterOverrideFailure = normalized.match(
5182
+ /AttributeError:\s*(property ['"][^'"]+['"] of ['"][^'"]+['"] object has no setter|can't set attribute|readonly attribute|read-only attribute)/i
5183
+ );
5184
+ if (propertySetterOverrideFailure) {
5185
+ return {
5186
+ reason: buildClassifiedReason(
5187
+ "configuration",
5188
+ `invalid test setup override (${buildExcerptDetail(
5189
+ `AttributeError: ${propertySetterOverrideFailure[1] ?? normalized}`,
5190
+ "AttributeError: can't set attribute"
5191
+ )})`
5192
+ ),
5193
+ group: "test configuration failures"
5194
+ };
5195
+ }
5196
+ const setupOverrideFailure = normalized.match(/\b(AttributeError|TypeError):\s*(.+)$/i);
5197
+ if (setupOverrideFailure && /(monkeypatch|patch|fixture|settings|conftest)/i.test(normalized)) {
5198
+ return {
5199
+ reason: buildClassifiedReason(
5200
+ "configuration",
5201
+ `invalid test setup override (${buildExcerptDetail(
5202
+ `${setupOverrideFailure[1]}: ${setupOverrideFailure[2] ?? ""}`,
5203
+ `${setupOverrideFailure[1]}`
5204
+ )})`
5205
+ ),
5206
+ group: "test configuration failures"
5207
+ };
5208
+ }
4503
5209
  const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
4504
5210
  if (typeErrorFailure) {
4505
5211
  return {
@@ -5205,6 +5911,9 @@ function classifyBucketTypeFromReason(reason) {
5205
5911
  if (reason.startsWith("missing module:")) {
5206
5912
  return "import_dependency_failure";
5207
5913
  }
5914
+ if (reason.startsWith("golden output drift:")) {
5915
+ return "golden_output_drift";
5916
+ }
5208
5917
  if (reason.startsWith("assertion failed:")) {
5209
5918
  return "assertion_failure";
5210
5919
  }
@@ -5581,13 +6290,17 @@ function analyzeTestStatus(input) {
5581
6290
  const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
5582
6291
  const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
5583
6292
  const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
6293
+ const statusItems = collectInlineFailureItemsWithStatus(input);
5584
6294
  const visibleErrorItems = chooseStrongestStatusFailureItems([
5585
6295
  ...collectionItems.map((item) => ({
5586
6296
  ...item,
5587
6297
  status: "error"
5588
6298
  })),
5589
- ...collectInlineFailureItemsWithStatus(input).filter((item) => item.status === "error")
6299
+ ...statusItems.filter((item) => item.status === "error")
5590
6300
  ]);
6301
+ const visibleFailedItems = chooseStrongestStatusFailureItems(
6302
+ statusItems.filter((item) => item.status === "failed")
6303
+ );
5591
6304
  const labels = collectFailureLabels(input);
5592
6305
  const visibleErrorLabels = labels.filter((item) => item.status === "error").map((item) => item.label);
5593
6306
  const visibleFailedLabels = labels.filter((item) => item.status === "failed").map((item) => item.label);
@@ -5646,6 +6359,7 @@ function analyzeTestStatus(input) {
5646
6359
  visibleErrorLabels,
5647
6360
  visibleFailedLabels,
5648
6361
  visibleErrorItems,
6362
+ visibleFailedItems,
5649
6363
  buckets
5650
6364
  };
5651
6365
  }
@@ -5706,20 +6420,18 @@ function testStatusHeuristic(input, detail = "standard") {
5706
6420
  return null;
5707
6421
  }
5708
6422
  function auditCriticalHeuristic(input) {
5709
- const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
5710
- if (!/\b(critical|high)\b/i.test(line)) {
5711
- return null;
5712
- }
5713
- const pkg2 = inferPackage(line);
5714
- if (!pkg2) {
5715
- return null;
5716
- }
5717
- return {
5718
- package: pkg2,
5719
- severity: inferSeverity(line),
5720
- remediation: inferRemediation(pkg2)
5721
- };
5722
- }).filter((item) => item !== null);
6423
+ if (/\bfound\s+0\s+vulnerabilities\b/i.test(input) || /\b0\s+vulnerabilities\b/i.test(input)) {
6424
+ return JSON.stringify(
6425
+ {
6426
+ status: "ok",
6427
+ vulnerabilities: [],
6428
+ summary: "No high or critical vulnerabilities found in the provided input."
6429
+ },
6430
+ null,
6431
+ 2
6432
+ );
6433
+ }
6434
+ const vulnerabilities = collectAuditCriticalVulnerabilities(input);
5723
6435
  if (vulnerabilities.length === 0) {
5724
6436
  return null;
5725
6437
  }
@@ -5735,16 +6447,19 @@ function auditCriticalHeuristic(input) {
5735
6447
  );
5736
6448
  }
5737
6449
  function infraRiskHeuristic(input) {
6450
+ const destroyTargets = collectInfraDestroyTargets(input);
6451
+ const blockers = collectInfraBlockers(input);
5738
6452
  const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
5739
- const riskEvidence = input.split("\n").map((line) => line.trim()).filter(
5740
- (line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
5741
- ).slice(0, 3);
6453
+ const riskEvidence = collectInfraRiskEvidence(input);
5742
6454
  if (riskEvidence.length > 0) {
5743
6455
  return JSON.stringify(
5744
6456
  {
5745
6457
  verdict: "fail",
5746
6458
  reason: "Destructive or clearly risky infrastructure change signals are present.",
5747
- evidence: riskEvidence
6459
+ evidence: riskEvidence,
6460
+ destroy_count: inferInfraDestroyCount(input, destroyTargets),
6461
+ destroy_targets: destroyTargets,
6462
+ blockers
5748
6463
  },
5749
6464
  null,
5750
6465
  2
@@ -5755,7 +6470,10 @@ function infraRiskHeuristic(input) {
5755
6470
  {
5756
6471
  verdict: "pass",
5757
6472
  reason: "The provided input explicitly indicates zero destructive changes.",
5758
- evidence: zeroDestructiveEvidence
6473
+ evidence: zeroDestructiveEvidence,
6474
+ destroy_count: 0,
6475
+ destroy_targets: [],
6476
+ blockers: []
5759
6477
  },
5760
6478
  null,
5761
6479
  2
@@ -5767,7 +6485,10 @@ function infraRiskHeuristic(input) {
5767
6485
  {
5768
6486
  verdict: "pass",
5769
6487
  reason: "The provided input explicitly indicates no risky infrastructure changes.",
5770
- evidence: safeEvidence
6488
+ evidence: safeEvidence,
6489
+ destroy_count: 0,
6490
+ destroy_targets: [],
6491
+ blockers: []
5771
6492
  },
5772
6493
  null,
5773
6494
  2
@@ -5775,6 +6496,551 @@ function infraRiskHeuristic(input) {
5775
6496
  }
5776
6497
  return null;
5777
6498
  }
6499
+ function parseTscErrors(input) {
6500
+ const diagnostics = [];
6501
+ for (const rawLine of input.split("\n")) {
6502
+ const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").trimEnd();
6503
+ if (!line.trim()) {
6504
+ continue;
6505
+ }
6506
+ let match = line.match(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
6507
+ if (match) {
6508
+ diagnostics.push({
6509
+ file: match[1].replace(/\\/g, "/").trim(),
6510
+ line: Number(match[2]),
6511
+ column: Number(match[3]),
6512
+ code: match[4],
6513
+ message: match[5].trim()
6514
+ });
6515
+ continue;
6516
+ }
6517
+ match = line.match(/^(.+):(\d+):(\d+)\s+-\s+error\s+(TS\d+):\s+(.+)$/);
6518
+ if (match) {
6519
+ diagnostics.push({
6520
+ file: match[1].replace(/\\/g, "/").trim(),
6521
+ line: Number(match[2]),
6522
+ column: Number(match[3]),
6523
+ code: match[4],
6524
+ message: match[5].trim()
6525
+ });
6526
+ continue;
6527
+ }
6528
+ match = line.match(/^\s*error\s+(TS\d+):\s+(.+)$/);
6529
+ if (match) {
6530
+ diagnostics.push({
6531
+ file: null,
6532
+ line: null,
6533
+ column: null,
6534
+ code: match[1],
6535
+ message: match[2].trim()
6536
+ });
6537
+ }
6538
+ }
6539
+ return diagnostics;
6540
+ }
6541
+ function extractTscSummary(input) {
6542
+ const matches = [
6543
+ ...input.matchAll(/\bFound\s+(\d+)\s+errors?\b(?:\s+in\s+(\d+)\s+files?)?\.?/gi)
6544
+ ];
6545
+ const summary = matches.at(-1);
6546
+ if (!summary) {
6547
+ return null;
6548
+ }
6549
+ return {
6550
+ errorCount: Number(summary[1]),
6551
+ fileCount: summary[2] ? Number(summary[2]) : null
6552
+ };
6553
+ }
6554
+ function formatTscGroup(args) {
6555
+ const label = TSC_CODE_LABELS[args.code];
6556
+ const displayFiles = formatDisplayedFiles(args.files);
6557
+ let line = `- ${args.code}`;
6558
+ if (label) {
6559
+ line += ` (${label})`;
6560
+ }
6561
+ line += `: ${formatCount2(args.count, "occurrence")}`;
6562
+ if (displayFiles.length > 0) {
6563
+ line += ` across ${displayFiles.join(", ")}`;
6564
+ }
6565
+ return `${line}.`;
6566
+ }
6567
+ function typecheckSummaryHeuristic(input) {
6568
+ if (input.trim().length === 0) {
6569
+ return null;
6570
+ }
6571
+ const diagnostics = parseTscErrors(input);
6572
+ const summary = extractTscSummary(input);
6573
+ const hasTscSignal = diagnostics.length > 0 || summary !== null || /\berror\s+TS\d+:/m.test(input);
6574
+ if (!hasTscSignal) {
6575
+ return null;
6576
+ }
6577
+ if (summary?.errorCount === 0) {
6578
+ return "No type errors.";
6579
+ }
6580
+ if (diagnostics.length === 0 && summary === null) {
6581
+ return null;
6582
+ }
6583
+ const errorCount = summary?.errorCount ?? diagnostics.length;
6584
+ const allFiles = new Set(
6585
+ diagnostics.map((diagnostic) => diagnostic.file).filter((file) => Boolean(file))
6586
+ );
6587
+ const fileCount = summary?.fileCount ?? (allFiles.size > 0 ? allFiles.size : null);
6588
+ const groups = /* @__PURE__ */ new Map();
6589
+ for (const diagnostic of diagnostics) {
6590
+ const group = groups.get(diagnostic.code) ?? {
6591
+ count: 0,
6592
+ files: /* @__PURE__ */ new Set()
6593
+ };
6594
+ group.count += 1;
6595
+ if (diagnostic.file) {
6596
+ group.files.add(diagnostic.file);
6597
+ }
6598
+ groups.set(diagnostic.code, group);
6599
+ }
6600
+ const bullets = [
6601
+ `- Typecheck failed: ${formatCount2(errorCount, "error")}${fileCount ? ` in ${formatCount2(fileCount, "file")}` : ""}.`
6602
+ ];
6603
+ const sortedGroups = [...groups.entries()].map(([code, group]) => ({
6604
+ code,
6605
+ count: group.count,
6606
+ files: group.files
6607
+ })).sort((left, right) => right.count - left.count || left.code.localeCompare(right.code));
6608
+ for (const group of sortedGroups.slice(0, 3)) {
6609
+ bullets.push(formatTscGroup(group));
6610
+ }
6611
+ if (sortedGroups.length > 3) {
6612
+ const overflowFiles = /* @__PURE__ */ new Set();
6613
+ for (const group of sortedGroups.slice(3)) {
6614
+ for (const file of group.files) {
6615
+ overflowFiles.add(file);
6616
+ }
6617
+ }
6618
+ let overflow = `- ${formatCount2(sortedGroups.length - 3, "more error code")}`;
6619
+ if (overflowFiles.size > 0) {
6620
+ overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
6621
+ }
6622
+ bullets.push(`${overflow}.`);
6623
+ }
6624
+ return bullets.join("\n");
6625
+ }
6626
+ function looksLikeEslintFileHeader(line) {
6627
+ if (line.trim().length === 0 || line.trim() !== line) {
6628
+ return false;
6629
+ }
6630
+ if (/^\s*[✖×x]\s+\d+\s+problems?\b/i.test(line) || /potentially\s+fixable/i.test(line) || /^\d+\s+problems?\b/i.test(line)) {
6631
+ return false;
6632
+ }
6633
+ const normalized = line.replace(/\\/g, "/");
6634
+ const pathLike = normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../") || /^[A-Za-z]:\//.test(normalized) || /^[A-Za-z0-9_.-]+\//.test(normalized);
6635
+ return pathLike && /\.[A-Za-z0-9]+$/.test(normalized);
6636
+ }
6637
+ function normalizeEslintRule(rule, message) {
6638
+ if (rule && rule.trim().length > 0) {
6639
+ return rule.trim();
6640
+ }
6641
+ if (/parsing error/i.test(message)) {
6642
+ return "parsing error";
6643
+ }
6644
+ if (/fatal/i.test(message)) {
6645
+ return "fatal error";
6646
+ }
6647
+ return "unclassified lint error";
6648
+ }
6649
+ function parseEslintStylish(input) {
6650
+ const violations = [];
6651
+ let currentFile = null;
6652
+ for (const rawLine of input.split("\n")) {
6653
+ const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").replace(/\r$/, "");
6654
+ if (looksLikeEslintFileHeader(line.trim())) {
6655
+ currentFile = line.trim().replace(/\\/g, "/");
6656
+ continue;
6657
+ }
6658
+ let match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)\s*$/);
6659
+ if (match) {
6660
+ violations.push({
6661
+ file: currentFile ?? "(unknown file)",
6662
+ line: Number(match[1]),
6663
+ column: Number(match[2]),
6664
+ severity: match[3],
6665
+ message: match[4].trim(),
6666
+ rule: normalizeEslintRule(match[5], match[4])
6667
+ });
6668
+ continue;
6669
+ }
6670
+ match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s*$/);
6671
+ if (match) {
6672
+ violations.push({
6673
+ file: currentFile ?? "(unknown file)",
6674
+ line: Number(match[1]),
6675
+ column: Number(match[2]),
6676
+ severity: match[3],
6677
+ message: match[4].trim(),
6678
+ rule: normalizeEslintRule(null, match[4])
6679
+ });
6680
+ }
6681
+ }
6682
+ return violations;
6683
+ }
6684
+ function extractEslintSummary(input) {
6685
+ const summaryMatches = [
6686
+ ...input.matchAll(
6687
+ /^\s*[✖×x]?\s*(\d+)\s+problems?\s+\((\d+)\s+errors?,\s+(\d+)\s+warnings?\)/gim
6688
+ )
6689
+ ];
6690
+ const summary = summaryMatches.at(-1);
6691
+ if (!summary) {
6692
+ return null;
6693
+ }
6694
+ const fixableMatch = input.match(
6695
+ /(\d+)\s+errors?\s+and\s+(\d+)\s+warnings?\s+(?:are|is)\s+potentially\s+fixable/i
6696
+ );
6697
+ return {
6698
+ problems: Number(summary[1]),
6699
+ errors: Number(summary[2]),
6700
+ warnings: Number(summary[3]),
6701
+ fixableProblems: fixableMatch ? Number(fixableMatch[1]) + Number(fixableMatch[2]) : null
6702
+ };
6703
+ }
6704
+ function formatLintGroup(args) {
6705
+ const totalErrors = args.errors;
6706
+ const totalWarnings = args.warnings;
6707
+ const displayFiles = formatDisplayedFiles(args.files);
6708
+ let detail = "";
6709
+ if (totalErrors > 0 && totalWarnings > 0) {
6710
+ detail = `${formatCount2(totalErrors, "error")}, ${formatCount2(totalWarnings, "warning")}`;
6711
+ } else if (totalErrors > 0) {
6712
+ detail = formatCount2(totalErrors, "error");
6713
+ } else {
6714
+ detail = formatCount2(totalWarnings, "warning");
6715
+ }
6716
+ let line = `- ${args.rule}: ${detail}`;
6717
+ if (displayFiles.length > 0) {
6718
+ line += ` across ${displayFiles.join(", ")}`;
6719
+ }
6720
+ return `${line}.`;
6721
+ }
6722
+ function lintFailuresHeuristic(input) {
6723
+ const trimmed = input.trim();
6724
+ if (trimmed.length === 0 || trimmed.startsWith("[") || trimmed.startsWith("{")) {
6725
+ return null;
6726
+ }
6727
+ const summary = extractEslintSummary(input);
6728
+ const violations = parseEslintStylish(input);
6729
+ if (summary === null && violations.length === 0) {
6730
+ return null;
6731
+ }
6732
+ if (summary?.problems === 0) {
6733
+ return "No lint failures.";
6734
+ }
6735
+ const problems = summary?.problems ?? violations.length;
6736
+ const errors = summary?.errors ?? countPattern(input, /^\s*\d+:\d+\s+error\b/gm);
6737
+ const warnings = summary?.warnings ?? countPattern(input, /^\s*\d+:\d+\s+warning\b/gm);
6738
+ const bullets = [];
6739
+ if (errors > 0) {
6740
+ let headline = `- Lint failed: ${formatCount2(problems, "problem")} (${formatCount2(errors, "error")}, ${formatCount2(warnings, "warning")}).`;
6741
+ if ((summary?.fixableProblems ?? 0) > 0) {
6742
+ headline += ` ${formatCount2(summary.fixableProblems, "problem")} potentially fixable with --fix.`;
6743
+ }
6744
+ bullets.push(headline);
6745
+ } else {
6746
+ bullets.push(`- No lint errors visible: ${formatCount2(warnings, "warning")}.`);
6747
+ }
6748
+ const groups = /* @__PURE__ */ new Map();
6749
+ for (const violation of violations) {
6750
+ const group = groups.get(violation.rule) ?? {
6751
+ errors: 0,
6752
+ warnings: 0,
6753
+ files: /* @__PURE__ */ new Set()
6754
+ };
6755
+ if (violation.severity === "error") {
6756
+ group.errors += 1;
6757
+ } else {
6758
+ group.warnings += 1;
6759
+ }
6760
+ group.files.add(violation.file);
6761
+ groups.set(violation.rule, group);
6762
+ }
6763
+ const sortedGroups = [...groups.entries()].map(([rule, group]) => ({
6764
+ rule,
6765
+ errors: group.errors,
6766
+ warnings: group.warnings,
6767
+ total: group.errors + group.warnings,
6768
+ files: group.files
6769
+ })).sort((left, right) => {
6770
+ const leftHasErrors = left.errors > 0 ? 1 : 0;
6771
+ const rightHasErrors = right.errors > 0 ? 1 : 0;
6772
+ return rightHasErrors - leftHasErrors || right.total - left.total || left.rule.localeCompare(right.rule);
6773
+ });
6774
+ for (const group of sortedGroups.slice(0, 3)) {
6775
+ bullets.push(formatLintGroup(group));
6776
+ }
6777
+ if (sortedGroups.length > 3) {
6778
+ const overflowFiles = /* @__PURE__ */ new Set();
6779
+ for (const group of sortedGroups.slice(3)) {
6780
+ for (const file of group.files) {
6781
+ overflowFiles.add(file);
6782
+ }
6783
+ }
6784
+ let overflow = `- ${formatCount2(sortedGroups.length - 3, "more rule")}`;
6785
+ if (overflowFiles.size > 0) {
6786
+ overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
6787
+ }
6788
+ bullets.push(`${overflow}.`);
6789
+ }
6790
+ return bullets.join("\n");
6791
+ }
6792
+ function stripAnsiText(input) {
6793
+ return input.replace(/\u001b\[[0-9;]*m/g, "");
6794
+ }
6795
+ function normalizeBuildPath(file) {
6796
+ return file.replace(/\\/g, "/").replace(/^\.\//, "").trim();
6797
+ }
6798
+ function trimTrailingSentencePunctuation(input) {
6799
+ return input.replace(/[.:]+$/, "").trim();
6800
+ }
6801
+ function containsKnownBuildFailureSignal(input) {
6802
+ 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);
6803
+ }
6804
+ function detectExplicitBuildSuccess(input) {
6805
+ if (containsKnownBuildFailureSignal(input)) {
6806
+ return false;
6807
+ }
6808
+ return /\bcompiled successfully\b/i.test(input) || /^\s*Build succeeded\.?\s*$/im.test(input) || /\bcompiled with 0 errors?\b/i.test(input);
6809
+ }
6810
+ function inferBuildFailureCategory(message) {
6811
+ if (/module not found|can't resolve|could not resolve|cannot find module|no required module provides package/i.test(
6812
+ message
6813
+ )) {
6814
+ return "module-resolution";
6815
+ }
6816
+ if (/no matching export|does not provide an export named|missing export/i.test(message)) {
6817
+ return "missing-export";
6818
+ }
6819
+ if (/cannot find name|cannot find value|not found in this scope|undefined:|undeclared identifier/i.test(
6820
+ message
6821
+ )) {
6822
+ return "undefined-identifier";
6823
+ }
6824
+ if (/syntax error|unexpected token|expected ['"`;)]|expected .* after expression/i.test(message)) {
6825
+ return "syntax";
6826
+ }
6827
+ if (/\bTS\d+\b/.test(message) || /type .* is not assignable|type error|no matching overload/i.test(message)) {
6828
+ return "type";
6829
+ }
6830
+ return "generic";
6831
+ }
6832
+ function buildFailureSuggestion(category) {
6833
+ switch (category) {
6834
+ case "module-resolution":
6835
+ return "Install the missing package or fix the import path.";
6836
+ case "missing-export":
6837
+ return "Check the export name in the source module.";
6838
+ case "undefined-identifier":
6839
+ return "Define or import the missing identifier.";
6840
+ case "syntax":
6841
+ return "Fix the syntax error at the indicated location.";
6842
+ case "type":
6843
+ return "Fix the type error at the indicated location.";
6844
+ case "wrapper":
6845
+ return "Check the underlying build tool output above.";
6846
+ default:
6847
+ return "Fix the first reported error and rebuild.";
6848
+ }
6849
+ }
6850
+ function formatBuildFailureOutput(match) {
6851
+ const message = trimTrailingSentencePunctuation(match.message);
6852
+ const suggestion = buildFailureSuggestion(match.category);
6853
+ const displayFile = match.file ? compactDisplayFile(match.file) : null;
6854
+ if (displayFile && match.line !== null) {
6855
+ return `Build failed: ${message} in ${displayFile}:${match.line}. Fix: ${suggestion}`;
6856
+ }
6857
+ if (displayFile) {
6858
+ return `Build failed: ${message} in ${displayFile}. Fix: ${suggestion}`;
6859
+ }
6860
+ return `Build failed: ${message}. Fix: ${suggestion}`;
6861
+ }
6862
+ function extractWebpackBuildFailure(input) {
6863
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
6864
+ for (let index = 0; index < lines.length; index += 1) {
6865
+ const match = lines[index]?.match(/^ERROR in (.+?)(?:\s+(\d+):(\d+))?$/);
6866
+ if (!match) {
6867
+ continue;
6868
+ }
6869
+ const candidates = [];
6870
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
6871
+ const candidate = lines[cursor]?.trim();
6872
+ if (!candidate) {
6873
+ continue;
6874
+ }
6875
+ if (/^ERROR in /.test(candidate) || /compiled with \d+ errors?/i.test(candidate)) {
6876
+ break;
6877
+ }
6878
+ if (/^(?:>|\|)|^\d+\s+\|/.test(candidate)) {
6879
+ continue;
6880
+ }
6881
+ candidates.push(candidate);
6882
+ }
6883
+ let message = "Compilation error";
6884
+ if (candidates.length > 0) {
6885
+ const preferred = candidates.find(
6886
+ (candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate) && inferBuildFailureCategory(candidate) !== "generic"
6887
+ ) ?? candidates.find(
6888
+ (candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate)
6889
+ ) ?? candidates[0];
6890
+ message = preferred ?? message;
6891
+ }
6892
+ return {
6893
+ message,
6894
+ file: normalizeBuildPath(match[1]),
6895
+ line: match[2] ? Number(match[2]) : null,
6896
+ column: match[3] ? Number(match[3]) : null,
6897
+ category: inferBuildFailureCategory(message)
6898
+ };
6899
+ }
6900
+ return null;
6901
+ }
6902
+ function extractViteImportAnalysisBuildFailure(input) {
6903
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trim());
6904
+ for (const line of lines) {
6905
+ const match = line.match(
6906
+ /^\[plugin:vite:import-analysis\]\s+Failed to resolve import\s+"([^"]+)"\s+from\s+"([^"]+)"/i
6907
+ );
6908
+ if (!match) {
6909
+ continue;
6910
+ }
6911
+ return {
6912
+ message: `Failed to resolve import "${match[1]}"`,
6913
+ file: normalizeBuildPath(match[2]),
6914
+ line: null,
6915
+ column: null,
6916
+ category: "module-resolution"
6917
+ };
6918
+ }
6919
+ return null;
6920
+ }
6921
+ function extractEsbuildBuildFailure(input) {
6922
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
6923
+ for (let index = 0; index < lines.length; index += 1) {
6924
+ const match = lines[index]?.match(/^(?:[✘✗]\s*)?\[ERROR\]\s*(.+)$/);
6925
+ if (!match) {
6926
+ continue;
6927
+ }
6928
+ const message = match[1].replace(/^\[vite\]\s*/i, "").trim();
6929
+ let file = null;
6930
+ let line = null;
6931
+ let column = null;
6932
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
6933
+ const locationMatch = lines[cursor]?.trim().match(/^(.+?):(\d+):(\d+):$/);
6934
+ if (!locationMatch) {
6935
+ continue;
6936
+ }
6937
+ file = normalizeBuildPath(locationMatch[1]);
6938
+ line = Number(locationMatch[2]);
6939
+ column = Number(locationMatch[3]);
6940
+ break;
6941
+ }
6942
+ return {
6943
+ message,
6944
+ file,
6945
+ line,
6946
+ column,
6947
+ category: inferBuildFailureCategory(message)
6948
+ };
6949
+ }
6950
+ return null;
6951
+ }
6952
+ function extractCargoBuildFailure(input) {
6953
+ if (!/^error(?:\[E\d+\])?:\s+/m.test(input) || !(/^\s*-->\s+/m.test(input) || /could not compile/i.test(input))) {
6954
+ return null;
6955
+ }
6956
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
6957
+ for (let index = 0; index < lines.length; index += 1) {
6958
+ const match = lines[index]?.match(/^error(?:\[(E\d+)\])?:\s+(.+)$/);
6959
+ if (!match) {
6960
+ continue;
6961
+ }
6962
+ const code = match[1];
6963
+ const locationMatch = lines.slice(index + 1, index + 7).join("\n").match(/^\s*-->\s+(.+?):(\d+):(\d+)/m);
6964
+ return {
6965
+ message: code ? `${code}: ${match[2].trim()}` : match[2].trim(),
6966
+ file: locationMatch ? normalizeBuildPath(locationMatch[1]) : null,
6967
+ line: locationMatch ? Number(locationMatch[2]) : null,
6968
+ column: locationMatch ? Number(locationMatch[3]) : null,
6969
+ category: inferBuildFailureCategory(match[2])
6970
+ };
6971
+ }
6972
+ return null;
6973
+ }
6974
+ function extractCompilerStyleBuildFailure(input) {
6975
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
6976
+ for (const rawLine of lines) {
6977
+ let match = rawLine.match(
6978
+ /^(.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm)):([0-9]+):([0-9]+):\s*error:\s+(.+)$/
6979
+ );
6980
+ if (match) {
6981
+ return {
6982
+ message: match[4].trim(),
6983
+ file: normalizeBuildPath(match[1]),
6984
+ line: Number(match[2]),
6985
+ column: Number(match[3]),
6986
+ category: inferBuildFailureCategory(match[4])
6987
+ };
6988
+ }
6989
+ match = rawLine.match(/^(.+?\.go):([0-9]+):([0-9]+):\s+(.+)$/);
6990
+ if (match && !/^\s*warning:/i.test(match[4])) {
6991
+ return {
6992
+ message: match[4].trim(),
6993
+ file: normalizeBuildPath(match[1]),
6994
+ line: Number(match[2]),
6995
+ column: Number(match[3]),
6996
+ category: inferBuildFailureCategory(match[4])
6997
+ };
6998
+ }
6999
+ }
7000
+ return null;
7001
+ }
7002
+ function extractTscBuildFailure(input) {
7003
+ const diagnostics = parseTscErrors(input);
7004
+ const first = diagnostics[0];
7005
+ if (!first) {
7006
+ return null;
7007
+ }
7008
+ return {
7009
+ message: `${first.code}: ${first.message}`,
7010
+ file: first.file,
7011
+ line: first.line,
7012
+ column: first.column,
7013
+ category: inferBuildFailureCategory(`${first.code}: ${first.message}`)
7014
+ };
7015
+ }
7016
+ function extractWrapperBuildFailure(input) {
7017
+ if (!/^\s*npm ERR!|\bERR_PNPM_|^\s*error Command failed/m.test(input)) {
7018
+ return null;
7019
+ }
7020
+ const npmCommandMatch = input.match(/^\s*npm ERR!\s+.*?\bbuild:\s+`([^`]+)`/m);
7021
+ const genericCommandMatch = input.match(/^\s*.+?\s+build:\s+`([^`]+)`/m);
7022
+ const command = npmCommandMatch?.[1] ?? genericCommandMatch?.[1] ?? null;
7023
+ return {
7024
+ message: command ? `build script \`${command}\` failed` : "the build script failed",
7025
+ file: null,
7026
+ line: null,
7027
+ column: null,
7028
+ category: "wrapper"
7029
+ };
7030
+ }
7031
+ function buildFailureHeuristic(input) {
7032
+ if (input.trim().length === 0) {
7033
+ return null;
7034
+ }
7035
+ if (detectExplicitBuildSuccess(input)) {
7036
+ return "Build succeeded.";
7037
+ }
7038
+ const match = extractViteImportAnalysisBuildFailure(input) ?? extractWebpackBuildFailure(input) ?? extractEsbuildBuildFailure(input) ?? extractCargoBuildFailure(input) ?? extractCompilerStyleBuildFailure(input) ?? extractTscBuildFailure(input) ?? extractWrapperBuildFailure(input);
7039
+ if (!match) {
7040
+ return null;
7041
+ }
7042
+ return formatBuildFailureOutput(match);
7043
+ }
5778
7044
  function applyHeuristicPolicy(policyName, input, detail) {
5779
7045
  if (!policyName) {
5780
7046
  return null;
@@ -5788,6 +7054,15 @@ function applyHeuristicPolicy(policyName, input, detail) {
5788
7054
  if (policyName === "test-status") {
5789
7055
  return testStatusHeuristic(input, detail);
5790
7056
  }
7057
+ if (policyName === "typecheck-summary") {
7058
+ return typecheckSummaryHeuristic(input);
7059
+ }
7060
+ if (policyName === "lint-failures") {
7061
+ return lintFailuresHeuristic(input);
7062
+ }
7063
+ if (policyName === "build-failure") {
7064
+ return buildFailureHeuristic(input);
7065
+ }
5791
7066
  return null;
5792
7067
  }
5793
7068
 
@@ -5867,36 +7142,168 @@ function truncateInput(input, options) {
5867
7142
  truncatedApplied: true
5868
7143
  };
5869
7144
  }
5870
-
5871
- // src/core/pipeline.ts
5872
- function prepareInput(raw, config) {
5873
- const sanitized = sanitizeInput(raw, config.stripAnsi);
5874
- const redacted = config.redact || config.redactStrict ? redactInput(sanitized, { strict: config.redactStrict }) : sanitized;
5875
- const truncated = truncateInput(redacted, {
5876
- maxInputChars: config.maxInputChars,
5877
- headChars: config.headChars,
5878
- tailChars: config.tailChars
7145
+
7146
+ // src/core/pipeline.ts
7147
+ function prepareInput(raw, config) {
7148
+ const sanitized = sanitizeInput(raw, config.stripAnsi);
7149
+ const redacted = config.redact || config.redactStrict ? redactInput(sanitized, { strict: config.redactStrict }) : sanitized;
7150
+ const truncated = truncateInput(redacted, {
7151
+ maxInputChars: config.maxInputChars,
7152
+ headChars: config.headChars,
7153
+ tailChars: config.tailChars
7154
+ });
7155
+ return {
7156
+ raw,
7157
+ sanitized,
7158
+ redacted,
7159
+ truncated: truncated.text,
7160
+ meta: {
7161
+ originalLength: raw.length,
7162
+ finalLength: truncated.text.length,
7163
+ redactionApplied: config.redact || config.redactStrict,
7164
+ truncatedApplied: truncated.truncatedApplied
7165
+ }
7166
+ };
7167
+ }
7168
+
7169
+ // src/core/rawSlice.ts
7170
+ function escapeRegExp2(value) {
7171
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7172
+ }
7173
+ function unique3(values) {
7174
+ return [...new Set(values)];
7175
+ }
7176
+ var genericBucketSearchTerms = /* @__PURE__ */ new Set([
7177
+ "runtimeerror",
7178
+ "typeerror",
7179
+ "error",
7180
+ "exception",
7181
+ "failed",
7182
+ "failure",
7183
+ "visible failure",
7184
+ "failing tests",
7185
+ "setup failures",
7186
+ "runtime failure",
7187
+ "assertion failed",
7188
+ "network",
7189
+ "permission",
7190
+ "configuration"
7191
+ ]);
7192
+ function normalizeSearchTerm(value) {
7193
+ return value.replace(/^['"`]+|['"`]+$/g, "").trim();
7194
+ }
7195
+ function isHighSignalSearchTerm(term) {
7196
+ const normalized = normalizeSearchTerm(term);
7197
+ if (normalized.length < 4) {
7198
+ return false;
7199
+ }
7200
+ const lower = normalized.toLowerCase();
7201
+ if (genericBucketSearchTerms.has(lower)) {
7202
+ return false;
7203
+ }
7204
+ if (/^(runtime|type|assertion|network|permission|configuration)\b/i.test(normalized)) {
7205
+ return false;
7206
+ }
7207
+ return true;
7208
+ }
7209
+ function scoreSearchTerm(term) {
7210
+ const normalized = normalizeSearchTerm(term);
7211
+ let score = normalized.length;
7212
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(normalized)) {
7213
+ score += 80;
7214
+ }
7215
+ if (/^TS\d+$/.test(normalized)) {
7216
+ score += 70;
7217
+ }
7218
+ if (/^[45]\d\d\b/.test(normalized) || /\bHTTPError:\s*[45]\d\d\b/i.test(normalized)) {
7219
+ score += 60;
7220
+ }
7221
+ if (normalized.includes("/") || normalized.includes("\\")) {
7222
+ score += 50;
7223
+ }
7224
+ if (/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/.test(normalized)) {
7225
+ score += 40;
7226
+ }
7227
+ if (/['"`]/.test(term)) {
7228
+ score += 30;
7229
+ }
7230
+ if (normalized.includes("::")) {
7231
+ score += 25;
7232
+ }
7233
+ return score;
7234
+ }
7235
+ function collectCandidateSearchTerms(value) {
7236
+ const candidates = [];
7237
+ const normalized = value.trim();
7238
+ if (!normalized) {
7239
+ return candidates;
7240
+ }
7241
+ for (const match of normalized.matchAll(/['"`]([^'"`]{4,})['"`]/g)) {
7242
+ candidates.push(match[1]);
7243
+ }
7244
+ for (const match of normalized.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) {
7245
+ candidates.push(match[0]);
7246
+ }
7247
+ for (const match of normalized.matchAll(/\bTS\d+\b/g)) {
7248
+ candidates.push(match[0]);
7249
+ }
7250
+ for (const match of normalized.matchAll(/\bHTTPError:\s*[45]\d\d\b/gi)) {
7251
+ candidates.push(match[0]);
7252
+ }
7253
+ for (const match of normalized.matchAll(/\/[A-Za-z0-9_./:{}-]{4,}/g)) {
7254
+ candidates.push(match[0]);
7255
+ }
7256
+ for (const match of normalized.matchAll(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g)) {
7257
+ candidates.push(match[0]);
7258
+ }
7259
+ for (const match of normalized.matchAll(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/g)) {
7260
+ candidates.push(match[0]);
7261
+ }
7262
+ const detail = normalized.split(":").slice(1).join(":").trim();
7263
+ if (detail.length >= 8) {
7264
+ candidates.push(detail);
7265
+ }
7266
+ return candidates;
7267
+ }
7268
+ function extractBucketSearchTerms(args) {
7269
+ const sources = [
7270
+ args.bucket.root_cause,
7271
+ ...args.bucket.evidence,
7272
+ ...args.readTargets.filter((target) => target.bucket_index === args.bucket.bucket_index).flatMap((target) => [target.context_hint.search_hint ?? "", target.file])
7273
+ ];
7274
+ const prioritized = unique3(
7275
+ sources.flatMap((value) => collectCandidateSearchTerms(value)).filter(isHighSignalSearchTerm)
7276
+ ).sort((left, right) => {
7277
+ const delta = scoreSearchTerm(right) - scoreSearchTerm(left);
7278
+ if (delta !== 0) {
7279
+ return delta;
7280
+ }
7281
+ return left.localeCompare(right);
5879
7282
  });
5880
- return {
5881
- raw,
5882
- sanitized,
5883
- redacted,
5884
- truncated: truncated.text,
5885
- meta: {
5886
- originalLength: raw.length,
5887
- finalLength: truncated.text.length,
5888
- redactionApplied: config.redact || config.redactStrict,
5889
- truncatedApplied: truncated.truncatedApplied
5890
- }
5891
- };
5892
- }
5893
-
5894
- // src/core/rawSlice.ts
5895
- function escapeRegExp2(value) {
5896
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7283
+ if (prioritized.length > 0) {
7284
+ return prioritized.slice(0, 6);
7285
+ }
7286
+ const fallbackTerms = unique3(
7287
+ [...args.bucket.evidence, args.bucket.root_cause].flatMap((value) => value.split(/->|:/).map((part) => normalizeSearchTerm(part))).filter(isHighSignalSearchTerm)
7288
+ );
7289
+ return fallbackTerms.slice(0, 4);
5897
7290
  }
5898
- function unique2(values) {
5899
- return [...new Set(values)];
7291
+ function clusterIndexes(indexes, maxGap = 12) {
7292
+ if (indexes.length === 0) {
7293
+ return [];
7294
+ }
7295
+ const clusters = [];
7296
+ let currentCluster = [indexes[0]];
7297
+ for (const index of indexes.slice(1)) {
7298
+ if (index - currentCluster[currentCluster.length - 1] <= maxGap) {
7299
+ currentCluster.push(index);
7300
+ continue;
7301
+ }
7302
+ clusters.push(currentCluster);
7303
+ currentCluster = [index];
7304
+ }
7305
+ clusters.push(currentCluster);
7306
+ return clusters;
5900
7307
  }
5901
7308
  function buildLineWindows(args) {
5902
7309
  const selected = /* @__PURE__ */ new Set();
@@ -5913,11 +7320,17 @@ function buildLineWindows(args) {
5913
7320
  }
5914
7321
  return [...selected].sort((left, right) => left - right).map((index) => args.lines[index]);
5915
7322
  }
7323
+ function buildPriorityLineGroup(args) {
7324
+ return unique3([
7325
+ ...args.indexes.map((index) => args.lines[index]).filter(Boolean),
7326
+ ...buildLineWindows(args)
7327
+ ]);
7328
+ }
5916
7329
  function collapseSelectedLines(args) {
5917
7330
  if (args.lines.length === 0) {
5918
7331
  return args.fallback();
5919
7332
  }
5920
- const joined = unique2(args.lines).join("\n").trim();
7333
+ const joined = unique3(args.lines).join("\n").trim();
5921
7334
  if (joined.length === 0) {
5922
7335
  return args.fallback();
5923
7336
  }
@@ -6061,15 +7474,16 @@ function buildTestStatusRawSlice(args) {
6061
7474
  ) ? index : -1
6062
7475
  ).filter((index) => index >= 0);
6063
7476
  const bucketGroups = args.contract.main_buckets.map((bucket) => {
6064
- const bucketTerms = unique2(
6065
- [bucket.root_cause, ...bucket.evidence].map((value) => value.split(":").at(-1)?.trim() ?? value.trim()).filter((value) => value.length >= 4)
6066
- );
7477
+ const bucketTerms = extractBucketSearchTerms({
7478
+ bucket,
7479
+ readTargets: args.contract.read_targets
7480
+ });
6067
7481
  const indexes = lines.map(
6068
7482
  (line, index) => bucketTerms.some((term) => new RegExp(escapeRegExp2(term), "i").test(line)) ? index : -1
6069
7483
  ).filter((index) => index >= 0);
6070
- return unique2([
7484
+ return unique3([
6071
7485
  ...indexes.map((index) => lines[index]).filter(Boolean),
6072
- ...buildLineWindows({
7486
+ ...buildPriorityLineGroup({
6073
7487
  lines,
6074
7488
  indexes,
6075
7489
  radius: 2,
@@ -6077,30 +7491,59 @@ function buildTestStatusRawSlice(args) {
6077
7491
  })
6078
7492
  ]);
6079
7493
  });
6080
- const targetGroups = args.contract.read_targets.map(
6081
- (target) => buildLineWindows({
7494
+ const targetGroups = args.contract.read_targets.flatMap((target) => {
7495
+ const searchHintIndexes = findSearchHintIndexes({
6082
7496
  lines,
6083
- indexes: unique2([
6084
- ...findReadTargetIndexes({
6085
- lines,
6086
- file: target.file,
6087
- line: target.line,
6088
- contextHint: target.context_hint
6089
- }),
6090
- ...findSearchHintIndexes({
6091
- lines,
6092
- searchHint: target.context_hint.search_hint
6093
- })
6094
- ]),
6095
- radius: target.line === null ? 1 : 2,
6096
- maxLines: target.line === null ? 6 : 8
7497
+ searchHint: target.context_hint.search_hint
7498
+ });
7499
+ const fileIndexes = findReadTargetIndexes({
7500
+ lines,
7501
+ file: target.file,
7502
+ line: target.line,
7503
+ contextHint: target.context_hint
7504
+ });
7505
+ const radius = target.line === null ? 1 : 2;
7506
+ const maxLines = target.line === null ? 6 : 8;
7507
+ const groups = [
7508
+ searchHintIndexes.length > 0 ? buildPriorityLineGroup({
7509
+ lines,
7510
+ indexes: searchHintIndexes,
7511
+ radius,
7512
+ maxLines
7513
+ }) : null,
7514
+ fileIndexes.length > 0 ? buildPriorityLineGroup({
7515
+ lines,
7516
+ indexes: fileIndexes,
7517
+ radius,
7518
+ maxLines
7519
+ }) : null
7520
+ ].filter((group) => group !== null && group.length > 0);
7521
+ if (groups.length > 0) {
7522
+ return groups;
7523
+ }
7524
+ return [
7525
+ buildPriorityLineGroup({
7526
+ lines,
7527
+ indexes: unique3([...searchHintIndexes, ...fileIndexes]),
7528
+ radius,
7529
+ maxLines
7530
+ })
7531
+ ];
7532
+ });
7533
+ const failureHeaderIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) ? index : -1).filter((index) => index >= 0);
7534
+ const failureIndexes = (failureHeaderIndexes.length > 0 ? failureHeaderIndexes : lines.map((line, index) => /^E\s/.test(line) ? index : -1).filter((index) => index >= 0)).filter((index) => index >= 0);
7535
+ const failureHeaderGroups = clusterIndexes(failureIndexes).slice(0, 8).map(
7536
+ (cluster) => buildPriorityLineGroup({
7537
+ lines,
7538
+ indexes: cluster,
7539
+ radius: 1,
7540
+ maxLines: 8
6097
7541
  })
6098
- );
6099
- const failureIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) || /^E\s/.test(line) ? index : -1).filter((index) => index >= 0);
7542
+ ).filter((group) => group.length > 0);
6100
7543
  const selected = collapseSelectedLineGroups({
6101
7544
  groups: [
6102
7545
  ...targetGroups,
6103
- unique2([
7546
+ unique3([
6104
7547
  ...summaryIndexes.map((index) => lines[index]).filter(Boolean),
6105
7548
  ...buildLineWindows({
6106
7549
  lines,
@@ -6110,12 +7553,14 @@ function buildTestStatusRawSlice(args) {
6110
7553
  })
6111
7554
  ]),
6112
7555
  ...bucketGroups,
6113
- buildLineWindows({
6114
- lines,
6115
- indexes: failureIndexes,
6116
- radius: 1,
6117
- maxLines: 24
6118
- })
7556
+ ...failureHeaderGroups.length > 0 ? failureHeaderGroups : [
7557
+ buildLineWindows({
7558
+ lines,
7559
+ indexes: failureIndexes,
7560
+ radius: 1,
7561
+ maxLines: 24
7562
+ })
7563
+ ]
6119
7564
  ],
6120
7565
  maxInputChars: args.config.maxInputChars,
6121
7566
  fallback: () => truncateInput(args.input, {
@@ -6256,7 +7701,8 @@ function withInsufficientHint(args) {
6256
7701
  return buildInsufficientSignalOutput({
6257
7702
  presetName: args.request.presetName,
6258
7703
  originalLength: args.prepared.meta.originalLength,
6259
- truncatedApplied: args.prepared.meta.truncatedApplied
7704
+ truncatedApplied: args.prepared.meta.truncatedApplied,
7705
+ recognizedRunner: detectTestRunner(args.prepared.redacted)
6260
7706
  });
6261
7707
  }
6262
7708
  async function generateWithRetry(args) {
@@ -6295,6 +7741,34 @@ function hasRecognizableTestStatusSignal(input) {
6295
7741
  const analysis = analyzeTestStatus(input);
6296
7742
  return analysis.collectionErrorCount !== void 0 || analysis.noTestsCollected || analysis.interrupted || analysis.failed > 0 || analysis.errors > 0 || analysis.passed > 0 || analysis.inlineItems.length > 0 || analysis.buckets.length > 0;
6297
7743
  }
7744
+ function shouldUseCompactTestStatusBypass(args) {
7745
+ if (args.request.policyName !== "test-status") {
7746
+ return false;
7747
+ }
7748
+ if (args.request.detail && args.request.detail !== "standard") {
7749
+ return false;
7750
+ }
7751
+ if (args.request.goal === "diagnose" && args.request.format === "json") {
7752
+ return false;
7753
+ }
7754
+ if (args.request.testStatusContext?.resolvedTests?.length || args.request.testStatusContext?.remainingTests?.length || args.request.testStatusContext?.remainingSubsetAvailable || args.request.testStatusContext?.remainingMode && args.request.testStatusContext.remainingMode !== "none") {
7755
+ return false;
7756
+ }
7757
+ return args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && args.analysis.inlineItems.length === 0 && args.analysis.buckets.length === 0 || args.analysis.noTestsCollected || args.analysis.interrupted && args.analysis.failed === 0 && args.analysis.errors === 0;
7758
+ }
7759
+ function sanitizeProviderFailureReason(reason) {
7760
+ const normalized = reason.trim();
7761
+ const httpStatus = normalized.match(/\bHTTP\s+(\d{3})\b/i)?.[1];
7762
+ if (httpStatus) {
7763
+ return `provider follow-up unavailable (HTTP ${httpStatus})`;
7764
+ }
7765
+ if (/unterminated string|invalid json|unexpected token|json at position|schema|zod|parse/i.test(
7766
+ normalized
7767
+ )) {
7768
+ return "provider follow-up returned unusable structured output";
7769
+ }
7770
+ return "provider follow-up failed";
7771
+ }
6298
7772
  function renderTestStatusDecisionOutput(args) {
6299
7773
  if (args.request.goal === "diagnose" && args.request.format === "json") {
6300
7774
  return JSON.stringify(
@@ -6316,12 +7790,49 @@ function renderTestStatusDecisionOutput(args) {
6316
7790
  return args.decision.standardText;
6317
7791
  }
6318
7792
  function buildTestStatusProviderFailureDecision(args) {
7793
+ const sanitizedReason = sanitizeProviderFailureReason(args.reason);
7794
+ const concreteReadTarget = args.baseDecision.contract.read_targets.find(
7795
+ (target) => Boolean(target.file)
7796
+ );
7797
+ const hasUnknownBucket = args.baseDecision.contract.main_buckets.some(
7798
+ (bucket) => bucket.root_cause.startsWith("unknown ")
7799
+ );
7800
+ if (concreteReadTarget && !hasUnknownBucket) {
7801
+ return buildTestStatusDiagnoseContract({
7802
+ input: args.input,
7803
+ analysis: args.analysis,
7804
+ resolvedTests: args.baseDecision.contract.resolved_tests,
7805
+ remainingTests: args.baseDecision.contract.remaining_tests,
7806
+ remainingMode: args.request.testStatusContext?.remainingMode,
7807
+ contractOverrides: {
7808
+ ...args.baseDecision.contract,
7809
+ diagnosis_complete: false,
7810
+ raw_needed: false,
7811
+ additional_source_read_likely_low_value: false,
7812
+ read_raw_only_if: null,
7813
+ decision: "read_source",
7814
+ provider_used: true,
7815
+ provider_confidence: null,
7816
+ provider_failed: true,
7817
+ raw_slice_used: args.rawSliceUsed,
7818
+ raw_slice_strategy: args.rawSliceStrategy,
7819
+ next_best_action: {
7820
+ code: "read_source_for_bucket",
7821
+ bucket_index: args.baseDecision.contract.dominant_blocker_bucket_index ?? concreteReadTarget.bucket_index,
7822
+ note: `${sanitizedReason[0].toUpperCase()}${sanitizedReason.slice(
7823
+ 1
7824
+ )}. The heuristic anchor is concrete enough to inspect source for the current bucket before reading raw traceback.`
7825
+ }
7826
+ }
7827
+ });
7828
+ }
6319
7829
  const shouldZoomFirst = args.request.detail !== "verbose";
6320
7830
  return buildTestStatusDiagnoseContract({
6321
7831
  input: args.input,
6322
7832
  analysis: args.analysis,
6323
7833
  resolvedTests: args.baseDecision.contract.resolved_tests,
6324
7834
  remainingTests: args.baseDecision.contract.remaining_tests,
7835
+ remainingMode: args.request.testStatusContext?.remainingMode,
6325
7836
  contractOverrides: {
6326
7837
  ...args.baseDecision.contract,
6327
7838
  diagnosis_complete: false,
@@ -6337,12 +7848,16 @@ function buildTestStatusProviderFailureDecision(args) {
6337
7848
  next_best_action: {
6338
7849
  code: shouldZoomFirst ? "insufficient_signal" : "read_raw_for_exact_traceback",
6339
7850
  bucket_index: args.baseDecision.contract.dominant_blocker_bucket_index ?? args.baseDecision.contract.main_buckets[0]?.bucket_index ?? null,
6340
- note: shouldZoomFirst ? `Provider follow-up failed (${args.reason}). Use one deeper sift pass on the same cached output before reading raw traceback lines.` : `Provider follow-up failed (${args.reason}). Read raw traceback only if exact stack lines are still needed.`
7851
+ note: shouldZoomFirst ? `${sanitizedReason[0].toUpperCase()}${sanitizedReason.slice(
7852
+ 1
7853
+ )}. Use one deeper sift pass on the same cached output before reading raw traceback lines.` : `${sanitizedReason[0].toUpperCase()}${sanitizedReason.slice(
7854
+ 1
7855
+ )}. Read raw traceback only if exact stack lines are still needed.`
6341
7856
  }
6342
7857
  }
6343
7858
  });
6344
7859
  }
6345
- async function runSift(request) {
7860
+ async function runSiftCore(request, recorder) {
6346
7861
  const prepared = prepareInput(request.stdin, request.config.input);
6347
7862
  const heuristicInput = prepared.redacted;
6348
7863
  const heuristicInputTruncated = false;
@@ -6358,23 +7873,28 @@ async function runSift(request) {
6358
7873
  const provider = createProvider(request.config);
6359
7874
  const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(heuristicInput);
6360
7875
  const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(heuristicInput) : null;
6361
- const testStatusDecision = hasTestStatusSignal && testStatusAnalysis ? buildTestStatusDiagnoseContract({
7876
+ const useCompactTestStatusOutput = hasTestStatusSignal && testStatusAnalysis ? shouldUseCompactTestStatusBypass({
7877
+ request,
7878
+ analysis: testStatusAnalysis
7879
+ }) : false;
7880
+ const testStatusDecision = hasTestStatusSignal && testStatusAnalysis && !useCompactTestStatusOutput ? buildTestStatusDiagnoseContract({
6362
7881
  input: heuristicInput,
6363
7882
  analysis: testStatusAnalysis,
6364
7883
  resolvedTests: request.testStatusContext?.resolvedTests,
6365
- remainingTests: request.testStatusContext?.remainingTests
7884
+ remainingTests: request.testStatusContext?.remainingTests,
7885
+ remainingMode: request.testStatusContext?.remainingMode
6366
7886
  }) : null;
6367
7887
  const testStatusHeuristicOutput = testStatusDecision ? renderTestStatusDecisionOutput({
6368
7888
  request,
6369
7889
  decision: testStatusDecision
6370
- }) : null;
7890
+ }) : useCompactTestStatusOutput ? applyHeuristicPolicy("test-status", heuristicInput, "standard") : null;
6371
7891
  if (request.config.runtime.verbose) {
6372
7892
  process.stderr.write(
6373
7893
  `${pc2.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
6374
7894
  `
6375
7895
  );
6376
7896
  }
6377
- const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, heuristicInput, request.detail);
7897
+ const heuristicOutput = request.policyName === "test-status" ? useCompactTestStatusOutput ? testStatusHeuristicOutput : testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, heuristicInput, request.detail);
6378
7898
  if (heuristicOutput) {
6379
7899
  if (request.config.runtime.verbose) {
6380
7900
  process.stderr.write(`${pc2.dim("sift")} heuristic=${request.policyName}
@@ -6428,6 +7948,7 @@ async function runSift(request) {
6428
7948
  finalOutput
6429
7949
  });
6430
7950
  }
7951
+ recorder?.heuristic();
6431
7952
  return finalOutput;
6432
7953
  }
6433
7954
  if (testStatusDecision && testStatusAnalysis) {
@@ -6497,6 +8018,7 @@ async function runSift(request) {
6497
8018
  analysis: testStatusAnalysis,
6498
8019
  resolvedTests: request.testStatusContext?.resolvedTests,
6499
8020
  remainingTests: request.testStatusContext?.remainingTests,
8021
+ remainingMode: request.testStatusContext?.remainingMode,
6500
8022
  providerBucketSupplements: supplement.bucket_supplements,
6501
8023
  contractOverrides: {
6502
8024
  diagnosis_complete: supplement.diagnosis_complete,
@@ -6527,6 +8049,7 @@ async function runSift(request) {
6527
8049
  providerInputChars: providerPrepared2.truncated.length,
6528
8050
  providerOutputChars: result.text.length
6529
8051
  });
8052
+ recorder?.provider(result.usage);
6530
8053
  return finalOutput;
6531
8054
  } catch (error) {
6532
8055
  const reason = error instanceof Error ? error.message : "unknown_error";
@@ -6561,6 +8084,7 @@ async function runSift(request) {
6561
8084
  rawSliceChars: rawSlice.text.length,
6562
8085
  providerInputChars: providerPrepared2.truncated.length
6563
8086
  });
8087
+ recorder?.fallback();
6564
8088
  return finalOutput;
6565
8089
  }
6566
8090
  }
@@ -6617,6 +8141,7 @@ async function runSift(request) {
6617
8141
  })) {
6618
8142
  throw new Error("Model output rejected by quality gate");
6619
8143
  }
8144
+ recorder?.provider(result.usage);
6620
8145
  return withInsufficientHint({
6621
8146
  output: normalizeOutput(result.text, providerPrompt.responseMode),
6622
8147
  request,
@@ -6624,6 +8149,7 @@ async function runSift(request) {
6624
8149
  });
6625
8150
  } catch (error) {
6626
8151
  const reason = error instanceof Error ? error.message : "unknown_error";
8152
+ recorder?.fallback();
6627
8153
  return withInsufficientHint({
6628
8154
  output: buildFallbackOutput({
6629
8155
  format: request.format,
@@ -6637,6 +8163,72 @@ async function runSift(request) {
6637
8163
  });
6638
8164
  }
6639
8165
  }
8166
+ async function runSift(request) {
8167
+ return runSiftCore(request);
8168
+ }
8169
+ async function runSiftWithStats(request) {
8170
+ if (request.dryRun) {
8171
+ return {
8172
+ output: await runSiftCore(request),
8173
+ stats: null
8174
+ };
8175
+ }
8176
+ const startedAt = Date.now();
8177
+ let layer = "fallback";
8178
+ let providerCalled = false;
8179
+ let totalTokens = null;
8180
+ const output = await runSiftCore(request, {
8181
+ heuristic() {
8182
+ layer = "heuristic";
8183
+ providerCalled = false;
8184
+ totalTokens = null;
8185
+ },
8186
+ provider(usage) {
8187
+ layer = "provider";
8188
+ providerCalled = true;
8189
+ totalTokens = usage?.totalTokens ?? null;
8190
+ },
8191
+ fallback() {
8192
+ layer = "fallback";
8193
+ providerCalled = true;
8194
+ totalTokens = null;
8195
+ }
8196
+ });
8197
+ return {
8198
+ output,
8199
+ stats: {
8200
+ layer,
8201
+ providerCalled,
8202
+ totalTokens,
8203
+ durationMs: Date.now() - startedAt,
8204
+ presetName: request.presetName
8205
+ }
8206
+ };
8207
+ }
8208
+
8209
+ // src/core/stats.ts
8210
+ import pc3 from "picocolors";
8211
+ function formatDuration(durationMs) {
8212
+ return durationMs >= 1e3 ? `${(durationMs / 1e3).toFixed(1)}s` : `${durationMs}ms`;
8213
+ }
8214
+ function formatStatsFooter(stats) {
8215
+ const duration = formatDuration(stats.durationMs);
8216
+ if (stats.layer === "heuristic") {
8217
+ return `[sift: heuristic \u2022 LLM skipped \u2022 summary ${duration}]`;
8218
+ }
8219
+ if (stats.layer === "provider") {
8220
+ const tokenSegment = stats.totalTokens !== null ? ` \u2022 ${stats.totalTokens} tokens` : "";
8221
+ return `[sift: provider \u2022 LLM used${tokenSegment} \u2022 summary ${duration}]`;
8222
+ }
8223
+ return `[sift: fallback \u2022 provider failed \u2022 summary ${duration}]`;
8224
+ }
8225
+ function emitStatsFooter(args) {
8226
+ if (args.quiet || !args.stats || !process.stderr.isTTY) {
8227
+ return;
8228
+ }
8229
+ process.stderr.write(`${pc3.dim(formatStatsFooter(args.stats))}
8230
+ `);
8231
+ }
6640
8232
 
6641
8233
  // src/core/testStatusState.ts
6642
8234
  import fs5 from "fs";
@@ -6672,6 +8264,7 @@ var failureBucketTypeSchema = z3.enum([
6672
8264
  "import_dependency_failure",
6673
8265
  "collection_failure",
6674
8266
  "assertion_failure",
8267
+ "golden_output_drift",
6675
8268
  "runtime_failure",
6676
8269
  "interrupted_run",
6677
8270
  "no_tests_collected",
@@ -6712,7 +8305,19 @@ var cachedPytestStateSchema = z3.object({
6712
8305
  failingNodeIds: z3.array(z3.string()),
6713
8306
  remainingNodeIds: z3.array(z3.string()).optional()
6714
8307
  }).optional();
6715
- var cachedRunSchema = z3.object({
8308
+ var testRunnerSchema = z3.enum(["pytest", "vitest", "jest", "unknown"]);
8309
+ var cachedRunnerSubsetSchema = z3.object({
8310
+ available: z3.boolean(),
8311
+ strategy: z3.enum(["pytest-node-ids", "none"]),
8312
+ baseArgv: z3.array(z3.string()).min(1).optional()
8313
+ });
8314
+ var cachedRunnerStateSchema = z3.object({
8315
+ name: testRunnerSchema,
8316
+ failingTargets: z3.array(z3.string()),
8317
+ baselineCommand: cachedCommandSchema,
8318
+ subset: cachedRunnerSubsetSchema
8319
+ });
8320
+ var cachedRunV1Schema = z3.object({
6716
8321
  version: z3.literal(1),
6717
8322
  timestamp: z3.string(),
6718
8323
  presetName: z3.literal("test-status"),
@@ -6730,6 +8335,25 @@ var cachedRunSchema = z3.object({
6730
8335
  analysis: cachedAnalysisSchema,
6731
8336
  pytest: cachedPytestStateSchema
6732
8337
  });
8338
+ var cachedRunV2Schema = z3.object({
8339
+ version: z3.literal(2),
8340
+ timestamp: z3.string(),
8341
+ presetName: z3.literal("test-status"),
8342
+ cwd: z3.string(),
8343
+ commandKey: z3.string(),
8344
+ commandPreview: z3.string(),
8345
+ command: cachedCommandSchema,
8346
+ detail: detailSchema,
8347
+ exitCode: z3.number().int(),
8348
+ rawOutput: z3.string(),
8349
+ capture: z3.object({
8350
+ originalChars: countSchema,
8351
+ truncatedApplied: z3.boolean()
8352
+ }),
8353
+ analysis: cachedAnalysisSchema,
8354
+ runner: cachedRunnerStateSchema
8355
+ });
8356
+ var cachedRunSchema = z3.discriminatedUnion("version", [cachedRunV1Schema, cachedRunV2Schema]);
6733
8357
  var MissingCachedTestStatusRunError = class extends Error {
6734
8358
  constructor() {
6735
8359
  super(
@@ -6778,6 +8402,37 @@ function isPytestExecutable(value) {
6778
8402
  function isPythonExecutable(value) {
6779
8403
  return basenameMatches(value, /^python(?:\d+(?:\.\d+)*)?(?:\.exe)?$/i);
6780
8404
  }
8405
+ function detectRunnerFromCommand(command) {
8406
+ if (!command) {
8407
+ return "unknown";
8408
+ }
8409
+ if (command.mode === "argv") {
8410
+ const [first, second, third] = command.argv;
8411
+ if (first && isPytestExecutable(first)) {
8412
+ return "pytest";
8413
+ }
8414
+ if (first && isPythonExecutable(first) && second === "-m" && third === "pytest") {
8415
+ return "pytest";
8416
+ }
8417
+ if (first && basenameMatches(first, /^vitest(?:\.exe)?$/i)) {
8418
+ return "vitest";
8419
+ }
8420
+ if (first && basenameMatches(first, /^jest(?:\.exe)?$/i)) {
8421
+ return "jest";
8422
+ }
8423
+ return "unknown";
8424
+ }
8425
+ if (/\bpython(?:\d+(?:\.\d+)*)?\s+-m\s+pytest\b|\bpytest\b/i.test(command.shellCommand)) {
8426
+ return "pytest";
8427
+ }
8428
+ if (/\bvitest\b/i.test(command.shellCommand)) {
8429
+ return "vitest";
8430
+ }
8431
+ if (/\bjest\b/i.test(command.shellCommand)) {
8432
+ return "jest";
8433
+ }
8434
+ return "unknown";
8435
+ }
6781
8436
  var shortPytestOptionsWithValue = /* @__PURE__ */ new Set([
6782
8437
  "-c",
6783
8438
  "-k",
@@ -6872,26 +8527,52 @@ function buildCachedCommand(args) {
6872
8527
  }
6873
8528
  return void 0;
6874
8529
  }
6875
- function buildFailingNodeIds(analysis) {
8530
+ function buildFailingTargets(analysis) {
8531
+ const runner = analysis.runner;
6876
8532
  const values = [];
6877
8533
  for (const value of [...analysis.visibleErrorLabels, ...analysis.visibleFailedLabels]) {
6878
- if (value.length > 0 && !values.includes(value)) {
6879
- values.push(value);
8534
+ const normalized = normalizeFailingTarget(value, runner);
8535
+ if (normalized.length > 0 && !values.includes(normalized)) {
8536
+ values.push(normalized);
6880
8537
  }
6881
8538
  }
6882
8539
  return values;
6883
8540
  }
6884
- function buildCachedPytestState(args) {
8541
+ function buildCachedRunnerState(args) {
6885
8542
  const baseArgv = args.command?.mode === "argv" && isSubsetCapablePytestArgv(args.command.argv) ? [...args.command.argv] : void 0;
8543
+ const runnerName = args.analysis.runner !== "unknown" ? args.analysis.runner : detectRunnerFromCommand(args.command);
6886
8544
  return {
6887
- subsetCapable: Boolean(baseArgv),
6888
- baseArgv,
6889
- failingNodeIds: buildFailingNodeIds(args.analysis),
6890
- remainingNodeIds: args.remainingNodeIds
8545
+ name: runnerName,
8546
+ failingTargets: buildFailingTargets(args.analysis),
8547
+ baselineCommand: args.command,
8548
+ subset: {
8549
+ available: runnerName === "pytest" && Boolean(baseArgv),
8550
+ strategy: runnerName === "pytest" && baseArgv ? "pytest-node-ids" : "none",
8551
+ ...runnerName === "pytest" && baseArgv ? { baseArgv } : {}
8552
+ }
6891
8553
  };
6892
8554
  }
8555
+ function normalizeCwd(value) {
8556
+ return path7.resolve(value).replace(/\\/g, "/");
8557
+ }
8558
+ function buildTestStatusBaselineIdentity(args) {
8559
+ const cwd = normalizeCwd(args.cwd);
8560
+ const command = args.command ?? buildCachedCommand({
8561
+ shellCommand: args.shellCommand,
8562
+ command: args.shellCommand ? void 0 : args.commandPreview?.split(" ")
8563
+ });
8564
+ const mode = command?.mode ?? (args.shellCommand ? "shell" : "argv");
8565
+ const normalizedCommand = command?.mode === "argv" ? command.argv.join("") : command?.mode === "shell" ? command.shellCommand.trim().replace(/\s+/g, " ") : (args.commandPreview ?? "").trim().replace(/\s+/g, " ");
8566
+ return [cwd, args.runner, mode, normalizedCommand].join("");
8567
+ }
6893
8568
  function buildTestStatusCommandKey(args) {
6894
- return `${args.shellCommand ? "shell" : "argv"}:${args.commandPreview}`;
8569
+ return buildTestStatusBaselineIdentity({
8570
+ cwd: args.cwd ?? process.cwd(),
8571
+ runner: args.runner ?? "unknown",
8572
+ command: args.command,
8573
+ commandPreview: args.commandPreview,
8574
+ shellCommand: args.shellCommand
8575
+ });
6895
8576
  }
6896
8577
  function snapshotTestStatusAnalysis(analysis) {
6897
8578
  return {
@@ -6917,13 +8598,22 @@ function createCachedTestStatusRun(args) {
6917
8598
  command: args.command,
6918
8599
  shellCommand: args.shellCommand
6919
8600
  });
8601
+ const runnerName = args.analysis.runner !== "unknown" ? args.analysis.runner : detectRunnerFromCommand(command);
8602
+ const commandPreview = args.commandPreview ?? args.shellCommand ?? (args.command ?? []).join(" ");
8603
+ const commandKey = args.commandKey ?? buildTestStatusBaselineIdentity({
8604
+ cwd: args.cwd,
8605
+ runner: runnerName,
8606
+ command,
8607
+ commandPreview,
8608
+ shellCommand: args.shellCommand
8609
+ });
6920
8610
  return {
6921
- version: 1,
8611
+ version: 2,
6922
8612
  timestamp: args.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
6923
8613
  presetName: "test-status",
6924
8614
  cwd: args.cwd,
6925
- commandKey: args.commandKey,
6926
- commandPreview: args.commandPreview,
8615
+ commandKey,
8616
+ commandPreview,
6927
8617
  command,
6928
8618
  detail: args.detail,
6929
8619
  exitCode: args.exitCode,
@@ -6933,13 +8623,61 @@ function createCachedTestStatusRun(args) {
6933
8623
  truncatedApplied: args.truncatedApplied
6934
8624
  },
6935
8625
  analysis: snapshotTestStatusAnalysis(args.analysis),
6936
- pytest: buildCachedPytestState({
8626
+ runner: buildCachedRunnerState({
6937
8627
  command,
6938
- analysis: args.analysis,
6939
- remainingNodeIds: args.remainingNodeIds
8628
+ analysis: args.analysis
6940
8629
  })
6941
8630
  };
6942
8631
  }
8632
+ function migrateCachedTestStatusRun(state) {
8633
+ if (state.version === 2) {
8634
+ return state;
8635
+ }
8636
+ const runnerFromOutput = detectTestRunner(state.rawOutput);
8637
+ const runner = runnerFromOutput !== "unknown" ? runnerFromOutput : detectRunnerFromCommand(state.command);
8638
+ const storedCommand = state.command;
8639
+ const fallbackBaseArgv = !storedCommand && state.pytest?.baseArgv ? {
8640
+ mode: "argv",
8641
+ argv: [...state.pytest.baseArgv]
8642
+ } : void 0;
8643
+ const baselineCommand = storedCommand ?? fallbackBaseArgv;
8644
+ const commandPreview = state.commandPreview ?? (baselineCommand?.mode === "argv" ? baselineCommand.argv.join(" ") : baselineCommand?.mode === "shell" ? baselineCommand.shellCommand : "");
8645
+ const commandKey = buildTestStatusBaselineIdentity({
8646
+ cwd: state.cwd,
8647
+ runner,
8648
+ command: baselineCommand,
8649
+ commandPreview
8650
+ });
8651
+ return {
8652
+ version: 2,
8653
+ timestamp: state.timestamp,
8654
+ presetName: state.presetName,
8655
+ cwd: state.cwd,
8656
+ commandKey,
8657
+ commandPreview,
8658
+ command: state.command,
8659
+ detail: state.detail,
8660
+ exitCode: state.exitCode,
8661
+ rawOutput: state.rawOutput,
8662
+ capture: state.capture,
8663
+ analysis: state.analysis,
8664
+ runner: {
8665
+ name: runner,
8666
+ failingTargets: [...new Set((state.pytest?.failingNodeIds ?? []).map(
8667
+ (target) => normalizeFailingTarget(target, runner)
8668
+ ))],
8669
+ baselineCommand,
8670
+ subset: {
8671
+ available: runner === "pytest" && Boolean(state.pytest?.baseArgv),
8672
+ strategy: runner === "pytest" && state.pytest?.baseArgv ? "pytest-node-ids" : "none",
8673
+ ...runner === "pytest" && state.pytest?.baseArgv ? {
8674
+ baseArgv: [...state.pytest.baseArgv]
8675
+ } : {}
8676
+ }
8677
+ },
8678
+ ...fallbackBaseArgv ? { runnerMigrationFallbackUsed: true } : {}
8679
+ };
8680
+ }
6943
8681
  function readCachedTestStatusRun(statePath = getDefaultTestStatusStatePath()) {
6944
8682
  let raw = "";
6945
8683
  try {
@@ -6951,7 +8689,7 @@ function readCachedTestStatusRun(statePath = getDefaultTestStatusStatePath()) {
6951
8689
  throw new InvalidCachedTestStatusRunError();
6952
8690
  }
6953
8691
  try {
6954
- return cachedRunSchema.parse(JSON.parse(raw));
8692
+ return migrateCachedTestStatusRun(cachedRunSchema.parse(JSON.parse(raw)));
6955
8693
  } catch {
6956
8694
  throw new InvalidCachedTestStatusRunError();
6957
8695
  }
@@ -6980,15 +8718,7 @@ function getNextEscalationDetail(detail) {
6980
8718
  return null;
6981
8719
  }
6982
8720
  function buildTargetDelta(args) {
6983
- if (args.previous.presetName !== "test-status" || args.current.presetName !== "test-status" || args.previous.cwd !== args.current.cwd || args.previous.commandKey !== args.current.commandKey) {
6984
- return {
6985
- comparable: false,
6986
- resolved: [],
6987
- remaining: [],
6988
- introduced: []
6989
- };
6990
- }
6991
- if (!args.previous.pytest || !args.current.pytest) {
8721
+ if (args.previous.presetName !== "test-status" || args.current.presetName !== "test-status" || args.previous.cwd !== args.current.cwd || args.previous.commandKey !== args.current.commandKey || args.previous.runner.name !== args.current.runner.name || args.previous.runner.name === "unknown") {
6992
8722
  return {
6993
8723
  comparable: false,
6994
8724
  resolved: [],
@@ -6996,8 +8726,8 @@ function buildTargetDelta(args) {
6996
8726
  introduced: []
6997
8727
  };
6998
8728
  }
6999
- const previousTargets = args.previous.pytest.failingNodeIds;
7000
- const currentTargets = args.current.pytest.failingNodeIds;
8729
+ const previousTargets = args.previous.runner.failingTargets;
8730
+ const currentTargets = args.current.runner.failingTargets;
7001
8731
  const currentTargetSet = new Set(currentTargets);
7002
8732
  const previousTargetSet = new Set(previousTargets);
7003
8733
  return {
@@ -7010,8 +8740,11 @@ function buildTargetDelta(args) {
7010
8740
  function diffTestStatusTargets(args) {
7011
8741
  return buildTargetDelta(args);
7012
8742
  }
8743
+ function isRemainingSubsetAvailable(state) {
8744
+ return state.runner.name === "pytest" && state.runner.subset.available;
8745
+ }
7013
8746
  function getRemainingPytestNodeIds(state) {
7014
- return state.pytest?.remainingNodeIds ?? state.pytest?.failingNodeIds ?? [];
8747
+ return state.runner.name === "pytest" ? state.runner.failingTargets : [];
7015
8748
  }
7016
8749
  function diffTestStatusRuns(args) {
7017
8750
  const targetDelta = buildTargetDelta(args);
@@ -7022,21 +8755,45 @@ function diffTestStatusRuns(args) {
7022
8755
  args.current.analysis.buckets.map((bucket) => [buildBucketSignature(bucket), bucket])
7023
8756
  );
7024
8757
  const lines = [];
7025
- if (targetDelta.resolved.length > 0) {
7026
- lines.push(
7027
- `- Resolved: ${formatCount3(targetDelta.resolved.length, "failing test/module", "failing tests/modules")} no longer appear${appendPreview(targetDelta.resolved)}.`
7028
- );
7029
- }
7030
- if (targetDelta.remaining.length > 0) {
7031
- lines.push(
7032
- `- Remaining: ${formatCount3(targetDelta.remaining.length, "failing test/module", "failing tests/modules")} still appear${appendPreview(targetDelta.remaining)}.`
7033
- );
7034
- }
7035
- if (targetDelta.introduced.length > 0) {
8758
+ const resolvedSummary = buildTestTargetSummary(targetDelta.resolved);
8759
+ const remainingSummary = buildTestTargetSummary(targetDelta.remaining);
8760
+ const introducedSummary = buildTestTargetSummary(targetDelta.introduced);
8761
+ const pushTargetLine = (args2) => {
8762
+ if (args2.summary.count === 0) {
8763
+ return;
8764
+ }
8765
+ const summaryText = describeTargetSummary(args2.summary);
8766
+ if (summaryText) {
8767
+ lines.push(
8768
+ `- ${args2.kind}: ${formatCount3(args2.summary.count, args2.countLabel, `${args2.countLabel}s`)} ${args2.verb} ${summaryText}.`
8769
+ );
8770
+ return;
8771
+ }
7036
8772
  lines.push(
7037
- `- New: ${formatCount3(targetDelta.introduced.length, "failing test/module", "failing tests/modules")} appeared${appendPreview(targetDelta.introduced)}.`
8773
+ `- ${args2.kind}: ${formatCount3(args2.summary.count, args2.countLabel, `${args2.countLabel}s`)} ${args2.verb}${appendPreview(args2.fallbackValues)}.`
7038
8774
  );
7039
- }
8775
+ };
8776
+ pushTargetLine({
8777
+ kind: "Resolved",
8778
+ summary: resolvedSummary,
8779
+ countLabel: "failing target",
8780
+ fallbackValues: targetDelta.resolved,
8781
+ verb: "no longer appear"
8782
+ });
8783
+ pushTargetLine({
8784
+ kind: "Remaining",
8785
+ summary: remainingSummary,
8786
+ countLabel: "failing target",
8787
+ fallbackValues: targetDelta.remaining,
8788
+ verb: "still appear"
8789
+ });
8790
+ pushTargetLine({
8791
+ kind: "New",
8792
+ summary: introducedSummary,
8793
+ countLabel: "failing target",
8794
+ fallbackValues: targetDelta.introduced,
8795
+ verb: "appeared"
8796
+ });
7040
8797
  for (const bucket of args.current.analysis.buckets) {
7041
8798
  const signature = buildBucketSignature(bucket);
7042
8799
  const previous = previousBuckets.get(signature);
@@ -7064,19 +8821,19 @@ function diffTestStatusRuns(args) {
7064
8821
  }
7065
8822
  }
7066
8823
  return {
7067
- lines: lines.slice(0, 4),
7068
- remainingNodeIds: targetDelta.comparable ? targetDelta.remaining : void 0
8824
+ lines: lines.slice(0, 4)
7069
8825
  };
7070
8826
  }
7071
8827
  function getCachedRerunCommand(state) {
7072
- if (state.command?.mode === "argv") {
8828
+ const baselineCommand = state.runner.baselineCommand ?? state.command;
8829
+ if (baselineCommand?.mode === "argv") {
7073
8830
  return {
7074
- command: [...state.command.argv]
8831
+ command: [...baselineCommand.argv]
7075
8832
  };
7076
8833
  }
7077
- if (state.command?.mode === "shell") {
8834
+ if (baselineCommand?.mode === "shell") {
7078
8835
  return {
7079
- shellCommand: state.command.shellCommand
8836
+ shellCommand: baselineCommand.shellCommand
7080
8837
  };
7081
8838
  }
7082
8839
  throw new Error(
@@ -7084,13 +8841,13 @@ function getCachedRerunCommand(state) {
7084
8841
  );
7085
8842
  }
7086
8843
  function getRemainingPytestRerunCommand(state) {
7087
- if (!state.pytest?.subsetCapable || !state.pytest.baseArgv) {
8844
+ if (!isRemainingSubsetAvailable(state) || !state.runner.subset.baseArgv) {
7088
8845
  throw new Error(
7089
8846
  "Cached test-status run cannot use `sift rerun --remaining`. Automatic remaining-subset reruns currently support only argv-mode `pytest ...` or `python -m pytest ...` commands. Run a narrowed command manually with `sift exec --preset test-status -- <narrowed pytest command>`."
7090
8847
  );
7091
8848
  }
7092
8849
  const remainingNodeIds = getRemainingPytestNodeIds(state);
7093
- return [...state.pytest.baseArgv, ...remainingNodeIds];
8850
+ return [...state.runner.subset.baseArgv, ...remainingNodeIds];
7094
8851
  }
7095
8852
 
7096
8853
  // src/core/escalate.ts
@@ -7114,7 +8871,7 @@ async function runEscalate(request) {
7114
8871
  const detail = resolveEscalationDetail(state, request.detail, request.showRaw);
7115
8872
  if (request.verbose) {
7116
8873
  process.stderr.write(
7117
- `${pc3.dim("sift")} escalate detail=${detail} cached_detail=${state.detail} command=${state.commandPreview}
8874
+ `${pc4.dim("sift")} escalate detail=${detail} cached_detail=${state.detail} command=${state.commandPreview}
7118
8875
  `
7119
8876
  );
7120
8877
  }
@@ -7124,7 +8881,7 @@ async function runEscalate(request) {
7124
8881
  process.stderr.write("\n");
7125
8882
  }
7126
8883
  }
7127
- let output = await runSift({
8884
+ const result = await runSiftWithStats({
7128
8885
  question: request.question,
7129
8886
  format: request.format,
7130
8887
  goal: request.goal,
@@ -7138,9 +8895,11 @@ async function runEscalate(request) {
7138
8895
  outputContract: request.outputContract,
7139
8896
  fallbackJson: request.fallbackJson,
7140
8897
  testStatusContext: {
7141
- remainingSubsetAvailable: Boolean(state.pytest?.subsetCapable) && (state.pytest?.failingNodeIds.length ?? 0) > 0
8898
+ remainingSubsetAvailable: isRemainingSubsetAvailable(state) && state.runner.failingTargets.length > 0,
8899
+ remainingMode: "none"
7142
8900
  }
7143
8901
  });
8902
+ let output = result.output;
7144
8903
  if (isInsufficientSignalOutput(output)) {
7145
8904
  output = buildInsufficientSignalOutput({
7146
8905
  presetName: "test-status",
@@ -7151,6 +8910,10 @@ async function runEscalate(request) {
7151
8910
  }
7152
8911
  process.stdout.write(`${output}
7153
8912
  `);
8913
+ emitStatsFooter({
8914
+ stats: result.stats,
8915
+ quiet: Boolean(request.quiet)
8916
+ });
7154
8917
  try {
7155
8918
  writeCachedTestStatusRun({
7156
8919
  ...state,
@@ -7159,7 +8922,7 @@ async function runEscalate(request) {
7159
8922
  } catch (error) {
7160
8923
  if (request.verbose) {
7161
8924
  const reason = error instanceof Error ? error.message : "unknown_error";
7162
- process.stderr.write(`${pc3.dim("sift")} cache_write=failed reason=${reason}
8925
+ process.stderr.write(`${pc4.dim("sift")} cache_write=failed reason=${reason}
7163
8926
  `);
7164
8927
  }
7165
8928
  }
@@ -7169,7 +8932,7 @@ async function runEscalate(request) {
7169
8932
  // src/core/exec.ts
7170
8933
  import { spawn } from "child_process";
7171
8934
  import { constants as osConstants } from "os";
7172
- import pc4 from "picocolors";
8935
+ import pc5 from "picocolors";
7173
8936
 
7174
8937
  // src/core/gate.ts
7175
8938
  var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
@@ -7347,8 +9110,9 @@ async function runTestStatusWatch(request, cycles) {
7347
9110
  testStatusContext: {
7348
9111
  ...request.testStatusContext,
7349
9112
  resolvedTests: targetDelta?.resolved ?? request.testStatusContext?.resolvedTests,
7350
- remainingTests: targetDelta?.remaining ?? currentRun.pytest?.failingNodeIds ?? request.testStatusContext?.remainingTests,
7351
- remainingSubsetAvailable: request.testStatusContext?.remainingSubsetAvailable ?? (Boolean(currentRun.pytest?.subsetCapable) && (currentRun.pytest?.failingNodeIds.length ?? 0) > 0)
9113
+ remainingTests: targetDelta?.remaining ?? currentRun.runner.failingTargets ?? request.testStatusContext?.remainingTests,
9114
+ remainingSubsetAvailable: request.testStatusContext?.remainingSubsetAvailable ?? (isRemainingSubsetAvailable(currentRun) && currentRun.runner.failingTargets.length > 0),
9115
+ remainingMode: request.testStatusContext?.remainingMode ?? "none"
7352
9116
  }
7353
9117
  });
7354
9118
  if (request.goal === "diagnose" && request.format === "json") {
@@ -7495,11 +9259,13 @@ async function runExec(request) {
7495
9259
  const shellPath = process.env.SHELL || "/bin/bash";
7496
9260
  const commandPreview = buildCommandPreview(request);
7497
9261
  const commandCwd = request.cwd ?? process.cwd();
7498
- const shouldCacheTestStatusBase = request.presetName === "test-status" && !request.skipCacheWrite;
7499
- const previousCachedRun = shouldCacheTestStatusBase ? tryReadCachedTestStatusRun() : null;
9262
+ const isTestStatusPreset = request.presetName === "test-status";
9263
+ const readCachedBaseline = isTestStatusPreset && (request.readCachedBaseline ?? true);
9264
+ const writeCachedBaselineRequested = isTestStatusPreset && (request.writeCachedBaseline ?? (request.skipCacheWrite ? false : true));
9265
+ const previousCachedRun = readCachedBaseline ? tryReadCachedTestStatusRun() : null;
7500
9266
  if (request.config.runtime.verbose) {
7501
9267
  process.stderr.write(
7502
- `${pc4.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
9268
+ `${pc5.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
7503
9269
  `
7504
9270
  );
7505
9271
  }
@@ -7528,7 +9294,7 @@ async function runExec(request) {
7528
9294
  }
7529
9295
  bypassed = true;
7530
9296
  if (request.config.runtime.verbose) {
7531
- process.stderr.write(`${pc4.dim("sift")} bypass=interactive-prompt
9297
+ process.stderr.write(`${pc5.dim("sift")} bypass=interactive-prompt
7532
9298
  `);
7533
9299
  }
7534
9300
  process.stderr.write(capture.render());
@@ -7554,18 +9320,20 @@ async function runExec(request) {
7554
9320
  const capturedOutput = capture.render();
7555
9321
  const autoWatchDetected = !request.watch && looksLikeWatchStream(capturedOutput);
7556
9322
  const useWatchFlow = Boolean(request.watch) || autoWatchDetected;
7557
- const shouldCacheTestStatus = shouldCacheTestStatusBase && !useWatchFlow;
9323
+ const shouldBuildTestStatusState = isTestStatusPreset && !useWatchFlow;
9324
+ const shouldWriteCachedBaseline = writeCachedBaselineRequested && !useWatchFlow;
7558
9325
  if (request.config.runtime.verbose) {
7559
9326
  process.stderr.write(
7560
- `${pc4.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
9327
+ `${pc5.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
7561
9328
  `
7562
9329
  );
7563
9330
  }
7564
9331
  if (autoWatchDetected) {
7565
- process.stderr.write(`${pc4.dim("sift")} auto-watch=detected
9332
+ process.stderr.write(`${pc5.dim("sift")} auto-watch=detected
7566
9333
  `);
7567
9334
  }
7568
9335
  if (!bypassed) {
9336
+ const reductionStartedAt = Date.now();
7569
9337
  if (request.showRaw && capturedOutput.length > 0) {
7570
9338
  process.stderr.write(capturedOutput);
7571
9339
  if (!capturedOutput.endsWith("\n")) {
@@ -7580,12 +9348,22 @@ async function runExec(request) {
7580
9348
  if (execSuccessShortcut && !request.dryRun) {
7581
9349
  if (request.config.runtime.verbose) {
7582
9350
  process.stderr.write(
7583
- `${pc4.dim("sift")} exec_shortcut=${request.presetName}
9351
+ `${pc5.dim("sift")} exec_shortcut=${request.presetName}
7584
9352
  `
7585
9353
  );
7586
9354
  }
7587
9355
  process.stdout.write(`${execSuccessShortcut}
7588
9356
  `);
9357
+ emitStatsFooter({
9358
+ stats: {
9359
+ layer: "heuristic",
9360
+ providerCalled: false,
9361
+ totalTokens: null,
9362
+ durationMs: Date.now() - reductionStartedAt,
9363
+ presetName: request.presetName
9364
+ },
9365
+ quiet: Boolean(request.quiet)
9366
+ });
7589
9367
  return exitCode;
7590
9368
  }
7591
9369
  if (useWatchFlow) {
@@ -7598,17 +9376,27 @@ async function runExec(request) {
7598
9376
  presetName: request.presetName,
7599
9377
  originalLength: capture.getTotalChars(),
7600
9378
  truncatedApplied: capture.wasTruncated(),
7601
- exitCode
9379
+ exitCode,
9380
+ recognizedRunner: detectTestRunner(capturedOutput)
7602
9381
  });
7603
9382
  }
7604
9383
  process.stdout.write(`${output2}
7605
9384
  `);
7606
9385
  return exitCode;
7607
9386
  }
7608
- const analysis = shouldCacheTestStatus ? analyzeTestStatus(capturedOutput) : null;
7609
- let currentCachedRun = shouldCacheTestStatus && analysis ? createCachedTestStatusRun({
9387
+ const analysis = shouldBuildTestStatusState ? analyzeTestStatus(capturedOutput) : null;
9388
+ let currentCachedRun = shouldBuildTestStatusState && analysis ? createCachedTestStatusRun({
7610
9389
  cwd: commandCwd,
7611
9390
  commandKey: buildTestStatusCommandKey({
9391
+ cwd: commandCwd,
9392
+ runner: analysis.runner,
9393
+ command: Array.isArray(request.command) && request.command.length > 0 ? {
9394
+ mode: "argv",
9395
+ argv: [...request.command]
9396
+ } : request.shellCommand ? {
9397
+ mode: "shell",
9398
+ shellCommand: request.shellCommand
9399
+ } : void 0,
7612
9400
  commandPreview,
7613
9401
  shellCommand: request.shellCommand
7614
9402
  }),
@@ -7622,36 +9410,39 @@ async function runExec(request) {
7622
9410
  truncatedApplied: capture.wasTruncated(),
7623
9411
  analysis
7624
9412
  }) : null;
7625
- const targetDelta = request.diff && !request.dryRun && previousCachedRun && currentCachedRun ? diffTestStatusTargets({
9413
+ const targetDelta = (request.diff || request.testStatusContext?.remainingMode === "subset_rerun" || request.testStatusContext?.remainingMode === "full_rerun_diff") && !request.dryRun && previousCachedRun && currentCachedRun ? diffTestStatusTargets({
7626
9414
  previous: previousCachedRun,
7627
9415
  current: currentCachedRun
7628
9416
  }) : null;
7629
- let output = await runSift({
9417
+ const result = await runSiftWithStats({
7630
9418
  ...request,
7631
9419
  stdin: capturedOutput,
7632
- analysisContext: request.skipCacheWrite && request.presetName === "test-status" ? [
9420
+ analysisContext: request.testStatusContext?.remainingMode && request.testStatusContext.remainingMode !== "none" && request.presetName === "test-status" ? [
7633
9421
  request.analysisContext,
7634
9422
  "Zoom context:",
7635
9423
  "- This pass is remaining-only.",
7636
9424
  "- The full-suite truth already exists from the cached full run.",
7637
9425
  "- Do not reintroduce resolved tests into the diagnosis."
7638
9426
  ].filter((value) => Boolean(value)).join("\n") : request.analysisContext,
7639
- testStatusContext: shouldCacheTestStatus && analysis ? {
9427
+ testStatusContext: shouldBuildTestStatusState && analysis ? {
7640
9428
  ...request.testStatusContext,
7641
9429
  resolvedTests: targetDelta?.resolved ?? request.testStatusContext?.resolvedTests,
7642
- remainingTests: targetDelta?.remaining ?? currentCachedRun?.pytest?.failingNodeIds ?? request.testStatusContext?.remainingTests,
9430
+ remainingTests: targetDelta?.remaining ?? currentCachedRun?.runner.failingTargets ?? request.testStatusContext?.remainingTests,
7643
9431
  remainingSubsetAvailable: request.testStatusContext?.remainingSubsetAvailable ?? Boolean(
7644
- currentCachedRun?.pytest?.subsetCapable && (targetDelta?.remaining ?? currentCachedRun?.pytest?.failingNodeIds ?? []).length > 0
7645
- )
9432
+ currentCachedRun && isRemainingSubsetAvailable(currentCachedRun) && (targetDelta?.remaining ?? currentCachedRun?.runner.failingTargets ?? []).length > 0
9433
+ ),
9434
+ remainingMode: request.testStatusContext?.remainingMode ?? "none"
7646
9435
  } : request.testStatusContext
7647
9436
  });
7648
- if (shouldCacheTestStatus) {
9437
+ let output = result.output;
9438
+ if (shouldBuildTestStatusState) {
7649
9439
  if (isInsufficientSignalOutput(output)) {
7650
9440
  output = buildInsufficientSignalOutput({
7651
9441
  presetName: request.presetName,
7652
9442
  originalLength: capture.getTotalChars(),
7653
9443
  truncatedApplied: capture.wasTruncated(),
7654
- exitCode
9444
+ exitCode,
9445
+ recognizedRunner: detectTestRunner(capturedOutput)
7655
9446
  });
7656
9447
  }
7657
9448
  if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
@@ -7659,32 +9450,18 @@ async function runExec(request) {
7659
9450
  previous: previousCachedRun,
7660
9451
  current: currentCachedRun
7661
9452
  });
7662
- currentCachedRun = createCachedTestStatusRun({
7663
- cwd: commandCwd,
7664
- commandKey: currentCachedRun.commandKey,
7665
- commandPreview,
7666
- command: request.command,
7667
- shellCommand: request.shellCommand,
7668
- detail: request.detail ?? "standard",
7669
- exitCode,
7670
- rawOutput: capturedOutput,
7671
- originalChars: capture.getTotalChars(),
7672
- truncatedApplied: capture.wasTruncated(),
7673
- analysis,
7674
- remainingNodeIds: delta.remainingNodeIds
7675
- });
7676
9453
  if (delta.lines.length > 0) {
7677
9454
  output = `${delta.lines.join("\n")}
7678
9455
  ${output}`;
7679
9456
  }
7680
9457
  }
7681
- if (currentCachedRun) {
9458
+ if (currentCachedRun && shouldWriteCachedBaseline) {
7682
9459
  try {
7683
9460
  writeCachedTestStatusRun(currentCachedRun);
7684
9461
  } catch (error) {
7685
9462
  if (request.config.runtime.verbose) {
7686
9463
  const reason = error instanceof Error ? error.message : "unknown_error";
7687
- process.stderr.write(`${pc4.dim("sift")} cache_write=failed reason=${reason}
9464
+ process.stderr.write(`${pc5.dim("sift")} cache_write=failed reason=${reason}
7688
9465
  `);
7689
9466
  }
7690
9467
  }
@@ -7694,11 +9471,16 @@ ${output}`;
7694
9471
  presetName: request.presetName,
7695
9472
  originalLength: capture.getTotalChars(),
7696
9473
  truncatedApplied: capture.wasTruncated(),
7697
- exitCode
9474
+ exitCode,
9475
+ recognizedRunner: detectTestRunner(capturedOutput)
7698
9476
  });
7699
9477
  }
7700
9478
  process.stdout.write(`${output}
7701
9479
  `);
9480
+ emitStatsFooter({
9481
+ stats: result.stats,
9482
+ quiet: Boolean(request.quiet)
9483
+ });
7702
9484
  if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
7703
9485
  presetName: request.presetName,
7704
9486
  output
@@ -7720,25 +9502,60 @@ async function runRerun(request) {
7720
9502
  diff: true,
7721
9503
  presetName: "test-status",
7722
9504
  detail: "standard",
7723
- showRaw: false
9505
+ showRaw: false,
9506
+ readCachedBaseline: true,
9507
+ writeCachedBaseline: true,
9508
+ testStatusContext: {
9509
+ ...request.testStatusContext,
9510
+ remainingMode: "none"
9511
+ }
7724
9512
  });
7725
9513
  }
7726
- const remainingNodeIds = getRemainingPytestNodeIds(state);
7727
- if (remainingNodeIds.length === 0) {
7728
- process.stdout.write("No remaining failing pytest targets.\n");
7729
- return 0;
9514
+ if (state.runner.name === "pytest") {
9515
+ const remainingNodeIds = getRemainingPytestNodeIds(state);
9516
+ if (remainingNodeIds.length === 0) {
9517
+ process.stdout.write("No remaining failing pytest targets.\n");
9518
+ return 0;
9519
+ }
9520
+ return runExec({
9521
+ ...request,
9522
+ command: getRemainingPytestRerunCommand(state),
9523
+ cwd: state.cwd,
9524
+ diff: false,
9525
+ presetName: "test-status",
9526
+ readCachedBaseline: true,
9527
+ writeCachedBaseline: false,
9528
+ testStatusContext: {
9529
+ ...request.testStatusContext,
9530
+ remainingSubsetAvailable: isRemainingSubsetAvailable(state),
9531
+ remainingMode: "subset_rerun"
9532
+ }
9533
+ });
7730
9534
  }
7731
- return runExec({
7732
- ...request,
7733
- command: getRemainingPytestRerunCommand(state),
7734
- cwd: state.cwd,
7735
- diff: false,
7736
- presetName: "test-status",
7737
- skipCacheWrite: true,
7738
- testStatusContext: {
7739
- remainingSubsetAvailable: true
9535
+ if (state.runner.name === "vitest" || state.runner.name === "jest") {
9536
+ if (!state.runner.baselineCommand || state.runnerMigrationFallbackUsed) {
9537
+ throw new Error(
9538
+ "Cached test-status run cannot use `sift rerun --remaining` yet because the original full command is unavailable from cache. Refresh the baseline with `sift exec --preset test-status -- <test command>` and retry."
9539
+ );
7740
9540
  }
7741
- });
9541
+ return runExec({
9542
+ ...request,
9543
+ ...getCachedRerunCommand(state),
9544
+ cwd: state.cwd,
9545
+ diff: false,
9546
+ presetName: "test-status",
9547
+ readCachedBaseline: true,
9548
+ writeCachedBaseline: false,
9549
+ testStatusContext: {
9550
+ ...request.testStatusContext,
9551
+ remainingSubsetAvailable: false,
9552
+ remainingMode: "full_rerun_diff"
9553
+ }
9554
+ });
9555
+ }
9556
+ throw new Error(
9557
+ "Cached test-status run cannot use `sift rerun --remaining` for this runner. Refresh with `sift exec --preset test-status -- <test command>` or rerun a narrowed command manually."
9558
+ );
7742
9559
  }
7743
9560
 
7744
9561
  // src/core/stdin.ts
@@ -7788,6 +9605,7 @@ var defaultCliDeps = {
7788
9605
  evaluateGate,
7789
9606
  readStdin,
7790
9607
  runSift,
9608
+ runSiftWithStats,
7791
9609
  runWatch,
7792
9610
  looksLikeWatchStream,
7793
9611
  getPreset
@@ -7896,7 +9714,7 @@ function applySharedOptions(command) {
7896
9714
  ).option(
7897
9715
  "--fail-on",
7898
9716
  "Fail with exit code 1 when a supported built-in preset produces a blocking result"
7899
- ).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
9717
+ ).option("--config <path>", "Path to config file").option("--quiet", "Suppress the stats footer on stderr").option("--verbose", "Enable verbose stderr logging");
7900
9718
  }
7901
9719
  function normalizeDetail(value) {
7902
9720
  if (value === void 0 || value === null || value === "") {
@@ -7995,21 +9813,25 @@ function createCliApp(args = {}) {
7995
9813
  stderr.write("\n");
7996
9814
  }
7997
9815
  }
7998
- const output = deps.looksLikeWatchStream(stdin) ? await deps.runWatch({
7999
- question: input.question,
8000
- format: input.format,
8001
- goal: input.goal,
8002
- stdin,
8003
- config,
8004
- dryRun: Boolean(input.options.dryRun),
8005
- showRaw: Boolean(input.options.showRaw),
8006
- includeTestIds: Boolean(input.options.includeTestIds),
8007
- detail: input.detail,
8008
- presetName: input.presetName,
8009
- policyName: input.policyName,
8010
- outputContract: input.outputContract,
8011
- fallbackJson: input.fallbackJson
8012
- }) : await deps.runSift({
9816
+ const isWatchStream = deps.looksLikeWatchStream(stdin);
9817
+ const result = isWatchStream ? {
9818
+ output: await deps.runWatch({
9819
+ question: input.question,
9820
+ format: input.format,
9821
+ goal: input.goal,
9822
+ stdin,
9823
+ config,
9824
+ dryRun: Boolean(input.options.dryRun),
9825
+ showRaw: Boolean(input.options.showRaw),
9826
+ includeTestIds: Boolean(input.options.includeTestIds),
9827
+ detail: input.detail,
9828
+ presetName: input.presetName,
9829
+ policyName: input.policyName,
9830
+ outputContract: input.outputContract,
9831
+ fallbackJson: input.fallbackJson
9832
+ }),
9833
+ stats: null
9834
+ } : await deps.runSiftWithStats({
8013
9835
  question: input.question,
8014
9836
  format: input.format,
8015
9837
  goal: input.goal,
@@ -8024,8 +9846,13 @@ function createCliApp(args = {}) {
8024
9846
  outputContract: input.outputContract,
8025
9847
  fallbackJson: input.fallbackJson
8026
9848
  });
9849
+ const output = result.output;
8027
9850
  stdout.write(`${output}
8028
9851
  `);
9852
+ emitStatsFooter({
9853
+ stats: result.stats,
9854
+ quiet: Boolean(input.options.quiet)
9855
+ });
8029
9856
  if (Boolean(input.options.failOn) && !Boolean(input.options.dryRun) && input.presetName && deps.evaluateGate({
8030
9857
  presetName: input.presetName,
8031
9858
  output
@@ -8055,6 +9882,7 @@ function createCliApp(args = {}) {
8055
9882
  dryRun: Boolean(input.options.dryRun),
8056
9883
  diff: input.diff,
8057
9884
  failOn: Boolean(input.options.failOn),
9885
+ quiet: Boolean(input.options.quiet),
8058
9886
  showRaw: Boolean(input.options.showRaw),
8059
9887
  includeTestIds: Boolean(input.options.includeTestIds),
8060
9888
  watch: Boolean(input.options.watch),
@@ -8234,13 +10062,20 @@ function createCliApp(args = {}) {
8234
10062
  outputContract: preset.outputContract,
8235
10063
  fallbackJson: preset.fallbackJson,
8236
10064
  detail: normalizeEscalateDetail(options.detail),
10065
+ quiet: Boolean(options.quiet),
8237
10066
  showRaw: Boolean(options.showRaw),
8238
10067
  verbose: Boolean(options.verbose)
8239
10068
  });
8240
10069
  });
8241
10070
  applySharedOptions(
8242
- cli.command("rerun", "Rerun the cached test-status command or only the remaining pytest subset")
8243
- ).usage("rerun [options]").example("rerun").example("rerun --remaining").example("rerun --remaining --detail focused").example("rerun --remaining --detail verbose --show-raw").option("--remaining", "Rerun only the remaining failing pytest node IDs from the cached full run").action(async (options) => {
10071
+ cli.command(
10072
+ "rerun",
10073
+ "Rerun the cached test-status command or focus on what still fails from the cached baseline"
10074
+ )
10075
+ ).usage("rerun [options]").example("rerun").example("rerun --remaining").example("rerun --remaining --detail focused").example("rerun --remaining --detail verbose --show-raw").option(
10076
+ "--remaining",
10077
+ "Focus on what still fails from the cached baseline; narrows automatically for pytest and diffs a full rerun for vitest/jest"
10078
+ ).action(async (options) => {
8244
10079
  const remaining = Boolean(options.remaining);
8245
10080
  if (!remaining && Boolean(options.showRaw)) {
8246
10081
  throw new Error("--show-raw is supported only with `sift rerun --remaining`.");