@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.
- package/dist/autoresearch/index.js +30 -1
- package/dist/autoresearch/sol-evaluation-writer.js +135 -0
- package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
- package/dist/autoresearch/sol-recommendations.js +130 -0
- package/dist/autoresearch/sol-report-renderer.js +282 -0
- package/dist/autoresearch/sol-run-health-bridge.js +246 -0
- package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cli/medic.js +65 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/config/defaults.js +3 -0
- package/dist/config/validator.js +141 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +231 -5
- package/dist/finalize/linear.js +8 -4
- package/dist/finalize/medic-gate.js +42 -0
- 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 +234 -28
- package/dist/loop/worker-packet.js +19 -1
- package/dist/medic/run-health-consult.js +381 -0
- package/dist/medic/treatment-packets.js +169 -0
- package/dist/qc/artifacts.js +27 -0
- package/dist/qc/orchestration.js +3 -2
- package/dist/qc/providers/coderabbit.js +114 -11
- package/dist/qc/repair-loop.js +49 -14
- package/dist/qc/runner.js +10 -2
- package/dist/qc/schemas.js +1 -0
- package/dist/run-health/foreman-symptoms.js +100 -0
- package/dist/run-health/index.js +301 -0
- package/dist/run-health/qc-escalation.js +155 -0
- package/dist/run-health/schema.js +123 -0
- package/dist/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- 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
|
+
}
|