@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.
- package/dist/autoresearch/index.js +20 -1
- package/dist/autoresearch/proposal.js +5 -0
- package/dist/autoresearch/score.js +174 -2
- package/dist/autoresearch/sol-evidence-loader.js +516 -0
- package/dist/autoresearch/sol-history.js +90 -0
- package/dist/autoresearch/sol-recommendations.js +382 -0
- package/dist/autoresearch/sol-report.js +197 -0
- package/dist/autoresearch/sol-scorer.js +524 -0
- package/dist/cli/autoresearch.js +185 -11
- package/dist/cli/index.js +1 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/validator.js +157 -0
- package/dist/finalize/index.js +1 -1
- package/dist/loop/parent.js +156 -5
- package/dist/loop/worker-packet.js +75 -1
- package/dist/qc/fixtures/repair-packets.js +170 -0
- package/dist/qc/index.js +2 -0
- package/dist/qc/orchestration.js +2 -0
- package/dist/qc/policy.js +30 -3
- package/dist/qc/providers/coderabbit.js +39 -7
- package/dist/qc/registry.js +36 -3
- package/dist/qc/repair-loop.js +423 -0
- package/dist/qc/repair-packets.js +420 -0
- package/dist/qc/runner.js +320 -37
- package/dist/qc/schemas.js +78 -1
- package/dist/types/sol-evidence.js +18 -0
- package/dist/types/sol-score.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SOL scoring engine.
|
|
4
|
+
*
|
|
5
|
+
* Computes diagnostic sub-scores for Foremen and Workers from aggregated
|
|
6
|
+
* SolEvidence. Each dimension independently scores one behavioral signal
|
|
7
|
+
* with an attached confidence and optional skipped_reason.
|
|
8
|
+
*
|
|
9
|
+
* Design rules:
|
|
10
|
+
* - Never throws on missing evidence — produces skipped dimensions.
|
|
11
|
+
* - Scores are 0.0–1.0 where 1.0 = optimal behavior.
|
|
12
|
+
* - Composite score = mean of non-null dimension scores.
|
|
13
|
+
* - Confidence tiers: high (≥2 signals), medium (1 signal), low (proxy only), none (skipped).
|
|
14
|
+
* - Binary gate diagnosis is preserved; these scores are additive.
|
|
15
|
+
* - No routing policy changes are triggered from scores (Non-goal, POL-481).
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.computeForemanScore = computeForemanScore;
|
|
19
|
+
exports.computeWorkerScore = computeWorkerScore;
|
|
20
|
+
exports.computeSolScoreReport = computeSolScoreReport;
|
|
21
|
+
// ──────────────────────────────────────────────
|
|
22
|
+
// Helpers
|
|
23
|
+
// ──────────────────────────────────────────────
|
|
24
|
+
function dim(dimension, score, confidence, opts = {}) {
|
|
25
|
+
return { dimension, score, confidence, ...opts };
|
|
26
|
+
}
|
|
27
|
+
function skipped(dimension, reason) {
|
|
28
|
+
return dim(dimension, null, "none", { skipped_reason: reason });
|
|
29
|
+
}
|
|
30
|
+
/** Clamp a value to [0, 1]. */
|
|
31
|
+
function clamp01(v) {
|
|
32
|
+
return Math.max(0, Math.min(1, v));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compute composite score and confidence from a list of dimension scores.
|
|
36
|
+
* Skipped dimensions (score === null) are not counted.
|
|
37
|
+
*/
|
|
38
|
+
function composite(dims) {
|
|
39
|
+
const scored = dims.filter((d) => d.score !== null);
|
|
40
|
+
if (scored.length === 0)
|
|
41
|
+
return { score: null, confidence: "none" };
|
|
42
|
+
const mean = scored.reduce((s, d) => s + d.score, 0) / scored.length;
|
|
43
|
+
// Composite confidence = worst of all scored dimensions
|
|
44
|
+
const rank = { high: 3, medium: 2, low: 1, none: 0 };
|
|
45
|
+
const worstConf = scored.reduce((worst, d) => {
|
|
46
|
+
return rank[d.confidence] < rank[worst] ? d.confidence : worst;
|
|
47
|
+
}, "high");
|
|
48
|
+
return { score: Number(mean.toFixed(4)), confidence: worstConf };
|
|
49
|
+
}
|
|
50
|
+
// ──────────────────────────────────────────────
|
|
51
|
+
// Foreman dimension scorers
|
|
52
|
+
// ──────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* token: Measures bootstrap context efficiency.
|
|
55
|
+
* Score = 1.0 when under budget, decreases linearly above 150k tokens.
|
|
56
|
+
* 150k = warning zone; 300k = 0.0.
|
|
57
|
+
*/
|
|
58
|
+
function scoreForemanToken(ev) {
|
|
59
|
+
const tokens = ev.foreman.max_bootstrap_tokens;
|
|
60
|
+
if (tokens === null)
|
|
61
|
+
return skipped("token", "no bootstrap-context-size events in telemetry");
|
|
62
|
+
const BUDGET = 150_000;
|
|
63
|
+
const MAX_PENALIZED = 300_000;
|
|
64
|
+
let score;
|
|
65
|
+
if (tokens <= BUDGET) {
|
|
66
|
+
score = 1.0;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Linear decay from 1.0 at BUDGET to 0.0 at MAX_PENALIZED
|
|
70
|
+
score = clamp01(1.0 - (tokens - BUDGET) / (MAX_PENALIZED - BUDGET));
|
|
71
|
+
}
|
|
72
|
+
return dim("token", Number(score.toFixed(4)), "high", { detail: `max_bootstrap_tokens=${tokens}` });
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* duration: Proxy via dispatch epoch overhead.
|
|
76
|
+
* Score = 1.0 at epoch 1, decays with each additional epoch beyond the
|
|
77
|
+
* continue_epoch (re-dispatch overhead).
|
|
78
|
+
* Skipped when no dispatch_boundary evidence.
|
|
79
|
+
*/
|
|
80
|
+
function scoreForemanDuration(ev) {
|
|
81
|
+
const { dispatch_epoch, continue_epoch } = ev.run;
|
|
82
|
+
if (dispatch_epoch === null)
|
|
83
|
+
return skipped("duration", "dispatch_epoch unavailable in run state");
|
|
84
|
+
const expected = continue_epoch !== null ? continue_epoch + 1 : 1;
|
|
85
|
+
const overhead = Math.max(0, dispatch_epoch - expected);
|
|
86
|
+
// Each extra epoch subtracts 0.25 from score
|
|
87
|
+
const score = clamp01(1.0 - overhead * 0.25);
|
|
88
|
+
return dim("duration", Number(score.toFixed(4)), overhead === 0 ? "medium" : "high", { detail: `dispatch_epoch=${dispatch_epoch}, continue_epoch=${continue_epoch}` });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* intervention: User and foreman corrective commits.
|
|
92
|
+
* Score = 1.0 when no interventions, 0.5 for foreman-only, 0.0 for user.
|
|
93
|
+
*/
|
|
94
|
+
function scoreForemanIntervention(ev) {
|
|
95
|
+
const { total_children } = ev.run;
|
|
96
|
+
// Need at least one child to evaluate
|
|
97
|
+
if (total_children === 0 && ev.children.length === 0) {
|
|
98
|
+
return skipped("intervention", "no children observed for intervention assessment");
|
|
99
|
+
}
|
|
100
|
+
const userInt = ev.intervention.user_intervened;
|
|
101
|
+
const foremanInt = ev.intervention.foreman_intervened;
|
|
102
|
+
if (userInt) {
|
|
103
|
+
return dim("intervention", 0.0, "high", { detail: "user_intervened=true" });
|
|
104
|
+
}
|
|
105
|
+
if (foremanInt) {
|
|
106
|
+
return dim("intervention", 0.5, "high", { detail: "foreman_intervened=true" });
|
|
107
|
+
}
|
|
108
|
+
// Both false → clean run
|
|
109
|
+
return dim("intervention", 1.0, "high", { detail: "no user or foreman interventions" });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* pre_analysis: Escalation events as a proxy for pre-analysis gaps.
|
|
113
|
+
* 0 escalations = 1.0; each escalation reduces score.
|
|
114
|
+
*/
|
|
115
|
+
function scoreForemanPreAnalysis(ev) {
|
|
116
|
+
const escalations = ev.foreman.escalation_events;
|
|
117
|
+
// Without telemetry at all, we can't distinguish "no escalations" from "no telemetry"
|
|
118
|
+
const hasTelemetry = ev.tokens.total_worker_heartbeats > 0 || ev.run.dispatch_epoch !== null;
|
|
119
|
+
if (!hasTelemetry)
|
|
120
|
+
return skipped("pre_analysis", "no telemetry events to assess escalations");
|
|
121
|
+
const score = clamp01(1.0 - escalations * 0.25);
|
|
122
|
+
const confidence = escalations > 0 ? "high" : "medium";
|
|
123
|
+
return dim("pre_analysis", Number(score.toFixed(4)), confidence, {
|
|
124
|
+
detail: `escalation_events=${escalations}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* dependency: Whether dependent children were dispatched at the correct epoch.
|
|
129
|
+
* Uses re-dispatch count as a proxy for dependency ordering issues.
|
|
130
|
+
* 0 re-dispatches = 1.0; each re-dispatch reduces score.
|
|
131
|
+
*/
|
|
132
|
+
function scoreForemanDependency(ev) {
|
|
133
|
+
const { redispatch_count } = ev.foreman;
|
|
134
|
+
const hasChildren = ev.run.total_children > 0 || ev.children.length > 0;
|
|
135
|
+
if (!hasChildren)
|
|
136
|
+
return skipped("dependency", "no children observed for dependency assessment");
|
|
137
|
+
const score = clamp01(1.0 - redispatch_count * 0.33);
|
|
138
|
+
const confidence = ev.run.dispatch_epoch !== null ? "medium" : "low";
|
|
139
|
+
return dim("dependency", Number(score.toFixed(4)), confidence, {
|
|
140
|
+
detail: `redispatch_count=${redispatch_count}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* dispatch: Re-dispatch rate.
|
|
145
|
+
* Score = 1.0 when no child was dispatched more than once.
|
|
146
|
+
* Penalized by proportion of re-dispatched children.
|
|
147
|
+
*/
|
|
148
|
+
function scoreForemanDispatch(ev) {
|
|
149
|
+
const total = ev.run.total_children;
|
|
150
|
+
if (total === 0)
|
|
151
|
+
return skipped("dispatch", "no children to assess dispatch accuracy");
|
|
152
|
+
const reDispatched = ev.foreman.redispatch_count;
|
|
153
|
+
const score = clamp01(1.0 - reDispatched / total);
|
|
154
|
+
const confidence = reDispatched > 0 ? "high" : "medium";
|
|
155
|
+
return dim("dispatch", Number(score.toFixed(4)), confidence, {
|
|
156
|
+
detail: `redispatched=${reDispatched}/${total}`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* evidence_validation: Heartbeat coverage and completion signals.
|
|
161
|
+
* High heartbeats per child = thorough evidence; low = minimal signal.
|
|
162
|
+
* Score = 1.0 when mean heartbeats ≥ 5 per child.
|
|
163
|
+
*/
|
|
164
|
+
function scoreForemanEvidenceValidation(ev) {
|
|
165
|
+
const totalHeartbeats = ev.worker.total_heartbeats;
|
|
166
|
+
const totalChildren = ev.children.length;
|
|
167
|
+
if (totalChildren === 0)
|
|
168
|
+
return skipped("evidence_validation", "no children observed");
|
|
169
|
+
if (totalHeartbeats === 0)
|
|
170
|
+
return skipped("evidence_validation", "no heartbeat events in telemetry");
|
|
171
|
+
const meanHeartbeats = totalHeartbeats / totalChildren;
|
|
172
|
+
// 5 heartbeats per child = 1.0; scales linearly down to 0 at 1 heartbeat
|
|
173
|
+
const EXPECTED = 5;
|
|
174
|
+
const score = clamp01(Math.min(meanHeartbeats, EXPECTED) / EXPECTED);
|
|
175
|
+
return dim("evidence_validation", Number(score.toFixed(4)), "medium", {
|
|
176
|
+
detail: `mean_heartbeats_per_child=${meanHeartbeats.toFixed(1)}`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* scope: Out-of-scope escalation events.
|
|
181
|
+
* 0 out-of-scope events = 1.0; each event reduces score.
|
|
182
|
+
*/
|
|
183
|
+
function scoreForemanScope(ev) {
|
|
184
|
+
const { out_of_scope_count } = ev.intervention;
|
|
185
|
+
const hasEscalationSignal = ev.intervention.blocked_event_count > 0 || ev.tokens.total_worker_heartbeats > 0;
|
|
186
|
+
if (!hasEscalationSignal && out_of_scope_count === 0) {
|
|
187
|
+
return skipped("scope", "no worker-blocked events in telemetry");
|
|
188
|
+
}
|
|
189
|
+
const score = clamp01(1.0 - out_of_scope_count * 0.5);
|
|
190
|
+
const confidence = out_of_scope_count > 0 ? "high" : "medium";
|
|
191
|
+
return dim("scope", Number(score.toFixed(4)), confidence, {
|
|
192
|
+
detail: `out_of_scope_events=${out_of_scope_count}`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* completion: Fraction of children that completed successfully.
|
|
197
|
+
* Score = workers_succeeded / total_children.
|
|
198
|
+
*/
|
|
199
|
+
function scoreForemanCompletion(ev) {
|
|
200
|
+
const total = ev.run.total_children;
|
|
201
|
+
if (total === 0)
|
|
202
|
+
return skipped("completion", "no children observed");
|
|
203
|
+
const succeeded = ev.worker.workers_succeeded;
|
|
204
|
+
const score = clamp01(succeeded / total);
|
|
205
|
+
const confidence = ev.worker.workers_failed > 0 || ev.worker.workers_blocked > 0 ? "high" : "medium";
|
|
206
|
+
return dim("completion", Number(score.toFixed(4)), confidence, {
|
|
207
|
+
detail: `succeeded=${succeeded}/${total}`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* qc_repair_loop: Observe QC repair loop outcomes.
|
|
212
|
+
*
|
|
213
|
+
* Scores repair loop terminal states without treating provider findings as
|
|
214
|
+
* ground truth. Best outcome = loop passed or no repairable findings.
|
|
215
|
+
* Worst = all providers failed, operator review required, or Medic referral.
|
|
216
|
+
* Skipped when QC is not configured or no repair loop ran.
|
|
217
|
+
*/
|
|
218
|
+
function scoreForemanQcRepairLoop(ev) {
|
|
219
|
+
if (ev.qc.availability === "future" || ev.qc.availability === "unavailable") {
|
|
220
|
+
return skipped("qc_repair_loop", `QC evidence availability=${ev.qc.availability}`);
|
|
221
|
+
}
|
|
222
|
+
const loop = ev.qc.repair_loop;
|
|
223
|
+
if (!loop) {
|
|
224
|
+
return skipped("qc_repair_loop", "no QC repair loop data available");
|
|
225
|
+
}
|
|
226
|
+
const { status, rounds_completed, max_rounds, packets_compiled, packets_failed, rerun_outcome } = loop;
|
|
227
|
+
switch (status) {
|
|
228
|
+
case "passed":
|
|
229
|
+
case "repaired":
|
|
230
|
+
case "no-repairable":
|
|
231
|
+
return dim("qc_repair_loop", 1.0, "high", {
|
|
232
|
+
detail: `status=${status}, rounds=${rounds_completed}/${max_rounds}, packets=${packets_compiled}, rerun=${rerun_outcome ?? "n/a"}`,
|
|
233
|
+
});
|
|
234
|
+
case "max-rounds":
|
|
235
|
+
return dim("qc_repair_loop", 0.5, "high", {
|
|
236
|
+
detail: `status=${status}, rounds=${rounds_completed}/${max_rounds}, packets=${packets_compiled}`,
|
|
237
|
+
});
|
|
238
|
+
case "all-providers-failed":
|
|
239
|
+
case "operator-review":
|
|
240
|
+
case "medic-referral":
|
|
241
|
+
return dim("qc_repair_loop", 0.0, "high", {
|
|
242
|
+
detail: `status=${status}, failed_packets=${packets_failed}, rerun=${rerun_outcome ?? "n/a"}`,
|
|
243
|
+
});
|
|
244
|
+
case "not-run":
|
|
245
|
+
case "not-configured":
|
|
246
|
+
case "in-progress":
|
|
247
|
+
case "unknown":
|
|
248
|
+
default:
|
|
249
|
+
return skipped("qc_repair_loop", `QC repair loop status=${status}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* recovery: State repair detection.
|
|
254
|
+
* Score = 1.0 when no medic/repair artifacts detected; 0.0 when repair required.
|
|
255
|
+
*/
|
|
256
|
+
function scoreForemanRecovery(ev) {
|
|
257
|
+
const repairRequired = ev.intervention.state_repair_required;
|
|
258
|
+
// We only know repair was required when the cluster dir is inspected.
|
|
259
|
+
// If clusterDir was null, this field defaults to false — treat as low confidence.
|
|
260
|
+
const hasClusterSignal = ev.children.length > 0 || ev.run.cluster_id !== null;
|
|
261
|
+
if (!hasClusterSignal)
|
|
262
|
+
return skipped("recovery", "no cluster artifact path to assess repair");
|
|
263
|
+
const score = repairRequired ? 0.0 : 1.0;
|
|
264
|
+
const confidence = repairRequired ? "high" : "low";
|
|
265
|
+
return dim("recovery", score, confidence, {
|
|
266
|
+
detail: `state_repair_required=${repairRequired}`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// ──────────────────────────────────────────────
|
|
270
|
+
// Worker dimension scorers (per child)
|
|
271
|
+
// ──────────────────────────────────────────────
|
|
272
|
+
/**
|
|
273
|
+
* token: Token usage efficiency.
|
|
274
|
+
* Uses tokens_by_child from telemetry. If no per-child token data:
|
|
275
|
+
* falls back to heartbeat_count as a proxy (medium confidence).
|
|
276
|
+
*/
|
|
277
|
+
function scoreWorkerToken(child, ev) {
|
|
278
|
+
const tokensUsed = ev.tokens.tokens_by_child[child.child_id];
|
|
279
|
+
if (tokensUsed !== undefined) {
|
|
280
|
+
// Per-child token budget: 200k = 1.0, 500k = 0.0
|
|
281
|
+
const BUDGET = 200_000;
|
|
282
|
+
const MAX_PENALIZED = 500_000;
|
|
283
|
+
const score = tokensUsed <= BUDGET
|
|
284
|
+
? 1.0
|
|
285
|
+
: clamp01(1.0 - (tokensUsed - BUDGET) / (MAX_PENALIZED - BUDGET));
|
|
286
|
+
return dim("token", Number(score.toFixed(4)), "high", { detail: `tokens_used=${tokensUsed}` });
|
|
287
|
+
}
|
|
288
|
+
// Fallback: heartbeat count proxy (low confidence)
|
|
289
|
+
if (child.heartbeat_count > 0) {
|
|
290
|
+
return dim("token", null, "none", {
|
|
291
|
+
skipped_reason: "no per-child token data in heartbeat events; tokens_by_child unavailable",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return skipped("token", "no token usage evidence available for this child");
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* duration: Runtime duration proxy via heartbeat count.
|
|
298
|
+
* Higher heartbeats = longer / more active run.
|
|
299
|
+
* Score = 1.0 at 1–6 heartbeats; penalty above 10 (runaway duration).
|
|
300
|
+
*/
|
|
301
|
+
function scoreWorkerDuration(child) {
|
|
302
|
+
const hb = child.heartbeat_count;
|
|
303
|
+
if (hb === 0)
|
|
304
|
+
return skipped("duration", "no heartbeat count evidence for this child");
|
|
305
|
+
// Heartbeats > 10 suggest a long/troubled run
|
|
306
|
+
const EXPECTED_MAX = 10;
|
|
307
|
+
const score = hb <= EXPECTED_MAX ? 1.0 : clamp01(1.0 - (hb - EXPECTED_MAX) * 0.05);
|
|
308
|
+
return dim("duration", Number(score.toFixed(4)), "medium", { detail: `heartbeat_count=${hb}` });
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* validation: Outcome of the worker's validation commands.
|
|
312
|
+
* Score = 1.0 for "passed", 0.0 for "failed", skipped for "skipped"/"unknown".
|
|
313
|
+
*/
|
|
314
|
+
function scoreWorkerValidation(child) {
|
|
315
|
+
const v = child.validation;
|
|
316
|
+
if (v === "passed")
|
|
317
|
+
return dim("validation", 1.0, "high", { detail: "validation=passed" });
|
|
318
|
+
if (v === "failed")
|
|
319
|
+
return dim("validation", 0.0, "high", { detail: "validation=failed" });
|
|
320
|
+
return skipped("validation", `validation outcome is '${v}'`);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* qc: QC findings attributed to this child.
|
|
324
|
+
* Uses per-child weighted score from QC provider signals.
|
|
325
|
+
* When QC is unavailable, dimension is skipped.
|
|
326
|
+
*/
|
|
327
|
+
function scoreWorkerQc(child, ev) {
|
|
328
|
+
if (ev.qc.availability === "future" || ev.qc.availability === "unavailable") {
|
|
329
|
+
return skipped("qc", `QC evidence availability=${ev.qc.availability}`);
|
|
330
|
+
}
|
|
331
|
+
// Find the child's signal in qc evidence (not directly on ev.qc, but
|
|
332
|
+
// QcScoreSummary's recurring_child_signals is on DiagnosisReport.qc_summary,
|
|
333
|
+
// not SolQcEvidence). We use ev.qc.total_findings as a proxy.
|
|
334
|
+
// Per-child QC attribution is not surfaced in SolEvidence v1 — skipped with note.
|
|
335
|
+
// ponytail: when per-child QC attribution is added to SolEvidence, replace this proxy.
|
|
336
|
+
return skipped("qc", "per-child QC attribution not available in SolEvidence v1; see qc_summary.recurring_child_signals in DiagnosisReport");
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* repair_iterations: Escalation count proxy for re-work.
|
|
340
|
+
* 0 escalations = 1.0; each escalation reduces score.
|
|
341
|
+
*/
|
|
342
|
+
function scoreWorkerRepairIterations(child) {
|
|
343
|
+
const esc = child.escalation_count;
|
|
344
|
+
const score = clamp01(1.0 - esc * 0.25);
|
|
345
|
+
const confidence = esc > 0 ? "high" : "medium";
|
|
346
|
+
return dim("repair_iterations", Number(score.toFixed(4)), confidence, {
|
|
347
|
+
detail: `escalation_count=${esc}`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* scope_adherence: Out-of-scope blocks detected in telemetry.
|
|
352
|
+
* This is a run-level signal (not per-child in SolEvidence v1).
|
|
353
|
+
* Score = 1.0 when worker didn't raise out-of-scope; 0.0 when it did.
|
|
354
|
+
* Uses the child's next_recommended_action as a fallback signal.
|
|
355
|
+
*/
|
|
356
|
+
function scoreWorkerScopeAdherence(child, ev) {
|
|
357
|
+
// Check if this child was blocked (out-of-scope)
|
|
358
|
+
if (child.status === "blocked") {
|
|
359
|
+
return dim("scope_adherence", 0.0, "high", { detail: "worker status=blocked" });
|
|
360
|
+
}
|
|
361
|
+
// Check intervention.out_of_scope_count at run level as a proxy
|
|
362
|
+
// (not per-child in v1, but if there are no out-of-scope events at all, this child is clean)
|
|
363
|
+
if (ev.intervention.out_of_scope_count > 0) {
|
|
364
|
+
// Can't attribute to a specific child in v1 — low confidence
|
|
365
|
+
return dim("scope_adherence", 0.5, "low", {
|
|
366
|
+
detail: `run has ${ev.intervention.out_of_scope_count} out-of-scope events; per-child attribution unavailable`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
// No out-of-scope events in the run and child is not blocked
|
|
370
|
+
const hasTelemetrySignal = ev.tokens.total_worker_heartbeats > 0;
|
|
371
|
+
const confidence = hasTelemetrySignal ? "medium" : "low";
|
|
372
|
+
return dim("scope_adherence", 1.0, confidence, { detail: "no out-of-scope events observed" });
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* acceptance_criteria: Validation commands completed successfully.
|
|
376
|
+
* Mirrors the validation dimension but focused on "did the child meet its
|
|
377
|
+
* acceptance criteria" — uses validation outcome plus status.
|
|
378
|
+
*/
|
|
379
|
+
function scoreWorkerAcceptanceCriteria(child) {
|
|
380
|
+
if (child.status === "done" && child.validation === "passed") {
|
|
381
|
+
return dim("acceptance_criteria", 1.0, "high", {
|
|
382
|
+
detail: "status=done, validation=passed",
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
if (child.status === "failed" || child.validation === "failed") {
|
|
386
|
+
return dim("acceptance_criteria", 0.0, "high", {
|
|
387
|
+
detail: `status=${child.status}, validation=${child.validation}`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (child.status === "blocked") {
|
|
391
|
+
return dim("acceptance_criteria", 0.0, "high", { detail: "status=blocked" });
|
|
392
|
+
}
|
|
393
|
+
return skipped("acceptance_criteria", `insufficient evidence: status=${child.status}, validation=${child.validation}`);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* first_pass: Did the worker succeed without foreman/user intervention?
|
|
397
|
+
* Score = 1.0 when user_intervened=false and foreman_intervened=false.
|
|
398
|
+
* Skipped when both are null (not yet scored).
|
|
399
|
+
*/
|
|
400
|
+
function scoreWorkerFirstPass(child) {
|
|
401
|
+
const { user_intervened, foreman_intervened } = child;
|
|
402
|
+
if (user_intervened === null && foreman_intervened === null) {
|
|
403
|
+
return skipped("first_pass", "intervention flags not yet scored (user_intervened=null, foreman_intervened=null)");
|
|
404
|
+
}
|
|
405
|
+
if (user_intervened === true) {
|
|
406
|
+
return dim("first_pass", 0.0, "high", { detail: "user_intervened=true" });
|
|
407
|
+
}
|
|
408
|
+
if (foreman_intervened === true) {
|
|
409
|
+
return dim("first_pass", 0.5, "high", { detail: "foreman_intervened=true" });
|
|
410
|
+
}
|
|
411
|
+
return dim("first_pass", 1.0, "high", { detail: "no user or foreman intervention" });
|
|
412
|
+
}
|
|
413
|
+
// ──────────────────────────────────────────────
|
|
414
|
+
// Public API
|
|
415
|
+
// ──────────────────────────────────────────────
|
|
416
|
+
/**
|
|
417
|
+
* Compute a SOL score report for the Foreman role from aggregated evidence.
|
|
418
|
+
*
|
|
419
|
+
* @param ev — SolEvidence loaded via aggregateSolEvidence
|
|
420
|
+
* @returns SolForemanScoreReport
|
|
421
|
+
*/
|
|
422
|
+
function computeForemanScore(ev) {
|
|
423
|
+
const tokenDim = scoreForemanToken(ev);
|
|
424
|
+
const durationDim = scoreForemanDuration(ev);
|
|
425
|
+
const interventionDim = scoreForemanIntervention(ev);
|
|
426
|
+
const preAnalysisDim = scoreForemanPreAnalysis(ev);
|
|
427
|
+
const dependencyDim = scoreForemanDependency(ev);
|
|
428
|
+
const dispatchDim = scoreForemanDispatch(ev);
|
|
429
|
+
const evidenceValidationDim = scoreForemanEvidenceValidation(ev);
|
|
430
|
+
const scopeDim = scoreForemanScope(ev);
|
|
431
|
+
const completionDim = scoreForemanCompletion(ev);
|
|
432
|
+
const recoveryDim = scoreForemanRecovery(ev);
|
|
433
|
+
const qcRepairLoopDim = scoreForemanQcRepairLoop(ev);
|
|
434
|
+
const dims = [
|
|
435
|
+
tokenDim, durationDim, interventionDim, preAnalysisDim,
|
|
436
|
+
dependencyDim, dispatchDim, evidenceValidationDim, scopeDim,
|
|
437
|
+
completionDim, recoveryDim, qcRepairLoopDim,
|
|
438
|
+
];
|
|
439
|
+
const { score: composite_score, confidence: composite_confidence } = composite(dims);
|
|
440
|
+
return {
|
|
441
|
+
composite_score,
|
|
442
|
+
composite_confidence,
|
|
443
|
+
token: tokenDim,
|
|
444
|
+
duration: durationDim,
|
|
445
|
+
intervention: interventionDim,
|
|
446
|
+
pre_analysis: preAnalysisDim,
|
|
447
|
+
dependency: dependencyDim,
|
|
448
|
+
dispatch: dispatchDim,
|
|
449
|
+
evidence_validation: evidenceValidationDim,
|
|
450
|
+
scope: scopeDim,
|
|
451
|
+
completion: completionDim,
|
|
452
|
+
recovery: recoveryDim,
|
|
453
|
+
qc_repair_loop: qcRepairLoopDim,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Compute a SOL score report for a single Worker child from aggregated evidence.
|
|
458
|
+
*
|
|
459
|
+
* @param childId — The child to score
|
|
460
|
+
* @param ev — SolEvidence loaded via aggregateSolEvidence
|
|
461
|
+
* @returns SolWorkerScoreReport or null when the child is not found in evidence
|
|
462
|
+
*/
|
|
463
|
+
function computeWorkerScore(childId, ev) {
|
|
464
|
+
const child = ev.children.find((c) => c.child_id === childId);
|
|
465
|
+
if (!child)
|
|
466
|
+
return null;
|
|
467
|
+
const tokenDim = scoreWorkerToken(child, ev);
|
|
468
|
+
const durationDim = scoreWorkerDuration(child);
|
|
469
|
+
const validationDim = scoreWorkerValidation(child);
|
|
470
|
+
const qcDim = scoreWorkerQc(child, ev);
|
|
471
|
+
const repairIterationsDim = scoreWorkerRepairIterations(child);
|
|
472
|
+
const scopeAdherenceDim = scoreWorkerScopeAdherence(child, ev);
|
|
473
|
+
const acceptanceCriteriaDim = scoreWorkerAcceptanceCriteria(child);
|
|
474
|
+
const firstPassDim = scoreWorkerFirstPass(child);
|
|
475
|
+
const dims = [
|
|
476
|
+
tokenDim, durationDim, validationDim, qcDim, repairIterationsDim,
|
|
477
|
+
scopeAdherenceDim, acceptanceCriteriaDim, firstPassDim,
|
|
478
|
+
];
|
|
479
|
+
const { score: composite_score, confidence: composite_confidence } = composite(dims);
|
|
480
|
+
return {
|
|
481
|
+
child_id: childId,
|
|
482
|
+
composite_score,
|
|
483
|
+
composite_confidence,
|
|
484
|
+
token: tokenDim,
|
|
485
|
+
duration: durationDim,
|
|
486
|
+
validation: validationDim,
|
|
487
|
+
qc: qcDim,
|
|
488
|
+
repair_iterations: repairIterationsDim,
|
|
489
|
+
scope_adherence: scopeAdherenceDim,
|
|
490
|
+
acceptance_criteria: acceptanceCriteriaDim,
|
|
491
|
+
first_pass: firstPassDim,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Compute a full SOL score report for a run (Foreman + all Workers).
|
|
496
|
+
*
|
|
497
|
+
* @param ev — SolEvidence loaded via aggregateSolEvidence
|
|
498
|
+
* @returns SolScoreReport
|
|
499
|
+
*/
|
|
500
|
+
function computeSolScoreReport(ev) {
|
|
501
|
+
const foreman = computeForemanScore(ev);
|
|
502
|
+
const workers = {};
|
|
503
|
+
for (const child of ev.children) {
|
|
504
|
+
const report = computeWorkerScore(child.child_id, ev);
|
|
505
|
+
if (report)
|
|
506
|
+
workers[child.child_id] = report;
|
|
507
|
+
}
|
|
508
|
+
// Run composite = mean of foreman + all workers
|
|
509
|
+
const composites = [
|
|
510
|
+
foreman.composite_score,
|
|
511
|
+
...Object.values(workers).map((w) => w.composite_score),
|
|
512
|
+
].filter((v) => v !== null);
|
|
513
|
+
const run_composite_score = composites.length > 0
|
|
514
|
+
? Number((composites.reduce((s, v) => s + v, 0) / composites.length).toFixed(4))
|
|
515
|
+
: null;
|
|
516
|
+
return {
|
|
517
|
+
run_id: ev.run_id,
|
|
518
|
+
cluster_id: ev.cluster_id,
|
|
519
|
+
scored_at: new Date().toISOString(),
|
|
520
|
+
foreman,
|
|
521
|
+
workers,
|
|
522
|
+
run_composite_score,
|
|
523
|
+
};
|
|
524
|
+
}
|