@lsctech/polaris 0.5.7 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-scorecard-calculator.js +865 -0
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +197 -3
- 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 +33 -28
- 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/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- package/package.json +1 -1
package/dist/loop/parent.js
CHANGED
|
@@ -2031,35 +2031,40 @@ async function runParentLoop(options) {
|
|
|
2031
2031
|
};
|
|
2032
2032
|
}
|
|
2033
2033
|
// ── Step 05 (post-dispatch): Re-check budget before next iteration ───
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2034
|
+
// Skip when open_children is empty: the cluster is complete and the
|
|
2035
|
+
// top-of-loop nextChild === null path handles cluster-complete and QC
|
|
2036
|
+
// repair-loop. Halting here would bypass QC repair on final-child completion.
|
|
2037
|
+
if (state.open_children.length > 0) {
|
|
2038
|
+
const postBudgetCheck = (0, budget_js_1.checkBudget)({
|
|
2039
|
+
childrenCompleted: state.context_budget.children_completed,
|
|
2040
|
+
lastChildStatus: workerStatus,
|
|
2041
|
+
policy: budgetPolicy,
|
|
2042
|
+
});
|
|
2043
|
+
if (postBudgetCheck.status === 'exhausted') {
|
|
2044
|
+
const nextPending = state.open_children[0] ?? null;
|
|
2045
|
+
if (!dryRun) {
|
|
2046
|
+
(0, checkpoint_js_1.writeStateAtomic)(stateFile, {
|
|
2047
|
+
...state,
|
|
2048
|
+
status: "budget-exhausted",
|
|
2049
|
+
step_cursor: "budget-check",
|
|
2050
|
+
next_open_child: nextPending,
|
|
2051
|
+
});
|
|
2052
|
+
appendTelemetry(telemetryFile, {
|
|
2053
|
+
event: "budget-exhausted",
|
|
2054
|
+
run_id: state.run_id,
|
|
2055
|
+
children_completed: state.context_budget.children_completed,
|
|
2056
|
+
next_child: nextPending,
|
|
2057
|
+
reason: postBudgetCheck.reason,
|
|
2058
|
+
timestamp: new Date().toISOString(),
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
return {
|
|
2062
|
+
haltReason: 'budget-exhausted',
|
|
2063
|
+
childrenDispatched,
|
|
2064
|
+
haltingChild: nextPending ?? undefined,
|
|
2065
|
+
message: postBudgetCheck.reason,
|
|
2066
|
+
};
|
|
2056
2067
|
}
|
|
2057
|
-
return {
|
|
2058
|
-
haltReason: 'budget-exhausted',
|
|
2059
|
-
childrenDispatched,
|
|
2060
|
-
haltingChild: nextPending ?? undefined,
|
|
2061
|
-
message: postBudgetCheck.reason,
|
|
2062
|
-
};
|
|
2063
2068
|
}
|
|
2064
2069
|
// ── Step 06: CONTINUE (back to step 02) ─────────────────────────────
|
|
2065
2070
|
}
|
package/dist/qc/artifacts.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.getQcArtifactDir = getQcArtifactDir;
|
|
|
37
37
|
exports.writeQcArtifact = writeQcArtifact;
|
|
38
38
|
exports.readQcArtifact = readQcArtifact;
|
|
39
39
|
exports.listQcArtifactIds = listQcArtifactIds;
|
|
40
|
+
exports.validateQcArtifactPointers = validateQcArtifactPointers;
|
|
40
41
|
const node_fs_1 = require("node:fs");
|
|
41
42
|
const path = __importStar(require("node:path"));
|
|
42
43
|
const schemas_js_1 = require("./schemas.js");
|
|
@@ -115,3 +116,29 @@ function listQcArtifactIds(clusterId, repoRoot) {
|
|
|
115
116
|
throw error;
|
|
116
117
|
}
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Validate that the artifacts referenced by QC run pointers still exist.
|
|
121
|
+
* Returns a list of missing primary artifacts and unavailable raw audit
|
|
122
|
+
* artifacts so callers can warn, prune, or mark pointers consistently.
|
|
123
|
+
*/
|
|
124
|
+
function validateQcArtifactPointers(qcRuns) {
|
|
125
|
+
const missing = [];
|
|
126
|
+
const unavailable = [];
|
|
127
|
+
if (!qcRuns) {
|
|
128
|
+
return { ok: true, missing, unavailable };
|
|
129
|
+
}
|
|
130
|
+
for (const pointer of Object.values(qcRuns)) {
|
|
131
|
+
if (!(0, node_fs_1.existsSync)(pointer.artifact_path)) {
|
|
132
|
+
missing.push(pointer.artifact_path);
|
|
133
|
+
}
|
|
134
|
+
for (const rawPath of pointer.raw_artifact_paths ?? []) {
|
|
135
|
+
if (!(0, node_fs_1.existsSync)(rawPath)) {
|
|
136
|
+
unavailable.push(rawPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (pointer.provider_attempt_artifact_path && !(0, node_fs_1.existsSync)(pointer.provider_attempt_artifact_path)) {
|
|
140
|
+
unavailable.push(pointer.provider_attempt_artifact_path);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { ok: missing.length === 0 && unavailable.length === 0, missing, unavailable };
|
|
144
|
+
}
|
package/dist/qc/orchestration.js
CHANGED
|
@@ -20,7 +20,7 @@ const attribution_js_1 = require("./attribution.js");
|
|
|
20
20
|
const autofix_js_1 = require("./autofix.js");
|
|
21
21
|
const routing_js_1 = require("./routing.js");
|
|
22
22
|
function buildAttributionContext(options) {
|
|
23
|
-
const { repoRoot, clusterId, branch, state } = options;
|
|
23
|
+
const { repoRoot, clusterId, branch, baseRef, state } = options;
|
|
24
24
|
const dispatchRecords = {};
|
|
25
25
|
if (state?.open_children_meta) {
|
|
26
26
|
for (const [childId, meta] of Object.entries(state.open_children_meta)) {
|
|
@@ -31,7 +31,7 @@ function buildAttributionContext(options) {
|
|
|
31
31
|
}
|
|
32
32
|
return {
|
|
33
33
|
repoRoot,
|
|
34
|
-
baseBranch: branch ?? state?.branch ?? "main",
|
|
34
|
+
baseBranch: baseRef ?? branch ?? state?.branch ?? "main",
|
|
35
35
|
completedResults: state?.completed_children_results,
|
|
36
36
|
dispatchRecords,
|
|
37
37
|
clusterState: repoRoot ? ((0, store_js_1.readClusterStateSync)(clusterId, repoRoot) ?? undefined) : undefined,
|
|
@@ -99,6 +99,7 @@ async function runQcAtTrigger(options) {
|
|
|
99
99
|
clusterId,
|
|
100
100
|
runId,
|
|
101
101
|
...(trigger === "pr" ? { prUrl } : { branch }),
|
|
102
|
+
...(options.baseRef ? { baseRef: options.baseRef } : {}),
|
|
102
103
|
};
|
|
103
104
|
try {
|
|
104
105
|
const rawResult = await (0, runner_js_1.executeQcProvider)(provider, scope, {
|
|
@@ -16,6 +16,79 @@ function pickString(...candidates) {
|
|
|
16
16
|
}
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
|
+
const FINDING_LOCATION_KEYS = ["file", "filePath", "path"];
|
|
20
|
+
const FINDING_REVIEW_KEYS = [
|
|
21
|
+
"severity",
|
|
22
|
+
"message",
|
|
23
|
+
"title",
|
|
24
|
+
"summary",
|
|
25
|
+
"description",
|
|
26
|
+
"body",
|
|
27
|
+
"rule",
|
|
28
|
+
"category",
|
|
29
|
+
"suggestion",
|
|
30
|
+
"suggestedAction",
|
|
31
|
+
"fix",
|
|
32
|
+
"providerFindingId",
|
|
33
|
+
"id",
|
|
34
|
+
"findingId",
|
|
35
|
+
];
|
|
36
|
+
const PROGRESS_SHAPE_KEYS = new Set(["event", "progress", "heartbeat", "complete", "review_context"]);
|
|
37
|
+
const PROGRESS_TYPE_VALUES = new Set([
|
|
38
|
+
"progress",
|
|
39
|
+
"status",
|
|
40
|
+
"heartbeat",
|
|
41
|
+
"complete",
|
|
42
|
+
"review_context",
|
|
43
|
+
"reviewcontext",
|
|
44
|
+
]);
|
|
45
|
+
const PROGRESS_STATUS_VALUES = new Set([
|
|
46
|
+
"in_progress",
|
|
47
|
+
"running",
|
|
48
|
+
"pending",
|
|
49
|
+
"complete",
|
|
50
|
+
"completed",
|
|
51
|
+
"done",
|
|
52
|
+
"heartbeat",
|
|
53
|
+
"ok",
|
|
54
|
+
"success",
|
|
55
|
+
]);
|
|
56
|
+
function hasFindingLocation(record) {
|
|
57
|
+
return FINDING_LOCATION_KEYS.some((key) => record[key] !== undefined);
|
|
58
|
+
}
|
|
59
|
+
function hasFindingReviewContent(record) {
|
|
60
|
+
return FINDING_REVIEW_KEYS.some((key) => record[key] !== undefined);
|
|
61
|
+
}
|
|
62
|
+
function isProgressRecord(record) {
|
|
63
|
+
// Check progress/status indicators FIRST before the generic finding-content guard
|
|
64
|
+
const keys = Object.keys(record);
|
|
65
|
+
if (keys.length === 0)
|
|
66
|
+
return false;
|
|
67
|
+
if (keys.some((key) => PROGRESS_SHAPE_KEYS.has(key)))
|
|
68
|
+
return true;
|
|
69
|
+
if (typeof record.type === "string" && PROGRESS_TYPE_VALUES.has(record.type.toLowerCase()))
|
|
70
|
+
return true;
|
|
71
|
+
if (typeof record.status === "string" && PROGRESS_STATUS_VALUES.has(record.status.toLowerCase()))
|
|
72
|
+
return true;
|
|
73
|
+
// Status-only records with category="status" are progress records even if they have message/title fields
|
|
74
|
+
if (typeof record.category === "string" && record.category.toLowerCase() === "status")
|
|
75
|
+
return true;
|
|
76
|
+
// Only reject as progress if it has both location AND review content (true finding shape)
|
|
77
|
+
if (hasFindingLocation(record) && hasFindingReviewContent(record)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function isActionableFinding(record) {
|
|
83
|
+
if (isProgressRecord(record))
|
|
84
|
+
return false;
|
|
85
|
+
return hasFindingLocation(record) || hasFindingReviewContent(record);
|
|
86
|
+
}
|
|
87
|
+
function makeUnusableOutputError(message) {
|
|
88
|
+
const err = new Error(message);
|
|
89
|
+
err.qcFailureReason = "unusable-output";
|
|
90
|
+
return err;
|
|
91
|
+
}
|
|
19
92
|
function buildRange(raw) {
|
|
20
93
|
const startLine = coerceNumber(raw.startLine ?? raw.line) ?? 1;
|
|
21
94
|
const endLine = coerceNumber(raw.endLine);
|
|
@@ -37,18 +110,16 @@ function parseFindingsFromPayload(payload) {
|
|
|
37
110
|
}
|
|
38
111
|
const record = payload;
|
|
39
112
|
if (Array.isArray(record.findings)) {
|
|
40
|
-
return record.findings;
|
|
113
|
+
return record.findings.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
41
114
|
}
|
|
42
115
|
if (Array.isArray(record.issues)) {
|
|
43
|
-
return record.issues;
|
|
116
|
+
return record.issues.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
44
117
|
}
|
|
45
118
|
if (Array.isArray(record.results)) {
|
|
46
|
-
return record.results;
|
|
119
|
+
return record.results.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
47
120
|
}
|
|
48
121
|
// Single finding wrapped in an object
|
|
49
|
-
if (record
|
|
50
|
-
record.message !== undefined ||
|
|
51
|
-
record.title !== undefined) {
|
|
122
|
+
if (isActionableFinding(record)) {
|
|
52
123
|
return [record];
|
|
53
124
|
}
|
|
54
125
|
return [];
|
|
@@ -77,10 +148,21 @@ function parseReport(output, format, parser) {
|
|
|
77
148
|
try {
|
|
78
149
|
const parsed = JSON.parse(text);
|
|
79
150
|
if (Array.isArray(parsed)) {
|
|
80
|
-
|
|
151
|
+
const findings = parsed.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
152
|
+
if (findings.length === 0 && parsed.length > 0) {
|
|
153
|
+
const progressCount = parsed.filter((item) => typeof item === "object" && item !== null && isProgressRecord(item)).length;
|
|
154
|
+
if (progressCount > 0) {
|
|
155
|
+
throw makeUnusableOutputError(`CodeRabbit output contained only progress/status/heartbeat records (${progressCount} items)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { findings };
|
|
159
|
+
}
|
|
160
|
+
const findings = parseFindingsFromPayload(parsed);
|
|
161
|
+
if (findings.length === 0 && isProgressRecord(parsed)) {
|
|
162
|
+
throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
|
|
81
163
|
}
|
|
82
164
|
return {
|
|
83
|
-
findings
|
|
165
|
+
findings,
|
|
84
166
|
...(typeof parsed?.prUrl === "string"
|
|
85
167
|
? { prUrl: parsed.prUrl }
|
|
86
168
|
: {}),
|
|
@@ -97,10 +179,23 @@ function parseReport(output, format, parser) {
|
|
|
97
179
|
// JSONL or generic line scanning: one finding per line
|
|
98
180
|
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
99
181
|
const lineFindings = [];
|
|
182
|
+
let progressLineCount = 0;
|
|
183
|
+
let parsedLineCount = 0;
|
|
100
184
|
for (const line of lines) {
|
|
101
185
|
try {
|
|
102
186
|
const parsed = JSON.parse(line);
|
|
103
|
-
|
|
187
|
+
parsedLineCount++;
|
|
188
|
+
if (!parsed || typeof parsed !== "object") {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const record = parsed;
|
|
192
|
+
if (isProgressRecord(record)) {
|
|
193
|
+
progressLineCount++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (isActionableFinding(record)) {
|
|
197
|
+
lineFindings.push(parsed);
|
|
198
|
+
}
|
|
104
199
|
}
|
|
105
200
|
catch {
|
|
106
201
|
// Ignore unparseable lines.
|
|
@@ -109,6 +204,12 @@ function parseReport(output, format, parser) {
|
|
|
109
204
|
if (lineFindings.length > 0) {
|
|
110
205
|
return { findings: lineFindings };
|
|
111
206
|
}
|
|
207
|
+
if (progressLineCount > 0) {
|
|
208
|
+
throw makeUnusableOutputError(`CodeRabbit output contained only progress/status/heartbeat records (${progressLineCount} lines)`);
|
|
209
|
+
}
|
|
210
|
+
if (parsedLineCount > 0) {
|
|
211
|
+
throw new Error("CodeRabbit output contained no actionable findings");
|
|
212
|
+
}
|
|
112
213
|
throw new Error("CodeRabbit output could not be parsed as JSON, JSONL, or metrics payload");
|
|
113
214
|
}
|
|
114
215
|
function normalizeFinding(raw, index) {
|
|
@@ -217,20 +318,22 @@ class CodeRabbitQcProvider {
|
|
|
217
318
|
if (execution.configPath) {
|
|
218
319
|
args.push("--config", execution.configPath);
|
|
219
320
|
}
|
|
321
|
+
const baseRef = scope.baseRef ?? scope.branch ?? "main";
|
|
220
322
|
if (scope.prUrl) {
|
|
221
323
|
args.push("--pr-url", scope.prUrl);
|
|
222
324
|
}
|
|
223
325
|
else {
|
|
224
|
-
args.push("--
|
|
326
|
+
args.push("--base", baseRef);
|
|
225
327
|
}
|
|
226
328
|
return { command: execution.command, args };
|
|
227
329
|
}
|
|
228
330
|
if (scope.prUrl) {
|
|
229
331
|
return { command: "coderabbit", args: ["review", "--agent", "--pr-url", scope.prUrl] };
|
|
230
332
|
}
|
|
333
|
+
const baseRef = scope.baseRef ?? scope.branch ?? "main";
|
|
231
334
|
return {
|
|
232
335
|
command: "coderabbit",
|
|
233
|
-
args: ["review", "--agent", "--
|
|
336
|
+
args: ["review", "--agent", "--base", baseRef],
|
|
234
337
|
};
|
|
235
338
|
}
|
|
236
339
|
parse(output) {
|
package/dist/qc/repair-loop.js
CHANGED
|
@@ -33,6 +33,43 @@ function appendTelemetry(telemetryFile, event) {
|
|
|
33
33
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
|
|
34
34
|
(0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
|
|
35
35
|
}
|
|
36
|
+
async function persistQcRepairOutcome(clusterId, repoRoot, outcome) {
|
|
37
|
+
try {
|
|
38
|
+
const clusterState = (0, store_js_1.readClusterStateSync)(clusterId, repoRoot);
|
|
39
|
+
if (!clusterState)
|
|
40
|
+
return;
|
|
41
|
+
await (0, store_js_1.writeClusterState)(clusterId, {
|
|
42
|
+
...clusterState,
|
|
43
|
+
state_generation: clusterState.state_generation + 1,
|
|
44
|
+
qc_repair_outcome: outcome,
|
|
45
|
+
}, repoRoot);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Best-effort: the loop outcome is already returned to the caller and
|
|
49
|
+
// recorded in the loop checkpoint. Do not block finalization on a
|
|
50
|
+
// cluster-state write race.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function persistQcRepairManifest(clusterId, repoRoot, round, manifestPath) {
|
|
54
|
+
try {
|
|
55
|
+
const clusterState = (0, store_js_1.readClusterStateSync)(clusterId, repoRoot);
|
|
56
|
+
if (!clusterState)
|
|
57
|
+
return;
|
|
58
|
+
const manifests = { ...(clusterState.qc_repair_manifests ?? {}), [round]: manifestPath };
|
|
59
|
+
if (clusterState.qc_repair_manifests &&
|
|
60
|
+
clusterState.qc_repair_manifests[round] === manifestPath) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await (0, store_js_1.writeClusterState)(clusterId, {
|
|
64
|
+
...clusterState,
|
|
65
|
+
state_generation: clusterState.state_generation + 1,
|
|
66
|
+
qc_repair_manifests: manifests,
|
|
67
|
+
}, repoRoot);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Best-effort durability for repair-round manifest pointer.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
36
73
|
/** Returns true when any finding in the results routes to repair-worker. */
|
|
37
74
|
function hasRepairableFindings(results) {
|
|
38
75
|
return results.some((r) => r.findings.some((f) => (f.routingDecision === "repair-worker" || f.routingDecision === "original-worker") &&
|
|
@@ -109,6 +146,7 @@ async function runQcRepairLoop(options) {
|
|
|
109
146
|
});
|
|
110
147
|
state.terminal_outcome = "qc-disabled";
|
|
111
148
|
state.updated_at = new Date().toISOString();
|
|
149
|
+
await persistQcRepairOutcome(clusterId, repoRoot, "qc-disabled");
|
|
112
150
|
return {
|
|
113
151
|
outcome: "qc-disabled",
|
|
114
152
|
rounds_completed: 0,
|
|
@@ -147,6 +185,7 @@ async function runQcRepairLoop(options) {
|
|
|
147
185
|
round,
|
|
148
186
|
timestamp: new Date().toISOString(),
|
|
149
187
|
});
|
|
188
|
+
await persistQcRepairOutcome(clusterId, repoRoot, "all-providers-failed");
|
|
150
189
|
return {
|
|
151
190
|
outcome: "all-providers-failed",
|
|
152
191
|
rounds_completed: roundsCompleted,
|
|
@@ -165,6 +204,7 @@ async function runQcRepairLoop(options) {
|
|
|
165
204
|
round,
|
|
166
205
|
timestamp: new Date().toISOString(),
|
|
167
206
|
});
|
|
207
|
+
await persistQcRepairOutcome(clusterId, repoRoot, "operator-review");
|
|
168
208
|
return {
|
|
169
209
|
outcome: "operator-review",
|
|
170
210
|
rounds_completed: roundsCompleted,
|
|
@@ -189,19 +229,7 @@ async function runQcRepairLoop(options) {
|
|
|
189
229
|
});
|
|
190
230
|
manifest = compiled.manifest;
|
|
191
231
|
// Update cluster state with manifest path.
|
|
192
|
-
|
|
193
|
-
const clusterState = (0, store_js_1.readClusterStateSync)(clusterId, repoRoot);
|
|
194
|
-
if (clusterState) {
|
|
195
|
-
const repairManifests = {
|
|
196
|
-
...(clusterState.qc_repair_manifests ?? {}),
|
|
197
|
-
[round]: compiled.manifestPath,
|
|
198
|
-
};
|
|
199
|
-
await (0, store_js_1.writeClusterState)(clusterId, { ...clusterState, state_generation: clusterState.state_generation + 1, qc_repair_manifests: repairManifests }, repoRoot);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
// Non-fatal: cluster state update is best-effort.
|
|
204
|
-
}
|
|
232
|
+
await persistQcRepairManifest(clusterId, repoRoot, round, compiled.manifestPath);
|
|
205
233
|
loopState = {
|
|
206
234
|
...loopState,
|
|
207
235
|
current_round: round,
|
|
@@ -221,13 +249,16 @@ async function runQcRepairLoop(options) {
|
|
|
221
249
|
});
|
|
222
250
|
}
|
|
223
251
|
else {
|
|
252
|
+
const existingManifestPath = loopState.manifest_path ??
|
|
253
|
+
(0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds", String(round), "repair-packets.json");
|
|
224
254
|
loopState = {
|
|
225
255
|
...loopState,
|
|
226
256
|
current_round: round,
|
|
227
|
-
manifest_path:
|
|
257
|
+
manifest_path: existingManifestPath,
|
|
228
258
|
updated_at: new Date().toISOString(),
|
|
229
259
|
};
|
|
230
260
|
onStateUpdate?.(loopState);
|
|
261
|
+
await persistQcRepairManifest(clusterId, repoRoot, round, existingManifestPath);
|
|
231
262
|
}
|
|
232
263
|
// ── Check for repairable packets ────────────────────────────────────────
|
|
233
264
|
const repairablePackets = manifest.packets.filter((p) => p.routingTarget === "repair-worker" &&
|
|
@@ -243,6 +274,7 @@ async function runQcRepairLoop(options) {
|
|
|
243
274
|
round,
|
|
244
275
|
timestamp: new Date().toISOString(),
|
|
245
276
|
});
|
|
277
|
+
await persistQcRepairOutcome(clusterId, repoRoot, "no-repairable");
|
|
246
278
|
return {
|
|
247
279
|
outcome: "no-repairable",
|
|
248
280
|
rounds_completed: roundsCompleted,
|
|
@@ -309,6 +341,7 @@ async function runQcRepairLoop(options) {
|
|
|
309
341
|
failed_count: failedWorkers.length,
|
|
310
342
|
timestamp: new Date().toISOString(),
|
|
311
343
|
});
|
|
344
|
+
await persistQcRepairOutcome(clusterId, repoRoot, "medic-referral");
|
|
312
345
|
return {
|
|
313
346
|
outcome: "medic-referral",
|
|
314
347
|
rounds_completed: roundsCompleted + 1,
|
|
@@ -394,6 +427,7 @@ async function runQcRepairLoop(options) {
|
|
|
394
427
|
round,
|
|
395
428
|
timestamp: new Date().toISOString(),
|
|
396
429
|
});
|
|
430
|
+
await persistQcRepairOutcome(clusterId, repoRoot, "pass");
|
|
397
431
|
return {
|
|
398
432
|
outcome: "pass",
|
|
399
433
|
rounds_completed: roundsCompleted,
|
|
@@ -413,6 +447,7 @@ async function runQcRepairLoop(options) {
|
|
|
413
447
|
rounds_completed: roundsCompleted,
|
|
414
448
|
timestamp: new Date().toISOString(),
|
|
415
449
|
});
|
|
450
|
+
await persistQcRepairOutcome(clusterId, repoRoot, "max-rounds");
|
|
416
451
|
return {
|
|
417
452
|
outcome: "max-rounds",
|
|
418
453
|
rounds_completed: roundsCompleted,
|
package/dist/qc/runner.js
CHANGED
|
@@ -298,8 +298,16 @@ async function runSingleProvider(provider, scope, options, fallbackSource) {
|
|
|
298
298
|
resolve({ result, success: true });
|
|
299
299
|
}
|
|
300
300
|
catch (parseError) {
|
|
301
|
-
const
|
|
302
|
-
|
|
301
|
+
const reason = typeof parseError === "object" &&
|
|
302
|
+
parseError !== null &&
|
|
303
|
+
"qcFailureReason" in parseError &&
|
|
304
|
+
typeof parseError.qcFailureReason === "string"
|
|
305
|
+
? parseError.qcFailureReason
|
|
306
|
+
: "parse-failed";
|
|
307
|
+
const result = buildFailedResult(provider, scope, startedAt, reason, output, {
|
|
308
|
+
parserResult: "failed",
|
|
309
|
+
});
|
|
310
|
+
emitProviderFailed(options.telemetryFile, scope.runId, scope.clusterId, provider.name, reason, output.exitCode);
|
|
303
311
|
resolve({ result, success: false });
|
|
304
312
|
}
|
|
305
313
|
});
|
package/dist/qc/schemas.js
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SOL raw metric event contracts.
|
|
4
|
+
*
|
|
5
|
+
* Typed contracts for the raw metric events that the SOL scoring pipeline
|
|
6
|
+
* distinguishes when building evidence and scorecards. These types name the
|
|
7
|
+
* six metric categories defined in the POL-487 acceptance criteria:
|
|
8
|
+
*
|
|
9
|
+
* 1. Provider startup failure
|
|
10
|
+
* 2. Router fallback
|
|
11
|
+
* 3. Worker execution failure
|
|
12
|
+
* 4. Validation failure
|
|
13
|
+
* 5. QC findings
|
|
14
|
+
* 6. User / Foreman intervention
|
|
15
|
+
*
|
|
16
|
+
* These are read-model types — they normalize the raw telemetry events and
|
|
17
|
+
* result packet signals into a typed contract that scoring functions consume.
|
|
18
|
+
* Nothing in this file writes to artifacts or triggers side effects.
|
|
19
|
+
*
|
|
20
|
+
* Relationship to SolEvidence:
|
|
21
|
+
* SolMetricEvent records are materialized by the evidence loader into the
|
|
22
|
+
* SolEvidence structure. The SolEvidence fields (foreman, worker, router,
|
|
23
|
+
* qc, intervention) already carry the aggregated versions; these typed
|
|
24
|
+
* event contracts let callers reason about individual metric records and
|
|
25
|
+
* construct per-scope scorecards.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.isProviderStartupFailure = isProviderStartupFailure;
|
|
29
|
+
exports.isRouterFallback = isRouterFallback;
|
|
30
|
+
exports.isWorkerExecutionFailure = isWorkerExecutionFailure;
|
|
31
|
+
exports.isValidationFailure = isValidationFailure;
|
|
32
|
+
exports.isQcFinding = isQcFinding;
|
|
33
|
+
exports.isIntervention = isIntervention;
|
|
34
|
+
exports.summarizeMetricEvents = summarizeMetricEvents;
|
|
35
|
+
// ──────────────────────────────────────────────
|
|
36
|
+
// Type guards
|
|
37
|
+
// ──────────────────────────────────────────────
|
|
38
|
+
function isProviderStartupFailure(e) {
|
|
39
|
+
return e.category === "provider-startup-failure";
|
|
40
|
+
}
|
|
41
|
+
function isRouterFallback(e) {
|
|
42
|
+
return e.category === "router-fallback";
|
|
43
|
+
}
|
|
44
|
+
function isWorkerExecutionFailure(e) {
|
|
45
|
+
return e.category === "worker-execution-failure";
|
|
46
|
+
}
|
|
47
|
+
function isValidationFailure(e) {
|
|
48
|
+
return e.category === "validation-failure";
|
|
49
|
+
}
|
|
50
|
+
function isQcFinding(e) {
|
|
51
|
+
return e.category === "qc-finding";
|
|
52
|
+
}
|
|
53
|
+
function isIntervention(e) {
|
|
54
|
+
return e.category === "user-intervention" || e.category === "foreman-intervention";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compute a SolMetricSummary from a list of metric events.
|
|
58
|
+
*/
|
|
59
|
+
function summarizeMetricEvents(events) {
|
|
60
|
+
let providerStartupFailures = 0;
|
|
61
|
+
let routerFallbacks = 0;
|
|
62
|
+
let routerFallbackSuccesses = 0;
|
|
63
|
+
let workerExecutionFailures = 0;
|
|
64
|
+
let validationFailures = 0;
|
|
65
|
+
let qcFindingsTotal = 0;
|
|
66
|
+
let qcFindingsBlocking = 0;
|
|
67
|
+
let qcFindingsUnvalidated = 0;
|
|
68
|
+
let userInterventions = 0;
|
|
69
|
+
let foremanInterventions = 0;
|
|
70
|
+
for (const e of events) {
|
|
71
|
+
if (isProviderStartupFailure(e))
|
|
72
|
+
providerStartupFailures++;
|
|
73
|
+
else if (isRouterFallback(e)) {
|
|
74
|
+
routerFallbacks++;
|
|
75
|
+
if (e.fallback_succeeded)
|
|
76
|
+
routerFallbackSuccesses++;
|
|
77
|
+
}
|
|
78
|
+
else if (isWorkerExecutionFailure(e))
|
|
79
|
+
workerExecutionFailures++;
|
|
80
|
+
else if (isValidationFailure(e))
|
|
81
|
+
validationFailures++;
|
|
82
|
+
else if (isQcFinding(e)) {
|
|
83
|
+
qcFindingsTotal++;
|
|
84
|
+
if (e.blocking)
|
|
85
|
+
qcFindingsBlocking++;
|
|
86
|
+
if (e.unvalidated)
|
|
87
|
+
qcFindingsUnvalidated++;
|
|
88
|
+
}
|
|
89
|
+
else if (isIntervention(e)) {
|
|
90
|
+
if (e.actor === "user")
|
|
91
|
+
userInterventions++;
|
|
92
|
+
else
|
|
93
|
+
foremanInterventions++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
provider_startup_failures: providerStartupFailures,
|
|
98
|
+
router_fallbacks: routerFallbacks,
|
|
99
|
+
router_fallback_successes: routerFallbackSuccesses,
|
|
100
|
+
worker_execution_failures: workerExecutionFailures,
|
|
101
|
+
validation_failures: validationFailures,
|
|
102
|
+
qc_findings_total: qcFindingsTotal,
|
|
103
|
+
qc_findings_blocking: qcFindingsBlocking,
|
|
104
|
+
qc_findings_unvalidated: qcFindingsUnvalidated,
|
|
105
|
+
user_interventions: userInterventions,
|
|
106
|
+
foreman_interventions: foremanInterventions,
|
|
107
|
+
};
|
|
108
|
+
}
|