@bilalimamoglu/sift 0.2.1 → 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 +163 -69
- package/dist/cli.js +1321 -252
- package/dist/index.d.ts +29 -1
- package/dist/index.js +569 -55
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -6,12 +6,17 @@ import pc2 from "picocolors";
|
|
|
6
6
|
// src/constants.ts
|
|
7
7
|
import os from "os";
|
|
8
8
|
import path from "path";
|
|
9
|
-
|
|
10
|
-
path.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
function getDefaultGlobalConfigPath() {
|
|
10
|
+
return path.join(os.homedir(), ".config", "sift", "config.yaml");
|
|
11
|
+
}
|
|
12
|
+
function getDefaultConfigSearchPaths() {
|
|
13
|
+
return [
|
|
14
|
+
path.resolve(process.cwd(), "sift.config.yaml"),
|
|
15
|
+
path.resolve(process.cwd(), "sift.config.yml"),
|
|
16
|
+
getDefaultGlobalConfigPath(),
|
|
17
|
+
path.join(os.homedir(), ".config", "sift", "config.yml")
|
|
18
|
+
];
|
|
19
|
+
}
|
|
15
20
|
var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
|
|
16
21
|
var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
|
|
17
22
|
var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
|
|
@@ -48,6 +53,29 @@ function evaluateGate(args) {
|
|
|
48
53
|
return { shouldFail: false };
|
|
49
54
|
}
|
|
50
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
|
+
|
|
51
79
|
// src/core/run.ts
|
|
52
80
|
import pc from "picocolors";
|
|
53
81
|
|
|
@@ -125,7 +153,7 @@ var OpenAIProvider = class {
|
|
|
125
153
|
if (!text) {
|
|
126
154
|
throw new Error("Provider returned an empty response");
|
|
127
155
|
}
|
|
128
|
-
|
|
156
|
+
const result = {
|
|
129
157
|
text,
|
|
130
158
|
usage: data?.usage ? {
|
|
131
159
|
inputTokens: data.usage.input_tokens,
|
|
@@ -134,13 +162,14 @@ var OpenAIProvider = class {
|
|
|
134
162
|
} : void 0,
|
|
135
163
|
raw: data
|
|
136
164
|
};
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
return result;
|
|
137
167
|
} catch (error) {
|
|
168
|
+
clearTimeout(timeout);
|
|
138
169
|
if (error.name === "AbortError") {
|
|
139
170
|
throw new Error("Provider request timed out");
|
|
140
171
|
}
|
|
141
172
|
throw error;
|
|
142
|
-
} finally {
|
|
143
|
-
clearTimeout(timeout);
|
|
144
173
|
}
|
|
145
174
|
}
|
|
146
175
|
};
|
|
@@ -222,7 +251,7 @@ var OpenAICompatibleProvider = class {
|
|
|
222
251
|
if (!text.trim()) {
|
|
223
252
|
throw new Error("Provider returned an empty response");
|
|
224
253
|
}
|
|
225
|
-
|
|
254
|
+
const result = {
|
|
226
255
|
text,
|
|
227
256
|
usage: data?.usage ? {
|
|
228
257
|
inputTokens: data.usage.prompt_tokens,
|
|
@@ -231,13 +260,14 @@ var OpenAICompatibleProvider = class {
|
|
|
231
260
|
} : void 0,
|
|
232
261
|
raw: data
|
|
233
262
|
};
|
|
263
|
+
clearTimeout(timeout);
|
|
264
|
+
return result;
|
|
234
265
|
} catch (error) {
|
|
266
|
+
clearTimeout(timeout);
|
|
235
267
|
if (error.name === "AbortError") {
|
|
236
268
|
throw new Error("Provider request timed out");
|
|
237
269
|
}
|
|
238
270
|
throw error;
|
|
239
|
-
} finally {
|
|
240
|
-
clearTimeout(timeout);
|
|
241
271
|
}
|
|
242
272
|
}
|
|
243
273
|
};
|
|
@@ -442,6 +472,19 @@ function buildPrompt(args) {
|
|
|
442
472
|
policyName: args.policyName,
|
|
443
473
|
outputContract: args.outputContract
|
|
444
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
|
+
] : [];
|
|
445
488
|
const prompt = [
|
|
446
489
|
"You are Sift, a CLI output reduction assistant for downstream agents and automation.",
|
|
447
490
|
"Hard rules:",
|
|
@@ -449,6 +492,7 @@ function buildPrompt(args) {
|
|
|
449
492
|
"",
|
|
450
493
|
`Task policy: ${policy.name}`,
|
|
451
494
|
...policy.taskRules.map((rule) => `- ${rule}`),
|
|
495
|
+
...detailRules.map((rule) => `- ${rule}`),
|
|
452
496
|
...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
|
|
453
497
|
"",
|
|
454
498
|
`Question: ${args.question}`,
|
|
@@ -561,6 +605,410 @@ function inferPackage(line) {
|
|
|
561
605
|
function inferRemediation(pkg) {
|
|
562
606
|
return `Upgrade ${pkg} to a patched version.`;
|
|
563
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
|
+
}
|
|
564
1012
|
function auditCriticalHeuristic(input) {
|
|
565
1013
|
const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
566
1014
|
if (!/\b(critical|high)\b/i.test(line)) {
|
|
@@ -631,7 +1079,7 @@ function infraRiskHeuristic(input) {
|
|
|
631
1079
|
}
|
|
632
1080
|
return null;
|
|
633
1081
|
}
|
|
634
|
-
function applyHeuristicPolicy(policyName, input) {
|
|
1082
|
+
function applyHeuristicPolicy(policyName, input, detail) {
|
|
635
1083
|
if (!policyName) {
|
|
636
1084
|
return null;
|
|
637
1085
|
}
|
|
@@ -641,6 +1089,9 @@ function applyHeuristicPolicy(policyName, input) {
|
|
|
641
1089
|
if (policyName === "infra-risk") {
|
|
642
1090
|
return infraRiskHeuristic(input);
|
|
643
1091
|
}
|
|
1092
|
+
if (policyName === "test-status") {
|
|
1093
|
+
return testStatusHeuristic(input, detail);
|
|
1094
|
+
}
|
|
644
1095
|
return null;
|
|
645
1096
|
}
|
|
646
1097
|
|
|
@@ -770,6 +1221,7 @@ function buildDryRunOutput(args) {
|
|
|
770
1221
|
},
|
|
771
1222
|
question: args.request.question,
|
|
772
1223
|
format: args.request.format,
|
|
1224
|
+
detail: args.request.detail ?? null,
|
|
773
1225
|
responseMode: args.responseMode,
|
|
774
1226
|
policy: args.request.policyName ?? null,
|
|
775
1227
|
heuristicOutput: args.heuristicOutput ?? null,
|
|
@@ -789,35 +1241,42 @@ function buildDryRunOutput(args) {
|
|
|
789
1241
|
async function delay(ms) {
|
|
790
1242
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
791
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
|
+
}
|
|
792
1254
|
async function generateWithRetry(args) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
process.stderr.write(
|
|
813
|
-
`${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}
|
|
814
1274
|
`
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
await delay(RETRY_DELAY_MS);
|
|
1275
|
+
);
|
|
818
1276
|
}
|
|
1277
|
+
await delay(RETRY_DELAY_MS);
|
|
819
1278
|
}
|
|
820
|
-
|
|
1279
|
+
return generate();
|
|
821
1280
|
}
|
|
822
1281
|
async function runSift(request) {
|
|
823
1282
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
@@ -825,6 +1284,7 @@ async function runSift(request) {
|
|
|
825
1284
|
question: request.question,
|
|
826
1285
|
format: request.format,
|
|
827
1286
|
input: prepared.truncated,
|
|
1287
|
+
detail: request.detail,
|
|
828
1288
|
policyName: request.policyName,
|
|
829
1289
|
outputContract: request.outputContract
|
|
830
1290
|
});
|
|
@@ -837,7 +1297,8 @@ async function runSift(request) {
|
|
|
837
1297
|
}
|
|
838
1298
|
const heuristicOutput = applyHeuristicPolicy(
|
|
839
1299
|
request.policyName,
|
|
840
|
-
prepared.truncated
|
|
1300
|
+
prepared.truncated,
|
|
1301
|
+
request.detail
|
|
841
1302
|
);
|
|
842
1303
|
if (heuristicOutput) {
|
|
843
1304
|
if (request.config.runtime.verbose) {
|
|
@@ -854,7 +1315,11 @@ async function runSift(request) {
|
|
|
854
1315
|
heuristicOutput
|
|
855
1316
|
});
|
|
856
1317
|
}
|
|
857
|
-
return
|
|
1318
|
+
return withInsufficientHint({
|
|
1319
|
+
output: heuristicOutput,
|
|
1320
|
+
request,
|
|
1321
|
+
prepared
|
|
1322
|
+
});
|
|
858
1323
|
}
|
|
859
1324
|
if (request.dryRun) {
|
|
860
1325
|
return buildDryRunOutput({
|
|
@@ -880,15 +1345,23 @@ async function runSift(request) {
|
|
|
880
1345
|
})) {
|
|
881
1346
|
throw new Error("Model output rejected by quality gate");
|
|
882
1347
|
}
|
|
883
|
-
return
|
|
1348
|
+
return withInsufficientHint({
|
|
1349
|
+
output: normalizeOutput(result.text, responseMode),
|
|
1350
|
+
request,
|
|
1351
|
+
prepared
|
|
1352
|
+
});
|
|
884
1353
|
} catch (error) {
|
|
885
1354
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
886
|
-
return
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
892
1365
|
});
|
|
893
1366
|
}
|
|
894
1367
|
}
|
|
@@ -971,6 +1444,15 @@ function buildCommandPreview(request) {
|
|
|
971
1444
|
}
|
|
972
1445
|
return (request.command ?? []).join(" ");
|
|
973
1446
|
}
|
|
1447
|
+
function getExecSuccessShortcut(args) {
|
|
1448
|
+
if (args.exitCode !== 0) {
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
|
|
1452
|
+
return "No type errors.";
|
|
1453
|
+
}
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
974
1456
|
async function runExec(request) {
|
|
975
1457
|
const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
|
|
976
1458
|
const hasShellCommand = typeof request.shellCommand === "string";
|
|
@@ -989,7 +1471,6 @@ async function runExec(request) {
|
|
|
989
1471
|
let bypassed = false;
|
|
990
1472
|
let childStatus = null;
|
|
991
1473
|
let childSignal = null;
|
|
992
|
-
let childSpawnError = null;
|
|
993
1474
|
const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
|
|
994
1475
|
stdio: ["inherit", "pipe", "pipe"]
|
|
995
1476
|
}) : spawn(request.command[0], request.command.slice(1), {
|
|
@@ -1017,7 +1498,6 @@ async function runExec(request) {
|
|
|
1017
1498
|
child.stderr.on("data", handleChunk);
|
|
1018
1499
|
await new Promise((resolve, reject) => {
|
|
1019
1500
|
child.on("error", (error) => {
|
|
1020
|
-
childSpawnError = error;
|
|
1021
1501
|
reject(error);
|
|
1022
1502
|
});
|
|
1023
1503
|
child.on("close", (status, signal) => {
|
|
@@ -1031,10 +1511,8 @@ async function runExec(request) {
|
|
|
1031
1511
|
}
|
|
1032
1512
|
throw new Error("Failed to start child process.");
|
|
1033
1513
|
});
|
|
1034
|
-
if (childSpawnError) {
|
|
1035
|
-
throw childSpawnError;
|
|
1036
|
-
}
|
|
1037
1514
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
1515
|
+
const capturedOutput = capture.render();
|
|
1038
1516
|
if (request.config.runtime.verbose) {
|
|
1039
1517
|
process.stderr.write(
|
|
1040
1518
|
`${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
@@ -1042,10 +1520,40 @@ async function runExec(request) {
|
|
|
1042
1520
|
);
|
|
1043
1521
|
}
|
|
1044
1522
|
if (!bypassed) {
|
|
1045
|
-
|
|
1523
|
+
if (request.showRaw && capturedOutput.length > 0) {
|
|
1524
|
+
process.stderr.write(capturedOutput);
|
|
1525
|
+
if (!capturedOutput.endsWith("\n")) {
|
|
1526
|
+
process.stderr.write("\n");
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
const execSuccessShortcut = getExecSuccessShortcut({
|
|
1530
|
+
presetName: request.presetName,
|
|
1531
|
+
exitCode,
|
|
1532
|
+
capturedOutput
|
|
1533
|
+
});
|
|
1534
|
+
if (execSuccessShortcut && !request.dryRun) {
|
|
1535
|
+
if (request.config.runtime.verbose) {
|
|
1536
|
+
process.stderr.write(
|
|
1537
|
+
`${pc2.dim("sift")} exec_shortcut=${request.presetName}
|
|
1538
|
+
`
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
process.stdout.write(`${execSuccessShortcut}
|
|
1542
|
+
`);
|
|
1543
|
+
return exitCode;
|
|
1544
|
+
}
|
|
1545
|
+
let output = await runSift({
|
|
1046
1546
|
...request,
|
|
1047
|
-
stdin:
|
|
1547
|
+
stdin: capturedOutput
|
|
1048
1548
|
});
|
|
1549
|
+
if (isInsufficientSignalOutput(output)) {
|
|
1550
|
+
output = buildInsufficientSignalOutput({
|
|
1551
|
+
presetName: request.presetName,
|
|
1552
|
+
originalLength: capture.getTotalChars(),
|
|
1553
|
+
truncatedApplied: capture.wasTruncated(),
|
|
1554
|
+
exitCode
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1049
1557
|
process.stdout.write(`${output}
|
|
1050
1558
|
`);
|
|
1051
1559
|
if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
|
|
@@ -1141,7 +1649,7 @@ function findConfigPath(explicitPath) {
|
|
|
1141
1649
|
}
|
|
1142
1650
|
return resolved;
|
|
1143
1651
|
}
|
|
1144
|
-
for (const candidate of
|
|
1652
|
+
for (const candidate of getDefaultConfigSearchPaths()) {
|
|
1145
1653
|
if (fs.existsSync(candidate)) {
|
|
1146
1654
|
return candidate;
|
|
1147
1655
|
}
|
|
@@ -1355,6 +1863,12 @@ function resolveConfig(options = {}) {
|
|
|
1355
1863
|
return siftConfigSchema.parse(merged);
|
|
1356
1864
|
}
|
|
1357
1865
|
export {
|
|
1866
|
+
BoundedCapture,
|
|
1867
|
+
buildCommandPreview,
|
|
1868
|
+
getExecSuccessShortcut,
|
|
1869
|
+
looksInteractivePrompt,
|
|
1870
|
+
mergeDefined,
|
|
1871
|
+
normalizeChildExitCode,
|
|
1358
1872
|
resolveConfig,
|
|
1359
1873
|
runExec,
|
|
1360
1874
|
runSift
|