@lsctech/polaris 0.5.7 → 0.5.8

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,865 @@
1
+ "use strict";
2
+ /**
3
+ * SOL scorecard calculator.
4
+ *
5
+ * Computes durable, versioned scorecards for SOL evaluation subjects:
6
+ * Foreman, worker, provider, model, and routing decisions.
7
+ *
8
+ * Design rules:
9
+ * - Reuses the existing SolScoreReport scoring logic where available to
10
+ * avoid conflicting formulas (POL-481).
11
+ * - Each scorecard preserves raw metrics and carries formula versions.
12
+ * - Missing evidence results in skipped subscores with reasons.
13
+ * - No routing policy or provider selection behavior is changed.
14
+ *
15
+ * Scope: src/autoresearch/**
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.buildForemanRawMetrics = buildForemanRawMetrics;
19
+ exports.buildWorkerRawMetrics = buildWorkerRawMetrics;
20
+ exports.buildProviderRawMetrics = buildProviderRawMetrics;
21
+ exports.buildModelRawMetrics = buildModelRawMetrics;
22
+ exports.buildRoutingRawMetrics = buildRoutingRawMetrics;
23
+ exports.computeForemanScorecard = computeForemanScorecard;
24
+ exports.computeWorkerScorecard = computeWorkerScorecard;
25
+ exports.computeProviderScorecard = computeProviderScorecard;
26
+ exports.computeModelScorecard = computeModelScorecard;
27
+ exports.computeRoutingScorecard = computeRoutingScorecard;
28
+ exports.computeAllScorecards = computeAllScorecards;
29
+ const sol_scorecard_js_1 = require("../types/sol-scorecard.js");
30
+ const sol_scorer_js_1 = require("./sol-scorer.js");
31
+ // ──────────────────────────────────────────────
32
+ // Helpers
33
+ // ──────────────────────────────────────────────
34
+ function clamp01(v) {
35
+ return Math.max(0, Math.min(1, v));
36
+ }
37
+ function subscore(dimension, formulaVersion, score, confidence, opts = {}) {
38
+ return { dimension, formula_version: formulaVersion, score, confidence, ...opts };
39
+ }
40
+ function skippedSubscore(dimension, formulaVersion, reason) {
41
+ return subscore(dimension, formulaVersion, null, "none", { skipped_reason: reason });
42
+ }
43
+ function toSolSubscore(dim, formulaVersion) {
44
+ return {
45
+ dimension: dim.dimension,
46
+ formula_version: formulaVersion,
47
+ score: dim.score,
48
+ confidence: dim.confidence,
49
+ skipped_reason: dim.skipped_reason,
50
+ detail: dim.detail,
51
+ };
52
+ }
53
+ function confidenceFromCount(count) {
54
+ if (count >= 5)
55
+ return "high";
56
+ if (count >= 2)
57
+ return "medium";
58
+ return "low";
59
+ }
60
+ function makeWindow(ev) {
61
+ return {
62
+ run_id: ev.run_id,
63
+ cluster_id: ev.cluster_id ?? undefined,
64
+ sample_count: ev.run.total_children > 0 ? ev.run.total_children : undefined,
65
+ };
66
+ }
67
+ function defaultSourceRefs(ev) {
68
+ const refs = [
69
+ { kind: "run-state", path: ".taskchain_artifacts/polaris-run/current-state.json", available: ev.run.status !== null },
70
+ { kind: "telemetry", path: `.taskchain_artifacts/polaris-run/runs/${ev.run_id}/telemetry.jsonl`, available: ev.tokens.total_worker_heartbeats > 0 },
71
+ ];
72
+ if (ev.cluster_id) {
73
+ refs.push({ kind: "cluster-state", path: `.polaris/clusters/${ev.cluster_id}/cluster-state.json`, available: true });
74
+ }
75
+ return refs;
76
+ }
77
+ // ──────────────────────────────────────────────
78
+ // Raw metric builders
79
+ // ──────────────────────────────────────────────
80
+ function emptyRawMetrics() {
81
+ return {
82
+ max_bootstrap_tokens: null,
83
+ worker_tokens_used: null,
84
+ dispatch_epoch: null,
85
+ continue_epoch: null,
86
+ total_children: null,
87
+ workers_succeeded: null,
88
+ workers_failed: null,
89
+ redispatch_count: null,
90
+ validation_outcome: null,
91
+ passed_commands: [],
92
+ qc_total_findings: null,
93
+ qc_blocking_findings: null,
94
+ qc_repaired_findings: null,
95
+ qc_repair_loop_status: null,
96
+ qc_repair_rounds: null,
97
+ escalation_count: null,
98
+ out_of_scope_count: null,
99
+ user_intervened: null,
100
+ foreman_intervened: null,
101
+ state_repair_required: null,
102
+ provider_selected: null,
103
+ router_fallback_used: null,
104
+ router_exhausted: null,
105
+ router_exhausted_reason: null,
106
+ provider_decisions: null,
107
+ provider_startup_failures: null,
108
+ provider_exhausted_decisions: null,
109
+ provider_fallback_attempts: null,
110
+ provider_successful_fallbacks: null,
111
+ model_decisions: null,
112
+ model_startup_failures: null,
113
+ model_exhausted_decisions: null,
114
+ model_fallback_attempts: null,
115
+ model_successful_fallbacks: null,
116
+ router_candidates_count: null,
117
+ router_child_status: null,
118
+ router_child_validation: null,
119
+ heartbeat_count: null,
120
+ };
121
+ }
122
+ function buildForemanRawMetrics(ev) {
123
+ const run = ev.run;
124
+ const foreman = ev.foreman;
125
+ const worker = ev.worker;
126
+ const intervention = ev.intervention;
127
+ return {
128
+ ...emptyRawMetrics(),
129
+ max_bootstrap_tokens: foreman.max_bootstrap_tokens,
130
+ dispatch_epoch: run.dispatch_epoch,
131
+ continue_epoch: run.continue_epoch,
132
+ total_children: run.total_children,
133
+ workers_succeeded: worker.workers_succeeded,
134
+ workers_failed: worker.workers_failed,
135
+ redispatch_count: foreman.redispatch_count,
136
+ qc_total_findings: ev.qc.total_findings,
137
+ qc_blocking_findings: ev.qc.blocking_findings,
138
+ qc_repaired_findings: ev.qc.repaired_findings,
139
+ qc_repair_loop_status: ev.qc.repair_loop?.status ?? null,
140
+ qc_repair_rounds: ev.qc.repair_loop
141
+ ? { completed: ev.qc.repair_loop.rounds_completed, max: ev.qc.repair_loop.max_rounds }
142
+ : null,
143
+ escalation_count: foreman.escalation_events,
144
+ out_of_scope_count: intervention.out_of_scope_count,
145
+ user_intervened: intervention.user_intervened,
146
+ foreman_intervened: intervention.foreman_intervened,
147
+ state_repair_required: intervention.state_repair_required,
148
+ heartbeat_count: worker.total_heartbeats,
149
+ };
150
+ }
151
+ function buildWorkerRawMetrics(ev, child) {
152
+ const validation = ev.validation.find((v) => v.child_id === child.child_id);
153
+ return {
154
+ ...emptyRawMetrics(),
155
+ worker_tokens_used: ev.tokens.tokens_by_child[child.child_id] ?? null,
156
+ validation_outcome: validation?.outcome ?? child.validation,
157
+ passed_commands: validation?.passed_commands ?? [],
158
+ qc_total_findings: ev.qc.total_findings,
159
+ qc_blocking_findings: ev.qc.blocking_findings,
160
+ qc_repaired_findings: ev.qc.repaired_findings,
161
+ qc_repair_loop_status: ev.qc.repair_loop?.status ?? null,
162
+ escalation_count: child.escalation_count,
163
+ user_intervened: child.user_intervened,
164
+ foreman_intervened: child.foreman_intervened,
165
+ heartbeat_count: child.heartbeat_count,
166
+ };
167
+ }
168
+ function aggregateForProvider(ev, provider) {
169
+ const children = ev.children.filter((c) => c.provider === provider);
170
+ const decisions = ev.router.decisions.filter((d) => d.selected_provider === provider || d.providers_tried.includes(provider));
171
+ let startupFailures = 0;
172
+ let exhaustedDecisions = 0;
173
+ let fallbackAttempts = 0;
174
+ let successfulFallbacks = 0;
175
+ for (const d of decisions) {
176
+ if (d.exhausted && (d.selected_provider ?? d.providers_tried[d.providers_tried.length - 1]) === provider) {
177
+ startupFailures++;
178
+ exhaustedDecisions++;
179
+ }
180
+ else if (d.exhausted && d.providers_tried.includes(provider)) {
181
+ exhaustedDecisions++;
182
+ }
183
+ if (d.fallback_used && d.providers_tried.includes(provider)) {
184
+ fallbackAttempts++;
185
+ const child = ev.children.find((c) => c.child_id === d.child_id);
186
+ if (child && child.status === "done")
187
+ successfulFallbacks++;
188
+ }
189
+ }
190
+ let succeeded = 0;
191
+ let failed = 0;
192
+ let blocked = 0;
193
+ let totalHeartbeats = 0;
194
+ let totalTokens = 0;
195
+ let tokenEvidenceMissing = false;
196
+ let validationPasses = 0;
197
+ let validationFailures = 0;
198
+ for (const c of children) {
199
+ if (c.status === "done")
200
+ succeeded++;
201
+ else if (c.status === "failed" || c.status === "error")
202
+ failed++;
203
+ else if (c.status === "blocked")
204
+ blocked++;
205
+ totalHeartbeats += c.heartbeat_count;
206
+ if (!Object.prototype.hasOwnProperty.call(ev.tokens.tokens_by_child, c.child_id)) {
207
+ tokenEvidenceMissing = true;
208
+ }
209
+ else {
210
+ totalTokens += ev.tokens.tokens_by_child[c.child_id] ?? 0;
211
+ }
212
+ const v = ev.validation.find((val) => val.child_id === c.child_id);
213
+ const outcome = v?.outcome ?? c.validation;
214
+ if (outcome === "passed")
215
+ validationPasses++;
216
+ else if (outcome === "failed")
217
+ validationFailures++;
218
+ }
219
+ return {
220
+ children,
221
+ decisions,
222
+ startupFailures,
223
+ exhaustedDecisions,
224
+ fallbackAttempts,
225
+ successfulFallbacks,
226
+ succeeded,
227
+ failed,
228
+ blocked,
229
+ totalHeartbeats,
230
+ totalTokens: children.length > 0 && !tokenEvidenceMissing ? totalTokens : null,
231
+ validationPasses,
232
+ validationFailures,
233
+ };
234
+ }
235
+ function buildProviderRawMetrics(ev, provider) {
236
+ const agg = aggregateForProvider(ev, provider);
237
+ // For provider/model scorecards, store mean-per-child tokens so over_token_budget
238
+ // comparisons against WORKER_TOKEN_EFFICIENCY_SPEC.budget remain valid (POL-509).
239
+ const meanTokensPerChild = agg.totalTokens !== null && agg.children.length > 0
240
+ ? Math.round(agg.totalTokens / agg.children.length)
241
+ : null;
242
+ return {
243
+ ...emptyRawMetrics(),
244
+ total_children: agg.children.length,
245
+ workers_succeeded: agg.succeeded,
246
+ workers_failed: agg.failed,
247
+ worker_tokens_used: meanTokensPerChild,
248
+ validation_outcome: agg.validationPasses > 0 || agg.validationFailures > 0
249
+ ? agg.validationFailures === 0
250
+ ? "passed"
251
+ : "failed"
252
+ : null,
253
+ passed_commands: agg.validationFailures === 0 && agg.validationPasses > 0 ? ["aggregate"] : [],
254
+ qc_total_findings: ev.qc.total_findings,
255
+ qc_blocking_findings: ev.qc.blocking_findings,
256
+ provider_selected: provider,
257
+ provider_decisions: agg.decisions.length,
258
+ provider_startup_failures: agg.startupFailures,
259
+ provider_exhausted_decisions: agg.exhaustedDecisions,
260
+ provider_fallback_attempts: agg.fallbackAttempts,
261
+ provider_successful_fallbacks: agg.successfulFallbacks,
262
+ heartbeat_count: agg.totalHeartbeats > 0 ? agg.totalHeartbeats : null,
263
+ };
264
+ }
265
+ function aggregateForModel(ev, model) {
266
+ const children = ev.children.filter((c) => c.grouping_keys.model === model);
267
+ // Model attribution on router decisions is best-effort via child grouping keys.
268
+ const childIds = new Set(children.map((c) => c.child_id));
269
+ const decisions = ev.router.decisions.filter((d) => childIds.has(d.child_id));
270
+ let startupFailures = 0;
271
+ let exhaustedDecisions = 0;
272
+ let fallbackAttempts = 0;
273
+ let successfulFallbacks = 0;
274
+ for (const d of decisions) {
275
+ if (d.exhausted) {
276
+ startupFailures++;
277
+ exhaustedDecisions++;
278
+ }
279
+ if (d.fallback_used) {
280
+ fallbackAttempts++;
281
+ const child = ev.children.find((c) => c.child_id === d.child_id);
282
+ if (child && child.status === "done")
283
+ successfulFallbacks++;
284
+ }
285
+ }
286
+ let succeeded = 0;
287
+ let failed = 0;
288
+ let blocked = 0;
289
+ let totalHeartbeats = 0;
290
+ let totalTokens = 0;
291
+ let tokenEvidenceMissing = false;
292
+ let validationPasses = 0;
293
+ let validationFailures = 0;
294
+ for (const c of children) {
295
+ if (c.status === "done")
296
+ succeeded++;
297
+ else if (c.status === "failed" || c.status === "error")
298
+ failed++;
299
+ else if (c.status === "blocked")
300
+ blocked++;
301
+ totalHeartbeats += c.heartbeat_count;
302
+ if (!Object.prototype.hasOwnProperty.call(ev.tokens.tokens_by_child, c.child_id)) {
303
+ tokenEvidenceMissing = true;
304
+ }
305
+ else {
306
+ totalTokens += ev.tokens.tokens_by_child[c.child_id] ?? 0;
307
+ }
308
+ const v = ev.validation.find((val) => val.child_id === c.child_id);
309
+ const outcome = v?.outcome ?? c.validation;
310
+ if (outcome === "passed")
311
+ validationPasses++;
312
+ else if (outcome === "failed")
313
+ validationFailures++;
314
+ }
315
+ return {
316
+ children,
317
+ decisions,
318
+ startupFailures,
319
+ exhaustedDecisions,
320
+ fallbackAttempts,
321
+ successfulFallbacks,
322
+ succeeded,
323
+ failed,
324
+ blocked,
325
+ totalHeartbeats,
326
+ totalTokens: children.length > 0 && !tokenEvidenceMissing ? totalTokens : null,
327
+ validationPasses,
328
+ validationFailures,
329
+ };
330
+ }
331
+ function buildModelRawMetrics(ev, model) {
332
+ const agg = aggregateForModel(ev, model);
333
+ // For provider/model scorecards, store mean-per-child tokens so over_token_budget
334
+ // comparisons against WORKER_TOKEN_EFFICIENCY_SPEC.budget remain valid (POL-509).
335
+ const meanTokensPerChild = agg.totalTokens !== null && agg.children.length > 0
336
+ ? Math.round(agg.totalTokens / agg.children.length)
337
+ : null;
338
+ return {
339
+ ...emptyRawMetrics(),
340
+ total_children: agg.children.length,
341
+ workers_succeeded: agg.succeeded,
342
+ workers_failed: agg.failed,
343
+ worker_tokens_used: meanTokensPerChild,
344
+ validation_outcome: agg.validationPasses > 0 || agg.validationFailures > 0
345
+ ? agg.validationFailures === 0
346
+ ? "passed"
347
+ : "failed"
348
+ : null,
349
+ passed_commands: agg.validationFailures === 0 && agg.validationPasses > 0 ? ["aggregate"] : [],
350
+ qc_total_findings: ev.qc.total_findings,
351
+ qc_blocking_findings: ev.qc.blocking_findings,
352
+ model_decisions: agg.decisions.length,
353
+ model_startup_failures: agg.startupFailures,
354
+ model_exhausted_decisions: agg.exhaustedDecisions,
355
+ model_fallback_attempts: agg.fallbackAttempts,
356
+ model_successful_fallbacks: agg.successfulFallbacks,
357
+ heartbeat_count: agg.totalHeartbeats > 0 ? agg.totalHeartbeats : null,
358
+ };
359
+ }
360
+ function buildRoutingRawMetrics(ev, decision) {
361
+ const child = ev.children.find((c) => c.child_id === decision.child_id);
362
+ const validation = ev.validation.find((v) => v.child_id === decision.child_id);
363
+ return {
364
+ ...emptyRawMetrics(),
365
+ total_children: child ? 1 : 0,
366
+ workers_succeeded: child?.status === "done" ? 1 : 0,
367
+ workers_failed: child && (child.status === "failed" || child.status === "error") ? 1 : 0,
368
+ worker_tokens_used: child ? ev.tokens.tokens_by_child[child.child_id] ?? null : null,
369
+ validation_outcome: validation?.outcome ?? child?.validation ?? null,
370
+ passed_commands: validation?.passed_commands ?? [],
371
+ qc_total_findings: ev.qc.total_findings,
372
+ qc_blocking_findings: ev.qc.blocking_findings,
373
+ provider_selected: decision.selected_provider,
374
+ router_fallback_used: decision.fallback_used,
375
+ router_exhausted: decision.exhausted,
376
+ router_exhausted_reason: decision.exhausted_reason,
377
+ router_candidates_count: decision.providers_tried.length,
378
+ router_child_status: child?.status ?? null,
379
+ router_child_validation: validation?.outcome ?? child?.validation ?? null,
380
+ heartbeat_count: child?.heartbeat_count ?? null,
381
+ };
382
+ }
383
+ // ──────────────────────────────────────────────
384
+ // Scorecard assembly
385
+ // ──────────────────────────────────────────────
386
+ function buildScorecard(subject, subjectKey, ev, rawMetrics, subscores, sourceRefs, groupingKeys) {
387
+ const aggregateScore = (0, sol_scorecard_js_1.computeAggregateScore)(subscores);
388
+ const availability = (0, sol_scorecard_js_1.determineScorecardAvailability)(subscores);
389
+ const availabilityReason = availability === "unavailable"
390
+ ? "No scoreable dimensions were produced"
391
+ : availability === "skipped"
392
+ ? "All dimensions were skipped due to missing evidence"
393
+ : undefined;
394
+ const scoredConfidences = subscores
395
+ .filter((s) => s.score !== null)
396
+ .map((s) => s.confidence);
397
+ const aggregateConfidence = scoredConfidences.length === 0
398
+ ? "none"
399
+ : (scoredConfidences.reduce((worst, c) => {
400
+ const rank = { high: 3, medium: 2, low: 1, none: 0 };
401
+ return rank[c] < rank[worst] ? c : worst;
402
+ }, "high"));
403
+ return {
404
+ schema_version: "1.0",
405
+ scorecard_id: `${subject}-${subjectKey}-${ev.run_id}`,
406
+ subject,
407
+ subject_key: subjectKey,
408
+ window: makeWindow(ev),
409
+ grouping_keys: groupingKeys ?? ev.grouping_keys,
410
+ generated_at: new Date().toISOString(),
411
+ availability,
412
+ availability_reason: availabilityReason,
413
+ raw_metrics: rawMetrics,
414
+ subscores,
415
+ aggregate_score: aggregateScore,
416
+ aggregate_confidence: aggregateConfidence,
417
+ source_refs: sourceRefs,
418
+ recommendation_inputs: (0, sol_scorecard_js_1.buildRecommendationInputs)(subscores, rawMetrics, aggregateScore),
419
+ aggregate_formula_version: sol_scorecard_js_1.SOL_FORMULA_VERSIONS.COMPOSITE_MEAN_V1,
420
+ };
421
+ }
422
+ // ──────────────────────────────────────────────
423
+ // Foreman scorecard
424
+ // ──────────────────────────────────────────────
425
+ /**
426
+ * Compute a scorecard for the Foreman subject.
427
+ */
428
+ function computeForemanScorecard(ev, sourceRefs, groupingKeys) {
429
+ const report = (0, sol_scorer_js_1.computeForemanScore)(ev);
430
+ const rawMetrics = buildForemanRawMetrics(ev);
431
+ const subscores = [
432
+ toSolSubscore(report.token, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.TOKEN_EFFICIENCY_V1),
433
+ toSolSubscore(report.duration, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.RUNTIME_PROXY_V1),
434
+ toSolSubscore(report.intervention, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.INTERVENTION_BINARY_V1),
435
+ toSolSubscore(report.dependency, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.DISPATCH_RATE_V1),
436
+ toSolSubscore(report.dispatch, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.DISPATCH_RATE_V1),
437
+ toSolSubscore(report.evidence_validation, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.VALIDATION_BINARY_V1),
438
+ toSolSubscore(report.scope, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.SCOPE_ADHERENCE_V1),
439
+ toSolSubscore(report.completion, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUALITY_OUTCOMES_V1),
440
+ toSolSubscore(report.recovery, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.INTERVENTION_BINARY_V1),
441
+ toSolSubscore(report.qc_repair_loop, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QC_REPAIR_LOOP_V1),
442
+ ];
443
+ // Quality-per-token is a synthetic dimension for the Foreman scorecard.
444
+ const qpt = computeQualityPerToken(rawMetrics, report.composite_score, sol_scorecard_js_1.FOREMAN_QUALITY_PER_TOKEN_SPEC.budget_denominator);
445
+ if (qpt)
446
+ subscores.push(qpt);
447
+ return buildScorecard("foreman", ev.run_id, ev, rawMetrics, subscores, sourceRefs ?? defaultSourceRefs(ev), groupingKeys);
448
+ }
449
+ // ──────────────────────────────────────────────
450
+ // Worker scorecard
451
+ // ──────────────────────────────────────────────
452
+ /**
453
+ * Compute a scorecard for a single worker child.
454
+ * Returns null when the child is not found in evidence.
455
+ */
456
+ function computeWorkerScorecard(ev, childId, sourceRefs, groupingKeys) {
457
+ const child = ev.children.find((c) => c.child_id === childId);
458
+ if (!child)
459
+ return null;
460
+ const report = (0, sol_scorer_js_1.computeWorkerScore)(childId, ev);
461
+ if (!report)
462
+ return null;
463
+ const rawMetrics = buildWorkerRawMetrics(ev, child);
464
+ const subscores = [
465
+ toSolSubscore(report.token, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.TOKEN_EFFICIENCY_V1),
466
+ toSolSubscore(report.duration, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.RUNTIME_PROXY_V1),
467
+ toSolSubscore(report.validation, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.VALIDATION_BINARY_V1),
468
+ toSolSubscore(report.qc, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QC_REPAIR_LOOP_V1),
469
+ toSolSubscore(report.repair_iterations, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QC_REPAIR_LOOP_V1),
470
+ toSolSubscore(report.scope_adherence, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.SCOPE_ADHERENCE_V1),
471
+ toSolSubscore(report.acceptance_criteria, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.VALIDATION_BINARY_V1),
472
+ toSolSubscore(report.first_pass, sol_scorecard_js_1.SOL_FORMULA_VERSIONS.INTERVENTION_BINARY_V1),
473
+ ];
474
+ const qpt = computeQualityPerToken(rawMetrics, report.composite_score, sol_scorecard_js_1.WORKER_QUALITY_PER_TOKEN_SPEC.budget_denominator);
475
+ if (qpt)
476
+ subscores.push(qpt);
477
+ return buildScorecard("worker", childId, ev, rawMetrics, subscores, sourceRefs ?? defaultSourceRefs(ev), groupingKeys ?? child.grouping_keys);
478
+ }
479
+ function computeQualityPerToken(rawMetrics, compositeScore, budget) {
480
+ const tokens = rawMetrics.worker_tokens_used ?? rawMetrics.max_bootstrap_tokens;
481
+ if (tokens === null || compositeScore === null)
482
+ return null;
483
+ if (tokens <= 0)
484
+ return null;
485
+ const normalizedCost = tokens / budget;
486
+ const value = compositeScore / normalizedCost;
487
+ // Cap quality-per-token at 2.0 (twice the budget efficiency) and map to 0–1
488
+ const score = clamp01(value / 2.0);
489
+ const confidence = rawMetrics.worker_tokens_used !== null ? "high" : rawMetrics.max_bootstrap_tokens !== null ? "medium" : "low";
490
+ return subscore("quality_per_token", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUALITY_PER_TOKEN_V1, Number(score.toFixed(4)), confidence, {
491
+ detail: `composite_score=${compositeScore}, tokens=${tokens}, budget=${budget}`,
492
+ });
493
+ }
494
+ // ──────────────────────────────────────────────
495
+ // Provider scorecard
496
+ // ──────────────────────────────────────────────
497
+ function scoreProviderStartupFailure(metrics) {
498
+ const total = metrics.provider_decisions ?? 0;
499
+ if (total === 0)
500
+ return skippedSubscore("startup_failure", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.STARTUP_FAILURE_RATE_V1, "no routing decisions for provider");
501
+ const failures = metrics.provider_startup_failures ?? 0;
502
+ const rate = failures / total;
503
+ const score = clamp01(1.0 - rate);
504
+ return subscore("startup_failure", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.STARTUP_FAILURE_RATE_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
505
+ detail: `startup_failures=${failures}, decisions=${total}`,
506
+ });
507
+ }
508
+ function scoreProviderQuotaExhaustion(metrics) {
509
+ const total = metrics.provider_decisions ?? 0;
510
+ if (total === 0)
511
+ return skippedSubscore("quota_exhaustion", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUOTA_EXHAUSTION_RATE_V1, "no routing decisions for provider");
512
+ const exhausted = metrics.provider_exhausted_decisions ?? 0;
513
+ const rate = exhausted / total;
514
+ const score = clamp01(1.0 - rate);
515
+ return subscore("quota_exhaustion", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUOTA_EXHAUSTION_RATE_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
516
+ detail: `exhausted_decisions=${exhausted}, decisions=${total}`,
517
+ });
518
+ }
519
+ function scoreProviderFallbackFrequency(metrics) {
520
+ const total = metrics.provider_decisions ?? 0;
521
+ if (total === 0)
522
+ return skippedSubscore("fallback_frequency", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.FALLBACK_FREQUENCY_V1, "no routing decisions for provider");
523
+ const fallbacks = metrics.provider_fallback_attempts ?? 0;
524
+ const rate = fallbacks / total;
525
+ // More fallbacks = lower score (provider needed help)
526
+ const score = clamp01(1.0 - rate);
527
+ return subscore("fallback_frequency", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.FALLBACK_FREQUENCY_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
528
+ detail: `fallback_attempts=${fallbacks}, successful_fallbacks=${metrics.provider_successful_fallbacks ?? 0}, decisions=${total}`,
529
+ });
530
+ }
531
+ function scoreProviderRoleSuitability(metrics) {
532
+ const total = metrics.total_children ?? 0;
533
+ if (total === 0)
534
+ return skippedSubscore("role_suitability", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROLE_SUITABILITY_V1, "no children assigned to provider");
535
+ const succeeded = metrics.workers_succeeded ?? 0;
536
+ const score = clamp01(succeeded / total);
537
+ return subscore("role_suitability", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROLE_SUITABILITY_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
538
+ detail: `succeeded=${succeeded}, total=${total}`,
539
+ });
540
+ }
541
+ function scoreProviderRuntime(metrics) {
542
+ const hb = metrics.heartbeat_count;
543
+ const total = metrics.total_children ?? 0;
544
+ if (hb === null || total === 0) {
545
+ return skippedSubscore("runtime", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.RUNTIME_PROXY_V1, "no heartbeat evidence for provider");
546
+ }
547
+ const mean = hb / total;
548
+ // Same proxy as worker duration: 1–10 heartbeats per child ideal
549
+ const EXPECTED_MAX = 10;
550
+ const score = clamp01(mean <= EXPECTED_MAX ? 1.0 : 1.0 - (mean - EXPECTED_MAX) * 0.05);
551
+ return subscore("runtime", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.RUNTIME_PROXY_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
552
+ detail: `mean_heartbeats_per_child=${mean.toFixed(1)}`,
553
+ });
554
+ }
555
+ function scoreProviderTokenEfficiency(metrics) {
556
+ const tokens = metrics.worker_tokens_used;
557
+ const total = metrics.total_children ?? 0;
558
+ if (tokens === null || total === 0) {
559
+ return skippedSubscore("token_efficiency", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.TOKEN_EFFICIENCY_V1, "no token evidence for provider");
560
+ }
561
+ const mean = tokens / total;
562
+ const BUDGET = sol_scorecard_js_1.WORKER_TOKEN_EFFICIENCY_SPEC.budget;
563
+ const MAX_PENALIZED = sol_scorecard_js_1.WORKER_TOKEN_EFFICIENCY_SPEC.max_penalized;
564
+ const score = mean <= BUDGET ? 1.0 : clamp01(1.0 - (mean - BUDGET) / (MAX_PENALIZED - BUDGET));
565
+ return subscore("token_efficiency", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.TOKEN_EFFICIENCY_V1, Number(score.toFixed(4)), "high", {
566
+ detail: `mean_tokens_per_child=${Math.round(mean)}`,
567
+ });
568
+ }
569
+ function scoreProviderQualityOutcomes(metrics) {
570
+ const total = metrics.total_children ?? 0;
571
+ if (total === 0)
572
+ return skippedSubscore("quality_outcomes", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUALITY_OUTCOMES_V1, "no children assigned to provider");
573
+ const succeeded = metrics.workers_succeeded ?? 0;
574
+ const validationOutcome = metrics.validation_outcome;
575
+ const score = validationOutcome === "passed" ? clamp01(succeeded / total) : clamp01(succeeded / total) * 0.8;
576
+ return subscore("quality_outcomes", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUALITY_OUTCOMES_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
577
+ detail: `succeeded=${succeeded}, total=${total}, validation_outcome=${validationOutcome ?? "unknown"}`,
578
+ });
579
+ }
580
+ /**
581
+ * Compute a scorecard for a provider.
582
+ */
583
+ function computeProviderScorecard(ev, provider, sourceRefs, groupingKeys) {
584
+ const rawMetrics = buildProviderRawMetrics(ev, provider);
585
+ const subscores = [
586
+ scoreProviderStartupFailure(rawMetrics),
587
+ scoreProviderQuotaExhaustion(rawMetrics),
588
+ scoreProviderFallbackFrequency(rawMetrics),
589
+ scoreProviderRoleSuitability(rawMetrics),
590
+ scoreProviderRuntime(rawMetrics),
591
+ scoreProviderTokenEfficiency(rawMetrics),
592
+ scoreProviderQualityOutcomes(rawMetrics),
593
+ ];
594
+ return buildScorecard("provider", provider, ev, rawMetrics, subscores, sourceRefs ?? defaultSourceRefs(ev), groupingKeys);
595
+ }
596
+ // ──────────────────────────────────────────────
597
+ // Model scorecard
598
+ // ──────────────────────────────────────────────
599
+ function scoreModelStartupFailure(metrics) {
600
+ const total = metrics.model_decisions ?? 0;
601
+ if (total === 0)
602
+ return skippedSubscore("startup_failure", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.STARTUP_FAILURE_RATE_V1, "no routing decisions for model");
603
+ const failures = metrics.model_startup_failures ?? 0;
604
+ const rate = failures / total;
605
+ const score = clamp01(1.0 - rate);
606
+ return subscore("startup_failure", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.STARTUP_FAILURE_RATE_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
607
+ detail: `startup_failures=${failures}, decisions=${total}`,
608
+ });
609
+ }
610
+ function scoreModelQuotaExhaustion(metrics) {
611
+ const total = metrics.model_decisions ?? 0;
612
+ if (total === 0)
613
+ return skippedSubscore("quota_exhaustion", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUOTA_EXHAUSTION_RATE_V1, "no routing decisions for model");
614
+ const exhausted = metrics.model_exhausted_decisions ?? 0;
615
+ const rate = exhausted / total;
616
+ const score = clamp01(1.0 - rate);
617
+ return subscore("quota_exhaustion", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUOTA_EXHAUSTION_RATE_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
618
+ detail: `exhausted_decisions=${exhausted}, decisions=${total}`,
619
+ });
620
+ }
621
+ function scoreModelFallbackFrequency(metrics) {
622
+ const total = metrics.model_decisions ?? 0;
623
+ if (total === 0)
624
+ return skippedSubscore("fallback_frequency", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.FALLBACK_FREQUENCY_V1, "no routing decisions for model");
625
+ const fallbacks = metrics.model_fallback_attempts ?? 0;
626
+ const rate = fallbacks / total;
627
+ const score = clamp01(1.0 - rate);
628
+ return subscore("fallback_frequency", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.FALLBACK_FREQUENCY_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
629
+ detail: `fallback_attempts=${fallbacks}, successful_fallbacks=${metrics.model_successful_fallbacks ?? 0}, decisions=${total}`,
630
+ });
631
+ }
632
+ function scoreModelRoleSuitability(metrics) {
633
+ const total = metrics.total_children ?? 0;
634
+ if (total === 0)
635
+ return skippedSubscore("role_suitability", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROLE_SUITABILITY_V1, "no children assigned to model");
636
+ const succeeded = metrics.workers_succeeded ?? 0;
637
+ const score = clamp01(succeeded / total);
638
+ return subscore("role_suitability", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROLE_SUITABILITY_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
639
+ detail: `succeeded=${succeeded}, total=${total}`,
640
+ });
641
+ }
642
+ function scoreModelRuntime(metrics) {
643
+ const hb = metrics.heartbeat_count;
644
+ const total = metrics.total_children ?? 0;
645
+ if (hb === null || total === 0) {
646
+ return skippedSubscore("runtime", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.RUNTIME_PROXY_V1, "no heartbeat evidence for model");
647
+ }
648
+ const mean = hb / total;
649
+ const EXPECTED_MAX = 10;
650
+ const score = clamp01(mean <= EXPECTED_MAX ? 1.0 : 1.0 - (mean - EXPECTED_MAX) * 0.05);
651
+ return subscore("runtime", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.RUNTIME_PROXY_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
652
+ detail: `mean_heartbeats_per_child=${mean.toFixed(1)}`,
653
+ });
654
+ }
655
+ function scoreModelTokenEfficiency(metrics) {
656
+ const tokens = metrics.worker_tokens_used;
657
+ const total = metrics.total_children ?? 0;
658
+ if (tokens === null || total === 0) {
659
+ return skippedSubscore("token_efficiency", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.TOKEN_EFFICIENCY_V1, "no token evidence for model");
660
+ }
661
+ const mean = tokens / total;
662
+ const BUDGET = sol_scorecard_js_1.WORKER_TOKEN_EFFICIENCY_SPEC.budget;
663
+ const MAX_PENALIZED = sol_scorecard_js_1.WORKER_TOKEN_EFFICIENCY_SPEC.max_penalized;
664
+ const score = mean <= BUDGET ? 1.0 : clamp01(1.0 - (mean - BUDGET) / (MAX_PENALIZED - BUDGET));
665
+ return subscore("token_efficiency", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.TOKEN_EFFICIENCY_V1, Number(score.toFixed(4)), "high", {
666
+ detail: `mean_tokens_per_child=${Math.round(mean)}`,
667
+ });
668
+ }
669
+ function scoreModelQualityOutcomes(metrics) {
670
+ const total = metrics.total_children ?? 0;
671
+ if (total === 0)
672
+ return skippedSubscore("quality_outcomes", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUALITY_OUTCOMES_V1, "no children assigned to model");
673
+ const succeeded = metrics.workers_succeeded ?? 0;
674
+ const validationOutcome = metrics.validation_outcome;
675
+ const score = validationOutcome === "passed" ? clamp01(succeeded / total) : clamp01(succeeded / total) * 0.8;
676
+ return subscore("quality_outcomes", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.QUALITY_OUTCOMES_V1, Number(score.toFixed(4)), confidenceFromCount(total), {
677
+ detail: `succeeded=${succeeded}, total=${total}, validation_outcome=${validationOutcome ?? "unknown"}`,
678
+ });
679
+ }
680
+ /**
681
+ * Compute a scorecard for a model.
682
+ */
683
+ function computeModelScorecard(ev, model, sourceRefs, groupingKeys) {
684
+ const rawMetrics = buildModelRawMetrics(ev, model);
685
+ const subscores = [
686
+ scoreModelStartupFailure(rawMetrics),
687
+ scoreModelQuotaExhaustion(rawMetrics),
688
+ scoreModelFallbackFrequency(rawMetrics),
689
+ scoreModelRoleSuitability(rawMetrics),
690
+ scoreModelRuntime(rawMetrics),
691
+ scoreModelTokenEfficiency(rawMetrics),
692
+ scoreModelQualityOutcomes(rawMetrics),
693
+ ];
694
+ return buildScorecard("model", model, ev, rawMetrics, subscores, sourceRefs ?? defaultSourceRefs(ev), groupingKeys);
695
+ }
696
+ // ──────────────────────────────────────────────
697
+ // Routing scorecard
698
+ // ──────────────────────────────────────────────
699
+ function scoreRouteSelected(metrics) {
700
+ if (metrics.router_exhausted) {
701
+ return subscore("route_selected", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_SELECTED_V1, 0.0, "high", {
702
+ detail: "router exhausted all providers",
703
+ });
704
+ }
705
+ if (metrics.provider_selected === null) {
706
+ return skippedSubscore("route_selected", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_SELECTED_V1, "no selected provider");
707
+ }
708
+ const status = metrics.router_child_status;
709
+ const score = status === "done" ? 1.0 : status === "blocked" ? 0.0 : 0.5;
710
+ return subscore("route_selected", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_SELECTED_V1, score, "high", {
711
+ detail: `selected_provider=${metrics.provider_selected}, child_status=${status ?? "unknown"}`,
712
+ });
713
+ }
714
+ function scoreRouteCandidates(metrics) {
715
+ const count = metrics.router_candidates_count;
716
+ if (count === null)
717
+ return skippedSubscore("candidates", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_CANDIDATES_V1, "no candidate count");
718
+ if (count === 0) {
719
+ return subscore("candidates", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_CANDIDATES_V1, 0.0, "low", {
720
+ detail: "candidates=0",
721
+ });
722
+ }
723
+ // 1 candidate = 0.5; 2 = 0.75; 3+ = 1.0
724
+ const score = count >= 3 ? 1.0 : count === 2 ? 0.75 : 0.5;
725
+ return subscore("candidates", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_CANDIDATES_V1, score, count > 0 ? "high" : "low", {
726
+ detail: `candidates=${count}`,
727
+ });
728
+ }
729
+ function scoreRouteFallbackPath(metrics) {
730
+ if (metrics.router_exhausted) {
731
+ return subscore("fallback_path", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_FALLBACK_PATH_V1, 0.0, "high", {
732
+ detail: "fallback exhausted",
733
+ });
734
+ }
735
+ if (!metrics.router_fallback_used) {
736
+ return subscore("fallback_path", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_FALLBACK_PATH_V1, 1.0, "high", {
737
+ detail: "no fallback used",
738
+ });
739
+ }
740
+ const status = metrics.router_child_status;
741
+ const score = status === "done" ? 0.5 : 0.0;
742
+ return subscore("fallback_path", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_FALLBACK_PATH_V1, score, "high", {
743
+ detail: `fallback_used=true, child_status=${status ?? "unknown"}`,
744
+ });
745
+ }
746
+ function scoreRouteOutcomeQuality(metrics) {
747
+ const status = metrics.router_child_status;
748
+ const validation = metrics.router_child_validation;
749
+ if (status === null && validation === null) {
750
+ return skippedSubscore("outcome_quality", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_OUTCOME_QUALITY_V1, "no child outcome for routing decision");
751
+ }
752
+ if (status === "done" && validation === "passed")
753
+ return subscore("outcome_quality", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_OUTCOME_QUALITY_V1, 1.0, "high", { detail: "status=done, validation=passed" });
754
+ if (status === "done")
755
+ return subscore("outcome_quality", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_OUTCOME_QUALITY_V1, 0.75, "high", { detail: `status=done, validation=${validation ?? "unknown"}` });
756
+ if (status === "blocked")
757
+ return subscore("outcome_quality", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_OUTCOME_QUALITY_V1, 0.0, "high", { detail: "status=blocked" });
758
+ return subscore("outcome_quality", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_OUTCOME_QUALITY_V1, 0.25, "high", { detail: `status=${status ?? "unknown"}, validation=${validation ?? "unknown"}` });
759
+ }
760
+ function scoreRouteTokenBurn(metrics) {
761
+ const tokens = metrics.worker_tokens_used;
762
+ if (tokens === null) {
763
+ return skippedSubscore("token_burn", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_TOKEN_BURN_V1, "no token data for routed child");
764
+ }
765
+ const BUDGET = sol_scorecard_js_1.WORKER_TOKEN_EFFICIENCY_SPEC.budget;
766
+ const MAX_PENALIZED = sol_scorecard_js_1.WORKER_TOKEN_EFFICIENCY_SPEC.max_penalized;
767
+ const score = tokens <= BUDGET ? 1.0 : clamp01(1.0 - (tokens - BUDGET) / (MAX_PENALIZED - BUDGET));
768
+ return subscore("token_burn", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_TOKEN_BURN_V1, Number(score.toFixed(4)), "high", {
769
+ detail: `tokens=${tokens}`,
770
+ });
771
+ }
772
+ function scoreRouteQcResult(metrics) {
773
+ const total = metrics.qc_total_findings;
774
+ if (total === null)
775
+ return skippedSubscore("qc_result", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_QC_RESULT_V1, "no QC evidence");
776
+ const blocking = metrics.qc_blocking_findings ?? 0;
777
+ if (blocking > 0) {
778
+ return subscore("qc_result", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_QC_RESULT_V1, 0.0, "high", {
779
+ detail: `blocking_findings=${blocking}`,
780
+ });
781
+ }
782
+ if (total > 0) {
783
+ return subscore("qc_result", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_QC_RESULT_V1, 0.5, "high", {
784
+ detail: `total_findings=${total}, blocking=0`,
785
+ });
786
+ }
787
+ return subscore("qc_result", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_QC_RESULT_V1, 1.0, "high", { detail: "no QC findings" });
788
+ }
789
+ function scoreRouteValidationResult(metrics) {
790
+ const outcome = metrics.router_child_validation ?? metrics.validation_outcome;
791
+ if (outcome === null) {
792
+ return skippedSubscore("validation_result", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_VALIDATION_RESULT_V1, "no validation outcome for routed child");
793
+ }
794
+ if (outcome === "passed")
795
+ return subscore("validation_result", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_VALIDATION_RESULT_V1, 1.0, "high", { detail: "validation=passed" });
796
+ if (outcome === "failed")
797
+ return subscore("validation_result", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_VALIDATION_RESULT_V1, 0.0, "high", { detail: "validation=failed" });
798
+ return subscore("validation_result", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_VALIDATION_RESULT_V1, 0.5, "medium", { detail: `validation=${outcome}` });
799
+ }
800
+ function scoreRouteConfirmedSignal(metrics) {
801
+ const status = metrics.router_child_status;
802
+ const validation = metrics.router_child_validation;
803
+ if (status === null || validation === null) {
804
+ return skippedSubscore("confirmed_signal", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_CONFIRMED_SIGNAL_V1, "no child outcome to confirm");
805
+ }
806
+ // A "confirmed" signal = selected route led to done+passed.
807
+ // A "contradicted" signal = selected route exists but child failed/blocked.
808
+ const confirmed = status === "done" && validation === "passed";
809
+ const contradicted = status === "blocked" || validation === "failed";
810
+ if (confirmed) {
811
+ return subscore("confirmed_signal", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_CONFIRMED_SIGNAL_V1, 1.0, "high", { detail: "outcome confirmed: done+passed" });
812
+ }
813
+ if (contradicted) {
814
+ return subscore("confirmed_signal", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_CONFIRMED_SIGNAL_V1, 0.0, "high", { detail: "outcome contradicted" });
815
+ }
816
+ return subscore("confirmed_signal", sol_scorecard_js_1.SOL_FORMULA_VERSIONS.ROUTE_CONFIRMED_SIGNAL_V1, 0.5, "medium", { detail: "outcome inconclusive" });
817
+ }
818
+ /**
819
+ * Compute a scorecard for one routing decision.
820
+ */
821
+ function computeRoutingScorecard(ev, decision, sourceRefs, groupingKeys) {
822
+ const rawMetrics = buildRoutingRawMetrics(ev, decision);
823
+ const subscores = [
824
+ scoreRouteSelected(rawMetrics),
825
+ scoreRouteCandidates(rawMetrics),
826
+ scoreRouteFallbackPath(rawMetrics),
827
+ scoreRouteOutcomeQuality(rawMetrics),
828
+ scoreRouteTokenBurn(rawMetrics),
829
+ scoreRouteQcResult(rawMetrics),
830
+ scoreRouteValidationResult(rawMetrics),
831
+ scoreRouteConfirmedSignal(rawMetrics),
832
+ ];
833
+ const key = decision.child_id || `${decision.selected_provider ?? "unknown"}-${decision.providers_tried.join(",")}`;
834
+ return buildScorecard("routing", key, ev, rawMetrics, subscores, sourceRefs ?? defaultSourceRefs(ev), groupingKeys);
835
+ }
836
+ /**
837
+ * Compute the full set of scorecards for a run.
838
+ */
839
+ function computeAllScorecards(ev, sourceRefs) {
840
+ const refs = sourceRefs ?? defaultSourceRefs(ev);
841
+ const foreman = computeForemanScorecard(ev, refs);
842
+ const workers = ev.children
843
+ .map((c) => computeWorkerScorecard(ev, c.child_id, refs))
844
+ .filter((s) => s !== null);
845
+ const providerKeys = new Set();
846
+ for (const child of ev.children) {
847
+ if (child.provider)
848
+ providerKeys.add(child.provider);
849
+ }
850
+ for (const decision of ev.router.decisions) {
851
+ if (decision.selected_provider)
852
+ providerKeys.add(decision.selected_provider);
853
+ for (const provider of decision.providers_tried) {
854
+ if (provider)
855
+ providerKeys.add(provider);
856
+ }
857
+ }
858
+ const providers = Array.from(providerKeys).map((provider) => computeProviderScorecard(ev, provider, refs));
859
+ const models = Array.from(new Set(ev.children.map((c) => c.grouping_keys.model).filter((m) => !!m)))
860
+ .map((model) => computeModelScorecard(ev, model, refs));
861
+ const routing = ev.router.availability === "available"
862
+ ? ev.router.decisions.map((d) => computeRoutingScorecard(ev, d, refs))
863
+ : [];
864
+ return { foreman, workers, providers, models, routing };
865
+ }