@lsctech/polaris 0.5.7 → 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.
@@ -2031,35 +2031,40 @@ async function runParentLoop(options) {
2031
2031
  };
2032
2032
  }
2033
2033
  // ── Step 05 (post-dispatch): Re-check budget before next iteration ───
2034
- const postBudgetCheck = (0, budget_js_1.checkBudget)({
2035
- childrenCompleted: state.context_budget.children_completed,
2036
- lastChildStatus: workerStatus,
2037
- policy: budgetPolicy,
2038
- });
2039
- if (postBudgetCheck.status === 'exhausted') {
2040
- const nextPending = state.open_children[0] ?? null;
2041
- if (!dryRun) {
2042
- (0, checkpoint_js_1.writeStateAtomic)(stateFile, {
2043
- ...state,
2044
- status: "budget-exhausted",
2045
- step_cursor: "budget-check",
2046
- next_open_child: nextPending,
2047
- });
2048
- appendTelemetry(telemetryFile, {
2049
- event: "budget-exhausted",
2050
- run_id: state.run_id,
2051
- children_completed: state.context_budget.children_completed,
2052
- next_child: nextPending,
2053
- reason: postBudgetCheck.reason,
2054
- timestamp: new Date().toISOString(),
2055
- });
2034
+ // Skip when open_children is empty: the cluster is complete and the
2035
+ // top-of-loop nextChild === null path handles cluster-complete and QC
2036
+ // repair-loop. Halting here would bypass QC repair on final-child completion.
2037
+ if (state.open_children.length > 0) {
2038
+ const postBudgetCheck = (0, budget_js_1.checkBudget)({
2039
+ childrenCompleted: state.context_budget.children_completed,
2040
+ lastChildStatus: workerStatus,
2041
+ policy: budgetPolicy,
2042
+ });
2043
+ if (postBudgetCheck.status === 'exhausted') {
2044
+ const nextPending = state.open_children[0] ?? null;
2045
+ if (!dryRun) {
2046
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, {
2047
+ ...state,
2048
+ status: "budget-exhausted",
2049
+ step_cursor: "budget-check",
2050
+ next_open_child: nextPending,
2051
+ });
2052
+ appendTelemetry(telemetryFile, {
2053
+ event: "budget-exhausted",
2054
+ run_id: state.run_id,
2055
+ children_completed: state.context_budget.children_completed,
2056
+ next_child: nextPending,
2057
+ reason: postBudgetCheck.reason,
2058
+ timestamp: new Date().toISOString(),
2059
+ });
2060
+ }
2061
+ return {
2062
+ haltReason: 'budget-exhausted',
2063
+ childrenDispatched,
2064
+ haltingChild: nextPending ?? undefined,
2065
+ message: postBudgetCheck.reason,
2066
+ };
2056
2067
  }
2057
- return {
2058
- haltReason: 'budget-exhausted',
2059
- childrenDispatched,
2060
- haltingChild: nextPending ?? undefined,
2061
- message: postBudgetCheck.reason,
2062
- };
2063
2068
  }
2064
2069
  // ── Step 06: CONTINUE (back to step 02) ─────────────────────────────
2065
2070
  }
@@ -37,6 +37,7 @@ exports.getQcArtifactDir = getQcArtifactDir;
37
37
  exports.writeQcArtifact = writeQcArtifact;
38
38
  exports.readQcArtifact = readQcArtifact;
39
39
  exports.listQcArtifactIds = listQcArtifactIds;
40
+ exports.validateQcArtifactPointers = validateQcArtifactPointers;
40
41
  const node_fs_1 = require("node:fs");
41
42
  const path = __importStar(require("node:path"));
42
43
  const schemas_js_1 = require("./schemas.js");
@@ -115,3 +116,29 @@ function listQcArtifactIds(clusterId, repoRoot) {
115
116
  throw error;
116
117
  }
117
118
  }
119
+ /**
120
+ * Validate that the artifacts referenced by QC run pointers still exist.
121
+ * Returns a list of missing primary artifacts and unavailable raw audit
122
+ * artifacts so callers can warn, prune, or mark pointers consistently.
123
+ */
124
+ function validateQcArtifactPointers(qcRuns) {
125
+ const missing = [];
126
+ const unavailable = [];
127
+ if (!qcRuns) {
128
+ return { ok: true, missing, unavailable };
129
+ }
130
+ for (const pointer of Object.values(qcRuns)) {
131
+ if (!(0, node_fs_1.existsSync)(pointer.artifact_path)) {
132
+ missing.push(pointer.artifact_path);
133
+ }
134
+ for (const rawPath of pointer.raw_artifact_paths ?? []) {
135
+ if (!(0, node_fs_1.existsSync)(rawPath)) {
136
+ unavailable.push(rawPath);
137
+ }
138
+ }
139
+ if (pointer.provider_attempt_artifact_path && !(0, node_fs_1.existsSync)(pointer.provider_attempt_artifact_path)) {
140
+ unavailable.push(pointer.provider_attempt_artifact_path);
141
+ }
142
+ }
143
+ return { ok: missing.length === 0 && unavailable.length === 0, missing, unavailable };
144
+ }
@@ -20,7 +20,7 @@ const attribution_js_1 = require("./attribution.js");
20
20
  const autofix_js_1 = require("./autofix.js");
21
21
  const routing_js_1 = require("./routing.js");
22
22
  function buildAttributionContext(options) {
23
- const { repoRoot, clusterId, branch, state } = options;
23
+ const { repoRoot, clusterId, branch, baseRef, state } = options;
24
24
  const dispatchRecords = {};
25
25
  if (state?.open_children_meta) {
26
26
  for (const [childId, meta] of Object.entries(state.open_children_meta)) {
@@ -31,7 +31,7 @@ function buildAttributionContext(options) {
31
31
  }
32
32
  return {
33
33
  repoRoot,
34
- baseBranch: branch ?? state?.branch ?? "main",
34
+ baseBranch: baseRef ?? branch ?? state?.branch ?? "main",
35
35
  completedResults: state?.completed_children_results,
36
36
  dispatchRecords,
37
37
  clusterState: repoRoot ? ((0, store_js_1.readClusterStateSync)(clusterId, repoRoot) ?? undefined) : undefined,
@@ -99,6 +99,7 @@ async function runQcAtTrigger(options) {
99
99
  clusterId,
100
100
  runId,
101
101
  ...(trigger === "pr" ? { prUrl } : { branch }),
102
+ ...(options.baseRef ? { baseRef: options.baseRef } : {}),
102
103
  };
103
104
  try {
104
105
  const rawResult = await (0, runner_js_1.executeQcProvider)(provider, scope, {
@@ -16,6 +16,79 @@ function pickString(...candidates) {
16
16
  }
17
17
  return undefined;
18
18
  }
19
+ const FINDING_LOCATION_KEYS = ["file", "filePath", "path"];
20
+ const FINDING_REVIEW_KEYS = [
21
+ "severity",
22
+ "message",
23
+ "title",
24
+ "summary",
25
+ "description",
26
+ "body",
27
+ "rule",
28
+ "category",
29
+ "suggestion",
30
+ "suggestedAction",
31
+ "fix",
32
+ "providerFindingId",
33
+ "id",
34
+ "findingId",
35
+ ];
36
+ const PROGRESS_SHAPE_KEYS = new Set(["event", "progress", "heartbeat", "complete", "review_context"]);
37
+ const PROGRESS_TYPE_VALUES = new Set([
38
+ "progress",
39
+ "status",
40
+ "heartbeat",
41
+ "complete",
42
+ "review_context",
43
+ "reviewcontext",
44
+ ]);
45
+ const PROGRESS_STATUS_VALUES = new Set([
46
+ "in_progress",
47
+ "running",
48
+ "pending",
49
+ "complete",
50
+ "completed",
51
+ "done",
52
+ "heartbeat",
53
+ "ok",
54
+ "success",
55
+ ]);
56
+ function hasFindingLocation(record) {
57
+ return FINDING_LOCATION_KEYS.some((key) => record[key] !== undefined);
58
+ }
59
+ function hasFindingReviewContent(record) {
60
+ return FINDING_REVIEW_KEYS.some((key) => record[key] !== undefined);
61
+ }
62
+ function isProgressRecord(record) {
63
+ // Check progress/status indicators FIRST before the generic finding-content guard
64
+ const keys = Object.keys(record);
65
+ if (keys.length === 0)
66
+ return false;
67
+ if (keys.some((key) => PROGRESS_SHAPE_KEYS.has(key)))
68
+ return true;
69
+ if (typeof record.type === "string" && PROGRESS_TYPE_VALUES.has(record.type.toLowerCase()))
70
+ return true;
71
+ if (typeof record.status === "string" && PROGRESS_STATUS_VALUES.has(record.status.toLowerCase()))
72
+ return true;
73
+ // Status-only records with category="status" are progress records even if they have message/title fields
74
+ if (typeof record.category === "string" && record.category.toLowerCase() === "status")
75
+ return true;
76
+ // Only reject as progress if it has both location AND review content (true finding shape)
77
+ if (hasFindingLocation(record) && hasFindingReviewContent(record)) {
78
+ return false;
79
+ }
80
+ return false;
81
+ }
82
+ function isActionableFinding(record) {
83
+ if (isProgressRecord(record))
84
+ return false;
85
+ return hasFindingLocation(record) || hasFindingReviewContent(record);
86
+ }
87
+ function makeUnusableOutputError(message) {
88
+ const err = new Error(message);
89
+ err.qcFailureReason = "unusable-output";
90
+ return err;
91
+ }
19
92
  function buildRange(raw) {
20
93
  const startLine = coerceNumber(raw.startLine ?? raw.line) ?? 1;
21
94
  const endLine = coerceNumber(raw.endLine);
@@ -37,18 +110,16 @@ function parseFindingsFromPayload(payload) {
37
110
  }
38
111
  const record = payload;
39
112
  if (Array.isArray(record.findings)) {
40
- return record.findings;
113
+ return record.findings.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
41
114
  }
42
115
  if (Array.isArray(record.issues)) {
43
- return record.issues;
116
+ return record.issues.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
44
117
  }
45
118
  if (Array.isArray(record.results)) {
46
- return record.results;
119
+ return record.results.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
47
120
  }
48
121
  // Single finding wrapped in an object
49
- if (record.severity !== undefined ||
50
- record.message !== undefined ||
51
- record.title !== undefined) {
122
+ if (isActionableFinding(record)) {
52
123
  return [record];
53
124
  }
54
125
  return [];
@@ -77,10 +148,21 @@ function parseReport(output, format, parser) {
77
148
  try {
78
149
  const parsed = JSON.parse(text);
79
150
  if (Array.isArray(parsed)) {
80
- return { findings: parsed };
151
+ const findings = parsed.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
152
+ if (findings.length === 0 && parsed.length > 0) {
153
+ const progressCount = parsed.filter((item) => typeof item === "object" && item !== null && isProgressRecord(item)).length;
154
+ if (progressCount > 0) {
155
+ throw makeUnusableOutputError(`CodeRabbit output contained only progress/status/heartbeat records (${progressCount} items)`);
156
+ }
157
+ }
158
+ return { findings };
159
+ }
160
+ const findings = parseFindingsFromPayload(parsed);
161
+ if (findings.length === 0 && isProgressRecord(parsed)) {
162
+ throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
81
163
  }
82
164
  return {
83
- findings: parseFindingsFromPayload(parsed),
165
+ findings,
84
166
  ...(typeof parsed?.prUrl === "string"
85
167
  ? { prUrl: parsed.prUrl }
86
168
  : {}),
@@ -97,10 +179,23 @@ function parseReport(output, format, parser) {
97
179
  // JSONL or generic line scanning: one finding per line
98
180
  const lines = text.split("\n").filter((line) => line.trim().length > 0);
99
181
  const lineFindings = [];
182
+ let progressLineCount = 0;
183
+ let parsedLineCount = 0;
100
184
  for (const line of lines) {
101
185
  try {
102
186
  const parsed = JSON.parse(line);
103
- lineFindings.push(parsed);
187
+ parsedLineCount++;
188
+ if (!parsed || typeof parsed !== "object") {
189
+ continue;
190
+ }
191
+ const record = parsed;
192
+ if (isProgressRecord(record)) {
193
+ progressLineCount++;
194
+ continue;
195
+ }
196
+ if (isActionableFinding(record)) {
197
+ lineFindings.push(parsed);
198
+ }
104
199
  }
105
200
  catch {
106
201
  // Ignore unparseable lines.
@@ -109,6 +204,12 @@ function parseReport(output, format, parser) {
109
204
  if (lineFindings.length > 0) {
110
205
  return { findings: lineFindings };
111
206
  }
207
+ if (progressLineCount > 0) {
208
+ throw makeUnusableOutputError(`CodeRabbit output contained only progress/status/heartbeat records (${progressLineCount} lines)`);
209
+ }
210
+ if (parsedLineCount > 0) {
211
+ throw new Error("CodeRabbit output contained no actionable findings");
212
+ }
112
213
  throw new Error("CodeRabbit output could not be parsed as JSON, JSONL, or metrics payload");
113
214
  }
114
215
  function normalizeFinding(raw, index) {
@@ -217,20 +318,22 @@ class CodeRabbitQcProvider {
217
318
  if (execution.configPath) {
218
319
  args.push("--config", execution.configPath);
219
320
  }
321
+ const baseRef = scope.baseRef ?? scope.branch ?? "main";
220
322
  if (scope.prUrl) {
221
323
  args.push("--pr-url", scope.prUrl);
222
324
  }
223
325
  else {
224
- args.push("--branch", scope.branch ?? "HEAD");
326
+ args.push("--base", baseRef);
225
327
  }
226
328
  return { command: execution.command, args };
227
329
  }
228
330
  if (scope.prUrl) {
229
331
  return { command: "coderabbit", args: ["review", "--agent", "--pr-url", scope.prUrl] };
230
332
  }
333
+ const baseRef = scope.baseRef ?? scope.branch ?? "main";
231
334
  return {
232
335
  command: "coderabbit",
233
- args: ["review", "--agent", "--branch", scope.branch ?? "HEAD"],
336
+ args: ["review", "--agent", "--base", baseRef],
234
337
  };
235
338
  }
236
339
  parse(output) {
@@ -33,6 +33,43 @@ function appendTelemetry(telemetryFile, event) {
33
33
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
34
34
  (0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
35
35
  }
36
+ async function persistQcRepairOutcome(clusterId, repoRoot, outcome) {
37
+ try {
38
+ const clusterState = (0, store_js_1.readClusterStateSync)(clusterId, repoRoot);
39
+ if (!clusterState)
40
+ return;
41
+ await (0, store_js_1.writeClusterState)(clusterId, {
42
+ ...clusterState,
43
+ state_generation: clusterState.state_generation + 1,
44
+ qc_repair_outcome: outcome,
45
+ }, repoRoot);
46
+ }
47
+ catch {
48
+ // Best-effort: the loop outcome is already returned to the caller and
49
+ // recorded in the loop checkpoint. Do not block finalization on a
50
+ // cluster-state write race.
51
+ }
52
+ }
53
+ async function persistQcRepairManifest(clusterId, repoRoot, round, manifestPath) {
54
+ try {
55
+ const clusterState = (0, store_js_1.readClusterStateSync)(clusterId, repoRoot);
56
+ if (!clusterState)
57
+ return;
58
+ const manifests = { ...(clusterState.qc_repair_manifests ?? {}), [round]: manifestPath };
59
+ if (clusterState.qc_repair_manifests &&
60
+ clusterState.qc_repair_manifests[round] === manifestPath) {
61
+ return;
62
+ }
63
+ await (0, store_js_1.writeClusterState)(clusterId, {
64
+ ...clusterState,
65
+ state_generation: clusterState.state_generation + 1,
66
+ qc_repair_manifests: manifests,
67
+ }, repoRoot);
68
+ }
69
+ catch {
70
+ // Best-effort durability for repair-round manifest pointer.
71
+ }
72
+ }
36
73
  /** Returns true when any finding in the results routes to repair-worker. */
37
74
  function hasRepairableFindings(results) {
38
75
  return results.some((r) => r.findings.some((f) => (f.routingDecision === "repair-worker" || f.routingDecision === "original-worker") &&
@@ -109,6 +146,7 @@ async function runQcRepairLoop(options) {
109
146
  });
110
147
  state.terminal_outcome = "qc-disabled";
111
148
  state.updated_at = new Date().toISOString();
149
+ await persistQcRepairOutcome(clusterId, repoRoot, "qc-disabled");
112
150
  return {
113
151
  outcome: "qc-disabled",
114
152
  rounds_completed: 0,
@@ -147,6 +185,7 @@ async function runQcRepairLoop(options) {
147
185
  round,
148
186
  timestamp: new Date().toISOString(),
149
187
  });
188
+ await persistQcRepairOutcome(clusterId, repoRoot, "all-providers-failed");
150
189
  return {
151
190
  outcome: "all-providers-failed",
152
191
  rounds_completed: roundsCompleted,
@@ -165,6 +204,7 @@ async function runQcRepairLoop(options) {
165
204
  round,
166
205
  timestamp: new Date().toISOString(),
167
206
  });
207
+ await persistQcRepairOutcome(clusterId, repoRoot, "operator-review");
168
208
  return {
169
209
  outcome: "operator-review",
170
210
  rounds_completed: roundsCompleted,
@@ -189,19 +229,7 @@ async function runQcRepairLoop(options) {
189
229
  });
190
230
  manifest = compiled.manifest;
191
231
  // Update cluster state with manifest path.
192
- try {
193
- const clusterState = (0, store_js_1.readClusterStateSync)(clusterId, repoRoot);
194
- if (clusterState) {
195
- const repairManifests = {
196
- ...(clusterState.qc_repair_manifests ?? {}),
197
- [round]: compiled.manifestPath,
198
- };
199
- await (0, store_js_1.writeClusterState)(clusterId, { ...clusterState, state_generation: clusterState.state_generation + 1, qc_repair_manifests: repairManifests }, repoRoot);
200
- }
201
- }
202
- catch {
203
- // Non-fatal: cluster state update is best-effort.
204
- }
232
+ await persistQcRepairManifest(clusterId, repoRoot, round, compiled.manifestPath);
205
233
  loopState = {
206
234
  ...loopState,
207
235
  current_round: round,
@@ -221,13 +249,16 @@ async function runQcRepairLoop(options) {
221
249
  });
222
250
  }
223
251
  else {
252
+ const existingManifestPath = loopState.manifest_path ??
253
+ (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds", String(round), "repair-packets.json");
224
254
  loopState = {
225
255
  ...loopState,
226
256
  current_round: round,
227
- manifest_path: loopState.manifest_path ?? (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds", String(round), "repair-packets.json"),
257
+ manifest_path: existingManifestPath,
228
258
  updated_at: new Date().toISOString(),
229
259
  };
230
260
  onStateUpdate?.(loopState);
261
+ await persistQcRepairManifest(clusterId, repoRoot, round, existingManifestPath);
231
262
  }
232
263
  // ── Check for repairable packets ────────────────────────────────────────
233
264
  const repairablePackets = manifest.packets.filter((p) => p.routingTarget === "repair-worker" &&
@@ -243,6 +274,7 @@ async function runQcRepairLoop(options) {
243
274
  round,
244
275
  timestamp: new Date().toISOString(),
245
276
  });
277
+ await persistQcRepairOutcome(clusterId, repoRoot, "no-repairable");
246
278
  return {
247
279
  outcome: "no-repairable",
248
280
  rounds_completed: roundsCompleted,
@@ -309,6 +341,7 @@ async function runQcRepairLoop(options) {
309
341
  failed_count: failedWorkers.length,
310
342
  timestamp: new Date().toISOString(),
311
343
  });
344
+ await persistQcRepairOutcome(clusterId, repoRoot, "medic-referral");
312
345
  return {
313
346
  outcome: "medic-referral",
314
347
  rounds_completed: roundsCompleted + 1,
@@ -394,6 +427,7 @@ async function runQcRepairLoop(options) {
394
427
  round,
395
428
  timestamp: new Date().toISOString(),
396
429
  });
430
+ await persistQcRepairOutcome(clusterId, repoRoot, "pass");
397
431
  return {
398
432
  outcome: "pass",
399
433
  rounds_completed: roundsCompleted,
@@ -413,6 +447,7 @@ async function runQcRepairLoop(options) {
413
447
  rounds_completed: roundsCompleted,
414
448
  timestamp: new Date().toISOString(),
415
449
  });
450
+ await persistQcRepairOutcome(clusterId, repoRoot, "max-rounds");
416
451
  return {
417
452
  outcome: "max-rounds",
418
453
  rounds_completed: roundsCompleted,
package/dist/qc/runner.js CHANGED
@@ -298,8 +298,16 @@ async function runSingleProvider(provider, scope, options, fallbackSource) {
298
298
  resolve({ result, success: true });
299
299
  }
300
300
  catch (parseError) {
301
- const result = buildFailedResult(provider, scope, startedAt, "parse-failed", output, { parserResult: "failed" });
302
- emitProviderFailed(options.telemetryFile, scope.runId, scope.clusterId, provider.name, "parse-failed", output.exitCode);
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);
303
311
  resolve({ result, success: false });
304
312
  }
305
313
  });
@@ -42,6 +42,7 @@ exports.qcFailureReasonSchema = zod_1.z.enum([
42
42
  "nonzero-exit",
43
43
  "parse-failed",
44
44
  "empty-output",
45
+ "unusable-output",
45
46
  "unsupported-mode",
46
47
  "unavailable-provider",
47
48
  ]);
@@ -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
+ }