@lsctech/polaris 0.5.7 → 0.5.9
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/sol-evaluation-writer.js +135 -0
- package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
- package/dist/autoresearch/sol-recommendations.js +130 -0
- package/dist/autoresearch/sol-report-renderer.js +282 -0
- package/dist/autoresearch/sol-run-health-bridge.js +246 -0
- package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cli/medic.js +65 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/config/defaults.js +3 -0
- package/dist/config/validator.js +141 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +231 -5
- package/dist/finalize/linear.js +8 -4
- package/dist/finalize/medic-gate.js +42 -0
- 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 +234 -28
- package/dist/loop/worker-packet.js +19 -1
- package/dist/medic/run-health-consult.js +381 -0
- package/dist/medic/treatment-packets.js +169 -0
- package/dist/qc/artifacts.js +27 -0
- package/dist/qc/orchestration.js +3 -2
- package/dist/qc/providers/coderabbit.js +114 -11
- package/dist/qc/repair-loop.js +49 -14
- package/dist/qc/runner.js +10 -2
- package/dist/qc/schemas.js +1 -0
- package/dist/run-health/foreman-symptoms.js +100 -0
- package/dist/run-health/index.js +301 -0
- package/dist/run-health/qc-escalation.js +155 -0
- package/dist/run-health/schema.js +123 -0
- package/dist/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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("--
|
|
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", "--
|
|
336
|
+
args: ["review", "--agent", "--base", baseRef],
|
|
234
337
|
};
|
|
235
338
|
}
|
|
236
339
|
parse(output) {
|
package/dist/qc/repair-loop.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
302
|
-
|
|
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
|
});
|
package/dist/qc/schemas.js
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Foreman-side runtime symptom emission.
|
|
4
|
+
*
|
|
5
|
+
* The Foreman appends symptoms ONLY for Polaris-runtime intervention events
|
|
6
|
+
* (state repair, cluster repair, missing packet/result repair, dispatch
|
|
7
|
+
* boundary repair, QC/finalize runtime failure, local/global binary mismatch,
|
|
8
|
+
* and manual intervention).
|
|
9
|
+
*
|
|
10
|
+
* Symptoms are NOT emitted for non-Polaris target repositories unless
|
|
11
|
+
* `run_health.foreman_symptoms.enabled` is explicitly set to true in
|
|
12
|
+
* polaris.config.json.
|
|
13
|
+
*
|
|
14
|
+
* QC providers remain unchanged artifact producers — they never write here.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.isForemanSymptomEnabled = isForemanSymptomEnabled;
|
|
18
|
+
exports.appendForemanSymptom = appendForemanSymptom;
|
|
19
|
+
const node_crypto_1 = require("node:crypto");
|
|
20
|
+
const index_js_1 = require("./index.js");
|
|
21
|
+
// ── Policy ────────────────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Returns true when Foreman-side symptom emission is enabled.
|
|
24
|
+
*
|
|
25
|
+
* Emission is enabled when `run_health.foreman_symptoms.enabled === true` in
|
|
26
|
+
* the project's polaris.config.json. Foreman symptoms are disabled by default
|
|
27
|
+
* so that non-Polaris target repositories are not automatically marked sick.
|
|
28
|
+
*/
|
|
29
|
+
function isForemanSymptomEnabled(config) {
|
|
30
|
+
return config?.run_health?.foreman_symptoms?.enabled === true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Append a Foreman-side runtime symptom to the run-health report.
|
|
34
|
+
*
|
|
35
|
+
* Creates the report when one does not yet exist; appends to it when one does.
|
|
36
|
+
* When `config` is provided, checks `isForemanSymptomEnabled(config)` and
|
|
37
|
+
* no-ops when disabled (so the caller does not need to guard).
|
|
38
|
+
* Never throws — symptom emission is advisory and must not block the run.
|
|
39
|
+
*/
|
|
40
|
+
function appendForemanSymptom(params) {
|
|
41
|
+
const { runId, clusterId, code, message, evidenceRefs, repoRoot, config } = params;
|
|
42
|
+
// Policy gate: skip when config is provided but policy is not enabled.
|
|
43
|
+
if (config !== undefined && !isForemanSymptomEnabled(config)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const sourceActor = {
|
|
47
|
+
role: "foreman",
|
|
48
|
+
};
|
|
49
|
+
const symptom = {
|
|
50
|
+
id: `foreman:${code}:${(0, node_crypto_1.randomUUID)()}`,
|
|
51
|
+
severity: foremanCodeSeverity(code),
|
|
52
|
+
code,
|
|
53
|
+
message,
|
|
54
|
+
source_actor: sourceActor,
|
|
55
|
+
evidence_refs: evidenceRefs ?? [],
|
|
56
|
+
occurred_at: new Date().toISOString(),
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const existing = (0, index_js_1.readRunHealthReport)(runId, repoRoot);
|
|
60
|
+
if (!existing) {
|
|
61
|
+
(0, index_js_1.createRunHealthReport)({
|
|
62
|
+
runId,
|
|
63
|
+
clusterId,
|
|
64
|
+
firstSymptom: symptom,
|
|
65
|
+
sourceActor,
|
|
66
|
+
repoRoot,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
(0, index_js_1.appendSymptom)(runId, symptom, repoRoot);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Symptom emission must never block the run — swallow errors silently.
|
|
75
|
+
// ponytail: surface to telemetry in a future pass
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Maps a Foreman symptom code to its default severity.
|
|
80
|
+
*/
|
|
81
|
+
function foremanCodeSeverity(code) {
|
|
82
|
+
switch (code) {
|
|
83
|
+
case "foreman-dispatch-boundary-repair":
|
|
84
|
+
return "critical";
|
|
85
|
+
case "foreman-state-repair":
|
|
86
|
+
case "foreman-cluster-repair":
|
|
87
|
+
case "foreman-packet-repair":
|
|
88
|
+
case "foreman-qc-runtime-failure":
|
|
89
|
+
case "foreman-medic-runtime-failure":
|
|
90
|
+
case "foreman-binary-mismatch":
|
|
91
|
+
case "foreman-wrong-run-telemetry":
|
|
92
|
+
return "high";
|
|
93
|
+
case "foreman-finalize-recovery":
|
|
94
|
+
return "medium";
|
|
95
|
+
case "foreman-manual-intervention":
|
|
96
|
+
return "low";
|
|
97
|
+
default:
|
|
98
|
+
return "medium";
|
|
99
|
+
}
|
|
100
|
+
}
|