@bilalimamoglu/sift 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +322 -82
- package/dist/cli.js +5404 -663
- package/dist/index.d.ts +43 -1
- package/dist/index.js +3471 -241
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -6,8 +6,14 @@ import pc2 from "picocolors";
|
|
|
6
6
|
// src/constants.ts
|
|
7
7
|
import os from "os";
|
|
8
8
|
import path from "path";
|
|
9
|
-
function getDefaultGlobalConfigPath() {
|
|
10
|
-
return path.join(
|
|
9
|
+
function getDefaultGlobalConfigPath(homeDir = os.homedir()) {
|
|
10
|
+
return path.join(homeDir, ".config", "sift", "config.yaml");
|
|
11
|
+
}
|
|
12
|
+
function getDefaultGlobalStateDir(homeDir = os.homedir()) {
|
|
13
|
+
return path.join(homeDir, ".config", "sift", "state");
|
|
14
|
+
}
|
|
15
|
+
function getDefaultTestStatusStatePath(homeDir = os.homedir()) {
|
|
16
|
+
return path.join(getDefaultGlobalStateDir(homeDir), "last-test-status.json");
|
|
11
17
|
}
|
|
12
18
|
function getDefaultConfigSearchPaths() {
|
|
13
19
|
return [
|
|
@@ -17,40 +23,2074 @@ function getDefaultConfigSearchPaths() {
|
|
|
17
23
|
path.join(os.homedir(), ".config", "sift", "config.yml")
|
|
18
24
|
];
|
|
19
25
|
}
|
|
20
|
-
var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
|
|
21
|
-
var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
|
|
22
|
-
var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
|
|
23
|
-
|
|
24
|
-
// src/core/gate.ts
|
|
25
|
-
var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
|
|
26
|
-
function parseJson(output) {
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(output);
|
|
29
|
-
} catch {
|
|
26
|
+
var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
|
|
27
|
+
var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
|
|
28
|
+
var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
|
|
29
|
+
|
|
30
|
+
// src/core/gate.ts
|
|
31
|
+
var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
|
|
32
|
+
function parseJson(output) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(output);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function supportsFailOnPreset(presetName) {
|
|
40
|
+
return typeof presetName === "string" && FAIL_ON_SUPPORTED_PRESETS.has(presetName);
|
|
41
|
+
}
|
|
42
|
+
function evaluateGate(args) {
|
|
43
|
+
const parsed = parseJson(args.output);
|
|
44
|
+
if (!parsed || typeof parsed !== "object") {
|
|
45
|
+
return { shouldFail: false };
|
|
46
|
+
}
|
|
47
|
+
if (args.presetName === "infra-risk") {
|
|
48
|
+
return {
|
|
49
|
+
shouldFail: parsed["verdict"] === "fail"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (args.presetName === "audit-critical") {
|
|
53
|
+
const status = parsed["status"];
|
|
54
|
+
const vulnerabilities = parsed["vulnerabilities"];
|
|
55
|
+
return {
|
|
56
|
+
shouldFail: status === "ok" && Array.isArray(vulnerabilities) && vulnerabilities.length > 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { shouldFail: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/core/testStatusDecision.ts
|
|
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[]}';
|
|
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,"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
|
+
var nextBestActionSchema = z.object({
|
|
67
|
+
code: z.enum([
|
|
68
|
+
"fix_dominant_blocker",
|
|
69
|
+
"read_source_for_bucket",
|
|
70
|
+
"read_raw_for_exact_traceback",
|
|
71
|
+
"insufficient_signal"
|
|
72
|
+
]),
|
|
73
|
+
bucket_index: z.number().int().nullable(),
|
|
74
|
+
note: z.string().min(1)
|
|
75
|
+
});
|
|
76
|
+
var testStatusProviderSupplementSchema = z.object({
|
|
77
|
+
diagnosis_complete: z.boolean(),
|
|
78
|
+
raw_needed: z.boolean(),
|
|
79
|
+
additional_source_read_likely_low_value: z.boolean(),
|
|
80
|
+
read_raw_only_if: z.string().nullable(),
|
|
81
|
+
decision: z.enum(["stop", "zoom", "read_source", "read_raw"]),
|
|
82
|
+
provider_confidence: z.number().min(0).max(1).nullable(),
|
|
83
|
+
next_best_action: nextBestActionSchema
|
|
84
|
+
});
|
|
85
|
+
var testStatusDiagnoseContractSchema = z.object({
|
|
86
|
+
status: z.enum(["ok", "insufficient"]),
|
|
87
|
+
diagnosis_complete: z.boolean(),
|
|
88
|
+
raw_needed: z.boolean(),
|
|
89
|
+
additional_source_read_likely_low_value: z.boolean(),
|
|
90
|
+
read_raw_only_if: z.string().nullable(),
|
|
91
|
+
decision: z.enum(["stop", "zoom", "read_source", "read_raw"]),
|
|
92
|
+
dominant_blocker_bucket_index: z.number().int().nullable(),
|
|
93
|
+
provider_used: z.boolean(),
|
|
94
|
+
provider_confidence: z.number().min(0).max(1).nullable(),
|
|
95
|
+
provider_failed: z.boolean(),
|
|
96
|
+
raw_slice_used: z.boolean(),
|
|
97
|
+
raw_slice_strategy: z.enum(["none", "bucket_evidence", "traceback_window", "head_tail"]),
|
|
98
|
+
resolved_tests: z.array(z.string()),
|
|
99
|
+
remaining_tests: z.array(z.string()),
|
|
100
|
+
main_buckets: z.array(
|
|
101
|
+
z.object({
|
|
102
|
+
bucket_index: z.number().int(),
|
|
103
|
+
label: z.string(),
|
|
104
|
+
count: z.number().int(),
|
|
105
|
+
root_cause: z.string(),
|
|
106
|
+
evidence: z.array(z.string()).max(2),
|
|
107
|
+
bucket_confidence: z.number(),
|
|
108
|
+
root_cause_confidence: z.number(),
|
|
109
|
+
dominant: z.boolean(),
|
|
110
|
+
secondary_visible_despite_blocker: z.boolean(),
|
|
111
|
+
mini_diff: z.object({
|
|
112
|
+
added_paths: z.number().int().optional(),
|
|
113
|
+
removed_models: z.number().int().optional(),
|
|
114
|
+
changed_task_mappings: z.number().int().optional()
|
|
115
|
+
}).nullable()
|
|
116
|
+
})
|
|
117
|
+
),
|
|
118
|
+
read_targets: z.array(
|
|
119
|
+
z.object({
|
|
120
|
+
file: z.string().min(1),
|
|
121
|
+
line: z.number().int().nullable(),
|
|
122
|
+
why: z.string().min(1),
|
|
123
|
+
bucket_index: z.number().int(),
|
|
124
|
+
context_hint: z.object({
|
|
125
|
+
start_line: z.number().int().nullable(),
|
|
126
|
+
end_line: z.number().int().nullable(),
|
|
127
|
+
search_hint: z.string().nullable()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
).max(5),
|
|
131
|
+
next_best_action: nextBestActionSchema
|
|
132
|
+
});
|
|
133
|
+
var testStatusTargetSummarySchema = z.object({
|
|
134
|
+
count: z.number().int().nonnegative(),
|
|
135
|
+
families: z.array(
|
|
136
|
+
z.object({
|
|
137
|
+
prefix: z.string().min(1),
|
|
138
|
+
count: z.number().int().nonnegative()
|
|
139
|
+
})
|
|
140
|
+
).max(5)
|
|
141
|
+
});
|
|
142
|
+
var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.omit({
|
|
143
|
+
resolved_tests: true,
|
|
144
|
+
remaining_tests: true
|
|
145
|
+
}).extend({
|
|
146
|
+
resolved_summary: testStatusTargetSummarySchema,
|
|
147
|
+
remaining_summary: testStatusTargetSummarySchema,
|
|
148
|
+
remaining_subset_available: z.boolean(),
|
|
149
|
+
resolved_tests: z.array(z.string()).optional(),
|
|
150
|
+
remaining_tests: z.array(z.string()).optional()
|
|
151
|
+
});
|
|
152
|
+
function parseTestStatusProviderSupplement(input) {
|
|
153
|
+
return testStatusProviderSupplementSchema.parse(JSON.parse(input));
|
|
154
|
+
}
|
|
155
|
+
function formatCount(count, singular, plural = `${singular}s`) {
|
|
156
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
157
|
+
}
|
|
158
|
+
function unique(values) {
|
|
159
|
+
return [...new Set(values)];
|
|
160
|
+
}
|
|
161
|
+
function normalizeTestId(value) {
|
|
162
|
+
return value.replace(/\\/g, "/").trim();
|
|
163
|
+
}
|
|
164
|
+
function extractTestFamilyPrefix(value) {
|
|
165
|
+
const normalized = normalizeTestId(value);
|
|
166
|
+
const testsMatch = normalized.match(/^(tests\/[^/]+\/)/);
|
|
167
|
+
if (testsMatch) {
|
|
168
|
+
return testsMatch[1];
|
|
169
|
+
}
|
|
170
|
+
const filePart = normalized.split("::")[0]?.trim() ?? "";
|
|
171
|
+
if (!filePart.includes("/")) {
|
|
172
|
+
return "other";
|
|
173
|
+
}
|
|
174
|
+
const segments = filePart.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
175
|
+
if (segments.length === 0) {
|
|
176
|
+
return "other";
|
|
177
|
+
}
|
|
178
|
+
return `${segments[0]}/`;
|
|
179
|
+
}
|
|
180
|
+
function buildTestTargetSummary(values) {
|
|
181
|
+
const counts = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const value of values) {
|
|
183
|
+
const prefix = extractTestFamilyPrefix(value);
|
|
184
|
+
counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
|
|
185
|
+
}
|
|
186
|
+
const families = [...counts.entries()].map(([prefix, count]) => ({
|
|
187
|
+
prefix,
|
|
188
|
+
count
|
|
189
|
+
})).sort((left, right) => {
|
|
190
|
+
if (right.count !== left.count) {
|
|
191
|
+
return right.count - left.count;
|
|
192
|
+
}
|
|
193
|
+
return left.prefix.localeCompare(right.prefix);
|
|
194
|
+
}).slice(0, 5);
|
|
195
|
+
return {
|
|
196
|
+
count: values.length,
|
|
197
|
+
families
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function formatTargetSummary(summary) {
|
|
201
|
+
if (summary.count === 0) {
|
|
202
|
+
return "count=0";
|
|
203
|
+
}
|
|
204
|
+
const families = summary.families.length > 0 ? summary.families.map((family) => `${family.prefix}${family.count}`).join(", ") : "none";
|
|
205
|
+
return `count=${summary.count}; families=${families}`;
|
|
206
|
+
}
|
|
207
|
+
function classifyGenericBucketType(reason) {
|
|
208
|
+
if (reason.startsWith("missing test env:")) {
|
|
209
|
+
return "shared_environment_blocker";
|
|
210
|
+
}
|
|
211
|
+
if (reason.startsWith("fixture guard:")) {
|
|
212
|
+
return "collection_failure";
|
|
213
|
+
}
|
|
214
|
+
if (reason.startsWith("service unavailable:")) {
|
|
215
|
+
return "runtime_failure";
|
|
216
|
+
}
|
|
217
|
+
if (reason.startsWith("db refused:")) {
|
|
218
|
+
return "runtime_failure";
|
|
219
|
+
}
|
|
220
|
+
if (reason.startsWith("auth bypass absent:")) {
|
|
221
|
+
return "runtime_failure";
|
|
222
|
+
}
|
|
223
|
+
if (reason.startsWith("missing module:")) {
|
|
224
|
+
return "import_dependency_failure";
|
|
225
|
+
}
|
|
226
|
+
if (reason.startsWith("assertion failed:")) {
|
|
227
|
+
return "assertion_failure";
|
|
228
|
+
}
|
|
229
|
+
if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
|
|
230
|
+
return "runtime_failure";
|
|
231
|
+
}
|
|
232
|
+
return "unknown_failure";
|
|
233
|
+
}
|
|
234
|
+
function buildGenericBuckets(analysis) {
|
|
235
|
+
const buckets = [];
|
|
236
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
237
|
+
const push = (reason, item) => {
|
|
238
|
+
const key = `${classifyGenericBucketType(reason)}:${reason}`;
|
|
239
|
+
const existing = grouped.get(key);
|
|
240
|
+
if (existing) {
|
|
241
|
+
existing.count += 1;
|
|
242
|
+
if (!existing.representativeItems.some((entry) => entry.label === item.label) && existing.representativeItems.length < 6) {
|
|
243
|
+
existing.representativeItems.push(item);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
grouped.set(key, {
|
|
248
|
+
type: classifyGenericBucketType(reason),
|
|
249
|
+
headline: "",
|
|
250
|
+
summaryLines: [],
|
|
251
|
+
reason,
|
|
252
|
+
count: 1,
|
|
253
|
+
confidence: reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62,
|
|
254
|
+
representativeItems: [item],
|
|
255
|
+
entities: [],
|
|
256
|
+
hint: void 0,
|
|
257
|
+
overflowCount: 0,
|
|
258
|
+
overflowLabel: "failing tests/modules"
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
for (const item of [...analysis.collectionItems, ...analysis.inlineItems]) {
|
|
262
|
+
push(item.reason, item);
|
|
263
|
+
}
|
|
264
|
+
for (const bucket of grouped.values()) {
|
|
265
|
+
const title = bucket.type === "assertion_failure" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures";
|
|
266
|
+
bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
|
|
267
|
+
bucket.summaryLines = [bucket.headline];
|
|
268
|
+
bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
|
|
269
|
+
buckets.push(bucket);
|
|
270
|
+
}
|
|
271
|
+
return buckets.sort((left, right) => right.count - left.count);
|
|
272
|
+
}
|
|
273
|
+
function normalizeBucketIdentity(bucket) {
|
|
274
|
+
return `${bucket.type}:${bucket.reason.toLowerCase().replace(/\s+/g, " ").trim()}`;
|
|
275
|
+
}
|
|
276
|
+
function mergeRepresentativeItems(left, right) {
|
|
277
|
+
const merged = [...left];
|
|
278
|
+
for (const item of right) {
|
|
279
|
+
if (merged.some(
|
|
280
|
+
(existing) => existing.label === item.label && existing.reason === item.reason
|
|
281
|
+
)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (merged.length >= 6) {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
merged.push(item);
|
|
288
|
+
}
|
|
289
|
+
return merged;
|
|
290
|
+
}
|
|
291
|
+
function mergeBucketDetails(existing, incoming) {
|
|
292
|
+
const representativeItems = mergeRepresentativeItems(
|
|
293
|
+
existing.representativeItems,
|
|
294
|
+
incoming.representativeItems
|
|
295
|
+
);
|
|
296
|
+
const count = Math.max(existing.count, incoming.count);
|
|
297
|
+
return {
|
|
298
|
+
...existing,
|
|
299
|
+
headline: existing.summaryLines.length >= incoming.summaryLines.length && existing.headline.length >= incoming.headline.length ? existing.headline : incoming.headline,
|
|
300
|
+
summaryLines: existing.summaryLines.length >= incoming.summaryLines.length ? existing.summaryLines : incoming.summaryLines,
|
|
301
|
+
count,
|
|
302
|
+
confidence: Math.max(existing.confidence, incoming.confidence),
|
|
303
|
+
representativeItems,
|
|
304
|
+
entities: unique([...existing.entities, ...incoming.entities]),
|
|
305
|
+
hint: existing.hint ?? incoming.hint,
|
|
306
|
+
overflowCount: Math.max(
|
|
307
|
+
existing.overflowCount,
|
|
308
|
+
incoming.overflowCount,
|
|
309
|
+
count - representativeItems.length
|
|
310
|
+
),
|
|
311
|
+
overflowLabel: existing.overflowLabel || incoming.overflowLabel
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function mergeBuckets(analysis) {
|
|
315
|
+
const mergedByIdentity = /* @__PURE__ */ new Map();
|
|
316
|
+
const merged = [];
|
|
317
|
+
const pushBucket = (bucket) => {
|
|
318
|
+
const identity = normalizeBucketIdentity(bucket);
|
|
319
|
+
const existing = mergedByIdentity.get(identity);
|
|
320
|
+
if (existing) {
|
|
321
|
+
const replacement = mergeBucketDetails(existing, bucket);
|
|
322
|
+
const index = merged.indexOf(existing);
|
|
323
|
+
if (index >= 0) {
|
|
324
|
+
merged[index] = replacement;
|
|
325
|
+
}
|
|
326
|
+
mergedByIdentity.set(identity, replacement);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
merged.push(bucket);
|
|
330
|
+
mergedByIdentity.set(identity, bucket);
|
|
331
|
+
};
|
|
332
|
+
for (const bucket of analysis.buckets.map((bucket2) => ({
|
|
333
|
+
type: bucket2.type,
|
|
334
|
+
headline: bucket2.headline,
|
|
335
|
+
summaryLines: [...bucket2.summaryLines],
|
|
336
|
+
reason: bucket2.reason,
|
|
337
|
+
count: bucket2.countClaimed ?? bucket2.countVisible,
|
|
338
|
+
confidence: bucket2.confidence,
|
|
339
|
+
representativeItems: [...bucket2.representativeItems],
|
|
340
|
+
entities: [...bucket2.entities],
|
|
341
|
+
hint: bucket2.hint,
|
|
342
|
+
overflowCount: bucket2.overflowCount,
|
|
343
|
+
overflowLabel: bucket2.overflowLabel
|
|
344
|
+
}))) {
|
|
345
|
+
pushBucket(bucket);
|
|
346
|
+
}
|
|
347
|
+
const coveredLabels = new Set(
|
|
348
|
+
merged.flatMap((bucket) => bucket.representativeItems.map((item) => item.label))
|
|
349
|
+
);
|
|
350
|
+
for (const bucket of buildGenericBuckets(analysis)) {
|
|
351
|
+
const identity = normalizeBucketIdentity(bucket);
|
|
352
|
+
const unseenItems = bucket.representativeItems.filter(
|
|
353
|
+
(item) => !coveredLabels.has(item.label)
|
|
354
|
+
);
|
|
355
|
+
if (!mergedByIdentity.has(identity) && unseenItems.length === 0) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
pushBucket({
|
|
359
|
+
...bucket,
|
|
360
|
+
count: Math.max(bucket.count, unseenItems.length),
|
|
361
|
+
representativeItems: mergedByIdentity.has(identity) || unseenItems.length === 0 ? bucket.representativeItems : unseenItems
|
|
362
|
+
});
|
|
363
|
+
for (const item of bucket.representativeItems) {
|
|
364
|
+
coveredLabels.add(item.label);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return merged;
|
|
368
|
+
}
|
|
369
|
+
function dominantBucketPriority(bucket) {
|
|
370
|
+
if (bucket.reason.startsWith("missing test env:")) {
|
|
371
|
+
return 5;
|
|
372
|
+
}
|
|
373
|
+
if (bucket.type === "shared_environment_blocker") {
|
|
374
|
+
return 4;
|
|
375
|
+
}
|
|
376
|
+
if (bucket.type === "import_dependency_failure") {
|
|
377
|
+
return 3;
|
|
378
|
+
}
|
|
379
|
+
if (bucket.type === "collection_failure") {
|
|
380
|
+
return 2;
|
|
381
|
+
}
|
|
382
|
+
if (bucket.type === "contract_snapshot_drift") {
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
function prioritizeBuckets(buckets) {
|
|
388
|
+
return [...buckets].sort((left, right) => {
|
|
389
|
+
const priorityDelta = dominantBucketPriority(right) - dominantBucketPriority(left);
|
|
390
|
+
if (priorityDelta !== 0) {
|
|
391
|
+
return priorityDelta;
|
|
392
|
+
}
|
|
393
|
+
if (right.count !== left.count) {
|
|
394
|
+
return right.count - left.count;
|
|
395
|
+
}
|
|
396
|
+
if (right.confidence !== left.confidence) {
|
|
397
|
+
return right.confidence - left.confidence;
|
|
398
|
+
}
|
|
399
|
+
return left.reason.localeCompare(right.reason);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function isDominantBlockerType(type) {
|
|
403
|
+
return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
|
|
404
|
+
}
|
|
405
|
+
function labelForBucket(bucket) {
|
|
406
|
+
if (bucket.reason.startsWith("missing test env:")) {
|
|
407
|
+
return "missing test env";
|
|
408
|
+
}
|
|
409
|
+
if (bucket.reason.startsWith("fixture guard:")) {
|
|
410
|
+
return "fixture guard";
|
|
411
|
+
}
|
|
412
|
+
if (bucket.reason.startsWith("service unavailable:")) {
|
|
413
|
+
return "service unavailable";
|
|
414
|
+
}
|
|
415
|
+
if (bucket.reason.startsWith("db refused:")) {
|
|
416
|
+
return "db refused";
|
|
417
|
+
}
|
|
418
|
+
if (bucket.reason.startsWith("auth bypass absent:")) {
|
|
419
|
+
return "auth bypass absent";
|
|
420
|
+
}
|
|
421
|
+
if (bucket.type === "contract_snapshot_drift") {
|
|
422
|
+
if (/openapi/i.test(bucket.headline) || bucket.entities.some((value) => value.startsWith("/api/"))) {
|
|
423
|
+
return "route drift";
|
|
424
|
+
}
|
|
425
|
+
if (/schema/i.test(bucket.headline)) {
|
|
426
|
+
return "schema freeze mismatch";
|
|
427
|
+
}
|
|
428
|
+
if (/model/i.test(bucket.headline)) {
|
|
429
|
+
return "model catalog drift";
|
|
430
|
+
}
|
|
431
|
+
return "stale snapshot";
|
|
432
|
+
}
|
|
433
|
+
if (bucket.type === "import_dependency_failure") {
|
|
434
|
+
return "import dependency failure";
|
|
435
|
+
}
|
|
436
|
+
if (bucket.type === "assertion_failure") {
|
|
437
|
+
return "assertion failure";
|
|
438
|
+
}
|
|
439
|
+
if (bucket.type === "collection_failure") {
|
|
440
|
+
return "collection failure";
|
|
441
|
+
}
|
|
442
|
+
if (bucket.type === "runtime_failure") {
|
|
443
|
+
return "runtime failure";
|
|
444
|
+
}
|
|
445
|
+
return "unknown failure";
|
|
446
|
+
}
|
|
447
|
+
function rootCauseConfidenceFor(bucket) {
|
|
448
|
+
if (bucket.reason.startsWith("missing test env:") || bucket.reason.startsWith("missing module:") || bucket.reason.startsWith("db refused:") || bucket.reason.startsWith("service unavailable:") || bucket.reason.startsWith("auth bypass absent:")) {
|
|
449
|
+
return 0.95;
|
|
450
|
+
}
|
|
451
|
+
if (bucket.type === "contract_snapshot_drift") {
|
|
452
|
+
return bucket.entities.length > 0 ? 0.92 : 0.76;
|
|
453
|
+
}
|
|
454
|
+
return Math.max(0.6, Math.min(bucket.confidence, 0.88));
|
|
455
|
+
}
|
|
456
|
+
function buildBucketEvidence(bucket) {
|
|
457
|
+
const evidence = bucket.representativeItems.slice(0, 2).map((item) => `${item.label} -> ${item.reason}`);
|
|
458
|
+
if (evidence.length > 0) {
|
|
459
|
+
return evidence;
|
|
460
|
+
}
|
|
461
|
+
return bucket.entities.slice(0, 2);
|
|
462
|
+
}
|
|
463
|
+
function formatReadTargetLocation(target) {
|
|
464
|
+
return target.line === null ? target.file : `${target.file}:${target.line}`;
|
|
465
|
+
}
|
|
466
|
+
function buildReadTargetContextHint(args) {
|
|
467
|
+
if (args.anchor.line !== null) {
|
|
468
|
+
return {
|
|
469
|
+
start_line: Math.max(1, args.anchor.line - 5),
|
|
470
|
+
end_line: args.anchor.line + 5,
|
|
471
|
+
search_hint: null
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
start_line: null,
|
|
476
|
+
end_line: null,
|
|
477
|
+
search_hint: buildReadTargetSearchHint(args.bucket, args.anchor)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function buildReadTargetWhy(args) {
|
|
481
|
+
const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
482
|
+
if (envVar) {
|
|
483
|
+
return `it contains the ${envVar} setup guard`;
|
|
484
|
+
}
|
|
485
|
+
if (args.bucket.reason.startsWith("fixture guard:")) {
|
|
486
|
+
return "it contains the fixture/setup guard behind this bucket";
|
|
487
|
+
}
|
|
488
|
+
if (args.bucket.reason.startsWith("db refused:")) {
|
|
489
|
+
return "it contains the database connection setup behind this bucket";
|
|
490
|
+
}
|
|
491
|
+
if (args.bucket.reason.startsWith("service unavailable:")) {
|
|
492
|
+
return "it contains the dependency service call or setup behind this bucket";
|
|
493
|
+
}
|
|
494
|
+
if (args.bucket.reason.startsWith("auth bypass absent:")) {
|
|
495
|
+
return "it contains the auth bypass setup behind this bucket";
|
|
496
|
+
}
|
|
497
|
+
if (args.bucket.type === "contract_snapshot_drift") {
|
|
498
|
+
if (args.bucketLabel === "route drift") {
|
|
499
|
+
return "it maps to the visible route drift bucket";
|
|
500
|
+
}
|
|
501
|
+
if (args.bucketLabel === "model catalog drift") {
|
|
502
|
+
return "it maps to the visible model drift bucket";
|
|
503
|
+
}
|
|
504
|
+
if (args.bucketLabel === "schema freeze mismatch") {
|
|
505
|
+
return "it maps to the visible schema freeze mismatch";
|
|
506
|
+
}
|
|
507
|
+
return "it maps to the visible stale snapshot expectation";
|
|
508
|
+
}
|
|
509
|
+
if (args.bucket.type === "import_dependency_failure") {
|
|
510
|
+
return "it is the first visible failing module in this missing dependency bucket";
|
|
511
|
+
}
|
|
512
|
+
if (args.bucket.type === "assertion_failure") {
|
|
513
|
+
return "it is the first visible failing test in this bucket";
|
|
514
|
+
}
|
|
515
|
+
if (args.bucket.type === "collection_failure") {
|
|
516
|
+
return "it is the first visible collection/setup anchor for this bucket";
|
|
517
|
+
}
|
|
518
|
+
return `it maps to the visible ${args.bucketLabel} bucket`;
|
|
519
|
+
}
|
|
520
|
+
function buildReadTargetSearchHint(bucket, anchor) {
|
|
521
|
+
const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
522
|
+
if (envVar) {
|
|
523
|
+
return envVar;
|
|
524
|
+
}
|
|
525
|
+
if (bucket.type === "contract_snapshot_drift") {
|
|
526
|
+
return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
|
|
527
|
+
}
|
|
528
|
+
const missingModule = bucket.reason.match(/^missing module:\s+(.+)$/)?.[1];
|
|
529
|
+
if (missingModule) {
|
|
530
|
+
return missingModule;
|
|
531
|
+
}
|
|
532
|
+
const fixtureGuard = bucket.reason.match(/^fixture guard:\s+(.+)$/)?.[1];
|
|
533
|
+
if (fixtureGuard) {
|
|
534
|
+
return fixtureGuard;
|
|
535
|
+
}
|
|
536
|
+
const serviceMarker = bucket.reason.match(
|
|
537
|
+
/^(?:service unavailable|db refused|auth bypass absent):\s+(.+)$/
|
|
538
|
+
)?.[1];
|
|
539
|
+
if (serviceMarker) {
|
|
540
|
+
return serviceMarker;
|
|
541
|
+
}
|
|
542
|
+
const assertionText = bucket.reason.match(/^assertion failed:\s+(.+)$/)?.[1];
|
|
543
|
+
if (assertionText) {
|
|
544
|
+
return assertionText;
|
|
545
|
+
}
|
|
546
|
+
const fallbackLabel = anchor.label.split("::")[1]?.trim();
|
|
547
|
+
return fallbackLabel || null;
|
|
548
|
+
}
|
|
549
|
+
function buildReadTargets(args) {
|
|
550
|
+
return args.buckets.map((bucket, index) => ({
|
|
551
|
+
bucket,
|
|
552
|
+
bucketIndex: index + 1,
|
|
553
|
+
bucketLabel: labelForBucket(bucket),
|
|
554
|
+
dominant: args.dominantBucketIndex === index + 1
|
|
555
|
+
})).sort((left, right) => {
|
|
556
|
+
if (left.dominant !== right.dominant) {
|
|
557
|
+
return left.dominant ? -1 : 1;
|
|
558
|
+
}
|
|
559
|
+
return left.bucketIndex - right.bucketIndex;
|
|
560
|
+
}).flatMap(({ bucket, bucketIndex, bucketLabel }) => {
|
|
561
|
+
const anchor = [...bucket.representativeItems].filter((item) => item.file).sort((left, right) => {
|
|
562
|
+
if (left.line !== null !== (right.line !== null)) {
|
|
563
|
+
return left.line !== null ? -1 : 1;
|
|
564
|
+
}
|
|
565
|
+
if (right.anchor_confidence !== left.anchor_confidence) {
|
|
566
|
+
return right.anchor_confidence - left.anchor_confidence;
|
|
567
|
+
}
|
|
568
|
+
return left.label.localeCompare(right.label);
|
|
569
|
+
})[0];
|
|
570
|
+
if (!anchor?.file) {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
return [
|
|
574
|
+
{
|
|
575
|
+
file: anchor.file,
|
|
576
|
+
line: anchor.line,
|
|
577
|
+
why: buildReadTargetWhy({
|
|
578
|
+
bucket,
|
|
579
|
+
bucketLabel
|
|
580
|
+
}),
|
|
581
|
+
bucket_index: bucketIndex,
|
|
582
|
+
context_hint: buildReadTargetContextHint({
|
|
583
|
+
bucket,
|
|
584
|
+
anchor
|
|
585
|
+
})
|
|
586
|
+
}
|
|
587
|
+
];
|
|
588
|
+
}).slice(0, 5);
|
|
589
|
+
}
|
|
590
|
+
function buildConcreteNextNote(args) {
|
|
591
|
+
const primaryTarget = args.readTargets.find((target) => target.bucket_index === args.nextBestAction.bucket_index) ?? args.readTargets[0];
|
|
592
|
+
if (!primaryTarget) {
|
|
593
|
+
return args.nextBestAction.note;
|
|
594
|
+
}
|
|
595
|
+
const lead = primaryTarget.context_hint.start_line !== null && primaryTarget.context_hint.end_line !== null ? `Read ${primaryTarget.file} lines ${primaryTarget.context_hint.start_line}-${primaryTarget.context_hint.end_line} first; ${primaryTarget.why}.` : primaryTarget.context_hint.search_hint ? `Search for ${primaryTarget.context_hint.search_hint} in ${primaryTarget.file} first; ${primaryTarget.why}.` : `Read ${formatReadTargetLocation(primaryTarget)} first; ${primaryTarget.why}.`;
|
|
596
|
+
if (args.nextBestAction.code === "fix_dominant_blocker") {
|
|
597
|
+
if (args.nextBestAction.bucket_index === 1 && args.hasSecondaryVisibleBucket) {
|
|
598
|
+
return "Fix bucket 1 first, then rerun the full suite at standard. Secondary buckets are already visible behind it.";
|
|
599
|
+
}
|
|
600
|
+
return `Fix bucket ${args.nextBestAction.bucket_index ?? 1} first, then rerun the full suite at standard.`;
|
|
601
|
+
}
|
|
602
|
+
if (args.nextBestAction.code === "read_source_for_bucket") {
|
|
603
|
+
return lead;
|
|
604
|
+
}
|
|
605
|
+
return args.nextBestAction.note;
|
|
606
|
+
}
|
|
607
|
+
function extractMiniDiff(input, bucket) {
|
|
608
|
+
if (bucket.type !== "contract_snapshot_drift") {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const addedPaths = unique(
|
|
612
|
+
[...input.matchAll(/[+-]\s+'(\/api\/[^']+)'/g)].map((match) => match[1])
|
|
613
|
+
).length;
|
|
614
|
+
const removedModels = unique(
|
|
615
|
+
[...input.matchAll(/[+-]\s+'([A-Za-z0-9._/-]+-[A-Za-z0-9._-]+)'/g)].map((match) => match[1])
|
|
616
|
+
).length;
|
|
617
|
+
const changedTaskMappings = unique(
|
|
618
|
+
[...input.matchAll(/[+-]\s+'([a-z]+(?:_[a-z0-9]+)+)'/g)].map((match) => match[1])
|
|
619
|
+
).length;
|
|
620
|
+
if (addedPaths === 0 && removedModels === 0 && changedTaskMappings === 0) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
...addedPaths > 0 ? { added_paths: addedPaths } : {},
|
|
625
|
+
...removedModels > 0 ? { removed_models: removedModels } : {},
|
|
626
|
+
...changedTaskMappings > 0 ? { changed_task_mappings: changedTaskMappings } : {}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function buildOutcomeLines(analysis) {
|
|
630
|
+
if (analysis.noTestsCollected) {
|
|
631
|
+
return ["- Tests did not run.", "- Collected 0 items."];
|
|
632
|
+
}
|
|
633
|
+
if (analysis.failed === 0 && analysis.errors === 0 && analysis.passed > 0) {
|
|
634
|
+
const parts = [formatCount(analysis.passed, "test")];
|
|
635
|
+
if (analysis.skipped > 0) {
|
|
636
|
+
parts.push(formatCount(analysis.skipped, "skip"));
|
|
637
|
+
}
|
|
638
|
+
return ["- Tests passed.", `- ${parts.join(", ")}.`];
|
|
639
|
+
}
|
|
640
|
+
if (analysis.collectionErrorCount && analysis.failed === 0) {
|
|
641
|
+
return [
|
|
642
|
+
"- Tests did not complete.",
|
|
643
|
+
`- ${formatCount(analysis.collectionErrorCount, "error")} occurred during collection.`
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
const counts = [];
|
|
647
|
+
if (analysis.failed > 0) {
|
|
648
|
+
counts.push(formatCount(analysis.failed, "test failed", "tests failed"));
|
|
649
|
+
}
|
|
650
|
+
if (analysis.errors > 0) {
|
|
651
|
+
counts.push(formatCount(analysis.errors, "error occurred", "errors occurred"));
|
|
652
|
+
}
|
|
653
|
+
if (counts.length === 0) {
|
|
654
|
+
return ["- Tests did not pass."];
|
|
655
|
+
}
|
|
656
|
+
return ["- Tests did not pass.", `- ${counts.join(". ")}.`];
|
|
657
|
+
}
|
|
658
|
+
function buildStopSignal(contract) {
|
|
659
|
+
if (contract.diagnosis_complete && !contract.raw_needed) {
|
|
660
|
+
return "- Stop signal: diagnosis complete; raw not needed.";
|
|
661
|
+
}
|
|
662
|
+
if (contract.raw_needed && contract.read_raw_only_if) {
|
|
663
|
+
return `- Stop signal: diagnosis incomplete; raw only if ${contract.read_raw_only_if}.`;
|
|
664
|
+
}
|
|
665
|
+
return "- Stop signal: diagnosis incomplete; provider or raw traceback may still help.";
|
|
666
|
+
}
|
|
667
|
+
function deriveDecision(contract) {
|
|
668
|
+
if (contract.raw_needed || contract.provider_failed) {
|
|
669
|
+
return "read_raw";
|
|
670
|
+
}
|
|
671
|
+
if (!contract.diagnosis_complete) {
|
|
672
|
+
return "zoom";
|
|
673
|
+
}
|
|
674
|
+
if (contract.main_buckets.length === 0 && contract.next_best_action.note === "No failing buckets remain.") {
|
|
675
|
+
return "stop";
|
|
676
|
+
}
|
|
677
|
+
if (contract.next_best_action.code === "read_source_for_bucket") {
|
|
678
|
+
return "read_source";
|
|
679
|
+
}
|
|
680
|
+
return "stop";
|
|
681
|
+
}
|
|
682
|
+
function buildDecisionLine(contract) {
|
|
683
|
+
if (contract.decision === "stop") {
|
|
684
|
+
return "- Decision: stop and act. Do not escalate unless you need exact traceback lines.";
|
|
685
|
+
}
|
|
686
|
+
if (contract.decision === "read_source") {
|
|
687
|
+
return "- Decision: read source next. Do not escalate unless exact traceback lines are still needed.";
|
|
688
|
+
}
|
|
689
|
+
if (contract.decision === "zoom") {
|
|
690
|
+
return "- Decision: zoom. One deeper sift pass is justified before raw.";
|
|
691
|
+
}
|
|
692
|
+
return "- Decision: raw only if exact traceback is required.";
|
|
693
|
+
}
|
|
694
|
+
function buildComparisonLines(contract) {
|
|
695
|
+
const lines = [];
|
|
696
|
+
if (contract.resolved_tests.length > 0) {
|
|
697
|
+
lines.push(
|
|
698
|
+
`- Resolved in this rerun: ${formatCount(contract.resolved_tests.length, "test")} dropped out of the failing set.`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
if (contract.resolved_tests.length > 0 && contract.remaining_tests.length > 0) {
|
|
702
|
+
lines.push(
|
|
703
|
+
`- Remaining failing targets: ${formatCount(contract.remaining_tests.length, "test/module", "tests/modules")}.`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
return lines;
|
|
707
|
+
}
|
|
708
|
+
function renderBucketHeadline(bucket) {
|
|
709
|
+
return `- Bucket ${bucket.bucket_index}: ${bucket.label} (${bucket.count}) -> ${bucket.root_cause}`;
|
|
710
|
+
}
|
|
711
|
+
function buildStandardAnchorText(target) {
|
|
712
|
+
if (!target) {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
if (target.context_hint.start_line !== null && target.context_hint.end_line !== null) {
|
|
716
|
+
return `${target.file} lines ${target.context_hint.start_line}-${target.context_hint.end_line}`;
|
|
717
|
+
}
|
|
718
|
+
if (target.context_hint.search_hint) {
|
|
719
|
+
return `search ${target.context_hint.search_hint} in ${target.file}`;
|
|
720
|
+
}
|
|
721
|
+
return formatReadTargetLocation(target);
|
|
722
|
+
}
|
|
723
|
+
function buildStandardFixText(args) {
|
|
724
|
+
if (args.bucket.hint) {
|
|
725
|
+
return args.bucket.hint;
|
|
726
|
+
}
|
|
727
|
+
const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
728
|
+
if (envVar) {
|
|
729
|
+
return `Set ${envVar} before rerunning the affected tests.`;
|
|
730
|
+
}
|
|
731
|
+
const missingModule = args.bucket.reason.match(/^missing module:\s+(.+)$/)?.[1];
|
|
732
|
+
if (missingModule) {
|
|
733
|
+
return `Install ${missingModule} and rerun the affected tests.`;
|
|
734
|
+
}
|
|
735
|
+
if (args.bucket.reason.startsWith("fixture guard:")) {
|
|
736
|
+
return "Restore the missing fixture/setup guard and rerun the full suite at standard.";
|
|
737
|
+
}
|
|
738
|
+
if (args.bucket.reason.startsWith("db refused:")) {
|
|
739
|
+
return "Fix the test database connectivity and rerun the full suite at standard.";
|
|
740
|
+
}
|
|
741
|
+
if (args.bucket.reason.startsWith("service unavailable:")) {
|
|
742
|
+
return "Restore the dependency service or test double and rerun the full suite at standard.";
|
|
743
|
+
}
|
|
744
|
+
if (args.bucket.reason.startsWith("auth bypass absent:")) {
|
|
745
|
+
return "Restore the test auth bypass setup and rerun the full suite at standard.";
|
|
746
|
+
}
|
|
747
|
+
if (args.bucket.type === "contract_snapshot_drift") {
|
|
748
|
+
return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
|
|
749
|
+
}
|
|
750
|
+
if (args.bucket.type === "assertion_failure") {
|
|
751
|
+
return "Inspect the failing assertion and rerun the full suite at standard.";
|
|
752
|
+
}
|
|
753
|
+
if (args.bucket.type === "collection_failure") {
|
|
754
|
+
return "Fix the collection/setup failure and rerun the full suite at standard.";
|
|
755
|
+
}
|
|
756
|
+
if (args.bucket.type === "runtime_failure") {
|
|
757
|
+
return `Fix the visible ${args.bucketLabel} and rerun the full suite at standard.`;
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
function buildStandardBucketSupport(args) {
|
|
762
|
+
return {
|
|
763
|
+
headline: args.bucket.summaryLines[0] ? `- ${args.bucket.summaryLines[0]}` : renderBucketHeadline(args.contractBucket),
|
|
764
|
+
anchorText: buildStandardAnchorText(args.readTarget),
|
|
765
|
+
fixText: buildStandardFixText({
|
|
766
|
+
bucket: args.bucket,
|
|
767
|
+
bucketLabel: args.contractBucket.label
|
|
768
|
+
})
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function renderStandard(args) {
|
|
772
|
+
const lines = [...buildOutcomeLines(args.analysis), ...buildComparisonLines(args.contract)];
|
|
773
|
+
if (args.contract.main_buckets.length > 0) {
|
|
774
|
+
for (const bucket of args.contract.main_buckets.slice(0, 3)) {
|
|
775
|
+
const rawBucket = args.buckets[bucket.bucket_index - 1];
|
|
776
|
+
if (!rawBucket) {
|
|
777
|
+
lines.push(renderBucketHeadline(bucket));
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const support = buildStandardBucketSupport({
|
|
781
|
+
bucket: rawBucket,
|
|
782
|
+
contractBucket: bucket,
|
|
783
|
+
readTarget: args.contract.read_targets.find(
|
|
784
|
+
(target) => target.bucket_index === bucket.bucket_index
|
|
785
|
+
)
|
|
786
|
+
});
|
|
787
|
+
lines.push(support.headline);
|
|
788
|
+
if (support.anchorText) {
|
|
789
|
+
lines.push(`- Anchor: ${support.anchorText}`);
|
|
790
|
+
}
|
|
791
|
+
if (support.fixText) {
|
|
792
|
+
lines.push(`- Fix: ${support.fixText}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
lines.push(buildDecisionLine(args.contract));
|
|
797
|
+
lines.push(`- Next: ${args.contract.next_best_action.note}`);
|
|
798
|
+
lines.push(buildStopSignal(args.contract));
|
|
799
|
+
return lines.join("\n");
|
|
800
|
+
}
|
|
801
|
+
function renderFocused(args) {
|
|
802
|
+
const lines = [...buildOutcomeLines(args.analysis), ...buildComparisonLines(args.contract)];
|
|
803
|
+
for (const bucket of args.contract.main_buckets) {
|
|
804
|
+
const rawBucket = args.buckets[bucket.bucket_index - 1];
|
|
805
|
+
lines.push(
|
|
806
|
+
...rawBucket?.summaryLines.length ? rawBucket.summaryLines.map((line) => `- ${line}`) : [renderBucketHeadline(bucket)]
|
|
807
|
+
);
|
|
808
|
+
for (const evidence of bucket.evidence) {
|
|
809
|
+
lines.push(` - ${evidence}`);
|
|
810
|
+
}
|
|
811
|
+
if (rawBucket?.hint) {
|
|
812
|
+
lines.push(` - Hint: ${rawBucket.hint}`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
lines.push(buildDecisionLine(args.contract));
|
|
816
|
+
lines.push(`- Next: ${args.contract.next_best_action.note}`);
|
|
817
|
+
lines.push(buildStopSignal(args.contract));
|
|
818
|
+
return lines.join("\n");
|
|
819
|
+
}
|
|
820
|
+
function renderVerbose(args) {
|
|
821
|
+
const lines = [...buildOutcomeLines(args.analysis), ...buildComparisonLines(args.contract)];
|
|
822
|
+
for (const bucket of args.contract.main_buckets) {
|
|
823
|
+
const rawBucket = args.buckets[bucket.bucket_index - 1];
|
|
824
|
+
lines.push(
|
|
825
|
+
...rawBucket?.summaryLines.length ? rawBucket.summaryLines.map((line) => `- ${line}`) : [renderBucketHeadline(bucket)]
|
|
826
|
+
);
|
|
827
|
+
for (const item of rawBucket?.representativeItems ?? []) {
|
|
828
|
+
lines.push(` - ${item.label} -> ${item.reason}`);
|
|
829
|
+
}
|
|
830
|
+
if (bucket.mini_diff) {
|
|
831
|
+
lines.push(` - mini-diff: ${JSON.stringify(bucket.mini_diff)}`);
|
|
832
|
+
}
|
|
833
|
+
if (rawBucket?.hint) {
|
|
834
|
+
lines.push(` - Hint: ${rawBucket.hint}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
lines.push(buildDecisionLine(args.contract));
|
|
838
|
+
lines.push(`- Next: ${args.contract.next_best_action.note}`);
|
|
839
|
+
lines.push(buildStopSignal(args.contract));
|
|
840
|
+
return lines.join("\n");
|
|
841
|
+
}
|
|
842
|
+
function buildTestStatusDiagnoseContract(args) {
|
|
843
|
+
const buckets = prioritizeBuckets(mergeBuckets(args.analysis)).slice(0, 3);
|
|
844
|
+
const simpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && buckets.length === 0;
|
|
845
|
+
const dominantBucket = buckets.map((bucket, index) => ({
|
|
846
|
+
bucket,
|
|
847
|
+
index
|
|
848
|
+
})).sort((left, right) => {
|
|
849
|
+
if (right.bucket.count !== left.bucket.count) {
|
|
850
|
+
return right.bucket.count - left.bucket.count;
|
|
851
|
+
}
|
|
852
|
+
return right.bucket.confidence - left.bucket.confidence;
|
|
853
|
+
})[0] ?? null;
|
|
854
|
+
const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && (dominantBucket?.bucket.confidence ?? 0) >= 0.7;
|
|
855
|
+
const rawNeeded = buckets.length > 0 ? buckets.every((bucket) => bucket.confidence < 0.7) : !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure);
|
|
856
|
+
const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
|
|
857
|
+
const readTargets = buildReadTargets({
|
|
858
|
+
buckets,
|
|
859
|
+
dominantBucketIndex: dominantBlockerBucketIndex
|
|
860
|
+
});
|
|
861
|
+
const mainBuckets = buckets.map((bucket, index) => ({
|
|
862
|
+
bucket_index: index + 1,
|
|
863
|
+
label: labelForBucket(bucket),
|
|
864
|
+
count: bucket.count,
|
|
865
|
+
root_cause: bucket.reason,
|
|
866
|
+
evidence: buildBucketEvidence(bucket),
|
|
867
|
+
bucket_confidence: Number(bucket.confidence.toFixed(2)),
|
|
868
|
+
root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
|
|
869
|
+
dominant: dominantBucket?.index === index,
|
|
870
|
+
secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== index + 1,
|
|
871
|
+
mini_diff: extractMiniDiff(args.input, bucket)
|
|
872
|
+
}));
|
|
873
|
+
const resolvedTests = unique(args.resolvedTests ?? []);
|
|
874
|
+
const remainingTests = unique(
|
|
875
|
+
args.remainingTests ?? unique([...args.analysis.visibleErrorLabels, ...args.analysis.visibleFailedLabels])
|
|
876
|
+
);
|
|
877
|
+
let nextBestAction;
|
|
878
|
+
if (args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0) {
|
|
879
|
+
nextBestAction = {
|
|
880
|
+
code: "read_source_for_bucket",
|
|
881
|
+
bucket_index: null,
|
|
882
|
+
note: "No failing buckets remain."
|
|
883
|
+
};
|
|
884
|
+
} else if (simpleCollectionFailure) {
|
|
885
|
+
nextBestAction = {
|
|
886
|
+
code: "read_source_for_bucket",
|
|
887
|
+
bucket_index: null,
|
|
888
|
+
note: "Inspect the collection traceback or setup code next; the run failed before tests executed."
|
|
889
|
+
};
|
|
890
|
+
} else if (!diagnosisComplete) {
|
|
891
|
+
nextBestAction = {
|
|
892
|
+
code: rawNeeded ? "read_raw_for_exact_traceback" : "insufficient_signal",
|
|
893
|
+
bucket_index: dominantBucket ? dominantBucket.index + 1 : null,
|
|
894
|
+
note: rawNeeded ? "Use focused or verbose detail, and read raw traceback only if exact stack lines are still needed." : "The visible output is not yet specific enough to diagnose reliably."
|
|
895
|
+
};
|
|
896
|
+
} else if (dominantBlockerBucketIndex !== null) {
|
|
897
|
+
nextBestAction = {
|
|
898
|
+
code: "fix_dominant_blocker",
|
|
899
|
+
bucket_index: dominantBlockerBucketIndex,
|
|
900
|
+
note: dominantBlockerBucketIndex === 1 && mainBuckets.some((bucket) => bucket.secondary_visible_despite_blocker) ? "Fix bucket 1 first, then rerun the full suite at standard. Secondary buckets are already visible behind it." : `Fix bucket ${dominantBlockerBucketIndex} first, then rerun the full suite at standard.`
|
|
901
|
+
};
|
|
902
|
+
} else {
|
|
903
|
+
nextBestAction = {
|
|
904
|
+
code: rawNeeded ? "read_raw_for_exact_traceback" : "read_source_for_bucket",
|
|
905
|
+
bucket_index: mainBuckets[0]?.bucket_index ?? null,
|
|
906
|
+
note: rawNeeded ? "Read raw traceback only if exact stack lines are required after the current diagnosis." : `Read the source or test code for bucket ${mainBuckets[0]?.bucket_index ?? 1} next.`
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const baseContract = {
|
|
910
|
+
status: diagnosisComplete ? "ok" : "insufficient",
|
|
911
|
+
diagnosis_complete: diagnosisComplete,
|
|
912
|
+
raw_needed: rawNeeded,
|
|
913
|
+
additional_source_read_likely_low_value: diagnosisComplete && !rawNeeded,
|
|
914
|
+
read_raw_only_if: rawNeeded ? "you still need exact traceback lines after focused or verbose detail" : null,
|
|
915
|
+
dominant_blocker_bucket_index: dominantBlockerBucketIndex,
|
|
916
|
+
provider_used: false,
|
|
917
|
+
provider_confidence: null,
|
|
918
|
+
provider_failed: false,
|
|
919
|
+
raw_slice_used: false,
|
|
920
|
+
raw_slice_strategy: "none",
|
|
921
|
+
resolved_tests: resolvedTests,
|
|
922
|
+
remaining_tests: remainingTests,
|
|
923
|
+
main_buckets: mainBuckets,
|
|
924
|
+
read_targets: readTargets,
|
|
925
|
+
next_best_action: nextBestAction
|
|
926
|
+
};
|
|
927
|
+
const effectiveNextBestAction = args.contractOverrides?.next_best_action ?? baseContract.next_best_action;
|
|
928
|
+
const mergedContractWithoutDecision = {
|
|
929
|
+
...baseContract,
|
|
930
|
+
...args.contractOverrides,
|
|
931
|
+
status: args.contractOverrides?.diagnosis_complete ?? diagnosisComplete ? "ok" : "insufficient",
|
|
932
|
+
next_best_action: {
|
|
933
|
+
...effectiveNextBestAction,
|
|
934
|
+
note: buildConcreteNextNote({
|
|
935
|
+
nextBestAction: effectiveNextBestAction,
|
|
936
|
+
readTargets,
|
|
937
|
+
hasSecondaryVisibleBucket: mainBuckets.some(
|
|
938
|
+
(bucket) => bucket.secondary_visible_despite_blocker
|
|
939
|
+
)
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
const contract = testStatusDiagnoseContractSchema.parse({
|
|
944
|
+
...mergedContractWithoutDecision,
|
|
945
|
+
decision: args.contractOverrides?.decision ?? deriveDecision(mergedContractWithoutDecision)
|
|
946
|
+
});
|
|
947
|
+
return {
|
|
948
|
+
contract,
|
|
949
|
+
standardText: renderStandard({
|
|
950
|
+
analysis: args.analysis,
|
|
951
|
+
contract,
|
|
952
|
+
buckets
|
|
953
|
+
}),
|
|
954
|
+
focusedText: renderFocused({
|
|
955
|
+
analysis: args.analysis,
|
|
956
|
+
contract,
|
|
957
|
+
buckets
|
|
958
|
+
}),
|
|
959
|
+
verboseText: renderVerbose({
|
|
960
|
+
analysis: args.analysis,
|
|
961
|
+
contract,
|
|
962
|
+
buckets
|
|
963
|
+
})
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function buildTestStatusPublicDiagnoseContract(args) {
|
|
967
|
+
const {
|
|
968
|
+
resolved_tests,
|
|
969
|
+
remaining_tests,
|
|
970
|
+
...rest
|
|
971
|
+
} = args.contract;
|
|
972
|
+
return testStatusPublicDiagnoseContractSchema.parse({
|
|
973
|
+
...rest,
|
|
974
|
+
resolved_summary: buildTestTargetSummary(resolved_tests),
|
|
975
|
+
remaining_summary: buildTestTargetSummary(remaining_tests),
|
|
976
|
+
remaining_subset_available: Boolean(args.remainingSubsetAvailable) && remaining_tests.length > 0,
|
|
977
|
+
...args.includeTestIds ? {
|
|
978
|
+
resolved_tests,
|
|
979
|
+
remaining_tests
|
|
980
|
+
} : {}
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
function buildTestStatusAnalysisContext(args) {
|
|
984
|
+
const publicContract = buildTestStatusPublicDiagnoseContract({
|
|
985
|
+
contract: args.contract,
|
|
986
|
+
includeTestIds: args.includeTestIds,
|
|
987
|
+
remainingSubsetAvailable: args.remainingSubsetAvailable
|
|
988
|
+
});
|
|
989
|
+
const bucketLines = args.contract.main_buckets.length === 0 ? ["- No failing buckets visible."] : args.contract.main_buckets.map(
|
|
990
|
+
(bucket) => `- Bucket ${bucket.bucket_index}: ${bucket.label}; count=${bucket.count}; root_cause=${bucket.root_cause}; dominant=${bucket.dominant}`
|
|
991
|
+
);
|
|
992
|
+
return [
|
|
993
|
+
"Heuristic extract:",
|
|
994
|
+
`- diagnosis_complete=${args.contract.diagnosis_complete}`,
|
|
995
|
+
`- raw_needed=${args.contract.raw_needed}`,
|
|
996
|
+
`- decision=${args.contract.decision}`,
|
|
997
|
+
`- provider_used=${args.contract.provider_used}`,
|
|
998
|
+
`- provider_failed=${args.contract.provider_failed}`,
|
|
999
|
+
`- raw_slice_strategy=${args.contract.raw_slice_strategy}`,
|
|
1000
|
+
`- resolved_summary=${formatTargetSummary(publicContract.resolved_summary)}`,
|
|
1001
|
+
`- remaining_summary=${formatTargetSummary(publicContract.remaining_summary)}`,
|
|
1002
|
+
`- remaining_subset_available=${publicContract.remaining_subset_available}`,
|
|
1003
|
+
...args.includeTestIds && args.contract.resolved_tests.length > 0 ? [`- resolved_tests=${args.contract.resolved_tests.join(", ")}`] : [],
|
|
1004
|
+
...args.includeTestIds && args.contract.remaining_tests.length > 0 ? [`- remaining_tests=${args.contract.remaining_tests.join(", ")}`] : [],
|
|
1005
|
+
...args.contract.read_targets.length > 0 ? args.contract.read_targets.map(
|
|
1006
|
+
(target) => `- read_target[bucket=${target.bucket_index}]=${formatReadTargetLocation(target)} -> ${target.why}${target.context_hint.start_line !== null && target.context_hint.end_line !== null ? `; lines=${target.context_hint.start_line}-${target.context_hint.end_line}` : target.context_hint.search_hint ? `; search=${target.context_hint.search_hint}` : ""}`
|
|
1007
|
+
) : [],
|
|
1008
|
+
...bucketLines,
|
|
1009
|
+
`- next_best_action=${args.contract.next_best_action.code}`
|
|
1010
|
+
].join("\n");
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/core/heuristics.ts
|
|
1014
|
+
var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
|
|
1015
|
+
var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
|
|
1016
|
+
var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
|
|
1017
|
+
function collectEvidence(input, matcher, limit = 3) {
|
|
1018
|
+
return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
|
|
1019
|
+
}
|
|
1020
|
+
function inferSeverity(token) {
|
|
1021
|
+
return token.toLowerCase().includes("critical") ? "critical" : "high";
|
|
1022
|
+
}
|
|
1023
|
+
function inferPackage(line) {
|
|
1024
|
+
const match = line.match(/^\s*([@a-z0-9._/-]+)\s*:/i);
|
|
1025
|
+
return match?.[1] ?? null;
|
|
1026
|
+
}
|
|
1027
|
+
function inferRemediation(pkg) {
|
|
1028
|
+
return `Upgrade ${pkg} to a patched version.`;
|
|
1029
|
+
}
|
|
1030
|
+
function getCount(input, label) {
|
|
1031
|
+
const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
|
|
1032
|
+
const lastMatch = matches.at(-1);
|
|
1033
|
+
return lastMatch ? Number(lastMatch[1]) : 0;
|
|
1034
|
+
}
|
|
1035
|
+
function formatCount2(count, singular, plural = `${singular}s`) {
|
|
1036
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
1037
|
+
}
|
|
1038
|
+
function countPattern(input, matcher) {
|
|
1039
|
+
return [...input.matchAll(matcher)].length;
|
|
1040
|
+
}
|
|
1041
|
+
function collectUniqueMatches(input, matcher, limit = 6) {
|
|
1042
|
+
const values = [];
|
|
1043
|
+
for (const match of input.matchAll(matcher)) {
|
|
1044
|
+
const candidate = match[1]?.trim();
|
|
1045
|
+
if (!candidate || values.includes(candidate)) {
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
values.push(candidate);
|
|
1049
|
+
if (values.length >= limit) {
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return values;
|
|
1054
|
+
}
|
|
1055
|
+
function emptyAnchor() {
|
|
1056
|
+
return {
|
|
1057
|
+
file: null,
|
|
1058
|
+
line: null,
|
|
1059
|
+
anchor_kind: "none",
|
|
1060
|
+
anchor_confidence: 0
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function normalizeAnchorFile(value) {
|
|
1064
|
+
return value.replace(/\\/g, "/").trim();
|
|
1065
|
+
}
|
|
1066
|
+
function inferFileFromLabel(label) {
|
|
1067
|
+
const candidate = cleanFailureLabel(label).split("::")[0]?.trim();
|
|
1068
|
+
if (!candidate) {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
if (!/[./\\]/.test(candidate) || !/\.[A-Za-z0-9]+$/.test(candidate)) {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
return normalizeAnchorFile(candidate);
|
|
1075
|
+
}
|
|
1076
|
+
function buildLabelAnchor(label) {
|
|
1077
|
+
const file = inferFileFromLabel(label);
|
|
1078
|
+
if (!file) {
|
|
1079
|
+
return emptyAnchor();
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
file,
|
|
1083
|
+
line: null,
|
|
1084
|
+
anchor_kind: "test_label",
|
|
1085
|
+
anchor_confidence: 0.72
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function parseObservedAnchor(line) {
|
|
1089
|
+
const normalized = line.trim();
|
|
1090
|
+
if (normalized.length === 0) {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
const fileWithLine = normalized.match(/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):(\d+)(?::\d+)?:\s+in\b/) ?? normalized.match(/^([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?:\s+in\b/);
|
|
1094
|
+
if (fileWithLine) {
|
|
1095
|
+
return {
|
|
1096
|
+
file: normalizeAnchorFile(fileWithLine[1]),
|
|
1097
|
+
line: Number(fileWithLine[2]),
|
|
1098
|
+
anchor_kind: "traceback",
|
|
1099
|
+
anchor_confidence: 1
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const pythonTraceback = normalized.match(/^File\s+"([^"]+)",\s+line\s+(\d+)/);
|
|
1103
|
+
if (pythonTraceback) {
|
|
1104
|
+
return {
|
|
1105
|
+
file: normalizeAnchorFile(pythonTraceback[1]),
|
|
1106
|
+
line: Number(pythonTraceback[2]),
|
|
1107
|
+
anchor_kind: "traceback",
|
|
1108
|
+
anchor_confidence: 1
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const importModule = normalized.match(
|
|
1112
|
+
/ImportError while importing test module ['"]([^'"]+\.[A-Za-z0-9]+)['"]/i
|
|
1113
|
+
);
|
|
1114
|
+
if (importModule) {
|
|
1115
|
+
return {
|
|
1116
|
+
file: normalizeAnchorFile(importModule[1]),
|
|
1117
|
+
line: null,
|
|
1118
|
+
anchor_kind: "traceback",
|
|
1119
|
+
anchor_confidence: 0.92
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
function resolveAnchorForLabel(args) {
|
|
1125
|
+
return args.observedAnchor ?? buildLabelAnchor(args.label);
|
|
1126
|
+
}
|
|
1127
|
+
function cleanFailureLabel(label) {
|
|
1128
|
+
return label.trim().replace(/^['"]|['"]$/g, "");
|
|
1129
|
+
}
|
|
1130
|
+
function isLowValueInternalReason(normalized) {
|
|
1131
|
+
return /^Hint:\s+make sure your test modules\/packages have valid Python names\.?$/i.test(
|
|
1132
|
+
normalized
|
|
1133
|
+
) || /^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(
|
|
1134
|
+
normalized
|
|
1135
|
+
) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
|
|
1136
|
+
}
|
|
1137
|
+
function scoreFailureReason(reason) {
|
|
1138
|
+
if (reason.startsWith("missing test env:")) {
|
|
1139
|
+
return 6;
|
|
1140
|
+
}
|
|
1141
|
+
if (reason.startsWith("missing module:")) {
|
|
1142
|
+
return 5;
|
|
1143
|
+
}
|
|
1144
|
+
if (reason.startsWith("assertion failed:")) {
|
|
1145
|
+
return 4;
|
|
1146
|
+
}
|
|
1147
|
+
if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
|
|
1148
|
+
return 3;
|
|
1149
|
+
}
|
|
1150
|
+
if (reason === "import error during collection") {
|
|
1151
|
+
return 2;
|
|
1152
|
+
}
|
|
1153
|
+
return 1;
|
|
1154
|
+
}
|
|
1155
|
+
function extractEnvBlockerName(normalized) {
|
|
1156
|
+
const directMatch = normalized.match(
|
|
1157
|
+
/\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
|
|
1158
|
+
);
|
|
1159
|
+
if (directMatch) {
|
|
1160
|
+
return directMatch[1];
|
|
1161
|
+
}
|
|
1162
|
+
const fallbackMatch = normalized.match(
|
|
1163
|
+
/\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]*DB-isolated tests)/
|
|
1164
|
+
);
|
|
1165
|
+
return fallbackMatch?.[1] ?? null;
|
|
1166
|
+
}
|
|
1167
|
+
function classifyFailureReason(line, options) {
|
|
1168
|
+
const normalized = line.trim().replace(/^[A-Z]\s+/, "");
|
|
1169
|
+
if (normalized.length === 0) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
if (isLowValueInternalReason(normalized)) {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
if (/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+(?::\d+)?:\s+in\b/.test(normalized) || /^([^:\s][^:]*\.[A-Za-z0-9]+):\d+(?::\d+)?:\s+in\b/.test(normalized) || /^File\s+"[^"]+",\s+line\s+\d+/.test(normalized)) {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
const envBlocker = extractEnvBlockerName(normalized);
|
|
1179
|
+
if (envBlocker) {
|
|
1180
|
+
return {
|
|
1181
|
+
reason: `missing test env: ${envBlocker}`,
|
|
1182
|
+
group: "DB-backed tests are blocked by missing test environment configuration"
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
const missingEnv = normalized.match(
|
|
1186
|
+
/\b(?:environment variable|env(?:ironment)? var(?:iable)?|Missing required env(?:ironment)? variable)\s+([A-Z][A-Z0-9_]{2,})\b/i
|
|
1187
|
+
);
|
|
1188
|
+
if (missingEnv) {
|
|
1189
|
+
return {
|
|
1190
|
+
reason: `missing test env: ${missingEnv[1]}`,
|
|
1191
|
+
group: "tests are blocked by missing environment configuration"
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
const keyErrorEnv = normalized.match(/KeyError:\s*['"]([A-Z][A-Z0-9_]{2,})['"]/);
|
|
1195
|
+
if (keyErrorEnv) {
|
|
1196
|
+
return {
|
|
1197
|
+
reason: `missing test env: ${keyErrorEnv[1]}`,
|
|
1198
|
+
group: "tests are blocked by missing environment configuration"
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
const fixtureGuard = normalized.match(
|
|
1202
|
+
/(?:FixtureLookupError|fixture guard|requires fixture)\b[^A-Za-z0-9_'-]*([a-z_][a-z0-9_]*)?/i
|
|
1203
|
+
);
|
|
1204
|
+
if (fixtureGuard) {
|
|
1205
|
+
return {
|
|
1206
|
+
reason: `fixture guard: ${fixtureGuard[1] ?? "required fixture unavailable"}`.trim(),
|
|
1207
|
+
group: "fixture guards or setup gates"
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (/(ECONNREFUSED|ConnectionRefusedError|connection refused|could not connect to server)/i.test(
|
|
1211
|
+
normalized
|
|
1212
|
+
) && /(postgres|database|db|5432)/i.test(normalized)) {
|
|
1213
|
+
return {
|
|
1214
|
+
reason: "db refused: database connection was refused",
|
|
1215
|
+
group: "database connectivity failures"
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
if (/(503\b|service unavailable|temporarily unavailable)/i.test(normalized)) {
|
|
1219
|
+
return {
|
|
1220
|
+
reason: "service unavailable: dependency service is unavailable",
|
|
1221
|
+
group: "service availability failures"
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
if (/(auth bypass|test auth|bypass token)/i.test(normalized) && /(missing|absent|not configured|not set|unavailable)/i.test(normalized)) {
|
|
1225
|
+
return {
|
|
1226
|
+
reason: "auth bypass absent: test auth bypass is missing",
|
|
1227
|
+
group: "authentication test setup failures"
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
const pythonMissingModule = normalized.match(
|
|
1231
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
|
|
1232
|
+
);
|
|
1233
|
+
if (pythonMissingModule) {
|
|
1234
|
+
return {
|
|
1235
|
+
reason: `missing module: ${pythonMissingModule[1]}`,
|
|
1236
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
|
|
1240
|
+
if (nodeMissingModule) {
|
|
1241
|
+
return {
|
|
1242
|
+
reason: `missing module: ${nodeMissingModule[1]}`,
|
|
1243
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
|
|
1247
|
+
if (assertionFailure) {
|
|
1248
|
+
return {
|
|
1249
|
+
reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
|
|
1250
|
+
group: "assertion failures"
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
|
|
1254
|
+
if (genericError) {
|
|
1255
|
+
const errorType = genericError[1];
|
|
1256
|
+
return {
|
|
1257
|
+
reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
|
|
1258
|
+
group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
if (/ImportError while importing test module/i.test(normalized)) {
|
|
1262
|
+
return {
|
|
1263
|
+
reason: "import error during collection",
|
|
1264
|
+
group: "import/dependency errors during collection"
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
if (!/[A-Za-z]/.test(normalized)) {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
reason: normalized.slice(0, 120),
|
|
1272
|
+
group: options.duringCollection ? "collection/import errors" : "other failures"
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function pushFocusedFailureItem(items, candidate) {
|
|
1276
|
+
if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
items.push(candidate);
|
|
1280
|
+
}
|
|
1281
|
+
function chooseStrongestFailureItems(items) {
|
|
1282
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
1283
|
+
const order = [];
|
|
1284
|
+
for (const item of items) {
|
|
1285
|
+
const existing = strongest.get(item.label);
|
|
1286
|
+
if (!existing) {
|
|
1287
|
+
strongest.set(item.label, item);
|
|
1288
|
+
order.push(item.label);
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
if (scoreFailureReason(item.reason) > scoreFailureReason(existing.reason)) {
|
|
1292
|
+
strongest.set(item.label, item);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return order.map((label) => strongest.get(label));
|
|
1296
|
+
}
|
|
1297
|
+
function collectCollectionFailureItems(input) {
|
|
1298
|
+
const items = [];
|
|
1299
|
+
const lines = input.split("\n");
|
|
1300
|
+
let currentLabel = null;
|
|
1301
|
+
let pendingGenericReason = null;
|
|
1302
|
+
let currentAnchor = null;
|
|
1303
|
+
for (const line of lines) {
|
|
1304
|
+
const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
|
|
1305
|
+
if (collecting) {
|
|
1306
|
+
if (currentLabel && pendingGenericReason) {
|
|
1307
|
+
const anchor2 = resolveAnchorForLabel({
|
|
1308
|
+
label: currentLabel,
|
|
1309
|
+
observedAnchor: currentAnchor
|
|
1310
|
+
});
|
|
1311
|
+
pushFocusedFailureItem(items, {
|
|
1312
|
+
label: currentLabel,
|
|
1313
|
+
reason: pendingGenericReason.reason,
|
|
1314
|
+
group: pendingGenericReason.group,
|
|
1315
|
+
...anchor2
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
currentLabel = cleanFailureLabel(collecting[1]);
|
|
1319
|
+
pendingGenericReason = null;
|
|
1320
|
+
currentAnchor = null;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
if (!currentLabel) {
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
currentAnchor = parseObservedAnchor(line) ?? currentAnchor;
|
|
1327
|
+
const classification = classifyFailureReason(line, {
|
|
1328
|
+
duringCollection: true
|
|
1329
|
+
});
|
|
1330
|
+
if (!classification) {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (classification.reason === "import error during collection") {
|
|
1334
|
+
pendingGenericReason = classification;
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
const anchor = resolveAnchorForLabel({
|
|
1338
|
+
label: currentLabel,
|
|
1339
|
+
observedAnchor: currentAnchor
|
|
1340
|
+
});
|
|
1341
|
+
pushFocusedFailureItem(items, {
|
|
1342
|
+
label: currentLabel,
|
|
1343
|
+
reason: classification.reason,
|
|
1344
|
+
group: classification.group,
|
|
1345
|
+
...anchor
|
|
1346
|
+
});
|
|
1347
|
+
currentLabel = null;
|
|
1348
|
+
pendingGenericReason = null;
|
|
1349
|
+
currentAnchor = null;
|
|
1350
|
+
}
|
|
1351
|
+
if (currentLabel && pendingGenericReason) {
|
|
1352
|
+
const anchor = resolveAnchorForLabel({
|
|
1353
|
+
label: currentLabel,
|
|
1354
|
+
observedAnchor: currentAnchor
|
|
1355
|
+
});
|
|
1356
|
+
pushFocusedFailureItem(items, {
|
|
1357
|
+
label: currentLabel,
|
|
1358
|
+
reason: pendingGenericReason.reason,
|
|
1359
|
+
group: pendingGenericReason.group,
|
|
1360
|
+
...anchor
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
return items;
|
|
1364
|
+
}
|
|
1365
|
+
function collectInlineFailureItems(input) {
|
|
1366
|
+
const items = [];
|
|
1367
|
+
for (const line of input.split("\n")) {
|
|
1368
|
+
const inlineFailure = line.match(/^(FAILED|ERROR)\s+(.+?)\s+-\s+(.+)$/);
|
|
1369
|
+
if (!inlineFailure) {
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const cleanedLabel = cleanFailureLabel(inlineFailure[2]);
|
|
1373
|
+
if (!cleanedLabel) {
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
const classification = classifyFailureReason(inlineFailure[3], {
|
|
1377
|
+
duringCollection: false
|
|
1378
|
+
});
|
|
1379
|
+
if (!classification) {
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
pushFocusedFailureItem(items, {
|
|
1383
|
+
label: cleanedLabel,
|
|
1384
|
+
reason: classification.reason,
|
|
1385
|
+
group: classification.group,
|
|
1386
|
+
...resolveAnchorForLabel({
|
|
1387
|
+
label: cleanedLabel,
|
|
1388
|
+
observedAnchor: parseObservedAnchor(inlineFailure[3])
|
|
1389
|
+
})
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
return items;
|
|
1393
|
+
}
|
|
1394
|
+
function collectInlineFailureItemsWithStatus(input) {
|
|
1395
|
+
const items = [];
|
|
1396
|
+
for (const line of input.split("\n")) {
|
|
1397
|
+
const inlineFailure = line.match(/^(FAILED|ERROR)\s+(.+?)(?:\s+-\s+(.+))?$/);
|
|
1398
|
+
if (!inlineFailure) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const cleanedLabel = cleanFailureLabel(inlineFailure[2]);
|
|
1402
|
+
if (!cleanedLabel) {
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const details = inlineFailure[3]?.trim();
|
|
1406
|
+
if (!details) {
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
const classification = classifyFailureReason(details, {
|
|
1410
|
+
duringCollection: false
|
|
1411
|
+
});
|
|
1412
|
+
if (!classification) {
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
items.push({
|
|
1416
|
+
label: cleanedLabel,
|
|
1417
|
+
reason: classification.reason,
|
|
1418
|
+
group: classification.group,
|
|
1419
|
+
status: inlineFailure[1] === "FAILED" ? "failed" : "error",
|
|
1420
|
+
...resolveAnchorForLabel({
|
|
1421
|
+
label: cleanedLabel,
|
|
1422
|
+
observedAnchor: parseObservedAnchor(details)
|
|
1423
|
+
})
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
return items;
|
|
1427
|
+
}
|
|
1428
|
+
function collectStandaloneErrorClassifications(input) {
|
|
1429
|
+
const classifications = [];
|
|
1430
|
+
for (const line of input.split("\n")) {
|
|
1431
|
+
const standalone = line.match(/^\s*E\s+(.+)$/);
|
|
1432
|
+
if (!standalone) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
const classification = classifyFailureReason(standalone[1], {
|
|
1436
|
+
duringCollection: false
|
|
1437
|
+
});
|
|
1438
|
+
if (!classification || classification.reason === "import error during collection") {
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
classifications.push(classification);
|
|
1442
|
+
}
|
|
1443
|
+
return classifications;
|
|
1444
|
+
}
|
|
1445
|
+
function chooseStrongestStatusFailureItems(items) {
|
|
1446
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
1447
|
+
const order = [];
|
|
1448
|
+
for (const item of items) {
|
|
1449
|
+
const key = `${item.status}:${item.label}`;
|
|
1450
|
+
const existing = strongest.get(key);
|
|
1451
|
+
if (!existing) {
|
|
1452
|
+
strongest.set(key, item);
|
|
1453
|
+
order.push(key);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (scoreFailureReason(item.reason) > scoreFailureReason(existing.reason)) {
|
|
1457
|
+
strongest.set(key, item);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return order.map((key) => strongest.get(key));
|
|
1461
|
+
}
|
|
1462
|
+
function summarizeRepeatedTestCauses(input, options) {
|
|
1463
|
+
const pythonMissingModules = collectUniqueMatches(
|
|
1464
|
+
input,
|
|
1465
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
|
|
1466
|
+
);
|
|
1467
|
+
const nodeMissingModules = collectUniqueMatches(
|
|
1468
|
+
input,
|
|
1469
|
+
/Cannot find module ['"]([^'"]+)['"]/gi
|
|
1470
|
+
);
|
|
1471
|
+
const missingModules = [...pythonMissingModules];
|
|
1472
|
+
for (const moduleName of nodeMissingModules) {
|
|
1473
|
+
if (!missingModules.includes(moduleName)) {
|
|
1474
|
+
missingModules.push(moduleName);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const missingModuleHits = countPattern(
|
|
1478
|
+
input,
|
|
1479
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
|
|
1480
|
+
) + countPattern(input, /Cannot find module ['"]([^'"]+)['"]/gi);
|
|
1481
|
+
const envBlockers = [];
|
|
1482
|
+
let envBlockerHits = 0;
|
|
1483
|
+
for (const line of input.split("\n")) {
|
|
1484
|
+
const envBlocker = extractEnvBlockerName(line.trim().replace(/^[A-Z]\s+/, ""));
|
|
1485
|
+
if (!envBlocker) {
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
envBlockerHits += 1;
|
|
1489
|
+
if (!envBlockers.includes(envBlocker) && envBlockers.length < 4) {
|
|
1490
|
+
envBlockers.push(envBlocker);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const importCollectionHits = countPattern(input, /ImportError while importing test module/gi) + countPattern(input, /^\s*_+\s+ERROR collecting\b/gim);
|
|
1494
|
+
const genericErrorTypes = collectUniqueMatches(
|
|
1495
|
+
input,
|
|
1496
|
+
/\b((?:Assertion|Import|Type|Value|Runtime|Reference|Key|Attribute)[A-Za-z]*Error)\b/gi,
|
|
1497
|
+
4
|
|
1498
|
+
);
|
|
1499
|
+
const bullets = [];
|
|
1500
|
+
if (envBlockers.length > 0 && envBlockerHits >= 2) {
|
|
1501
|
+
bullets.push(`- Shared test environment blocker detected: ${envBlockers.join(", ")}.`);
|
|
1502
|
+
}
|
|
1503
|
+
if (bullets.length < 2 && (options.duringCollection && (importCollectionHits >= 2 || missingModuleHits >= 2) || !options.duringCollection && missingModuleHits >= 2)) {
|
|
1504
|
+
bullets.push(
|
|
1505
|
+
options.duringCollection ? "- Most failures are import/dependency errors during test collection." : "- Most failures are import/dependency errors."
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
if (bullets.length < 2) {
|
|
1509
|
+
if (missingModules.length > 1) {
|
|
1510
|
+
bullets.push(`- Missing modules include ${missingModules.join(", ")}.`);
|
|
1511
|
+
} else if (missingModules.length === 1 && missingModuleHits >= 2) {
|
|
1512
|
+
bullets.push(`- Missing module repeated across failures: ${missingModules[0]}.`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (bullets.length < 2 && genericErrorTypes.length >= 2) {
|
|
1516
|
+
bullets.push(`- Repeated error types include ${genericErrorTypes.join(", ")}.`);
|
|
1517
|
+
}
|
|
1518
|
+
return bullets.slice(0, 2);
|
|
1519
|
+
}
|
|
1520
|
+
function collectFailureLabels(input) {
|
|
1521
|
+
const labels = [];
|
|
1522
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1523
|
+
const pushLabel = (label, status) => {
|
|
1524
|
+
const cleaned = cleanFailureLabel(label);
|
|
1525
|
+
if (!cleaned) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const key = `${status}:${cleaned}`;
|
|
1529
|
+
if (seen.has(key)) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
seen.add(key);
|
|
1533
|
+
labels.push({
|
|
1534
|
+
label: cleaned,
|
|
1535
|
+
status
|
|
1536
|
+
});
|
|
1537
|
+
};
|
|
1538
|
+
for (const line of input.split("\n")) {
|
|
1539
|
+
const progress = line.match(
|
|
1540
|
+
/^(tests\/.+?)(?:\s+<-\s+\S+)?\s+(FAILED|ERROR)\s+\[[^\]]+\]\s*$/
|
|
1541
|
+
);
|
|
1542
|
+
if (progress) {
|
|
1543
|
+
pushLabel(progress[1], progress[2] === "FAILED" ? "failed" : "error");
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const summary = line.match(/^(FAILED|ERROR)\s+(.+?)(?:\s+-\s+.*)?$/);
|
|
1547
|
+
if (summary) {
|
|
1548
|
+
pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return labels;
|
|
1552
|
+
}
|
|
1553
|
+
function classifyBucketTypeFromReason(reason) {
|
|
1554
|
+
if (reason.startsWith("missing test env:")) {
|
|
1555
|
+
return "shared_environment_blocker";
|
|
1556
|
+
}
|
|
1557
|
+
if (reason.startsWith("fixture guard:")) {
|
|
1558
|
+
return "fixture_guard_failure";
|
|
1559
|
+
}
|
|
1560
|
+
if (reason.startsWith("service unavailable:")) {
|
|
1561
|
+
return "service_unavailable";
|
|
1562
|
+
}
|
|
1563
|
+
if (reason.startsWith("db refused:")) {
|
|
1564
|
+
return "db_connection_failure";
|
|
1565
|
+
}
|
|
1566
|
+
if (reason.startsWith("auth bypass absent:")) {
|
|
1567
|
+
return "auth_bypass_absent";
|
|
1568
|
+
}
|
|
1569
|
+
if (reason.startsWith("missing module:")) {
|
|
1570
|
+
return "import_dependency_failure";
|
|
1571
|
+
}
|
|
1572
|
+
if (reason.startsWith("assertion failed:")) {
|
|
1573
|
+
return "assertion_failure";
|
|
1574
|
+
}
|
|
1575
|
+
if (/^RuntimeError:|^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
|
|
1576
|
+
return "runtime_failure";
|
|
1577
|
+
}
|
|
1578
|
+
return "unknown_failure";
|
|
1579
|
+
}
|
|
1580
|
+
function synthesizeSharedBlockerBucket(args) {
|
|
1581
|
+
if (args.errors === 0) {
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
const visibleReasonGroups = /* @__PURE__ */ new Map();
|
|
1585
|
+
for (const item of args.visibleErrorItems) {
|
|
1586
|
+
const entry = visibleReasonGroups.get(item.reason);
|
|
1587
|
+
if (entry) {
|
|
1588
|
+
entry.count += 1;
|
|
1589
|
+
entry.items.push(item);
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
visibleReasonGroups.set(item.reason, {
|
|
1593
|
+
count: 1,
|
|
1594
|
+
group: item.group,
|
|
1595
|
+
items: [item]
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
const top = [...visibleReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
|
|
1599
|
+
const standaloneReasonGroups = /* @__PURE__ */ new Map();
|
|
1600
|
+
for (const classification of collectStandaloneErrorClassifications(args.input)) {
|
|
1601
|
+
const entry = standaloneReasonGroups.get(classification.reason);
|
|
1602
|
+
if (entry) {
|
|
1603
|
+
entry.count += 1;
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
standaloneReasonGroups.set(classification.reason, {
|
|
1607
|
+
count: 1,
|
|
1608
|
+
group: classification.group
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
const standaloneTop = [...standaloneReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
|
|
1612
|
+
const visibleTopReason = top?.[0];
|
|
1613
|
+
const visibleTopStats = top?.[1];
|
|
1614
|
+
const standaloneTopReason = standaloneTop?.[0];
|
|
1615
|
+
const chosenReason = visibleTopReason && standaloneTopReason ? standaloneReasonGroups.get(standaloneTopReason).count > visibleTopStats.count ? standaloneTopReason : visibleTopReason : visibleTopReason ?? standaloneTopReason;
|
|
1616
|
+
const singleEnvBlockerItem = !chosenReason && args.visibleErrorItems.length === 1 && args.visibleErrorItems[0].reason.startsWith("missing test env:") ? args.visibleErrorItems[0] : null;
|
|
1617
|
+
const effectiveReason = chosenReason ?? singleEnvBlockerItem?.reason;
|
|
1618
|
+
if (!effectiveReason || effectiveReason === "import error during collection") {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
const visibleStats = visibleReasonGroups.get(effectiveReason);
|
|
1622
|
+
const standaloneStats = standaloneReasonGroups.get(effectiveReason);
|
|
1623
|
+
const resolvedStats = visibleStats ?? standaloneStats;
|
|
1624
|
+
const bucketType = classifyBucketTypeFromReason(effectiveReason);
|
|
1625
|
+
const countVisible = resolvedStats.count;
|
|
1626
|
+
const visibleReasonsAreUniform = args.visibleErrorItems.length === 0 || args.visibleErrorItems.every((item) => item.reason === effectiveReason);
|
|
1627
|
+
const canClaimAllErrors = (args.errorStatusLabels.length >= 3 || Boolean(singleEnvBlockerItem)) && visibleReasonsAreUniform && args.errors >= countVisible;
|
|
1628
|
+
const countClaimed = canClaimAllErrors ? args.errors : void 0;
|
|
1629
|
+
const countText = countClaimed ?? countVisible;
|
|
1630
|
+
const atLeastPrefix = countClaimed ? "" : "At least ";
|
|
1631
|
+
const group = resolvedStats.group;
|
|
1632
|
+
const representativeItems = visibleStats?.items.slice(0, 4).map((item) => ({
|
|
1633
|
+
label: item.label,
|
|
1634
|
+
reason: effectiveReason,
|
|
1635
|
+
group,
|
|
1636
|
+
file: item.file,
|
|
1637
|
+
line: item.line,
|
|
1638
|
+
anchor_kind: item.anchor_kind,
|
|
1639
|
+
anchor_confidence: item.anchor_confidence
|
|
1640
|
+
})) ?? args.errorStatusLabels.slice(0, 4).map((label) => ({
|
|
1641
|
+
label,
|
|
1642
|
+
reason: effectiveReason,
|
|
1643
|
+
group,
|
|
1644
|
+
...buildLabelAnchor(label)
|
|
1645
|
+
}));
|
|
1646
|
+
const envVar = effectiveReason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
1647
|
+
let hint;
|
|
1648
|
+
if (envVar) {
|
|
1649
|
+
hint = `Set ${envVar} (or pass --pgtest-dsn) before rerunning DB-isolated tests.`;
|
|
1650
|
+
} else if (effectiveReason.startsWith("fixture guard:")) {
|
|
1651
|
+
hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
|
|
1652
|
+
} else if (effectiveReason.startsWith("db refused:")) {
|
|
1653
|
+
hint = "Start the expected test database or fix the DSN before rerunning DB-backed tests.";
|
|
1654
|
+
} else if (effectiveReason.startsWith("service unavailable:")) {
|
|
1655
|
+
hint = "Restore the unavailable service dependency before rerunning the affected tests.";
|
|
1656
|
+
} else if (effectiveReason.startsWith("auth bypass absent:")) {
|
|
1657
|
+
hint = "Configure the expected auth bypass or test auth fixture before rerunning the affected tests.";
|
|
1658
|
+
} else if (effectiveReason.startsWith("missing module:")) {
|
|
1659
|
+
hint = "Install the missing dependency and rerun the affected tests.";
|
|
1660
|
+
}
|
|
1661
|
+
let headline;
|
|
1662
|
+
if (envVar) {
|
|
1663
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors require ${envVar} for DB-isolated tests.`;
|
|
1664
|
+
} else if (effectiveReason.startsWith("fixture guard:")) {
|
|
1665
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
|
|
1666
|
+
} else if (effectiveReason.startsWith("db refused:")) {
|
|
1667
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by refused database connections.`;
|
|
1668
|
+
} else if (effectiveReason.startsWith("service unavailable:")) {
|
|
1669
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by an unavailable service dependency.`;
|
|
1670
|
+
} else if (effectiveReason.startsWith("auth bypass absent:")) {
|
|
1671
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by missing auth bypass setup.`;
|
|
1672
|
+
} else if (effectiveReason.startsWith("missing module:")) {
|
|
1673
|
+
const moduleName = effectiveReason.replace("missing module:", "").trim();
|
|
1674
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by missing module ${moduleName}.`;
|
|
1675
|
+
} else {
|
|
1676
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors share ${effectiveReason}.`;
|
|
1677
|
+
}
|
|
1678
|
+
return {
|
|
1679
|
+
type: bucketType,
|
|
1680
|
+
headline,
|
|
1681
|
+
countVisible,
|
|
1682
|
+
countClaimed,
|
|
1683
|
+
reason: effectiveReason,
|
|
1684
|
+
representativeItems,
|
|
1685
|
+
entities: envVar ? [envVar] : [],
|
|
1686
|
+
hint,
|
|
1687
|
+
confidence: countClaimed ? 0.95 : 0.75,
|
|
1688
|
+
summaryLines: [headline],
|
|
1689
|
+
overflowCount: Math.max((countClaimed ?? countVisible) - representativeItems.length, 0),
|
|
1690
|
+
overflowLabel: "failing tests/modules"
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function synthesizeImportDependencyBucket(args) {
|
|
1694
|
+
if (args.errors === 0) {
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
const importItems = args.visibleErrorItems.filter((item) => item.reason.startsWith("missing module:"));
|
|
1698
|
+
if (importItems.length < 2) {
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
|
|
1702
|
+
const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >= 3 && args.errors >= importItems.length ? args.errors : void 0;
|
|
1703
|
+
const modules = Array.from(
|
|
1704
|
+
new Set(
|
|
1705
|
+
importItems.map((item) => item.reason.replace("missing module:", "").trim()).filter(Boolean)
|
|
1706
|
+
)
|
|
1707
|
+
).slice(0, 6);
|
|
1708
|
+
const headlineCount = countClaimed ?? importItems.length;
|
|
1709
|
+
const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible errors are caused by missing dependencies during test collection.`;
|
|
1710
|
+
const summaryLines = [headline];
|
|
1711
|
+
if (modules.length > 0) {
|
|
1712
|
+
summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
|
|
1713
|
+
}
|
|
1714
|
+
return {
|
|
1715
|
+
type: "import_dependency_failure",
|
|
1716
|
+
headline,
|
|
1717
|
+
countVisible: importItems.length,
|
|
1718
|
+
countClaimed,
|
|
1719
|
+
reason: "missing dependencies during test collection",
|
|
1720
|
+
representativeItems: importItems.slice(0, 4).map((item) => ({
|
|
1721
|
+
label: item.label,
|
|
1722
|
+
reason: item.reason,
|
|
1723
|
+
group: item.group,
|
|
1724
|
+
file: item.file,
|
|
1725
|
+
line: item.line,
|
|
1726
|
+
anchor_kind: item.anchor_kind,
|
|
1727
|
+
anchor_confidence: item.anchor_confidence
|
|
1728
|
+
})),
|
|
1729
|
+
entities: modules,
|
|
1730
|
+
hint: modules.length === 1 ? `Install ${modules[0]} and rerun the affected tests.` : "Install the missing dependencies and rerun the affected tests.",
|
|
1731
|
+
confidence: countClaimed ? 0.95 : 0.8,
|
|
1732
|
+
summaryLines,
|
|
1733
|
+
overflowCount: Math.max((countClaimed ?? importItems.length) - Math.min(importItems.length, 4), 0),
|
|
1734
|
+
overflowLabel: "failing tests/modules"
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function isContractDriftLabel(label) {
|
|
1738
|
+
return /(freeze|snapshot|contract|manifest|openapi)/i.test(label);
|
|
1739
|
+
}
|
|
1740
|
+
function looksLikeTaskKey(value) {
|
|
1741
|
+
return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
|
|
1742
|
+
}
|
|
1743
|
+
function looksLikeModelId(value) {
|
|
1744
|
+
return !value.startsWith("/api/") && /^[a-z0-9][a-z0-9._/-]*-[a-z0-9._-]+$/i.test(value);
|
|
1745
|
+
}
|
|
1746
|
+
function extractContractDriftEntities(input) {
|
|
1747
|
+
const apiPaths = [];
|
|
1748
|
+
const taskKeys = [];
|
|
1749
|
+
const modelIds = [];
|
|
1750
|
+
const snapshotKeys = [];
|
|
1751
|
+
for (const line of input.split("\n")) {
|
|
1752
|
+
const diffPathMatch = line.match(/^\s*(?:E\s+)?[+-]\s+'(\/api\/[^']+)'/);
|
|
1753
|
+
if (diffPathMatch) {
|
|
1754
|
+
const candidatePath = diffPathMatch[1].trim();
|
|
1755
|
+
if (candidatePath && !apiPaths.includes(candidatePath) && apiPaths.length < 6) {
|
|
1756
|
+
apiPaths.push(candidatePath);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
const diffMatch = line.match(/^\s*(?:E\s+)?[+-]\s+'([^']+)'[,]?\s*$/);
|
|
1760
|
+
if (!diffMatch) {
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
const candidate = diffMatch[1].trim();
|
|
1764
|
+
if (!candidate) {
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (candidate.startsWith("/api/")) {
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
if (looksLikeModelId(candidate)) {
|
|
1771
|
+
if (!modelIds.includes(candidate) && modelIds.length < 6) {
|
|
1772
|
+
modelIds.push(candidate);
|
|
1773
|
+
}
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
if (looksLikeTaskKey(candidate)) {
|
|
1777
|
+
if (!taskKeys.includes(candidate) && taskKeys.length < 6) {
|
|
1778
|
+
taskKeys.push(candidate);
|
|
1779
|
+
}
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
if (!snapshotKeys.includes(candidate) && snapshotKeys.length < 6) {
|
|
1783
|
+
snapshotKeys.push(candidate);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (apiPaths.length === 0) {
|
|
1787
|
+
apiPaths.push(
|
|
1788
|
+
...collectUniqueMatches(input, /['"](\/api\/[A-Za-z0-9_./{}:-]+)['"]/g, 6)
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
return {
|
|
1792
|
+
apiPaths,
|
|
1793
|
+
modelIds,
|
|
1794
|
+
taskKeys,
|
|
1795
|
+
snapshotKeys
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
function buildContractRepresentativeReason(args) {
|
|
1799
|
+
if (/openapi/i.test(args.label) && args.entities.apiPaths.length > 0) {
|
|
1800
|
+
const nextPath = args.entities.apiPaths.find((path4) => !args.usedPaths.has(path4)) ?? args.entities.apiPaths[0];
|
|
1801
|
+
args.usedPaths.add(nextPath);
|
|
1802
|
+
return `added path: ${nextPath}`;
|
|
1803
|
+
}
|
|
1804
|
+
if (/(feature|task|manifest|snapshot)/i.test(args.label) && args.entities.modelIds.length > 0) {
|
|
1805
|
+
const nextModel = args.entities.modelIds.find((modelId) => !args.usedModels.has(modelId)) ?? args.entities.modelIds[0];
|
|
1806
|
+
args.usedModels.add(nextModel);
|
|
1807
|
+
return `removed model: ${nextModel}`;
|
|
1808
|
+
}
|
|
1809
|
+
if (args.entities.snapshotKeys.length > 0) {
|
|
1810
|
+
return `snapshot content changed: ${args.entities.snapshotKeys[0]}`;
|
|
1811
|
+
}
|
|
1812
|
+
return "snapshot content changed";
|
|
1813
|
+
}
|
|
1814
|
+
function synthesizeContractDriftBucket(args) {
|
|
1815
|
+
const contractLabels = args.visibleFailedLabels.filter(isContractDriftLabel);
|
|
1816
|
+
if (contractLabels.length === 0) {
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
const entities = extractContractDriftEntities(args.input);
|
|
1820
|
+
const usedPaths = /* @__PURE__ */ new Set();
|
|
1821
|
+
const usedModels = /* @__PURE__ */ new Set();
|
|
1822
|
+
const representativeItems = contractLabels.slice(0, 4).map((label) => ({
|
|
1823
|
+
label,
|
|
1824
|
+
reason: buildContractRepresentativeReason({
|
|
1825
|
+
label,
|
|
1826
|
+
entities,
|
|
1827
|
+
usedPaths,
|
|
1828
|
+
usedModels
|
|
1829
|
+
}),
|
|
1830
|
+
group: "contract drift",
|
|
1831
|
+
...buildLabelAnchor(label)
|
|
1832
|
+
}));
|
|
1833
|
+
const summaryLines = [
|
|
1834
|
+
`Contract drift: ${formatCount2(contractLabels.length, "freeze test")} ${contractLabels.length === 1 ? "is" : "are"} out of sync with current API/model state.`
|
|
1835
|
+
];
|
|
1836
|
+
if (entities.apiPaths.length > 0 && entities.modelIds.length > 0) {
|
|
1837
|
+
summaryLines.push(
|
|
1838
|
+
`Contract drift includes ${formatCount2(entities.apiPaths.length, "added API path")} and removed model ids such as ${entities.modelIds.slice(0, 3).join(", ")}.`
|
|
1839
|
+
);
|
|
1840
|
+
} else if (entities.apiPaths.length > 0) {
|
|
1841
|
+
summaryLines.push(
|
|
1842
|
+
`OpenAPI drift includes ${formatCount2(entities.apiPaths.length, "added API path")}.`
|
|
1843
|
+
);
|
|
1844
|
+
} else if (entities.modelIds.length > 0) {
|
|
1845
|
+
summaryLines.push(
|
|
1846
|
+
`Snapshot drift includes removed model ids such as ${entities.modelIds.slice(0, 3).join(", ")}.`
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
const explicitCommand = args.input.match(/python\s+scripts\/update_contract_snapshots\.py/);
|
|
1850
|
+
const hint = explicitCommand ? `If these changes are intentional, run ${explicitCommand[0]} and rerun the freeze tests.` : "If these API/model changes are intentional, regenerate the contract snapshots and rerun the freeze tests.";
|
|
1851
|
+
return {
|
|
1852
|
+
type: "contract_snapshot_drift",
|
|
1853
|
+
headline: summaryLines[0],
|
|
1854
|
+
countVisible: contractLabels.length,
|
|
1855
|
+
countClaimed: contractLabels.length,
|
|
1856
|
+
reason: "freeze snapshots are out of sync with current API/model state",
|
|
1857
|
+
representativeItems,
|
|
1858
|
+
entities: [...entities.apiPaths, ...entities.modelIds, ...entities.taskKeys, ...entities.snapshotKeys].slice(0, 6),
|
|
1859
|
+
hint,
|
|
1860
|
+
confidence: entities.apiPaths.length > 0 || entities.modelIds.length > 0 ? 0.95 : 0.7,
|
|
1861
|
+
summaryLines,
|
|
1862
|
+
overflowCount: Math.max(
|
|
1863
|
+
[...entities.apiPaths, ...entities.modelIds, ...entities.taskKeys, ...entities.snapshotKeys].slice(0, 6).length - representativeItems.length,
|
|
1864
|
+
0
|
|
1865
|
+
),
|
|
1866
|
+
overflowLabel: "changed entities"
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
function analyzeTestStatus(input) {
|
|
1870
|
+
const passed = getCount(input, "passed");
|
|
1871
|
+
const failed = getCount(input, "failed");
|
|
1872
|
+
const errors = Math.max(getCount(input, "errors"), getCount(input, "error"));
|
|
1873
|
+
const skipped = getCount(input, "skipped");
|
|
1874
|
+
const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
|
|
1875
|
+
const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
|
|
1876
|
+
const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
|
|
1877
|
+
const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
|
|
1878
|
+
const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
|
|
1879
|
+
const visibleErrorItems = chooseStrongestStatusFailureItems([
|
|
1880
|
+
...collectionItems.map((item) => ({
|
|
1881
|
+
...item,
|
|
1882
|
+
status: "error"
|
|
1883
|
+
})),
|
|
1884
|
+
...collectInlineFailureItemsWithStatus(input).filter((item) => item.status === "error")
|
|
1885
|
+
]);
|
|
1886
|
+
const labels = collectFailureLabels(input);
|
|
1887
|
+
const visibleErrorLabels = labels.filter((item) => item.status === "error").map((item) => item.label);
|
|
1888
|
+
const visibleFailedLabels = labels.filter((item) => item.status === "failed").map((item) => item.label);
|
|
1889
|
+
const buckets = [];
|
|
1890
|
+
const sharedBlocker = synthesizeSharedBlockerBucket({
|
|
1891
|
+
input,
|
|
1892
|
+
errors,
|
|
1893
|
+
visibleErrorItems,
|
|
1894
|
+
errorStatusLabels: visibleErrorLabels
|
|
1895
|
+
});
|
|
1896
|
+
if (sharedBlocker) {
|
|
1897
|
+
buckets.push(sharedBlocker);
|
|
1898
|
+
}
|
|
1899
|
+
if (!sharedBlocker) {
|
|
1900
|
+
const importDependencyBucket = synthesizeImportDependencyBucket({
|
|
1901
|
+
errors,
|
|
1902
|
+
visibleErrorItems
|
|
1903
|
+
});
|
|
1904
|
+
if (importDependencyBucket) {
|
|
1905
|
+
buckets.push(importDependencyBucket);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const contractDrift = synthesizeContractDriftBucket({
|
|
1909
|
+
input,
|
|
1910
|
+
visibleFailedLabels
|
|
1911
|
+
});
|
|
1912
|
+
if (contractDrift) {
|
|
1913
|
+
buckets.push(contractDrift);
|
|
1914
|
+
}
|
|
1915
|
+
return {
|
|
1916
|
+
passed,
|
|
1917
|
+
failed,
|
|
1918
|
+
errors,
|
|
1919
|
+
skipped,
|
|
1920
|
+
noTestsCollected,
|
|
1921
|
+
interrupted,
|
|
1922
|
+
collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
|
|
1923
|
+
inlineItems,
|
|
1924
|
+
collectionItems,
|
|
1925
|
+
visibleErrorLabels,
|
|
1926
|
+
visibleFailedLabels,
|
|
1927
|
+
visibleErrorItems,
|
|
1928
|
+
buckets
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function testStatusHeuristic(input, detail = "standard") {
|
|
1932
|
+
const normalized = input.trim();
|
|
1933
|
+
if (normalized === "") {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
const analysis = analyzeTestStatus(input);
|
|
1937
|
+
if (analysis.collectionErrorCount) {
|
|
1938
|
+
if (analysis.collectionItems.length > 0 || analysis.buckets.length > 0) {
|
|
1939
|
+
const decision = buildTestStatusDiagnoseContract({
|
|
1940
|
+
input,
|
|
1941
|
+
analysis
|
|
1942
|
+
});
|
|
1943
|
+
if (detail === "verbose") {
|
|
1944
|
+
return decision.verboseText;
|
|
1945
|
+
}
|
|
1946
|
+
if (detail === "focused") {
|
|
1947
|
+
return decision.focusedText;
|
|
1948
|
+
}
|
|
1949
|
+
return decision.standardText;
|
|
1950
|
+
}
|
|
1951
|
+
return [
|
|
1952
|
+
"- Tests did not complete.",
|
|
1953
|
+
`- ${formatCount2(analysis.collectionErrorCount, "error")} occurred during collection.`,
|
|
1954
|
+
...summarizeRepeatedTestCauses(input, {
|
|
1955
|
+
duringCollection: true
|
|
1956
|
+
})
|
|
1957
|
+
].join("\n");
|
|
1958
|
+
}
|
|
1959
|
+
if (analysis.noTestsCollected) {
|
|
1960
|
+
return ["- Tests did not run.", "- Collected 0 items."].join("\n");
|
|
1961
|
+
}
|
|
1962
|
+
if (analysis.interrupted && analysis.failed === 0 && analysis.errors === 0) {
|
|
1963
|
+
return "- Test run was interrupted.";
|
|
1964
|
+
}
|
|
1965
|
+
if (analysis.failed === 0 && analysis.errors === 0 && analysis.passed > 0) {
|
|
1966
|
+
const details = [formatCount2(analysis.passed, "test")];
|
|
1967
|
+
if (analysis.skipped > 0) {
|
|
1968
|
+
details.push(formatCount2(analysis.skipped, "skip"));
|
|
1969
|
+
}
|
|
1970
|
+
return ["- Tests passed.", `- ${details.join(", ")}.`].join("\n");
|
|
1971
|
+
}
|
|
1972
|
+
if (analysis.failed > 0 || analysis.errors > 0 || analysis.inlineItems.length > 0 || analysis.buckets.length > 0) {
|
|
1973
|
+
const decision = buildTestStatusDiagnoseContract({
|
|
1974
|
+
input,
|
|
1975
|
+
analysis
|
|
1976
|
+
});
|
|
1977
|
+
if (detail === "verbose") {
|
|
1978
|
+
return decision.verboseText;
|
|
1979
|
+
}
|
|
1980
|
+
if (detail === "focused") {
|
|
1981
|
+
return decision.focusedText;
|
|
1982
|
+
}
|
|
1983
|
+
return decision.standardText;
|
|
1984
|
+
}
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
function auditCriticalHeuristic(input) {
|
|
1988
|
+
const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
1989
|
+
if (!/\b(critical|high)\b/i.test(line)) {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
const pkg = inferPackage(line);
|
|
1993
|
+
if (!pkg) {
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
return {
|
|
1997
|
+
package: pkg,
|
|
1998
|
+
severity: inferSeverity(line),
|
|
1999
|
+
remediation: inferRemediation(pkg)
|
|
2000
|
+
};
|
|
2001
|
+
}).filter((item) => item !== null);
|
|
2002
|
+
if (vulnerabilities.length === 0) {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
const firstVulnerability = vulnerabilities[0];
|
|
2006
|
+
return JSON.stringify(
|
|
2007
|
+
{
|
|
2008
|
+
status: "ok",
|
|
2009
|
+
vulnerabilities,
|
|
2010
|
+
summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
|
|
2011
|
+
},
|
|
2012
|
+
null,
|
|
2013
|
+
2
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
function infraRiskHeuristic(input) {
|
|
2017
|
+
const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
|
|
2018
|
+
const riskEvidence = input.split("\n").map((line) => line.trim()).filter(
|
|
2019
|
+
(line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
|
|
2020
|
+
).slice(0, 3);
|
|
2021
|
+
if (riskEvidence.length > 0) {
|
|
2022
|
+
return JSON.stringify(
|
|
2023
|
+
{
|
|
2024
|
+
verdict: "fail",
|
|
2025
|
+
reason: "Destructive or clearly risky infrastructure change signals are present.",
|
|
2026
|
+
evidence: riskEvidence
|
|
2027
|
+
},
|
|
2028
|
+
null,
|
|
2029
|
+
2
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
if (zeroDestructiveEvidence.length > 0) {
|
|
2033
|
+
return JSON.stringify(
|
|
2034
|
+
{
|
|
2035
|
+
verdict: "pass",
|
|
2036
|
+
reason: "The provided input explicitly indicates zero destructive changes.",
|
|
2037
|
+
evidence: zeroDestructiveEvidence
|
|
2038
|
+
},
|
|
2039
|
+
null,
|
|
2040
|
+
2
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
|
|
2044
|
+
if (safeEvidence.length > 0) {
|
|
2045
|
+
return JSON.stringify(
|
|
2046
|
+
{
|
|
2047
|
+
verdict: "pass",
|
|
2048
|
+
reason: "The provided input explicitly indicates no risky infrastructure changes.",
|
|
2049
|
+
evidence: safeEvidence
|
|
2050
|
+
},
|
|
2051
|
+
null,
|
|
2052
|
+
2
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
function applyHeuristicPolicy(policyName, input, detail) {
|
|
2058
|
+
if (!policyName) {
|
|
30
2059
|
return null;
|
|
31
2060
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return typeof presetName === "string" && FAIL_ON_SUPPORTED_PRESETS.has(presetName);
|
|
35
|
-
}
|
|
36
|
-
function evaluateGate(args) {
|
|
37
|
-
const parsed = parseJson(args.output);
|
|
38
|
-
if (!parsed || typeof parsed !== "object") {
|
|
39
|
-
return { shouldFail: false };
|
|
2061
|
+
if (policyName === "audit-critical") {
|
|
2062
|
+
return auditCriticalHeuristic(input);
|
|
40
2063
|
}
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
43
|
-
shouldFail: parsed["verdict"] === "fail"
|
|
44
|
-
};
|
|
2064
|
+
if (policyName === "infra-risk") {
|
|
2065
|
+
return infraRiskHeuristic(input);
|
|
45
2066
|
}
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
const vulnerabilities = parsed["vulnerabilities"];
|
|
49
|
-
return {
|
|
50
|
-
shouldFail: status === "ok" && Array.isArray(vulnerabilities) && vulnerabilities.length > 0
|
|
51
|
-
};
|
|
2067
|
+
if (policyName === "test-status") {
|
|
2068
|
+
return testStatusHeuristic(input, detail);
|
|
52
2069
|
}
|
|
53
|
-
return
|
|
2070
|
+
return null;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// src/core/insufficient.ts
|
|
2074
|
+
function isInsufficientSignalOutput(output) {
|
|
2075
|
+
const trimmed = output.trim();
|
|
2076
|
+
return trimmed === INSUFFICIENT_SIGNAL_TEXT || trimmed.startsWith(`${INSUFFICIENT_SIGNAL_TEXT}
|
|
2077
|
+
Hint:`);
|
|
2078
|
+
}
|
|
2079
|
+
function buildInsufficientSignalOutput(input) {
|
|
2080
|
+
let hint;
|
|
2081
|
+
if (input.originalLength === 0) {
|
|
2082
|
+
hint = "Hint: no command output was captured.";
|
|
2083
|
+
} else if (input.truncatedApplied) {
|
|
2084
|
+
hint = "Hint: captured output was truncated before a clear summary was found.";
|
|
2085
|
+
} else if (input.presetName === "test-status" && input.exitCode === 0) {
|
|
2086
|
+
hint = "Hint: command succeeded, but no recognizable test summary was found.";
|
|
2087
|
+
} else if (input.presetName === "test-status" && typeof input.exitCode === "number") {
|
|
2088
|
+
hint = "Hint: command failed, but the captured output did not include a recognizable test summary.";
|
|
2089
|
+
} else {
|
|
2090
|
+
hint = "Hint: the captured output did not contain a clear answer for this preset.";
|
|
2091
|
+
}
|
|
2092
|
+
return `${INSUFFICIENT_SIGNAL_TEXT}
|
|
2093
|
+
${hint}`;
|
|
54
2094
|
}
|
|
55
2095
|
|
|
56
2096
|
// src/core/run.ts
|
|
@@ -130,7 +2170,7 @@ var OpenAIProvider = class {
|
|
|
130
2170
|
if (!text) {
|
|
131
2171
|
throw new Error("Provider returned an empty response");
|
|
132
2172
|
}
|
|
133
|
-
|
|
2173
|
+
const result = {
|
|
134
2174
|
text,
|
|
135
2175
|
usage: data?.usage ? {
|
|
136
2176
|
inputTokens: data.usage.input_tokens,
|
|
@@ -139,13 +2179,14 @@ var OpenAIProvider = class {
|
|
|
139
2179
|
} : void 0,
|
|
140
2180
|
raw: data
|
|
141
2181
|
};
|
|
2182
|
+
clearTimeout(timeout);
|
|
2183
|
+
return result;
|
|
142
2184
|
} catch (error) {
|
|
2185
|
+
clearTimeout(timeout);
|
|
143
2186
|
if (error.name === "AbortError") {
|
|
144
2187
|
throw new Error("Provider request timed out");
|
|
145
2188
|
}
|
|
146
2189
|
throw error;
|
|
147
|
-
} finally {
|
|
148
|
-
clearTimeout(timeout);
|
|
149
2190
|
}
|
|
150
2191
|
}
|
|
151
2192
|
};
|
|
@@ -227,7 +2268,7 @@ var OpenAICompatibleProvider = class {
|
|
|
227
2268
|
if (!text.trim()) {
|
|
228
2269
|
throw new Error("Provider returned an empty response");
|
|
229
2270
|
}
|
|
230
|
-
|
|
2271
|
+
const result = {
|
|
231
2272
|
text,
|
|
232
2273
|
usage: data?.usage ? {
|
|
233
2274
|
inputTokens: data.usage.prompt_tokens,
|
|
@@ -236,13 +2277,14 @@ var OpenAICompatibleProvider = class {
|
|
|
236
2277
|
} : void 0,
|
|
237
2278
|
raw: data
|
|
238
2279
|
};
|
|
2280
|
+
clearTimeout(timeout);
|
|
2281
|
+
return result;
|
|
239
2282
|
} catch (error) {
|
|
2283
|
+
clearTimeout(timeout);
|
|
240
2284
|
if (error.name === "AbortError") {
|
|
241
2285
|
throw new Error("Provider request timed out");
|
|
242
2286
|
}
|
|
243
2287
|
throw error;
|
|
244
|
-
} finally {
|
|
245
|
-
clearTimeout(timeout);
|
|
246
2288
|
}
|
|
247
2289
|
}
|
|
248
2290
|
};
|
|
@@ -423,6 +2465,29 @@ var BUILT_IN_POLICIES = {
|
|
|
423
2465
|
}
|
|
424
2466
|
};
|
|
425
2467
|
function resolvePromptPolicy(args) {
|
|
2468
|
+
if (args.policyName === "test-status" && args.goal === "diagnose") {
|
|
2469
|
+
return {
|
|
2470
|
+
name: "test-status",
|
|
2471
|
+
responseMode: args.format === "json" ? "json" : "text",
|
|
2472
|
+
outputContract: args.format === "json" ? args.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT : void 0,
|
|
2473
|
+
sharedRules: SHARED_RULES,
|
|
2474
|
+
taskRules: args.format === "json" ? [
|
|
2475
|
+
"Return only valid JSON.",
|
|
2476
|
+
`Use this exact contract: ${args.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT}.`,
|
|
2477
|
+
"Treat the heuristic context as extraction guidance, but do not invent hidden failures.",
|
|
2478
|
+
"Use the heuristic extract as the bucket truth unless the visible command output clearly disproves it.",
|
|
2479
|
+
"Identify the dominant blocker, remaining visible failure buckets, the decision, and the next best action.",
|
|
2480
|
+
"Set diagnosis_complete to true only when the visible output is already sufficient to stop and act.",
|
|
2481
|
+
"Set raw_needed to true only when exact traceback lines are still required.",
|
|
2482
|
+
"Set provider_confidence to a number between 0 and 1, or null only when confidence cannot be estimated."
|
|
2483
|
+
] : [
|
|
2484
|
+
"Produce a decision-complete diagnosis.",
|
|
2485
|
+
"Name the main failure buckets, include counts and dominant root cause, and end with an explicit Decision line plus an explicit stop signal.",
|
|
2486
|
+
"Prefer blocker-first ordering and keep evidence budget small.",
|
|
2487
|
+
"Do not ask for more context."
|
|
2488
|
+
]
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
426
2491
|
if (args.policyName) {
|
|
427
2492
|
const policy = BUILT_IN_POLICIES[args.policyName];
|
|
428
2493
|
return {
|
|
@@ -444,17 +2509,35 @@ function resolvePromptPolicy(args) {
|
|
|
444
2509
|
function buildPrompt(args) {
|
|
445
2510
|
const policy = resolvePromptPolicy({
|
|
446
2511
|
format: args.format,
|
|
2512
|
+
goal: args.goal,
|
|
447
2513
|
policyName: args.policyName,
|
|
448
2514
|
outputContract: args.outputContract
|
|
449
2515
|
});
|
|
2516
|
+
const detailRules = args.policyName === "test-status" && args.detail === "focused" ? [
|
|
2517
|
+
"Use a focused failure view.",
|
|
2518
|
+
"When the output clearly maps failures to specific tests or modules, group them by dominant error type first.",
|
|
2519
|
+
"Within each error group, prefer compact bullets in the form '- test-or-module -> dominant reason'.",
|
|
2520
|
+
"Cap focused entries at 6 per error group and end with '- and N more failing modules' if more clear mappings are visible.",
|
|
2521
|
+
"If per-test or per-module mapping is unclear, fall back to grouped root causes instead of guessing."
|
|
2522
|
+
] : args.policyName === "test-status" && args.detail === "verbose" ? [
|
|
2523
|
+
"Use a verbose failure view.",
|
|
2524
|
+
"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'.",
|
|
2525
|
+
"Preserve the original file or module order when the mapping is visible.",
|
|
2526
|
+
"Prefer concrete normalized reasons such as missing modules or assertion failures over traceback plumbing.",
|
|
2527
|
+
"If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
|
|
2528
|
+
] : [];
|
|
450
2529
|
const prompt = [
|
|
451
2530
|
"You are Sift, a CLI output reduction assistant for downstream agents and automation.",
|
|
452
2531
|
"Hard rules:",
|
|
453
2532
|
...policy.sharedRules.map((rule) => `- ${rule}`),
|
|
454
2533
|
"",
|
|
2534
|
+
`Goal: ${args.goal ?? "summarize"}`,
|
|
2535
|
+
"",
|
|
455
2536
|
`Task policy: ${policy.name}`,
|
|
456
2537
|
...policy.taskRules.map((rule) => `- ${rule}`),
|
|
2538
|
+
...detailRules.map((rule) => `- ${rule}`),
|
|
457
2539
|
...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
|
|
2540
|
+
...args.analysisContext ? ["", "Visible heuristic context:", '"""', args.analysisContext, '"""'] : [],
|
|
458
2541
|
"",
|
|
459
2542
|
`Question: ${args.question}`,
|
|
460
2543
|
"",
|
|
@@ -523,7 +2606,10 @@ function buildStructuredError(reason) {
|
|
|
523
2606
|
return {
|
|
524
2607
|
status: "error",
|
|
525
2608
|
reason,
|
|
526
|
-
retriable: isRetriableReason(reason)
|
|
2609
|
+
retriable: isRetriableReason(reason),
|
|
2610
|
+
provider_failed: true,
|
|
2611
|
+
raw_needed: true,
|
|
2612
|
+
why_raw_needed: "Provider follow-up failed, so the reduced answer may still need exact raw evidence."
|
|
527
2613
|
};
|
|
528
2614
|
}
|
|
529
2615
|
function buildFallbackOutput(args) {
|
|
@@ -543,110 +2629,11 @@ function buildFallbackOutput(args) {
|
|
|
543
2629
|
return JSON.stringify(buildStructuredError(args.reason), null, 2);
|
|
544
2630
|
}
|
|
545
2631
|
const prefix = `Sift fallback triggered (${args.reason}).`;
|
|
2632
|
+
const rawHint = "Raw may still be needed because provider follow-up failed.";
|
|
546
2633
|
if (!args.rawFallback) {
|
|
547
|
-
return prefix
|
|
548
|
-
}
|
|
549
|
-
return [prefix, "", args.rawInput.slice(-RAW_FALLBACK_SLICE)].join("\n");
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// src/core/heuristics.ts
|
|
553
|
-
var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
|
|
554
|
-
var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
|
|
555
|
-
var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
|
|
556
|
-
function collectEvidence(input, matcher, limit = 3) {
|
|
557
|
-
return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
|
|
558
|
-
}
|
|
559
|
-
function inferSeverity(token) {
|
|
560
|
-
return token.toLowerCase().includes("critical") ? "critical" : "high";
|
|
561
|
-
}
|
|
562
|
-
function inferPackage(line) {
|
|
563
|
-
const match = line.match(/^\s*([@a-z0-9._/-]+)\s*:/i);
|
|
564
|
-
return match?.[1] ?? null;
|
|
565
|
-
}
|
|
566
|
-
function inferRemediation(pkg) {
|
|
567
|
-
return `Upgrade ${pkg} to a patched version.`;
|
|
568
|
-
}
|
|
569
|
-
function auditCriticalHeuristic(input) {
|
|
570
|
-
const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
571
|
-
if (!/\b(critical|high)\b/i.test(line)) {
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
const pkg = inferPackage(line);
|
|
575
|
-
if (!pkg) {
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
return {
|
|
579
|
-
package: pkg,
|
|
580
|
-
severity: inferSeverity(line),
|
|
581
|
-
remediation: inferRemediation(pkg)
|
|
582
|
-
};
|
|
583
|
-
}).filter((item) => item !== null);
|
|
584
|
-
if (vulnerabilities.length === 0) {
|
|
585
|
-
return null;
|
|
586
|
-
}
|
|
587
|
-
const firstVulnerability = vulnerabilities[0];
|
|
588
|
-
return JSON.stringify(
|
|
589
|
-
{
|
|
590
|
-
status: "ok",
|
|
591
|
-
vulnerabilities,
|
|
592
|
-
summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
|
|
593
|
-
},
|
|
594
|
-
null,
|
|
595
|
-
2
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
function infraRiskHeuristic(input) {
|
|
599
|
-
const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
|
|
600
|
-
const riskEvidence = input.split("\n").map((line) => line.trim()).filter(
|
|
601
|
-
(line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
|
|
602
|
-
).slice(0, 3);
|
|
603
|
-
if (riskEvidence.length > 0) {
|
|
604
|
-
return JSON.stringify(
|
|
605
|
-
{
|
|
606
|
-
verdict: "fail",
|
|
607
|
-
reason: "Destructive or clearly risky infrastructure change signals are present.",
|
|
608
|
-
evidence: riskEvidence
|
|
609
|
-
},
|
|
610
|
-
null,
|
|
611
|
-
2
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
if (zeroDestructiveEvidence.length > 0) {
|
|
615
|
-
return JSON.stringify(
|
|
616
|
-
{
|
|
617
|
-
verdict: "pass",
|
|
618
|
-
reason: "The provided input explicitly indicates zero destructive changes.",
|
|
619
|
-
evidence: zeroDestructiveEvidence
|
|
620
|
-
},
|
|
621
|
-
null,
|
|
622
|
-
2
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
|
|
626
|
-
if (safeEvidence.length > 0) {
|
|
627
|
-
return JSON.stringify(
|
|
628
|
-
{
|
|
629
|
-
verdict: "pass",
|
|
630
|
-
reason: "The provided input explicitly indicates no risky infrastructure changes.",
|
|
631
|
-
evidence: safeEvidence
|
|
632
|
-
},
|
|
633
|
-
null,
|
|
634
|
-
2
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
function applyHeuristicPolicy(policyName, input) {
|
|
640
|
-
if (!policyName) {
|
|
641
|
-
return null;
|
|
2634
|
+
return `${prefix} ${rawHint}`;
|
|
642
2635
|
}
|
|
643
|
-
|
|
644
|
-
return auditCriticalHeuristic(input);
|
|
645
|
-
}
|
|
646
|
-
if (policyName === "infra-risk") {
|
|
647
|
-
return infraRiskHeuristic(input);
|
|
648
|
-
}
|
|
649
|
-
return null;
|
|
2636
|
+
return [prefix, rawHint, "", args.rawInput.slice(-RAW_FALLBACK_SLICE)].join("\n");
|
|
650
2637
|
}
|
|
651
2638
|
|
|
652
2639
|
// src/core/redact.ts
|
|
@@ -749,8 +2736,297 @@ function prepareInput(raw, config) {
|
|
|
749
2736
|
};
|
|
750
2737
|
}
|
|
751
2738
|
|
|
2739
|
+
// src/core/rawSlice.ts
|
|
2740
|
+
function escapeRegExp(value) {
|
|
2741
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2742
|
+
}
|
|
2743
|
+
function unique2(values) {
|
|
2744
|
+
return [...new Set(values)];
|
|
2745
|
+
}
|
|
2746
|
+
function buildLineWindows(args) {
|
|
2747
|
+
const selected = /* @__PURE__ */ new Set();
|
|
2748
|
+
for (const index of args.indexes) {
|
|
2749
|
+
for (let cursor = Math.max(0, index - args.radius); cursor <= Math.min(args.lines.length - 1, index + args.radius); cursor += 1) {
|
|
2750
|
+
selected.add(cursor);
|
|
2751
|
+
if (selected.size >= args.maxLines) {
|
|
2752
|
+
break;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
if (selected.size >= args.maxLines) {
|
|
2756
|
+
break;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
return [...selected].sort((left, right) => left - right).map((index) => args.lines[index]);
|
|
2760
|
+
}
|
|
2761
|
+
function collapseSelectedLines(args) {
|
|
2762
|
+
if (args.lines.length === 0) {
|
|
2763
|
+
return args.fallback();
|
|
2764
|
+
}
|
|
2765
|
+
const joined = unique2(args.lines).join("\n").trim();
|
|
2766
|
+
if (joined.length === 0) {
|
|
2767
|
+
return args.fallback();
|
|
2768
|
+
}
|
|
2769
|
+
if (joined.length <= args.maxInputChars) {
|
|
2770
|
+
return joined;
|
|
2771
|
+
}
|
|
2772
|
+
return truncateInput(joined, {
|
|
2773
|
+
maxInputChars: args.maxInputChars,
|
|
2774
|
+
headChars: Math.min(Math.max(200, Math.floor(args.maxInputChars * 0.55)), args.maxInputChars),
|
|
2775
|
+
tailChars: Math.min(Math.max(120, Math.floor(args.maxInputChars * 0.2)), args.maxInputChars)
|
|
2776
|
+
}).text;
|
|
2777
|
+
}
|
|
2778
|
+
function collapseSelectedLineGroups(args) {
|
|
2779
|
+
const selected = [];
|
|
2780
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2781
|
+
const groups = args.groups.map(
|
|
2782
|
+
(group) => group.map((line) => line.trimEnd()).filter((line) => line.length > 0)
|
|
2783
|
+
);
|
|
2784
|
+
const cursors = groups.map(() => 0);
|
|
2785
|
+
let addedInPass = true;
|
|
2786
|
+
while (addedInPass) {
|
|
2787
|
+
addedInPass = false;
|
|
2788
|
+
for (const [groupIndex, group] of groups.entries()) {
|
|
2789
|
+
while (cursors[groupIndex] < group.length) {
|
|
2790
|
+
const line = group[cursors[groupIndex]];
|
|
2791
|
+
cursors[groupIndex] = cursors[groupIndex] + 1;
|
|
2792
|
+
if (seen.has(line)) {
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
const candidate = [...selected, line].join("\n");
|
|
2796
|
+
if (candidate.length > args.maxInputChars) {
|
|
2797
|
+
break;
|
|
2798
|
+
}
|
|
2799
|
+
selected.push(line);
|
|
2800
|
+
seen.add(line);
|
|
2801
|
+
addedInPass = true;
|
|
2802
|
+
break;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
if (selected.length === 0) {
|
|
2807
|
+
return args.fallback();
|
|
2808
|
+
}
|
|
2809
|
+
return selected.join("\n");
|
|
2810
|
+
}
|
|
2811
|
+
function buildHeadTailFallback(input, config) {
|
|
2812
|
+
const fallback = truncateInput(input, {
|
|
2813
|
+
maxInputChars: config.maxInputChars,
|
|
2814
|
+
headChars: config.headChars,
|
|
2815
|
+
tailChars: config.tailChars
|
|
2816
|
+
});
|
|
2817
|
+
return {
|
|
2818
|
+
text: fallback.text,
|
|
2819
|
+
strategy: "head_tail",
|
|
2820
|
+
used: fallback.truncatedApplied
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
function findReadTargetIndexes(args) {
|
|
2824
|
+
const escapedFile = escapeRegExp(args.file);
|
|
2825
|
+
const exactPatterns = args.line === null ? [new RegExp(escapedFile)] : [
|
|
2826
|
+
new RegExp(`${escapedFile}:${args.line}(?::\\d+)?`),
|
|
2827
|
+
new RegExp(`File\\s+"${escapedFile}",\\s+line\\s+${args.line}\\b`),
|
|
2828
|
+
new RegExp(`['"]${escapedFile}['"].*\\b${args.line}\\b`)
|
|
2829
|
+
];
|
|
2830
|
+
const matches = args.lines.map(
|
|
2831
|
+
(line, index) => exactPatterns.some((pattern) => pattern.test(line)) ? index : -1
|
|
2832
|
+
).filter((index) => index >= 0);
|
|
2833
|
+
if (matches.length > 0) {
|
|
2834
|
+
return matches;
|
|
2835
|
+
}
|
|
2836
|
+
if (args.contextHint.start_line !== null && args.contextHint.end_line !== null) {
|
|
2837
|
+
const startLine = args.contextHint.start_line;
|
|
2838
|
+
const endLine = args.contextHint.end_line;
|
|
2839
|
+
const rangeMatches = args.lines.map((line, index) => {
|
|
2840
|
+
const fileWithLine = line.match(/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):(\d+)(?::\d+)?:\s+in\b/) ?? line.match(/^([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?:\s+in\b/) ?? line.match(/^File\s+"([^"]+)",\s+line\s+(\d+)/);
|
|
2841
|
+
if (!fileWithLine || !fileWithLine[1] || !fileWithLine[2]) {
|
|
2842
|
+
return -1;
|
|
2843
|
+
}
|
|
2844
|
+
if (fileWithLine[1].replace(/\\/g, "/") !== args.file) {
|
|
2845
|
+
return -1;
|
|
2846
|
+
}
|
|
2847
|
+
const lineNumber = Number(fileWithLine[2]);
|
|
2848
|
+
return lineNumber >= startLine && lineNumber <= endLine ? index : -1;
|
|
2849
|
+
}).filter((index) => index >= 0);
|
|
2850
|
+
if (rangeMatches.length > 0) {
|
|
2851
|
+
return rangeMatches;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
if (args.line !== null) {
|
|
2855
|
+
return [];
|
|
2856
|
+
}
|
|
2857
|
+
return args.lines.map((line, index) => line.includes(args.file) ? index : -1).filter((index) => index >= 0);
|
|
2858
|
+
}
|
|
2859
|
+
function findSearchHintIndexes(args) {
|
|
2860
|
+
if (!args.searchHint) {
|
|
2861
|
+
return [];
|
|
2862
|
+
}
|
|
2863
|
+
const pattern = new RegExp(escapeRegExp(args.searchHint), "i");
|
|
2864
|
+
return args.lines.map((line, index) => pattern.test(line) ? index : -1).filter((index) => index >= 0);
|
|
2865
|
+
}
|
|
2866
|
+
function buildTracebackSlice(args) {
|
|
2867
|
+
const lines = args.input.split("\n");
|
|
2868
|
+
const indexes = lines.map(
|
|
2869
|
+
(line, index) => /(traceback|^E\s|error\b|failed\b|exception\b|assertionerror\b|runtimeerror\b)/i.test(line) ? index : -1
|
|
2870
|
+
).filter((index) => index >= 0);
|
|
2871
|
+
if (indexes.length === 0) {
|
|
2872
|
+
return buildHeadTailFallback(args.input, args.config);
|
|
2873
|
+
}
|
|
2874
|
+
const text = collapseSelectedLines({
|
|
2875
|
+
lines: buildLineWindows({
|
|
2876
|
+
lines,
|
|
2877
|
+
indexes,
|
|
2878
|
+
radius: 3,
|
|
2879
|
+
maxLines: 80
|
|
2880
|
+
}),
|
|
2881
|
+
maxInputChars: args.config.maxInputChars,
|
|
2882
|
+
fallback: () => truncateInput(args.input, {
|
|
2883
|
+
maxInputChars: args.config.maxInputChars,
|
|
2884
|
+
headChars: args.config.headChars,
|
|
2885
|
+
tailChars: args.config.tailChars
|
|
2886
|
+
}).text
|
|
2887
|
+
});
|
|
2888
|
+
return {
|
|
2889
|
+
text,
|
|
2890
|
+
strategy: "traceback_window",
|
|
2891
|
+
used: true
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
function buildTestStatusRawSlice(args) {
|
|
2895
|
+
if (args.input.length <= args.config.maxInputChars) {
|
|
2896
|
+
return {
|
|
2897
|
+
text: args.input,
|
|
2898
|
+
strategy: "none",
|
|
2899
|
+
used: false
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
const lines = args.input.split("\n");
|
|
2903
|
+
const summaryIndexes = lines.map(
|
|
2904
|
+
(line, index) => /(=+.*(?:failed|errors?|passed|no tests ran|interrupted).*=+|\b\d+\s+failed\b|\b\d+\s+errors?\b)/i.test(
|
|
2905
|
+
line
|
|
2906
|
+
) ? index : -1
|
|
2907
|
+
).filter((index) => index >= 0);
|
|
2908
|
+
const bucketGroups = args.contract.main_buckets.map((bucket) => {
|
|
2909
|
+
const bucketTerms = unique2(
|
|
2910
|
+
[bucket.root_cause, ...bucket.evidence].map((value) => value.split(":").at(-1)?.trim() ?? value.trim()).filter((value) => value.length >= 4)
|
|
2911
|
+
);
|
|
2912
|
+
const indexes = lines.map(
|
|
2913
|
+
(line, index) => bucketTerms.some((term) => new RegExp(escapeRegExp(term), "i").test(line)) ? index : -1
|
|
2914
|
+
).filter((index) => index >= 0);
|
|
2915
|
+
return unique2([
|
|
2916
|
+
...indexes.map((index) => lines[index]).filter(Boolean),
|
|
2917
|
+
...buildLineWindows({
|
|
2918
|
+
lines,
|
|
2919
|
+
indexes,
|
|
2920
|
+
radius: 2,
|
|
2921
|
+
maxLines: 16
|
|
2922
|
+
})
|
|
2923
|
+
]);
|
|
2924
|
+
});
|
|
2925
|
+
const targetGroups = args.contract.read_targets.map(
|
|
2926
|
+
(target) => buildLineWindows({
|
|
2927
|
+
lines,
|
|
2928
|
+
indexes: unique2([
|
|
2929
|
+
...findReadTargetIndexes({
|
|
2930
|
+
lines,
|
|
2931
|
+
file: target.file,
|
|
2932
|
+
line: target.line,
|
|
2933
|
+
contextHint: target.context_hint
|
|
2934
|
+
}),
|
|
2935
|
+
...findSearchHintIndexes({
|
|
2936
|
+
lines,
|
|
2937
|
+
searchHint: target.context_hint.search_hint
|
|
2938
|
+
})
|
|
2939
|
+
]),
|
|
2940
|
+
radius: target.line === null ? 1 : 2,
|
|
2941
|
+
maxLines: target.line === null ? 6 : 8
|
|
2942
|
+
})
|
|
2943
|
+
);
|
|
2944
|
+
const failureIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) || /^E\s/.test(line) ? index : -1).filter((index) => index >= 0);
|
|
2945
|
+
const selected = collapseSelectedLineGroups({
|
|
2946
|
+
groups: [
|
|
2947
|
+
...targetGroups,
|
|
2948
|
+
unique2([
|
|
2949
|
+
...summaryIndexes.map((index) => lines[index]).filter(Boolean),
|
|
2950
|
+
...buildLineWindows({
|
|
2951
|
+
lines,
|
|
2952
|
+
indexes: summaryIndexes,
|
|
2953
|
+
radius: 1,
|
|
2954
|
+
maxLines: 12
|
|
2955
|
+
})
|
|
2956
|
+
]),
|
|
2957
|
+
...bucketGroups,
|
|
2958
|
+
buildLineWindows({
|
|
2959
|
+
lines,
|
|
2960
|
+
indexes: failureIndexes,
|
|
2961
|
+
radius: 1,
|
|
2962
|
+
maxLines: 24
|
|
2963
|
+
})
|
|
2964
|
+
],
|
|
2965
|
+
maxInputChars: args.config.maxInputChars,
|
|
2966
|
+
fallback: () => truncateInput(args.input, {
|
|
2967
|
+
maxInputChars: args.config.maxInputChars,
|
|
2968
|
+
headChars: args.config.headChars,
|
|
2969
|
+
tailChars: args.config.tailChars
|
|
2970
|
+
}).text
|
|
2971
|
+
});
|
|
2972
|
+
if (selected.trim().length === 0) {
|
|
2973
|
+
return buildTracebackSlice({
|
|
2974
|
+
input: args.input,
|
|
2975
|
+
config: args.config
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
return {
|
|
2979
|
+
text: selected,
|
|
2980
|
+
strategy: "bucket_evidence",
|
|
2981
|
+
used: true
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
function buildGenericRawSlice(args) {
|
|
2985
|
+
if (args.input.length <= args.config.maxInputChars) {
|
|
2986
|
+
return {
|
|
2987
|
+
text: args.input,
|
|
2988
|
+
strategy: "none",
|
|
2989
|
+
used: false
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
return buildTracebackSlice(args);
|
|
2993
|
+
}
|
|
2994
|
+
|
|
752
2995
|
// src/core/run.ts
|
|
753
2996
|
var RETRY_DELAY_MS = 300;
|
|
2997
|
+
function estimateTokenCount(text) {
|
|
2998
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
2999
|
+
}
|
|
3000
|
+
function getDiagnosisCompleteAtLayer(contract) {
|
|
3001
|
+
if (contract.raw_needed || contract.provider_failed) {
|
|
3002
|
+
return "raw";
|
|
3003
|
+
}
|
|
3004
|
+
if (contract.provider_used) {
|
|
3005
|
+
return "provider";
|
|
3006
|
+
}
|
|
3007
|
+
return "heuristic";
|
|
3008
|
+
}
|
|
3009
|
+
function logVerboseTestStatusTelemetry(args) {
|
|
3010
|
+
if (!args.request.config.runtime.verbose) {
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
const lines = [
|
|
3014
|
+
`${pc.dim("sift")} diagnosis_complete_at_layer=${getDiagnosisCompleteAtLayer(args.contract)}`,
|
|
3015
|
+
`${pc.dim("sift")} heuristic_short_circuit=${!args.contract.provider_used && args.contract.diagnosis_complete && !args.contract.raw_needed && !args.contract.provider_failed}`,
|
|
3016
|
+
`${pc.dim("sift")} raw_input_chars=${args.request.stdin.length}`,
|
|
3017
|
+
`${pc.dim("sift")} prepared_input_chars=${args.prepared.meta.finalLength}`,
|
|
3018
|
+
`${pc.dim("sift")} raw_slice_chars=${args.rawSliceChars ?? 0}`,
|
|
3019
|
+
`${pc.dim("sift")} provider_input_chars=${args.providerInputChars ?? 0}`,
|
|
3020
|
+
`${pc.dim("sift")} provider_output_chars=${args.providerOutputChars ?? 0}`,
|
|
3021
|
+
`${pc.dim("sift")} final_output_chars=${args.finalOutput.length}`,
|
|
3022
|
+
`${pc.dim("sift")} final_output_tokens_est=${estimateTokenCount(args.finalOutput)}`,
|
|
3023
|
+
`${pc.dim("sift")} read_targets_count=${args.contract.read_targets.length}`,
|
|
3024
|
+
`${pc.dim("sift")} remaining_count=${args.contract.remaining_tests.length}`,
|
|
3025
|
+
`${pc.dim("sift")} remaining_ids_exposed=${Boolean(args.request.includeTestIds)}`
|
|
3026
|
+
];
|
|
3027
|
+
process.stderr.write(`${lines.join("\n")}
|
|
3028
|
+
`);
|
|
3029
|
+
}
|
|
754
3030
|
function normalizeOutput(text, responseMode) {
|
|
755
3031
|
if (responseMode !== "json") {
|
|
756
3032
|
return text.trim();
|
|
@@ -766,7 +3042,7 @@ function buildDryRunOutput(args) {
|
|
|
766
3042
|
return JSON.stringify(
|
|
767
3043
|
{
|
|
768
3044
|
status: "dry-run",
|
|
769
|
-
strategy: args.heuristicOutput ? "heuristic" : "provider",
|
|
3045
|
+
strategy: args.strategy ?? (args.heuristicOutput ? "heuristic" : "provider"),
|
|
770
3046
|
provider: {
|
|
771
3047
|
name: args.providerName,
|
|
772
3048
|
model: args.request.config.provider.model,
|
|
@@ -775,6 +3051,7 @@ function buildDryRunOutput(args) {
|
|
|
775
3051
|
},
|
|
776
3052
|
question: args.request.question,
|
|
777
3053
|
format: args.request.format,
|
|
3054
|
+
detail: args.request.detail ?? null,
|
|
778
3055
|
responseMode: args.responseMode,
|
|
779
3056
|
policy: args.request.policyName ?? null,
|
|
780
3057
|
heuristicOutput: args.heuristicOutput ?? null,
|
|
@@ -794,108 +3071,936 @@ function buildDryRunOutput(args) {
|
|
|
794
3071
|
async function delay(ms) {
|
|
795
3072
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
796
3073
|
}
|
|
3074
|
+
function withInsufficientHint(args) {
|
|
3075
|
+
if (!isInsufficientSignalOutput(args.output)) {
|
|
3076
|
+
return args.output;
|
|
3077
|
+
}
|
|
3078
|
+
return buildInsufficientSignalOutput({
|
|
3079
|
+
presetName: args.request.presetName,
|
|
3080
|
+
originalLength: args.prepared.meta.originalLength,
|
|
3081
|
+
truncatedApplied: args.prepared.meta.truncatedApplied
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
797
3084
|
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}
|
|
3085
|
+
const generate = () => args.provider.generate({
|
|
3086
|
+
model: args.request.config.provider.model,
|
|
3087
|
+
prompt: args.prompt,
|
|
3088
|
+
temperature: args.request.config.provider.temperature,
|
|
3089
|
+
maxOutputTokens: args.request.config.provider.maxOutputTokens,
|
|
3090
|
+
timeoutMs: args.request.config.provider.timeoutMs,
|
|
3091
|
+
responseMode: args.responseMode,
|
|
3092
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
3093
|
+
});
|
|
3094
|
+
try {
|
|
3095
|
+
return await generate();
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
3098
|
+
if (!isRetriableReason(reason)) {
|
|
3099
|
+
throw error;
|
|
3100
|
+
}
|
|
3101
|
+
if (args.request.config.runtime.verbose) {
|
|
3102
|
+
process.stderr.write(
|
|
3103
|
+
`${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
819
3104
|
`
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
await delay(RETRY_DELAY_MS);
|
|
3105
|
+
);
|
|
823
3106
|
}
|
|
3107
|
+
await delay(RETRY_DELAY_MS);
|
|
3108
|
+
}
|
|
3109
|
+
return generate();
|
|
3110
|
+
}
|
|
3111
|
+
function hasRecognizableTestStatusSignal(input) {
|
|
3112
|
+
const analysis = analyzeTestStatus(input);
|
|
3113
|
+
return analysis.collectionErrorCount !== void 0 || analysis.noTestsCollected || analysis.interrupted || analysis.failed > 0 || analysis.errors > 0 || analysis.passed > 0 || analysis.inlineItems.length > 0 || analysis.buckets.length > 0;
|
|
3114
|
+
}
|
|
3115
|
+
function renderTestStatusDecisionOutput(args) {
|
|
3116
|
+
if (args.request.goal === "diagnose" && args.request.format === "json") {
|
|
3117
|
+
return JSON.stringify(
|
|
3118
|
+
buildTestStatusPublicDiagnoseContract({
|
|
3119
|
+
contract: args.decision.contract,
|
|
3120
|
+
includeTestIds: args.request.includeTestIds,
|
|
3121
|
+
remainingSubsetAvailable: args.request.testStatusContext?.remainingSubsetAvailable
|
|
3122
|
+
}),
|
|
3123
|
+
null,
|
|
3124
|
+
2
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
if (args.request.detail === "verbose") {
|
|
3128
|
+
return args.decision.verboseText;
|
|
824
3129
|
}
|
|
825
|
-
|
|
3130
|
+
if (args.request.detail === "focused") {
|
|
3131
|
+
return args.decision.focusedText;
|
|
3132
|
+
}
|
|
3133
|
+
return args.decision.standardText;
|
|
3134
|
+
}
|
|
3135
|
+
function buildTestStatusProviderFailureDecision(args) {
|
|
3136
|
+
const shouldZoomFirst = args.request.detail !== "verbose";
|
|
3137
|
+
return buildTestStatusDiagnoseContract({
|
|
3138
|
+
input: args.input,
|
|
3139
|
+
analysis: args.analysis,
|
|
3140
|
+
resolvedTests: args.baseDecision.contract.resolved_tests,
|
|
3141
|
+
remainingTests: args.baseDecision.contract.remaining_tests,
|
|
3142
|
+
contractOverrides: {
|
|
3143
|
+
...args.baseDecision.contract,
|
|
3144
|
+
diagnosis_complete: false,
|
|
3145
|
+
raw_needed: true,
|
|
3146
|
+
additional_source_read_likely_low_value: false,
|
|
3147
|
+
read_raw_only_if: shouldZoomFirst ? "the provider follow-up failed and one deeper sift pass still is not enough" : "the provider follow-up failed and you still need exact traceback lines",
|
|
3148
|
+
decision: shouldZoomFirst ? "zoom" : "read_raw",
|
|
3149
|
+
provider_used: true,
|
|
3150
|
+
provider_confidence: null,
|
|
3151
|
+
provider_failed: true,
|
|
3152
|
+
raw_slice_used: args.rawSliceUsed,
|
|
3153
|
+
raw_slice_strategy: args.rawSliceStrategy,
|
|
3154
|
+
next_best_action: {
|
|
3155
|
+
code: shouldZoomFirst ? "insufficient_signal" : "read_raw_for_exact_traceback",
|
|
3156
|
+
bucket_index: args.baseDecision.contract.dominant_blocker_bucket_index ?? args.baseDecision.contract.main_buckets[0]?.bucket_index ?? null,
|
|
3157
|
+
note: shouldZoomFirst ? `Provider follow-up failed (${args.reason}). Use one deeper sift pass on the same cached output before reading raw traceback lines.` : `Provider follow-up failed (${args.reason}). Read raw traceback only if exact stack lines are still needed.`
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
826
3161
|
}
|
|
827
3162
|
async function runSift(request) {
|
|
828
3163
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
829
|
-
const { prompt, responseMode } = buildPrompt({
|
|
830
|
-
question: request.question,
|
|
831
|
-
format: request.format,
|
|
832
|
-
input: prepared.truncated,
|
|
833
|
-
policyName: request.policyName,
|
|
834
|
-
outputContract: request.outputContract
|
|
835
|
-
});
|
|
836
3164
|
const provider = createProvider(request.config);
|
|
3165
|
+
const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(prepared.truncated);
|
|
3166
|
+
const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(prepared.truncated) : null;
|
|
3167
|
+
const testStatusDecision = hasTestStatusSignal && testStatusAnalysis ? buildTestStatusDiagnoseContract({
|
|
3168
|
+
input: prepared.truncated,
|
|
3169
|
+
analysis: testStatusAnalysis,
|
|
3170
|
+
resolvedTests: request.testStatusContext?.resolvedTests,
|
|
3171
|
+
remainingTests: request.testStatusContext?.remainingTests
|
|
3172
|
+
}) : null;
|
|
3173
|
+
const testStatusHeuristicOutput = testStatusDecision ? renderTestStatusDecisionOutput({
|
|
3174
|
+
request,
|
|
3175
|
+
decision: testStatusDecision
|
|
3176
|
+
}) : null;
|
|
837
3177
|
if (request.config.runtime.verbose) {
|
|
838
3178
|
process.stderr.write(
|
|
839
3179
|
`${pc.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
|
|
840
3180
|
`
|
|
841
3181
|
);
|
|
842
3182
|
}
|
|
843
|
-
const heuristicOutput = applyHeuristicPolicy(
|
|
844
|
-
request.policyName,
|
|
845
|
-
prepared.truncated
|
|
846
|
-
);
|
|
3183
|
+
const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, prepared.truncated, request.detail);
|
|
847
3184
|
if (heuristicOutput) {
|
|
848
3185
|
if (request.config.runtime.verbose) {
|
|
849
3186
|
process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
|
|
850
3187
|
`);
|
|
851
3188
|
}
|
|
3189
|
+
const heuristicPrompt = buildPrompt({
|
|
3190
|
+
question: request.question,
|
|
3191
|
+
format: request.format,
|
|
3192
|
+
goal: request.goal,
|
|
3193
|
+
input: prepared.truncated,
|
|
3194
|
+
detail: request.detail,
|
|
3195
|
+
policyName: request.policyName,
|
|
3196
|
+
outputContract: request.policyName === "test-status" && request.goal === "diagnose" && request.format === "json" ? request.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT : request.outputContract,
|
|
3197
|
+
analysisContext: [
|
|
3198
|
+
request.analysisContext,
|
|
3199
|
+
testStatusDecision ? buildTestStatusAnalysisContext({
|
|
3200
|
+
contract: testStatusDecision.contract,
|
|
3201
|
+
includeTestIds: request.includeTestIds,
|
|
3202
|
+
remainingSubsetAvailable: request.testStatusContext?.remainingSubsetAvailable
|
|
3203
|
+
}) : void 0
|
|
3204
|
+
].filter((value) => Boolean(value)).join("\n\n")
|
|
3205
|
+
});
|
|
3206
|
+
if (request.dryRun) {
|
|
3207
|
+
return buildDryRunOutput({
|
|
3208
|
+
request,
|
|
3209
|
+
providerName: provider.name,
|
|
3210
|
+
prompt: heuristicPrompt.prompt,
|
|
3211
|
+
responseMode: heuristicPrompt.responseMode,
|
|
3212
|
+
prepared,
|
|
3213
|
+
heuristicOutput,
|
|
3214
|
+
strategy: "heuristic"
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
const finalOutput = withInsufficientHint({
|
|
3218
|
+
output: heuristicOutput,
|
|
3219
|
+
request,
|
|
3220
|
+
prepared
|
|
3221
|
+
});
|
|
3222
|
+
if (testStatusDecision) {
|
|
3223
|
+
logVerboseTestStatusTelemetry({
|
|
3224
|
+
request,
|
|
3225
|
+
prepared,
|
|
3226
|
+
contract: testStatusDecision.contract,
|
|
3227
|
+
finalOutput
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
return finalOutput;
|
|
3231
|
+
}
|
|
3232
|
+
if (testStatusDecision && testStatusAnalysis) {
|
|
3233
|
+
const rawSlice = buildTestStatusRawSlice({
|
|
3234
|
+
input: prepared.redacted,
|
|
3235
|
+
config: request.config.input,
|
|
3236
|
+
contract: testStatusDecision.contract
|
|
3237
|
+
});
|
|
3238
|
+
const prompt = buildPrompt({
|
|
3239
|
+
question: "Complete the diagnosis. Use the heuristic extract as the bucket truth and only change the decision when the sliced command output proves it.",
|
|
3240
|
+
format: "json",
|
|
3241
|
+
goal: "diagnose",
|
|
3242
|
+
input: rawSlice.text,
|
|
3243
|
+
detail: request.detail,
|
|
3244
|
+
policyName: "test-status",
|
|
3245
|
+
outputContract: TEST_STATUS_PROVIDER_SUPPLEMENT_JSON_CONTRACT,
|
|
3246
|
+
analysisContext: [
|
|
3247
|
+
request.analysisContext,
|
|
3248
|
+
buildTestStatusAnalysisContext({
|
|
3249
|
+
contract: {
|
|
3250
|
+
...testStatusDecision.contract,
|
|
3251
|
+
provider_used: true,
|
|
3252
|
+
provider_failed: false,
|
|
3253
|
+
raw_slice_used: rawSlice.used,
|
|
3254
|
+
raw_slice_strategy: rawSlice.strategy
|
|
3255
|
+
},
|
|
3256
|
+
includeTestIds: request.includeTestIds,
|
|
3257
|
+
remainingSubsetAvailable: request.testStatusContext?.remainingSubsetAvailable
|
|
3258
|
+
})
|
|
3259
|
+
].filter((value) => Boolean(value)).join("\n\n")
|
|
3260
|
+
});
|
|
3261
|
+
const providerPrepared2 = {
|
|
3262
|
+
...prepared,
|
|
3263
|
+
truncated: rawSlice.text,
|
|
3264
|
+
meta: {
|
|
3265
|
+
...prepared.meta,
|
|
3266
|
+
finalLength: rawSlice.text.length,
|
|
3267
|
+
truncatedApplied: rawSlice.used || prepared.meta.truncatedApplied
|
|
3268
|
+
}
|
|
3269
|
+
};
|
|
852
3270
|
if (request.dryRun) {
|
|
853
3271
|
return buildDryRunOutput({
|
|
854
3272
|
request,
|
|
855
3273
|
providerName: provider.name,
|
|
856
|
-
prompt,
|
|
857
|
-
responseMode,
|
|
3274
|
+
prompt: prompt.prompt,
|
|
3275
|
+
responseMode: prompt.responseMode,
|
|
3276
|
+
prepared: providerPrepared2,
|
|
3277
|
+
heuristicOutput: testStatusHeuristicOutput,
|
|
3278
|
+
strategy: "hybrid"
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
try {
|
|
3282
|
+
const result = await generateWithRetry({
|
|
3283
|
+
provider,
|
|
3284
|
+
request,
|
|
3285
|
+
prompt: prompt.prompt,
|
|
3286
|
+
responseMode: prompt.responseMode
|
|
3287
|
+
});
|
|
3288
|
+
const supplement = parseTestStatusProviderSupplement(result.text);
|
|
3289
|
+
const mergedDecision = buildTestStatusDiagnoseContract({
|
|
3290
|
+
input: prepared.truncated,
|
|
3291
|
+
analysis: testStatusAnalysis,
|
|
3292
|
+
resolvedTests: request.testStatusContext?.resolvedTests,
|
|
3293
|
+
remainingTests: request.testStatusContext?.remainingTests,
|
|
3294
|
+
contractOverrides: {
|
|
3295
|
+
diagnosis_complete: supplement.diagnosis_complete,
|
|
3296
|
+
raw_needed: supplement.raw_needed,
|
|
3297
|
+
additional_source_read_likely_low_value: supplement.additional_source_read_likely_low_value,
|
|
3298
|
+
read_raw_only_if: supplement.read_raw_only_if,
|
|
3299
|
+
decision: supplement.decision,
|
|
3300
|
+
provider_used: true,
|
|
3301
|
+
provider_confidence: supplement.provider_confidence,
|
|
3302
|
+
provider_failed: false,
|
|
3303
|
+
raw_slice_used: rawSlice.used,
|
|
3304
|
+
raw_slice_strategy: rawSlice.strategy,
|
|
3305
|
+
next_best_action: supplement.next_best_action
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
const finalOutput = renderTestStatusDecisionOutput({
|
|
3309
|
+
request,
|
|
3310
|
+
decision: mergedDecision
|
|
3311
|
+
});
|
|
3312
|
+
logVerboseTestStatusTelemetry({
|
|
3313
|
+
request,
|
|
3314
|
+
prepared,
|
|
3315
|
+
contract: mergedDecision.contract,
|
|
3316
|
+
finalOutput,
|
|
3317
|
+
rawSliceChars: rawSlice.text.length,
|
|
3318
|
+
providerInputChars: providerPrepared2.truncated.length,
|
|
3319
|
+
providerOutputChars: result.text.length
|
|
3320
|
+
});
|
|
3321
|
+
return finalOutput;
|
|
3322
|
+
} catch (error) {
|
|
3323
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
3324
|
+
const failureDecision = buildTestStatusProviderFailureDecision({
|
|
3325
|
+
request,
|
|
3326
|
+
baseDecision: testStatusDecision,
|
|
3327
|
+
input: prepared.truncated,
|
|
3328
|
+
analysis: testStatusAnalysis,
|
|
3329
|
+
reason,
|
|
3330
|
+
rawSliceUsed: rawSlice.used,
|
|
3331
|
+
rawSliceStrategy: rawSlice.strategy
|
|
3332
|
+
});
|
|
3333
|
+
const finalOutput = request.goal === "diagnose" && request.format === "json" ? JSON.stringify(
|
|
3334
|
+
buildTestStatusPublicDiagnoseContract({
|
|
3335
|
+
contract: failureDecision.contract,
|
|
3336
|
+
includeTestIds: request.includeTestIds,
|
|
3337
|
+
remainingSubsetAvailable: request.testStatusContext?.remainingSubsetAvailable
|
|
3338
|
+
}),
|
|
3339
|
+
null,
|
|
3340
|
+
2
|
|
3341
|
+
) : renderTestStatusDecisionOutput({
|
|
3342
|
+
request,
|
|
3343
|
+
decision: failureDecision
|
|
3344
|
+
});
|
|
3345
|
+
logVerboseTestStatusTelemetry({
|
|
3346
|
+
request,
|
|
858
3347
|
prepared,
|
|
859
|
-
|
|
3348
|
+
contract: failureDecision.contract,
|
|
3349
|
+
finalOutput,
|
|
3350
|
+
rawSliceChars: rawSlice.text.length,
|
|
3351
|
+
providerInputChars: providerPrepared2.truncated.length
|
|
860
3352
|
});
|
|
3353
|
+
return finalOutput;
|
|
861
3354
|
}
|
|
862
|
-
return heuristicOutput;
|
|
863
3355
|
}
|
|
3356
|
+
const genericRawSlice = buildGenericRawSlice({
|
|
3357
|
+
input: prepared.redacted,
|
|
3358
|
+
config: request.config.input
|
|
3359
|
+
});
|
|
3360
|
+
const providerPrompt = buildPrompt({
|
|
3361
|
+
question: request.question,
|
|
3362
|
+
format: request.format,
|
|
3363
|
+
goal: request.goal,
|
|
3364
|
+
input: genericRawSlice.text,
|
|
3365
|
+
detail: request.detail,
|
|
3366
|
+
policyName: request.policyName,
|
|
3367
|
+
outputContract: request.outputContract,
|
|
3368
|
+
analysisContext: request.analysisContext
|
|
3369
|
+
});
|
|
3370
|
+
const providerPrepared = {
|
|
3371
|
+
...prepared,
|
|
3372
|
+
truncated: genericRawSlice.text,
|
|
3373
|
+
meta: {
|
|
3374
|
+
...prepared.meta,
|
|
3375
|
+
finalLength: genericRawSlice.text.length,
|
|
3376
|
+
truncatedApplied: genericRawSlice.used || prepared.meta.truncatedApplied
|
|
3377
|
+
}
|
|
3378
|
+
};
|
|
864
3379
|
if (request.dryRun) {
|
|
865
3380
|
return buildDryRunOutput({
|
|
866
3381
|
request,
|
|
867
3382
|
providerName: provider.name,
|
|
868
|
-
prompt,
|
|
869
|
-
responseMode,
|
|
870
|
-
prepared,
|
|
871
|
-
heuristicOutput: null
|
|
3383
|
+
prompt: providerPrompt.prompt,
|
|
3384
|
+
responseMode: providerPrompt.responseMode,
|
|
3385
|
+
prepared: providerPrepared,
|
|
3386
|
+
heuristicOutput: testStatusDecision ? testStatusHeuristicOutput : null,
|
|
3387
|
+
strategy: testStatusDecision ? "hybrid" : "provider"
|
|
872
3388
|
});
|
|
873
3389
|
}
|
|
874
3390
|
try {
|
|
875
3391
|
const result = await generateWithRetry({
|
|
876
3392
|
provider,
|
|
877
3393
|
request,
|
|
878
|
-
prompt,
|
|
879
|
-
responseMode
|
|
3394
|
+
prompt: providerPrompt.prompt,
|
|
3395
|
+
responseMode: providerPrompt.responseMode
|
|
880
3396
|
});
|
|
881
3397
|
if (looksLikeRejectedModelOutput({
|
|
882
|
-
source:
|
|
3398
|
+
source: genericRawSlice.text,
|
|
883
3399
|
candidate: result.text,
|
|
884
|
-
responseMode
|
|
3400
|
+
responseMode: providerPrompt.responseMode
|
|
885
3401
|
})) {
|
|
886
3402
|
throw new Error("Model output rejected by quality gate");
|
|
887
3403
|
}
|
|
888
|
-
return
|
|
3404
|
+
return withInsufficientHint({
|
|
3405
|
+
output: normalizeOutput(result.text, providerPrompt.responseMode),
|
|
3406
|
+
request,
|
|
3407
|
+
prepared: providerPrepared
|
|
3408
|
+
});
|
|
889
3409
|
} catch (error) {
|
|
890
3410
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
891
|
-
return
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
3411
|
+
return withInsufficientHint({
|
|
3412
|
+
output: buildFallbackOutput({
|
|
3413
|
+
format: request.format,
|
|
3414
|
+
reason,
|
|
3415
|
+
rawInput: providerPrepared.truncated,
|
|
3416
|
+
rawFallback: request.config.runtime.rawFallback,
|
|
3417
|
+
jsonFallback: request.fallbackJson
|
|
3418
|
+
}),
|
|
3419
|
+
request,
|
|
3420
|
+
prepared: providerPrepared
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
// src/core/testStatusState.ts
|
|
3426
|
+
import fs from "fs";
|
|
3427
|
+
import path2 from "path";
|
|
3428
|
+
import { z as z2 } from "zod";
|
|
3429
|
+
var detailSchema = z2.enum(["standard", "focused", "verbose"]);
|
|
3430
|
+
var failureBucketTypeSchema = z2.enum([
|
|
3431
|
+
"shared_environment_blocker",
|
|
3432
|
+
"fixture_guard_failure",
|
|
3433
|
+
"service_unavailable",
|
|
3434
|
+
"db_connection_failure",
|
|
3435
|
+
"auth_bypass_absent",
|
|
3436
|
+
"contract_snapshot_drift",
|
|
3437
|
+
"import_dependency_failure",
|
|
3438
|
+
"collection_failure",
|
|
3439
|
+
"assertion_failure",
|
|
3440
|
+
"runtime_failure",
|
|
3441
|
+
"interrupted_run",
|
|
3442
|
+
"no_tests_collected",
|
|
3443
|
+
"unknown_failure"
|
|
3444
|
+
]);
|
|
3445
|
+
var countSchema = z2.number().int().nonnegative();
|
|
3446
|
+
var cachedBucketSchema = z2.object({
|
|
3447
|
+
type: failureBucketTypeSchema,
|
|
3448
|
+
headline: z2.string(),
|
|
3449
|
+
countVisible: countSchema,
|
|
3450
|
+
countClaimed: countSchema.optional(),
|
|
3451
|
+
reason: z2.string(),
|
|
3452
|
+
entities: z2.array(z2.string())
|
|
3453
|
+
});
|
|
3454
|
+
var cachedAnalysisSchema = z2.object({
|
|
3455
|
+
passed: countSchema,
|
|
3456
|
+
failed: countSchema,
|
|
3457
|
+
errors: countSchema,
|
|
3458
|
+
skipped: countSchema,
|
|
3459
|
+
noTestsCollected: z2.boolean(),
|
|
3460
|
+
interrupted: z2.boolean(),
|
|
3461
|
+
collectionErrorCount: countSchema.optional(),
|
|
3462
|
+
buckets: z2.array(cachedBucketSchema)
|
|
3463
|
+
});
|
|
3464
|
+
var cachedCommandSchema = z2.discriminatedUnion("mode", [
|
|
3465
|
+
z2.object({
|
|
3466
|
+
mode: z2.literal("argv"),
|
|
3467
|
+
argv: z2.array(z2.string()).min(1)
|
|
3468
|
+
}),
|
|
3469
|
+
z2.object({
|
|
3470
|
+
mode: z2.literal("shell"),
|
|
3471
|
+
shellCommand: z2.string().min(1)
|
|
3472
|
+
})
|
|
3473
|
+
]).optional();
|
|
3474
|
+
var cachedPytestStateSchema = z2.object({
|
|
3475
|
+
subsetCapable: z2.boolean(),
|
|
3476
|
+
baseArgv: z2.array(z2.string()).min(1).optional(),
|
|
3477
|
+
failingNodeIds: z2.array(z2.string()),
|
|
3478
|
+
remainingNodeIds: z2.array(z2.string()).optional()
|
|
3479
|
+
}).optional();
|
|
3480
|
+
var cachedRunSchema = z2.object({
|
|
3481
|
+
version: z2.literal(1),
|
|
3482
|
+
timestamp: z2.string(),
|
|
3483
|
+
presetName: z2.literal("test-status"),
|
|
3484
|
+
cwd: z2.string(),
|
|
3485
|
+
commandKey: z2.string(),
|
|
3486
|
+
commandPreview: z2.string(),
|
|
3487
|
+
command: cachedCommandSchema,
|
|
3488
|
+
detail: detailSchema,
|
|
3489
|
+
exitCode: z2.number().int(),
|
|
3490
|
+
rawOutput: z2.string(),
|
|
3491
|
+
capture: z2.object({
|
|
3492
|
+
originalChars: countSchema,
|
|
3493
|
+
truncatedApplied: z2.boolean()
|
|
3494
|
+
}),
|
|
3495
|
+
analysis: cachedAnalysisSchema,
|
|
3496
|
+
pytest: cachedPytestStateSchema
|
|
3497
|
+
});
|
|
3498
|
+
var MissingCachedTestStatusRunError = class extends Error {
|
|
3499
|
+
constructor() {
|
|
3500
|
+
super(
|
|
3501
|
+
"No cached test-status run found. Start with `sift exec --preset test-status -- <test command>`."
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
};
|
|
3505
|
+
var InvalidCachedTestStatusRunError = class extends Error {
|
|
3506
|
+
constructor() {
|
|
3507
|
+
super(
|
|
3508
|
+
"Cached test-status state is invalid. Run `sift exec --preset test-status -- <test command>` again."
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
};
|
|
3512
|
+
function normalizeBucketReason(reason) {
|
|
3513
|
+
return reason.trim().replace(/\s+/g, " ");
|
|
3514
|
+
}
|
|
3515
|
+
function getBucketCount(bucket) {
|
|
3516
|
+
return bucket.countClaimed ?? bucket.countVisible;
|
|
3517
|
+
}
|
|
3518
|
+
function formatCount3(count, singular, plural = `${singular}s`) {
|
|
3519
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
3520
|
+
}
|
|
3521
|
+
function appendPreview(values) {
|
|
3522
|
+
if (values.length === 0) {
|
|
3523
|
+
return "";
|
|
3524
|
+
}
|
|
3525
|
+
const preview = values.slice(0, 2);
|
|
3526
|
+
const overflowCount = values.length - preview.length;
|
|
3527
|
+
const suffix = overflowCount > 0 ? `, and ${overflowCount} more` : "";
|
|
3528
|
+
return ` (${preview.join(", ")}${suffix})`;
|
|
3529
|
+
}
|
|
3530
|
+
function buildBucketSignature(bucket) {
|
|
3531
|
+
return JSON.stringify([
|
|
3532
|
+
bucket.type,
|
|
3533
|
+
[...bucket.entities].sort(),
|
|
3534
|
+
normalizeBucketReason(bucket.reason)
|
|
3535
|
+
]);
|
|
3536
|
+
}
|
|
3537
|
+
function basenameMatches(value, matcher) {
|
|
3538
|
+
return matcher.test(path2.basename(value));
|
|
3539
|
+
}
|
|
3540
|
+
function isPytestExecutable(value) {
|
|
3541
|
+
return basenameMatches(value, /^pytest(?:\.exe)?$/i);
|
|
3542
|
+
}
|
|
3543
|
+
function isPythonExecutable(value) {
|
|
3544
|
+
return basenameMatches(value, /^python(?:\d+(?:\.\d+)*)?(?:\.exe)?$/i);
|
|
3545
|
+
}
|
|
3546
|
+
var shortPytestOptionsWithValue = /* @__PURE__ */ new Set([
|
|
3547
|
+
"-c",
|
|
3548
|
+
"-k",
|
|
3549
|
+
"-m",
|
|
3550
|
+
"-n",
|
|
3551
|
+
"-o",
|
|
3552
|
+
"-p",
|
|
3553
|
+
"-W"
|
|
3554
|
+
]);
|
|
3555
|
+
var longPytestOptionsWithValue = /* @__PURE__ */ new Set([
|
|
3556
|
+
"--asyncio-mode",
|
|
3557
|
+
"--basetemp",
|
|
3558
|
+
"--capture",
|
|
3559
|
+
"--color",
|
|
3560
|
+
"--confcutdir",
|
|
3561
|
+
"--cov",
|
|
3562
|
+
"--cov-config",
|
|
3563
|
+
"--cov-report",
|
|
3564
|
+
"--deselect",
|
|
3565
|
+
"--durations",
|
|
3566
|
+
"--durations-min",
|
|
3567
|
+
"--ignore",
|
|
3568
|
+
"--ignore-glob",
|
|
3569
|
+
"--import-mode",
|
|
3570
|
+
"--junitxml",
|
|
3571
|
+
"--log-cli-level",
|
|
3572
|
+
"--log-date-format",
|
|
3573
|
+
"--log-file",
|
|
3574
|
+
"--log-file-level",
|
|
3575
|
+
"--log-format",
|
|
3576
|
+
"--log-level",
|
|
3577
|
+
"--maxfail",
|
|
3578
|
+
"--override-ini",
|
|
3579
|
+
"--pyargs",
|
|
3580
|
+
"--rootdir",
|
|
3581
|
+
"--tb"
|
|
3582
|
+
]);
|
|
3583
|
+
function isSubsetCapablePytestArgv(argv) {
|
|
3584
|
+
let offset = -1;
|
|
3585
|
+
if (argv.length > 0 && isPytestExecutable(argv[0])) {
|
|
3586
|
+
offset = 1;
|
|
3587
|
+
} else if (argv.length > 2 && isPythonExecutable(argv[0]) && argv[1] === "-m" && argv[2] === "pytest") {
|
|
3588
|
+
offset = 3;
|
|
3589
|
+
}
|
|
3590
|
+
if (offset === -1) {
|
|
3591
|
+
return false;
|
|
3592
|
+
}
|
|
3593
|
+
for (let index = offset; index < argv.length; index += 1) {
|
|
3594
|
+
const arg = argv[index];
|
|
3595
|
+
if (arg === "--") {
|
|
3596
|
+
return false;
|
|
3597
|
+
}
|
|
3598
|
+
if (!arg.startsWith("-")) {
|
|
3599
|
+
return false;
|
|
3600
|
+
}
|
|
3601
|
+
if (arg.startsWith("--")) {
|
|
3602
|
+
if (arg.includes("=")) {
|
|
3603
|
+
continue;
|
|
3604
|
+
}
|
|
3605
|
+
if (longPytestOptionsWithValue.has(arg)) {
|
|
3606
|
+
index += 1;
|
|
3607
|
+
if (index >= argv.length) {
|
|
3608
|
+
return false;
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
continue;
|
|
3612
|
+
}
|
|
3613
|
+
const shortOption = arg.slice(0, 2);
|
|
3614
|
+
if (shortPytestOptionsWithValue.has(shortOption)) {
|
|
3615
|
+
if (arg.length === 2) {
|
|
3616
|
+
index += 1;
|
|
3617
|
+
if (index >= argv.length) {
|
|
3618
|
+
return false;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
return true;
|
|
3624
|
+
}
|
|
3625
|
+
function buildCachedCommand(args) {
|
|
3626
|
+
if (Array.isArray(args.command) && args.command.length > 0) {
|
|
3627
|
+
return {
|
|
3628
|
+
mode: "argv",
|
|
3629
|
+
argv: [...args.command]
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
if (typeof args.shellCommand === "string" && args.shellCommand.length > 0) {
|
|
3633
|
+
return {
|
|
3634
|
+
mode: "shell",
|
|
3635
|
+
shellCommand: args.shellCommand
|
|
3636
|
+
};
|
|
3637
|
+
}
|
|
3638
|
+
return void 0;
|
|
3639
|
+
}
|
|
3640
|
+
function buildFailingNodeIds(analysis) {
|
|
3641
|
+
const values = [];
|
|
3642
|
+
for (const value of [...analysis.visibleErrorLabels, ...analysis.visibleFailedLabels]) {
|
|
3643
|
+
if (value.length > 0 && !values.includes(value)) {
|
|
3644
|
+
values.push(value);
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
return values;
|
|
3648
|
+
}
|
|
3649
|
+
function buildCachedPytestState(args) {
|
|
3650
|
+
const baseArgv = args.command?.mode === "argv" && isSubsetCapablePytestArgv(args.command.argv) ? [...args.command.argv] : void 0;
|
|
3651
|
+
return {
|
|
3652
|
+
subsetCapable: Boolean(baseArgv),
|
|
3653
|
+
baseArgv,
|
|
3654
|
+
failingNodeIds: buildFailingNodeIds(args.analysis),
|
|
3655
|
+
remainingNodeIds: args.remainingNodeIds
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
function buildTestStatusCommandKey(args) {
|
|
3659
|
+
return `${args.shellCommand ? "shell" : "argv"}:${args.commandPreview}`;
|
|
3660
|
+
}
|
|
3661
|
+
function snapshotTestStatusAnalysis(analysis) {
|
|
3662
|
+
return {
|
|
3663
|
+
passed: analysis.passed,
|
|
3664
|
+
failed: analysis.failed,
|
|
3665
|
+
errors: analysis.errors,
|
|
3666
|
+
skipped: analysis.skipped,
|
|
3667
|
+
noTestsCollected: analysis.noTestsCollected,
|
|
3668
|
+
interrupted: analysis.interrupted,
|
|
3669
|
+
collectionErrorCount: analysis.collectionErrorCount,
|
|
3670
|
+
buckets: analysis.buckets.map((bucket) => ({
|
|
3671
|
+
type: bucket.type,
|
|
3672
|
+
headline: bucket.headline,
|
|
3673
|
+
countVisible: bucket.countVisible,
|
|
3674
|
+
countClaimed: bucket.countClaimed,
|
|
3675
|
+
reason: bucket.reason,
|
|
3676
|
+
entities: [...bucket.entities]
|
|
3677
|
+
}))
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3680
|
+
function createCachedTestStatusRun(args) {
|
|
3681
|
+
const command = buildCachedCommand({
|
|
3682
|
+
command: args.command,
|
|
3683
|
+
shellCommand: args.shellCommand
|
|
3684
|
+
});
|
|
3685
|
+
return {
|
|
3686
|
+
version: 1,
|
|
3687
|
+
timestamp: args.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3688
|
+
presetName: "test-status",
|
|
3689
|
+
cwd: args.cwd,
|
|
3690
|
+
commandKey: args.commandKey,
|
|
3691
|
+
commandPreview: args.commandPreview,
|
|
3692
|
+
command,
|
|
3693
|
+
detail: args.detail,
|
|
3694
|
+
exitCode: args.exitCode,
|
|
3695
|
+
rawOutput: args.rawOutput,
|
|
3696
|
+
capture: {
|
|
3697
|
+
originalChars: args.originalChars,
|
|
3698
|
+
truncatedApplied: args.truncatedApplied
|
|
3699
|
+
},
|
|
3700
|
+
analysis: snapshotTestStatusAnalysis(args.analysis),
|
|
3701
|
+
pytest: buildCachedPytestState({
|
|
3702
|
+
command,
|
|
3703
|
+
analysis: args.analysis,
|
|
3704
|
+
remainingNodeIds: args.remainingNodeIds
|
|
3705
|
+
})
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
function readCachedTestStatusRun(statePath = getDefaultTestStatusStatePath()) {
|
|
3709
|
+
let raw = "";
|
|
3710
|
+
try {
|
|
3711
|
+
raw = fs.readFileSync(statePath, "utf8");
|
|
3712
|
+
} catch (error) {
|
|
3713
|
+
if (error.code === "ENOENT") {
|
|
3714
|
+
throw new MissingCachedTestStatusRunError();
|
|
3715
|
+
}
|
|
3716
|
+
throw new InvalidCachedTestStatusRunError();
|
|
3717
|
+
}
|
|
3718
|
+
try {
|
|
3719
|
+
return cachedRunSchema.parse(JSON.parse(raw));
|
|
3720
|
+
} catch {
|
|
3721
|
+
throw new InvalidCachedTestStatusRunError();
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath()) {
|
|
3725
|
+
try {
|
|
3726
|
+
return readCachedTestStatusRun(statePath);
|
|
3727
|
+
} catch {
|
|
3728
|
+
return null;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
|
|
3732
|
+
fs.mkdirSync(path2.dirname(statePath), {
|
|
3733
|
+
recursive: true
|
|
3734
|
+
});
|
|
3735
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
|
|
3736
|
+
`, "utf8");
|
|
3737
|
+
}
|
|
3738
|
+
function buildTargetDelta(args) {
|
|
3739
|
+
if (args.previous.presetName !== "test-status" || args.current.presetName !== "test-status" || args.previous.cwd !== args.current.cwd || args.previous.commandKey !== args.current.commandKey) {
|
|
3740
|
+
return {
|
|
3741
|
+
comparable: false,
|
|
3742
|
+
resolved: [],
|
|
3743
|
+
remaining: [],
|
|
3744
|
+
introduced: []
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3747
|
+
if (!args.previous.pytest || !args.current.pytest) {
|
|
3748
|
+
return {
|
|
3749
|
+
comparable: false,
|
|
3750
|
+
resolved: [],
|
|
3751
|
+
remaining: [],
|
|
3752
|
+
introduced: []
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
const previousTargets = args.previous.pytest.failingNodeIds;
|
|
3756
|
+
const currentTargets = args.current.pytest.failingNodeIds;
|
|
3757
|
+
const currentTargetSet = new Set(currentTargets);
|
|
3758
|
+
const previousTargetSet = new Set(previousTargets);
|
|
3759
|
+
return {
|
|
3760
|
+
comparable: true,
|
|
3761
|
+
resolved: previousTargets.filter((target) => !currentTargetSet.has(target)),
|
|
3762
|
+
remaining: currentTargets.filter((target) => previousTargetSet.has(target)),
|
|
3763
|
+
introduced: currentTargets.filter((target) => !previousTargetSet.has(target))
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
function diffTestStatusTargets(args) {
|
|
3767
|
+
return buildTargetDelta(args);
|
|
3768
|
+
}
|
|
3769
|
+
function diffTestStatusRuns(args) {
|
|
3770
|
+
const targetDelta = buildTargetDelta(args);
|
|
3771
|
+
const previousBuckets = new Map(
|
|
3772
|
+
args.previous.analysis.buckets.map((bucket) => [buildBucketSignature(bucket), bucket])
|
|
3773
|
+
);
|
|
3774
|
+
const currentBuckets = new Map(
|
|
3775
|
+
args.current.analysis.buckets.map((bucket) => [buildBucketSignature(bucket), bucket])
|
|
3776
|
+
);
|
|
3777
|
+
const lines = [];
|
|
3778
|
+
if (targetDelta.resolved.length > 0) {
|
|
3779
|
+
lines.push(
|
|
3780
|
+
`- Resolved: ${formatCount3(targetDelta.resolved.length, "failing test/module", "failing tests/modules")} no longer appear${appendPreview(targetDelta.resolved)}.`
|
|
3781
|
+
);
|
|
3782
|
+
}
|
|
3783
|
+
if (targetDelta.remaining.length > 0) {
|
|
3784
|
+
lines.push(
|
|
3785
|
+
`- Remaining: ${formatCount3(targetDelta.remaining.length, "failing test/module", "failing tests/modules")} still appear${appendPreview(targetDelta.remaining)}.`
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
if (targetDelta.introduced.length > 0) {
|
|
3789
|
+
lines.push(
|
|
3790
|
+
`- New: ${formatCount3(targetDelta.introduced.length, "failing test/module", "failing tests/modules")} appeared${appendPreview(targetDelta.introduced)}.`
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
for (const bucket of args.current.analysis.buckets) {
|
|
3794
|
+
const signature = buildBucketSignature(bucket);
|
|
3795
|
+
const previous = previousBuckets.get(signature);
|
|
3796
|
+
if (!previous) {
|
|
3797
|
+
continue;
|
|
3798
|
+
}
|
|
3799
|
+
const previousCount = getBucketCount(previous);
|
|
3800
|
+
const currentCount = getBucketCount(bucket);
|
|
3801
|
+
if (previousCount !== currentCount) {
|
|
3802
|
+
lines.push(`- Changed: ${bucket.headline} (${previousCount} -> ${currentCount}).`);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
if (lines.length === 0) {
|
|
3806
|
+
for (const bucket of args.previous.analysis.buckets) {
|
|
3807
|
+
const signature = buildBucketSignature(bucket);
|
|
3808
|
+
if (!currentBuckets.has(signature)) {
|
|
3809
|
+
lines.push(`- Resolved: ${bucket.headline} (${getBucketCount(bucket)}).`);
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
for (const bucket of args.current.analysis.buckets) {
|
|
3813
|
+
const signature = buildBucketSignature(bucket);
|
|
3814
|
+
if (!previousBuckets.has(signature)) {
|
|
3815
|
+
lines.push(`- New: ${bucket.headline} (${getBucketCount(bucket)}).`);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
return {
|
|
3820
|
+
lines: lines.slice(0, 4),
|
|
3821
|
+
remainingNodeIds: targetDelta.comparable ? targetDelta.remaining : void 0
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
// src/core/watch.ts
|
|
3826
|
+
var CLEAR_SCREEN_PATTERN = /\u001bc|\u001b\[2J(?:\u001b\[H)?/g;
|
|
3827
|
+
var SUMMARY_BOUNDARY_PATTERN = /^={5,}.*(?:passed|failed|errors?|no tests ran|interrupted).*={5,}\s*$/i;
|
|
3828
|
+
function normalizeWatchInput(input) {
|
|
3829
|
+
return input.replace(/\r\n/g, "\n");
|
|
3830
|
+
}
|
|
3831
|
+
function hasVisibleContent(input) {
|
|
3832
|
+
return input.split("\n").some((line) => line.trim().length > 0);
|
|
3833
|
+
}
|
|
3834
|
+
function splitBySummaryBoundaries(input) {
|
|
3835
|
+
const cycles = [];
|
|
3836
|
+
let current = [];
|
|
3837
|
+
for (const line of input.split("\n")) {
|
|
3838
|
+
current.push(line);
|
|
3839
|
+
if (SUMMARY_BOUNDARY_PATTERN.test(line.trim())) {
|
|
3840
|
+
const candidate = current.join("\n").trim();
|
|
3841
|
+
if (candidate.length > 0) {
|
|
3842
|
+
cycles.push(candidate);
|
|
3843
|
+
}
|
|
3844
|
+
current = [];
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
const trailing = current.join("\n").trim();
|
|
3848
|
+
if (trailing.length > 0) {
|
|
3849
|
+
cycles.push(trailing);
|
|
3850
|
+
}
|
|
3851
|
+
return cycles;
|
|
3852
|
+
}
|
|
3853
|
+
function splitWatchCycles(input) {
|
|
3854
|
+
const normalized = normalizeWatchInput(input);
|
|
3855
|
+
const clearScreenChunks = normalized.split(CLEAR_SCREEN_PATTERN).map((chunk) => chunk.trim()).filter((chunk) => chunk.length > 0);
|
|
3856
|
+
if (clearScreenChunks.length > 1) {
|
|
3857
|
+
return clearScreenChunks;
|
|
3858
|
+
}
|
|
3859
|
+
const summaryChunks = splitBySummaryBoundaries(normalized);
|
|
3860
|
+
if (summaryChunks.length > 1) {
|
|
3861
|
+
return summaryChunks;
|
|
3862
|
+
}
|
|
3863
|
+
return hasVisibleContent(normalized) ? [normalized.trim()] : [];
|
|
3864
|
+
}
|
|
3865
|
+
function looksLikeWatchStream(input) {
|
|
3866
|
+
const normalized = normalizeWatchInput(input);
|
|
3867
|
+
if (/\u001bc|\u001b\[2J(?:\u001b\[H)?/.test(normalized)) {
|
|
3868
|
+
return splitWatchCycles(input).length > 1;
|
|
3869
|
+
}
|
|
3870
|
+
return /(watch(?:ing)?|waiting for file changes|rerunning|re-running)/i.test(normalized) && splitWatchCycles(input).length > 1;
|
|
3871
|
+
}
|
|
3872
|
+
function indentBlock(text) {
|
|
3873
|
+
return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
|
|
3874
|
+
}
|
|
3875
|
+
async function runGenericWatch(request, cycles) {
|
|
3876
|
+
const rendered = [];
|
|
3877
|
+
let previousSummary = null;
|
|
3878
|
+
for (const [index, cycle] of cycles.entries()) {
|
|
3879
|
+
const currentSummary = await runSift({
|
|
3880
|
+
...request,
|
|
3881
|
+
stdin: cycle
|
|
3882
|
+
});
|
|
3883
|
+
if (index === 0) {
|
|
3884
|
+
rendered.push(`- Cycle 1
|
|
3885
|
+
${indentBlock(currentSummary)}`);
|
|
3886
|
+
previousSummary = currentSummary;
|
|
3887
|
+
continue;
|
|
3888
|
+
}
|
|
3889
|
+
const changeSummary = await runSift({
|
|
3890
|
+
...request,
|
|
3891
|
+
goal: "summarize",
|
|
3892
|
+
format: "bullets",
|
|
3893
|
+
policyName: void 0,
|
|
3894
|
+
detail: void 0,
|
|
3895
|
+
outputContract: void 0,
|
|
3896
|
+
analysisContext: void 0,
|
|
3897
|
+
fallbackJson: void 0,
|
|
3898
|
+
question: "What changed since the previous cycle? Mention what resolved, what stayed, and the next best action.",
|
|
3899
|
+
stdin: [
|
|
3900
|
+
"Previous cycle summary:",
|
|
3901
|
+
previousSummary ?? "",
|
|
3902
|
+
"",
|
|
3903
|
+
"Current cycle summary:",
|
|
3904
|
+
currentSummary
|
|
3905
|
+
].join("\n")
|
|
3906
|
+
});
|
|
3907
|
+
rendered.push(
|
|
3908
|
+
[`- Cycle ${index + 1}`, indentBlock(changeSummary), indentBlock(currentSummary)].join("\n")
|
|
3909
|
+
);
|
|
3910
|
+
previousSummary = currentSummary;
|
|
3911
|
+
}
|
|
3912
|
+
return rendered.join("\n\n");
|
|
3913
|
+
}
|
|
3914
|
+
async function runTestStatusWatch(request, cycles) {
|
|
3915
|
+
const rendered = [];
|
|
3916
|
+
const cyclePayloads = [];
|
|
3917
|
+
let previousRun = null;
|
|
3918
|
+
for (const [index, cycle] of cycles.entries()) {
|
|
3919
|
+
const analysis = analyzeTestStatus(cycle);
|
|
3920
|
+
let currentRun = createCachedTestStatusRun({
|
|
3921
|
+
cwd: process.cwd(),
|
|
3922
|
+
commandKey: `watch:${request.question}`,
|
|
3923
|
+
commandPreview: `watch:${request.question}`,
|
|
3924
|
+
detail: request.detail ?? "standard",
|
|
3925
|
+
exitCode: analysis.failed > 0 || analysis.errors > 0 || analysis.collectionErrorCount ? 1 : 0,
|
|
3926
|
+
rawOutput: cycle,
|
|
3927
|
+
originalChars: cycle.length,
|
|
3928
|
+
truncatedApplied: false,
|
|
3929
|
+
analysis
|
|
3930
|
+
});
|
|
3931
|
+
const targetDelta = previousRun === null ? null : diffTestStatusTargets({
|
|
3932
|
+
previous: previousRun,
|
|
3933
|
+
current: currentRun
|
|
3934
|
+
});
|
|
3935
|
+
const diffLines = previousRun === null ? [] : diffTestStatusRuns({
|
|
3936
|
+
previous: previousRun,
|
|
3937
|
+
current: currentRun
|
|
3938
|
+
}).lines;
|
|
3939
|
+
const output = await runSift({
|
|
3940
|
+
...request,
|
|
3941
|
+
stdin: cycle,
|
|
3942
|
+
analysisContext: [
|
|
3943
|
+
request.analysisContext,
|
|
3944
|
+
"Watch context:",
|
|
3945
|
+
"- Treat this as a redraw/change cycle, not a fresh full-suite baseline.",
|
|
3946
|
+
...previousRun === null ? [] : [
|
|
3947
|
+
"- Prefer what changed, what resolved, and what still remains.",
|
|
3948
|
+
"- Keep the current blocker and remaining failures in focus."
|
|
3949
|
+
]
|
|
3950
|
+
].join("\n"),
|
|
3951
|
+
testStatusContext: {
|
|
3952
|
+
...request.testStatusContext,
|
|
3953
|
+
resolvedTests: targetDelta?.resolved ?? request.testStatusContext?.resolvedTests,
|
|
3954
|
+
remainingTests: targetDelta?.remaining ?? currentRun.pytest?.failingNodeIds ?? request.testStatusContext?.remainingTests,
|
|
3955
|
+
remainingSubsetAvailable: request.testStatusContext?.remainingSubsetAvailable ?? (Boolean(currentRun.pytest?.subsetCapable) && (currentRun.pytest?.failingNodeIds.length ?? 0) > 0)
|
|
3956
|
+
}
|
|
897
3957
|
});
|
|
3958
|
+
if (request.goal === "diagnose" && request.format === "json") {
|
|
3959
|
+
cyclePayloads.push({
|
|
3960
|
+
cycle: index + 1,
|
|
3961
|
+
diagnosis: JSON.parse(output),
|
|
3962
|
+
changes: diffLines
|
|
3963
|
+
});
|
|
3964
|
+
} else {
|
|
3965
|
+
const block = [`- Cycle ${index + 1}`];
|
|
3966
|
+
if (diffLines.length > 0) {
|
|
3967
|
+
block.push(...diffLines.map((line) => ` ${line}`));
|
|
3968
|
+
}
|
|
3969
|
+
block.push(indentBlock(output));
|
|
3970
|
+
rendered.push(block.join("\n"));
|
|
3971
|
+
}
|
|
3972
|
+
previousRun = currentRun;
|
|
3973
|
+
}
|
|
3974
|
+
if (request.goal === "diagnose" && request.format === "json") {
|
|
3975
|
+
const lastDiagnosis = cyclePayloads.at(-1)?.diagnosis;
|
|
3976
|
+
return JSON.stringify(
|
|
3977
|
+
{
|
|
3978
|
+
status: cyclePayloads.some(
|
|
3979
|
+
(payload) => typeof payload.diagnosis === "object" && payload.diagnosis !== null && "status" in payload.diagnosis && payload.diagnosis.status === "insufficient"
|
|
3980
|
+
) ? "insufficient" : "ok",
|
|
3981
|
+
cycles: cyclePayloads,
|
|
3982
|
+
next_best_action: lastDiagnosis?.next_best_action ?? null
|
|
3983
|
+
},
|
|
3984
|
+
null,
|
|
3985
|
+
2
|
|
3986
|
+
);
|
|
3987
|
+
}
|
|
3988
|
+
return rendered.join("\n\n");
|
|
3989
|
+
}
|
|
3990
|
+
async function runWatch(request) {
|
|
3991
|
+
const cycles = splitWatchCycles(request.stdin);
|
|
3992
|
+
if (cycles.length <= 1) {
|
|
3993
|
+
return runSift(request);
|
|
898
3994
|
}
|
|
3995
|
+
if (request.goal === "diagnose" && request.format === "json" && request.policyName !== "test-status") {
|
|
3996
|
+
throw new Error(
|
|
3997
|
+
"`--goal diagnose --format json` is currently supported only for `test-status` watch flows."
|
|
3998
|
+
);
|
|
3999
|
+
}
|
|
4000
|
+
if (request.policyName === "test-status") {
|
|
4001
|
+
return runTestStatusWatch(request, cycles);
|
|
4002
|
+
}
|
|
4003
|
+
return runGenericWatch(request, cycles);
|
|
899
4004
|
}
|
|
900
4005
|
|
|
901
4006
|
// src/core/exec.ts
|
|
@@ -992,9 +4097,13 @@ async function runExec(request) {
|
|
|
992
4097
|
throw new Error("Provide either --shell <command> or -- <program> [args...].");
|
|
993
4098
|
}
|
|
994
4099
|
const shellPath = process.env.SHELL || "/bin/bash";
|
|
4100
|
+
const commandPreview = buildCommandPreview(request);
|
|
4101
|
+
const commandCwd = request.cwd ?? process.cwd();
|
|
4102
|
+
const shouldCacheTestStatusBase = request.presetName === "test-status" && !request.skipCacheWrite;
|
|
4103
|
+
const previousCachedRun = shouldCacheTestStatusBase ? tryReadCachedTestStatusRun() : null;
|
|
995
4104
|
if (request.config.runtime.verbose) {
|
|
996
4105
|
process.stderr.write(
|
|
997
|
-
`${pc2.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${
|
|
4106
|
+
`${pc2.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
|
|
998
4107
|
`
|
|
999
4108
|
);
|
|
1000
4109
|
}
|
|
@@ -1003,10 +4112,11 @@ async function runExec(request) {
|
|
|
1003
4112
|
let bypassed = false;
|
|
1004
4113
|
let childStatus = null;
|
|
1005
4114
|
let childSignal = null;
|
|
1006
|
-
let childSpawnError = null;
|
|
1007
4115
|
const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
|
|
4116
|
+
cwd: commandCwd,
|
|
1008
4117
|
stdio: ["inherit", "pipe", "pipe"]
|
|
1009
4118
|
}) : spawn(request.command[0], request.command.slice(1), {
|
|
4119
|
+
cwd: commandCwd,
|
|
1010
4120
|
stdio: ["inherit", "pipe", "pipe"]
|
|
1011
4121
|
});
|
|
1012
4122
|
const handleChunk = (chunk) => {
|
|
@@ -1031,7 +4141,6 @@ async function runExec(request) {
|
|
|
1031
4141
|
child.stderr.on("data", handleChunk);
|
|
1032
4142
|
await new Promise((resolve, reject) => {
|
|
1033
4143
|
child.on("error", (error) => {
|
|
1034
|
-
childSpawnError = error;
|
|
1035
4144
|
reject(error);
|
|
1036
4145
|
});
|
|
1037
4146
|
child.on("close", (status, signal) => {
|
|
@@ -1045,19 +4154,29 @@ async function runExec(request) {
|
|
|
1045
4154
|
}
|
|
1046
4155
|
throw new Error("Failed to start child process.");
|
|
1047
4156
|
});
|
|
1048
|
-
if (childSpawnError) {
|
|
1049
|
-
throw childSpawnError;
|
|
1050
|
-
}
|
|
1051
4157
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
1052
4158
|
const capturedOutput = capture.render();
|
|
4159
|
+
const autoWatchDetected = !request.watch && looksLikeWatchStream(capturedOutput);
|
|
4160
|
+
const useWatchFlow = Boolean(request.watch) || autoWatchDetected;
|
|
4161
|
+
const shouldCacheTestStatus = shouldCacheTestStatusBase && !useWatchFlow;
|
|
1053
4162
|
if (request.config.runtime.verbose) {
|
|
1054
4163
|
process.stderr.write(
|
|
1055
4164
|
`${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
1056
4165
|
`
|
|
1057
4166
|
);
|
|
1058
4167
|
}
|
|
4168
|
+
if (autoWatchDetected) {
|
|
4169
|
+
process.stderr.write(`${pc2.dim("sift")} auto-watch=detected
|
|
4170
|
+
`);
|
|
4171
|
+
}
|
|
1059
4172
|
if (!bypassed) {
|
|
1060
|
-
|
|
4173
|
+
if (request.showRaw && capturedOutput.length > 0) {
|
|
4174
|
+
process.stderr.write(capturedOutput);
|
|
4175
|
+
if (!capturedOutput.endsWith("\n")) {
|
|
4176
|
+
process.stderr.write("\n");
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
const execSuccessShortcut = useWatchFlow ? null : getExecSuccessShortcut({
|
|
1061
4180
|
presetName: request.presetName,
|
|
1062
4181
|
exitCode,
|
|
1063
4182
|
capturedOutput
|
|
@@ -1073,10 +4192,115 @@ async function runExec(request) {
|
|
|
1073
4192
|
`);
|
|
1074
4193
|
return exitCode;
|
|
1075
4194
|
}
|
|
1076
|
-
|
|
4195
|
+
if (useWatchFlow) {
|
|
4196
|
+
let output2 = await runWatch({
|
|
4197
|
+
...request,
|
|
4198
|
+
stdin: capturedOutput
|
|
4199
|
+
});
|
|
4200
|
+
if (isInsufficientSignalOutput(output2)) {
|
|
4201
|
+
output2 = buildInsufficientSignalOutput({
|
|
4202
|
+
presetName: request.presetName,
|
|
4203
|
+
originalLength: capture.getTotalChars(),
|
|
4204
|
+
truncatedApplied: capture.wasTruncated(),
|
|
4205
|
+
exitCode
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4208
|
+
process.stdout.write(`${output2}
|
|
4209
|
+
`);
|
|
4210
|
+
return exitCode;
|
|
4211
|
+
}
|
|
4212
|
+
const analysis = shouldCacheTestStatus ? analyzeTestStatus(capturedOutput) : null;
|
|
4213
|
+
let currentCachedRun = shouldCacheTestStatus && analysis ? createCachedTestStatusRun({
|
|
4214
|
+
cwd: commandCwd,
|
|
4215
|
+
commandKey: buildTestStatusCommandKey({
|
|
4216
|
+
commandPreview,
|
|
4217
|
+
shellCommand: request.shellCommand
|
|
4218
|
+
}),
|
|
4219
|
+
commandPreview,
|
|
4220
|
+
command: request.command,
|
|
4221
|
+
shellCommand: request.shellCommand,
|
|
4222
|
+
detail: request.detail ?? "standard",
|
|
4223
|
+
exitCode,
|
|
4224
|
+
rawOutput: capturedOutput,
|
|
4225
|
+
originalChars: capture.getTotalChars(),
|
|
4226
|
+
truncatedApplied: capture.wasTruncated(),
|
|
4227
|
+
analysis
|
|
4228
|
+
}) : null;
|
|
4229
|
+
const targetDelta = request.diff && !request.dryRun && previousCachedRun && currentCachedRun ? diffTestStatusTargets({
|
|
4230
|
+
previous: previousCachedRun,
|
|
4231
|
+
current: currentCachedRun
|
|
4232
|
+
}) : null;
|
|
4233
|
+
let output = await runSift({
|
|
1077
4234
|
...request,
|
|
1078
|
-
stdin: capturedOutput
|
|
4235
|
+
stdin: capturedOutput,
|
|
4236
|
+
analysisContext: request.skipCacheWrite && request.presetName === "test-status" ? [
|
|
4237
|
+
request.analysisContext,
|
|
4238
|
+
"Zoom context:",
|
|
4239
|
+
"- This pass is remaining-only.",
|
|
4240
|
+
"- The full-suite truth already exists from the cached full run.",
|
|
4241
|
+
"- Do not reintroduce resolved tests into the diagnosis."
|
|
4242
|
+
].filter((value) => Boolean(value)).join("\n") : request.analysisContext,
|
|
4243
|
+
testStatusContext: shouldCacheTestStatus && analysis ? {
|
|
4244
|
+
...request.testStatusContext,
|
|
4245
|
+
resolvedTests: targetDelta?.resolved ?? request.testStatusContext?.resolvedTests,
|
|
4246
|
+
remainingTests: targetDelta?.remaining ?? currentCachedRun?.pytest?.failingNodeIds ?? request.testStatusContext?.remainingTests,
|
|
4247
|
+
remainingSubsetAvailable: request.testStatusContext?.remainingSubsetAvailable ?? Boolean(
|
|
4248
|
+
currentCachedRun?.pytest?.subsetCapable && (targetDelta?.remaining ?? currentCachedRun?.pytest?.failingNodeIds ?? []).length > 0
|
|
4249
|
+
)
|
|
4250
|
+
} : request.testStatusContext
|
|
1079
4251
|
});
|
|
4252
|
+
if (shouldCacheTestStatus) {
|
|
4253
|
+
if (isInsufficientSignalOutput(output)) {
|
|
4254
|
+
output = buildInsufficientSignalOutput({
|
|
4255
|
+
presetName: request.presetName,
|
|
4256
|
+
originalLength: capture.getTotalChars(),
|
|
4257
|
+
truncatedApplied: capture.wasTruncated(),
|
|
4258
|
+
exitCode
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
|
|
4262
|
+
const delta = diffTestStatusRuns({
|
|
4263
|
+
previous: previousCachedRun,
|
|
4264
|
+
current: currentCachedRun
|
|
4265
|
+
});
|
|
4266
|
+
currentCachedRun = createCachedTestStatusRun({
|
|
4267
|
+
cwd: commandCwd,
|
|
4268
|
+
commandKey: currentCachedRun.commandKey,
|
|
4269
|
+
commandPreview,
|
|
4270
|
+
command: request.command,
|
|
4271
|
+
shellCommand: request.shellCommand,
|
|
4272
|
+
detail: request.detail ?? "standard",
|
|
4273
|
+
exitCode,
|
|
4274
|
+
rawOutput: capturedOutput,
|
|
4275
|
+
originalChars: capture.getTotalChars(),
|
|
4276
|
+
truncatedApplied: capture.wasTruncated(),
|
|
4277
|
+
analysis,
|
|
4278
|
+
remainingNodeIds: delta.remainingNodeIds
|
|
4279
|
+
});
|
|
4280
|
+
if (delta.lines.length > 0) {
|
|
4281
|
+
output = `${delta.lines.join("\n")}
|
|
4282
|
+
${output}`;
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
if (currentCachedRun) {
|
|
4286
|
+
try {
|
|
4287
|
+
writeCachedTestStatusRun(currentCachedRun);
|
|
4288
|
+
} catch (error) {
|
|
4289
|
+
if (request.config.runtime.verbose) {
|
|
4290
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
4291
|
+
process.stderr.write(`${pc2.dim("sift")} cache_write=failed reason=${reason}
|
|
4292
|
+
`);
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
} else if (isInsufficientSignalOutput(output)) {
|
|
4297
|
+
output = buildInsufficientSignalOutput({
|
|
4298
|
+
presetName: request.presetName,
|
|
4299
|
+
originalLength: capture.getTotalChars(),
|
|
4300
|
+
truncatedApplied: capture.wasTruncated(),
|
|
4301
|
+
exitCode
|
|
4302
|
+
});
|
|
4303
|
+
}
|
|
1080
4304
|
process.stdout.write(`${output}
|
|
1081
4305
|
`);
|
|
1082
4306
|
if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
|
|
@@ -1161,19 +4385,19 @@ var defaultConfig = {
|
|
|
1161
4385
|
};
|
|
1162
4386
|
|
|
1163
4387
|
// src/config/load.ts
|
|
1164
|
-
import
|
|
1165
|
-
import
|
|
4388
|
+
import fs2 from "fs";
|
|
4389
|
+
import path3 from "path";
|
|
1166
4390
|
import YAML from "yaml";
|
|
1167
4391
|
function findConfigPath(explicitPath) {
|
|
1168
4392
|
if (explicitPath) {
|
|
1169
|
-
const resolved =
|
|
1170
|
-
if (!
|
|
4393
|
+
const resolved = path3.resolve(explicitPath);
|
|
4394
|
+
if (!fs2.existsSync(resolved)) {
|
|
1171
4395
|
throw new Error(`Config file not found: ${resolved}`);
|
|
1172
4396
|
}
|
|
1173
4397
|
return resolved;
|
|
1174
4398
|
}
|
|
1175
4399
|
for (const candidate of getDefaultConfigSearchPaths()) {
|
|
1176
|
-
if (
|
|
4400
|
+
if (fs2.existsSync(candidate)) {
|
|
1177
4401
|
return candidate;
|
|
1178
4402
|
}
|
|
1179
4403
|
}
|
|
@@ -1184,7 +4408,7 @@ function loadRawConfig(explicitPath) {
|
|
|
1184
4408
|
if (!configPath) {
|
|
1185
4409
|
return {};
|
|
1186
4410
|
}
|
|
1187
|
-
const content =
|
|
4411
|
+
const content = fs2.readFileSync(configPath, "utf8");
|
|
1188
4412
|
return YAML.parse(content) ?? {};
|
|
1189
4413
|
}
|
|
1190
4414
|
|
|
@@ -1238,17 +4462,17 @@ function resolveProviderApiKey(provider, baseUrl, env) {
|
|
|
1238
4462
|
}
|
|
1239
4463
|
|
|
1240
4464
|
// src/config/schema.ts
|
|
1241
|
-
import { z } from "zod";
|
|
1242
|
-
var providerNameSchema =
|
|
1243
|
-
var outputFormatSchema =
|
|
4465
|
+
import { z as z3 } from "zod";
|
|
4466
|
+
var providerNameSchema = z3.enum(["openai", "openai-compatible"]);
|
|
4467
|
+
var outputFormatSchema = z3.enum([
|
|
1244
4468
|
"brief",
|
|
1245
4469
|
"bullets",
|
|
1246
4470
|
"json",
|
|
1247
4471
|
"verdict"
|
|
1248
4472
|
]);
|
|
1249
|
-
var responseModeSchema =
|
|
1250
|
-
var jsonResponseFormatModeSchema =
|
|
1251
|
-
var promptPolicyNameSchema =
|
|
4473
|
+
var responseModeSchema = z3.enum(["text", "json"]);
|
|
4474
|
+
var jsonResponseFormatModeSchema = z3.enum(["auto", "on", "off"]);
|
|
4475
|
+
var promptPolicyNameSchema = z3.enum([
|
|
1252
4476
|
"test-status",
|
|
1253
4477
|
"audit-critical",
|
|
1254
4478
|
"diff-summary",
|
|
@@ -1258,41 +4482,41 @@ var promptPolicyNameSchema = z.enum([
|
|
|
1258
4482
|
"typecheck-summary",
|
|
1259
4483
|
"lint-failures"
|
|
1260
4484
|
]);
|
|
1261
|
-
var providerConfigSchema =
|
|
4485
|
+
var providerConfigSchema = z3.object({
|
|
1262
4486
|
provider: providerNameSchema,
|
|
1263
|
-
model:
|
|
1264
|
-
baseUrl:
|
|
1265
|
-
apiKey:
|
|
4487
|
+
model: z3.string().min(1),
|
|
4488
|
+
baseUrl: z3.string().url(),
|
|
4489
|
+
apiKey: z3.string().optional(),
|
|
1266
4490
|
jsonResponseFormat: jsonResponseFormatModeSchema,
|
|
1267
|
-
timeoutMs:
|
|
1268
|
-
temperature:
|
|
1269
|
-
maxOutputTokens:
|
|
4491
|
+
timeoutMs: z3.number().int().positive(),
|
|
4492
|
+
temperature: z3.number().min(0).max(2),
|
|
4493
|
+
maxOutputTokens: z3.number().int().positive()
|
|
1270
4494
|
});
|
|
1271
|
-
var inputConfigSchema =
|
|
1272
|
-
stripAnsi:
|
|
1273
|
-
redact:
|
|
1274
|
-
redactStrict:
|
|
1275
|
-
maxCaptureChars:
|
|
1276
|
-
maxInputChars:
|
|
1277
|
-
headChars:
|
|
1278
|
-
tailChars:
|
|
4495
|
+
var inputConfigSchema = z3.object({
|
|
4496
|
+
stripAnsi: z3.boolean(),
|
|
4497
|
+
redact: z3.boolean(),
|
|
4498
|
+
redactStrict: z3.boolean(),
|
|
4499
|
+
maxCaptureChars: z3.number().int().positive(),
|
|
4500
|
+
maxInputChars: z3.number().int().positive(),
|
|
4501
|
+
headChars: z3.number().int().positive(),
|
|
4502
|
+
tailChars: z3.number().int().positive()
|
|
1279
4503
|
});
|
|
1280
|
-
var runtimeConfigSchema =
|
|
1281
|
-
rawFallback:
|
|
1282
|
-
verbose:
|
|
4504
|
+
var runtimeConfigSchema = z3.object({
|
|
4505
|
+
rawFallback: z3.boolean(),
|
|
4506
|
+
verbose: z3.boolean()
|
|
1283
4507
|
});
|
|
1284
|
-
var presetDefinitionSchema =
|
|
1285
|
-
question:
|
|
4508
|
+
var presetDefinitionSchema = z3.object({
|
|
4509
|
+
question: z3.string().min(1),
|
|
1286
4510
|
format: outputFormatSchema,
|
|
1287
4511
|
policy: promptPolicyNameSchema.optional(),
|
|
1288
|
-
outputContract:
|
|
1289
|
-
fallbackJson:
|
|
4512
|
+
outputContract: z3.string().optional(),
|
|
4513
|
+
fallbackJson: z3.unknown().optional()
|
|
1290
4514
|
});
|
|
1291
|
-
var siftConfigSchema =
|
|
4515
|
+
var siftConfigSchema = z3.object({
|
|
1292
4516
|
provider: providerConfigSchema,
|
|
1293
4517
|
input: inputConfigSchema,
|
|
1294
4518
|
runtime: runtimeConfigSchema,
|
|
1295
|
-
presets:
|
|
4519
|
+
presets: z3.record(presetDefinitionSchema)
|
|
1296
4520
|
});
|
|
1297
4521
|
|
|
1298
4522
|
// src/config/resolve.ts
|
|
@@ -1386,6 +4610,12 @@ function resolveConfig(options = {}) {
|
|
|
1386
4610
|
return siftConfigSchema.parse(merged);
|
|
1387
4611
|
}
|
|
1388
4612
|
export {
|
|
4613
|
+
BoundedCapture,
|
|
4614
|
+
buildCommandPreview,
|
|
4615
|
+
getExecSuccessShortcut,
|
|
4616
|
+
looksInteractivePrompt,
|
|
4617
|
+
mergeDefined,
|
|
4618
|
+
normalizeChildExitCode,
|
|
1389
4619
|
resolveConfig,
|
|
1390
4620
|
runExec,
|
|
1391
4621
|
runSift
|