@lsctech/polaris 0.5.7 → 0.5.9

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.
Files changed (36) hide show
  1. package/dist/autoresearch/index.js +30 -1
  2. package/dist/autoresearch/sol-evaluation-writer.js +135 -0
  3. package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
  4. package/dist/autoresearch/sol-recommendations.js +130 -0
  5. package/dist/autoresearch/sol-report-renderer.js +282 -0
  6. package/dist/autoresearch/sol-run-health-bridge.js +246 -0
  7. package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
  8. package/dist/cli/autoresearch.js +77 -0
  9. package/dist/cli/medic.js +65 -0
  10. package/dist/cluster-state/store.js +56 -0
  11. package/dist/config/defaults.js +3 -0
  12. package/dist/config/validator.js +141 -0
  13. package/dist/finalize/artifact-policy.js +2 -0
  14. package/dist/finalize/github.js +13 -9
  15. package/dist/finalize/index.js +231 -5
  16. package/dist/finalize/linear.js +8 -4
  17. package/dist/finalize/medic-gate.js +42 -0
  18. package/dist/finalize/steps/08-create-pr.js +2 -2
  19. package/dist/finalize/steps/11-update-linear.js +2 -2
  20. package/dist/loop/parent.js +234 -28
  21. package/dist/loop/worker-packet.js +19 -1
  22. package/dist/medic/run-health-consult.js +381 -0
  23. package/dist/medic/treatment-packets.js +169 -0
  24. package/dist/qc/artifacts.js +27 -0
  25. package/dist/qc/orchestration.js +3 -2
  26. package/dist/qc/providers/coderabbit.js +114 -11
  27. package/dist/qc/repair-loop.js +49 -14
  28. package/dist/qc/runner.js +10 -2
  29. package/dist/qc/schemas.js +1 -0
  30. package/dist/run-health/foreman-symptoms.js +100 -0
  31. package/dist/run-health/index.js +301 -0
  32. package/dist/run-health/qc-escalation.js +155 -0
  33. package/dist/run-health/schema.js +123 -0
  34. package/dist/types/sol-metrics.js +108 -0
  35. package/dist/types/sol-scorecard.js +148 -0
  36. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatRecommendationsCli = exports.recommendationToProposal = exports.recommendationsToProposals = exports.generateRecommendations = exports.formatReportCli = exports.generateReport = exports.getHistoryFilePath = exports.buildSnapshot = exports.loadSnapshots = exports.appendSnapshot = exports.computeSolScoreReport = exports.computeWorkerScore = exports.computeForemanScore = exports.aggregateSolEvidence = exports.routeProposals = exports.validateDiagnosisReport = exports.loadDiagnosisReport = exports.buildProposals = exports.FIX_ZONE_MAP = exports.ALL_GATES = exports.buildDiagnosisHints = exports.computeScore = exports.loadRunArtifacts = exports.scoreRun = exports.assertPolarisDevContext = exports.isPolarisDevContext = void 0;
3
+ exports.scorecardToRecommendationSummary = exports.formatRecommendationsCli = exports.recommendationToProposal = exports.recommendationsToProposals = exports.generateRecommendations = exports.renderSolMarkdown = exports.getSolMarkdownReportPath = exports.getScorecardPath = exports.getEvaluationRecordPath = exports.getSolReportsDir = exports.getSolScorecardsDir = exports.getSolEvaluationsDir = exports.writeSolMarkdownReport = exports.writeScorecardSet = exports.writeScorecard = exports.writeEvaluationRecord = exports.buildEvaluationRecord = exports.formatReportCli = exports.generateReport = exports.getHistoryFilePath = exports.buildSnapshot = exports.loadSnapshots = exports.appendSnapshot = exports.buildRoutingRawMetrics = exports.buildModelRawMetrics = exports.buildProviderRawMetrics = exports.buildWorkerRawMetrics = exports.buildForemanRawMetrics = exports.computeAllScorecards = exports.computeRoutingScorecard = exports.computeModelScorecard = exports.computeProviderScorecard = exports.computeWorkerScorecard = exports.computeForemanScorecard = exports.computeSolScoreReport = exports.computeWorkerScore = exports.computeForemanScore = exports.aggregateSolEvidence = exports.routeProposals = exports.validateDiagnosisReport = exports.loadDiagnosisReport = exports.buildProposals = exports.FIX_ZONE_MAP = exports.ALL_GATES = exports.buildDiagnosisHints = exports.computeScore = exports.loadRunArtifacts = exports.scoreRun = exports.assertPolarisDevContext = exports.isPolarisDevContext = void 0;
4
+ exports.scorecardsToRecommendationSummaries = void 0;
4
5
  var dev_gate_js_1 = require("./dev-gate.js");
5
6
  Object.defineProperty(exports, "isPolarisDevContext", { enumerable: true, get: function () { return dev_gate_js_1.isPolarisDevContext; } });
6
7
  Object.defineProperty(exports, "assertPolarisDevContext", { enumerable: true, get: function () { return dev_gate_js_1.assertPolarisDevContext; } });
@@ -24,6 +25,18 @@ var sol_scorer_js_1 = require("./sol-scorer.js");
24
25
  Object.defineProperty(exports, "computeForemanScore", { enumerable: true, get: function () { return sol_scorer_js_1.computeForemanScore; } });
25
26
  Object.defineProperty(exports, "computeWorkerScore", { enumerable: true, get: function () { return sol_scorer_js_1.computeWorkerScore; } });
26
27
  Object.defineProperty(exports, "computeSolScoreReport", { enumerable: true, get: function () { return sol_scorer_js_1.computeSolScoreReport; } });
28
+ var sol_scorecard_calculator_js_1 = require("./sol-scorecard-calculator.js");
29
+ Object.defineProperty(exports, "computeForemanScorecard", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.computeForemanScorecard; } });
30
+ Object.defineProperty(exports, "computeWorkerScorecard", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.computeWorkerScorecard; } });
31
+ Object.defineProperty(exports, "computeProviderScorecard", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.computeProviderScorecard; } });
32
+ Object.defineProperty(exports, "computeModelScorecard", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.computeModelScorecard; } });
33
+ Object.defineProperty(exports, "computeRoutingScorecard", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.computeRoutingScorecard; } });
34
+ Object.defineProperty(exports, "computeAllScorecards", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.computeAllScorecards; } });
35
+ Object.defineProperty(exports, "buildForemanRawMetrics", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.buildForemanRawMetrics; } });
36
+ Object.defineProperty(exports, "buildWorkerRawMetrics", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.buildWorkerRawMetrics; } });
37
+ Object.defineProperty(exports, "buildProviderRawMetrics", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.buildProviderRawMetrics; } });
38
+ Object.defineProperty(exports, "buildModelRawMetrics", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.buildModelRawMetrics; } });
39
+ Object.defineProperty(exports, "buildRoutingRawMetrics", { enumerable: true, get: function () { return sol_scorecard_calculator_js_1.buildRoutingRawMetrics; } });
27
40
  var sol_history_js_1 = require("./sol-history.js");
28
41
  Object.defineProperty(exports, "appendSnapshot", { enumerable: true, get: function () { return sol_history_js_1.appendSnapshot; } });
29
42
  Object.defineProperty(exports, "loadSnapshots", { enumerable: true, get: function () { return sol_history_js_1.loadSnapshots; } });
@@ -32,8 +45,24 @@ Object.defineProperty(exports, "getHistoryFilePath", { enumerable: true, get: fu
32
45
  var sol_report_js_1 = require("./sol-report.js");
33
46
  Object.defineProperty(exports, "generateReport", { enumerable: true, get: function () { return sol_report_js_1.generateReport; } });
34
47
  Object.defineProperty(exports, "formatReportCli", { enumerable: true, get: function () { return sol_report_js_1.formatReportCli; } });
48
+ var sol_evaluation_writer_js_1 = require("./sol-evaluation-writer.js");
49
+ Object.defineProperty(exports, "buildEvaluationRecord", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.buildEvaluationRecord; } });
50
+ Object.defineProperty(exports, "writeEvaluationRecord", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.writeEvaluationRecord; } });
51
+ Object.defineProperty(exports, "writeScorecard", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.writeScorecard; } });
52
+ Object.defineProperty(exports, "writeScorecardSet", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.writeScorecardSet; } });
53
+ Object.defineProperty(exports, "writeSolMarkdownReport", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.writeSolMarkdownReport; } });
54
+ Object.defineProperty(exports, "getSolEvaluationsDir", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.getSolEvaluationsDir; } });
55
+ Object.defineProperty(exports, "getSolScorecardsDir", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.getSolScorecardsDir; } });
56
+ Object.defineProperty(exports, "getSolReportsDir", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.getSolReportsDir; } });
57
+ Object.defineProperty(exports, "getEvaluationRecordPath", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.getEvaluationRecordPath; } });
58
+ Object.defineProperty(exports, "getScorecardPath", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.getScorecardPath; } });
59
+ Object.defineProperty(exports, "getSolMarkdownReportPath", { enumerable: true, get: function () { return sol_evaluation_writer_js_1.getSolMarkdownReportPath; } });
60
+ var sol_report_renderer_js_1 = require("./sol-report-renderer.js");
61
+ Object.defineProperty(exports, "renderSolMarkdown", { enumerable: true, get: function () { return sol_report_renderer_js_1.renderSolMarkdown; } });
35
62
  var sol_recommendations_js_1 = require("./sol-recommendations.js");
36
63
  Object.defineProperty(exports, "generateRecommendations", { enumerable: true, get: function () { return sol_recommendations_js_1.generateRecommendations; } });
37
64
  Object.defineProperty(exports, "recommendationsToProposals", { enumerable: true, get: function () { return sol_recommendations_js_1.recommendationsToProposals; } });
38
65
  Object.defineProperty(exports, "recommendationToProposal", { enumerable: true, get: function () { return sol_recommendations_js_1.recommendationToProposal; } });
39
66
  Object.defineProperty(exports, "formatRecommendationsCli", { enumerable: true, get: function () { return sol_recommendations_js_1.formatRecommendationsCli; } });
67
+ Object.defineProperty(exports, "scorecardToRecommendationSummary", { enumerable: true, get: function () { return sol_recommendations_js_1.scorecardToRecommendationSummary; } });
68
+ Object.defineProperty(exports, "scorecardsToRecommendationSummaries", { enumerable: true, get: function () { return sol_recommendations_js_1.scorecardsToRecommendationSummaries; } });
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ /**
3
+ * SOL evaluation artifact writer.
4
+ *
5
+ * Persists machine-readable SOL evaluation records and scorecard snapshots
6
+ * to deterministic local paths. All writes are append-only / generation-safe:
7
+ * existing source run evidence is never read or rewritten.
8
+ *
9
+ * Output locations:
10
+ * - Evaluations: .polaris/sol/evaluations/<run-id>.json
11
+ * - Scorecards: .polaris/sol/scorecards/<subject>/<scorecard-id>.json
12
+ * - Reports: smartdocs/reports/sol/<run-id>-evaluation-report.md
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.getSolEvaluationsDir = getSolEvaluationsDir;
16
+ exports.getSolScorecardsDir = getSolScorecardsDir;
17
+ exports.getSolReportsDir = getSolReportsDir;
18
+ exports.buildEvaluationRecord = buildEvaluationRecord;
19
+ exports.getEvaluationRecordPath = getEvaluationRecordPath;
20
+ exports.writeEvaluationRecord = writeEvaluationRecord;
21
+ exports.getScorecardPath = getScorecardPath;
22
+ exports.writeScorecard = writeScorecard;
23
+ exports.writeScorecardSet = writeScorecardSet;
24
+ exports.getSolMarkdownReportPath = getSolMarkdownReportPath;
25
+ exports.writeSolMarkdownReport = writeSolMarkdownReport;
26
+ const node_fs_1 = require("node:fs");
27
+ const node_path_1 = require("node:path");
28
+ // ──────────────────────────────────────────────
29
+ // Paths
30
+ // ──────────────────────────────────────────────
31
+ const EVALUATIONS_DIR = ".polaris/sol/evaluations";
32
+ const SCORECARDS_DIR = ".polaris/sol/scorecards";
33
+ const REPORTS_DIR = "smartdocs/reports/sol";
34
+ function getSolEvaluationsDir(repoRoot) {
35
+ return (0, node_path_1.join)(repoRoot, EVALUATIONS_DIR);
36
+ }
37
+ function getSolScorecardsDir(repoRoot) {
38
+ return (0, node_path_1.join)(repoRoot, SCORECARDS_DIR);
39
+ }
40
+ function getSolReportsDir(repoRoot) {
41
+ return (0, node_path_1.join)(repoRoot, REPORTS_DIR);
42
+ }
43
+ // ──────────────────────────────────────────────
44
+ // Filename safety
45
+ // ──────────────────────────────────────────────
46
+ function safeFilename(value) {
47
+ // Prevent path traversal and reserved characters while keeping readability.
48
+ const sanitized = value
49
+ .replace(/[\\/]+/g, "-")
50
+ .replace(/[:\?*"<>|]+/g, "-")
51
+ .replace(/\.{2,}/g, "-")
52
+ .replace(/^[.]+/, "")
53
+ .trim();
54
+ return sanitized.length > 0 ? sanitized : "unnamed";
55
+ }
56
+ function buildEvaluationRecord(report) {
57
+ return {
58
+ schema_version: "1.0",
59
+ record_type: "sol-evaluation",
60
+ run_id: report.run_id,
61
+ cluster_id: report.cluster_id,
62
+ scored_at: report.scored_at,
63
+ report,
64
+ generated_at: new Date().toISOString(),
65
+ };
66
+ }
67
+ function getEvaluationRecordPath(repoRoot, runId) {
68
+ return (0, node_path_1.join)(getSolEvaluationsDir(repoRoot), `${safeFilename(runId)}.json`);
69
+ }
70
+ /**
71
+ * Write a SOL evaluation record under .polaris/sol/evaluations/.
72
+ * Creates directories as needed. Overwrites only the derived record file.
73
+ */
74
+ function writeEvaluationRecord(repoRoot, report) {
75
+ const record = buildEvaluationRecord(report);
76
+ const dir = getSolEvaluationsDir(repoRoot);
77
+ if (!(0, node_fs_1.existsSync)(dir))
78
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
79
+ const filePath = getEvaluationRecordPath(repoRoot, report.run_id);
80
+ (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(record, null, 2) + "\n", "utf-8");
81
+ return { record, path: filePath };
82
+ }
83
+ // ──────────────────────────────────────────────
84
+ // Scorecard snapshots
85
+ // ──────────────────────────────────────────────
86
+ function scorecardSubjectDir(subject) {
87
+ return subject;
88
+ }
89
+ function getScorecardPath(repoRoot, scorecard) {
90
+ return (0, node_path_1.join)(getSolScorecardsDir(repoRoot), scorecardSubjectDir(scorecard.subject), `${safeFilename(scorecard.scorecard_id)}.json`);
91
+ }
92
+ /**
93
+ * Write a single scorecard snapshot under .polaris/sol/scorecards/<subject>/.
94
+ */
95
+ function writeScorecard(repoRoot, scorecard) {
96
+ const dir = (0, node_path_1.join)(getSolScorecardsDir(repoRoot), scorecardSubjectDir(scorecard.subject));
97
+ if (!(0, node_fs_1.existsSync)(dir))
98
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
99
+ const filePath = getScorecardPath(repoRoot, scorecard);
100
+ (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(scorecard, null, 2) + "\n", "utf-8");
101
+ return filePath;
102
+ }
103
+ /**
104
+ * Write all scorecards in a SolScorecardSet. Returns the paths written.
105
+ */
106
+ function writeScorecardSet(repoRoot, scorecardSet) {
107
+ const paths = [];
108
+ paths.push(writeScorecard(repoRoot, scorecardSet.foreman));
109
+ for (const s of scorecardSet.workers)
110
+ paths.push(writeScorecard(repoRoot, s));
111
+ for (const s of scorecardSet.providers)
112
+ paths.push(writeScorecard(repoRoot, s));
113
+ for (const s of scorecardSet.models)
114
+ paths.push(writeScorecard(repoRoot, s));
115
+ for (const s of scorecardSet.routing)
116
+ paths.push(writeScorecard(repoRoot, s));
117
+ return paths;
118
+ }
119
+ // ──────────────────────────────────────────────
120
+ // Human-readable SmartDocs report
121
+ // ──────────────────────────────────────────────
122
+ function getSolMarkdownReportPath(repoRoot, runId) {
123
+ return (0, node_path_1.join)(getSolReportsDir(repoRoot), `${safeFilename(runId)}-evaluation-report.md`);
124
+ }
125
+ /**
126
+ * Write a rendered Markdown SOL report to smartdocs/reports/sol/.
127
+ */
128
+ function writeSolMarkdownReport(repoRoot, runId, markdown) {
129
+ const dir = getSolReportsDir(repoRoot);
130
+ if (!(0, node_fs_1.existsSync)(dir))
131
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
132
+ const filePath = getSolMarkdownReportPath(repoRoot, runId);
133
+ (0, node_fs_1.writeFileSync)(filePath, markdown, "utf-8");
134
+ return filePath;
135
+ }
@@ -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
+ }
@@ -21,6 +21,8 @@ exports.recommendationsToProposals = recommendationsToProposals;
21
21
  exports.formatRecommendationsCli = formatRecommendationsCli;
22
22
  exports.generateQcRecommendations = generateQcRecommendations;
23
23
  exports.formatQcRecommendations = formatQcRecommendations;
24
+ exports.scorecardToRecommendationSummary = scorecardToRecommendationSummary;
25
+ exports.scorecardsToRecommendationSummaries = scorecardsToRecommendationSummaries;
24
26
  const sol_report_js_1 = require("./sol-report.js");
25
27
  // ──────────────────────────────────────────────
26
28
  // Constants and helpers
@@ -380,3 +382,131 @@ function formatQcRecommendations(report) {
380
382
  }
381
383
  return lines.join("\n");
382
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
+ }