@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.
Files changed (37) hide show
  1. package/dist/autoresearch/index.js +30 -1
  2. package/dist/autoresearch/proposal.js +5 -0
  3. package/dist/autoresearch/score.js +174 -2
  4. package/dist/autoresearch/sol-evaluation-writer.js +135 -0
  5. package/dist/autoresearch/sol-evidence-loader.js +40 -3
  6. package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
  7. package/dist/autoresearch/sol-recommendations.js +212 -0
  8. package/dist/autoresearch/sol-report-renderer.js +282 -0
  9. package/dist/autoresearch/sol-report.js +44 -0
  10. package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
  11. package/dist/autoresearch/sol-scorer.js +45 -1
  12. package/dist/cli/autoresearch.js +77 -0
  13. package/dist/cluster-state/store.js +56 -0
  14. package/dist/config/defaults.js +1 -0
  15. package/dist/config/validator.js +157 -0
  16. package/dist/finalize/artifact-policy.js +2 -0
  17. package/dist/finalize/github.js +13 -9
  18. package/dist/finalize/index.js +198 -4
  19. package/dist/finalize/linear.js +8 -4
  20. package/dist/finalize/steps/08-create-pr.js +2 -2
  21. package/dist/finalize/steps/11-update-linear.js +2 -2
  22. package/dist/loop/parent.js +189 -33
  23. package/dist/loop/worker-packet.js +75 -1
  24. package/dist/qc/artifacts.js +27 -0
  25. package/dist/qc/fixtures/repair-packets.js +170 -0
  26. package/dist/qc/index.js +2 -0
  27. package/dist/qc/orchestration.js +5 -2
  28. package/dist/qc/policy.js +30 -3
  29. package/dist/qc/providers/coderabbit.js +152 -17
  30. package/dist/qc/registry.js +36 -3
  31. package/dist/qc/repair-loop.js +458 -0
  32. package/dist/qc/repair-packets.js +420 -0
  33. package/dist/qc/runner.js +328 -37
  34. package/dist/qc/schemas.js +79 -1
  35. package/dist/types/sol-metrics.js +108 -0
  36. package/dist/types/sol-scorecard.js +148 -0
  37. 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, handles timeouts, and normalizes
6
- * the raw output into a Polaris QcResult with runtime fields populated.
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
- function makeSyntheticResult(provider, scope, startedAt, status, summary) {
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: false,
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 resolveExecutionFailure(provider, scope, options, startedAt, output, reason) {
50
- const result = makeSyntheticResult(provider, scope, startedAt, "failed", reason);
51
- appendTelemetry(options.telemetryFile, {
52
- event: "qc-provider-execution-failed",
53
- run_id: scope.runId,
54
- cluster_id: scope.clusterId,
55
- provider: provider.name,
56
- exit_code: output.exitCode,
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
- timestamp: result.completedAt,
143
+ exit_code: exitCode,
144
+ timestamp: new Date().toISOString(),
59
145
  });
60
- return result;
61
146
  }
62
- /**
63
- * Execute a provider review command for the given scope.
64
- *
65
- * Timeouts produce a synthetic "failed" result so that policy logic downstream
66
- * can decide whether to block, follow-up, or report passively.
67
- */
68
- async function executeQcProvider(provider, scope, options) {
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
- // Detect timeout: execFile kills with SIGTERM on timeout.
85
- if (error && error.killed && error.signal === "SIGTERM") {
86
- const result = makeSyntheticResult(provider, scope, startedAt, "failed", `QC provider ${provider.name} timed out after ${timeoutMs}ms`);
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-timeout",
249
+ event: "qc-provider-executed",
89
250
  run_id: scope.runId,
90
251
  cluster_id: scope.clusterId,
91
252
  provider: provider.name,
92
- timeout_ms: timeoutMs,
93
- timestamp: new Date().toISOString(),
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
- resolve(resolveExecutionFailure(provider, scope, options, startedAt, output, `QC provider ${provider.name} parse failed: ${parseError instanceof Error ? parseError.message : String(parseError)}`));
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
+ }
@@ -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
+ }