@lsctech/polaris 0.5.5 → 0.5.7

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.
@@ -0,0 +1,382 @@
1
+ "use strict";
2
+ /**
3
+ * SOL recommendation engine.
4
+ *
5
+ * Generates explainable routing recommendations and Polaris-specific
6
+ * self-improvement proposals from historical SOL score snapshots.
7
+ *
8
+ * Design rules:
9
+ * - Output is advisory by default; no tracker or filesystem mutation happens
10
+ * during recommendation generation.
11
+ * - Filing tracker issues requires an explicit opt-in and is gated to the
12
+ * Polaris development context (see src/cli/autoresearch.ts).
13
+ * - Each recommendation carries evidence references, affected routing
14
+ * dimensions, confidence, and a proposed policy action.
15
+ * - Deterministic: same snapshot input produces the same recommendation set.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.generateRecommendations = generateRecommendations;
19
+ exports.recommendationToProposal = recommendationToProposal;
20
+ exports.recommendationsToProposals = recommendationsToProposals;
21
+ exports.formatRecommendationsCli = formatRecommendationsCli;
22
+ exports.generateQcRecommendations = generateQcRecommendations;
23
+ exports.formatQcRecommendations = formatQcRecommendations;
24
+ const sol_report_js_1 = require("./sol-report.js");
25
+ // ──────────────────────────────────────────────
26
+ // Constants and helpers
27
+ // ──────────────────────────────────────────────
28
+ const DEFAULT_THRESHOLD = 0.7;
29
+ const DEFAULT_MIN_SAMPLES = 2;
30
+ function clamp01(v) {
31
+ return Math.max(0, Math.min(1, v));
32
+ }
33
+ function parseGroupKey(groupKey) {
34
+ const affected = {};
35
+ for (const part of groupKey.split("|")) {
36
+ const eq = part.indexOf("=");
37
+ if (eq === -1)
38
+ continue;
39
+ const key = part.slice(0, eq);
40
+ const value = part.slice(eq + 1);
41
+ if (!value || value === "unknown")
42
+ continue;
43
+ if (key === "route")
44
+ affected.route = value;
45
+ else if (key === "task_type")
46
+ affected.task_type = value;
47
+ else if (key === "role")
48
+ affected.role = value;
49
+ else if (key === "provider")
50
+ affected.provider = value;
51
+ else if (key === "model")
52
+ affected.model = value;
53
+ }
54
+ return affected;
55
+ }
56
+ function snapshotGroupKey(snapshot, dimension) {
57
+ const keys = snapshot.grouping_keys;
58
+ switch (dimension) {
59
+ case "repo":
60
+ return keys.repo ?? "unknown";
61
+ case "route":
62
+ return keys.route ?? "unknown";
63
+ case "task_type":
64
+ return keys.task_type ?? "unknown";
65
+ case "role":
66
+ return keys.role ?? "unknown";
67
+ case "risk":
68
+ return keys.risk ?? "unknown";
69
+ case "provider":
70
+ return keys.provider ?? "unknown";
71
+ case "model":
72
+ return keys.model ?? "unknown";
73
+ case "worker_id":
74
+ return snapshot.worker_ids.length > 0 ? snapshot.worker_ids.join(",") : "unknown";
75
+ case "run_id":
76
+ return snapshot.report.run_id;
77
+ case "time_window": {
78
+ const d = new Date(snapshot.report.scored_at);
79
+ if (isNaN(d.getTime()))
80
+ return "unknown";
81
+ // Default windowDays = 7 to match generateReport default.
82
+ const WINDOW_DAYS = 7;
83
+ const daysSinceEpoch = Math.floor(d.getTime() / (86400000 * WINDOW_DAYS));
84
+ const bucketStart = new Date(daysSinceEpoch * 86400000 * WINDOW_DAYS);
85
+ return bucketStart.toISOString().slice(0, 10);
86
+ }
87
+ }
88
+ }
89
+ function groupValueFromLabel(label, dimension) {
90
+ for (const part of label.split("|")) {
91
+ const eq = part.indexOf("=");
92
+ if (eq === -1)
93
+ continue;
94
+ const key = part.slice(0, eq);
95
+ const value = part.slice(eq + 1);
96
+ if (key === dimension)
97
+ return value ?? "unknown";
98
+ }
99
+ return "unknown";
100
+ }
101
+ function isWorkerSignal(evidence) {
102
+ const workerMean = evidence.mean_worker_composite ?? null;
103
+ const foremanMean = evidence.mean_foreman_composite ?? null;
104
+ if (workerMean === null || foremanMean === null)
105
+ return true; // default to worker-facing
106
+ return workerMean <= foremanMean;
107
+ }
108
+ function buildProposedAction(dimension, affected, evidence) {
109
+ const target = affected.provider ?? affected.model ?? affected.role ?? affected.route ?? affected.task_type ?? dimension;
110
+ const workerFocus = isWorkerSignal(evidence);
111
+ switch (dimension) {
112
+ case "provider":
113
+ return `Review provider eligibility and role assignment for provider '${target}'; consider updating execution.providerPolicy or provider trust/cost thresholds.`;
114
+ case "model":
115
+ return `Review model selection for model '${target}'; update role model mapping if the trend persists.`;
116
+ case "role":
117
+ return `Review role assignment for role '${target}'; verify provider capabilities, packet scope, and routing policy.`;
118
+ case "route":
119
+ return workerFocus
120
+ ? `Analyze route health for '${target}'; inspect worker templates and validation commands.`
121
+ : `Analyze route health for '${target}'; inspect scoring rules and foreman/runtime configuration.`;
122
+ case "task_type":
123
+ return `Review task-type routing for '${target}'; verify provider capabilities and role mapping.`;
124
+ case "risk":
125
+ return `Review trust/cost thresholds for risk tier '${target}'; adjust policy filters if this tier is consistently underperforming.`;
126
+ case "worker_id":
127
+ return `Review worker behavior for '${target}'; consider tightening packet scope or validation commands.`;
128
+ default:
129
+ return `Investigate ${workerFocus ? "worker" : "runtime"} performance for '${target}' and update the relevant ${workerFocus ? "worker template/policy" : "runtime config/scoring rules"}.`;
130
+ }
131
+ }
132
+ function categoryFor(dimension) {
133
+ const map = {
134
+ provider: "provider_policy",
135
+ model: "provider_policy",
136
+ role: "role_assignment",
137
+ route: "routing",
138
+ task_type: "routing",
139
+ repo: "routing",
140
+ risk: "trust_threshold",
141
+ };
142
+ return map[dimension] ?? "runtime_improvement";
143
+ }
144
+ function actionTypeFor(evidence) {
145
+ return isWorkerSignal(evidence) ? "implement" : "analyze";
146
+ }
147
+ function artifactTypeFor(dimension, evidence) {
148
+ const workerFocus = isWorkerSignal(evidence);
149
+ switch (dimension) {
150
+ case "provider":
151
+ case "model":
152
+ return "provider-role-recommendation";
153
+ case "route":
154
+ return workerFocus ? "worker-template" : "scoring-rule";
155
+ case "task_type":
156
+ case "role":
157
+ case "risk":
158
+ return workerFocus ? "worker-template" : "runtime-config";
159
+ case "repo":
160
+ case "worker_id":
161
+ case "run_id":
162
+ case "time_window":
163
+ default:
164
+ return workerFocus ? "worker-template" : "runtime-config";
165
+ }
166
+ }
167
+ function confidenceFor(mean, threshold, count) {
168
+ const gap = Math.max(0, threshold - mean);
169
+ const sampleBoost = Math.min(count / 10, 0.2);
170
+ return clamp01(gap + sampleBoost);
171
+ }
172
+ // ──────────────────────────────────────────────
173
+ // Recommendation generation
174
+ // ──────────────────────────────────────────────
175
+ /**
176
+ * Scan historical SOL snapshots for underperforming groups and emit
177
+ * review-gated recommendations.
178
+ *
179
+ * The function is pure: it does not read files, call APIs, or mutate inputs.
180
+ */
181
+ function generateRecommendations(snapshots, options = {}) {
182
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
183
+ const minSamples = options.minSamples ?? DEFAULT_MIN_SAMPLES;
184
+ const dimensions = options.groupBy ?? ["provider", "model", "role", "route", "task_type"];
185
+ const recommendations = [];
186
+ for (const dim of dimensions) {
187
+ const report = (0, sol_report_js_1.generateReport)(snapshots, { groupBy: [dim] });
188
+ for (const group of report.groups) {
189
+ const mean = group.mean_composite;
190
+ if (mean === null || mean >= threshold || group.count < minSamples)
191
+ continue;
192
+ const affected = parseGroupKey(group.group_key);
193
+ const evidence = {
194
+ group_key: group.group_key,
195
+ grouped_by: [dim],
196
+ count: group.count,
197
+ mean_composite: group.mean_composite,
198
+ min_composite: group.min_composite,
199
+ max_composite: group.max_composite,
200
+ mean_foreman_composite: group.mean_foreman_composite,
201
+ mean_worker_composite: group.mean_worker_composite,
202
+ run_ids: [
203
+ ...new Set(snapshots
204
+ .filter((s) => snapshotGroupKey(s, dim) === groupValueFromLabel(group.group_key, dim))
205
+ .map((s) => s.report.run_id)),
206
+ ].sort(),
207
+ };
208
+ recommendations.push({
209
+ id: `${dim}:${group.group_key}`,
210
+ category: categoryFor(dim),
211
+ action_type: actionTypeFor(evidence),
212
+ affected,
213
+ proposed_action: buildProposedAction(dim, affected, evidence),
214
+ confidence: confidenceFor(mean, threshold, group.count),
215
+ evidence,
216
+ rationale: `Mean composite score ${mean.toFixed(4)} is below threshold ${threshold.toFixed(2)} across ${group.count} snapshots.`,
217
+ });
218
+ }
219
+ }
220
+ // Deterministic ordering: highest confidence first, then stable id sort.
221
+ recommendations.sort((a, b) => b.confidence - a.confidence || a.id.localeCompare(b.id));
222
+ return {
223
+ generated_at: new Date().toISOString(),
224
+ total_snapshots: snapshots.length,
225
+ threshold,
226
+ min_samples: minSamples,
227
+ recommendations,
228
+ };
229
+ }
230
+ // ──────────────────────────────────────────────
231
+ // Proposal conversion (tracker filing)
232
+ // ──────────────────────────────────────────────
233
+ /**
234
+ * Convert a single recommendation into an AutresearchProposal suitable for
235
+ * `routeProposals()`. This is the bridge from advisory SOL output to
236
+ * review-gated tracker issues.
237
+ */
238
+ function recommendationToProposal(recommendation, runId) {
239
+ const { evidence, affected, proposed_action, confidence, id, action_type, rationale } = recommendation;
240
+ const dim = evidence.grouped_by[0] ?? "run_id";
241
+ const artifactType = artifactTypeFor(dim, evidence);
242
+ const affectedLabel = affected.provider ?? affected.model ?? affected.role ?? affected.route ?? affected.task_type ?? dim;
243
+ const gateId = `sol-recommendation:${id}`;
244
+ return {
245
+ gate_id: gateId,
246
+ artifact_type: artifactType,
247
+ hint: `[${action_type}] ${proposed_action}\n\nRationale: ${rationale}\nEvidence: group=${evidence.group_key}, mean=${evidence.mean_composite?.toFixed(4) ?? "N/A"}, count=${evidence.count}, run_ids=${evidence.run_ids.join(",")}`,
248
+ run_id: runId ?? evidence.run_ids[0] ?? "sol-history",
249
+ evidence_run_ids: evidence.run_ids,
250
+ confidence,
251
+ fix_zone: `${artifactType}/${gateId}`,
252
+ };
253
+ }
254
+ /**
255
+ * Convert recommendations to tracker proposals.
256
+ */
257
+ function recommendationsToProposals(recommendations, runId) {
258
+ return recommendations.map((r) => recommendationToProposal(r, runId));
259
+ }
260
+ // ──────────────────────────────────────────────
261
+ // Human-readable formatter
262
+ // ──────────────────────────────────────────────
263
+ function fmtScore(v) {
264
+ return v !== null ? v.toFixed(4) : "N/A";
265
+ }
266
+ function fmtAffected(affected) {
267
+ const parts = [];
268
+ if (affected.provider)
269
+ parts.push(`provider=${affected.provider}`);
270
+ if (affected.model)
271
+ parts.push(`model=${affected.model}`);
272
+ if (affected.role)
273
+ parts.push(`role=${affected.role}`);
274
+ if (affected.route)
275
+ parts.push(`route=${affected.route}`);
276
+ if (affected.task_type)
277
+ parts.push(`task_type=${affected.task_type}`);
278
+ return parts.length > 0 ? parts.join(", ") : "global";
279
+ }
280
+ function formatRecommendationsCli(report) {
281
+ const lines = [];
282
+ lines.push("SOL Routing Recommendations");
283
+ lines.push(`Generated: ${report.generated_at}`);
284
+ lines.push(`Total snapshots: ${report.total_snapshots}`);
285
+ lines.push(`Threshold: ${report.threshold.toFixed(2)}, min samples: ${report.min_samples}`);
286
+ lines.push("");
287
+ if (report.recommendations.length === 0) {
288
+ lines.push("No underperforming groups detected.");
289
+ return lines.join("\n") + "\n";
290
+ }
291
+ for (const r of report.recommendations) {
292
+ lines.push(`[${r.action_type}] ${r.id}`);
293
+ lines.push(` Category: ${r.category}`);
294
+ lines.push(` Affected: ${fmtAffected(r.affected)}`);
295
+ lines.push(` Confidence: ${(r.confidence * 100).toFixed(1)}%`);
296
+ lines.push(` Evidence: ${r.evidence.group_key} | mean=${fmtScore(r.evidence.mean_composite)} min=${fmtScore(r.evidence.min_composite)} max=${fmtScore(r.evidence.max_composite)} count=${r.evidence.count}`);
297
+ lines.push(` Action: ${r.proposed_action}`);
298
+ lines.push(` Rationale: ${r.rationale}`);
299
+ lines.push("");
300
+ }
301
+ return lines.join("\n");
302
+ }
303
+ function makeQcRecommendation(id, category, actionType, affected, proposedAction, rationale, confidence, evidenceRunId) {
304
+ return {
305
+ id,
306
+ category,
307
+ action_type: actionType,
308
+ affected,
309
+ proposed_action: proposedAction,
310
+ confidence: clamp01(confidence),
311
+ evidence: {
312
+ group_key: `run_id=${evidenceRunId}`,
313
+ grouped_by: ["run_id"],
314
+ count: 1,
315
+ mean_composite: null,
316
+ min_composite: null,
317
+ max_composite: null,
318
+ mean_foreman_composite: null,
319
+ mean_worker_composite: null,
320
+ run_ids: [evidenceRunId],
321
+ },
322
+ rationale,
323
+ };
324
+ }
325
+ /**
326
+ * Generate QC repair-loop follow-up recommendations from a single run's
327
+ * SOL evidence. These are advisory signals for noisy providers, repeated
328
+ * repair failures, unresolved high-severity findings, and max-round exhaustion.
329
+ */
330
+ function generateQcRecommendations(evidence) {
331
+ const recommendations = [];
332
+ const qc = evidence.qc;
333
+ const runId = evidence.run_id;
334
+ if (qc.availability !== "available") {
335
+ return {
336
+ generated_at: new Date().toISOString(),
337
+ evidence_run_id: runId,
338
+ recommendations,
339
+ };
340
+ }
341
+ for (const provider of qc.noisy_providers) {
342
+ const counts = qc.provider_breakdown[provider] ?? { total: 0, blocking: 0, unvalidated: 0 };
343
+ const confidence = counts.total > 0 ? counts.unvalidated / counts.total : 0;
344
+ recommendations.push(makeQcRecommendation(`qc-noisy-provider:${provider}`, "provider_policy", "analyze", { provider }, `Review QC provider '${provider}' for noisy/unvalidated findings; consider raising attribution confidence thresholds or disabling the provider for high-risk routes.`, `${counts.unvalidated}/${counts.total} findings from '${provider}' are unvalidated noise.`, confidence, runId));
345
+ }
346
+ if (qc.has_repair_failures) {
347
+ const failed = qc.repair_loop?.packets_failed ?? 0;
348
+ recommendations.push(makeQcRecommendation(`qc-repair-failure:${runId}`, "qc_follow_up", "implement", {}, "Repeated repair worker failures detected — inspect repair packet scope, validation commands, and Medic referral path.", `${failed} repair packet(s) failed; consider tightening acceptance criteria or escalating to Medic.`, 0.9, runId));
349
+ }
350
+ if (qc.unresolved_high_severity > 0) {
351
+ recommendations.push(makeQcRecommendation(`qc-unresolved-high-severity:${runId}`, "qc_follow_up", "analyze", {}, `${qc.unresolved_high_severity} unresolved critical/high QC findings remain — triage before delivery.`, `${qc.unresolved_high_severity} critical/high findings are still open after repair loop.`, Math.min(1, qc.unresolved_high_severity / 5), runId));
352
+ }
353
+ if (qc.max_round_exhausted) {
354
+ recommendations.push(makeQcRecommendation(`qc-max-rounds:${runId}`, "qc_follow_up", "analyze", {}, "QC repair loop exhausted max rounds — review provider noise, repair packet scope, and escalation policy.", `Repair loop reached round ${qc.repair_loop?.rounds_completed ?? 0}/${qc.repair_loop?.max_rounds ?? 0} without passing.`, 0.85, runId));
355
+ }
356
+ return {
357
+ generated_at: new Date().toISOString(),
358
+ evidence_run_id: runId,
359
+ recommendations,
360
+ };
361
+ }
362
+ function formatQcRecommendations(report) {
363
+ const lines = [];
364
+ lines.push("SOL QC Follow-Up Recommendations");
365
+ lines.push(`Generated: ${report.generated_at}`);
366
+ lines.push(`Run: ${report.evidence_run_id}`);
367
+ lines.push("");
368
+ if (report.recommendations.length === 0) {
369
+ lines.push("No QC follow-up signals detected.");
370
+ return lines.join("\n") + "\n";
371
+ }
372
+ for (const r of report.recommendations) {
373
+ lines.push(`[${r.action_type}] ${r.id}`);
374
+ lines.push(` Category: ${r.category}`);
375
+ lines.push(` Affected: ${fmtAffected(r.affected)}`);
376
+ lines.push(` Confidence: ${(r.confidence * 100).toFixed(1)}%`);
377
+ lines.push(` Action: ${r.proposed_action}`);
378
+ lines.push(` Rationale: ${r.rationale}`);
379
+ lines.push("");
380
+ }
381
+ return lines.join("\n");
382
+ }
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ /**
3
+ * SOL report generator.
4
+ *
5
+ * Groups historical SOL score snapshots by configurable dimensions and
6
+ * produces summary reports in JSON or human-readable CLI format.
7
+ *
8
+ * Grouping dimensions: repo, route, task_type, role, risk, provider,
9
+ * model, worker_id, run_id, and time window.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.generateReport = generateReport;
13
+ exports.formatReportCli = formatReportCli;
14
+ exports.formatQcEvidence = formatQcEvidence;
15
+ // ──────────────────────────────────────────────
16
+ // Helpers
17
+ // ──────────────────────────────────────────────
18
+ function getGroupKey(snapshot, dimension, windowDays) {
19
+ const keys = snapshot.grouping_keys;
20
+ switch (dimension) {
21
+ case "repo": return keys.repo ?? "unknown";
22
+ case "route": return keys.route ?? "unknown";
23
+ case "task_type": return keys.task_type ?? "unknown";
24
+ case "role": return keys.role ?? "unknown";
25
+ case "risk": return keys.risk ?? "unknown";
26
+ case "provider": return keys.provider ?? "unknown";
27
+ case "model": return keys.model ?? "unknown";
28
+ case "worker_id":
29
+ return snapshot.worker_ids.length > 0 ? snapshot.worker_ids.join(",") : "unknown";
30
+ case "run_id": return snapshot.report.run_id;
31
+ case "time_window": {
32
+ const d = new Date(snapshot.report.scored_at);
33
+ if (isNaN(d.getTime()))
34
+ return "unknown";
35
+ // Bucket by windowDays from epoch
36
+ const daysSinceEpoch = Math.floor(d.getTime() / (86400000 * windowDays));
37
+ const bucketStart = new Date(daysSinceEpoch * 86400000 * windowDays);
38
+ return bucketStart.toISOString().slice(0, 10);
39
+ }
40
+ }
41
+ }
42
+ function compositeGroupKey(snapshot, dimensions, windowDays) {
43
+ return dimensions.map((d) => `${d}=${getGroupKey(snapshot, d, windowDays)}`).join("|");
44
+ }
45
+ function meanOrNull(nums) {
46
+ const valid = nums.filter((n) => n !== null);
47
+ if (valid.length === 0)
48
+ return null;
49
+ return Number((valid.reduce((a, b) => a + b, 0) / valid.length).toFixed(4));
50
+ }
51
+ function minOrNull(nums) {
52
+ const valid = nums.filter((n) => n !== null);
53
+ return valid.length > 0 ? Math.min(...valid) : null;
54
+ }
55
+ function maxOrNull(nums) {
56
+ const valid = nums.filter((n) => n !== null);
57
+ return valid.length > 0 ? Math.max(...valid) : null;
58
+ }
59
+ // ──────────────────────────────────────────────
60
+ // Report generation
61
+ // ──────────────────────────────────────────────
62
+ function generateReport(snapshots, options = {}) {
63
+ const groupBy = options.groupBy ?? ["run_id"];
64
+ const windowDays = options.windowDays ?? 7;
65
+ // Group snapshots
66
+ const groups = new Map();
67
+ for (const s of snapshots) {
68
+ const key = compositeGroupKey(s, groupBy, windowDays);
69
+ const arr = groups.get(key);
70
+ if (arr)
71
+ arr.push(s);
72
+ else
73
+ groups.set(key, [s]);
74
+ }
75
+ // Build summaries
76
+ const summaries = [];
77
+ for (const [key, snaps] of groups) {
78
+ const composites = snaps.map((s) => s.report.run_composite_score);
79
+ const foremanComposites = snaps.map((s) => s.report.foreman.composite_score);
80
+ // Flatten all worker composites across snapshots
81
+ const workerComposites = [];
82
+ for (const s of snaps) {
83
+ for (const w of Object.values(s.report.workers)) {
84
+ workerComposites.push(w.composite_score);
85
+ }
86
+ }
87
+ const scoredAts = snaps
88
+ .map((s) => s.report.scored_at)
89
+ .filter((t) => t)
90
+ .sort();
91
+ summaries.push({
92
+ group_key: key,
93
+ count: snaps.length,
94
+ mean_composite: meanOrNull(composites),
95
+ min_composite: minOrNull(composites),
96
+ max_composite: maxOrNull(composites),
97
+ mean_foreman_composite: meanOrNull(foremanComposites),
98
+ mean_worker_composite: meanOrNull(workerComposites),
99
+ earliest: scoredAts[0] ?? null,
100
+ latest: scoredAts[scoredAts.length - 1] ?? null,
101
+ });
102
+ }
103
+ // Sort groups by key for deterministic output
104
+ summaries.sort((a, b) => a.group_key.localeCompare(b.group_key));
105
+ const allComposites = snapshots.map((s) => s.report.run_composite_score);
106
+ return {
107
+ total_snapshots: snapshots.length,
108
+ grouped_by: groupBy,
109
+ groups: summaries,
110
+ overall_mean_composite: meanOrNull(allComposites),
111
+ generated_at: new Date().toISOString(),
112
+ };
113
+ }
114
+ // ──────────────────────────────────────────────
115
+ // Human-readable formatter
116
+ // ──────────────────────────────────────────────
117
+ function fmtScore(v) {
118
+ return v !== null ? v.toFixed(4) : "N/A";
119
+ }
120
+ function formatReportCli(report) {
121
+ const lines = [];
122
+ lines.push(`SOL History Report`);
123
+ lines.push(`Generated: ${report.generated_at}`);
124
+ lines.push(`Total snapshots: ${report.total_snapshots}`);
125
+ lines.push(`Grouped by: ${report.grouped_by.join(", ")}`);
126
+ lines.push(`Overall mean composite: ${fmtScore(report.overall_mean_composite)}`);
127
+ lines.push("");
128
+ if (report.groups.length === 0) {
129
+ lines.push("No data.");
130
+ return lines.join("\n");
131
+ }
132
+ // Table header
133
+ lines.push(padRight("Group", 40) +
134
+ padRight("Count", 7) +
135
+ padRight("Mean", 8) +
136
+ padRight("Min", 8) +
137
+ padRight("Max", 8) +
138
+ padRight("Foreman", 9) +
139
+ "Worker");
140
+ lines.push("-".repeat(88));
141
+ for (const g of report.groups) {
142
+ lines.push(padRight(g.group_key.slice(0, 39), 40) +
143
+ padRight(String(g.count), 7) +
144
+ padRight(fmtScore(g.mean_composite), 8) +
145
+ padRight(fmtScore(g.min_composite), 8) +
146
+ padRight(fmtScore(g.max_composite), 8) +
147
+ padRight(fmtScore(g.mean_foreman_composite), 9) +
148
+ fmtScore(g.mean_worker_composite));
149
+ }
150
+ return lines.join("\n") + "\n";
151
+ }
152
+ function padRight(s, n) {
153
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
154
+ }
155
+ /**
156
+ * Render a human-readable summary of QC evidence and repair-loop outcomes.
157
+ */
158
+ function formatQcEvidence(qc) {
159
+ const lines = [];
160
+ lines.push("QC Evidence Summary");
161
+ lines.push(` availability: ${qc.availability}`);
162
+ lines.push(` qc_run_count: ${qc.qc_run_count}`);
163
+ lines.push(` total_findings: ${qc.total_findings}`);
164
+ lines.push(` repaired: ${qc.repaired_findings}, open: ${qc.total_findings - qc.repaired_findings - qc.autofixed_findings - qc.waived_findings}`);
165
+ lines.push(` unresolved high/critical: ${qc.unresolved_high_severity}`);
166
+ lines.push(` blocks_delivery: ${qc.blocks_delivery}`);
167
+ lines.push("");
168
+ const providers = Object.entries(qc.provider_breakdown);
169
+ if (providers.length > 0) {
170
+ lines.push("Provider breakdown:");
171
+ for (const [provider, counts] of providers) {
172
+ lines.push(` ${provider}: total=${counts.total}, blocking=${counts.blocking}, unvalidated=${counts.unvalidated}`);
173
+ }
174
+ lines.push("");
175
+ }
176
+ if (qc.repair_loop) {
177
+ const loop = qc.repair_loop;
178
+ lines.push("Repair loop:");
179
+ lines.push(` status: ${loop.status}`);
180
+ lines.push(` rounds: ${loop.rounds_completed}/${loop.max_rounds}`);
181
+ lines.push(` packets: ${loop.packets_compiled} compiled, ${loop.packets_completed} completed, ${loop.packets_failed} failed`);
182
+ lines.push(` rerun_outcome: ${loop.rerun_outcome ?? "n/a"}`);
183
+ lines.push(` provider_attempts: total=${loop.provider_attempts.total}, success=${loop.provider_attempts.success}, failure=${loop.provider_attempts.failure}, fallback=${loop.provider_attempts.fallback}, skipped=${loop.provider_attempts.skipped}`);
184
+ lines.push(` all_providers_failed: ${loop.provider_attempts.all_providers_failed}`);
185
+ lines.push("");
186
+ }
187
+ if (qc.noisy_providers.length > 0) {
188
+ lines.push(`Noisy providers: ${qc.noisy_providers.join(", ")}`);
189
+ }
190
+ if (qc.has_repair_failures) {
191
+ lines.push("Repeated repair failures detected.");
192
+ }
193
+ if (qc.max_round_exhausted) {
194
+ lines.push("Max repair rounds exhausted.");
195
+ }
196
+ return lines.join("\n").trim() + "\n";
197
+ }