@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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SOL SmartDocs Markdown report renderer.
|
|
4
|
+
*
|
|
5
|
+
* Renders human-readable SOL evaluation reports from a run-level
|
|
6
|
+
* evaluation record, the computed scorecard set, and optional QC
|
|
7
|
+
* follow-up recommendations.
|
|
8
|
+
*
|
|
9
|
+
* Reports include:
|
|
10
|
+
* - Source references back to raw evidence
|
|
11
|
+
* - Per-subject confidence and availability
|
|
12
|
+
* - Skipped dimensions with reasons
|
|
13
|
+
* - Window / grouping metadata
|
|
14
|
+
* - Recommendation inputs for downstream routing advice
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.renderSolMarkdown = renderSolMarkdown;
|
|
18
|
+
// ──────────────────────────────────────────────
|
|
19
|
+
// Formatting helpers
|
|
20
|
+
// ──────────────────────────────────────────────
|
|
21
|
+
function fmtScore(v) {
|
|
22
|
+
return v !== null ? v.toFixed(4) : "N/A";
|
|
23
|
+
}
|
|
24
|
+
function fmtBoolean(v) {
|
|
25
|
+
if (v === null)
|
|
26
|
+
return "N/A";
|
|
27
|
+
return v ? "Yes" : "No";
|
|
28
|
+
}
|
|
29
|
+
function fmtList(items) {
|
|
30
|
+
if (!items || items.length === 0)
|
|
31
|
+
return "_None_";
|
|
32
|
+
return items.map((i) => `- ${i}`).join("\n");
|
|
33
|
+
}
|
|
34
|
+
function escapeCell(s) {
|
|
35
|
+
// Normalize line breaks so table cells remain valid Markdown.
|
|
36
|
+
return s.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
37
|
+
}
|
|
38
|
+
// ──────────────────────────────────────────────
|
|
39
|
+
// Subscore table
|
|
40
|
+
// ──────────────────────────────────────────────
|
|
41
|
+
function renderSubscoreTable(scorecard) {
|
|
42
|
+
const rows = scorecard.subscores.map((s) => {
|
|
43
|
+
const value = s.score !== null ? fmtScore(s.score) : "_skipped_";
|
|
44
|
+
const reason = s.skipped_reason ? ` (${s.skipped_reason})` : "";
|
|
45
|
+
const detail = s.detail ? ` ${s.detail}` : "";
|
|
46
|
+
return `| ${s.dimension} | ${value} | ${s.confidence} | ${escapeCell(reason + detail) || "—"} |`;
|
|
47
|
+
});
|
|
48
|
+
return [
|
|
49
|
+
"| Dimension | Score | Confidence | Notes |",
|
|
50
|
+
"|---|---|---|---|",
|
|
51
|
+
...rows,
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
54
|
+
// ──────────────────────────────────────────────
|
|
55
|
+
// Source references
|
|
56
|
+
// ──────────────────────────────────────────────
|
|
57
|
+
function renderSourceRefs(refs) {
|
|
58
|
+
const unique = new Map();
|
|
59
|
+
for (const ref of refs) {
|
|
60
|
+
unique.set(`${ref.kind}|${ref.path}`, ref);
|
|
61
|
+
}
|
|
62
|
+
const rows = Array.from(unique.values())
|
|
63
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
64
|
+
.map((r) => {
|
|
65
|
+
const status = r.available ? "available" : `missing${r.unavailable_reason ? `: ${r.unavailable_reason}` : ""}`;
|
|
66
|
+
return `| ${r.kind} | \`${r.path}\` | ${status} |`;
|
|
67
|
+
});
|
|
68
|
+
return [
|
|
69
|
+
"| Kind | Path | Status |",
|
|
70
|
+
"|---|---|---|",
|
|
71
|
+
...rows,
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
// ──────────────────────────────────────────────
|
|
75
|
+
// Recommendation inputs
|
|
76
|
+
// ──────────────────────────────────────────────
|
|
77
|
+
function renderRecommendationInputs(scorecard) {
|
|
78
|
+
const i = scorecard.recommendation_inputs;
|
|
79
|
+
const flags = [];
|
|
80
|
+
if (i.below_threshold)
|
|
81
|
+
flags.push("below threshold");
|
|
82
|
+
if (i.over_token_budget)
|
|
83
|
+
flags.push("over token budget");
|
|
84
|
+
if (i.intervention_detected)
|
|
85
|
+
flags.push("intervention detected");
|
|
86
|
+
if (i.router_issue_detected)
|
|
87
|
+
flags.push("router issue detected");
|
|
88
|
+
if (i.qc_issue_detected)
|
|
89
|
+
flags.push("QC issue detected");
|
|
90
|
+
const cells = [
|
|
91
|
+
scorecard.subject,
|
|
92
|
+
scorecard.subject_key,
|
|
93
|
+
flags.length > 0 ? flags.join(", ") : "—",
|
|
94
|
+
i.low_scoring_dimensions.join(", ") || "—",
|
|
95
|
+
i.skipped_dimensions.join(", ") || "—",
|
|
96
|
+
];
|
|
97
|
+
return `| ${cells.map(escapeCell).join(" | ")} |`;
|
|
98
|
+
}
|
|
99
|
+
function renderRecommendationInputsHeader() {
|
|
100
|
+
return [
|
|
101
|
+
"| Subject | Key | Flags | Low dimensions | Skipped dimensions |",
|
|
102
|
+
"|---|---|---|---|---|",
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
// ──────────────────────────────────────────────
|
|
106
|
+
// Scorecard detail section
|
|
107
|
+
// ──────────────────────────────────────────────
|
|
108
|
+
function renderScorecardDetail(scorecard) {
|
|
109
|
+
const lines = [];
|
|
110
|
+
lines.push(`### ${scorecard.subject}: ${scorecard.subject_key}`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
lines.push(`- **Availability:** ${scorecard.availability}${scorecard.availability_reason ? ` — ${scorecard.availability_reason}` : ""}`);
|
|
113
|
+
lines.push(`- **Aggregate score:** ${fmtScore(scorecard.aggregate_score)} (${scorecard.aggregate_confidence})`);
|
|
114
|
+
lines.push(`- **Aggregate formula:** \`${scorecard.aggregate_formula_version}\``);
|
|
115
|
+
lines.push(`- **Window:** run_id=${scorecard.window.run_id ?? "n/a"}, cluster_id=${scorecard.window.cluster_id ?? "n/a"}, sample_count=${scorecard.window.sample_count ?? "n/a"}`);
|
|
116
|
+
const grouping = Object.entries(scorecard.grouping_keys)
|
|
117
|
+
.filter(([, v]) => v !== undefined)
|
|
118
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
119
|
+
.join(", ");
|
|
120
|
+
lines.push(`- **Grouping keys:** ${grouping || "—"}`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(renderSubscoreTable(scorecard));
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push("**Source references**");
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push(renderSourceRefs(scorecard.source_refs));
|
|
127
|
+
lines.push("");
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Render a complete SOL evaluation report as Markdown.
|
|
132
|
+
*/
|
|
133
|
+
function renderSolMarkdown(record, scorecardSet, qcRecommendations) {
|
|
134
|
+
const allScorecards = [
|
|
135
|
+
scorecardSet.foreman,
|
|
136
|
+
...scorecardSet.workers,
|
|
137
|
+
...scorecardSet.providers,
|
|
138
|
+
...scorecardSet.models,
|
|
139
|
+
...scorecardSet.routing,
|
|
140
|
+
];
|
|
141
|
+
const skipped = allScorecards.flatMap((s) => s.subscores
|
|
142
|
+
.filter((sub) => sub.score === null && sub.skipped_reason)
|
|
143
|
+
.map((sub) => ({ subject: s.subject, key: s.subject_key, dimension: sub.dimension, reason: sub.skipped_reason })));
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`# SOL Evaluation Report: ${record.run_id}`);
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push(`**Generated:** ${record.generated_at}`);
|
|
148
|
+
lines.push(`**Scored at:** ${record.scored_at}`);
|
|
149
|
+
lines.push(`**Cluster:** ${record.cluster_id ?? "n/a"}`);
|
|
150
|
+
lines.push("");
|
|
151
|
+
// Run-level summary
|
|
152
|
+
lines.push("## Run summary");
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push(`| Metric | Value |`);
|
|
155
|
+
lines.push(`|---|---|`);
|
|
156
|
+
lines.push(`| Run composite score | ${fmtScore(record.report.run_composite_score)} |`);
|
|
157
|
+
lines.push(`| Foreman composite | ${fmtScore(record.report.foreman.composite_score)} (${record.report.foreman.composite_confidence}) |`);
|
|
158
|
+
lines.push(`| Worker count | ${Object.keys(record.report.workers).length} |`);
|
|
159
|
+
lines.push(`| Scorecards produced | ${allScorecards.length} |`);
|
|
160
|
+
lines.push("");
|
|
161
|
+
// Scorecard index
|
|
162
|
+
lines.push("## Scorecard index");
|
|
163
|
+
lines.push("");
|
|
164
|
+
lines.push("| Subject | Key | Aggregate | Confidence | Availability |");
|
|
165
|
+
lines.push("|---|---|---|---|---|");
|
|
166
|
+
for (const s of allScorecards) {
|
|
167
|
+
lines.push(`| ${s.subject} | ${escapeCell(s.subject_key)} | ${fmtScore(s.aggregate_score)} | ${s.aggregate_confidence} | ${s.availability} |`);
|
|
168
|
+
}
|
|
169
|
+
lines.push("");
|
|
170
|
+
// Foreman
|
|
171
|
+
lines.push("## Foreman");
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push(renderScorecardDetail(scorecardSet.foreman));
|
|
174
|
+
// Workers
|
|
175
|
+
if (scorecardSet.workers.length > 0) {
|
|
176
|
+
lines.push("## Workers");
|
|
177
|
+
lines.push("");
|
|
178
|
+
for (const s of scorecardSet.workers) {
|
|
179
|
+
lines.push(renderScorecardDetail(s));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Providers
|
|
183
|
+
if (scorecardSet.providers.length > 0) {
|
|
184
|
+
lines.push("## Providers");
|
|
185
|
+
lines.push("");
|
|
186
|
+
for (const s of scorecardSet.providers) {
|
|
187
|
+
lines.push(renderScorecardDetail(s));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Models
|
|
191
|
+
if (scorecardSet.models.length > 0) {
|
|
192
|
+
lines.push("## Models");
|
|
193
|
+
lines.push("");
|
|
194
|
+
for (const s of scorecardSet.models) {
|
|
195
|
+
lines.push(renderScorecardDetail(s));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Routing
|
|
199
|
+
if (scorecardSet.routing.length > 0) {
|
|
200
|
+
lines.push("## Routing decisions");
|
|
201
|
+
lines.push("");
|
|
202
|
+
for (const s of scorecardSet.routing) {
|
|
203
|
+
lines.push(renderScorecardDetail(s));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Token efficiency
|
|
207
|
+
lines.push("## Token efficiency");
|
|
208
|
+
lines.push("");
|
|
209
|
+
const qptScorecards = allScorecards.filter((s) => s.subscores.some((sub) => sub.dimension === "quality_per_token"));
|
|
210
|
+
if (qptScorecards.length === 0) {
|
|
211
|
+
lines.push("No quality-per-token subscores were computed (token evidence may be missing).");
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
lines.push("| Subject | Key | Quality / token | Confidence |");
|
|
215
|
+
lines.push("|---|---|---|---|");
|
|
216
|
+
for (const s of qptScorecards) {
|
|
217
|
+
const qpt = s.subscores.find((sub) => sub.dimension === "quality_per_token");
|
|
218
|
+
lines.push(`| ${s.subject} | ${escapeCell(s.subject_key)} | ${fmtScore(qpt?.score ?? null)} | ${qpt?.confidence ?? "none"} |`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
lines.push("");
|
|
222
|
+
// QC outcome
|
|
223
|
+
lines.push("## QC outcome");
|
|
224
|
+
lines.push("");
|
|
225
|
+
const foremanQc = scorecardSet.foreman.subscores.find((s) => s.dimension === "qc_repair_loop");
|
|
226
|
+
if (!foremanQc) {
|
|
227
|
+
lines.push("No QC repair-loop subscore available.");
|
|
228
|
+
}
|
|
229
|
+
else if (foremanQc.score === null) {
|
|
230
|
+
lines.push(`QC repair-loop dimension skipped: ${foremanQc.skipped_reason ?? "no reason given"}.`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
lines.push(`QC repair-loop score: **${fmtScore(foremanQc.score)}** (${foremanQc.confidence})${foremanQc.detail ? ` — ${escapeCell(foremanQc.detail)}` : ""}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push("");
|
|
236
|
+
if (qcRecommendations && qcRecommendations.recommendations.length > 0) {
|
|
237
|
+
lines.push("### QC follow-up recommendations");
|
|
238
|
+
lines.push("");
|
|
239
|
+
for (const r of qcRecommendations.recommendations) {
|
|
240
|
+
lines.push(`- **[${r.action_type}] ${r.id}** — ${escapeCell(r.proposed_action)} (${(r.confidence * 100).toFixed(1)}% confidence)`);
|
|
241
|
+
lines.push(` - Rationale: ${escapeCell(r.rationale)}`);
|
|
242
|
+
}
|
|
243
|
+
lines.push("");
|
|
244
|
+
}
|
|
245
|
+
// Recommendation inputs
|
|
246
|
+
lines.push("## Recommendation inputs");
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push(renderRecommendationInputsHeader());
|
|
249
|
+
for (const s of allScorecards) {
|
|
250
|
+
lines.push(renderRecommendationInputs(s));
|
|
251
|
+
}
|
|
252
|
+
lines.push("");
|
|
253
|
+
// Skipped evidence
|
|
254
|
+
lines.push("## Skipped evidence");
|
|
255
|
+
lines.push("");
|
|
256
|
+
if (skipped.length === 0) {
|
|
257
|
+
lines.push("All dimensions were scored; no evidence was skipped.");
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
lines.push("| Subject | Key | Dimension | Reason |");
|
|
261
|
+
lines.push("|---|---|---|---|");
|
|
262
|
+
for (const row of skipped) {
|
|
263
|
+
lines.push(`| ${row.subject} | ${escapeCell(row.key)} | ${row.dimension} | ${escapeCell(row.reason)} |`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
lines.push("");
|
|
267
|
+
// Source references (aggregated)
|
|
268
|
+
lines.push("## Source references");
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push(renderSourceRefs(allScorecards.flatMap((s) => s.source_refs)));
|
|
271
|
+
lines.push("");
|
|
272
|
+
const markdown = lines.join("\n");
|
|
273
|
+
return {
|
|
274
|
+
markdown,
|
|
275
|
+
summary: {
|
|
276
|
+
run_id: record.run_id,
|
|
277
|
+
aggregate_score: record.report.run_composite_score,
|
|
278
|
+
scorecard_count: allScorecards.length,
|
|
279
|
+
skipped_dimensions: skipped.length,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SOL → run-health bridge.
|
|
4
|
+
*
|
|
5
|
+
* Evaluates SOL score reports against operator-configured thresholds and,
|
|
6
|
+
* when policy enables it, appends run-health symptoms so that Medic can be
|
|
7
|
+
* engaged even when no worker explicitly wrote symptoms.
|
|
8
|
+
*
|
|
9
|
+
* Design rules:
|
|
10
|
+
* - SOL is ADVISORY by default. No symptoms are created unless the operator
|
|
11
|
+
* sets `sol.thresholds.enabled = true` AND at least one policy flag
|
|
12
|
+
* (`createRunHealthReport` or `requireMedic`) is true.
|
|
13
|
+
* - Symptoms reference the SOL evidence file as their source; they do NOT
|
|
14
|
+
* mutate raw metric artifacts.
|
|
15
|
+
* - Each threshold crossing produces exactly one symptom per evaluation.
|
|
16
|
+
* Re-evaluating the same run does not duplicate symptoms because the
|
|
17
|
+
* symptom id encodes the threshold name.
|
|
18
|
+
* - Never throws — threshold evaluation must not block the run.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.detectSolThresholdCrossings = detectSolThresholdCrossings;
|
|
22
|
+
exports.evaluateSolThresholds = evaluateSolThresholds;
|
|
23
|
+
const index_js_1 = require("../run-health/index.js");
|
|
24
|
+
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
25
|
+
const DEFAULT_LOW_COMPOSITE_SCORE = 0.4;
|
|
26
|
+
const DEFAULT_QC_REPAIR_LOOP_FAILURE_STATUSES = [
|
|
27
|
+
"max-rounds",
|
|
28
|
+
"medic-referral",
|
|
29
|
+
"all-providers-failed",
|
|
30
|
+
];
|
|
31
|
+
const DEFAULT_REPEATED_PROVIDER_FAILURES = 3;
|
|
32
|
+
const DEFAULT_FOREMAN_INTERVENTION_COUNT = 2;
|
|
33
|
+
const DEFAULT_STALE_WRONG_RUN_TELEMETRY = true;
|
|
34
|
+
const DEFAULT_VALIDATION_FAILURES = 2;
|
|
35
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
36
|
+
function makeSolSourceActor() {
|
|
37
|
+
return { role: "sol" };
|
|
38
|
+
}
|
|
39
|
+
function makeSolSymptom(crossing) {
|
|
40
|
+
return {
|
|
41
|
+
// Deterministic id: code only (no UUID) so repeated evaluations of the
|
|
42
|
+
// same run are idempotent — the same threshold crossing produces the
|
|
43
|
+
// same id, and a caller that deduplicates by id will skip the duplicate.
|
|
44
|
+
// ponytail: when run-health supports upsert-by-id, switch to that.
|
|
45
|
+
id: `sol:${crossing.code}`,
|
|
46
|
+
severity: solThresholdSeverity(crossing.code),
|
|
47
|
+
code: crossing.code,
|
|
48
|
+
message: crossing.message,
|
|
49
|
+
source_actor: makeSolSourceActor(),
|
|
50
|
+
evidence_refs: crossing.evidenceRefs,
|
|
51
|
+
occurred_at: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function solThresholdSeverity(code) {
|
|
55
|
+
switch (code) {
|
|
56
|
+
case "sol-qc-repair-loop-failure":
|
|
57
|
+
case "sol-repeated-provider-failures":
|
|
58
|
+
return "high";
|
|
59
|
+
case "sol-low-composite-score":
|
|
60
|
+
case "sol-high-foreman-intervention":
|
|
61
|
+
case "sol-validation-failures":
|
|
62
|
+
return "medium";
|
|
63
|
+
case "sol-stale-wrong-run-telemetry":
|
|
64
|
+
return "high";
|
|
65
|
+
default:
|
|
66
|
+
return "medium";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function upsertSolSymptom(runId, clusterId, symptom, repoRoot) {
|
|
70
|
+
const existing = (0, index_js_1.readRunHealthReport)(runId, repoRoot);
|
|
71
|
+
if (!existing) {
|
|
72
|
+
(0, index_js_1.createRunHealthReport)({
|
|
73
|
+
runId,
|
|
74
|
+
clusterId,
|
|
75
|
+
firstSymptom: symptom,
|
|
76
|
+
sourceActor: makeSolSourceActor(),
|
|
77
|
+
repoRoot,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Deduplicate: skip if a symptom with the same id already exists
|
|
82
|
+
if (existing.symptoms.some((s) => s.id === symptom.id)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
(0, index_js_1.appendSymptom)(runId, symptom, repoRoot);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ── Threshold evaluation ──────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Detect which thresholds are crossed given the SOL score report.
|
|
91
|
+
* Returns an array of crossing descriptors without any I/O side effects.
|
|
92
|
+
*/
|
|
93
|
+
function detectSolThresholdCrossings(scoreReport, thresholdsConfig, evidencePaths) {
|
|
94
|
+
const crossings = [];
|
|
95
|
+
const lowCompositeThreshold = thresholdsConfig.low_composite_score ?? DEFAULT_LOW_COMPOSITE_SCORE;
|
|
96
|
+
const qcFailureStatuses = thresholdsConfig.qc_repair_loop_failure_statuses ?? DEFAULT_QC_REPAIR_LOOP_FAILURE_STATUSES;
|
|
97
|
+
const providerFailureThreshold = thresholdsConfig.repeated_provider_failures ?? DEFAULT_REPEATED_PROVIDER_FAILURES;
|
|
98
|
+
const interventionThreshold = thresholdsConfig.foreman_intervention_count ?? DEFAULT_FOREMAN_INTERVENTION_COUNT;
|
|
99
|
+
const checkStaleWrongRun = thresholdsConfig.stale_wrong_run_telemetry ?? DEFAULT_STALE_WRONG_RUN_TELEMETRY;
|
|
100
|
+
const validationFailureThreshold = thresholdsConfig.validation_failures ?? DEFAULT_VALIDATION_FAILURES;
|
|
101
|
+
// 1. Low composite score
|
|
102
|
+
if (scoreReport.run_composite_score !== null &&
|
|
103
|
+
scoreReport.run_composite_score < lowCompositeThreshold) {
|
|
104
|
+
crossings.push({
|
|
105
|
+
code: "sol-low-composite-score",
|
|
106
|
+
message: `SOL run composite score ${scoreReport.run_composite_score.toFixed(4)} is below ` +
|
|
107
|
+
`configured threshold ${lowCompositeThreshold} — run health may be degraded.`,
|
|
108
|
+
evidenceRefs: evidencePaths,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// 2. QC repair-loop failure state
|
|
112
|
+
const foremanReport = scoreReport.foreman;
|
|
113
|
+
const qcRepairLoopDetail = foremanReport.qc_repair_loop?.detail ?? "";
|
|
114
|
+
for (const status of qcFailureStatuses) {
|
|
115
|
+
if (qcRepairLoopDetail.includes(`status=${status}`)) {
|
|
116
|
+
crossings.push({
|
|
117
|
+
code: "sol-qc-repair-loop-failure",
|
|
118
|
+
message: `SOL evidence shows QC repair loop reached failure status "${status}" — ` +
|
|
119
|
+
`Medic consultation is recommended.`,
|
|
120
|
+
evidenceRefs: evidencePaths,
|
|
121
|
+
});
|
|
122
|
+
break; // One symptom per run for QC repair-loop failures
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Also detect via qc_repair_loop score == 0
|
|
126
|
+
if (foremanReport.qc_repair_loop?.score !== null &&
|
|
127
|
+
foremanReport.qc_repair_loop?.score === 0 &&
|
|
128
|
+
crossings.every((c) => c.code !== "sol-qc-repair-loop-failure")) {
|
|
129
|
+
crossings.push({
|
|
130
|
+
code: "sol-qc-repair-loop-failure",
|
|
131
|
+
message: `SOL evidence shows QC repair loop score 0.0 (worst outcome) — ` +
|
|
132
|
+
`Medic consultation is recommended.`,
|
|
133
|
+
evidenceRefs: evidencePaths,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// 3. Repeated provider failures — inferred from worker composite scores ≤ 0
|
|
137
|
+
// and workers_failed count (available through foreman scores proxy).
|
|
138
|
+
const failedWorkers = Object.values(scoreReport.workers).filter((w) => w.composite_score !== null && w.composite_score <= 0);
|
|
139
|
+
if (failedWorkers.length >= providerFailureThreshold) {
|
|
140
|
+
crossings.push({
|
|
141
|
+
code: "sol-repeated-provider-failures",
|
|
142
|
+
message: `SOL evidence shows ${failedWorkers.length} worker(s) with zero composite score ` +
|
|
143
|
+
`(threshold: ${providerFailureThreshold}) — possible repeated provider failures.`,
|
|
144
|
+
evidenceRefs: evidencePaths,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// 4. High Foreman intervention count
|
|
148
|
+
// intervention.score: 0.0 = user_intervened, 0.5 = foreman_intervened
|
|
149
|
+
const interventionScore = foremanReport.intervention?.score;
|
|
150
|
+
if (interventionScore !== null && interventionScore !== undefined) {
|
|
151
|
+
// pre_analysis dimension uses escalation_events as a proxy
|
|
152
|
+
// intervention score < 0.5 means foreman_intervened; 0.0 means user_intervened
|
|
153
|
+
// We use the foreman.pre_analysis detail to extract escalation_events
|
|
154
|
+
const preAnalysisDetail = foremanReport.pre_analysis?.detail ?? "";
|
|
155
|
+
const escalationMatch = /escalation_events=(\d+)/.exec(preAnalysisDetail);
|
|
156
|
+
const escalationEvents = escalationMatch ? parseInt(escalationMatch[1], 10) : 0;
|
|
157
|
+
if (escalationEvents > interventionThreshold) {
|
|
158
|
+
crossings.push({
|
|
159
|
+
code: "sol-high-foreman-intervention",
|
|
160
|
+
message: `SOL evidence shows ${escalationEvents} escalation event(s) ` +
|
|
161
|
+
`(threshold: ${interventionThreshold}) — possible Foreman intervention loop.`,
|
|
162
|
+
evidenceRefs: evidencePaths,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 5. Stale / wrong-run telemetry
|
|
167
|
+
// Detected via foreman.dispatch and foreman.duration dimension details
|
|
168
|
+
if (checkStaleWrongRun) {
|
|
169
|
+
const dispatchDetail = foremanReport.dispatch?.detail ?? "";
|
|
170
|
+
const durationDetail = foremanReport.duration?.detail ?? "";
|
|
171
|
+
const hasRedispatch = /redispatched=\d+\//.exec(dispatchDetail);
|
|
172
|
+
const redispatchCount = hasRedispatch
|
|
173
|
+
? parseInt(/redispatched=(\d+)/.exec(dispatchDetail)?.[1] ?? "0", 10)
|
|
174
|
+
: 0;
|
|
175
|
+
// dispatch_epoch >> continue_epoch suggests wrong-run telemetry
|
|
176
|
+
const dispatchEpochMatch = /dispatch_epoch=(\d+)/.exec(durationDetail);
|
|
177
|
+
const continueEpochMatch = /continue_epoch=(\d+)/.exec(durationDetail);
|
|
178
|
+
const dispatchEpoch = dispatchEpochMatch ? parseInt(dispatchEpochMatch[1], 10) : 0;
|
|
179
|
+
const continueEpoch = continueEpochMatch ? parseInt(continueEpochMatch[1], 10) : 0;
|
|
180
|
+
const epochOverhead = dispatchEpoch > 0 ? dispatchEpoch - (continueEpoch + 1) : 0;
|
|
181
|
+
if (redispatchCount >= providerFailureThreshold || epochOverhead >= 3) {
|
|
182
|
+
crossings.push({
|
|
183
|
+
code: "sol-stale-wrong-run-telemetry",
|
|
184
|
+
message: `SOL evidence suggests stale or wrong-run telemetry: ` +
|
|
185
|
+
`redispatched=${redispatchCount}, epoch_overhead=${epochOverhead}. ` +
|
|
186
|
+
`This pattern is consistent with the POL-509 wrong-run failure mode.`,
|
|
187
|
+
evidenceRefs: evidencePaths,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 6. Validation failures
|
|
192
|
+
const validationFailedWorkers = Object.values(scoreReport.workers).filter((w) => w.validation?.score !== null && w.validation?.score === 0);
|
|
193
|
+
if (validationFailedWorkers.length >= validationFailureThreshold) {
|
|
194
|
+
crossings.push({
|
|
195
|
+
code: "sol-validation-failures",
|
|
196
|
+
message: `SOL evidence shows ${validationFailedWorkers.length} worker(s) with validation failures ` +
|
|
197
|
+
`(threshold: ${validationFailureThreshold}).`,
|
|
198
|
+
evidenceRefs: evidencePaths,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return crossings;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Evaluate SOL score thresholds and, when policy enables it, append
|
|
205
|
+
* run-health symptoms for each crossing.
|
|
206
|
+
*
|
|
207
|
+
* Advisory only by default. To activate run-health writes:
|
|
208
|
+
* `sol.thresholds.enabled = true` AND one of:
|
|
209
|
+
* `sol.thresholds.policy.createRunHealthReport = true`
|
|
210
|
+
* `sol.thresholds.policy.requireMedic = true`
|
|
211
|
+
*
|
|
212
|
+
* Never throws — threshold evaluation must not block the run.
|
|
213
|
+
*/
|
|
214
|
+
function evaluateSolThresholds(params) {
|
|
215
|
+
const { runId, clusterId, scoreReport, evidencePaths = [], thresholdsConfig, repoRoot } = params;
|
|
216
|
+
const crossings = detectSolThresholdCrossings(scoreReport, thresholdsConfig, evidencePaths);
|
|
217
|
+
const policyEnabled = thresholdsConfig.enabled === true;
|
|
218
|
+
const createReport = thresholdsConfig.policy?.createRunHealthReport === true;
|
|
219
|
+
const requireMedic = thresholdsConfig.policy?.requireMedic === true;
|
|
220
|
+
const shouldWrite = policyEnabled && (createReport || requireMedic);
|
|
221
|
+
if (!shouldWrite || crossings.length === 0) {
|
|
222
|
+
return { crossings, symptomsAppended: 0, medicRequired: false };
|
|
223
|
+
}
|
|
224
|
+
let symptomsAppended = 0;
|
|
225
|
+
let medicDecisionWritten = false;
|
|
226
|
+
try {
|
|
227
|
+
for (const crossing of crossings) {
|
|
228
|
+
upsertSolSymptom(runId, clusterId, makeSolSymptom(crossing), repoRoot);
|
|
229
|
+
symptomsAppended++;
|
|
230
|
+
}
|
|
231
|
+
// If requireMedic is true, set medic_consult to pending so finalize gate fires.
|
|
232
|
+
if (requireMedic && crossings.length > 0) {
|
|
233
|
+
(0, index_js_1.markMedicDecision)(runId, { status: "pending" }, repoRoot);
|
|
234
|
+
medicDecisionWritten = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Threshold evaluation must never block the run.
|
|
239
|
+
// ponytail: surface write errors to telemetry in a future pass
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
crossings,
|
|
243
|
+
symptomsAppended,
|
|
244
|
+
medicRequired: medicDecisionWritten,
|
|
245
|
+
};
|
|
246
|
+
}
|