@lsctech/polaris 0.5.4 → 0.5.6
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 +20 -1
- package/dist/autoresearch/sol-evidence-loader.js +479 -0
- package/dist/autoresearch/sol-history.js +90 -0
- package/dist/autoresearch/sol-recommendations.js +300 -0
- package/dist/autoresearch/sol-report.js +153 -0
- package/dist/autoresearch/sol-scorer.js +480 -0
- package/dist/cli/autoresearch.js +185 -11
- package/dist/cli/index.js +19 -2
- package/dist/loop/dispatch.js +3 -1
- package/dist/loop/evidence-backfill.js +10 -3
- package/dist/loop/parent.js +46 -0
- package/dist/loop/run-bootstrap.js +13 -0
- package/dist/types/sol-evidence.js +18 -0
- package/dist/types/sol-score.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
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
|
+
const sol_report_js_1 = require("./sol-report.js");
|
|
23
|
+
// ──────────────────────────────────────────────
|
|
24
|
+
// Constants and helpers
|
|
25
|
+
// ──────────────────────────────────────────────
|
|
26
|
+
const DEFAULT_THRESHOLD = 0.7;
|
|
27
|
+
const DEFAULT_MIN_SAMPLES = 2;
|
|
28
|
+
function clamp01(v) {
|
|
29
|
+
return Math.max(0, Math.min(1, v));
|
|
30
|
+
}
|
|
31
|
+
function parseGroupKey(groupKey) {
|
|
32
|
+
const affected = {};
|
|
33
|
+
for (const part of groupKey.split("|")) {
|
|
34
|
+
const eq = part.indexOf("=");
|
|
35
|
+
if (eq === -1)
|
|
36
|
+
continue;
|
|
37
|
+
const key = part.slice(0, eq);
|
|
38
|
+
const value = part.slice(eq + 1);
|
|
39
|
+
if (!value || value === "unknown")
|
|
40
|
+
continue;
|
|
41
|
+
if (key === "route")
|
|
42
|
+
affected.route = value;
|
|
43
|
+
else if (key === "task_type")
|
|
44
|
+
affected.task_type = value;
|
|
45
|
+
else if (key === "role")
|
|
46
|
+
affected.role = value;
|
|
47
|
+
else if (key === "provider")
|
|
48
|
+
affected.provider = value;
|
|
49
|
+
else if (key === "model")
|
|
50
|
+
affected.model = value;
|
|
51
|
+
}
|
|
52
|
+
return affected;
|
|
53
|
+
}
|
|
54
|
+
function snapshotGroupKey(snapshot, dimension) {
|
|
55
|
+
const keys = snapshot.grouping_keys;
|
|
56
|
+
switch (dimension) {
|
|
57
|
+
case "repo":
|
|
58
|
+
return keys.repo ?? "unknown";
|
|
59
|
+
case "route":
|
|
60
|
+
return keys.route ?? "unknown";
|
|
61
|
+
case "task_type":
|
|
62
|
+
return keys.task_type ?? "unknown";
|
|
63
|
+
case "role":
|
|
64
|
+
return keys.role ?? "unknown";
|
|
65
|
+
case "risk":
|
|
66
|
+
return keys.risk ?? "unknown";
|
|
67
|
+
case "provider":
|
|
68
|
+
return keys.provider ?? "unknown";
|
|
69
|
+
case "model":
|
|
70
|
+
return keys.model ?? "unknown";
|
|
71
|
+
case "worker_id":
|
|
72
|
+
return snapshot.worker_ids.length > 0 ? snapshot.worker_ids.join(",") : "unknown";
|
|
73
|
+
case "run_id":
|
|
74
|
+
return snapshot.report.run_id;
|
|
75
|
+
case "time_window": {
|
|
76
|
+
const d = new Date(snapshot.report.scored_at);
|
|
77
|
+
if (isNaN(d.getTime()))
|
|
78
|
+
return "unknown";
|
|
79
|
+
// Default windowDays = 7 to match generateReport default.
|
|
80
|
+
const WINDOW_DAYS = 7;
|
|
81
|
+
const daysSinceEpoch = Math.floor(d.getTime() / (86400000 * WINDOW_DAYS));
|
|
82
|
+
const bucketStart = new Date(daysSinceEpoch * 86400000 * WINDOW_DAYS);
|
|
83
|
+
return bucketStart.toISOString().slice(0, 10);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function groupValueFromLabel(label, dimension) {
|
|
88
|
+
for (const part of label.split("|")) {
|
|
89
|
+
const eq = part.indexOf("=");
|
|
90
|
+
if (eq === -1)
|
|
91
|
+
continue;
|
|
92
|
+
const key = part.slice(0, eq);
|
|
93
|
+
const value = part.slice(eq + 1);
|
|
94
|
+
if (key === dimension)
|
|
95
|
+
return value ?? "unknown";
|
|
96
|
+
}
|
|
97
|
+
return "unknown";
|
|
98
|
+
}
|
|
99
|
+
function isWorkerSignal(evidence) {
|
|
100
|
+
const workerMean = evidence.mean_worker_composite ?? null;
|
|
101
|
+
const foremanMean = evidence.mean_foreman_composite ?? null;
|
|
102
|
+
if (workerMean === null || foremanMean === null)
|
|
103
|
+
return true; // default to worker-facing
|
|
104
|
+
return workerMean <= foremanMean;
|
|
105
|
+
}
|
|
106
|
+
function buildProposedAction(dimension, affected, evidence) {
|
|
107
|
+
const target = affected.provider ?? affected.model ?? affected.role ?? affected.route ?? affected.task_type ?? dimension;
|
|
108
|
+
const workerFocus = isWorkerSignal(evidence);
|
|
109
|
+
switch (dimension) {
|
|
110
|
+
case "provider":
|
|
111
|
+
return `Review provider eligibility and role assignment for provider '${target}'; consider updating execution.providerPolicy or provider trust/cost thresholds.`;
|
|
112
|
+
case "model":
|
|
113
|
+
return `Review model selection for model '${target}'; update role model mapping if the trend persists.`;
|
|
114
|
+
case "role":
|
|
115
|
+
return `Review role assignment for role '${target}'; verify provider capabilities, packet scope, and routing policy.`;
|
|
116
|
+
case "route":
|
|
117
|
+
return workerFocus
|
|
118
|
+
? `Analyze route health for '${target}'; inspect worker templates and validation commands.`
|
|
119
|
+
: `Analyze route health for '${target}'; inspect scoring rules and foreman/runtime configuration.`;
|
|
120
|
+
case "task_type":
|
|
121
|
+
return `Review task-type routing for '${target}'; verify provider capabilities and role mapping.`;
|
|
122
|
+
case "risk":
|
|
123
|
+
return `Review trust/cost thresholds for risk tier '${target}'; adjust policy filters if this tier is consistently underperforming.`;
|
|
124
|
+
case "worker_id":
|
|
125
|
+
return `Review worker behavior for '${target}'; consider tightening packet scope or validation commands.`;
|
|
126
|
+
default:
|
|
127
|
+
return `Investigate ${workerFocus ? "worker" : "runtime"} performance for '${target}' and update the relevant ${workerFocus ? "worker template/policy" : "runtime config/scoring rules"}.`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function categoryFor(dimension) {
|
|
131
|
+
const map = {
|
|
132
|
+
provider: "provider_policy",
|
|
133
|
+
model: "provider_policy",
|
|
134
|
+
role: "role_assignment",
|
|
135
|
+
route: "routing",
|
|
136
|
+
task_type: "routing",
|
|
137
|
+
repo: "routing",
|
|
138
|
+
risk: "trust_threshold",
|
|
139
|
+
};
|
|
140
|
+
return map[dimension] ?? "runtime_improvement";
|
|
141
|
+
}
|
|
142
|
+
function actionTypeFor(evidence) {
|
|
143
|
+
return isWorkerSignal(evidence) ? "implement" : "analyze";
|
|
144
|
+
}
|
|
145
|
+
function artifactTypeFor(dimension, evidence) {
|
|
146
|
+
const workerFocus = isWorkerSignal(evidence);
|
|
147
|
+
switch (dimension) {
|
|
148
|
+
case "provider":
|
|
149
|
+
case "model":
|
|
150
|
+
return "provider-role-recommendation";
|
|
151
|
+
case "route":
|
|
152
|
+
return workerFocus ? "worker-template" : "scoring-rule";
|
|
153
|
+
case "task_type":
|
|
154
|
+
case "role":
|
|
155
|
+
case "risk":
|
|
156
|
+
return workerFocus ? "worker-template" : "runtime-config";
|
|
157
|
+
case "repo":
|
|
158
|
+
case "worker_id":
|
|
159
|
+
case "run_id":
|
|
160
|
+
case "time_window":
|
|
161
|
+
default:
|
|
162
|
+
return workerFocus ? "worker-template" : "runtime-config";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function confidenceFor(mean, threshold, count) {
|
|
166
|
+
const gap = Math.max(0, threshold - mean);
|
|
167
|
+
const sampleBoost = Math.min(count / 10, 0.2);
|
|
168
|
+
return clamp01(gap + sampleBoost);
|
|
169
|
+
}
|
|
170
|
+
// ──────────────────────────────────────────────
|
|
171
|
+
// Recommendation generation
|
|
172
|
+
// ──────────────────────────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* Scan historical SOL snapshots for underperforming groups and emit
|
|
175
|
+
* review-gated recommendations.
|
|
176
|
+
*
|
|
177
|
+
* The function is pure: it does not read files, call APIs, or mutate inputs.
|
|
178
|
+
*/
|
|
179
|
+
function generateRecommendations(snapshots, options = {}) {
|
|
180
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
181
|
+
const minSamples = options.minSamples ?? DEFAULT_MIN_SAMPLES;
|
|
182
|
+
const dimensions = options.groupBy ?? ["provider", "model", "role", "route", "task_type"];
|
|
183
|
+
const recommendations = [];
|
|
184
|
+
for (const dim of dimensions) {
|
|
185
|
+
const report = (0, sol_report_js_1.generateReport)(snapshots, { groupBy: [dim] });
|
|
186
|
+
for (const group of report.groups) {
|
|
187
|
+
const mean = group.mean_composite;
|
|
188
|
+
if (mean === null || mean >= threshold || group.count < minSamples)
|
|
189
|
+
continue;
|
|
190
|
+
const affected = parseGroupKey(group.group_key);
|
|
191
|
+
const evidence = {
|
|
192
|
+
group_key: group.group_key,
|
|
193
|
+
grouped_by: [dim],
|
|
194
|
+
count: group.count,
|
|
195
|
+
mean_composite: group.mean_composite,
|
|
196
|
+
min_composite: group.min_composite,
|
|
197
|
+
max_composite: group.max_composite,
|
|
198
|
+
mean_foreman_composite: group.mean_foreman_composite,
|
|
199
|
+
mean_worker_composite: group.mean_worker_composite,
|
|
200
|
+
run_ids: [
|
|
201
|
+
...new Set(snapshots
|
|
202
|
+
.filter((s) => snapshotGroupKey(s, dim) === groupValueFromLabel(group.group_key, dim))
|
|
203
|
+
.map((s) => s.report.run_id)),
|
|
204
|
+
].sort(),
|
|
205
|
+
};
|
|
206
|
+
recommendations.push({
|
|
207
|
+
id: `${dim}:${group.group_key}`,
|
|
208
|
+
category: categoryFor(dim),
|
|
209
|
+
action_type: actionTypeFor(evidence),
|
|
210
|
+
affected,
|
|
211
|
+
proposed_action: buildProposedAction(dim, affected, evidence),
|
|
212
|
+
confidence: confidenceFor(mean, threshold, group.count),
|
|
213
|
+
evidence,
|
|
214
|
+
rationale: `Mean composite score ${mean.toFixed(4)} is below threshold ${threshold.toFixed(2)} across ${group.count} snapshots.`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Deterministic ordering: highest confidence first, then stable id sort.
|
|
219
|
+
recommendations.sort((a, b) => b.confidence - a.confidence || a.id.localeCompare(b.id));
|
|
220
|
+
return {
|
|
221
|
+
generated_at: new Date().toISOString(),
|
|
222
|
+
total_snapshots: snapshots.length,
|
|
223
|
+
threshold,
|
|
224
|
+
min_samples: minSamples,
|
|
225
|
+
recommendations,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ──────────────────────────────────────────────
|
|
229
|
+
// Proposal conversion (tracker filing)
|
|
230
|
+
// ──────────────────────────────────────────────
|
|
231
|
+
/**
|
|
232
|
+
* Convert a single recommendation into an AutresearchProposal suitable for
|
|
233
|
+
* `routeProposals()`. This is the bridge from advisory SOL output to
|
|
234
|
+
* review-gated tracker issues.
|
|
235
|
+
*/
|
|
236
|
+
function recommendationToProposal(recommendation, runId) {
|
|
237
|
+
const { evidence, affected, proposed_action, confidence, id, action_type, rationale } = recommendation;
|
|
238
|
+
const dim = evidence.grouped_by[0] ?? "run_id";
|
|
239
|
+
const artifactType = artifactTypeFor(dim, evidence);
|
|
240
|
+
const affectedLabel = affected.provider ?? affected.model ?? affected.role ?? affected.route ?? affected.task_type ?? dim;
|
|
241
|
+
const gateId = `sol-recommendation:${id}`;
|
|
242
|
+
return {
|
|
243
|
+
gate_id: gateId,
|
|
244
|
+
artifact_type: artifactType,
|
|
245
|
+
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(",")}`,
|
|
246
|
+
run_id: runId ?? evidence.run_ids[0] ?? "sol-history",
|
|
247
|
+
evidence_run_ids: evidence.run_ids,
|
|
248
|
+
confidence,
|
|
249
|
+
fix_zone: `${artifactType}/${gateId}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Convert recommendations to tracker proposals.
|
|
254
|
+
*/
|
|
255
|
+
function recommendationsToProposals(recommendations, runId) {
|
|
256
|
+
return recommendations.map((r) => recommendationToProposal(r, runId));
|
|
257
|
+
}
|
|
258
|
+
// ──────────────────────────────────────────────
|
|
259
|
+
// Human-readable formatter
|
|
260
|
+
// ──────────────────────────────────────────────
|
|
261
|
+
function fmtScore(v) {
|
|
262
|
+
return v !== null ? v.toFixed(4) : "N/A";
|
|
263
|
+
}
|
|
264
|
+
function fmtAffected(affected) {
|
|
265
|
+
const parts = [];
|
|
266
|
+
if (affected.provider)
|
|
267
|
+
parts.push(`provider=${affected.provider}`);
|
|
268
|
+
if (affected.model)
|
|
269
|
+
parts.push(`model=${affected.model}`);
|
|
270
|
+
if (affected.role)
|
|
271
|
+
parts.push(`role=${affected.role}`);
|
|
272
|
+
if (affected.route)
|
|
273
|
+
parts.push(`route=${affected.route}`);
|
|
274
|
+
if (affected.task_type)
|
|
275
|
+
parts.push(`task_type=${affected.task_type}`);
|
|
276
|
+
return parts.length > 0 ? parts.join(", ") : "global";
|
|
277
|
+
}
|
|
278
|
+
function formatRecommendationsCli(report) {
|
|
279
|
+
const lines = [];
|
|
280
|
+
lines.push("SOL Routing Recommendations");
|
|
281
|
+
lines.push(`Generated: ${report.generated_at}`);
|
|
282
|
+
lines.push(`Total snapshots: ${report.total_snapshots}`);
|
|
283
|
+
lines.push(`Threshold: ${report.threshold.toFixed(2)}, min samples: ${report.min_samples}`);
|
|
284
|
+
lines.push("");
|
|
285
|
+
if (report.recommendations.length === 0) {
|
|
286
|
+
lines.push("No underperforming groups detected.");
|
|
287
|
+
return lines.join("\n") + "\n";
|
|
288
|
+
}
|
|
289
|
+
for (const r of report.recommendations) {
|
|
290
|
+
lines.push(`[${r.action_type}] ${r.id}`);
|
|
291
|
+
lines.push(` Category: ${r.category}`);
|
|
292
|
+
lines.push(` Affected: ${fmtAffected(r.affected)}`);
|
|
293
|
+
lines.push(` Confidence: ${(r.confidence * 100).toFixed(1)}%`);
|
|
294
|
+
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}`);
|
|
295
|
+
lines.push(` Action: ${r.proposed_action}`);
|
|
296
|
+
lines.push(` Rationale: ${r.rationale}`);
|
|
297
|
+
lines.push("");
|
|
298
|
+
}
|
|
299
|
+
return lines.join("\n");
|
|
300
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
// ──────────────────────────────────────────────
|
|
15
|
+
// Helpers
|
|
16
|
+
// ──────────────────────────────────────────────
|
|
17
|
+
function getGroupKey(snapshot, dimension, windowDays) {
|
|
18
|
+
const keys = snapshot.grouping_keys;
|
|
19
|
+
switch (dimension) {
|
|
20
|
+
case "repo": return keys.repo ?? "unknown";
|
|
21
|
+
case "route": return keys.route ?? "unknown";
|
|
22
|
+
case "task_type": return keys.task_type ?? "unknown";
|
|
23
|
+
case "role": return keys.role ?? "unknown";
|
|
24
|
+
case "risk": return keys.risk ?? "unknown";
|
|
25
|
+
case "provider": return keys.provider ?? "unknown";
|
|
26
|
+
case "model": return keys.model ?? "unknown";
|
|
27
|
+
case "worker_id":
|
|
28
|
+
return snapshot.worker_ids.length > 0 ? snapshot.worker_ids.join(",") : "unknown";
|
|
29
|
+
case "run_id": return snapshot.report.run_id;
|
|
30
|
+
case "time_window": {
|
|
31
|
+
const d = new Date(snapshot.report.scored_at);
|
|
32
|
+
if (isNaN(d.getTime()))
|
|
33
|
+
return "unknown";
|
|
34
|
+
// Bucket by windowDays from epoch
|
|
35
|
+
const daysSinceEpoch = Math.floor(d.getTime() / (86400000 * windowDays));
|
|
36
|
+
const bucketStart = new Date(daysSinceEpoch * 86400000 * windowDays);
|
|
37
|
+
return bucketStart.toISOString().slice(0, 10);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function compositeGroupKey(snapshot, dimensions, windowDays) {
|
|
42
|
+
return dimensions.map((d) => `${d}=${getGroupKey(snapshot, d, windowDays)}`).join("|");
|
|
43
|
+
}
|
|
44
|
+
function meanOrNull(nums) {
|
|
45
|
+
const valid = nums.filter((n) => n !== null);
|
|
46
|
+
if (valid.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
return Number((valid.reduce((a, b) => a + b, 0) / valid.length).toFixed(4));
|
|
49
|
+
}
|
|
50
|
+
function minOrNull(nums) {
|
|
51
|
+
const valid = nums.filter((n) => n !== null);
|
|
52
|
+
return valid.length > 0 ? Math.min(...valid) : null;
|
|
53
|
+
}
|
|
54
|
+
function maxOrNull(nums) {
|
|
55
|
+
const valid = nums.filter((n) => n !== null);
|
|
56
|
+
return valid.length > 0 ? Math.max(...valid) : null;
|
|
57
|
+
}
|
|
58
|
+
// ──────────────────────────────────────────────
|
|
59
|
+
// Report generation
|
|
60
|
+
// ──────────────────────────────────────────────
|
|
61
|
+
function generateReport(snapshots, options = {}) {
|
|
62
|
+
const groupBy = options.groupBy ?? ["run_id"];
|
|
63
|
+
const windowDays = options.windowDays ?? 7;
|
|
64
|
+
// Group snapshots
|
|
65
|
+
const groups = new Map();
|
|
66
|
+
for (const s of snapshots) {
|
|
67
|
+
const key = compositeGroupKey(s, groupBy, windowDays);
|
|
68
|
+
const arr = groups.get(key);
|
|
69
|
+
if (arr)
|
|
70
|
+
arr.push(s);
|
|
71
|
+
else
|
|
72
|
+
groups.set(key, [s]);
|
|
73
|
+
}
|
|
74
|
+
// Build summaries
|
|
75
|
+
const summaries = [];
|
|
76
|
+
for (const [key, snaps] of groups) {
|
|
77
|
+
const composites = snaps.map((s) => s.report.run_composite_score);
|
|
78
|
+
const foremanComposites = snaps.map((s) => s.report.foreman.composite_score);
|
|
79
|
+
// Flatten all worker composites across snapshots
|
|
80
|
+
const workerComposites = [];
|
|
81
|
+
for (const s of snaps) {
|
|
82
|
+
for (const w of Object.values(s.report.workers)) {
|
|
83
|
+
workerComposites.push(w.composite_score);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const scoredAts = snaps
|
|
87
|
+
.map((s) => s.report.scored_at)
|
|
88
|
+
.filter((t) => t)
|
|
89
|
+
.sort();
|
|
90
|
+
summaries.push({
|
|
91
|
+
group_key: key,
|
|
92
|
+
count: snaps.length,
|
|
93
|
+
mean_composite: meanOrNull(composites),
|
|
94
|
+
min_composite: minOrNull(composites),
|
|
95
|
+
max_composite: maxOrNull(composites),
|
|
96
|
+
mean_foreman_composite: meanOrNull(foremanComposites),
|
|
97
|
+
mean_worker_composite: meanOrNull(workerComposites),
|
|
98
|
+
earliest: scoredAts[0] ?? null,
|
|
99
|
+
latest: scoredAts[scoredAts.length - 1] ?? null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Sort groups by key for deterministic output
|
|
103
|
+
summaries.sort((a, b) => a.group_key.localeCompare(b.group_key));
|
|
104
|
+
const allComposites = snapshots.map((s) => s.report.run_composite_score);
|
|
105
|
+
return {
|
|
106
|
+
total_snapshots: snapshots.length,
|
|
107
|
+
grouped_by: groupBy,
|
|
108
|
+
groups: summaries,
|
|
109
|
+
overall_mean_composite: meanOrNull(allComposites),
|
|
110
|
+
generated_at: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ──────────────────────────────────────────────
|
|
114
|
+
// Human-readable formatter
|
|
115
|
+
// ──────────────────────────────────────────────
|
|
116
|
+
function fmtScore(v) {
|
|
117
|
+
return v !== null ? v.toFixed(4) : "N/A";
|
|
118
|
+
}
|
|
119
|
+
function formatReportCli(report) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
lines.push(`SOL History Report`);
|
|
122
|
+
lines.push(`Generated: ${report.generated_at}`);
|
|
123
|
+
lines.push(`Total snapshots: ${report.total_snapshots}`);
|
|
124
|
+
lines.push(`Grouped by: ${report.grouped_by.join(", ")}`);
|
|
125
|
+
lines.push(`Overall mean composite: ${fmtScore(report.overall_mean_composite)}`);
|
|
126
|
+
lines.push("");
|
|
127
|
+
if (report.groups.length === 0) {
|
|
128
|
+
lines.push("No data.");
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
// Table header
|
|
132
|
+
lines.push(padRight("Group", 40) +
|
|
133
|
+
padRight("Count", 7) +
|
|
134
|
+
padRight("Mean", 8) +
|
|
135
|
+
padRight("Min", 8) +
|
|
136
|
+
padRight("Max", 8) +
|
|
137
|
+
padRight("Foreman", 9) +
|
|
138
|
+
"Worker");
|
|
139
|
+
lines.push("-".repeat(88));
|
|
140
|
+
for (const g of report.groups) {
|
|
141
|
+
lines.push(padRight(g.group_key.slice(0, 39), 40) +
|
|
142
|
+
padRight(String(g.count), 7) +
|
|
143
|
+
padRight(fmtScore(g.mean_composite), 8) +
|
|
144
|
+
padRight(fmtScore(g.min_composite), 8) +
|
|
145
|
+
padRight(fmtScore(g.max_composite), 8) +
|
|
146
|
+
padRight(fmtScore(g.mean_foreman_composite), 9) +
|
|
147
|
+
fmtScore(g.mean_worker_composite));
|
|
148
|
+
}
|
|
149
|
+
return lines.join("\n") + "\n";
|
|
150
|
+
}
|
|
151
|
+
function padRight(s, n) {
|
|
152
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
153
|
+
}
|