@bilalimamoglu/sift 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/core/exec.ts
2
2
  import { spawn } from "child_process";
3
3
  import { constants as osConstants } from "os";
4
- import pc2 from "picocolors";
4
+ import pc3 from "picocolors";
5
5
 
6
6
  // src/constants.ts
7
7
  import os from "os";
@@ -61,7 +61,7 @@ function evaluateGate(args) {
61
61
 
62
62
  // src/core/testStatusDecision.ts
63
63
  import { z } from "zod";
64
- var TEST_STATUS_DIAGNOSE_JSON_CONTRACT = '{"status":"ok|insufficient","diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","dominant_blocker_bucket_index":number|null,"provider_used":boolean,"provider_confidence":number|null,"provider_failed":boolean,"raw_slice_used":boolean,"raw_slice_strategy":"none|bucket_evidence|traceback_window|head_tail","resolved_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_subset_available":boolean,"main_buckets":[{"bucket_index":number,"label":string,"count":number,"root_cause":string,"evidence":string[],"bucket_confidence":number,"root_cause_confidence":number,"dominant":boolean,"secondary_visible_despite_blocker":boolean,"mini_diff":{"added_paths"?:number,"removed_models"?:number,"changed_task_mappings"?:number}|null}],"read_targets":[{"file":string,"line":number|null,"why":string,"bucket_index":number,"context_hint":{"start_line":number|null,"end_line":number|null,"search_hint":string|null}}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string},"resolved_tests"?:string[],"remaining_tests"?:string[]}';
64
+ var TEST_STATUS_DIAGNOSE_JSON_CONTRACT = '{"status":"ok|insufficient","diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","primary_suspect_kind":"test|app_code|config|environment|tooling|unknown","confidence_reason":string,"dominant_blocker_bucket_index":number|null,"provider_used":boolean,"provider_confidence":number|null,"provider_failed":boolean,"raw_slice_used":boolean,"raw_slice_strategy":"none|bucket_evidence|traceback_window|head_tail","resolved_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_subset_available":boolean,"main_buckets":[{"bucket_index":number,"label":string,"count":number,"root_cause":string,"suspect_kind":"test|app_code|config|environment|tooling|unknown","fix_hint":string,"evidence":string[],"bucket_confidence":number,"root_cause_confidence":number,"dominant":boolean,"secondary_visible_despite_blocker":boolean,"mini_diff":{"added_paths"?:number,"removed_models"?:number,"changed_task_mappings"?:number}|null}],"read_targets":[{"file":string,"line":number|null,"why":string,"bucket_index":number,"context_hint":{"start_line":number|null,"end_line":number|null,"search_hint":string|null}}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string},"resolved_tests"?:string[],"remaining_tests"?:string[]}';
65
65
  var TEST_STATUS_PROVIDER_SUPPLEMENT_JSON_CONTRACT = '{"diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","provider_confidence":number|null,"bucket_supplements":[{"label":string,"count":number,"root_cause":string,"anchor":{"file":string|null,"line":number|null,"search_hint":string|null},"fix_hint":string|null,"confidence":number}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string}}';
66
66
  var nextBestActionSchema = z.object({
67
67
  code: z.enum([
@@ -103,6 +103,15 @@ var testStatusDiagnoseContractSchema = z.object({
103
103
  additional_source_read_likely_low_value: z.boolean(),
104
104
  read_raw_only_if: z.string().nullable(),
105
105
  decision: z.enum(["stop", "zoom", "read_source", "read_raw"]),
106
+ primary_suspect_kind: z.enum([
107
+ "test",
108
+ "app_code",
109
+ "config",
110
+ "environment",
111
+ "tooling",
112
+ "unknown"
113
+ ]),
114
+ confidence_reason: z.string().min(1),
106
115
  dominant_blocker_bucket_index: z.number().int().nullable(),
107
116
  provider_used: z.boolean(),
108
117
  provider_confidence: z.number().min(0).max(1).nullable(),
@@ -117,6 +126,15 @@ var testStatusDiagnoseContractSchema = z.object({
117
126
  label: z.string(),
118
127
  count: z.number().int(),
119
128
  root_cause: z.string(),
129
+ suspect_kind: z.enum([
130
+ "test",
131
+ "app_code",
132
+ "config",
133
+ "environment",
134
+ "tooling",
135
+ "unknown"
136
+ ]),
137
+ fix_hint: z.string().min(1),
120
138
  evidence: z.array(z.string()).max(2),
121
139
  bucket_confidence: z.number(),
122
140
  root_cause_confidence: z.number(),
@@ -167,6 +185,42 @@ function parseTestStatusProviderSupplement(input) {
167
185
  return testStatusProviderSupplementSchema.parse(JSON.parse(input));
168
186
  }
169
187
  var extendedBucketSpecs = [
188
+ {
189
+ prefix: "service unavailable:",
190
+ type: "service_unavailable",
191
+ label: "service unavailable",
192
+ genericTitle: "Service unavailable failures",
193
+ defaultCoverage: "error",
194
+ rootCauseConfidence: 0.9,
195
+ dominantPriority: 2,
196
+ dominantBlocker: true,
197
+ why: "it contains the dependency service or API path that is unavailable in the test environment",
198
+ fix: "Restore the dependency service or test double before rerunning the full suite."
199
+ },
200
+ {
201
+ prefix: "db refused:",
202
+ type: "db_connection_failure",
203
+ label: "database connection",
204
+ genericTitle: "Database connection failures",
205
+ defaultCoverage: "error",
206
+ rootCauseConfidence: 0.9,
207
+ dominantPriority: 2,
208
+ dominantBlocker: true,
209
+ why: "it contains the database host, DSN, or startup path that is refusing connections",
210
+ fix: "Restore the test database connectivity before rerunning the full suite."
211
+ },
212
+ {
213
+ prefix: "auth bypass absent:",
214
+ type: "auth_bypass_absent",
215
+ label: "auth bypass missing",
216
+ genericTitle: "Auth bypass setup failures",
217
+ defaultCoverage: "error",
218
+ rootCauseConfidence: 0.86,
219
+ dominantPriority: 2,
220
+ dominantBlocker: true,
221
+ why: "it contains the auth bypass fixture or setup path that tests expected to be active",
222
+ fix: "Restore the test auth bypass fixture or mock before rerunning the full suite."
223
+ },
170
224
  {
171
225
  prefix: "snapshot mismatch:",
172
226
  type: "snapshot_mismatch",
@@ -351,6 +405,16 @@ var extendedBucketSpecs = [
351
405
  why: "it contains the deprecated API or warning filter that is failing the test run",
352
406
  fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
353
407
  },
408
+ {
409
+ prefix: "assertion failed:",
410
+ type: "assertion_failure",
411
+ label: "assertion failure",
412
+ genericTitle: "Assertion failures",
413
+ defaultCoverage: "failed",
414
+ rootCauseConfidence: 0.76,
415
+ why: "it contains the expected-versus-actual assertion that failed inside the visible test",
416
+ fix: "Read the assertion diff or expectation and fix the code or expected value before rerunning."
417
+ },
354
418
  {
355
419
  prefix: "xfail strict:",
356
420
  type: "xfail_strict_unexpected_pass",
@@ -1140,7 +1204,7 @@ function buildProviderSupplementBuckets(args) {
1140
1204
  });
1141
1205
  }
1142
1206
  function pickUnknownAnchor(args) {
1143
- const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : null;
1207
+ const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : args.analysis.visibleFailedItems[0];
1144
1208
  if (fromStatusItems) {
1145
1209
  return {
1146
1210
  label: fromStatusItems.label,
@@ -1177,12 +1241,14 @@ function buildUnknownBucket(args) {
1177
1241
  const isError = args.kind === "error";
1178
1242
  const label = isError ? "unknown setup blocker" : "unknown failure family";
1179
1243
  const reason = isError ? "unknown setup blocker: setup failures share a repeated but unclassified pattern" : "unknown failure family: failing tests share a repeated but unclassified pattern";
1244
+ const firstConcreteSignal = anchor && anchor.reason !== reason && anchor.reason !== "setup failures share a repeated but unclassified pattern" && anchor.reason !== "failing tests share a repeated but unclassified pattern" ? `First concrete signal: ${anchor.reason}` : null;
1180
1245
  return {
1181
1246
  type: "unknown_failure",
1182
1247
  headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
1183
1248
  summaryLines: [
1184
- `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`
1185
- ],
1249
+ `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
1250
+ firstConcreteSignal
1251
+ ].filter((value) => Boolean(value)),
1186
1252
  reason,
1187
1253
  count: args.count,
1188
1254
  confidence: 0.45,
@@ -1309,7 +1375,7 @@ function buildStandardAnchorText(target) {
1309
1375
  }
1310
1376
  return formatReadTargetLocation(target);
1311
1377
  }
1312
- function buildStandardFixText(args) {
1378
+ function resolveBucketFixHint(args) {
1313
1379
  if (args.bucket.hint) {
1314
1380
  return args.bucket.hint;
1315
1381
  }
@@ -1358,13 +1424,75 @@ function buildStandardFixText(args) {
1358
1424
  if (args.bucket.type === "runtime_failure") {
1359
1425
  return `Fix the visible ${args.bucketLabel} and rerun the full suite at standard.`;
1360
1426
  }
1361
- return null;
1427
+ return "Inspect the first visible anchor for this bucket, apply the smallest fix that explains it, then rerun the full suite at standard.";
1428
+ }
1429
+ function deriveBucketSuspectKind(args) {
1430
+ if (args.bucket.type === "shared_environment_blocker" || args.bucket.type === "fixture_guard_failure" || args.bucket.type === "permission_denied_failure" || args.bucket.type === "django_db_access_denied" || args.bucket.type === "network_failure" || args.bucket.type === "service_unavailable" || args.bucket.type === "db_connection_failure" || args.bucket.type === "auth_bypass_absent" || args.bucket.type === "fixture_teardown_failure") {
1431
+ return "environment";
1432
+ }
1433
+ if (args.bucket.type === "configuration_error" || args.bucket.type === "db_migration_failure" || args.bucket.type === "import_dependency_failure" || args.bucket.type === "collection_failure" || args.bucket.type === "no_tests_collected" || args.bucket.type === "deprecation_warning_as_error" || args.bucket.type === "file_not_found_failure") {
1434
+ return "config";
1435
+ }
1436
+ if (args.bucket.type === "contract_snapshot_drift" || args.bucket.type === "snapshot_mismatch" || args.bucket.type === "flaky_test_detected" || args.bucket.type === "xfail_strict_unexpected_pass") {
1437
+ return "test";
1438
+ }
1439
+ if (args.bucket.type === "xdist_worker_crash" || args.bucket.type === "timeout_failure" || args.bucket.type === "async_event_loop_failure" || args.bucket.type === "subprocess_crash_segfault" || args.bucket.type === "memory_error" || args.bucket.type === "resource_leak_warning" || args.bucket.type === "interrupted_run") {
1440
+ return "tooling";
1441
+ }
1442
+ if (args.bucket.type === "unknown_failure") {
1443
+ return "unknown";
1444
+ }
1445
+ if (args.bucket.type === "assertion_failure" || args.bucket.type === "runtime_failure" || args.bucket.type === "type_error_failure" || args.bucket.type === "serialization_encoding_failure") {
1446
+ const file = args.readTarget?.file ?? "";
1447
+ if (file.startsWith("src/")) {
1448
+ return "app_code";
1449
+ }
1450
+ if (file.startsWith("test/") || file.startsWith("tests/")) {
1451
+ return "test";
1452
+ }
1453
+ return "unknown";
1454
+ }
1455
+ return "unknown";
1456
+ }
1457
+ function derivePrimarySuspectKind(args) {
1458
+ const primaryBucket = (args.dominantBlockerBucketIndex !== null ? args.mainBuckets.find((bucket) => bucket.bucket_index === args.dominantBlockerBucketIndex) : null) ?? args.mainBuckets[0];
1459
+ return primaryBucket?.suspect_kind ?? "unknown";
1460
+ }
1461
+ function buildConfidenceReason(args) {
1462
+ const primaryBucket = args.mainBuckets.find((bucket) => bucket.dominant) ?? args.mainBuckets[0];
1463
+ if (args.decision === "stop" && primaryBucket && args.primarySuspectKind !== "unknown") {
1464
+ return `Dominant blocker (${primaryBucket.label}) is anchored and actionable.`;
1465
+ }
1466
+ if (args.decision === "zoom") {
1467
+ return "Unknown or low-confidence buckets remain; one deeper sift pass is justified.";
1468
+ }
1469
+ if (args.decision === "read_source") {
1470
+ return "The bucket is identified, but source context is still needed to make the next fix clear.";
1471
+ }
1472
+ return "Heuristic signal is still insufficient; exact traceback lines are needed.";
1473
+ }
1474
+ function formatSuspectKindLabel(kind) {
1475
+ switch (kind) {
1476
+ case "test":
1477
+ return "test code";
1478
+ case "app_code":
1479
+ return "application code";
1480
+ case "config":
1481
+ return "test or project configuration";
1482
+ case "environment":
1483
+ return "environment setup";
1484
+ case "tooling":
1485
+ return "test runner or tooling";
1486
+ default:
1487
+ return "unknown";
1488
+ }
1362
1489
  }
1363
1490
  function buildStandardBucketSupport(args) {
1364
1491
  return {
1365
1492
  headline: args.bucket.summaryLines[0] ? `- ${args.bucket.summaryLines[0]}` : renderBucketHeadline(args.contractBucket),
1493
+ firstConcreteSignalText: args.bucket.source === "unknown" ? args.bucket.summaryLines[1] ?? null : null,
1366
1494
  anchorText: buildStandardAnchorText(args.readTarget),
1367
- fixText: buildStandardFixText({
1495
+ fixText: resolveBucketFixHint({
1368
1496
  bucket: args.bucket,
1369
1497
  bucketLabel: args.contractBucket.label
1370
1498
  })
@@ -1387,6 +1515,9 @@ function renderStandard(args) {
1387
1515
  )
1388
1516
  });
1389
1517
  lines.push(support.headline);
1518
+ if (support.firstConcreteSignalText) {
1519
+ lines.push(`- ${support.firstConcreteSignalText}`);
1520
+ }
1390
1521
  if (support.anchorText) {
1391
1522
  lines.push(`- Anchor: ${support.anchorText}`);
1392
1523
  }
@@ -1396,6 +1527,7 @@ function renderStandard(args) {
1396
1527
  }
1397
1528
  }
1398
1529
  lines.push(buildDecisionLine(args.contract));
1530
+ lines.push(`- Likely owner: ${formatSuspectKindLabel(args.contract.primary_suspect_kind)}`);
1399
1531
  lines.push(`- Next: ${args.contract.next_best_action.note}`);
1400
1532
  lines.push(buildStopSignal(args.contract));
1401
1533
  return lines.join("\n");
@@ -1483,29 +1615,49 @@ function buildTestStatusDiagnoseContract(args) {
1483
1615
  })[0] ?? null;
1484
1616
  const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
1485
1617
  const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
1486
- const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && hasConcreteCoverage && !hasUnknownBucket && (dominantBucket?.bucket.confidence ?? 0) >= 0.6;
1487
- const rawNeeded = buckets.length === 0 ? !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure) : !diagnosisComplete && !hasUnknownBucket && buckets.every((bucket) => bucket.confidence < 0.7);
1488
1618
  const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
1489
1619
  const readTargets = buildReadTargets({
1490
1620
  buckets,
1491
1621
  dominantBucketIndex: dominantBlockerBucketIndex
1492
1622
  });
1493
- const mainBuckets = buckets.map((bucket, index) => ({
1494
- bucket_index: index + 1,
1495
- label: labelForBucket(bucket),
1496
- count: bucket.count,
1497
- root_cause: bucket.reason,
1498
- evidence: buildBucketEvidence(bucket),
1499
- bucket_confidence: Number(bucket.confidence.toFixed(2)),
1500
- root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
1501
- dominant: dominantBucket?.index === index,
1502
- secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== index + 1,
1503
- mini_diff: extractMiniDiff(args.input, bucket)
1504
- }));
1623
+ const dominantBucketHasConcreteAnchor = dominantBucket !== null && (readTargets.some((target) => target.bucket_index === dominantBucket.index + 1 && target.file.length > 0) || dominantBucket.bucket.representativeItems.some((item) => item.anchor_kind !== "none"));
1624
+ const smallConcreteSuite = args.analysis.failed + args.analysis.errors <= 2 && residuals.remainingErrors === 0 && residuals.remainingFailed === 0 && buckets.length === 1 && !hasUnknownBucket && dominantBucket !== null && dominantBucketHasConcreteAnchor;
1625
+ const dominantConfidenceThreshold = smallConcreteSuite ? 0.55 : 0.6;
1626
+ const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && hasConcreteCoverage && !hasUnknownBucket && (dominantBucket?.bucket.confidence ?? 0) >= dominantConfidenceThreshold;
1627
+ const rawNeeded = buckets.length === 0 ? !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure) : !diagnosisComplete && !hasUnknownBucket && buckets.every((bucket) => bucket.confidence < 0.7);
1628
+ const mainBuckets = buckets.map((bucket, index) => {
1629
+ const bucketIndex = index + 1;
1630
+ const label = labelForBucket(bucket);
1631
+ const readTarget = readTargets.find((target) => target.bucket_index === bucketIndex);
1632
+ return {
1633
+ bucket_index: bucketIndex,
1634
+ label,
1635
+ count: bucket.count,
1636
+ root_cause: bucket.reason,
1637
+ suspect_kind: deriveBucketSuspectKind({
1638
+ bucket,
1639
+ readTarget
1640
+ }),
1641
+ fix_hint: resolveBucketFixHint({
1642
+ bucket,
1643
+ bucketLabel: label
1644
+ }),
1645
+ evidence: buildBucketEvidence(bucket),
1646
+ bucket_confidence: Number(bucket.confidence.toFixed(2)),
1647
+ root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
1648
+ dominant: dominantBucket?.index === index,
1649
+ secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== bucketIndex,
1650
+ mini_diff: extractMiniDiff(args.input, bucket)
1651
+ };
1652
+ });
1505
1653
  const resolvedTests = unique(args.resolvedTests ?? []);
1506
1654
  const remainingTests = unique(
1507
1655
  args.remainingTests ?? unique([...args.analysis.visibleErrorLabels, ...args.analysis.visibleFailedLabels])
1508
1656
  );
1657
+ const primarySuspectKind = derivePrimarySuspectKind({
1658
+ mainBuckets,
1659
+ dominantBlockerBucketIndex
1660
+ });
1509
1661
  let nextBestAction;
1510
1662
  if (args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0) {
1511
1663
  nextBestAction = {
@@ -1551,6 +1703,8 @@ function buildTestStatusDiagnoseContract(args) {
1551
1703
  additional_source_read_likely_low_value: diagnosisComplete && !rawNeeded,
1552
1704
  read_raw_only_if: rawNeeded ? "you still need exact traceback lines after focused or verbose detail" : null,
1553
1705
  dominant_blocker_bucket_index: dominantBlockerBucketIndex,
1706
+ primary_suspect_kind: primarySuspectKind,
1707
+ confidence_reason: "Unknown or low-confidence buckets remain; one deeper sift pass is justified.",
1554
1708
  provider_used: false,
1555
1709
  provider_confidence: null,
1556
1710
  provider_failed: false,
@@ -1582,9 +1736,16 @@ function buildTestStatusDiagnoseContract(args) {
1582
1736
  })
1583
1737
  }
1584
1738
  };
1739
+ const resolvedDecision = effectiveDecision ?? deriveDecision(mergedContractWithoutDecision);
1740
+ const resolvedConfidenceReason = buildConfidenceReason({
1741
+ decision: resolvedDecision,
1742
+ mainBuckets,
1743
+ primarySuspectKind: mergedContractWithoutDecision.primary_suspect_kind
1744
+ });
1585
1745
  const contract = testStatusDiagnoseContractSchema.parse({
1586
1746
  ...mergedContractWithoutDecision,
1587
- decision: effectiveDecision ?? deriveDecision(mergedContractWithoutDecision)
1747
+ confidence_reason: resolvedConfidenceReason,
1748
+ decision: resolvedDecision
1588
1749
  });
1589
1750
  return {
1590
1751
  contract,
@@ -1656,6 +1817,27 @@ function buildTestStatusAnalysisContext(args) {
1656
1817
  var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
1657
1818
  var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
1658
1819
  var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
1820
+ var RESOURCE_DESTROY_HEADER_PATTERN = /^#\s+.+\bwill be (destroyed|deleted|replaced)\b/i;
1821
+ var DESTROY_ERROR_PATTERN = /(instance cannot be destroyed|prevent_destroy|downtime|data loss)/i;
1822
+ var ACTION_DESTROY_PATTERN = /^-\s+destroy$/i;
1823
+ var TSC_CODE_LABELS = {
1824
+ TS1002: "syntax error",
1825
+ TS1005: "syntax error",
1826
+ TS2304: "cannot find name",
1827
+ TS2307: "cannot find module",
1828
+ TS2322: "type mismatch",
1829
+ TS2339: "missing property on type",
1830
+ TS2345: "argument type mismatch",
1831
+ TS2554: "wrong argument count",
1832
+ TS2741: "missing required property",
1833
+ TS2769: "no matching overload",
1834
+ TS5083: "config file error",
1835
+ TS6133: "declared but unused",
1836
+ TS7006: "implicit any",
1837
+ TS18003: "no inputs were found",
1838
+ TS18046: "unknown type",
1839
+ TS18048: "possibly undefined"
1840
+ };
1659
1841
  function collectEvidence(input, matcher, limit = 3) {
1660
1842
  return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
1661
1843
  }
@@ -1669,11 +1851,179 @@ function inferPackage(line) {
1669
1851
  function inferRemediation(pkg) {
1670
1852
  return `Upgrade ${pkg} to a patched version.`;
1671
1853
  }
1854
+ function parseCompactAuditVulnerability(line) {
1855
+ if (/^Severity:\s*/i.test(line)) {
1856
+ return null;
1857
+ }
1858
+ if (!/\b(critical|high)\b/i.test(line)) {
1859
+ return null;
1860
+ }
1861
+ const pkg = inferPackage(line);
1862
+ if (!pkg) {
1863
+ return null;
1864
+ }
1865
+ return {
1866
+ package: pkg,
1867
+ severity: inferSeverity(line),
1868
+ remediation: inferRemediation(pkg)
1869
+ };
1870
+ }
1871
+ function inferAuditPackageHeader(line) {
1872
+ const trimmed = line.trim();
1873
+ if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.includes(":") || /^node_modules\//i.test(trimmed)) {
1874
+ return null;
1875
+ }
1876
+ const match = trimmed.match(/^([@a-z0-9._/-]+)(?:\s{2,}|\s+(?:[<>=~^*]|\d))/i);
1877
+ return match?.[1] ?? null;
1878
+ }
1879
+ function collectAuditCriticalVulnerabilities(input) {
1880
+ const lines = input.split("\n");
1881
+ const vulnerabilities = [];
1882
+ const seen = /* @__PURE__ */ new Set();
1883
+ const pushVulnerability = (pkg, severity) => {
1884
+ const key = `${pkg}:${severity}`;
1885
+ if (seen.has(key)) {
1886
+ return;
1887
+ }
1888
+ seen.add(key);
1889
+ vulnerabilities.push({
1890
+ package: pkg,
1891
+ severity,
1892
+ remediation: inferRemediation(pkg)
1893
+ });
1894
+ };
1895
+ for (let index = 0; index < lines.length; index += 1) {
1896
+ const line = lines[index].trim();
1897
+ if (!line) {
1898
+ continue;
1899
+ }
1900
+ const compact = parseCompactAuditVulnerability(line);
1901
+ if (compact) {
1902
+ pushVulnerability(compact.package, compact.severity);
1903
+ continue;
1904
+ }
1905
+ const pkg = inferAuditPackageHeader(line);
1906
+ if (!pkg) {
1907
+ continue;
1908
+ }
1909
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 5); cursor += 1) {
1910
+ const candidate = lines[cursor].trim();
1911
+ if (!candidate) {
1912
+ continue;
1913
+ }
1914
+ const severityMatch = candidate.match(/^Severity:\s*(critical|high)\b/i);
1915
+ if (severityMatch) {
1916
+ pushVulnerability(pkg, severityMatch[1].toLowerCase());
1917
+ break;
1918
+ }
1919
+ if (inferAuditPackageHeader(candidate) || parseCompactAuditVulnerability(candidate)) {
1920
+ break;
1921
+ }
1922
+ }
1923
+ }
1924
+ return vulnerabilities;
1925
+ }
1672
1926
  function getCount(input, label) {
1673
1927
  const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
1674
1928
  const lastMatch = matches.at(-1);
1675
1929
  return lastMatch ? Number(lastMatch[1]) : 0;
1676
1930
  }
1931
+ function collectInfraRiskEvidence(input) {
1932
+ const lines = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1933
+ const evidence = [];
1934
+ const seen = /* @__PURE__ */ new Set();
1935
+ const pushMatches = (matcher, options) => {
1936
+ let added = 0;
1937
+ for (const line of lines) {
1938
+ if (!matcher.test(line)) {
1939
+ continue;
1940
+ }
1941
+ if (options?.exclude?.test(line)) {
1942
+ continue;
1943
+ }
1944
+ if (seen.has(line)) {
1945
+ continue;
1946
+ }
1947
+ evidence.push(line);
1948
+ seen.add(line);
1949
+ added += 1;
1950
+ if (options?.limit && added >= options.limit) {
1951
+ return;
1952
+ }
1953
+ if (evidence.length >= (options?.maxEvidence ?? 4)) {
1954
+ return;
1955
+ }
1956
+ }
1957
+ };
1958
+ pushMatches(/Plan:/i, {
1959
+ exclude: ZERO_DESTRUCTIVE_SUMMARY_PATTERN,
1960
+ limit: 1
1961
+ });
1962
+ if (evidence.length < 4) {
1963
+ pushMatches(RESOURCE_DESTROY_HEADER_PATTERN, { limit: 2 });
1964
+ }
1965
+ if (evidence.length < 4) {
1966
+ pushMatches(DESTROY_ERROR_PATTERN, { limit: 1 });
1967
+ }
1968
+ if (evidence.length < 4) {
1969
+ pushMatches(ACTION_DESTROY_PATTERN, { limit: 1 });
1970
+ }
1971
+ if (evidence.length < 4) {
1972
+ pushMatches(RISK_LINE_PATTERN, {
1973
+ exclude: /->\s+null$|\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i,
1974
+ maxEvidence: 4
1975
+ });
1976
+ }
1977
+ return evidence.slice(0, 4);
1978
+ }
1979
+ function collectInfraDestroyTargets(input) {
1980
+ const targets = [];
1981
+ const seen = /* @__PURE__ */ new Set();
1982
+ for (const line of input.split("\n").map((entry) => entry.trim())) {
1983
+ const match = line.match(/^#\s+(.+?)\s+will be (destroyed|deleted|replaced)\b/i);
1984
+ const target = match?.[1]?.trim();
1985
+ if (!target || seen.has(target)) {
1986
+ continue;
1987
+ }
1988
+ seen.add(target);
1989
+ targets.push(target);
1990
+ }
1991
+ return targets;
1992
+ }
1993
+ function inferInfraDestroyCount(input, destroyTargets) {
1994
+ const matches = [
1995
+ ...input.matchAll(/\b(\d+)\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/gi)
1996
+ ];
1997
+ const lastMatch = matches.at(-1);
1998
+ return lastMatch ? Number(lastMatch[1]) : destroyTargets.length;
1999
+ }
2000
+ function collectInfraBlockers(input) {
2001
+ const lines = input.split("\n");
2002
+ const blockers = [];
2003
+ const seen = /* @__PURE__ */ new Set();
2004
+ for (let index = 0; index < lines.length; index += 1) {
2005
+ const trimmed = lines[index]?.trim();
2006
+ const errorMatch = trimmed?.match(/^(?:[│|]\s*)?Error:\s+(.+)$/);
2007
+ if (!errorMatch) {
2008
+ continue;
2009
+ }
2010
+ const message = errorMatch[1].trim();
2011
+ const nearby = lines.slice(index, index + 8).join("\n");
2012
+ const preventDestroyTarget = nearby.match(/Resource\s+([^\s]+)\s+has lifecycle\.prevent_destroy set/i)?.[1] ?? null;
2013
+ const type = preventDestroyTarget ? "prevent_destroy" : "destroy_blocked";
2014
+ const key = `${type}:${preventDestroyTarget ?? ""}:${message}`;
2015
+ if (seen.has(key)) {
2016
+ continue;
2017
+ }
2018
+ seen.add(key);
2019
+ blockers.push({
2020
+ type,
2021
+ target: preventDestroyTarget,
2022
+ message
2023
+ });
2024
+ }
2025
+ return blockers;
2026
+ }
1677
2027
  function detectTestRunner(input) {
1678
2028
  if (/^\s*Test Files?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /^\s*Tests?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /^\s*Snapshots?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(input)) {
1679
2029
  return "vitest";
@@ -1751,6 +2101,21 @@ function collectUniqueMatches(input, matcher, limit = 6) {
1751
2101
  }
1752
2102
  return values;
1753
2103
  }
2104
+ function compactDisplayFile(file) {
2105
+ const normalized = file.replace(/\\/g, "/").trim();
2106
+ if (!normalized) {
2107
+ return file;
2108
+ }
2109
+ const looksAbsolute = normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized);
2110
+ if (!looksAbsolute && normalized.length <= 60) {
2111
+ return normalized;
2112
+ }
2113
+ const basename = normalized.split("/").at(-1);
2114
+ return basename && basename.length > 0 ? basename : normalized;
2115
+ }
2116
+ function formatDisplayedFiles(files, limit = 3) {
2117
+ return [...new Set([...files].map((file) => file.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right)).slice(0, limit).map((file) => compactDisplayFile(file));
2118
+ }
1754
2119
  function emptyAnchor() {
1755
2120
  return {
1756
2121
  file: null,
@@ -2030,6 +2395,31 @@ function classifyFailureReason(line, options) {
2030
2395
  group: "permission or locked resource failures"
2031
2396
  };
2032
2397
  }
2398
+ const osDiskFullFailure = normalized.match(
2399
+ /(OSError:\s*\[Errno 28\][^$]*|No space left on device)/i
2400
+ );
2401
+ if (osDiskFullFailure) {
2402
+ return {
2403
+ reason: buildClassifiedReason(
2404
+ "configuration",
2405
+ `disk full (${buildExcerptDetail(
2406
+ osDiskFullFailure[1] ?? normalized,
2407
+ "No space left on device"
2408
+ )})`
2409
+ ),
2410
+ group: "test configuration failures"
2411
+ };
2412
+ }
2413
+ const osPermissionFailure = normalized.match(/OSError:\s*\[Errno 13\][^$]*/i);
2414
+ if (osPermissionFailure) {
2415
+ return {
2416
+ reason: buildClassifiedReason(
2417
+ "permission",
2418
+ buildExcerptDetail(osPermissionFailure[0] ?? normalized, "permission denied")
2419
+ ),
2420
+ group: "permission or locked resource failures"
2421
+ };
2422
+ }
2033
2423
  const xdistWorkerCrash = normalized.match(
2034
2424
  /(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
2035
2425
  );
@@ -2055,7 +2445,7 @@ function classifyFailureReason(line, options) {
2055
2445
  };
2056
2446
  }
2057
2447
  const networkFailure = normalized.match(
2058
- /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable)/i
2448
+ /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable|ConnectionResetError[^,;]*|BrokenPipeError[^,;]*|HTTPError:\s*[45]\d\d[^,;]*)/i
2059
2449
  );
2060
2450
  if (networkFailure) {
2061
2451
  return {
@@ -2066,6 +2456,15 @@ function classifyFailureReason(line, options) {
2066
2456
  group: "network dependency failures"
2067
2457
  };
2068
2458
  }
2459
+ const matcherAssertionFailure = normalized.match(
2460
+ /(expect\(received\)\.(?:toBe|toEqual|toStrictEqual|toMatchObject)\(expected\))/i
2461
+ );
2462
+ if (matcherAssertionFailure) {
2463
+ return {
2464
+ reason: `assertion failed: ${matcherAssertionFailure[1]}`.slice(0, 120),
2465
+ group: "assertion failures"
2466
+ };
2467
+ }
2069
2468
  const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
2070
2469
  if (relationMigration) {
2071
2470
  return {
@@ -2104,6 +2503,34 @@ function classifyFailureReason(line, options) {
2104
2503
  group: "memory exhaustion failures"
2105
2504
  };
2106
2505
  }
2506
+ const propertySetterOverrideFailure = normalized.match(
2507
+ /AttributeError:\s*(property ['"][^'"]+['"] of ['"][^'"]+['"] object has no setter|can't set attribute|readonly attribute|read-only attribute)/i
2508
+ );
2509
+ if (propertySetterOverrideFailure) {
2510
+ return {
2511
+ reason: buildClassifiedReason(
2512
+ "configuration",
2513
+ `invalid test setup override (${buildExcerptDetail(
2514
+ `AttributeError: ${propertySetterOverrideFailure[1] ?? normalized}`,
2515
+ "AttributeError: can't set attribute"
2516
+ )})`
2517
+ ),
2518
+ group: "test configuration failures"
2519
+ };
2520
+ }
2521
+ const setupOverrideFailure = normalized.match(/\b(AttributeError|TypeError):\s*(.+)$/i);
2522
+ if (setupOverrideFailure && /(monkeypatch|patch|fixture|settings|conftest)/i.test(normalized)) {
2523
+ return {
2524
+ reason: buildClassifiedReason(
2525
+ "configuration",
2526
+ `invalid test setup override (${buildExcerptDetail(
2527
+ `${setupOverrideFailure[1]}: ${setupOverrideFailure[2] ?? ""}`,
2528
+ `${setupOverrideFailure[1]}`
2529
+ )})`
2530
+ ),
2531
+ group: "test configuration failures"
2532
+ };
2533
+ }
2107
2534
  const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
2108
2535
  if (typeErrorFailure) {
2109
2536
  return {
@@ -3185,13 +3612,17 @@ function analyzeTestStatus(input) {
3185
3612
  const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
3186
3613
  const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
3187
3614
  const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
3615
+ const statusItems = collectInlineFailureItemsWithStatus(input);
3188
3616
  const visibleErrorItems = chooseStrongestStatusFailureItems([
3189
3617
  ...collectionItems.map((item) => ({
3190
3618
  ...item,
3191
3619
  status: "error"
3192
3620
  })),
3193
- ...collectInlineFailureItemsWithStatus(input).filter((item) => item.status === "error")
3621
+ ...statusItems.filter((item) => item.status === "error")
3194
3622
  ]);
3623
+ const visibleFailedItems = chooseStrongestStatusFailureItems(
3624
+ statusItems.filter((item) => item.status === "failed")
3625
+ );
3195
3626
  const labels = collectFailureLabels(input);
3196
3627
  const visibleErrorLabels = labels.filter((item) => item.status === "error").map((item) => item.label);
3197
3628
  const visibleFailedLabels = labels.filter((item) => item.status === "failed").map((item) => item.label);
@@ -3250,6 +3681,7 @@ function analyzeTestStatus(input) {
3250
3681
  visibleErrorLabels,
3251
3682
  visibleFailedLabels,
3252
3683
  visibleErrorItems,
3684
+ visibleFailedItems,
3253
3685
  buckets
3254
3686
  };
3255
3687
  }
@@ -3310,20 +3742,18 @@ function testStatusHeuristic(input, detail = "standard") {
3310
3742
  return null;
3311
3743
  }
3312
3744
  function auditCriticalHeuristic(input) {
3313
- const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
3314
- if (!/\b(critical|high)\b/i.test(line)) {
3315
- return null;
3316
- }
3317
- const pkg = inferPackage(line);
3318
- if (!pkg) {
3319
- return null;
3320
- }
3321
- return {
3322
- package: pkg,
3323
- severity: inferSeverity(line),
3324
- remediation: inferRemediation(pkg)
3325
- };
3326
- }).filter((item) => item !== null);
3745
+ if (/\bfound\s+0\s+vulnerabilities\b/i.test(input) || /\b0\s+vulnerabilities\b/i.test(input)) {
3746
+ return JSON.stringify(
3747
+ {
3748
+ status: "ok",
3749
+ vulnerabilities: [],
3750
+ summary: "No high or critical vulnerabilities found in the provided input."
3751
+ },
3752
+ null,
3753
+ 2
3754
+ );
3755
+ }
3756
+ const vulnerabilities = collectAuditCriticalVulnerabilities(input);
3327
3757
  if (vulnerabilities.length === 0) {
3328
3758
  return null;
3329
3759
  }
@@ -3339,16 +3769,19 @@ function auditCriticalHeuristic(input) {
3339
3769
  );
3340
3770
  }
3341
3771
  function infraRiskHeuristic(input) {
3772
+ const destroyTargets = collectInfraDestroyTargets(input);
3773
+ const blockers = collectInfraBlockers(input);
3342
3774
  const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
3343
- const riskEvidence = input.split("\n").map((line) => line.trim()).filter(
3344
- (line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
3345
- ).slice(0, 3);
3775
+ const riskEvidence = collectInfraRiskEvidence(input);
3346
3776
  if (riskEvidence.length > 0) {
3347
3777
  return JSON.stringify(
3348
3778
  {
3349
3779
  verdict: "fail",
3350
3780
  reason: "Destructive or clearly risky infrastructure change signals are present.",
3351
- evidence: riskEvidence
3781
+ evidence: riskEvidence,
3782
+ destroy_count: inferInfraDestroyCount(input, destroyTargets),
3783
+ destroy_targets: destroyTargets,
3784
+ blockers
3352
3785
  },
3353
3786
  null,
3354
3787
  2
@@ -3359,7 +3792,10 @@ function infraRiskHeuristic(input) {
3359
3792
  {
3360
3793
  verdict: "pass",
3361
3794
  reason: "The provided input explicitly indicates zero destructive changes.",
3362
- evidence: zeroDestructiveEvidence
3795
+ evidence: zeroDestructiveEvidence,
3796
+ destroy_count: 0,
3797
+ destroy_targets: [],
3798
+ blockers: []
3363
3799
  },
3364
3800
  null,
3365
3801
  2
@@ -3371,7 +3807,10 @@ function infraRiskHeuristic(input) {
3371
3807
  {
3372
3808
  verdict: "pass",
3373
3809
  reason: "The provided input explicitly indicates no risky infrastructure changes.",
3374
- evidence: safeEvidence
3810
+ evidence: safeEvidence,
3811
+ destroy_count: 0,
3812
+ destroy_targets: [],
3813
+ blockers: []
3375
3814
  },
3376
3815
  null,
3377
3816
  2
@@ -3379,6 +3818,551 @@ function infraRiskHeuristic(input) {
3379
3818
  }
3380
3819
  return null;
3381
3820
  }
3821
+ function parseTscErrors(input) {
3822
+ const diagnostics = [];
3823
+ for (const rawLine of input.split("\n")) {
3824
+ const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").trimEnd();
3825
+ if (!line.trim()) {
3826
+ continue;
3827
+ }
3828
+ let match = line.match(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
3829
+ if (match) {
3830
+ diagnostics.push({
3831
+ file: match[1].replace(/\\/g, "/").trim(),
3832
+ line: Number(match[2]),
3833
+ column: Number(match[3]),
3834
+ code: match[4],
3835
+ message: match[5].trim()
3836
+ });
3837
+ continue;
3838
+ }
3839
+ match = line.match(/^(.+):(\d+):(\d+)\s+-\s+error\s+(TS\d+):\s+(.+)$/);
3840
+ if (match) {
3841
+ diagnostics.push({
3842
+ file: match[1].replace(/\\/g, "/").trim(),
3843
+ line: Number(match[2]),
3844
+ column: Number(match[3]),
3845
+ code: match[4],
3846
+ message: match[5].trim()
3847
+ });
3848
+ continue;
3849
+ }
3850
+ match = line.match(/^\s*error\s+(TS\d+):\s+(.+)$/);
3851
+ if (match) {
3852
+ diagnostics.push({
3853
+ file: null,
3854
+ line: null,
3855
+ column: null,
3856
+ code: match[1],
3857
+ message: match[2].trim()
3858
+ });
3859
+ }
3860
+ }
3861
+ return diagnostics;
3862
+ }
3863
+ function extractTscSummary(input) {
3864
+ const matches = [
3865
+ ...input.matchAll(/\bFound\s+(\d+)\s+errors?\b(?:\s+in\s+(\d+)\s+files?)?\.?/gi)
3866
+ ];
3867
+ const summary = matches.at(-1);
3868
+ if (!summary) {
3869
+ return null;
3870
+ }
3871
+ return {
3872
+ errorCount: Number(summary[1]),
3873
+ fileCount: summary[2] ? Number(summary[2]) : null
3874
+ };
3875
+ }
3876
+ function formatTscGroup(args) {
3877
+ const label = TSC_CODE_LABELS[args.code];
3878
+ const displayFiles = formatDisplayedFiles(args.files);
3879
+ let line = `- ${args.code}`;
3880
+ if (label) {
3881
+ line += ` (${label})`;
3882
+ }
3883
+ line += `: ${formatCount2(args.count, "occurrence")}`;
3884
+ if (displayFiles.length > 0) {
3885
+ line += ` across ${displayFiles.join(", ")}`;
3886
+ }
3887
+ return `${line}.`;
3888
+ }
3889
+ function typecheckSummaryHeuristic(input) {
3890
+ if (input.trim().length === 0) {
3891
+ return null;
3892
+ }
3893
+ const diagnostics = parseTscErrors(input);
3894
+ const summary = extractTscSummary(input);
3895
+ const hasTscSignal = diagnostics.length > 0 || summary !== null || /\berror\s+TS\d+:/m.test(input);
3896
+ if (!hasTscSignal) {
3897
+ return null;
3898
+ }
3899
+ if (summary?.errorCount === 0) {
3900
+ return "No type errors.";
3901
+ }
3902
+ if (diagnostics.length === 0 && summary === null) {
3903
+ return null;
3904
+ }
3905
+ const errorCount = summary?.errorCount ?? diagnostics.length;
3906
+ const allFiles = new Set(
3907
+ diagnostics.map((diagnostic) => diagnostic.file).filter((file) => Boolean(file))
3908
+ );
3909
+ const fileCount = summary?.fileCount ?? (allFiles.size > 0 ? allFiles.size : null);
3910
+ const groups = /* @__PURE__ */ new Map();
3911
+ for (const diagnostic of diagnostics) {
3912
+ const group = groups.get(diagnostic.code) ?? {
3913
+ count: 0,
3914
+ files: /* @__PURE__ */ new Set()
3915
+ };
3916
+ group.count += 1;
3917
+ if (diagnostic.file) {
3918
+ group.files.add(diagnostic.file);
3919
+ }
3920
+ groups.set(diagnostic.code, group);
3921
+ }
3922
+ const bullets = [
3923
+ `- Typecheck failed: ${formatCount2(errorCount, "error")}${fileCount ? ` in ${formatCount2(fileCount, "file")}` : ""}.`
3924
+ ];
3925
+ const sortedGroups = [...groups.entries()].map(([code, group]) => ({
3926
+ code,
3927
+ count: group.count,
3928
+ files: group.files
3929
+ })).sort((left, right) => right.count - left.count || left.code.localeCompare(right.code));
3930
+ for (const group of sortedGroups.slice(0, 3)) {
3931
+ bullets.push(formatTscGroup(group));
3932
+ }
3933
+ if (sortedGroups.length > 3) {
3934
+ const overflowFiles = /* @__PURE__ */ new Set();
3935
+ for (const group of sortedGroups.slice(3)) {
3936
+ for (const file of group.files) {
3937
+ overflowFiles.add(file);
3938
+ }
3939
+ }
3940
+ let overflow = `- ${formatCount2(sortedGroups.length - 3, "more error code")}`;
3941
+ if (overflowFiles.size > 0) {
3942
+ overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
3943
+ }
3944
+ bullets.push(`${overflow}.`);
3945
+ }
3946
+ return bullets.join("\n");
3947
+ }
3948
+ function looksLikeEslintFileHeader(line) {
3949
+ if (line.trim().length === 0 || line.trim() !== line) {
3950
+ return false;
3951
+ }
3952
+ if (/^\s*[✖×x]\s+\d+\s+problems?\b/i.test(line) || /potentially\s+fixable/i.test(line) || /^\d+\s+problems?\b/i.test(line)) {
3953
+ return false;
3954
+ }
3955
+ const normalized = line.replace(/\\/g, "/");
3956
+ const pathLike = normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../") || /^[A-Za-z]:\//.test(normalized) || /^[A-Za-z0-9_.-]+\//.test(normalized);
3957
+ return pathLike && /\.[A-Za-z0-9]+$/.test(normalized);
3958
+ }
3959
+ function normalizeEslintRule(rule, message) {
3960
+ if (rule && rule.trim().length > 0) {
3961
+ return rule.trim();
3962
+ }
3963
+ if (/parsing error/i.test(message)) {
3964
+ return "parsing error";
3965
+ }
3966
+ if (/fatal/i.test(message)) {
3967
+ return "fatal error";
3968
+ }
3969
+ return "unclassified lint error";
3970
+ }
3971
+ function parseEslintStylish(input) {
3972
+ const violations = [];
3973
+ let currentFile = null;
3974
+ for (const rawLine of input.split("\n")) {
3975
+ const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").replace(/\r$/, "");
3976
+ if (looksLikeEslintFileHeader(line.trim())) {
3977
+ currentFile = line.trim().replace(/\\/g, "/");
3978
+ continue;
3979
+ }
3980
+ let match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)\s*$/);
3981
+ if (match) {
3982
+ violations.push({
3983
+ file: currentFile ?? "(unknown file)",
3984
+ line: Number(match[1]),
3985
+ column: Number(match[2]),
3986
+ severity: match[3],
3987
+ message: match[4].trim(),
3988
+ rule: normalizeEslintRule(match[5], match[4])
3989
+ });
3990
+ continue;
3991
+ }
3992
+ match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s*$/);
3993
+ if (match) {
3994
+ violations.push({
3995
+ file: currentFile ?? "(unknown file)",
3996
+ line: Number(match[1]),
3997
+ column: Number(match[2]),
3998
+ severity: match[3],
3999
+ message: match[4].trim(),
4000
+ rule: normalizeEslintRule(null, match[4])
4001
+ });
4002
+ }
4003
+ }
4004
+ return violations;
4005
+ }
4006
+ function extractEslintSummary(input) {
4007
+ const summaryMatches = [
4008
+ ...input.matchAll(
4009
+ /^\s*[✖×x]?\s*(\d+)\s+problems?\s+\((\d+)\s+errors?,\s+(\d+)\s+warnings?\)/gim
4010
+ )
4011
+ ];
4012
+ const summary = summaryMatches.at(-1);
4013
+ if (!summary) {
4014
+ return null;
4015
+ }
4016
+ const fixableMatch = input.match(
4017
+ /(\d+)\s+errors?\s+and\s+(\d+)\s+warnings?\s+(?:are|is)\s+potentially\s+fixable/i
4018
+ );
4019
+ return {
4020
+ problems: Number(summary[1]),
4021
+ errors: Number(summary[2]),
4022
+ warnings: Number(summary[3]),
4023
+ fixableProblems: fixableMatch ? Number(fixableMatch[1]) + Number(fixableMatch[2]) : null
4024
+ };
4025
+ }
4026
+ function formatLintGroup(args) {
4027
+ const totalErrors = args.errors;
4028
+ const totalWarnings = args.warnings;
4029
+ const displayFiles = formatDisplayedFiles(args.files);
4030
+ let detail = "";
4031
+ if (totalErrors > 0 && totalWarnings > 0) {
4032
+ detail = `${formatCount2(totalErrors, "error")}, ${formatCount2(totalWarnings, "warning")}`;
4033
+ } else if (totalErrors > 0) {
4034
+ detail = formatCount2(totalErrors, "error");
4035
+ } else {
4036
+ detail = formatCount2(totalWarnings, "warning");
4037
+ }
4038
+ let line = `- ${args.rule}: ${detail}`;
4039
+ if (displayFiles.length > 0) {
4040
+ line += ` across ${displayFiles.join(", ")}`;
4041
+ }
4042
+ return `${line}.`;
4043
+ }
4044
+ function lintFailuresHeuristic(input) {
4045
+ const trimmed = input.trim();
4046
+ if (trimmed.length === 0 || trimmed.startsWith("[") || trimmed.startsWith("{")) {
4047
+ return null;
4048
+ }
4049
+ const summary = extractEslintSummary(input);
4050
+ const violations = parseEslintStylish(input);
4051
+ if (summary === null && violations.length === 0) {
4052
+ return null;
4053
+ }
4054
+ if (summary?.problems === 0) {
4055
+ return "No lint failures.";
4056
+ }
4057
+ const problems = summary?.problems ?? violations.length;
4058
+ const errors = summary?.errors ?? countPattern(input, /^\s*\d+:\d+\s+error\b/gm);
4059
+ const warnings = summary?.warnings ?? countPattern(input, /^\s*\d+:\d+\s+warning\b/gm);
4060
+ const bullets = [];
4061
+ if (errors > 0) {
4062
+ let headline = `- Lint failed: ${formatCount2(problems, "problem")} (${formatCount2(errors, "error")}, ${formatCount2(warnings, "warning")}).`;
4063
+ if ((summary?.fixableProblems ?? 0) > 0) {
4064
+ headline += ` ${formatCount2(summary.fixableProblems, "problem")} potentially fixable with --fix.`;
4065
+ }
4066
+ bullets.push(headline);
4067
+ } else {
4068
+ bullets.push(`- No lint errors visible: ${formatCount2(warnings, "warning")}.`);
4069
+ }
4070
+ const groups = /* @__PURE__ */ new Map();
4071
+ for (const violation of violations) {
4072
+ const group = groups.get(violation.rule) ?? {
4073
+ errors: 0,
4074
+ warnings: 0,
4075
+ files: /* @__PURE__ */ new Set()
4076
+ };
4077
+ if (violation.severity === "error") {
4078
+ group.errors += 1;
4079
+ } else {
4080
+ group.warnings += 1;
4081
+ }
4082
+ group.files.add(violation.file);
4083
+ groups.set(violation.rule, group);
4084
+ }
4085
+ const sortedGroups = [...groups.entries()].map(([rule, group]) => ({
4086
+ rule,
4087
+ errors: group.errors,
4088
+ warnings: group.warnings,
4089
+ total: group.errors + group.warnings,
4090
+ files: group.files
4091
+ })).sort((left, right) => {
4092
+ const leftHasErrors = left.errors > 0 ? 1 : 0;
4093
+ const rightHasErrors = right.errors > 0 ? 1 : 0;
4094
+ return rightHasErrors - leftHasErrors || right.total - left.total || left.rule.localeCompare(right.rule);
4095
+ });
4096
+ for (const group of sortedGroups.slice(0, 3)) {
4097
+ bullets.push(formatLintGroup(group));
4098
+ }
4099
+ if (sortedGroups.length > 3) {
4100
+ const overflowFiles = /* @__PURE__ */ new Set();
4101
+ for (const group of sortedGroups.slice(3)) {
4102
+ for (const file of group.files) {
4103
+ overflowFiles.add(file);
4104
+ }
4105
+ }
4106
+ let overflow = `- ${formatCount2(sortedGroups.length - 3, "more rule")}`;
4107
+ if (overflowFiles.size > 0) {
4108
+ overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
4109
+ }
4110
+ bullets.push(`${overflow}.`);
4111
+ }
4112
+ return bullets.join("\n");
4113
+ }
4114
+ function stripAnsiText(input) {
4115
+ return input.replace(/\u001b\[[0-9;]*m/g, "");
4116
+ }
4117
+ function normalizeBuildPath(file) {
4118
+ return file.replace(/\\/g, "/").replace(/^\.\//, "").trim();
4119
+ }
4120
+ function trimTrailingSentencePunctuation(input) {
4121
+ return input.replace(/[.:]+$/, "").trim();
4122
+ }
4123
+ function containsKnownBuildFailureSignal(input) {
4124
+ return /^ERROR in /m.test(input) || /^(?:[✘✗]\s*)?\[ERROR\]\s+/m.test(input) || /^error(?:\[E\d+\])?:\s+/m.test(input) || /^.+?\.go:\d+:\d+:\s+\S+/m.test(input) || /^.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm):\d+:\d+:\s*error:\s+/m.test(input) || /\berror\s+TS\d+:/m.test(input) || /^\s*npm ERR!/m.test(input) || /\bERR_PNPM_/m.test(input) || /^\s*error Command failed/m.test(input);
4125
+ }
4126
+ function detectExplicitBuildSuccess(input) {
4127
+ if (containsKnownBuildFailureSignal(input)) {
4128
+ return false;
4129
+ }
4130
+ return /\bcompiled successfully\b/i.test(input) || /^\s*Build succeeded\.?\s*$/im.test(input) || /\bcompiled with 0 errors?\b/i.test(input);
4131
+ }
4132
+ function inferBuildFailureCategory(message) {
4133
+ if (/module not found|can't resolve|could not resolve|cannot find module|no required module provides package/i.test(
4134
+ message
4135
+ )) {
4136
+ return "module-resolution";
4137
+ }
4138
+ if (/no matching export|does not provide an export named|missing export/i.test(message)) {
4139
+ return "missing-export";
4140
+ }
4141
+ if (/cannot find name|cannot find value|not found in this scope|undefined:|undeclared identifier/i.test(
4142
+ message
4143
+ )) {
4144
+ return "undefined-identifier";
4145
+ }
4146
+ if (/syntax error|unexpected token|expected ['"`;)]|expected .* after expression/i.test(message)) {
4147
+ return "syntax";
4148
+ }
4149
+ if (/\bTS\d+\b/.test(message) || /type .* is not assignable|type error|no matching overload/i.test(message)) {
4150
+ return "type";
4151
+ }
4152
+ return "generic";
4153
+ }
4154
+ function buildFailureSuggestion(category) {
4155
+ switch (category) {
4156
+ case "module-resolution":
4157
+ return "Install the missing package or fix the import path.";
4158
+ case "missing-export":
4159
+ return "Check the export name in the source module.";
4160
+ case "undefined-identifier":
4161
+ return "Define or import the missing identifier.";
4162
+ case "syntax":
4163
+ return "Fix the syntax error at the indicated location.";
4164
+ case "type":
4165
+ return "Fix the type error at the indicated location.";
4166
+ case "wrapper":
4167
+ return "Check the underlying build tool output above.";
4168
+ default:
4169
+ return "Fix the first reported error and rebuild.";
4170
+ }
4171
+ }
4172
+ function formatBuildFailureOutput(match) {
4173
+ const message = trimTrailingSentencePunctuation(match.message);
4174
+ const suggestion = buildFailureSuggestion(match.category);
4175
+ const displayFile = match.file ? compactDisplayFile(match.file) : null;
4176
+ if (displayFile && match.line !== null) {
4177
+ return `Build failed: ${message} in ${displayFile}:${match.line}. Fix: ${suggestion}`;
4178
+ }
4179
+ if (displayFile) {
4180
+ return `Build failed: ${message} in ${displayFile}. Fix: ${suggestion}`;
4181
+ }
4182
+ return `Build failed: ${message}. Fix: ${suggestion}`;
4183
+ }
4184
+ function extractWebpackBuildFailure(input) {
4185
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
4186
+ for (let index = 0; index < lines.length; index += 1) {
4187
+ const match = lines[index]?.match(/^ERROR in (.+?)(?:\s+(\d+):(\d+))?$/);
4188
+ if (!match) {
4189
+ continue;
4190
+ }
4191
+ const candidates = [];
4192
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
4193
+ const candidate = lines[cursor]?.trim();
4194
+ if (!candidate) {
4195
+ continue;
4196
+ }
4197
+ if (/^ERROR in /.test(candidate) || /compiled with \d+ errors?/i.test(candidate)) {
4198
+ break;
4199
+ }
4200
+ if (/^(?:>|\|)|^\d+\s+\|/.test(candidate)) {
4201
+ continue;
4202
+ }
4203
+ candidates.push(candidate);
4204
+ }
4205
+ let message = "Compilation error";
4206
+ if (candidates.length > 0) {
4207
+ const preferred = candidates.find(
4208
+ (candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate) && inferBuildFailureCategory(candidate) !== "generic"
4209
+ ) ?? candidates.find(
4210
+ (candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate)
4211
+ ) ?? candidates[0];
4212
+ message = preferred ?? message;
4213
+ }
4214
+ return {
4215
+ message,
4216
+ file: normalizeBuildPath(match[1]),
4217
+ line: match[2] ? Number(match[2]) : null,
4218
+ column: match[3] ? Number(match[3]) : null,
4219
+ category: inferBuildFailureCategory(message)
4220
+ };
4221
+ }
4222
+ return null;
4223
+ }
4224
+ function extractViteImportAnalysisBuildFailure(input) {
4225
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trim());
4226
+ for (const line of lines) {
4227
+ const match = line.match(
4228
+ /^\[plugin:vite:import-analysis\]\s+Failed to resolve import\s+"([^"]+)"\s+from\s+"([^"]+)"/i
4229
+ );
4230
+ if (!match) {
4231
+ continue;
4232
+ }
4233
+ return {
4234
+ message: `Failed to resolve import "${match[1]}"`,
4235
+ file: normalizeBuildPath(match[2]),
4236
+ line: null,
4237
+ column: null,
4238
+ category: "module-resolution"
4239
+ };
4240
+ }
4241
+ return null;
4242
+ }
4243
+ function extractEsbuildBuildFailure(input) {
4244
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
4245
+ for (let index = 0; index < lines.length; index += 1) {
4246
+ const match = lines[index]?.match(/^(?:[✘✗]\s*)?\[ERROR\]\s*(.+)$/);
4247
+ if (!match) {
4248
+ continue;
4249
+ }
4250
+ const message = match[1].replace(/^\[vite\]\s*/i, "").trim();
4251
+ let file = null;
4252
+ let line = null;
4253
+ let column = null;
4254
+ for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
4255
+ const locationMatch = lines[cursor]?.trim().match(/^(.+?):(\d+):(\d+):$/);
4256
+ if (!locationMatch) {
4257
+ continue;
4258
+ }
4259
+ file = normalizeBuildPath(locationMatch[1]);
4260
+ line = Number(locationMatch[2]);
4261
+ column = Number(locationMatch[3]);
4262
+ break;
4263
+ }
4264
+ return {
4265
+ message,
4266
+ file,
4267
+ line,
4268
+ column,
4269
+ category: inferBuildFailureCategory(message)
4270
+ };
4271
+ }
4272
+ return null;
4273
+ }
4274
+ function extractCargoBuildFailure(input) {
4275
+ if (!/^error(?:\[E\d+\])?:\s+/m.test(input) || !(/^\s*-->\s+/m.test(input) || /could not compile/i.test(input))) {
4276
+ return null;
4277
+ }
4278
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
4279
+ for (let index = 0; index < lines.length; index += 1) {
4280
+ const match = lines[index]?.match(/^error(?:\[(E\d+)\])?:\s+(.+)$/);
4281
+ if (!match) {
4282
+ continue;
4283
+ }
4284
+ const code = match[1];
4285
+ const locationMatch = lines.slice(index + 1, index + 7).join("\n").match(/^\s*-->\s+(.+?):(\d+):(\d+)/m);
4286
+ return {
4287
+ message: code ? `${code}: ${match[2].trim()}` : match[2].trim(),
4288
+ file: locationMatch ? normalizeBuildPath(locationMatch[1]) : null,
4289
+ line: locationMatch ? Number(locationMatch[2]) : null,
4290
+ column: locationMatch ? Number(locationMatch[3]) : null,
4291
+ category: inferBuildFailureCategory(match[2])
4292
+ };
4293
+ }
4294
+ return null;
4295
+ }
4296
+ function extractCompilerStyleBuildFailure(input) {
4297
+ const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
4298
+ for (const rawLine of lines) {
4299
+ let match = rawLine.match(
4300
+ /^(.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm)):([0-9]+):([0-9]+):\s*error:\s+(.+)$/
4301
+ );
4302
+ if (match) {
4303
+ return {
4304
+ message: match[4].trim(),
4305
+ file: normalizeBuildPath(match[1]),
4306
+ line: Number(match[2]),
4307
+ column: Number(match[3]),
4308
+ category: inferBuildFailureCategory(match[4])
4309
+ };
4310
+ }
4311
+ match = rawLine.match(/^(.+?\.go):([0-9]+):([0-9]+):\s+(.+)$/);
4312
+ if (match && !/^\s*warning:/i.test(match[4])) {
4313
+ return {
4314
+ message: match[4].trim(),
4315
+ file: normalizeBuildPath(match[1]),
4316
+ line: Number(match[2]),
4317
+ column: Number(match[3]),
4318
+ category: inferBuildFailureCategory(match[4])
4319
+ };
4320
+ }
4321
+ }
4322
+ return null;
4323
+ }
4324
+ function extractTscBuildFailure(input) {
4325
+ const diagnostics = parseTscErrors(input);
4326
+ const first = diagnostics[0];
4327
+ if (!first) {
4328
+ return null;
4329
+ }
4330
+ return {
4331
+ message: `${first.code}: ${first.message}`,
4332
+ file: first.file,
4333
+ line: first.line,
4334
+ column: first.column,
4335
+ category: inferBuildFailureCategory(`${first.code}: ${first.message}`)
4336
+ };
4337
+ }
4338
+ function extractWrapperBuildFailure(input) {
4339
+ if (!/^\s*npm ERR!|\bERR_PNPM_|^\s*error Command failed/m.test(input)) {
4340
+ return null;
4341
+ }
4342
+ const npmCommandMatch = input.match(/^\s*npm ERR!\s+.*?\bbuild:\s+`([^`]+)`/m);
4343
+ const genericCommandMatch = input.match(/^\s*.+?\s+build:\s+`([^`]+)`/m);
4344
+ const command = npmCommandMatch?.[1] ?? genericCommandMatch?.[1] ?? null;
4345
+ return {
4346
+ message: command ? `build script \`${command}\` failed` : "the build script failed",
4347
+ file: null,
4348
+ line: null,
4349
+ column: null,
4350
+ category: "wrapper"
4351
+ };
4352
+ }
4353
+ function buildFailureHeuristic(input) {
4354
+ if (input.trim().length === 0) {
4355
+ return null;
4356
+ }
4357
+ if (detectExplicitBuildSuccess(input)) {
4358
+ return "Build succeeded.";
4359
+ }
4360
+ const match = extractViteImportAnalysisBuildFailure(input) ?? extractWebpackBuildFailure(input) ?? extractEsbuildBuildFailure(input) ?? extractCargoBuildFailure(input) ?? extractCompilerStyleBuildFailure(input) ?? extractTscBuildFailure(input) ?? extractWrapperBuildFailure(input);
4361
+ if (!match) {
4362
+ return null;
4363
+ }
4364
+ return formatBuildFailureOutput(match);
4365
+ }
3382
4366
  function applyHeuristicPolicy(policyName, input, detail) {
3383
4367
  if (!policyName) {
3384
4368
  return null;
@@ -3392,6 +4376,15 @@ function applyHeuristicPolicy(policyName, input, detail) {
3392
4376
  if (policyName === "test-status") {
3393
4377
  return testStatusHeuristic(input, detail);
3394
4378
  }
4379
+ if (policyName === "typecheck-summary") {
4380
+ return typecheckSummaryHeuristic(input);
4381
+ }
4382
+ if (policyName === "lint-failures") {
4383
+ return lintFailuresHeuristic(input);
4384
+ }
4385
+ if (policyName === "build-failure") {
4386
+ return buildFailureHeuristic(input);
4387
+ }
3395
4388
  return null;
3396
4389
  }
3397
4390
 
@@ -3414,8 +4407,8 @@ function buildInsufficientSignalOutput(input) {
3414
4407
  } else {
3415
4408
  hint = "Hint: the captured output did not contain a clear answer for this preset.";
3416
4409
  }
3417
- return `${INSUFFICIENT_SIGNAL_TEXT}
3418
- ${hint}`;
4410
+ const presetSuggestion = input.recognizedRunner && input.recognizedRunner !== "unknown" && input.presetName !== "test-status" ? `Hint: captured output looks like ${input.recognizedRunner} test output; try --preset test-status.` : null;
4411
+ return [INSUFFICIENT_SIGNAL_TEXT, hint, presetSuggestion].filter((value) => Boolean(value)).join("\n");
3419
4412
  }
3420
4413
 
3421
4414
  // src/core/run.ts
@@ -4079,6 +5072,138 @@ function escapeRegExp(value) {
4079
5072
  function unique2(values) {
4080
5073
  return [...new Set(values)];
4081
5074
  }
5075
+ var genericBucketSearchTerms = /* @__PURE__ */ new Set([
5076
+ "runtimeerror",
5077
+ "typeerror",
5078
+ "error",
5079
+ "exception",
5080
+ "failed",
5081
+ "failure",
5082
+ "visible failure",
5083
+ "failing tests",
5084
+ "setup failures",
5085
+ "runtime failure",
5086
+ "assertion failed",
5087
+ "network",
5088
+ "permission",
5089
+ "configuration"
5090
+ ]);
5091
+ function normalizeSearchTerm(value) {
5092
+ return value.replace(/^['"`]+|['"`]+$/g, "").trim();
5093
+ }
5094
+ function isHighSignalSearchTerm(term) {
5095
+ const normalized = normalizeSearchTerm(term);
5096
+ if (normalized.length < 4) {
5097
+ return false;
5098
+ }
5099
+ const lower = normalized.toLowerCase();
5100
+ if (genericBucketSearchTerms.has(lower)) {
5101
+ return false;
5102
+ }
5103
+ if (/^(runtime|type|assertion|network|permission|configuration)\b/i.test(normalized)) {
5104
+ return false;
5105
+ }
5106
+ return true;
5107
+ }
5108
+ function scoreSearchTerm(term) {
5109
+ const normalized = normalizeSearchTerm(term);
5110
+ let score = normalized.length;
5111
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(normalized)) {
5112
+ score += 80;
5113
+ }
5114
+ if (/^TS\d+$/.test(normalized)) {
5115
+ score += 70;
5116
+ }
5117
+ if (/^[45]\d\d\b/.test(normalized) || /\bHTTPError:\s*[45]\d\d\b/i.test(normalized)) {
5118
+ score += 60;
5119
+ }
5120
+ if (normalized.includes("/") || normalized.includes("\\")) {
5121
+ score += 50;
5122
+ }
5123
+ if (/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/.test(normalized)) {
5124
+ score += 40;
5125
+ }
5126
+ if (/['"`]/.test(term)) {
5127
+ score += 30;
5128
+ }
5129
+ if (normalized.includes("::")) {
5130
+ score += 25;
5131
+ }
5132
+ return score;
5133
+ }
5134
+ function collectCandidateSearchTerms(value) {
5135
+ const candidates = [];
5136
+ const normalized = value.trim();
5137
+ if (!normalized) {
5138
+ return candidates;
5139
+ }
5140
+ for (const match of normalized.matchAll(/['"`]([^'"`]{4,})['"`]/g)) {
5141
+ candidates.push(match[1]);
5142
+ }
5143
+ for (const match of normalized.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) {
5144
+ candidates.push(match[0]);
5145
+ }
5146
+ for (const match of normalized.matchAll(/\bTS\d+\b/g)) {
5147
+ candidates.push(match[0]);
5148
+ }
5149
+ for (const match of normalized.matchAll(/\bHTTPError:\s*[45]\d\d\b/gi)) {
5150
+ candidates.push(match[0]);
5151
+ }
5152
+ for (const match of normalized.matchAll(/\/[A-Za-z0-9_./:{}-]{4,}/g)) {
5153
+ candidates.push(match[0]);
5154
+ }
5155
+ for (const match of normalized.matchAll(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g)) {
5156
+ candidates.push(match[0]);
5157
+ }
5158
+ for (const match of normalized.matchAll(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/g)) {
5159
+ candidates.push(match[0]);
5160
+ }
5161
+ const detail = normalized.split(":").slice(1).join(":").trim();
5162
+ if (detail.length >= 8) {
5163
+ candidates.push(detail);
5164
+ }
5165
+ return candidates;
5166
+ }
5167
+ function extractBucketSearchTerms(args) {
5168
+ const sources = [
5169
+ args.bucket.root_cause,
5170
+ ...args.bucket.evidence,
5171
+ ...args.readTargets.filter((target) => target.bucket_index === args.bucket.bucket_index).flatMap((target) => [target.context_hint.search_hint ?? "", target.file])
5172
+ ];
5173
+ const prioritized = unique2(
5174
+ sources.flatMap((value) => collectCandidateSearchTerms(value)).filter(isHighSignalSearchTerm)
5175
+ ).sort((left, right) => {
5176
+ const delta = scoreSearchTerm(right) - scoreSearchTerm(left);
5177
+ if (delta !== 0) {
5178
+ return delta;
5179
+ }
5180
+ return left.localeCompare(right);
5181
+ });
5182
+ if (prioritized.length > 0) {
5183
+ return prioritized.slice(0, 6);
5184
+ }
5185
+ const fallbackTerms = unique2(
5186
+ [...args.bucket.evidence, args.bucket.root_cause].flatMap((value) => value.split(/->|:/).map((part) => normalizeSearchTerm(part))).filter(isHighSignalSearchTerm)
5187
+ );
5188
+ return fallbackTerms.slice(0, 4);
5189
+ }
5190
+ function clusterIndexes(indexes, maxGap = 12) {
5191
+ if (indexes.length === 0) {
5192
+ return [];
5193
+ }
5194
+ const clusters = [];
5195
+ let currentCluster = [indexes[0]];
5196
+ for (const index of indexes.slice(1)) {
5197
+ if (index - currentCluster[currentCluster.length - 1] <= maxGap) {
5198
+ currentCluster.push(index);
5199
+ continue;
5200
+ }
5201
+ clusters.push(currentCluster);
5202
+ currentCluster = [index];
5203
+ }
5204
+ clusters.push(currentCluster);
5205
+ return clusters;
5206
+ }
4082
5207
  function buildLineWindows(args) {
4083
5208
  const selected = /* @__PURE__ */ new Set();
4084
5209
  for (const index of args.indexes) {
@@ -4094,6 +5219,12 @@ function buildLineWindows(args) {
4094
5219
  }
4095
5220
  return [...selected].sort((left, right) => left - right).map((index) => args.lines[index]);
4096
5221
  }
5222
+ function buildPriorityLineGroup(args) {
5223
+ return unique2([
5224
+ ...args.indexes.map((index) => args.lines[index]).filter(Boolean),
5225
+ ...buildLineWindows(args)
5226
+ ]);
5227
+ }
4097
5228
  function collapseSelectedLines(args) {
4098
5229
  if (args.lines.length === 0) {
4099
5230
  return args.fallback();
@@ -4242,15 +5373,16 @@ function buildTestStatusRawSlice(args) {
4242
5373
  ) ? index : -1
4243
5374
  ).filter((index) => index >= 0);
4244
5375
  const bucketGroups = args.contract.main_buckets.map((bucket) => {
4245
- const bucketTerms = unique2(
4246
- [bucket.root_cause, ...bucket.evidence].map((value) => value.split(":").at(-1)?.trim() ?? value.trim()).filter((value) => value.length >= 4)
4247
- );
5376
+ const bucketTerms = extractBucketSearchTerms({
5377
+ bucket,
5378
+ readTargets: args.contract.read_targets
5379
+ });
4248
5380
  const indexes = lines.map(
4249
5381
  (line, index) => bucketTerms.some((term) => new RegExp(escapeRegExp(term), "i").test(line)) ? index : -1
4250
5382
  ).filter((index) => index >= 0);
4251
5383
  return unique2([
4252
5384
  ...indexes.map((index) => lines[index]).filter(Boolean),
4253
- ...buildLineWindows({
5385
+ ...buildPriorityLineGroup({
4254
5386
  lines,
4255
5387
  indexes,
4256
5388
  radius: 2,
@@ -4258,26 +5390,55 @@ function buildTestStatusRawSlice(args) {
4258
5390
  })
4259
5391
  ]);
4260
5392
  });
4261
- const targetGroups = args.contract.read_targets.map(
4262
- (target) => buildLineWindows({
5393
+ const targetGroups = args.contract.read_targets.flatMap((target) => {
5394
+ const searchHintIndexes = findSearchHintIndexes({
4263
5395
  lines,
4264
- indexes: unique2([
4265
- ...findReadTargetIndexes({
4266
- lines,
4267
- file: target.file,
4268
- line: target.line,
4269
- contextHint: target.context_hint
4270
- }),
4271
- ...findSearchHintIndexes({
4272
- lines,
4273
- searchHint: target.context_hint.search_hint
4274
- })
4275
- ]),
4276
- radius: target.line === null ? 1 : 2,
4277
- maxLines: target.line === null ? 6 : 8
5396
+ searchHint: target.context_hint.search_hint
5397
+ });
5398
+ const fileIndexes = findReadTargetIndexes({
5399
+ lines,
5400
+ file: target.file,
5401
+ line: target.line,
5402
+ contextHint: target.context_hint
5403
+ });
5404
+ const radius = target.line === null ? 1 : 2;
5405
+ const maxLines = target.line === null ? 6 : 8;
5406
+ const groups = [
5407
+ searchHintIndexes.length > 0 ? buildPriorityLineGroup({
5408
+ lines,
5409
+ indexes: searchHintIndexes,
5410
+ radius,
5411
+ maxLines
5412
+ }) : null,
5413
+ fileIndexes.length > 0 ? buildPriorityLineGroup({
5414
+ lines,
5415
+ indexes: fileIndexes,
5416
+ radius,
5417
+ maxLines
5418
+ }) : null
5419
+ ].filter((group) => group !== null && group.length > 0);
5420
+ if (groups.length > 0) {
5421
+ return groups;
5422
+ }
5423
+ return [
5424
+ buildPriorityLineGroup({
5425
+ lines,
5426
+ indexes: unique2([...searchHintIndexes, ...fileIndexes]),
5427
+ radius,
5428
+ maxLines
5429
+ })
5430
+ ];
5431
+ });
5432
+ const failureHeaderIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) ? index : -1).filter((index) => index >= 0);
5433
+ const failureIndexes = (failureHeaderIndexes.length > 0 ? failureHeaderIndexes : lines.map((line, index) => /^E\s/.test(line) ? index : -1).filter((index) => index >= 0)).filter((index) => index >= 0);
5434
+ const failureHeaderGroups = clusterIndexes(failureIndexes).slice(0, 8).map(
5435
+ (cluster) => buildPriorityLineGroup({
5436
+ lines,
5437
+ indexes: cluster,
5438
+ radius: 1,
5439
+ maxLines: 8
4278
5440
  })
4279
- );
4280
- const failureIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) || /^E\s/.test(line) ? index : -1).filter((index) => index >= 0);
5441
+ ).filter((group) => group.length > 0);
4281
5442
  const selected = collapseSelectedLineGroups({
4282
5443
  groups: [
4283
5444
  ...targetGroups,
@@ -4291,12 +5452,14 @@ function buildTestStatusRawSlice(args) {
4291
5452
  })
4292
5453
  ]),
4293
5454
  ...bucketGroups,
4294
- buildLineWindows({
4295
- lines,
4296
- indexes: failureIndexes,
4297
- radius: 1,
4298
- maxLines: 24
4299
- })
5455
+ ...failureHeaderGroups.length > 0 ? failureHeaderGroups : [
5456
+ buildLineWindows({
5457
+ lines,
5458
+ indexes: failureIndexes,
5459
+ radius: 1,
5460
+ maxLines: 24
5461
+ })
5462
+ ]
4300
5463
  ],
4301
5464
  maxInputChars: args.config.maxInputChars,
4302
5465
  fallback: () => truncateInput(args.input, {
@@ -4437,7 +5600,8 @@ function withInsufficientHint(args) {
4437
5600
  return buildInsufficientSignalOutput({
4438
5601
  presetName: args.request.presetName,
4439
5602
  originalLength: args.prepared.meta.originalLength,
4440
- truncatedApplied: args.prepared.meta.truncatedApplied
5603
+ truncatedApplied: args.prepared.meta.truncatedApplied,
5604
+ recognizedRunner: detectTestRunner(args.prepared.redacted)
4441
5605
  });
4442
5606
  }
4443
5607
  async function generateWithRetry(args) {
@@ -4497,6 +5661,38 @@ function renderTestStatusDecisionOutput(args) {
4497
5661
  return args.decision.standardText;
4498
5662
  }
4499
5663
  function buildTestStatusProviderFailureDecision(args) {
5664
+ const concreteReadTarget = args.baseDecision.contract.read_targets.find(
5665
+ (target) => Boolean(target.file)
5666
+ );
5667
+ const hasUnknownBucket = args.baseDecision.contract.main_buckets.some(
5668
+ (bucket) => bucket.root_cause.startsWith("unknown ")
5669
+ );
5670
+ if (concreteReadTarget && !hasUnknownBucket) {
5671
+ return buildTestStatusDiagnoseContract({
5672
+ input: args.input,
5673
+ analysis: args.analysis,
5674
+ resolvedTests: args.baseDecision.contract.resolved_tests,
5675
+ remainingTests: args.baseDecision.contract.remaining_tests,
5676
+ contractOverrides: {
5677
+ ...args.baseDecision.contract,
5678
+ diagnosis_complete: false,
5679
+ raw_needed: false,
5680
+ additional_source_read_likely_low_value: false,
5681
+ read_raw_only_if: null,
5682
+ decision: "read_source",
5683
+ provider_used: true,
5684
+ provider_confidence: null,
5685
+ provider_failed: true,
5686
+ raw_slice_used: args.rawSliceUsed,
5687
+ raw_slice_strategy: args.rawSliceStrategy,
5688
+ next_best_action: {
5689
+ code: "read_source_for_bucket",
5690
+ bucket_index: args.baseDecision.contract.dominant_blocker_bucket_index ?? concreteReadTarget.bucket_index,
5691
+ note: `Provider follow-up failed (${args.reason}). The heuristic anchor is concrete enough to inspect source for the current bucket before reading raw traceback.`
5692
+ }
5693
+ }
5694
+ });
5695
+ }
4500
5696
  const shouldZoomFirst = args.request.detail !== "verbose";
4501
5697
  return buildTestStatusDiagnoseContract({
4502
5698
  input: args.input,
@@ -4523,7 +5719,7 @@ function buildTestStatusProviderFailureDecision(args) {
4523
5719
  }
4524
5720
  });
4525
5721
  }
4526
- async function runSift(request) {
5722
+ async function runSiftCore(request, recorder) {
4527
5723
  const prepared = prepareInput(request.stdin, request.config.input);
4528
5724
  const heuristicInput = prepared.redacted;
4529
5725
  const heuristicInputTruncated = false;
@@ -4609,6 +5805,7 @@ async function runSift(request) {
4609
5805
  finalOutput
4610
5806
  });
4611
5807
  }
5808
+ recorder?.heuristic();
4612
5809
  return finalOutput;
4613
5810
  }
4614
5811
  if (testStatusDecision && testStatusAnalysis) {
@@ -4708,6 +5905,7 @@ async function runSift(request) {
4708
5905
  providerInputChars: providerPrepared2.truncated.length,
4709
5906
  providerOutputChars: result.text.length
4710
5907
  });
5908
+ recorder?.provider(result.usage);
4711
5909
  return finalOutput;
4712
5910
  } catch (error) {
4713
5911
  const reason = error instanceof Error ? error.message : "unknown_error";
@@ -4742,6 +5940,7 @@ async function runSift(request) {
4742
5940
  rawSliceChars: rawSlice.text.length,
4743
5941
  providerInputChars: providerPrepared2.truncated.length
4744
5942
  });
5943
+ recorder?.fallback();
4745
5944
  return finalOutput;
4746
5945
  }
4747
5946
  }
@@ -4798,6 +5997,7 @@ async function runSift(request) {
4798
5997
  })) {
4799
5998
  throw new Error("Model output rejected by quality gate");
4800
5999
  }
6000
+ recorder?.provider(result.usage);
4801
6001
  return withInsufficientHint({
4802
6002
  output: normalizeOutput(result.text, providerPrompt.responseMode),
4803
6003
  request,
@@ -4805,6 +6005,7 @@ async function runSift(request) {
4805
6005
  });
4806
6006
  } catch (error) {
4807
6007
  const reason = error instanceof Error ? error.message : "unknown_error";
6008
+ recorder?.fallback();
4808
6009
  return withInsufficientHint({
4809
6010
  output: buildFallbackOutput({
4810
6011
  format: request.format,
@@ -4818,6 +6019,72 @@ async function runSift(request) {
4818
6019
  });
4819
6020
  }
4820
6021
  }
6022
+ async function runSift(request) {
6023
+ return runSiftCore(request);
6024
+ }
6025
+ async function runSiftWithStats(request) {
6026
+ if (request.dryRun) {
6027
+ return {
6028
+ output: await runSiftCore(request),
6029
+ stats: null
6030
+ };
6031
+ }
6032
+ const startedAt = Date.now();
6033
+ let layer = "fallback";
6034
+ let providerCalled = false;
6035
+ let totalTokens = null;
6036
+ const output = await runSiftCore(request, {
6037
+ heuristic() {
6038
+ layer = "heuristic";
6039
+ providerCalled = false;
6040
+ totalTokens = null;
6041
+ },
6042
+ provider(usage) {
6043
+ layer = "provider";
6044
+ providerCalled = true;
6045
+ totalTokens = usage?.totalTokens ?? null;
6046
+ },
6047
+ fallback() {
6048
+ layer = "fallback";
6049
+ providerCalled = true;
6050
+ totalTokens = null;
6051
+ }
6052
+ });
6053
+ return {
6054
+ output,
6055
+ stats: {
6056
+ layer,
6057
+ providerCalled,
6058
+ totalTokens,
6059
+ durationMs: Date.now() - startedAt,
6060
+ presetName: request.presetName
6061
+ }
6062
+ };
6063
+ }
6064
+
6065
+ // src/core/stats.ts
6066
+ import pc2 from "picocolors";
6067
+ function formatDuration(durationMs) {
6068
+ return durationMs >= 1e3 ? `${(durationMs / 1e3).toFixed(1)}s` : `${durationMs}ms`;
6069
+ }
6070
+ function formatStatsFooter(stats) {
6071
+ const duration = formatDuration(stats.durationMs);
6072
+ if (stats.layer === "heuristic") {
6073
+ return `[sift: heuristic \u2022 LLM skipped \u2022 summary ${duration}]`;
6074
+ }
6075
+ if (stats.layer === "provider") {
6076
+ const tokenSegment = stats.totalTokens !== null ? ` \u2022 ${stats.totalTokens} tokens` : "";
6077
+ return `[sift: provider \u2022 LLM used${tokenSegment} \u2022 summary ${duration}]`;
6078
+ }
6079
+ return `[sift: fallback \u2022 provider failed \u2022 summary ${duration}]`;
6080
+ }
6081
+ function emitStatsFooter(args) {
6082
+ if (args.quiet || !args.stats || !process.stderr.isTTY) {
6083
+ return;
6084
+ }
6085
+ process.stderr.write(`${pc2.dim(formatStatsFooter(args.stats))}
6086
+ `);
6087
+ }
4821
6088
 
4822
6089
  // src/core/testStatusState.ts
4823
6090
  import fs from "fs";
@@ -5519,7 +6786,7 @@ async function runExec(request) {
5519
6786
  const previousCachedRun = shouldCacheTestStatusBase ? tryReadCachedTestStatusRun() : null;
5520
6787
  if (request.config.runtime.verbose) {
5521
6788
  process.stderr.write(
5522
- `${pc2.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
6789
+ `${pc3.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
5523
6790
  `
5524
6791
  );
5525
6792
  }
@@ -5548,7 +6815,7 @@ async function runExec(request) {
5548
6815
  }
5549
6816
  bypassed = true;
5550
6817
  if (request.config.runtime.verbose) {
5551
- process.stderr.write(`${pc2.dim("sift")} bypass=interactive-prompt
6818
+ process.stderr.write(`${pc3.dim("sift")} bypass=interactive-prompt
5552
6819
  `);
5553
6820
  }
5554
6821
  process.stderr.write(capture.render());
@@ -5577,15 +6844,16 @@ async function runExec(request) {
5577
6844
  const shouldCacheTestStatus = shouldCacheTestStatusBase && !useWatchFlow;
5578
6845
  if (request.config.runtime.verbose) {
5579
6846
  process.stderr.write(
5580
- `${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
6847
+ `${pc3.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
5581
6848
  `
5582
6849
  );
5583
6850
  }
5584
6851
  if (autoWatchDetected) {
5585
- process.stderr.write(`${pc2.dim("sift")} auto-watch=detected
6852
+ process.stderr.write(`${pc3.dim("sift")} auto-watch=detected
5586
6853
  `);
5587
6854
  }
5588
6855
  if (!bypassed) {
6856
+ const reductionStartedAt = Date.now();
5589
6857
  if (request.showRaw && capturedOutput.length > 0) {
5590
6858
  process.stderr.write(capturedOutput);
5591
6859
  if (!capturedOutput.endsWith("\n")) {
@@ -5600,12 +6868,22 @@ async function runExec(request) {
5600
6868
  if (execSuccessShortcut && !request.dryRun) {
5601
6869
  if (request.config.runtime.verbose) {
5602
6870
  process.stderr.write(
5603
- `${pc2.dim("sift")} exec_shortcut=${request.presetName}
6871
+ `${pc3.dim("sift")} exec_shortcut=${request.presetName}
5604
6872
  `
5605
6873
  );
5606
6874
  }
5607
6875
  process.stdout.write(`${execSuccessShortcut}
5608
6876
  `);
6877
+ emitStatsFooter({
6878
+ stats: {
6879
+ layer: "heuristic",
6880
+ providerCalled: false,
6881
+ totalTokens: null,
6882
+ durationMs: Date.now() - reductionStartedAt,
6883
+ presetName: request.presetName
6884
+ },
6885
+ quiet: Boolean(request.quiet)
6886
+ });
5609
6887
  return exitCode;
5610
6888
  }
5611
6889
  if (useWatchFlow) {
@@ -5618,7 +6896,8 @@ async function runExec(request) {
5618
6896
  presetName: request.presetName,
5619
6897
  originalLength: capture.getTotalChars(),
5620
6898
  truncatedApplied: capture.wasTruncated(),
5621
- exitCode
6899
+ exitCode,
6900
+ recognizedRunner: detectTestRunner(capturedOutput)
5622
6901
  });
5623
6902
  }
5624
6903
  process.stdout.write(`${output2}
@@ -5646,7 +6925,7 @@ async function runExec(request) {
5646
6925
  previous: previousCachedRun,
5647
6926
  current: currentCachedRun
5648
6927
  }) : null;
5649
- let output = await runSift({
6928
+ const result = await runSiftWithStats({
5650
6929
  ...request,
5651
6930
  stdin: capturedOutput,
5652
6931
  analysisContext: request.skipCacheWrite && request.presetName === "test-status" ? [
@@ -5665,13 +6944,15 @@ async function runExec(request) {
5665
6944
  )
5666
6945
  } : request.testStatusContext
5667
6946
  });
6947
+ let output = result.output;
5668
6948
  if (shouldCacheTestStatus) {
5669
6949
  if (isInsufficientSignalOutput(output)) {
5670
6950
  output = buildInsufficientSignalOutput({
5671
6951
  presetName: request.presetName,
5672
6952
  originalLength: capture.getTotalChars(),
5673
6953
  truncatedApplied: capture.wasTruncated(),
5674
- exitCode
6954
+ exitCode,
6955
+ recognizedRunner: detectTestRunner(capturedOutput)
5675
6956
  });
5676
6957
  }
5677
6958
  if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
@@ -5704,7 +6985,7 @@ ${output}`;
5704
6985
  } catch (error) {
5705
6986
  if (request.config.runtime.verbose) {
5706
6987
  const reason = error instanceof Error ? error.message : "unknown_error";
5707
- process.stderr.write(`${pc2.dim("sift")} cache_write=failed reason=${reason}
6988
+ process.stderr.write(`${pc3.dim("sift")} cache_write=failed reason=${reason}
5708
6989
  `);
5709
6990
  }
5710
6991
  }
@@ -5714,11 +6995,16 @@ ${output}`;
5714
6995
  presetName: request.presetName,
5715
6996
  originalLength: capture.getTotalChars(),
5716
6997
  truncatedApplied: capture.wasTruncated(),
5717
- exitCode
6998
+ exitCode,
6999
+ recognizedRunner: detectTestRunner(capturedOutput)
5718
7000
  });
5719
7001
  }
5720
7002
  process.stdout.write(`${output}
5721
7003
  `);
7004
+ emitStatsFooter({
7005
+ stats: result.stats,
7006
+ quiet: Boolean(request.quiet)
7007
+ });
5722
7008
  if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
5723
7009
  presetName: request.presetName,
5724
7010
  output
@@ -6078,5 +7364,6 @@ export {
6078
7364
  normalizeChildExitCode,
6079
7365
  resolveConfig,
6080
7366
  runExec,
6081
- runSift
7367
+ runSift,
7368
+ runSiftWithStats
6082
7369
  };