@lsctech/polaris 0.5.10 → 0.5.12
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/proposal.js +61 -0
- package/dist/autoresearch/score.js +124 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/qc.js +97 -0
- package/dist/config/validator.js +16 -6
- package/dist/finalize/artifact-policy.js +35 -3
- package/dist/finalize/index.js +23 -4
- package/dist/finalize/run-report.js +189 -19
- package/dist/finalize/steps/05-generate-report.js +5 -1
- package/dist/finalize/steps/06-commit.js +2 -1
- package/dist/finalize/steps/12-archive.js +6 -0
- package/dist/loop/adapters/terminal-cli.js +131 -25
- package/dist/loop/body-parser.js +69 -6
- package/dist/loop/continue.js +19 -1
- package/dist/loop/dispatch.js +102 -67
- package/dist/loop/parent.js +22 -3
- package/dist/loop/router/engine.js +1 -0
- package/dist/loop/run-preflight.js +4 -1
- package/dist/loop/worker-packet.js +81 -2
- package/dist/medic/routing-signals.js +60 -0
- package/dist/qc/policy.js +2 -0
- package/dist/qc/providers/coderabbit.js +153 -14
- package/dist/qc/repair-loop.js +147 -1
- package/dist/qc/repair-packets.js +16 -3
- package/dist/qc/routing.js +6 -0
- package/dist/qc/types.js +3 -0
- package/dist/tracker/local-graph.js +106 -3
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ function pickString(...candidates) {
|
|
|
16
16
|
}
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
|
-
const FINDING_LOCATION_KEYS = ["file", "filePath", "path"];
|
|
19
|
+
const FINDING_LOCATION_KEYS = ["file", "filePath", "path", "fileName"];
|
|
20
20
|
const FINDING_CONTENT_KEYS = [
|
|
21
21
|
"message",
|
|
22
22
|
"title",
|
|
@@ -26,6 +26,7 @@ const FINDING_CONTENT_KEYS = [
|
|
|
26
26
|
"suggestion",
|
|
27
27
|
"suggestedAction",
|
|
28
28
|
"fix",
|
|
29
|
+
"codegenInstructions",
|
|
29
30
|
];
|
|
30
31
|
const FINDING_BOOKKEEPING_KEYS = [
|
|
31
32
|
"severity",
|
|
@@ -36,6 +37,8 @@ const FINDING_BOOKKEEPING_KEYS = [
|
|
|
36
37
|
"findingId",
|
|
37
38
|
];
|
|
38
39
|
const PROGRESS_SHAPE_KEYS = new Set(["event", "progress", "heartbeat", "complete", "review_context"]);
|
|
40
|
+
const ERROR_TYPE_RATE_LIMIT = new Set(["rate_limit", "rate-limit", "ratelimit", "rate limit"]);
|
|
41
|
+
const ERROR_TYPE_AUTH = new Set(["auth", "authentication", "unauthorized", "unauthorised", "forbidden"]);
|
|
39
42
|
const PROGRESS_TYPE_VALUES = new Set([
|
|
40
43
|
"progress",
|
|
41
44
|
"status",
|
|
@@ -55,6 +58,14 @@ const PROGRESS_STATUS_VALUES = new Set([
|
|
|
55
58
|
"ok",
|
|
56
59
|
"success",
|
|
57
60
|
]);
|
|
61
|
+
const TERMINAL_COMPLETE_STATUSES = new Set([
|
|
62
|
+
"review_completed",
|
|
63
|
+
"review_skipped",
|
|
64
|
+
"completed",
|
|
65
|
+
"complete",
|
|
66
|
+
"done",
|
|
67
|
+
"success",
|
|
68
|
+
]);
|
|
58
69
|
function hasFindingLocation(record) {
|
|
59
70
|
return FINDING_LOCATION_KEYS.some((key) => record[key] !== undefined);
|
|
60
71
|
}
|
|
@@ -111,6 +122,8 @@ function isProgressRecord(record) {
|
|
|
111
122
|
return false;
|
|
112
123
|
}
|
|
113
124
|
function isActionableFinding(record) {
|
|
125
|
+
if (isErrorRecord(record))
|
|
126
|
+
return false;
|
|
114
127
|
if (isProgressRecord(record))
|
|
115
128
|
return false;
|
|
116
129
|
return hasFindingLocation(record) || hasFindingContent(record);
|
|
@@ -123,6 +136,85 @@ function makeUnusableOutputError(message) {
|
|
|
123
136
|
err.qcFailureReason = "unusable-output";
|
|
124
137
|
return err;
|
|
125
138
|
}
|
|
139
|
+
function makeQcFailureError(reason, message) {
|
|
140
|
+
const err = new Error(message);
|
|
141
|
+
err.qcFailureReason = reason;
|
|
142
|
+
return err;
|
|
143
|
+
}
|
|
144
|
+
function isErrorRecord(record) {
|
|
145
|
+
if (record.type === "error")
|
|
146
|
+
return true;
|
|
147
|
+
if (typeof record.errorType === "string")
|
|
148
|
+
return true;
|
|
149
|
+
if (record.error === true || record.error === "true")
|
|
150
|
+
return true;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
function errorTypeFromRecord(record) {
|
|
154
|
+
const fromType = typeof record.errorType === "string" ? record.errorType : undefined;
|
|
155
|
+
const fromMessage = typeof record.message === "string" ? record.message : undefined;
|
|
156
|
+
return fromType ?? fromMessage;
|
|
157
|
+
}
|
|
158
|
+
function classifyErrorType(record) {
|
|
159
|
+
const raw = errorTypeFromRecord(record);
|
|
160
|
+
const errorType = (raw ?? "").toString().toLowerCase().trim();
|
|
161
|
+
if (errorType === "")
|
|
162
|
+
return "unavailable-provider";
|
|
163
|
+
if (ERROR_TYPE_RATE_LIMIT.has(errorType))
|
|
164
|
+
return "rate-limited";
|
|
165
|
+
if (ERROR_TYPE_AUTH.has(errorType))
|
|
166
|
+
return "auth-failure";
|
|
167
|
+
if (errorType.includes("rate limit"))
|
|
168
|
+
return "rate-limited";
|
|
169
|
+
if (errorType.includes("auth"))
|
|
170
|
+
return "auth-failure";
|
|
171
|
+
if (errorType.includes("unauthorized"))
|
|
172
|
+
return "auth-failure";
|
|
173
|
+
if (errorType.includes("forbidden"))
|
|
174
|
+
return "auth-failure";
|
|
175
|
+
if (["timeout", "timed out"].includes(errorType))
|
|
176
|
+
return "timeout";
|
|
177
|
+
if (errorType.includes("not found") || errorType.includes("not_found"))
|
|
178
|
+
return "command-not-found";
|
|
179
|
+
if (errorType.includes("unavailable") || errorType.includes("api_error") || errorType.includes("apierror") || errorType.includes("server") || errorType.includes("network") || errorType.includes("connection") || errorType.includes("internal")) {
|
|
180
|
+
return "unavailable-provider";
|
|
181
|
+
}
|
|
182
|
+
return "unavailable-provider";
|
|
183
|
+
}
|
|
184
|
+
function getTerminalCompleteFindingsCount(record) {
|
|
185
|
+
if (record.findings !== undefined) {
|
|
186
|
+
if (Array.isArray(record.findings)) {
|
|
187
|
+
return record.findings.length;
|
|
188
|
+
}
|
|
189
|
+
const count = typeof record.findings === "number" ? record.findings : Number(record.findings);
|
|
190
|
+
if (Number.isFinite(count))
|
|
191
|
+
return count;
|
|
192
|
+
}
|
|
193
|
+
const summary = record.summary;
|
|
194
|
+
if (summary && typeof summary === "object") {
|
|
195
|
+
const summaryRecord = summary;
|
|
196
|
+
if (summaryRecord.total !== undefined) {
|
|
197
|
+
const count = Number(summaryRecord.total);
|
|
198
|
+
if (Number.isFinite(count))
|
|
199
|
+
return count;
|
|
200
|
+
}
|
|
201
|
+
if (summaryRecord.issues !== undefined) {
|
|
202
|
+
const count = Number(summaryRecord.issues);
|
|
203
|
+
if (Number.isFinite(count))
|
|
204
|
+
return count;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
function isTerminalCompleteNoFindings(record) {
|
|
210
|
+
if (record.type !== "complete")
|
|
211
|
+
return false;
|
|
212
|
+
const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
|
|
213
|
+
if (!TERMINAL_COMPLETE_STATUSES.has(status))
|
|
214
|
+
return false;
|
|
215
|
+
const count = getTerminalCompleteFindingsCount(record);
|
|
216
|
+
return count === undefined || count === 0;
|
|
217
|
+
}
|
|
126
218
|
function buildRange(raw) {
|
|
127
219
|
const startLine = coerceNumber(raw.startLine ?? raw.line) ?? 1;
|
|
128
220
|
const endLine = coerceNumber(raw.endLine);
|
|
@@ -143,14 +235,27 @@ function parseFindingsFromPayload(payload) {
|
|
|
143
235
|
return [];
|
|
144
236
|
}
|
|
145
237
|
const record = payload;
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
if (Array.isArray(record.issues)) {
|
|
150
|
-
return record.issues.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
238
|
+
if (isErrorRecord(record)) {
|
|
239
|
+
throw makeQcFailureError(classifyErrorType(record), `CodeRabbit provider returned an error: ${record.message ?? record.errorType ?? "unknown"}`);
|
|
151
240
|
}
|
|
152
|
-
|
|
153
|
-
|
|
241
|
+
const arrays = [record.findings, record.issues, record.results];
|
|
242
|
+
for (const arr of arrays) {
|
|
243
|
+
if (Array.isArray(arr)) {
|
|
244
|
+
const findings = [];
|
|
245
|
+
for (const item of arr) {
|
|
246
|
+
if (typeof item !== "object" || item === null) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const itemRecord = item;
|
|
250
|
+
if (isErrorRecord(itemRecord)) {
|
|
251
|
+
throw makeQcFailureError(classifyErrorType(itemRecord), `CodeRabbit provider returned an error in findings array: ${itemRecord.message ?? itemRecord.errorType ?? "unknown"}`);
|
|
252
|
+
}
|
|
253
|
+
if (isActionableFinding(itemRecord)) {
|
|
254
|
+
findings.push(item);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return findings;
|
|
258
|
+
}
|
|
154
259
|
}
|
|
155
260
|
// Single finding wrapped in an object
|
|
156
261
|
if (isActionableFinding(record)) {
|
|
@@ -171,6 +276,9 @@ function parseReport(output, format, parser) {
|
|
|
171
276
|
const findings = parseFindingsFromPayload(data);
|
|
172
277
|
const record = data;
|
|
173
278
|
if (findings.length === 0) {
|
|
279
|
+
if (isTerminalCompleteNoFindings(record)) {
|
|
280
|
+
return { findings: [] };
|
|
281
|
+
}
|
|
174
282
|
if (isProgressRecord(record)) {
|
|
175
283
|
throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
|
|
176
284
|
}
|
|
@@ -197,7 +305,19 @@ function parseReport(output, format, parser) {
|
|
|
197
305
|
try {
|
|
198
306
|
const parsed = JSON.parse(text);
|
|
199
307
|
if (Array.isArray(parsed)) {
|
|
200
|
-
const findings =
|
|
308
|
+
const findings = [];
|
|
309
|
+
for (const item of parsed) {
|
|
310
|
+
if (typeof item !== "object" || item === null) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const itemRecord = item;
|
|
314
|
+
if (isErrorRecord(itemRecord)) {
|
|
315
|
+
throw makeQcFailureError(classifyErrorType(itemRecord), `CodeRabbit provider returned an error in JSON array: ${itemRecord.message ?? itemRecord.errorType ?? "unknown"}`);
|
|
316
|
+
}
|
|
317
|
+
if (isActionableFinding(itemRecord)) {
|
|
318
|
+
findings.push(item);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
201
321
|
if (findings.length === 0 && parsed.length > 0) {
|
|
202
322
|
const unusableCount = parsed.filter((item) => typeof item === "object" && item !== null && isUnusableFindingRecord(item)).length;
|
|
203
323
|
if (unusableCount > 0) {
|
|
@@ -209,6 +329,9 @@ function parseReport(output, format, parser) {
|
|
|
209
329
|
const findings = parseFindingsFromPayload(parsed);
|
|
210
330
|
if (findings.length === 0) {
|
|
211
331
|
const parsedRecord = parsed;
|
|
332
|
+
if (isTerminalCompleteNoFindings(parsedRecord)) {
|
|
333
|
+
return { findings: [] };
|
|
334
|
+
}
|
|
212
335
|
if (isProgressRecord(parsedRecord)) {
|
|
213
336
|
throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
|
|
214
337
|
}
|
|
@@ -250,6 +373,7 @@ function parseReport(output, format, parser) {
|
|
|
250
373
|
let progressLineCount = 0;
|
|
251
374
|
let unusableLineCount = 0;
|
|
252
375
|
let parsedLineCount = 0;
|
|
376
|
+
let sawExplicitSkip = false;
|
|
253
377
|
for (const line of lines) {
|
|
254
378
|
try {
|
|
255
379
|
const parsed = JSON.parse(line);
|
|
@@ -258,6 +382,12 @@ function parseReport(output, format, parser) {
|
|
|
258
382
|
continue;
|
|
259
383
|
}
|
|
260
384
|
const record = parsed;
|
|
385
|
+
if (isTerminalCompleteNoFindings(record)) {
|
|
386
|
+
sawExplicitSkip = true;
|
|
387
|
+
}
|
|
388
|
+
if (isErrorRecord(record)) {
|
|
389
|
+
throw makeQcFailureError(classifyErrorType(record), `CodeRabbit provider returned an error in JSONL: ${record.message ?? record.errorType ?? "unknown"}`);
|
|
390
|
+
}
|
|
261
391
|
if (isProgressRecord(record)) {
|
|
262
392
|
progressLineCount++;
|
|
263
393
|
continue;
|
|
@@ -269,13 +399,19 @@ function parseReport(output, format, parser) {
|
|
|
269
399
|
unusableLineCount++;
|
|
270
400
|
}
|
|
271
401
|
}
|
|
272
|
-
catch {
|
|
402
|
+
catch (err) {
|
|
403
|
+
if (typeof err === "object" && err !== null && "qcFailureReason" in err) {
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
273
406
|
// Ignore unparseable lines.
|
|
274
407
|
}
|
|
275
408
|
}
|
|
276
409
|
if (lineFindings.length > 0) {
|
|
277
410
|
return { findings: lineFindings };
|
|
278
411
|
}
|
|
412
|
+
if (sawExplicitSkip) {
|
|
413
|
+
return { findings: [] };
|
|
414
|
+
}
|
|
279
415
|
if (progressLineCount > 0 || unusableLineCount > 0) {
|
|
280
416
|
throw makeUnusableOutputError(`CodeRabbit output contained only progress/status/heartbeat and bookkeeping records (${progressLineCount + unusableLineCount} lines)`);
|
|
281
417
|
}
|
|
@@ -288,9 +424,9 @@ function normalizeFinding(raw, index) {
|
|
|
288
424
|
const severityLabel = pickString(raw.severity, raw.level) ?? "info";
|
|
289
425
|
const severity = (0, severity_js_1.normalizeSeverity)(severityLabel);
|
|
290
426
|
const title = pickString(raw.title, raw.summary, raw.rule, raw.type, raw.category) ?? `Finding #${index + 1}`;
|
|
291
|
-
const message = pickString(raw.message, raw.description, raw.body);
|
|
292
|
-
const filePath = pickString(raw.file, raw.filePath, raw.path);
|
|
293
|
-
const suggestedAction = pickString(raw.suggestion, raw.suggestedAction, raw.fix);
|
|
427
|
+
const message = pickString(raw.message, raw.description, raw.body, raw.codegenInstructions);
|
|
428
|
+
const filePath = pickString(raw.file, raw.filePath, raw.path, raw.fileName);
|
|
429
|
+
const suggestedAction = pickString(raw.suggestion, raw.suggestedAction, raw.fix, ...(Array.isArray(raw.suggestions) ? raw.suggestions : []));
|
|
294
430
|
const providerFindingId = pickString(raw.providerFindingId, raw.id, raw.findingId);
|
|
295
431
|
const confidence = coerceNumber(raw.confidence);
|
|
296
432
|
const attribution = {
|
|
@@ -298,7 +434,10 @@ function normalizeFinding(raw, index) {
|
|
|
298
434
|
reason: "provider-uncertain",
|
|
299
435
|
filePath,
|
|
300
436
|
};
|
|
301
|
-
const fixAvailable = raw.fixAvailable === true ||
|
|
437
|
+
const fixAvailable = raw.fixAvailable === true ||
|
|
438
|
+
raw.autofixEligible === true ||
|
|
439
|
+
Boolean(raw.fix) ||
|
|
440
|
+
Boolean(raw.codegenInstructions);
|
|
302
441
|
return {
|
|
303
442
|
findingId: `coderabbit-${index + 1}-${Date.now()}`,
|
|
304
443
|
...(providerFindingId ? { providerFindingId } : {}),
|
package/dist/qc/repair-loop.js
CHANGED
|
@@ -19,10 +19,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
exports.DEFAULT_REPAIR_DISPATCH_TIMEOUT_MS = exports.DEFAULT_MAX_REPAIR_ROUNDS = void 0;
|
|
20
20
|
exports.partitionRepairPackets = partitionRepairPackets;
|
|
21
21
|
exports.initRepairLoopState = initRepairLoopState;
|
|
22
|
+
exports.getQcResolutionArtifactPath = getQcResolutionArtifactPath;
|
|
23
|
+
exports.resolveQcResolutionFindings = resolveQcResolutionFindings;
|
|
24
|
+
exports.getResolverIdentity = getResolverIdentity;
|
|
25
|
+
exports.isValidQcResolutionArtifact = isValidQcResolutionArtifact;
|
|
26
|
+
exports.readQcResolutionArtifact = readQcResolutionArtifact;
|
|
27
|
+
exports.writeQcResolutionArtifact = writeQcResolutionArtifact;
|
|
22
28
|
exports.runQcRepairLoop = runQcRepairLoop;
|
|
23
29
|
const node_fs_1 = require("node:fs");
|
|
30
|
+
const node_child_process_1 = require("node:child_process");
|
|
24
31
|
const node_path_1 = require("node:path");
|
|
25
32
|
const repair_packets_js_1 = require("./repair-packets.js");
|
|
33
|
+
const types_js_1 = require("./types.js");
|
|
26
34
|
const orchestration_js_1 = require("./orchestration.js");
|
|
27
35
|
const store_js_1 = require("../cluster-state/store.js");
|
|
28
36
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
@@ -189,6 +197,127 @@ function initRepairLoopState(opts) {
|
|
|
189
197
|
updated_at: now,
|
|
190
198
|
};
|
|
191
199
|
}
|
|
200
|
+
// ── Operator resolution artifact helpers ──────────────────────────────────────
|
|
201
|
+
/** Path for the operator-written resolution artifact for a repair round. */
|
|
202
|
+
function getQcResolutionArtifactPath(clusterId, round, repoRoot) {
|
|
203
|
+
return (0, node_path_1.join)((0, repair_packets_js_1.getRepairRoundDir)(clusterId, round, repoRoot), "resolution.json");
|
|
204
|
+
}
|
|
205
|
+
/** Resolve the finding IDs covered by a resolution artifact. */
|
|
206
|
+
function resolveQcResolutionFindings(manifest, explicitFindings) {
|
|
207
|
+
const manifestFindings = [
|
|
208
|
+
...new Set(manifest.packets.flatMap((p) => p.findingIds)),
|
|
209
|
+
].sort();
|
|
210
|
+
if (!explicitFindings || explicitFindings.length === 0) {
|
|
211
|
+
return manifestFindings;
|
|
212
|
+
}
|
|
213
|
+
const allowed = new Set(manifestFindings);
|
|
214
|
+
const unknown = explicitFindings.filter((id) => !allowed.has(id));
|
|
215
|
+
if (unknown.length > 0) {
|
|
216
|
+
throw new Error(`Unknown finding IDs: ${unknown.join(", ")}. Expected one of: ${manifestFindings.join(", ") || "none"}`);
|
|
217
|
+
}
|
|
218
|
+
return [...new Set(explicitFindings)].sort();
|
|
219
|
+
}
|
|
220
|
+
/** Determine the resolver identity from git config or the default operator. */
|
|
221
|
+
function getResolverIdentity(repoRoot) {
|
|
222
|
+
try {
|
|
223
|
+
const name = (0, node_child_process_1.execFileSync)("git", ["config", "user.name"], {
|
|
224
|
+
cwd: repoRoot || process.cwd(),
|
|
225
|
+
encoding: "utf-8",
|
|
226
|
+
}).trim();
|
|
227
|
+
return name || "lsctech";
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return "lsctech";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const QC_RESOLUTION_OUTCOME_SET = new Set(types_js_1.QC_RESOLUTION_OUTCOMES);
|
|
234
|
+
/** Validate a resolution artifact object. */
|
|
235
|
+
function isValidQcResolutionArtifact(value) {
|
|
236
|
+
if (!value || typeof value !== "object")
|
|
237
|
+
return false;
|
|
238
|
+
const v = value;
|
|
239
|
+
if (typeof v.schemaVersion !== "string")
|
|
240
|
+
return false;
|
|
241
|
+
if (typeof v.clusterId !== "string")
|
|
242
|
+
return false;
|
|
243
|
+
if (typeof v.round !== "number")
|
|
244
|
+
return false;
|
|
245
|
+
if (typeof v.resolvedAt !== "string")
|
|
246
|
+
return false;
|
|
247
|
+
if (typeof v.resolver !== "string" || v.resolver === "")
|
|
248
|
+
return false;
|
|
249
|
+
if (typeof v.resolvedOutcome !== "string" ||
|
|
250
|
+
!QC_RESOLUTION_OUTCOME_SET.has(v.resolvedOutcome)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (typeof v.reason !== "string" || v.reason.trim() === "")
|
|
254
|
+
return false;
|
|
255
|
+
if (!Array.isArray(v.findings) ||
|
|
256
|
+
v.findings.length === 0 ||
|
|
257
|
+
!v.findings.every((f) => typeof f === "string")) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
/** Read and validate a resolution artifact, if it exists. */
|
|
263
|
+
function readQcResolutionArtifact(clusterId, round, repoRoot) {
|
|
264
|
+
const artifactPath = getQcResolutionArtifactPath(clusterId, round, repoRoot);
|
|
265
|
+
try {
|
|
266
|
+
const data = (0, node_fs_1.readFileSync)(artifactPath, "utf-8");
|
|
267
|
+
const parsed = JSON.parse(data);
|
|
268
|
+
if (isValidQcResolutionArtifact(parsed)) {
|
|
269
|
+
// Verify identity matches expected clusterId and round
|
|
270
|
+
if (parsed.clusterId !== clusterId || parsed.round !== round) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return parsed;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error.code === "ENOENT") {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
if (error instanceof SyntaxError) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Write a resolution artifact atomically and return its absolute path. */
|
|
288
|
+
function writeQcResolutionArtifact(options) {
|
|
289
|
+
const { clusterId, round, resolver, resolvedOutcome, reason, findings, repoRoot, } = options;
|
|
290
|
+
const artifactPath = getQcResolutionArtifactPath(clusterId, round, repoRoot);
|
|
291
|
+
const dir = (0, node_path_1.dirname)(artifactPath);
|
|
292
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
293
|
+
const artifact = {
|
|
294
|
+
schemaVersion: "1.0",
|
|
295
|
+
clusterId,
|
|
296
|
+
round,
|
|
297
|
+
resolvedAt: new Date().toISOString(),
|
|
298
|
+
resolver,
|
|
299
|
+
resolvedOutcome,
|
|
300
|
+
reason,
|
|
301
|
+
findings,
|
|
302
|
+
};
|
|
303
|
+
const tempPath = `${artifactPath}.tmp.${process.pid}.${Date.now()}.${Math.random()
|
|
304
|
+
.toString(36)
|
|
305
|
+
.slice(2, 8)}`;
|
|
306
|
+
try {
|
|
307
|
+
(0, node_fs_1.writeFileSync)(tempPath, JSON.stringify(artifact, null, 2), "utf-8");
|
|
308
|
+
(0, node_fs_1.renameSync)(tempPath, artifactPath);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
try {
|
|
312
|
+
(0, node_fs_1.unlinkSync)(tempPath);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Ignore cleanup failure.
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
return artifactPath;
|
|
320
|
+
}
|
|
192
321
|
// ── Main repair loop ───────────────────────────────────────────────────────────
|
|
193
322
|
/**
|
|
194
323
|
* Run the bounded QC repair loop.
|
|
@@ -293,10 +422,21 @@ async function runQcRepairLoop(options) {
|
|
|
293
422
|
else {
|
|
294
423
|
const existingManifestPath = loopState.manifest_path ??
|
|
295
424
|
(0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds", String(round), "repair-packets.json");
|
|
425
|
+
// Synchronize manifest packet statuses with the persisted loop state.
|
|
426
|
+
const completedSet = new Set(loopState.completed_packet_ids);
|
|
427
|
+
for (const packet of manifest.packets) {
|
|
428
|
+
if (completedSet.has(packet.packetId)) {
|
|
429
|
+
packet.status = "completed";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
(0, repair_packets_js_1.writeRepairPacketManifest)(manifest, repoRoot);
|
|
296
433
|
loopState = {
|
|
297
434
|
...loopState,
|
|
298
435
|
current_round: round,
|
|
299
436
|
manifest_path: existingManifestPath,
|
|
437
|
+
pending_packet_ids: manifest.packets
|
|
438
|
+
.filter((p) => p.status === "pending" && !completedSet.has(p.packetId))
|
|
439
|
+
.map((p) => p.packetId),
|
|
300
440
|
updated_at: new Date().toISOString(),
|
|
301
441
|
};
|
|
302
442
|
onStateUpdate?.(loopState);
|
|
@@ -367,10 +507,16 @@ async function runQcRepairLoop(options) {
|
|
|
367
507
|
const result = await dispatchRepairWorkerWithTimeout(dispatchRepairWorker, pkt, round, manifest, repairDispatchTimeoutMs, telemetryFile, runId, clusterId);
|
|
368
508
|
allWorkerResults.push(result);
|
|
369
509
|
}
|
|
370
|
-
// Record completed packet IDs.
|
|
510
|
+
// Record completed packet IDs and update manifest statuses.
|
|
371
511
|
const completedIds = allWorkerResults
|
|
372
512
|
.filter((r) => r.status !== "skipped")
|
|
373
513
|
.map((r) => r.packetId);
|
|
514
|
+
for (const packet of manifest.packets) {
|
|
515
|
+
if (completedIds.includes(packet.packetId)) {
|
|
516
|
+
packet.status = "completed";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
(0, repair_packets_js_1.writeRepairPacketManifest)(manifest, repoRoot);
|
|
374
520
|
loopState = {
|
|
375
521
|
...loopState,
|
|
376
522
|
completed_packet_ids: [...loopState.completed_packet_ids, ...completedIds],
|
|
@@ -78,6 +78,16 @@ function subsystemFromFilePath(filePath) {
|
|
|
78
78
|
const lastSlash = filePath.lastIndexOf("/");
|
|
79
79
|
return lastSlash === -1 ? "root" : filePath.slice(0, lastSlash);
|
|
80
80
|
}
|
|
81
|
+
function deriveSourceQcRunId(finding, fallbackRunId) {
|
|
82
|
+
// Some findingId formats encode the source QC run as `<provider>-<index>-<runId>`
|
|
83
|
+
// (e.g. coderabbit-1-1783818427443). Prefer that provenance over the result
|
|
84
|
+
// run ID so packet sourceQcRunIds match the runs that emitted the findings.
|
|
85
|
+
const match = finding.findingId.match(/^(.+?)-(\d+)-(\d+)$/);
|
|
86
|
+
if (match) {
|
|
87
|
+
return `${match[1]}-${match[3]}`;
|
|
88
|
+
}
|
|
89
|
+
return fallbackRunId;
|
|
90
|
+
}
|
|
81
91
|
function enrichFinding(finding, config, sourceQcRunId) {
|
|
82
92
|
return {
|
|
83
93
|
...finding,
|
|
@@ -85,7 +95,7 @@ function enrichFinding(finding, config, sourceQcRunId) {
|
|
|
85
95
|
risk: riskLevel(finding),
|
|
86
96
|
confidenceBand: confidenceBand(finding),
|
|
87
97
|
subsystem: subsystemFromFilePath(finding.filePath),
|
|
88
|
-
sourceQcRunId,
|
|
98
|
+
sourceQcRunId: deriveSourceQcRunId(finding, sourceQcRunId),
|
|
89
99
|
};
|
|
90
100
|
}
|
|
91
101
|
/**
|
|
@@ -213,6 +223,8 @@ function buildPacket(group, index, clusterId, round, allRunIds, validationComman
|
|
|
213
223
|
const packetId = `pkt-${clusterId}-r${round}-${String(index).padStart(3, "0")}`;
|
|
214
224
|
const findingIds = group.map((f) => f.findingId).sort();
|
|
215
225
|
const sourceRunIds = [...new Set(group.map((f) => f.sourceQcRunId))].sort();
|
|
226
|
+
// Preserve the artifact run ID while also capturing the finding-level source run.
|
|
227
|
+
const combinedSourceRunIds = [...new Set([...allRunIds, ...sourceRunIds])].sort();
|
|
216
228
|
const severityFloor = group.reduce((acc, f) => (0, severity_js_1.maxSeverity)(acc, f.severity), "info");
|
|
217
229
|
const categories = [...new Set(group.map((f) => f.category).filter(Boolean))].sort();
|
|
218
230
|
const filePaths = group.map((f) => f.filePath).filter((f) => Boolean(f));
|
|
@@ -230,7 +242,7 @@ function buildPacket(group, index, clusterId, round, allRunIds, validationComman
|
|
|
230
242
|
packetId,
|
|
231
243
|
round,
|
|
232
244
|
clusterId,
|
|
233
|
-
sourceQcRunIds:
|
|
245
|
+
sourceQcRunIds: combinedSourceRunIds,
|
|
234
246
|
findingIds,
|
|
235
247
|
severityFloor,
|
|
236
248
|
rootCauseHint,
|
|
@@ -346,12 +358,13 @@ function compileRepairPackets(input) {
|
|
|
346
358
|
packets.push(buildPacket(group, packets.length, clusterId, round, allRunIds, validationCommands, compiledAt));
|
|
347
359
|
}
|
|
348
360
|
const finalPackets = assignParallelGroups(packets);
|
|
361
|
+
const manifestSourceRunIds = [...new Set(finalPackets.flatMap((p) => p.sourceQcRunIds))].sort();
|
|
349
362
|
const manifest = {
|
|
350
363
|
schemaVersion: "1.0",
|
|
351
364
|
clusterId,
|
|
352
365
|
round,
|
|
353
366
|
compiledAt,
|
|
354
|
-
sourceQcRunIds: allRunIds,
|
|
367
|
+
sourceQcRunIds: manifestSourceRunIds.length > 0 ? manifestSourceRunIds : allRunIds,
|
|
355
368
|
packets: finalPackets,
|
|
356
369
|
};
|
|
357
370
|
const manifestPath = getRepairPacketManifestPath(clusterId, round, repoRoot);
|
package/dist/qc/routing.js
CHANGED
|
@@ -12,6 +12,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.decideRepairRouting = decideRepairRouting;
|
|
13
13
|
const security_category_js_1 = require("./security-category.js");
|
|
14
14
|
const severity_js_1 = require("./severity.js");
|
|
15
|
+
function isStateRepairCategory(category) {
|
|
16
|
+
return category === "state-repair";
|
|
17
|
+
}
|
|
15
18
|
function getBlockThreshold(config, context) {
|
|
16
19
|
const route = context.routeName ? config.routes?.[context.routeName] : undefined;
|
|
17
20
|
return route?.blockThreshold ?? config.severityThresholds?.block ?? "high";
|
|
@@ -34,6 +37,9 @@ function decideRepairRouting(finding, config, autofixEligible, context = {}) {
|
|
|
34
37
|
if ((0, security_category_js_1.isSecurityCategory)(finding.category)) {
|
|
35
38
|
return "operator-review";
|
|
36
39
|
}
|
|
40
|
+
if (isStateRepairCategory(finding.category)) {
|
|
41
|
+
return "operator-review";
|
|
42
|
+
}
|
|
37
43
|
const attribution = finding.attribution;
|
|
38
44
|
const clearAttribution = attribution.confidence === "high" || attribution.confidence === "medium";
|
|
39
45
|
if (autofixEligible && clearAttribution && attribution.childId) {
|
package/dist/qc/types.js
CHANGED
|
@@ -7,3 +7,6 @@
|
|
|
7
7
|
* shapes so that Polaris can support multiple QC backends without vendor lock-in.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.QC_RESOLUTION_OUTCOMES = void 0;
|
|
11
|
+
/** Allowed outcomes for an operator resolution artifact. */
|
|
12
|
+
exports.QC_RESOLUTION_OUTCOMES = ["pass", "no-repairable"];
|