@lsctech/polaris 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autoresearch/index.js +30 -1
- package/dist/autoresearch/proposal.js +5 -0
- package/dist/autoresearch/score.js +174 -2
- package/dist/autoresearch/sol-evaluation-writer.js +135 -0
- package/dist/autoresearch/sol-evidence-loader.js +40 -3
- package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
- package/dist/autoresearch/sol-recommendations.js +212 -0
- package/dist/autoresearch/sol-report-renderer.js +282 -0
- package/dist/autoresearch/sol-report.js +44 -0
- package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
- package/dist/autoresearch/sol-scorer.js +45 -1
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/config/defaults.js +1 -0
- package/dist/config/validator.js +157 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +198 -4
- package/dist/finalize/linear.js +8 -4
- package/dist/finalize/steps/08-create-pr.js +2 -2
- package/dist/finalize/steps/11-update-linear.js +2 -2
- package/dist/loop/parent.js +189 -33
- package/dist/loop/worker-packet.js +75 -1
- package/dist/qc/artifacts.js +27 -0
- package/dist/qc/fixtures/repair-packets.js +170 -0
- package/dist/qc/index.js +2 -0
- package/dist/qc/orchestration.js +5 -2
- package/dist/qc/policy.js +30 -3
- package/dist/qc/providers/coderabbit.js +152 -17
- package/dist/qc/registry.js +36 -3
- package/dist/qc/repair-loop.js +458 -0
- package/dist/qc/repair-packets.js +420 -0
- package/dist/qc/runner.js +328 -37
- package/dist/qc/schemas.js +79 -1
- package/dist/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- package/package.json +1 -1
package/dist/qc/runner.js
CHANGED
|
@@ -2,15 +2,29 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* QC provider execution runner.
|
|
4
4
|
*
|
|
5
|
-
* Runs a provider-specific review command,
|
|
6
|
-
* the
|
|
5
|
+
* Runs a provider-specific review command, classifies non-finding failures, and
|
|
6
|
+
* normalizes the outcome into a Polaris QcResult with a durable providerAttempt
|
|
7
|
+
* record. Supports optional fallback chains driven by per-provider failure policy.
|
|
7
8
|
*/
|
|
8
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
10
|
exports.executeQcProvider = executeQcProvider;
|
|
10
11
|
const node_child_process_1 = require("node:child_process");
|
|
11
12
|
const node_fs_1 = require("node:fs");
|
|
12
13
|
const node_path_1 = require("node:path");
|
|
13
|
-
|
|
14
|
+
const policy_js_1 = require("./policy.js");
|
|
15
|
+
function makeProviderAttempt(providerName, status, output, overrides) {
|
|
16
|
+
return {
|
|
17
|
+
provider: providerName,
|
|
18
|
+
status,
|
|
19
|
+
rawOutputAvailable: (output.stdout?.length ?? 0) > 0 || (output.stderr?.length ?? 0) > 0,
|
|
20
|
+
rawOutputRetained: false,
|
|
21
|
+
stdoutLength: output.stdout?.length ?? 0,
|
|
22
|
+
stderrLength: output.stderr?.length ?? 0,
|
|
23
|
+
exitCode: output.exitCode,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function makeSyntheticResult(provider, scope, startedAt, status, summary, providerAttempt) {
|
|
14
28
|
const completedAt = new Date().toISOString();
|
|
15
29
|
return {
|
|
16
30
|
schemaVersion: "1.0",
|
|
@@ -28,11 +42,12 @@ function makeSyntheticResult(provider, scope, startedAt, status, summary) {
|
|
|
28
42
|
rawArtifactPaths: [],
|
|
29
43
|
parserVersion: `${provider.name}-synthetic`,
|
|
30
44
|
policyDecision: {
|
|
31
|
-
blocksDelivery:
|
|
32
|
-
requiresOperatorReview: status !== "passed",
|
|
45
|
+
blocksDelivery: status === "blocked" || status === "failed",
|
|
46
|
+
requiresOperatorReview: status !== "passed" && status !== "skipped",
|
|
33
47
|
routedToRepair: false,
|
|
34
48
|
summary,
|
|
35
49
|
},
|
|
50
|
+
providerAttempt,
|
|
36
51
|
};
|
|
37
52
|
}
|
|
38
53
|
function appendTelemetry(telemetryFile, event) {
|
|
@@ -46,30 +61,131 @@ function appendTelemetry(telemetryFile, event) {
|
|
|
46
61
|
// Telemetry is best-effort.
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
function resolveMode(scope) {
|
|
65
|
+
return scope.prUrl ? "pr" : "local";
|
|
66
|
+
}
|
|
67
|
+
function isModeSupported(provider, scope) {
|
|
68
|
+
const mode = resolveMode(scope);
|
|
69
|
+
return provider.supportedModes.includes(mode);
|
|
70
|
+
}
|
|
71
|
+
function getProviderConfig(name, config) {
|
|
72
|
+
return config?.providers?.[name];
|
|
73
|
+
}
|
|
74
|
+
function classifyTerminalFailure(error, output) {
|
|
75
|
+
const execError = error;
|
|
76
|
+
if (execError?.killed && execError?.signal === "SIGTERM") {
|
|
77
|
+
return "timeout";
|
|
78
|
+
}
|
|
79
|
+
if (execError?.code === "ENOENT") {
|
|
80
|
+
return "command-not-found";
|
|
81
|
+
}
|
|
82
|
+
// Only scan output for failure keywords if the command actually failed
|
|
83
|
+
if (!error && output.exitCode === 0) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const text = `${output.stdout ?? ""}\n${output.stderr ?? ""}`.toLowerCase();
|
|
87
|
+
if (text.includes("rate limit") ||
|
|
88
|
+
text.includes("429") ||
|
|
89
|
+
text.includes("too many requests")) {
|
|
90
|
+
return "rate-limited";
|
|
91
|
+
}
|
|
92
|
+
if (text.includes("unauthorized") ||
|
|
93
|
+
text.includes("authentication failed") ||
|
|
94
|
+
text.includes("invalid token") ||
|
|
95
|
+
text.includes("401") ||
|
|
96
|
+
text.includes("forbidden") ||
|
|
97
|
+
text.includes("403")) {
|
|
98
|
+
return "auth-failure";
|
|
99
|
+
}
|
|
100
|
+
if (text.includes("unavailable") ||
|
|
101
|
+
text.includes("service unavailable") ||
|
|
102
|
+
text.includes("503") ||
|
|
103
|
+
text.includes("connection refused") ||
|
|
104
|
+
text.includes("econnrefused")) {
|
|
105
|
+
return "unavailable-provider";
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
function classifyPostParseFailure(output, parsed) {
|
|
110
|
+
if (parsed.findings.length > 0) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const stdout = output.stdout ?? "";
|
|
114
|
+
const stderr = output.stderr ?? "";
|
|
115
|
+
if (stdout.trim().length === 0 && stderr.trim().length === 0) {
|
|
116
|
+
if (output.exitCode !== 0) {
|
|
117
|
+
return "empty-output";
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (output.exitCode !== 0) {
|
|
122
|
+
return "nonzero-exit";
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function emitProviderAttempted(telemetryFile, runId, clusterId, providerName, fallbackSource) {
|
|
127
|
+
appendTelemetry(telemetryFile, {
|
|
128
|
+
event: fallbackSource ? "qc-fallback-attempted" : "qc-provider-attempted",
|
|
129
|
+
run_id: runId,
|
|
130
|
+
cluster_id: clusterId,
|
|
131
|
+
provider: providerName,
|
|
132
|
+
fallback_source: fallbackSource,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function emitProviderFailed(telemetryFile, runId, clusterId, providerName, reason, exitCode) {
|
|
137
|
+
appendTelemetry(telemetryFile, {
|
|
138
|
+
event: "qc-provider-failed",
|
|
139
|
+
run_id: runId,
|
|
140
|
+
cluster_id: clusterId,
|
|
141
|
+
provider: providerName,
|
|
57
142
|
reason,
|
|
58
|
-
|
|
143
|
+
exit_code: exitCode,
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
59
145
|
});
|
|
60
|
-
return result;
|
|
61
146
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
147
|
+
function emitFallbackSucceeded(telemetryFile, runId, clusterId, providerName, fallbackSource) {
|
|
148
|
+
appendTelemetry(telemetryFile, {
|
|
149
|
+
event: "qc-fallback-succeeded",
|
|
150
|
+
run_id: runId,
|
|
151
|
+
cluster_id: clusterId,
|
|
152
|
+
provider: providerName,
|
|
153
|
+
fallback_source: fallbackSource,
|
|
154
|
+
timestamp: new Date().toISOString(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function emitAllProvidersFailed(telemetryFile, runId, clusterId, attempted) {
|
|
158
|
+
appendTelemetry(telemetryFile, {
|
|
159
|
+
event: "qc-all-providers-failed",
|
|
160
|
+
run_id: runId,
|
|
161
|
+
cluster_id: clusterId,
|
|
162
|
+
attempted_providers: attempted,
|
|
163
|
+
timestamp: new Date().toISOString(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function buildFailedResult(provider, scope, startedAt, reason, output, overrides) {
|
|
167
|
+
const providerAttempt = makeProviderAttempt(provider.name, "failure", output, {
|
|
168
|
+
failureReason: reason,
|
|
169
|
+
...overrides,
|
|
170
|
+
});
|
|
171
|
+
return makeSyntheticResult(provider, scope, startedAt, "failed", `QC provider ${provider.name} failed: ${reason}`, providerAttempt);
|
|
172
|
+
}
|
|
173
|
+
async function runSingleProvider(provider, scope, options, fallbackSource) {
|
|
69
174
|
const startedAt = new Date().toISOString();
|
|
70
175
|
const command = provider.buildReviewCommand(scope);
|
|
71
176
|
const execFn = options.execFileImpl ?? node_child_process_1.execFile;
|
|
72
177
|
const timeoutMs = options.timeoutMs ?? 300_000; // 5 minutes default
|
|
178
|
+
const providerConfig = getProviderConfig(provider.name, options.config);
|
|
179
|
+
emitProviderAttempted(options.telemetryFile, scope.runId, scope.clusterId, provider.name, fallbackSource);
|
|
180
|
+
if (!isModeSupported(provider, scope)) {
|
|
181
|
+
const output = {
|
|
182
|
+
provider: provider.name,
|
|
183
|
+
exitCode: 1,
|
|
184
|
+
};
|
|
185
|
+
const result = buildFailedResult(provider, scope, startedAt, "unsupported-mode", output);
|
|
186
|
+
emitProviderFailed(options.telemetryFile, scope.runId, scope.clusterId, provider.name, "unsupported-mode");
|
|
187
|
+
return { result, success: false };
|
|
188
|
+
}
|
|
73
189
|
return new Promise((resolve) => {
|
|
74
190
|
let stdout = "";
|
|
75
191
|
let stderr = "";
|
|
@@ -81,28 +197,77 @@ async function executeQcProvider(provider, scope, options) {
|
|
|
81
197
|
}, (error, out, err) => {
|
|
82
198
|
stdout = out ?? "";
|
|
83
199
|
stderr = err ?? "";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
200
|
+
const output = {
|
|
201
|
+
provider: provider.name,
|
|
202
|
+
stdout,
|
|
203
|
+
stderr,
|
|
204
|
+
exitCode: error ? (typeof error.code === "number" ? error.code : 1) : 0,
|
|
205
|
+
};
|
|
206
|
+
const terminalReason = classifyTerminalFailure(error, output);
|
|
207
|
+
if (terminalReason) {
|
|
208
|
+
const result = buildFailedResult(provider, scope, startedAt, terminalReason, output);
|
|
209
|
+
emitProviderFailed(options.telemetryFile, scope.runId, scope.clusterId, provider.name, terminalReason, output.exitCode);
|
|
210
|
+
resolve({ result, success: false });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Empty output combined with a nonzero exit is an explicit failure.
|
|
214
|
+
if (output.exitCode !== 0 && stdout.trim().length === 0 && stderr.trim().length === 0) {
|
|
215
|
+
const result = buildFailedResult(provider, scope, startedAt, "empty-output", output);
|
|
216
|
+
emitProviderFailed(options.telemetryFile, scope.runId, scope.clusterId, provider.name, "empty-output", output.exitCode);
|
|
217
|
+
resolve({ result, success: false });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Empty success output is a valid "no findings" result.
|
|
221
|
+
if (output.exitCode === 0 && stdout.trim().length === 0 && stderr.trim().length === 0) {
|
|
222
|
+
const completedAt = new Date().toISOString();
|
|
223
|
+
const result = {
|
|
224
|
+
schemaVersion: "1.0",
|
|
225
|
+
qcRunId: `${provider.name}-${Date.now()}`,
|
|
226
|
+
runId: scope.runId,
|
|
227
|
+
clusterId: scope.clusterId,
|
|
228
|
+
trigger: scope.prUrl ? "pr" : "completed-cluster",
|
|
229
|
+
provider: provider.name,
|
|
230
|
+
providerMode: scope.prUrl ? "pr" : "local",
|
|
231
|
+
prUrl: scope.prUrl,
|
|
232
|
+
startedAt,
|
|
233
|
+
completedAt,
|
|
234
|
+
status: "passed",
|
|
235
|
+
findings: [],
|
|
236
|
+
rawArtifactPaths: [],
|
|
237
|
+
parserVersion: `${provider.name}-1.0`,
|
|
238
|
+
policyDecision: {
|
|
239
|
+
blocksDelivery: false,
|
|
240
|
+
requiresOperatorReview: false,
|
|
241
|
+
routedToRepair: false,
|
|
242
|
+
summary: "No findings",
|
|
243
|
+
},
|
|
244
|
+
providerAttempt: makeProviderAttempt(provider.name, "success", output, {
|
|
245
|
+
parserResult: "success",
|
|
246
|
+
}),
|
|
247
|
+
};
|
|
87
248
|
appendTelemetry(options.telemetryFile, {
|
|
88
|
-
event: "qc-provider-
|
|
249
|
+
event: "qc-provider-executed",
|
|
89
250
|
run_id: scope.runId,
|
|
90
251
|
cluster_id: scope.clusterId,
|
|
91
252
|
provider: provider.name,
|
|
92
|
-
|
|
93
|
-
|
|
253
|
+
trigger: result.trigger,
|
|
254
|
+
status: result.status,
|
|
255
|
+
findings_count: 0,
|
|
256
|
+
exit_code: 0,
|
|
257
|
+
timestamp: completedAt,
|
|
94
258
|
});
|
|
95
|
-
resolve(result);
|
|
259
|
+
resolve({ result, success: true });
|
|
96
260
|
return;
|
|
97
261
|
}
|
|
98
|
-
const output = {
|
|
99
|
-
provider: provider.name,
|
|
100
|
-
stdout,
|
|
101
|
-
stderr,
|
|
102
|
-
exitCode: error ? (typeof error.code === "number" ? error.code : 1) : 0,
|
|
103
|
-
};
|
|
104
262
|
try {
|
|
105
263
|
const parsed = provider.parse(output);
|
|
264
|
+
const postParseReason = classifyPostParseFailure(output, parsed);
|
|
265
|
+
if (postParseReason) {
|
|
266
|
+
const result = buildFailedResult(provider, scope, startedAt, postParseReason, output);
|
|
267
|
+
emitProviderFailed(options.telemetryFile, scope.runId, scope.clusterId, provider.name, postParseReason, output.exitCode);
|
|
268
|
+
resolve({ result, success: false });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
106
271
|
const completedAt = new Date().toISOString();
|
|
107
272
|
const result = {
|
|
108
273
|
...parsed,
|
|
@@ -115,6 +280,9 @@ async function executeQcProvider(provider, scope, options) {
|
|
|
115
280
|
prUrl: scope.prUrl,
|
|
116
281
|
startedAt,
|
|
117
282
|
completedAt,
|
|
283
|
+
providerAttempt: makeProviderAttempt(provider.name, "success", output, {
|
|
284
|
+
parserResult: "success",
|
|
285
|
+
}),
|
|
118
286
|
};
|
|
119
287
|
appendTelemetry(options.telemetryFile, {
|
|
120
288
|
event: "qc-provider-executed",
|
|
@@ -127,11 +295,134 @@ async function executeQcProvider(provider, scope, options) {
|
|
|
127
295
|
exit_code: output.exitCode,
|
|
128
296
|
timestamp: completedAt,
|
|
129
297
|
});
|
|
130
|
-
resolve(result);
|
|
298
|
+
resolve({ result, success: true });
|
|
131
299
|
}
|
|
132
300
|
catch (parseError) {
|
|
133
|
-
|
|
301
|
+
const reason = typeof parseError === "object" &&
|
|
302
|
+
parseError !== null &&
|
|
303
|
+
"qcFailureReason" in parseError &&
|
|
304
|
+
typeof parseError.qcFailureReason === "string"
|
|
305
|
+
? parseError.qcFailureReason
|
|
306
|
+
: "parse-failed";
|
|
307
|
+
const result = buildFailedResult(provider, scope, startedAt, reason, output, {
|
|
308
|
+
parserResult: "failed",
|
|
309
|
+
});
|
|
310
|
+
emitProviderFailed(options.telemetryFile, scope.runId, scope.clusterId, provider.name, reason, output.exitCode);
|
|
311
|
+
resolve({ result, success: false });
|
|
134
312
|
}
|
|
135
313
|
});
|
|
136
314
|
});
|
|
137
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Execute a provider review command for the given scope.
|
|
318
|
+
*
|
|
319
|
+
* Timeouts, parse failures, and other non-finding outcomes are normalized
|
|
320
|
+
* into QcResult objects that carry a providerAttempt classification. When a
|
|
321
|
+
* QcConfig and QcProviderRegistry are supplied, the runner follows each
|
|
322
|
+
* provider's fallback list according to its failurePolicy.
|
|
323
|
+
*/
|
|
324
|
+
async function executeQcProvider(provider, scope, options) {
|
|
325
|
+
const visited = new Set();
|
|
326
|
+
const attempted = [];
|
|
327
|
+
async function attemptChain(currentProvider, fallbackSource) {
|
|
328
|
+
if (visited.has(currentProvider.name)) {
|
|
329
|
+
return {
|
|
330
|
+
result: buildFailedResult(currentProvider, scope, new Date().toISOString(), "unavailable-provider", { provider: currentProvider.name, exitCode: 1 }),
|
|
331
|
+
success: false,
|
|
332
|
+
sourceProvider: currentProvider.name,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
visited.add(currentProvider.name);
|
|
336
|
+
attempted.push(currentProvider.name);
|
|
337
|
+
const { result, success } = await runSingleProvider(currentProvider, scope, options, fallbackSource);
|
|
338
|
+
if (success) {
|
|
339
|
+
return { result, success, sourceProvider: currentProvider.name };
|
|
340
|
+
}
|
|
341
|
+
const providerConfig = getProviderConfig(currentProvider.name, options.config);
|
|
342
|
+
const failureAction = (0, policy_js_1.decideProviderFailureAction)(result.providerAttempt?.failureReason ?? "nonzero-exit", providerConfig?.failurePolicy);
|
|
343
|
+
if (failureAction === "ignore") {
|
|
344
|
+
const skippedResult = {
|
|
345
|
+
...result,
|
|
346
|
+
status: "skipped",
|
|
347
|
+
providerAttempt: result.providerAttempt
|
|
348
|
+
? { ...result.providerAttempt, status: "skipped" }
|
|
349
|
+
: undefined,
|
|
350
|
+
policyDecision: {
|
|
351
|
+
...result.policyDecision,
|
|
352
|
+
blocksDelivery: false,
|
|
353
|
+
requiresOperatorReview: false,
|
|
354
|
+
summary: `QC provider ${currentProvider.name} skipped per failure policy`,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
return { result: skippedResult, success: true, sourceProvider: currentProvider.name };
|
|
358
|
+
}
|
|
359
|
+
if (failureAction === "block") {
|
|
360
|
+
const blockedResult = {
|
|
361
|
+
...result,
|
|
362
|
+
status: "blocked",
|
|
363
|
+
policyDecision: {
|
|
364
|
+
...result.policyDecision,
|
|
365
|
+
blocksDelivery: true,
|
|
366
|
+
summary: `QC provider ${currentProvider.name} blocked per failure policy`,
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
return { result: blockedResult, success: false, sourceProvider: currentProvider.name };
|
|
370
|
+
}
|
|
371
|
+
if (failureAction !== "fallback" || !options.registry || !options.config) {
|
|
372
|
+
return { result, success: false, sourceProvider: currentProvider.name };
|
|
373
|
+
}
|
|
374
|
+
const fallbackNames = providerConfig?.fallback ?? [];
|
|
375
|
+
for (const fallbackName of fallbackNames) {
|
|
376
|
+
const fallbackProvider = options.registry.get(fallbackName);
|
|
377
|
+
if (!fallbackProvider) {
|
|
378
|
+
attempted.push(fallbackName);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const chainResult = await attemptChain(fallbackProvider, currentProvider.name);
|
|
382
|
+
if (chainResult.success) {
|
|
383
|
+
// Only emit telemetry and set fallbackSource if not already set by a deeper level
|
|
384
|
+
const existingFallbackSource = chainResult.result.providerAttempt?.fallbackSource;
|
|
385
|
+
if (!existingFallbackSource) {
|
|
386
|
+
emitFallbackSucceeded(options.telemetryFile, scope.runId, scope.clusterId, chainResult.sourceProvider, currentProvider.name);
|
|
387
|
+
}
|
|
388
|
+
const finalResult = {
|
|
389
|
+
...chainResult.result,
|
|
390
|
+
providerAttempt: chainResult.result.providerAttempt
|
|
391
|
+
? {
|
|
392
|
+
...chainResult.result.providerAttempt,
|
|
393
|
+
fallbackSource: existingFallbackSource ?? currentProvider.name,
|
|
394
|
+
}
|
|
395
|
+
: undefined,
|
|
396
|
+
};
|
|
397
|
+
return { result: finalResult, success: true, sourceProvider: chainResult.sourceProvider };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { result, success: false, sourceProvider: currentProvider.name };
|
|
401
|
+
}
|
|
402
|
+
const { result, success, sourceProvider } = await attemptChain(provider);
|
|
403
|
+
if (success) {
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
// If the primary provider (or its fallback chain) failed and no fallback
|
|
407
|
+
// succeeded, emit an all-providers-failed result and telemetry event when a
|
|
408
|
+
// registry/config was supplied so the chain was actually resolvable.
|
|
409
|
+
if (options.registry && options.config) {
|
|
410
|
+
emitAllProvidersFailed(options.telemetryFile, scope.runId, scope.clusterId, attempted);
|
|
411
|
+
const allFailedResult = {
|
|
412
|
+
...result,
|
|
413
|
+
status: "failed",
|
|
414
|
+
allProvidersFailed: true,
|
|
415
|
+
providerAttempt: result.providerAttempt
|
|
416
|
+
? { ...result.providerAttempt, status: "failure" }
|
|
417
|
+
: undefined,
|
|
418
|
+
policyDecision: {
|
|
419
|
+
...result.policyDecision,
|
|
420
|
+
blocksDelivery: true,
|
|
421
|
+
requiresOperatorReview: true,
|
|
422
|
+
summary: `All QC providers failed for ${provider.name}`,
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
return allFailedResult;
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
package/dist/qc/schemas.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.qcResultSchema = exports.qcPolicyDecisionSchema = exports.qcFindingSchema = exports.qcAttributionSchema = exports.qcCodeRangeSchema = exports.qcAttributionReasonSchema = exports.qcAttributionConfidenceSchema = exports.qcRoutingDecisionSchema = exports.qcFindingStatusSchema = exports.qcSeveritySchema = void 0;
|
|
3
|
+
exports.qcRepairPacketManifestSchema = exports.qcRepairPacketSchema = exports.qcRepairPacketStatusSchema = exports.qcResultSchema = exports.qcPolicyDecisionSchema = exports.qcFindingSchema = exports.qcAttributionSchema = exports.qcCodeRangeSchema = exports.qcProviderAttemptSchema = exports.qcProviderAttemptStatusSchema = exports.qcParserResultSchema = exports.qcFailureReasonSchema = exports.qcAttributionReasonSchema = exports.qcAttributionConfidenceSchema = exports.qcRoutingDecisionSchema = exports.qcFindingStatusSchema = exports.qcSeveritySchema = void 0;
|
|
4
4
|
exports.validateQcResult = validateQcResult;
|
|
5
5
|
exports.validateQcFinding = validateQcFinding;
|
|
6
|
+
exports.validateRepairPacketManifest = validateRepairPacketManifest;
|
|
6
7
|
const zod_1 = require("zod");
|
|
7
8
|
exports.qcSeveritySchema = zod_1.z.enum(["critical", "high", "medium", "low", "info"]);
|
|
8
9
|
exports.qcFindingStatusSchema = zod_1.z.enum([
|
|
@@ -33,6 +34,38 @@ exports.qcAttributionReasonSchema = zod_1.z.enum([
|
|
|
33
34
|
"provider-uncertain",
|
|
34
35
|
"unattributed",
|
|
35
36
|
]);
|
|
37
|
+
exports.qcFailureReasonSchema = zod_1.z.enum([
|
|
38
|
+
"timeout",
|
|
39
|
+
"rate-limited",
|
|
40
|
+
"auth-failure",
|
|
41
|
+
"command-not-found",
|
|
42
|
+
"nonzero-exit",
|
|
43
|
+
"parse-failed",
|
|
44
|
+
"empty-output",
|
|
45
|
+
"unusable-output",
|
|
46
|
+
"unsupported-mode",
|
|
47
|
+
"unavailable-provider",
|
|
48
|
+
]);
|
|
49
|
+
exports.qcParserResultSchema = zod_1.z.enum(["success", "partial", "failed"]);
|
|
50
|
+
exports.qcProviderAttemptStatusSchema = zod_1.z.enum([
|
|
51
|
+
"success",
|
|
52
|
+
"failure",
|
|
53
|
+
"fallback",
|
|
54
|
+
"skipped",
|
|
55
|
+
]);
|
|
56
|
+
exports.qcProviderAttemptSchema = zod_1.z.object({
|
|
57
|
+
provider: zod_1.z.string(),
|
|
58
|
+
status: exports.qcProviderAttemptStatusSchema,
|
|
59
|
+
failureReason: exports.qcFailureReasonSchema.optional(),
|
|
60
|
+
fallbackSource: zod_1.z.string().optional(),
|
|
61
|
+
rawOutputAvailable: zod_1.z.boolean(),
|
|
62
|
+
rawOutputRetained: zod_1.z.boolean(),
|
|
63
|
+
rawOutputArtifactPath: zod_1.z.string().optional(),
|
|
64
|
+
parserResult: exports.qcParserResultSchema.optional(),
|
|
65
|
+
exitCode: zod_1.z.number().optional(),
|
|
66
|
+
stdoutLength: zod_1.z.number().int().min(0),
|
|
67
|
+
stderrLength: zod_1.z.number().int().min(0),
|
|
68
|
+
});
|
|
36
69
|
exports.qcCodeRangeSchema = zod_1.z.object({
|
|
37
70
|
startLine: zod_1.z.number().int().positive(),
|
|
38
71
|
startColumn: zod_1.z.number().int().positive().optional(),
|
|
@@ -86,6 +119,8 @@ exports.qcResultSchema = zod_1.z.object({
|
|
|
86
119
|
rawArtifactPaths: zod_1.z.array(zod_1.z.string()),
|
|
87
120
|
parserVersion: zod_1.z.string(),
|
|
88
121
|
policyDecision: exports.qcPolicyDecisionSchema,
|
|
122
|
+
providerAttempt: exports.qcProviderAttemptSchema.optional(),
|
|
123
|
+
allProvidersFailed: zod_1.z.boolean().optional(),
|
|
89
124
|
});
|
|
90
125
|
/**
|
|
91
126
|
* Validate an unknown value as a normalized QC result.
|
|
@@ -108,3 +143,46 @@ function validateQcFinding(value) {
|
|
|
108
143
|
}
|
|
109
144
|
return { success: false, errors: parsed.error.issues.map((issue) => issue.message) };
|
|
110
145
|
}
|
|
146
|
+
exports.qcRepairPacketStatusSchema = zod_1.z.enum([
|
|
147
|
+
"pending",
|
|
148
|
+
"dispatched",
|
|
149
|
+
"completed",
|
|
150
|
+
"failed",
|
|
151
|
+
"escalated",
|
|
152
|
+
]);
|
|
153
|
+
exports.qcRepairPacketSchema = zod_1.z.object({
|
|
154
|
+
packetId: zod_1.z.string(),
|
|
155
|
+
round: zod_1.z.number().int().min(0),
|
|
156
|
+
clusterId: zod_1.z.string(),
|
|
157
|
+
sourceQcRunIds: zod_1.z.array(zod_1.z.string()),
|
|
158
|
+
findingIds: zod_1.z.array(zod_1.z.string()),
|
|
159
|
+
severityFloor: exports.qcSeveritySchema,
|
|
160
|
+
rootCauseHint: zod_1.z.string(),
|
|
161
|
+
allowedScope: zod_1.z.array(zod_1.z.string()),
|
|
162
|
+
prohibitedScope: zod_1.z.array(zod_1.z.string()),
|
|
163
|
+
validationCommands: zod_1.z.array(zod_1.z.string()),
|
|
164
|
+
routingTarget: exports.qcRoutingDecisionSchema,
|
|
165
|
+
parallelGroup: zod_1.z.string().nullable(),
|
|
166
|
+
conflicts: zod_1.z.array(zod_1.z.string()),
|
|
167
|
+
medic: zod_1.z.boolean(),
|
|
168
|
+
status: exports.qcRepairPacketStatusSchema,
|
|
169
|
+
createdAt: zod_1.z.string().datetime(),
|
|
170
|
+
});
|
|
171
|
+
exports.qcRepairPacketManifestSchema = zod_1.z.object({
|
|
172
|
+
schemaVersion: zod_1.z.string(),
|
|
173
|
+
clusterId: zod_1.z.string(),
|
|
174
|
+
round: zod_1.z.number().int().min(0),
|
|
175
|
+
compiledAt: zod_1.z.string().datetime(),
|
|
176
|
+
sourceQcRunIds: zod_1.z.array(zod_1.z.string()),
|
|
177
|
+
packets: zod_1.z.array(exports.qcRepairPacketSchema),
|
|
178
|
+
});
|
|
179
|
+
/**
|
|
180
|
+
* Validate an unknown value as a compiled repair packet manifest.
|
|
181
|
+
*/
|
|
182
|
+
function validateRepairPacketManifest(value) {
|
|
183
|
+
const parsed = exports.qcRepairPacketManifestSchema.safeParse(value);
|
|
184
|
+
if (parsed.success) {
|
|
185
|
+
return { success: true, manifest: parsed.data };
|
|
186
|
+
}
|
|
187
|
+
return { success: false, errors: parsed.error.issues.map((issue) => issue.message) };
|
|
188
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SOL raw metric event contracts.
|
|
4
|
+
*
|
|
5
|
+
* Typed contracts for the raw metric events that the SOL scoring pipeline
|
|
6
|
+
* distinguishes when building evidence and scorecards. These types name the
|
|
7
|
+
* six metric categories defined in the POL-487 acceptance criteria:
|
|
8
|
+
*
|
|
9
|
+
* 1. Provider startup failure
|
|
10
|
+
* 2. Router fallback
|
|
11
|
+
* 3. Worker execution failure
|
|
12
|
+
* 4. Validation failure
|
|
13
|
+
* 5. QC findings
|
|
14
|
+
* 6. User / Foreman intervention
|
|
15
|
+
*
|
|
16
|
+
* These are read-model types — they normalize the raw telemetry events and
|
|
17
|
+
* result packet signals into a typed contract that scoring functions consume.
|
|
18
|
+
* Nothing in this file writes to artifacts or triggers side effects.
|
|
19
|
+
*
|
|
20
|
+
* Relationship to SolEvidence:
|
|
21
|
+
* SolMetricEvent records are materialized by the evidence loader into the
|
|
22
|
+
* SolEvidence structure. The SolEvidence fields (foreman, worker, router,
|
|
23
|
+
* qc, intervention) already carry the aggregated versions; these typed
|
|
24
|
+
* event contracts let callers reason about individual metric records and
|
|
25
|
+
* construct per-scope scorecards.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.isProviderStartupFailure = isProviderStartupFailure;
|
|
29
|
+
exports.isRouterFallback = isRouterFallback;
|
|
30
|
+
exports.isWorkerExecutionFailure = isWorkerExecutionFailure;
|
|
31
|
+
exports.isValidationFailure = isValidationFailure;
|
|
32
|
+
exports.isQcFinding = isQcFinding;
|
|
33
|
+
exports.isIntervention = isIntervention;
|
|
34
|
+
exports.summarizeMetricEvents = summarizeMetricEvents;
|
|
35
|
+
// ──────────────────────────────────────────────
|
|
36
|
+
// Type guards
|
|
37
|
+
// ──────────────────────────────────────────────
|
|
38
|
+
function isProviderStartupFailure(e) {
|
|
39
|
+
return e.category === "provider-startup-failure";
|
|
40
|
+
}
|
|
41
|
+
function isRouterFallback(e) {
|
|
42
|
+
return e.category === "router-fallback";
|
|
43
|
+
}
|
|
44
|
+
function isWorkerExecutionFailure(e) {
|
|
45
|
+
return e.category === "worker-execution-failure";
|
|
46
|
+
}
|
|
47
|
+
function isValidationFailure(e) {
|
|
48
|
+
return e.category === "validation-failure";
|
|
49
|
+
}
|
|
50
|
+
function isQcFinding(e) {
|
|
51
|
+
return e.category === "qc-finding";
|
|
52
|
+
}
|
|
53
|
+
function isIntervention(e) {
|
|
54
|
+
return e.category === "user-intervention" || e.category === "foreman-intervention";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compute a SolMetricSummary from a list of metric events.
|
|
58
|
+
*/
|
|
59
|
+
function summarizeMetricEvents(events) {
|
|
60
|
+
let providerStartupFailures = 0;
|
|
61
|
+
let routerFallbacks = 0;
|
|
62
|
+
let routerFallbackSuccesses = 0;
|
|
63
|
+
let workerExecutionFailures = 0;
|
|
64
|
+
let validationFailures = 0;
|
|
65
|
+
let qcFindingsTotal = 0;
|
|
66
|
+
let qcFindingsBlocking = 0;
|
|
67
|
+
let qcFindingsUnvalidated = 0;
|
|
68
|
+
let userInterventions = 0;
|
|
69
|
+
let foremanInterventions = 0;
|
|
70
|
+
for (const e of events) {
|
|
71
|
+
if (isProviderStartupFailure(e))
|
|
72
|
+
providerStartupFailures++;
|
|
73
|
+
else if (isRouterFallback(e)) {
|
|
74
|
+
routerFallbacks++;
|
|
75
|
+
if (e.fallback_succeeded)
|
|
76
|
+
routerFallbackSuccesses++;
|
|
77
|
+
}
|
|
78
|
+
else if (isWorkerExecutionFailure(e))
|
|
79
|
+
workerExecutionFailures++;
|
|
80
|
+
else if (isValidationFailure(e))
|
|
81
|
+
validationFailures++;
|
|
82
|
+
else if (isQcFinding(e)) {
|
|
83
|
+
qcFindingsTotal++;
|
|
84
|
+
if (e.blocking)
|
|
85
|
+
qcFindingsBlocking++;
|
|
86
|
+
if (e.unvalidated)
|
|
87
|
+
qcFindingsUnvalidated++;
|
|
88
|
+
}
|
|
89
|
+
else if (isIntervention(e)) {
|
|
90
|
+
if (e.actor === "user")
|
|
91
|
+
userInterventions++;
|
|
92
|
+
else
|
|
93
|
+
foremanInterventions++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
provider_startup_failures: providerStartupFailures,
|
|
98
|
+
router_fallbacks: routerFallbacks,
|
|
99
|
+
router_fallback_successes: routerFallbackSuccesses,
|
|
100
|
+
worker_execution_failures: workerExecutionFailures,
|
|
101
|
+
validation_failures: validationFailures,
|
|
102
|
+
qc_findings_total: qcFindingsTotal,
|
|
103
|
+
qc_findings_blocking: qcFindingsBlocking,
|
|
104
|
+
qc_findings_unvalidated: qcFindingsUnvalidated,
|
|
105
|
+
user_interventions: userInterventions,
|
|
106
|
+
foreman_interventions: foremanInterventions,
|
|
107
|
+
};
|
|
108
|
+
}
|