@lsctech/polaris 0.5.6 → 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.
- package/dist/autoresearch/index.js +30 -1
- package/dist/autoresearch/proposal.js +5 -0
- package/dist/autoresearch/score.js +174 -2
- package/dist/autoresearch/sol-evaluation-writer.js +135 -0
- package/dist/autoresearch/sol-evidence-loader.js +40 -3
- package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
- package/dist/autoresearch/sol-recommendations.js +212 -0
- package/dist/autoresearch/sol-report-renderer.js +282 -0
- package/dist/autoresearch/sol-report.js +44 -0
- package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
- package/dist/autoresearch/sol-scorer.js +45 -1
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/config/defaults.js +1 -0
- package/dist/config/validator.js +157 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +198 -4
- package/dist/finalize/linear.js +8 -4
- package/dist/finalize/steps/08-create-pr.js +2 -2
- package/dist/finalize/steps/11-update-linear.js +2 -2
- package/dist/loop/parent.js +189 -33
- package/dist/loop/worker-packet.js +75 -1
- package/dist/qc/artifacts.js +27 -0
- package/dist/qc/fixtures/repair-packets.js +170 -0
- package/dist/qc/index.js +2 -0
- package/dist/qc/orchestration.js +5 -2
- package/dist/qc/policy.js +30 -3
- package/dist/qc/providers/coderabbit.js +152 -17
- package/dist/qc/registry.js +36 -3
- package/dist/qc/repair-loop.js +458 -0
- package/dist/qc/repair-packets.js +420 -0
- package/dist/qc/runner.js +328 -37
- package/dist/qc/schemas.js +79 -1
- package/dist/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SOL evidence normalizer.
|
|
4
|
+
*
|
|
5
|
+
* Materializes raw metric events (SolMetricEvent[]) and source references
|
|
6
|
+
* (SolSourceRef[]) from a loaded SolEvidence record. This is the bridge
|
|
7
|
+
* between the evidence loader and scorecard generation.
|
|
8
|
+
*
|
|
9
|
+
* Design rules:
|
|
10
|
+
* - Never throws on missing data — absent optional fields reduce confidence.
|
|
11
|
+
* - Source refs track every artifact that contributed evidence (present or not).
|
|
12
|
+
* - Provider startup failures are distinguished from worker execution failures:
|
|
13
|
+
* startup failure = provider-exhausted or selected_provider=null in router evidence
|
|
14
|
+
* worker execution failure = worker ran but status="failed"|"error"
|
|
15
|
+
* - Router fallback + candidate/rejection context are preserved when present.
|
|
16
|
+
* - Missing router or QC evidence sets availability flags; no metric events
|
|
17
|
+
* are emitted for those categories.
|
|
18
|
+
* - Confidence: "high" when all expected source refs are available,
|
|
19
|
+
* "medium" when some are absent, "low" when most are absent.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.normalizeSolEvidence = normalizeSolEvidence;
|
|
23
|
+
function makeSourceRef(kind, path, available, unavailableReason) {
|
|
24
|
+
return available
|
|
25
|
+
? { kind, path, available: true }
|
|
26
|
+
: { kind, path, available: false, unavailable_reason: unavailableReason };
|
|
27
|
+
}
|
|
28
|
+
function computeConfidence(refs) {
|
|
29
|
+
if (refs.length === 0)
|
|
30
|
+
return "none";
|
|
31
|
+
const available = refs.filter((r) => r.available).length;
|
|
32
|
+
const ratio = available / refs.length;
|
|
33
|
+
if (ratio >= 0.8)
|
|
34
|
+
return "high";
|
|
35
|
+
if (ratio >= 0.4)
|
|
36
|
+
return "medium";
|
|
37
|
+
if (ratio > 0)
|
|
38
|
+
return "low";
|
|
39
|
+
return "none";
|
|
40
|
+
}
|
|
41
|
+
// ──────────────────────────────────────────────
|
|
42
|
+
// Source ref builders
|
|
43
|
+
// ──────────────────────────────────────────────
|
|
44
|
+
function buildSourceRefs(evidence, paths) {
|
|
45
|
+
const refs = [];
|
|
46
|
+
// Run state
|
|
47
|
+
if (paths.runStatePath) {
|
|
48
|
+
refs.push(makeSourceRef("run-state", paths.runStatePath, evidence.run.status !== null));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
refs.push(makeSourceRef("run-state", ".taskchain_artifacts/polaris-run/current-state.json", false, "path not resolved"));
|
|
52
|
+
}
|
|
53
|
+
// Telemetry
|
|
54
|
+
if (paths.telemetryPath) {
|
|
55
|
+
const hasTelemetry = evidence.tokens.total_worker_heartbeats > 0 || evidence.foreman.escalation_events > 0;
|
|
56
|
+
refs.push(makeSourceRef("telemetry", paths.telemetryPath, hasTelemetry));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
refs.push(makeSourceRef("telemetry", ".taskchain_artifacts/polaris-run/runs/<run-id>/telemetry.jsonl", false, "path not resolved"));
|
|
60
|
+
}
|
|
61
|
+
// Cluster state — available when the path was resolved (regardless of QC status)
|
|
62
|
+
if (paths.clusterStatePath) {
|
|
63
|
+
refs.push(makeSourceRef("cluster-state", paths.clusterStatePath, true));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
refs.push(makeSourceRef("cluster-state", `.polaris/clusters/${evidence.cluster_id ?? "unknown"}/cluster-state.json`, false, evidence.cluster_id ? "not found" : "cluster id not resolved"));
|
|
67
|
+
}
|
|
68
|
+
// Result packets (one per child with a known path)
|
|
69
|
+
for (const child of evidence.children) {
|
|
70
|
+
// Match filenames that are exactly "{child_id}.json" or start with "{child_id}-"
|
|
71
|
+
// to handle commit-suffixed names (e.g. "POL-001-abc.json") while avoiding
|
|
72
|
+
// false positives when IDs share a prefix (e.g. "1" vs "10").
|
|
73
|
+
const packetPath = paths.resultPacketPaths.find((p) => {
|
|
74
|
+
const filename = p.split('/').pop() ?? '';
|
|
75
|
+
return filename === `${child.child_id}.json` || filename.startsWith(`${child.child_id}-`);
|
|
76
|
+
});
|
|
77
|
+
const path = packetPath ?? `.polaris/clusters/${evidence.cluster_id ?? "unknown"}/results/${child.child_id}.json`;
|
|
78
|
+
refs.push(makeSourceRef("result-packet", path, packetPath !== undefined));
|
|
79
|
+
}
|
|
80
|
+
// QC artifacts
|
|
81
|
+
if (evidence.qc.availability === "available") {
|
|
82
|
+
refs.push(makeSourceRef("qc-finding", paths.qcDir ?? `.polaris/clusters/${evidence.cluster_id ?? "unknown"}/qc`, true));
|
|
83
|
+
}
|
|
84
|
+
else if (evidence.qc.availability === "future") {
|
|
85
|
+
refs.push(makeSourceRef("qc-finding", `.polaris/clusters/${evidence.cluster_id ?? "unknown"}/qc`, false, "QC not yet run for this cluster"));
|
|
86
|
+
}
|
|
87
|
+
// "unavailable" (qc-disabled): no ref emitted
|
|
88
|
+
// Run report
|
|
89
|
+
if (paths.runReportPath) {
|
|
90
|
+
refs.push(makeSourceRef("run-report", paths.runReportPath, true));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
refs.push(makeSourceRef("run-report", `smartdocs/reports/sol/${evidence.run_id}-evaluation-report.md`, false, "path not resolved"));
|
|
94
|
+
}
|
|
95
|
+
return refs;
|
|
96
|
+
}
|
|
97
|
+
// ──────────────────────────────────────────────
|
|
98
|
+
// Metric event materializers
|
|
99
|
+
// ──────────────────────────────────────────────
|
|
100
|
+
function materializeProviderStartupFailures(evidence) {
|
|
101
|
+
if (evidence.router.availability !== "available")
|
|
102
|
+
return [];
|
|
103
|
+
return evidence.router.decisions
|
|
104
|
+
.filter((d) => d.exhausted)
|
|
105
|
+
.map((d) => ({
|
|
106
|
+
category: "provider-startup-failure",
|
|
107
|
+
run_id: evidence.run_id,
|
|
108
|
+
child_id: d.child_id || undefined,
|
|
109
|
+
provider: d.selected_provider ?? d.providers_tried[d.providers_tried.length - 1] ?? "unknown",
|
|
110
|
+
failure_reason: d.exhausted_reason,
|
|
111
|
+
providers_tried: d.providers_tried,
|
|
112
|
+
all_providers_exhausted: d.exhausted,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
function materializeRouterFallbacks(evidence) {
|
|
116
|
+
if (evidence.router.availability !== "available")
|
|
117
|
+
return [];
|
|
118
|
+
return evidence.router.decisions
|
|
119
|
+
.filter((d) => d.fallback_used && !d.exhausted)
|
|
120
|
+
.map((d) => {
|
|
121
|
+
const originalProvider = d.providers_tried[0] ?? null;
|
|
122
|
+
const fallbackProvider = d.selected_provider;
|
|
123
|
+
// Child succeeded if it has a "done" completion (check worker evidence)
|
|
124
|
+
const childResult = evidence.children.find((c) => c.child_id === d.child_id);
|
|
125
|
+
const fallbackSucceeded = childResult?.status === "done";
|
|
126
|
+
return {
|
|
127
|
+
category: "router-fallback",
|
|
128
|
+
run_id: evidence.run_id,
|
|
129
|
+
child_id: d.child_id || undefined,
|
|
130
|
+
original_provider: originalProvider,
|
|
131
|
+
fallback_provider: fallbackProvider,
|
|
132
|
+
providers_tried: d.providers_tried,
|
|
133
|
+
fallback_succeeded: fallbackSucceeded ?? false,
|
|
134
|
+
rejection_reasons: d.rejection_reasons,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function materializeWorkerExecutionFailures(evidence) {
|
|
139
|
+
// Worker execution failures: worker ran (not a startup failure) but status failed/error
|
|
140
|
+
const exhaustedChildIds = new Set(evidence.router.availability === "available"
|
|
141
|
+
? evidence.router.decisions.filter((d) => d.exhausted).map((d) => d.child_id)
|
|
142
|
+
: []);
|
|
143
|
+
return evidence.children
|
|
144
|
+
.filter((c) => {
|
|
145
|
+
// Must be a failed/error status (not "done" or "blocked")
|
|
146
|
+
if (c.status !== "failed" && c.status !== "error")
|
|
147
|
+
return false;
|
|
148
|
+
// Exclude children where the provider never started (startup failure)
|
|
149
|
+
if (exhaustedChildIds.has(c.child_id))
|
|
150
|
+
return false;
|
|
151
|
+
return true;
|
|
152
|
+
})
|
|
153
|
+
.map((c) => {
|
|
154
|
+
const validationRecord = evidence.validation.find((v) => v.child_id === c.child_id);
|
|
155
|
+
return {
|
|
156
|
+
category: "worker-execution-failure",
|
|
157
|
+
run_id: evidence.run_id,
|
|
158
|
+
child_id: c.child_id,
|
|
159
|
+
worker_status: c.status,
|
|
160
|
+
validation: validationRecord?.outcome ?? "unknown",
|
|
161
|
+
provider: c.provider,
|
|
162
|
+
error_message: validationRecord?.error_message ?? null,
|
|
163
|
+
out_of_scope_escalation: evidence.intervention.out_of_scope_count > 0,
|
|
164
|
+
escalation_count: c.escalation_count,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function materializeValidationFailures(evidence) {
|
|
169
|
+
return evidence.validation
|
|
170
|
+
.filter((v) => v.outcome === "failed")
|
|
171
|
+
.map((v) => {
|
|
172
|
+
const childResult = evidence.children.find((c) => c.child_id === v.child_id);
|
|
173
|
+
return {
|
|
174
|
+
category: "validation-failure",
|
|
175
|
+
run_id: evidence.run_id,
|
|
176
|
+
child_id: v.child_id,
|
|
177
|
+
worker_status: childResult?.status ?? "unknown",
|
|
178
|
+
failed_commands: [], // Commands not individually tracked in SolValidationEvidence
|
|
179
|
+
passed_commands: v.passed_commands,
|
|
180
|
+
error_message: v.error_message,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function materializeQcFindings(evidence) {
|
|
185
|
+
if (evidence.qc.availability !== "available")
|
|
186
|
+
return [];
|
|
187
|
+
const events = [];
|
|
188
|
+
// Blocking open findings by severity
|
|
189
|
+
const severities = ["critical", "high", "medium", "low", "info"];
|
|
190
|
+
for (const sev of severities) {
|
|
191
|
+
const count = evidence.qc.open_by_severity[sev];
|
|
192
|
+
for (let i = 0; i < count; i++) {
|
|
193
|
+
const isHighSeverity = sev === "critical" || sev === "high";
|
|
194
|
+
events.push({
|
|
195
|
+
category: "qc-finding",
|
|
196
|
+
run_id: evidence.run_id,
|
|
197
|
+
qc_provider: Object.keys(evidence.qc.provider_breakdown)[0] ?? "unknown",
|
|
198
|
+
severity: sev,
|
|
199
|
+
blocking: isHighSeverity && evidence.qc.blocks_delivery,
|
|
200
|
+
autofixed: false,
|
|
201
|
+
repaired: false,
|
|
202
|
+
waived: false,
|
|
203
|
+
unvalidated: false,
|
|
204
|
+
summary: `Open ${sev} QC finding`,
|
|
205
|
+
attribution_confidence: isHighSeverity ? "high" : "medium",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Emit a single unvalidated aggregate event when unvalidated_findings > 0
|
|
210
|
+
if (evidence.qc.unvalidated_findings > 0) {
|
|
211
|
+
const noisyProvider = evidence.qc.noisy_providers[0] ?? "unknown";
|
|
212
|
+
events.push({
|
|
213
|
+
category: "qc-finding",
|
|
214
|
+
run_id: evidence.run_id,
|
|
215
|
+
qc_provider: noisyProvider,
|
|
216
|
+
severity: "info",
|
|
217
|
+
blocking: false,
|
|
218
|
+
autofixed: false,
|
|
219
|
+
repaired: false,
|
|
220
|
+
waived: false,
|
|
221
|
+
unvalidated: true,
|
|
222
|
+
summary: `${evidence.qc.unvalidated_findings} unvalidated finding(s) from provider ${noisyProvider}`,
|
|
223
|
+
attribution_confidence: "none",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return events;
|
|
227
|
+
}
|
|
228
|
+
function materializeInterventions(evidence) {
|
|
229
|
+
const events = [];
|
|
230
|
+
if (evidence.intervention.user_intervened) {
|
|
231
|
+
// Find the children that had user interventions for child_id attribution
|
|
232
|
+
const intervened = evidence.children.filter((c) => c.user_intervened === true);
|
|
233
|
+
if (intervened.length > 0) {
|
|
234
|
+
for (const child of intervened) {
|
|
235
|
+
events.push({
|
|
236
|
+
category: "user-intervention",
|
|
237
|
+
run_id: evidence.run_id,
|
|
238
|
+
child_id: child.child_id,
|
|
239
|
+
actor: "user",
|
|
240
|
+
intervention_type: "commit",
|
|
241
|
+
resolved: true,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
events.push({
|
|
247
|
+
category: "user-intervention",
|
|
248
|
+
run_id: evidence.run_id,
|
|
249
|
+
actor: "user",
|
|
250
|
+
intervention_type: "unspecified",
|
|
251
|
+
resolved: true,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (evidence.intervention.foreman_intervened) {
|
|
256
|
+
const intervened = evidence.children.filter((c) => c.foreman_intervened === true);
|
|
257
|
+
if (intervened.length > 0) {
|
|
258
|
+
for (const child of intervened) {
|
|
259
|
+
events.push({
|
|
260
|
+
category: "foreman-intervention",
|
|
261
|
+
run_id: evidence.run_id,
|
|
262
|
+
child_id: child.child_id,
|
|
263
|
+
actor: "foreman",
|
|
264
|
+
intervention_type: "commit",
|
|
265
|
+
resolved: true,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
events.push({
|
|
271
|
+
category: "foreman-intervention",
|
|
272
|
+
run_id: evidence.run_id,
|
|
273
|
+
actor: "foreman",
|
|
274
|
+
intervention_type: "unspecified",
|
|
275
|
+
resolved: true,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (evidence.intervention.state_repair_required) {
|
|
280
|
+
events.push({
|
|
281
|
+
category: "foreman-intervention",
|
|
282
|
+
run_id: evidence.run_id,
|
|
283
|
+
actor: "foreman",
|
|
284
|
+
intervention_type: "state-repair",
|
|
285
|
+
resolved: false,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (evidence.intervention.out_of_scope_count > 0) {
|
|
289
|
+
events.push({
|
|
290
|
+
category: "user-intervention",
|
|
291
|
+
run_id: evidence.run_id,
|
|
292
|
+
actor: "user",
|
|
293
|
+
intervention_type: "out-of-scope",
|
|
294
|
+
resolved: evidence.intervention.blocked_event_count > 0,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return events;
|
|
298
|
+
}
|
|
299
|
+
// ──────────────────────────────────────────────
|
|
300
|
+
// Main entry point
|
|
301
|
+
// ──────────────────────────────────────────────
|
|
302
|
+
/**
|
|
303
|
+
* Normalize a loaded SolEvidence record into raw metric events and source refs.
|
|
304
|
+
*
|
|
305
|
+
* Does not throw on missing data — absent optional fields mark source refs
|
|
306
|
+
* as unavailable and reduce confidence. Missing router or QC inputs emit no
|
|
307
|
+
* events for those categories.
|
|
308
|
+
*
|
|
309
|
+
* @param evidence — already-loaded SolEvidence (from aggregateSolEvidence)
|
|
310
|
+
* @param paths — artifact path context for source ref generation
|
|
311
|
+
* @returns SolEvidenceNormalizationResult
|
|
312
|
+
*/
|
|
313
|
+
function normalizeSolEvidence(evidence, paths = {
|
|
314
|
+
runStatePath: null,
|
|
315
|
+
telemetryPath: null,
|
|
316
|
+
clusterStatePath: null,
|
|
317
|
+
resultPacketPaths: [],
|
|
318
|
+
qcDir: null,
|
|
319
|
+
runReportPath: null,
|
|
320
|
+
}) {
|
|
321
|
+
const source_refs = buildSourceRefs(evidence, paths);
|
|
322
|
+
const events = [
|
|
323
|
+
...materializeProviderStartupFailures(evidence),
|
|
324
|
+
...materializeRouterFallbacks(evidence),
|
|
325
|
+
...materializeWorkerExecutionFailures(evidence),
|
|
326
|
+
...materializeValidationFailures(evidence),
|
|
327
|
+
...materializeQcFindings(evidence),
|
|
328
|
+
...materializeInterventions(evidence),
|
|
329
|
+
];
|
|
330
|
+
return {
|
|
331
|
+
run_id: evidence.run_id,
|
|
332
|
+
normalized_at: new Date().toISOString(),
|
|
333
|
+
events,
|
|
334
|
+
source_refs,
|
|
335
|
+
evidence_confidence: computeConfidence(source_refs),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -19,6 +19,10 @@ exports.generateRecommendations = generateRecommendations;
|
|
|
19
19
|
exports.recommendationToProposal = recommendationToProposal;
|
|
20
20
|
exports.recommendationsToProposals = recommendationsToProposals;
|
|
21
21
|
exports.formatRecommendationsCli = formatRecommendationsCli;
|
|
22
|
+
exports.generateQcRecommendations = generateQcRecommendations;
|
|
23
|
+
exports.formatQcRecommendations = formatQcRecommendations;
|
|
24
|
+
exports.scorecardToRecommendationSummary = scorecardToRecommendationSummary;
|
|
25
|
+
exports.scorecardsToRecommendationSummaries = scorecardsToRecommendationSummaries;
|
|
22
26
|
const sol_report_js_1 = require("./sol-report.js");
|
|
23
27
|
// ──────────────────────────────────────────────
|
|
24
28
|
// Constants and helpers
|
|
@@ -298,3 +302,211 @@ function formatRecommendationsCli(report) {
|
|
|
298
302
|
}
|
|
299
303
|
return lines.join("\n");
|
|
300
304
|
}
|
|
305
|
+
function makeQcRecommendation(id, category, actionType, affected, proposedAction, rationale, confidence, evidenceRunId) {
|
|
306
|
+
return {
|
|
307
|
+
id,
|
|
308
|
+
category,
|
|
309
|
+
action_type: actionType,
|
|
310
|
+
affected,
|
|
311
|
+
proposed_action: proposedAction,
|
|
312
|
+
confidence: clamp01(confidence),
|
|
313
|
+
evidence: {
|
|
314
|
+
group_key: `run_id=${evidenceRunId}`,
|
|
315
|
+
grouped_by: ["run_id"],
|
|
316
|
+
count: 1,
|
|
317
|
+
mean_composite: null,
|
|
318
|
+
min_composite: null,
|
|
319
|
+
max_composite: null,
|
|
320
|
+
mean_foreman_composite: null,
|
|
321
|
+
mean_worker_composite: null,
|
|
322
|
+
run_ids: [evidenceRunId],
|
|
323
|
+
},
|
|
324
|
+
rationale,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Generate QC repair-loop follow-up recommendations from a single run's
|
|
329
|
+
* SOL evidence. These are advisory signals for noisy providers, repeated
|
|
330
|
+
* repair failures, unresolved high-severity findings, and max-round exhaustion.
|
|
331
|
+
*/
|
|
332
|
+
function generateQcRecommendations(evidence) {
|
|
333
|
+
const recommendations = [];
|
|
334
|
+
const qc = evidence.qc;
|
|
335
|
+
const runId = evidence.run_id;
|
|
336
|
+
if (qc.availability !== "available") {
|
|
337
|
+
return {
|
|
338
|
+
generated_at: new Date().toISOString(),
|
|
339
|
+
evidence_run_id: runId,
|
|
340
|
+
recommendations,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
for (const provider of qc.noisy_providers) {
|
|
344
|
+
const counts = qc.provider_breakdown[provider] ?? { total: 0, blocking: 0, unvalidated: 0 };
|
|
345
|
+
const confidence = counts.total > 0 ? counts.unvalidated / counts.total : 0;
|
|
346
|
+
recommendations.push(makeQcRecommendation(`qc-noisy-provider:${provider}`, "provider_policy", "analyze", { provider }, `Review QC provider '${provider}' for noisy/unvalidated findings; consider raising attribution confidence thresholds or disabling the provider for high-risk routes.`, `${counts.unvalidated}/${counts.total} findings from '${provider}' are unvalidated noise.`, confidence, runId));
|
|
347
|
+
}
|
|
348
|
+
if (qc.has_repair_failures) {
|
|
349
|
+
const failed = qc.repair_loop?.packets_failed ?? 0;
|
|
350
|
+
recommendations.push(makeQcRecommendation(`qc-repair-failure:${runId}`, "qc_follow_up", "implement", {}, "Repeated repair worker failures detected — inspect repair packet scope, validation commands, and Medic referral path.", `${failed} repair packet(s) failed; consider tightening acceptance criteria or escalating to Medic.`, 0.9, runId));
|
|
351
|
+
}
|
|
352
|
+
if (qc.unresolved_high_severity > 0) {
|
|
353
|
+
recommendations.push(makeQcRecommendation(`qc-unresolved-high-severity:${runId}`, "qc_follow_up", "analyze", {}, `${qc.unresolved_high_severity} unresolved critical/high QC findings remain — triage before delivery.`, `${qc.unresolved_high_severity} critical/high findings are still open after repair loop.`, Math.min(1, qc.unresolved_high_severity / 5), runId));
|
|
354
|
+
}
|
|
355
|
+
if (qc.max_round_exhausted) {
|
|
356
|
+
recommendations.push(makeQcRecommendation(`qc-max-rounds:${runId}`, "qc_follow_up", "analyze", {}, "QC repair loop exhausted max rounds — review provider noise, repair packet scope, and escalation policy.", `Repair loop reached round ${qc.repair_loop?.rounds_completed ?? 0}/${qc.repair_loop?.max_rounds ?? 0} without passing.`, 0.85, runId));
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
generated_at: new Date().toISOString(),
|
|
360
|
+
evidence_run_id: runId,
|
|
361
|
+
recommendations,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function formatQcRecommendations(report) {
|
|
365
|
+
const lines = [];
|
|
366
|
+
lines.push("SOL QC Follow-Up Recommendations");
|
|
367
|
+
lines.push(`Generated: ${report.generated_at}`);
|
|
368
|
+
lines.push(`Run: ${report.evidence_run_id}`);
|
|
369
|
+
lines.push("");
|
|
370
|
+
if (report.recommendations.length === 0) {
|
|
371
|
+
lines.push("No QC follow-up signals detected.");
|
|
372
|
+
return lines.join("\n") + "\n";
|
|
373
|
+
}
|
|
374
|
+
for (const r of report.recommendations) {
|
|
375
|
+
lines.push(`[${r.action_type}] ${r.id}`);
|
|
376
|
+
lines.push(` Category: ${r.category}`);
|
|
377
|
+
lines.push(` Affected: ${fmtAffected(r.affected)}`);
|
|
378
|
+
lines.push(` Confidence: ${(r.confidence * 100).toFixed(1)}%`);
|
|
379
|
+
lines.push(` Action: ${r.proposed_action}`);
|
|
380
|
+
lines.push(` Rationale: ${r.rationale}`);
|
|
381
|
+
lines.push("");
|
|
382
|
+
}
|
|
383
|
+
return lines.join("\n");
|
|
384
|
+
}
|
|
385
|
+
// ── Internal helpers ──
|
|
386
|
+
function extractQptEvidence(subscores) {
|
|
387
|
+
const s = subscores.find((ss) => ss.dimension === "quality_per_token");
|
|
388
|
+
return { score: s?.score ?? null, detail: s?.detail ?? null };
|
|
389
|
+
}
|
|
390
|
+
function extractQcEvidence(subscores, rawMetrics) {
|
|
391
|
+
const qcSub = subscores.find((ss) => ss.dimension === "qc" || ss.dimension === "qc_repair_loop" || ss.dimension === "qc_result");
|
|
392
|
+
return {
|
|
393
|
+
qc_findings: rawMetrics.qc_total_findings,
|
|
394
|
+
blocking_findings: rawMetrics.qc_blocking_findings,
|
|
395
|
+
qc_repair_status: rawMetrics.qc_repair_loop_status,
|
|
396
|
+
qc_score: qcSub?.score ?? null,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function extractValidationEvidence(subscores, rawMetrics) {
|
|
400
|
+
const valSub = subscores.find((ss) => ss.dimension === "validation" ||
|
|
401
|
+
ss.dimension === "evidence_validation" ||
|
|
402
|
+
ss.dimension === "validation_result");
|
|
403
|
+
return {
|
|
404
|
+
validation_outcome: rawMetrics.validation_outcome,
|
|
405
|
+
passed_commands: [...(rawMetrics.passed_commands ?? [])],
|
|
406
|
+
validation_score: valSub?.score ?? null,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function deriveVerdict(scorecard, qcEvidence) {
|
|
410
|
+
const { subscores, aggregate_score, recommendation_inputs } = scorecard;
|
|
411
|
+
// Blocking QC findings always contradict.
|
|
412
|
+
if ((qcEvidence.blocking_findings ?? 0) > 0)
|
|
413
|
+
return "contradicted";
|
|
414
|
+
// confirmed_signal subscore is the strongest direct verdict signal.
|
|
415
|
+
const confirmed = subscores.find((s) => s.dimension === "confirmed_signal");
|
|
416
|
+
if (confirmed !== undefined && confirmed.score !== null) {
|
|
417
|
+
if (confirmed.score >= 0.9)
|
|
418
|
+
return "supported";
|
|
419
|
+
if (confirmed.score <= 0.1)
|
|
420
|
+
return "contradicted";
|
|
421
|
+
}
|
|
422
|
+
// Aggregate score determines verdict; intervention/router issues limit to inconclusive.
|
|
423
|
+
if (aggregate_score !== null) {
|
|
424
|
+
if (aggregate_score < 0.5)
|
|
425
|
+
return "contradicted";
|
|
426
|
+
if (aggregate_score >= 0.75 &&
|
|
427
|
+
!recommendation_inputs.intervention_detected &&
|
|
428
|
+
!recommendation_inputs.router_issue_detected) {
|
|
429
|
+
return "supported";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (recommendation_inputs.intervention_detected || recommendation_inputs.router_issue_detected) {
|
|
433
|
+
return "inconclusive";
|
|
434
|
+
}
|
|
435
|
+
if (aggregate_score === null)
|
|
436
|
+
return "inconclusive";
|
|
437
|
+
if (aggregate_score >= 0.75)
|
|
438
|
+
return "supported";
|
|
439
|
+
if (aggregate_score < 0.6)
|
|
440
|
+
return "contradicted";
|
|
441
|
+
return "inconclusive";
|
|
442
|
+
}
|
|
443
|
+
function confidenceFromScorecard(scorecard) {
|
|
444
|
+
const { subscores, aggregate_score, recommendation_inputs } = scorecard;
|
|
445
|
+
const scoredCount = subscores.filter((s) => s.score !== null).length;
|
|
446
|
+
if (scoredCount === 0)
|
|
447
|
+
return 0.1; // minimal confidence without evidence
|
|
448
|
+
// Base: proportion of subscores with data.
|
|
449
|
+
const coverage = scoredCount / Math.max(subscores.length, 1);
|
|
450
|
+
// Small penalty when below threshold (more uncertainty in the recommendation).
|
|
451
|
+
const scorePenalty = aggregate_score !== null && recommendation_inputs.below_threshold ? 0.1 : 0;
|
|
452
|
+
return clamp01(coverage - scorePenalty);
|
|
453
|
+
}
|
|
454
|
+
function affectedFromScorecard(scorecard) {
|
|
455
|
+
const keys = scorecard.grouping_keys ?? {};
|
|
456
|
+
const affected = {};
|
|
457
|
+
if (keys.route)
|
|
458
|
+
affected.route = keys.route;
|
|
459
|
+
if (keys.task_type)
|
|
460
|
+
affected.task_type = keys.task_type;
|
|
461
|
+
if (keys.role)
|
|
462
|
+
affected.role = keys.role;
|
|
463
|
+
if (keys.provider)
|
|
464
|
+
affected.provider = keys.provider;
|
|
465
|
+
if (keys.model)
|
|
466
|
+
affected.model = keys.model;
|
|
467
|
+
// Subject-level key as fallback for provider/model scorecards.
|
|
468
|
+
if (!affected.provider && scorecard.subject === "provider")
|
|
469
|
+
affected.provider = scorecard.subject_key;
|
|
470
|
+
if (!affected.model && scorecard.subject === "model")
|
|
471
|
+
affected.model = scorecard.subject_key;
|
|
472
|
+
return affected;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Derive a `ScorecardRecommendationSummary` from a `SolScorecard`.
|
|
476
|
+
*
|
|
477
|
+
* This is a pure function: it does not read files, call APIs, or mutate
|
|
478
|
+
* the scorecard or any runtime state. Its output is advisory by default.
|
|
479
|
+
*/
|
|
480
|
+
function scorecardToRecommendationSummary(scorecard) {
|
|
481
|
+
const { subscores, raw_metrics, source_refs, recommendation_inputs } = scorecard;
|
|
482
|
+
const qc = extractQcEvidence(subscores, raw_metrics);
|
|
483
|
+
const validation = extractValidationEvidence(subscores, raw_metrics);
|
|
484
|
+
const quality_per_token = extractQptEvidence(subscores);
|
|
485
|
+
const verdict = deriveVerdict(scorecard, qc);
|
|
486
|
+
const confidence = confidenceFromScorecard(scorecard);
|
|
487
|
+
const affected = affectedFromScorecard(scorecard);
|
|
488
|
+
const clonedSourceRefs = source_refs.map((ref) => ({ ...ref }));
|
|
489
|
+
return {
|
|
490
|
+
scorecard_id: scorecard.scorecard_id,
|
|
491
|
+
subject: scorecard.subject,
|
|
492
|
+
subject_key: scorecard.subject_key,
|
|
493
|
+
affected,
|
|
494
|
+
verdict,
|
|
495
|
+
aggregate_score: scorecard.aggregate_score,
|
|
496
|
+
confidence,
|
|
497
|
+
low_scoring_dimensions: [...recommendation_inputs.low_scoring_dimensions],
|
|
498
|
+
quality_per_token,
|
|
499
|
+
qc,
|
|
500
|
+
validation,
|
|
501
|
+
source_refs: clonedSourceRefs,
|
|
502
|
+
intervention_detected: recommendation_inputs.intervention_detected,
|
|
503
|
+
router_issue_detected: recommendation_inputs.router_issue_detected,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Derive recommendation summaries from a set of scorecards.
|
|
508
|
+
* Returns all summaries; callers filter by verdict as needed.
|
|
509
|
+
*/
|
|
510
|
+
function scorecardsToRecommendationSummaries(scorecards) {
|
|
511
|
+
return scorecards.map(scorecardToRecommendationSummary);
|
|
512
|
+
}
|