@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.
Files changed (36) hide show
  1. package/dist/autoresearch/index.js +30 -1
  2. package/dist/autoresearch/sol-evaluation-writer.js +135 -0
  3. package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
  4. package/dist/autoresearch/sol-recommendations.js +130 -0
  5. package/dist/autoresearch/sol-report-renderer.js +282 -0
  6. package/dist/autoresearch/sol-run-health-bridge.js +246 -0
  7. package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
  8. package/dist/cli/autoresearch.js +77 -0
  9. package/dist/cli/medic.js +65 -0
  10. package/dist/cluster-state/store.js +56 -0
  11. package/dist/config/defaults.js +3 -0
  12. package/dist/config/validator.js +141 -0
  13. package/dist/finalize/artifact-policy.js +2 -0
  14. package/dist/finalize/github.js +13 -9
  15. package/dist/finalize/index.js +231 -5
  16. package/dist/finalize/linear.js +8 -4
  17. package/dist/finalize/medic-gate.js +42 -0
  18. package/dist/finalize/steps/08-create-pr.js +2 -2
  19. package/dist/finalize/steps/11-update-linear.js +2 -2
  20. package/dist/loop/parent.js +234 -28
  21. package/dist/loop/worker-packet.js +19 -1
  22. package/dist/medic/run-health-consult.js +381 -0
  23. package/dist/medic/treatment-packets.js +169 -0
  24. package/dist/qc/artifacts.js +27 -0
  25. package/dist/qc/orchestration.js +3 -2
  26. package/dist/qc/providers/coderabbit.js +114 -11
  27. package/dist/qc/repair-loop.js +49 -14
  28. package/dist/qc/runner.js +10 -2
  29. package/dist/qc/schemas.js +1 -0
  30. package/dist/run-health/foreman-symptoms.js +100 -0
  31. package/dist/run-health/index.js +301 -0
  32. package/dist/run-health/qc-escalation.js +155 -0
  33. package/dist/run-health/schema.js +123 -0
  34. package/dist/types/sol-metrics.js +108 -0
  35. package/dist/types/sol-scorecard.js +148 -0
  36. 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
+ }