@bilalimamoglu/sift 0.2.2 → 0.2.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/README.md +139 -89
- package/dist/cli.js +1132 -347
- package/dist/index.d.ts +29 -1
- package/dist/index.js +530 -47
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -53,6 +53,29 @@ function evaluateGate(args) {
|
|
|
53
53
|
return { shouldFail: false };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// src/core/insufficient.ts
|
|
57
|
+
function isInsufficientSignalOutput(output) {
|
|
58
|
+
const trimmed = output.trim();
|
|
59
|
+
return trimmed === INSUFFICIENT_SIGNAL_TEXT || trimmed.startsWith(`${INSUFFICIENT_SIGNAL_TEXT}
|
|
60
|
+
Hint:`);
|
|
61
|
+
}
|
|
62
|
+
function buildInsufficientSignalOutput(input) {
|
|
63
|
+
let hint;
|
|
64
|
+
if (input.originalLength === 0) {
|
|
65
|
+
hint = "Hint: no command output was captured.";
|
|
66
|
+
} else if (input.truncatedApplied) {
|
|
67
|
+
hint = "Hint: captured output was truncated before a clear summary was found.";
|
|
68
|
+
} else if (input.presetName === "test-status" && input.exitCode === 0) {
|
|
69
|
+
hint = "Hint: command succeeded, but no recognizable test summary was found.";
|
|
70
|
+
} else if (input.presetName === "test-status" && typeof input.exitCode === "number") {
|
|
71
|
+
hint = "Hint: command failed, but the captured output did not include a recognizable test summary.";
|
|
72
|
+
} else {
|
|
73
|
+
hint = "Hint: the captured output did not contain a clear answer for this preset.";
|
|
74
|
+
}
|
|
75
|
+
return `${INSUFFICIENT_SIGNAL_TEXT}
|
|
76
|
+
${hint}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
// src/core/run.ts
|
|
57
80
|
import pc from "picocolors";
|
|
58
81
|
|
|
@@ -130,7 +153,7 @@ var OpenAIProvider = class {
|
|
|
130
153
|
if (!text) {
|
|
131
154
|
throw new Error("Provider returned an empty response");
|
|
132
155
|
}
|
|
133
|
-
|
|
156
|
+
const result = {
|
|
134
157
|
text,
|
|
135
158
|
usage: data?.usage ? {
|
|
136
159
|
inputTokens: data.usage.input_tokens,
|
|
@@ -139,13 +162,14 @@ var OpenAIProvider = class {
|
|
|
139
162
|
} : void 0,
|
|
140
163
|
raw: data
|
|
141
164
|
};
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
return result;
|
|
142
167
|
} catch (error) {
|
|
168
|
+
clearTimeout(timeout);
|
|
143
169
|
if (error.name === "AbortError") {
|
|
144
170
|
throw new Error("Provider request timed out");
|
|
145
171
|
}
|
|
146
172
|
throw error;
|
|
147
|
-
} finally {
|
|
148
|
-
clearTimeout(timeout);
|
|
149
173
|
}
|
|
150
174
|
}
|
|
151
175
|
};
|
|
@@ -227,7 +251,7 @@ var OpenAICompatibleProvider = class {
|
|
|
227
251
|
if (!text.trim()) {
|
|
228
252
|
throw new Error("Provider returned an empty response");
|
|
229
253
|
}
|
|
230
|
-
|
|
254
|
+
const result = {
|
|
231
255
|
text,
|
|
232
256
|
usage: data?.usage ? {
|
|
233
257
|
inputTokens: data.usage.prompt_tokens,
|
|
@@ -236,13 +260,14 @@ var OpenAICompatibleProvider = class {
|
|
|
236
260
|
} : void 0,
|
|
237
261
|
raw: data
|
|
238
262
|
};
|
|
263
|
+
clearTimeout(timeout);
|
|
264
|
+
return result;
|
|
239
265
|
} catch (error) {
|
|
266
|
+
clearTimeout(timeout);
|
|
240
267
|
if (error.name === "AbortError") {
|
|
241
268
|
throw new Error("Provider request timed out");
|
|
242
269
|
}
|
|
243
270
|
throw error;
|
|
244
|
-
} finally {
|
|
245
|
-
clearTimeout(timeout);
|
|
246
271
|
}
|
|
247
272
|
}
|
|
248
273
|
};
|
|
@@ -447,6 +472,19 @@ function buildPrompt(args) {
|
|
|
447
472
|
policyName: args.policyName,
|
|
448
473
|
outputContract: args.outputContract
|
|
449
474
|
});
|
|
475
|
+
const detailRules = args.policyName === "test-status" && args.detail === "focused" ? [
|
|
476
|
+
"Use a focused failure view.",
|
|
477
|
+
"When the output clearly maps failures to specific tests or modules, group them by dominant error type first.",
|
|
478
|
+
"Within each error group, prefer compact bullets in the form '- test-or-module -> dominant reason'.",
|
|
479
|
+
"Cap focused entries at 6 per error group and end with '- and N more failing modules' if more clear mappings are visible.",
|
|
480
|
+
"If per-test or per-module mapping is unclear, fall back to grouped root causes instead of guessing."
|
|
481
|
+
] : args.policyName === "test-status" && args.detail === "verbose" ? [
|
|
482
|
+
"Use a verbose failure view.",
|
|
483
|
+
"When the output clearly maps failures to specific tests or modules, list each visible failing test or module on its own line in the form '- test-or-module -> normalized reason'.",
|
|
484
|
+
"Preserve the original file or module order when the mapping is visible.",
|
|
485
|
+
"Prefer concrete normalized reasons such as missing modules or assertion failures over traceback plumbing.",
|
|
486
|
+
"If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
|
|
487
|
+
] : [];
|
|
450
488
|
const prompt = [
|
|
451
489
|
"You are Sift, a CLI output reduction assistant for downstream agents and automation.",
|
|
452
490
|
"Hard rules:",
|
|
@@ -454,6 +492,7 @@ function buildPrompt(args) {
|
|
|
454
492
|
"",
|
|
455
493
|
`Task policy: ${policy.name}`,
|
|
456
494
|
...policy.taskRules.map((rule) => `- ${rule}`),
|
|
495
|
+
...detailRules.map((rule) => `- ${rule}`),
|
|
457
496
|
...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
|
|
458
497
|
"",
|
|
459
498
|
`Question: ${args.question}`,
|
|
@@ -566,6 +605,410 @@ function inferPackage(line) {
|
|
|
566
605
|
function inferRemediation(pkg) {
|
|
567
606
|
return `Upgrade ${pkg} to a patched version.`;
|
|
568
607
|
}
|
|
608
|
+
function getCount(input, label) {
|
|
609
|
+
const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
|
|
610
|
+
const lastMatch = matches.at(-1);
|
|
611
|
+
return lastMatch ? Number(lastMatch[1]) : 0;
|
|
612
|
+
}
|
|
613
|
+
function formatCount(count, singular, plural = `${singular}s`) {
|
|
614
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
615
|
+
}
|
|
616
|
+
function countPattern(input, matcher) {
|
|
617
|
+
return [...input.matchAll(matcher)].length;
|
|
618
|
+
}
|
|
619
|
+
function collectUniqueMatches(input, matcher, limit = 6) {
|
|
620
|
+
const values = [];
|
|
621
|
+
for (const match of input.matchAll(matcher)) {
|
|
622
|
+
const candidate = match[1]?.trim();
|
|
623
|
+
if (!candidate || values.includes(candidate)) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
values.push(candidate);
|
|
627
|
+
if (values.length >= limit) {
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return values;
|
|
632
|
+
}
|
|
633
|
+
function cleanFailureLabel(label) {
|
|
634
|
+
return label.trim().replace(/^['"]|['"]$/g, "");
|
|
635
|
+
}
|
|
636
|
+
function isLowValueInternalReason(normalized) {
|
|
637
|
+
return /^Hint:\s+make sure your test modules\/packages have valid Python names\.?$/i.test(
|
|
638
|
+
normalized
|
|
639
|
+
) || /^Traceback\b/i.test(normalized) || /^return _bootstrap\._gcd_import/i.test(normalized) || /(?:^|[/\\])(?:site-packages[/\\])?_pytest(?:[/\\]|$)/i.test(normalized) || /(?:^|[/\\])importlib[/\\]__init__\.py:\d+:\s+in\s+import_module\b/i.test(
|
|
640
|
+
normalized
|
|
641
|
+
) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
|
|
642
|
+
}
|
|
643
|
+
function scoreFailureReason(reason) {
|
|
644
|
+
if (reason.startsWith("missing module:")) {
|
|
645
|
+
return 5;
|
|
646
|
+
}
|
|
647
|
+
if (reason.startsWith("assertion failed:")) {
|
|
648
|
+
return 4;
|
|
649
|
+
}
|
|
650
|
+
if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
|
|
651
|
+
return 3;
|
|
652
|
+
}
|
|
653
|
+
if (reason === "import error during collection") {
|
|
654
|
+
return 2;
|
|
655
|
+
}
|
|
656
|
+
return 1;
|
|
657
|
+
}
|
|
658
|
+
function classifyFailureReason(line, options) {
|
|
659
|
+
const normalized = line.trim().replace(/^[A-Z]\s+/, "");
|
|
660
|
+
if (normalized.length === 0) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
if (isLowValueInternalReason(normalized)) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
const pythonMissingModule = normalized.match(
|
|
667
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
|
|
668
|
+
);
|
|
669
|
+
if (pythonMissingModule) {
|
|
670
|
+
return {
|
|
671
|
+
reason: `missing module: ${pythonMissingModule[1]}`,
|
|
672
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
|
|
676
|
+
if (nodeMissingModule) {
|
|
677
|
+
return {
|
|
678
|
+
reason: `missing module: ${nodeMissingModule[1]}`,
|
|
679
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
|
|
683
|
+
if (assertionFailure) {
|
|
684
|
+
return {
|
|
685
|
+
reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
|
|
686
|
+
group: "assertion failures"
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
|
|
690
|
+
if (genericError) {
|
|
691
|
+
const errorType = genericError[1];
|
|
692
|
+
return {
|
|
693
|
+
reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
|
|
694
|
+
group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (/ImportError while importing test module/i.test(normalized)) {
|
|
698
|
+
return {
|
|
699
|
+
reason: "import error during collection",
|
|
700
|
+
group: "import/dependency errors during collection"
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (!/[A-Za-z]/.test(normalized)) {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
reason: normalized.slice(0, 120),
|
|
708
|
+
group: options.duringCollection ? "collection/import errors" : "other failures"
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function pushFocusedFailureItem(items, candidate) {
|
|
712
|
+
if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
items.push(candidate);
|
|
716
|
+
}
|
|
717
|
+
function chooseStrongestFailureItems(items) {
|
|
718
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
719
|
+
const order = [];
|
|
720
|
+
for (const item of items) {
|
|
721
|
+
const existing = strongest.get(item.label);
|
|
722
|
+
if (!existing) {
|
|
723
|
+
strongest.set(item.label, item);
|
|
724
|
+
order.push(item.label);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (scoreFailureReason(item.reason) > scoreFailureReason(existing.reason)) {
|
|
728
|
+
strongest.set(item.label, item);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return order.map((label) => strongest.get(label));
|
|
732
|
+
}
|
|
733
|
+
function collectCollectionFailureItems(input) {
|
|
734
|
+
const items = [];
|
|
735
|
+
const lines = input.split("\n");
|
|
736
|
+
let currentLabel = null;
|
|
737
|
+
let pendingGenericReason = null;
|
|
738
|
+
for (const line of lines) {
|
|
739
|
+
const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
|
|
740
|
+
if (collecting) {
|
|
741
|
+
if (currentLabel && pendingGenericReason) {
|
|
742
|
+
pushFocusedFailureItem(
|
|
743
|
+
items,
|
|
744
|
+
{
|
|
745
|
+
label: currentLabel,
|
|
746
|
+
reason: pendingGenericReason.reason,
|
|
747
|
+
group: pendingGenericReason.group
|
|
748
|
+
}
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
currentLabel = cleanFailureLabel(collecting[1]);
|
|
752
|
+
pendingGenericReason = null;
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (!currentLabel) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const classification = classifyFailureReason(line, {
|
|
759
|
+
duringCollection: true
|
|
760
|
+
});
|
|
761
|
+
if (!classification) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (classification.reason === "import error during collection") {
|
|
765
|
+
pendingGenericReason = classification;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
pushFocusedFailureItem(
|
|
769
|
+
items,
|
|
770
|
+
{
|
|
771
|
+
label: currentLabel,
|
|
772
|
+
reason: classification.reason,
|
|
773
|
+
group: classification.group
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
currentLabel = null;
|
|
777
|
+
pendingGenericReason = null;
|
|
778
|
+
}
|
|
779
|
+
if (currentLabel && pendingGenericReason) {
|
|
780
|
+
pushFocusedFailureItem(
|
|
781
|
+
items,
|
|
782
|
+
{
|
|
783
|
+
label: currentLabel,
|
|
784
|
+
reason: pendingGenericReason.reason,
|
|
785
|
+
group: pendingGenericReason.group
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return items;
|
|
790
|
+
}
|
|
791
|
+
function collectInlineFailureItems(input) {
|
|
792
|
+
const items = [];
|
|
793
|
+
for (const line of input.split("\n")) {
|
|
794
|
+
const inlineFailure = line.match(/^(FAILED|ERROR)\s+(.+?)\s+-\s+(.+)$/);
|
|
795
|
+
if (!inlineFailure) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const classification = classifyFailureReason(inlineFailure[3], {
|
|
799
|
+
duringCollection: false
|
|
800
|
+
});
|
|
801
|
+
if (!classification) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
pushFocusedFailureItem(
|
|
805
|
+
items,
|
|
806
|
+
{
|
|
807
|
+
label: cleanFailureLabel(inlineFailure[2]),
|
|
808
|
+
reason: classification.reason,
|
|
809
|
+
group: classification.group
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
return items;
|
|
814
|
+
}
|
|
815
|
+
function formatFocusedFailureGroups(args) {
|
|
816
|
+
const maxGroups = args.maxGroups ?? 3;
|
|
817
|
+
const maxPerGroup = args.maxPerGroup ?? 6;
|
|
818
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
819
|
+
for (const item of args.items) {
|
|
820
|
+
const entries = grouped.get(item.group) ?? [];
|
|
821
|
+
entries.push(item);
|
|
822
|
+
grouped.set(item.group, entries);
|
|
823
|
+
}
|
|
824
|
+
const lines = [];
|
|
825
|
+
const visibleGroups = [...grouped.entries()].slice(0, maxGroups);
|
|
826
|
+
for (const [group, entries] of visibleGroups) {
|
|
827
|
+
lines.push(`- ${group}`);
|
|
828
|
+
for (const item of entries.slice(0, maxPerGroup)) {
|
|
829
|
+
lines.push(` - ${item.label} -> ${item.reason}`);
|
|
830
|
+
}
|
|
831
|
+
const remaining = entries.length - Math.min(entries.length, maxPerGroup);
|
|
832
|
+
if (remaining > 0) {
|
|
833
|
+
lines.push(` - and ${remaining} more failing ${args.remainderLabel}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const hiddenGroups = grouped.size - visibleGroups.length;
|
|
837
|
+
if (hiddenGroups > 0) {
|
|
838
|
+
lines.push(`- and ${hiddenGroups} more error group${hiddenGroups === 1 ? "" : "s"}`);
|
|
839
|
+
}
|
|
840
|
+
return lines;
|
|
841
|
+
}
|
|
842
|
+
function formatVerboseFailureItems(args) {
|
|
843
|
+
return chooseStrongestFailureItems(args.items).map(
|
|
844
|
+
(item) => `- ${item.label} -> ${item.reason}`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
function summarizeRepeatedTestCauses(input, options) {
|
|
848
|
+
const pythonMissingModules = collectUniqueMatches(
|
|
849
|
+
input,
|
|
850
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
|
|
851
|
+
);
|
|
852
|
+
const nodeMissingModules = collectUniqueMatches(
|
|
853
|
+
input,
|
|
854
|
+
/Cannot find module ['"]([^'"]+)['"]/gi
|
|
855
|
+
);
|
|
856
|
+
const missingModules = [...pythonMissingModules];
|
|
857
|
+
for (const moduleName of nodeMissingModules) {
|
|
858
|
+
if (!missingModules.includes(moduleName)) {
|
|
859
|
+
missingModules.push(moduleName);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const missingModuleHits = countPattern(
|
|
863
|
+
input,
|
|
864
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
|
|
865
|
+
) + countPattern(input, /Cannot find module ['"]([^'"]+)['"]/gi);
|
|
866
|
+
const importCollectionHits = countPattern(input, /ImportError while importing test module/gi) + countPattern(input, /^\s*_+\s+ERROR collecting\b/gim);
|
|
867
|
+
const genericErrorTypes = collectUniqueMatches(
|
|
868
|
+
input,
|
|
869
|
+
/\b((?:Assertion|Import|Type|Value|Runtime|Reference|Key|Attribute)[A-Za-z]*Error)\b/gi,
|
|
870
|
+
4
|
|
871
|
+
);
|
|
872
|
+
const bullets = [];
|
|
873
|
+
if (options.duringCollection && (importCollectionHits >= 2 || missingModuleHits >= 2) || !options.duringCollection && missingModuleHits >= 2) {
|
|
874
|
+
bullets.push(
|
|
875
|
+
options.duringCollection ? "- Most failures are import/dependency errors during test collection." : "- Most failures are import/dependency errors."
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (missingModules.length > 1) {
|
|
879
|
+
bullets.push(`- Missing modules include ${missingModules.join(", ")}.`);
|
|
880
|
+
} else if (missingModules.length === 1 && missingModuleHits >= 2) {
|
|
881
|
+
bullets.push(`- Missing module repeated across failures: ${missingModules[0]}.`);
|
|
882
|
+
}
|
|
883
|
+
if (bullets.length < 2 && genericErrorTypes.length >= 2) {
|
|
884
|
+
bullets.push(`- Repeated error types include ${genericErrorTypes.join(", ")}.`);
|
|
885
|
+
}
|
|
886
|
+
return bullets.slice(0, 2);
|
|
887
|
+
}
|
|
888
|
+
function testStatusHeuristic(input, detail = "standard") {
|
|
889
|
+
const normalized = input.trim();
|
|
890
|
+
if (normalized === "") {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
const passed = getCount(input, "passed");
|
|
894
|
+
const failed = getCount(input, "failed");
|
|
895
|
+
const errors = Math.max(
|
|
896
|
+
getCount(input, "errors"),
|
|
897
|
+
getCount(input, "error")
|
|
898
|
+
);
|
|
899
|
+
const skipped = getCount(input, "skipped");
|
|
900
|
+
const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
|
|
901
|
+
const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
|
|
902
|
+
const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
|
|
903
|
+
const inlineItems = collectInlineFailureItems(input);
|
|
904
|
+
if (collectionErrors) {
|
|
905
|
+
const count = Number(collectionErrors[1]);
|
|
906
|
+
const items = chooseStrongestFailureItems(collectCollectionFailureItems(input));
|
|
907
|
+
if (detail === "verbose") {
|
|
908
|
+
if (items.length > 0) {
|
|
909
|
+
return [
|
|
910
|
+
"- Tests did not complete.",
|
|
911
|
+
`- ${formatCount(count, "error")} occurred during collection.`,
|
|
912
|
+
...formatVerboseFailureItems({
|
|
913
|
+
items
|
|
914
|
+
})
|
|
915
|
+
].join("\n");
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (detail === "focused") {
|
|
919
|
+
if (items.length > 0) {
|
|
920
|
+
const groupedLines = formatFocusedFailureGroups({
|
|
921
|
+
items,
|
|
922
|
+
remainderLabel: "modules"
|
|
923
|
+
});
|
|
924
|
+
if (groupedLines.length > 0) {
|
|
925
|
+
return [
|
|
926
|
+
"- Tests did not complete.",
|
|
927
|
+
`- ${formatCount(count, "error")} occurred during collection.`,
|
|
928
|
+
...groupedLines
|
|
929
|
+
].join("\n");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
const causes = summarizeRepeatedTestCauses(input, {
|
|
934
|
+
duringCollection: true
|
|
935
|
+
});
|
|
936
|
+
return [
|
|
937
|
+
"- Tests did not complete.",
|
|
938
|
+
`- ${formatCount(count, "error")} occurred during collection.`,
|
|
939
|
+
...causes
|
|
940
|
+
].join("\n");
|
|
941
|
+
}
|
|
942
|
+
if (noTestsCollected) {
|
|
943
|
+
return ["- Tests did not run.", "- Collected 0 items."].join("\n");
|
|
944
|
+
}
|
|
945
|
+
if (interrupted && failed === 0 && errors === 0) {
|
|
946
|
+
return "- Test run was interrupted.";
|
|
947
|
+
}
|
|
948
|
+
if (failed === 0 && errors === 0 && passed > 0) {
|
|
949
|
+
const details = [formatCount(passed, "test")];
|
|
950
|
+
if (skipped > 0) {
|
|
951
|
+
details.push(formatCount(skipped, "skip"));
|
|
952
|
+
}
|
|
953
|
+
return [
|
|
954
|
+
"- Tests passed.",
|
|
955
|
+
`- ${details.join(", ")}.`
|
|
956
|
+
].join("\n");
|
|
957
|
+
}
|
|
958
|
+
if (failed > 0 || errors > 0 || inlineItems.length > 0) {
|
|
959
|
+
const summarizedInlineItems = chooseStrongestFailureItems(inlineItems);
|
|
960
|
+
if (detail === "verbose") {
|
|
961
|
+
if (summarizedInlineItems.length > 0) {
|
|
962
|
+
const detailLines2 = [];
|
|
963
|
+
if (failed > 0) {
|
|
964
|
+
detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
|
|
965
|
+
}
|
|
966
|
+
if (errors > 0) {
|
|
967
|
+
detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
|
|
968
|
+
}
|
|
969
|
+
return [
|
|
970
|
+
"- Tests did not pass.",
|
|
971
|
+
...detailLines2,
|
|
972
|
+
...formatVerboseFailureItems({
|
|
973
|
+
items: summarizedInlineItems
|
|
974
|
+
})
|
|
975
|
+
].join("\n");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (detail === "focused") {
|
|
979
|
+
if (summarizedInlineItems.length > 0) {
|
|
980
|
+
const detailLines2 = [];
|
|
981
|
+
if (failed > 0) {
|
|
982
|
+
detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
|
|
983
|
+
}
|
|
984
|
+
if (errors > 0) {
|
|
985
|
+
detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
|
|
986
|
+
}
|
|
987
|
+
return [
|
|
988
|
+
"- Tests did not pass.",
|
|
989
|
+
...detailLines2,
|
|
990
|
+
...formatFocusedFailureGroups({
|
|
991
|
+
items: summarizedInlineItems,
|
|
992
|
+
remainderLabel: "tests or modules"
|
|
993
|
+
})
|
|
994
|
+
].join("\n");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const detailLines = [];
|
|
998
|
+
const causes = summarizeRepeatedTestCauses(input, {
|
|
999
|
+
duringCollection: false
|
|
1000
|
+
});
|
|
1001
|
+
if (failed > 0) {
|
|
1002
|
+
detailLines.push(`- ${formatCount(failed, "test")} failed.`);
|
|
1003
|
+
}
|
|
1004
|
+
if (errors > 0) {
|
|
1005
|
+
detailLines.push(`- ${formatCount(errors, "error")} occurred.`);
|
|
1006
|
+
}
|
|
1007
|
+
const evidence = input.split("\n").map((line) => line.trim()).filter((line) => /\b(FAILED|ERROR)\b/.test(line)).slice(0, 3).map((line) => `- ${line}`);
|
|
1008
|
+
return ["- Tests did not pass.", ...detailLines, ...causes, ...evidence].join("\n");
|
|
1009
|
+
}
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
569
1012
|
function auditCriticalHeuristic(input) {
|
|
570
1013
|
const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
571
1014
|
if (!/\b(critical|high)\b/i.test(line)) {
|
|
@@ -636,7 +1079,7 @@ function infraRiskHeuristic(input) {
|
|
|
636
1079
|
}
|
|
637
1080
|
return null;
|
|
638
1081
|
}
|
|
639
|
-
function applyHeuristicPolicy(policyName, input) {
|
|
1082
|
+
function applyHeuristicPolicy(policyName, input, detail) {
|
|
640
1083
|
if (!policyName) {
|
|
641
1084
|
return null;
|
|
642
1085
|
}
|
|
@@ -646,6 +1089,9 @@ function applyHeuristicPolicy(policyName, input) {
|
|
|
646
1089
|
if (policyName === "infra-risk") {
|
|
647
1090
|
return infraRiskHeuristic(input);
|
|
648
1091
|
}
|
|
1092
|
+
if (policyName === "test-status") {
|
|
1093
|
+
return testStatusHeuristic(input, detail);
|
|
1094
|
+
}
|
|
649
1095
|
return null;
|
|
650
1096
|
}
|
|
651
1097
|
|
|
@@ -775,6 +1221,7 @@ function buildDryRunOutput(args) {
|
|
|
775
1221
|
},
|
|
776
1222
|
question: args.request.question,
|
|
777
1223
|
format: args.request.format,
|
|
1224
|
+
detail: args.request.detail ?? null,
|
|
778
1225
|
responseMode: args.responseMode,
|
|
779
1226
|
policy: args.request.policyName ?? null,
|
|
780
1227
|
heuristicOutput: args.heuristicOutput ?? null,
|
|
@@ -794,35 +1241,42 @@ function buildDryRunOutput(args) {
|
|
|
794
1241
|
async function delay(ms) {
|
|
795
1242
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
796
1243
|
}
|
|
1244
|
+
function withInsufficientHint(args) {
|
|
1245
|
+
if (!isInsufficientSignalOutput(args.output)) {
|
|
1246
|
+
return args.output;
|
|
1247
|
+
}
|
|
1248
|
+
return buildInsufficientSignalOutput({
|
|
1249
|
+
presetName: args.request.presetName,
|
|
1250
|
+
originalLength: args.prepared.meta.originalLength,
|
|
1251
|
+
truncatedApplied: args.prepared.meta.truncatedApplied
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
797
1254
|
async function generateWithRetry(args) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
process.stderr.write(
|
|
818
|
-
`${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
1255
|
+
const generate = () => args.provider.generate({
|
|
1256
|
+
model: args.request.config.provider.model,
|
|
1257
|
+
prompt: args.prompt,
|
|
1258
|
+
temperature: args.request.config.provider.temperature,
|
|
1259
|
+
maxOutputTokens: args.request.config.provider.maxOutputTokens,
|
|
1260
|
+
timeoutMs: args.request.config.provider.timeoutMs,
|
|
1261
|
+
responseMode: args.responseMode,
|
|
1262
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
1263
|
+
});
|
|
1264
|
+
try {
|
|
1265
|
+
return await generate();
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
1268
|
+
if (!isRetriableReason(reason)) {
|
|
1269
|
+
throw error;
|
|
1270
|
+
}
|
|
1271
|
+
if (args.request.config.runtime.verbose) {
|
|
1272
|
+
process.stderr.write(
|
|
1273
|
+
`${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
819
1274
|
`
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
await delay(RETRY_DELAY_MS);
|
|
1275
|
+
);
|
|
823
1276
|
}
|
|
1277
|
+
await delay(RETRY_DELAY_MS);
|
|
824
1278
|
}
|
|
825
|
-
|
|
1279
|
+
return generate();
|
|
826
1280
|
}
|
|
827
1281
|
async function runSift(request) {
|
|
828
1282
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
@@ -830,6 +1284,7 @@ async function runSift(request) {
|
|
|
830
1284
|
question: request.question,
|
|
831
1285
|
format: request.format,
|
|
832
1286
|
input: prepared.truncated,
|
|
1287
|
+
detail: request.detail,
|
|
833
1288
|
policyName: request.policyName,
|
|
834
1289
|
outputContract: request.outputContract
|
|
835
1290
|
});
|
|
@@ -842,7 +1297,8 @@ async function runSift(request) {
|
|
|
842
1297
|
}
|
|
843
1298
|
const heuristicOutput = applyHeuristicPolicy(
|
|
844
1299
|
request.policyName,
|
|
845
|
-
prepared.truncated
|
|
1300
|
+
prepared.truncated,
|
|
1301
|
+
request.detail
|
|
846
1302
|
);
|
|
847
1303
|
if (heuristicOutput) {
|
|
848
1304
|
if (request.config.runtime.verbose) {
|
|
@@ -859,7 +1315,11 @@ async function runSift(request) {
|
|
|
859
1315
|
heuristicOutput
|
|
860
1316
|
});
|
|
861
1317
|
}
|
|
862
|
-
return
|
|
1318
|
+
return withInsufficientHint({
|
|
1319
|
+
output: heuristicOutput,
|
|
1320
|
+
request,
|
|
1321
|
+
prepared
|
|
1322
|
+
});
|
|
863
1323
|
}
|
|
864
1324
|
if (request.dryRun) {
|
|
865
1325
|
return buildDryRunOutput({
|
|
@@ -885,15 +1345,23 @@ async function runSift(request) {
|
|
|
885
1345
|
})) {
|
|
886
1346
|
throw new Error("Model output rejected by quality gate");
|
|
887
1347
|
}
|
|
888
|
-
return
|
|
1348
|
+
return withInsufficientHint({
|
|
1349
|
+
output: normalizeOutput(result.text, responseMode),
|
|
1350
|
+
request,
|
|
1351
|
+
prepared
|
|
1352
|
+
});
|
|
889
1353
|
} catch (error) {
|
|
890
1354
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
891
|
-
return
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1355
|
+
return withInsufficientHint({
|
|
1356
|
+
output: buildFallbackOutput({
|
|
1357
|
+
format: request.format,
|
|
1358
|
+
reason,
|
|
1359
|
+
rawInput: prepared.truncated,
|
|
1360
|
+
rawFallback: request.config.runtime.rawFallback,
|
|
1361
|
+
jsonFallback: request.fallbackJson
|
|
1362
|
+
}),
|
|
1363
|
+
request,
|
|
1364
|
+
prepared
|
|
897
1365
|
});
|
|
898
1366
|
}
|
|
899
1367
|
}
|
|
@@ -1003,7 +1471,6 @@ async function runExec(request) {
|
|
|
1003
1471
|
let bypassed = false;
|
|
1004
1472
|
let childStatus = null;
|
|
1005
1473
|
let childSignal = null;
|
|
1006
|
-
let childSpawnError = null;
|
|
1007
1474
|
const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
|
|
1008
1475
|
stdio: ["inherit", "pipe", "pipe"]
|
|
1009
1476
|
}) : spawn(request.command[0], request.command.slice(1), {
|
|
@@ -1031,7 +1498,6 @@ async function runExec(request) {
|
|
|
1031
1498
|
child.stderr.on("data", handleChunk);
|
|
1032
1499
|
await new Promise((resolve, reject) => {
|
|
1033
1500
|
child.on("error", (error) => {
|
|
1034
|
-
childSpawnError = error;
|
|
1035
1501
|
reject(error);
|
|
1036
1502
|
});
|
|
1037
1503
|
child.on("close", (status, signal) => {
|
|
@@ -1045,9 +1511,6 @@ async function runExec(request) {
|
|
|
1045
1511
|
}
|
|
1046
1512
|
throw new Error("Failed to start child process.");
|
|
1047
1513
|
});
|
|
1048
|
-
if (childSpawnError) {
|
|
1049
|
-
throw childSpawnError;
|
|
1050
|
-
}
|
|
1051
1514
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
1052
1515
|
const capturedOutput = capture.render();
|
|
1053
1516
|
if (request.config.runtime.verbose) {
|
|
@@ -1057,6 +1520,12 @@ async function runExec(request) {
|
|
|
1057
1520
|
);
|
|
1058
1521
|
}
|
|
1059
1522
|
if (!bypassed) {
|
|
1523
|
+
if (request.showRaw && capturedOutput.length > 0) {
|
|
1524
|
+
process.stderr.write(capturedOutput);
|
|
1525
|
+
if (!capturedOutput.endsWith("\n")) {
|
|
1526
|
+
process.stderr.write("\n");
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1060
1529
|
const execSuccessShortcut = getExecSuccessShortcut({
|
|
1061
1530
|
presetName: request.presetName,
|
|
1062
1531
|
exitCode,
|
|
@@ -1073,10 +1542,18 @@ async function runExec(request) {
|
|
|
1073
1542
|
`);
|
|
1074
1543
|
return exitCode;
|
|
1075
1544
|
}
|
|
1076
|
-
|
|
1545
|
+
let output = await runSift({
|
|
1077
1546
|
...request,
|
|
1078
1547
|
stdin: capturedOutput
|
|
1079
1548
|
});
|
|
1549
|
+
if (isInsufficientSignalOutput(output)) {
|
|
1550
|
+
output = buildInsufficientSignalOutput({
|
|
1551
|
+
presetName: request.presetName,
|
|
1552
|
+
originalLength: capture.getTotalChars(),
|
|
1553
|
+
truncatedApplied: capture.wasTruncated(),
|
|
1554
|
+
exitCode
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1080
1557
|
process.stdout.write(`${output}
|
|
1081
1558
|
`);
|
|
1082
1559
|
if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
|
|
@@ -1386,6 +1863,12 @@ function resolveConfig(options = {}) {
|
|
|
1386
1863
|
return siftConfigSchema.parse(merged);
|
|
1387
1864
|
}
|
|
1388
1865
|
export {
|
|
1866
|
+
BoundedCapture,
|
|
1867
|
+
buildCommandPreview,
|
|
1868
|
+
getExecSuccessShortcut,
|
|
1869
|
+
looksInteractivePrompt,
|
|
1870
|
+
mergeDefined,
|
|
1871
|
+
normalizeChildExitCode,
|
|
1389
1872
|
resolveConfig,
|
|
1390
1873
|
runExec,
|
|
1391
1874
|
runSift
|