@lsctech/polaris 0.5.11 → 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 +138 -10
- 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
|
@@ -37,6 +37,8 @@ const FINDING_BOOKKEEPING_KEYS = [
|
|
|
37
37
|
"findingId",
|
|
38
38
|
];
|
|
39
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"]);
|
|
40
42
|
const PROGRESS_TYPE_VALUES = new Set([
|
|
41
43
|
"progress",
|
|
42
44
|
"status",
|
|
@@ -56,6 +58,14 @@ const PROGRESS_STATUS_VALUES = new Set([
|
|
|
56
58
|
"ok",
|
|
57
59
|
"success",
|
|
58
60
|
]);
|
|
61
|
+
const TERMINAL_COMPLETE_STATUSES = new Set([
|
|
62
|
+
"review_completed",
|
|
63
|
+
"review_skipped",
|
|
64
|
+
"completed",
|
|
65
|
+
"complete",
|
|
66
|
+
"done",
|
|
67
|
+
"success",
|
|
68
|
+
]);
|
|
59
69
|
function hasFindingLocation(record) {
|
|
60
70
|
return FINDING_LOCATION_KEYS.some((key) => record[key] !== undefined);
|
|
61
71
|
}
|
|
@@ -112,6 +122,8 @@ function isProgressRecord(record) {
|
|
|
112
122
|
return false;
|
|
113
123
|
}
|
|
114
124
|
function isActionableFinding(record) {
|
|
125
|
+
if (isErrorRecord(record))
|
|
126
|
+
return false;
|
|
115
127
|
if (isProgressRecord(record))
|
|
116
128
|
return false;
|
|
117
129
|
return hasFindingLocation(record) || hasFindingContent(record);
|
|
@@ -124,6 +136,85 @@ function makeUnusableOutputError(message) {
|
|
|
124
136
|
err.qcFailureReason = "unusable-output";
|
|
125
137
|
return err;
|
|
126
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
|
+
}
|
|
127
218
|
function buildRange(raw) {
|
|
128
219
|
const startLine = coerceNumber(raw.startLine ?? raw.line) ?? 1;
|
|
129
220
|
const endLine = coerceNumber(raw.endLine);
|
|
@@ -144,14 +235,27 @@ function parseFindingsFromPayload(payload) {
|
|
|
144
235
|
return [];
|
|
145
236
|
}
|
|
146
237
|
const record = payload;
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
if (Array.isArray(record.issues)) {
|
|
151
|
-
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"}`);
|
|
152
240
|
}
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}
|
|
155
259
|
}
|
|
156
260
|
// Single finding wrapped in an object
|
|
157
261
|
if (isActionableFinding(record)) {
|
|
@@ -172,6 +276,9 @@ function parseReport(output, format, parser) {
|
|
|
172
276
|
const findings = parseFindingsFromPayload(data);
|
|
173
277
|
const record = data;
|
|
174
278
|
if (findings.length === 0) {
|
|
279
|
+
if (isTerminalCompleteNoFindings(record)) {
|
|
280
|
+
return { findings: [] };
|
|
281
|
+
}
|
|
175
282
|
if (isProgressRecord(record)) {
|
|
176
283
|
throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
|
|
177
284
|
}
|
|
@@ -198,7 +305,19 @@ function parseReport(output, format, parser) {
|
|
|
198
305
|
try {
|
|
199
306
|
const parsed = JSON.parse(text);
|
|
200
307
|
if (Array.isArray(parsed)) {
|
|
201
|
-
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
|
+
}
|
|
202
321
|
if (findings.length === 0 && parsed.length > 0) {
|
|
203
322
|
const unusableCount = parsed.filter((item) => typeof item === "object" && item !== null && isUnusableFindingRecord(item)).length;
|
|
204
323
|
if (unusableCount > 0) {
|
|
@@ -210,6 +329,9 @@ function parseReport(output, format, parser) {
|
|
|
210
329
|
const findings = parseFindingsFromPayload(parsed);
|
|
211
330
|
if (findings.length === 0) {
|
|
212
331
|
const parsedRecord = parsed;
|
|
332
|
+
if (isTerminalCompleteNoFindings(parsedRecord)) {
|
|
333
|
+
return { findings: [] };
|
|
334
|
+
}
|
|
213
335
|
if (isProgressRecord(parsedRecord)) {
|
|
214
336
|
throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
|
|
215
337
|
}
|
|
@@ -260,9 +382,12 @@ function parseReport(output, format, parser) {
|
|
|
260
382
|
continue;
|
|
261
383
|
}
|
|
262
384
|
const record = parsed;
|
|
263
|
-
if (record
|
|
385
|
+
if (isTerminalCompleteNoFindings(record)) {
|
|
264
386
|
sawExplicitSkip = true;
|
|
265
387
|
}
|
|
388
|
+
if (isErrorRecord(record)) {
|
|
389
|
+
throw makeQcFailureError(classifyErrorType(record), `CodeRabbit provider returned an error in JSONL: ${record.message ?? record.errorType ?? "unknown"}`);
|
|
390
|
+
}
|
|
266
391
|
if (isProgressRecord(record)) {
|
|
267
392
|
progressLineCount++;
|
|
268
393
|
continue;
|
|
@@ -274,7 +399,10 @@ function parseReport(output, format, parser) {
|
|
|
274
399
|
unusableLineCount++;
|
|
275
400
|
}
|
|
276
401
|
}
|
|
277
|
-
catch {
|
|
402
|
+
catch (err) {
|
|
403
|
+
if (typeof err === "object" && err !== null && "qcFailureReason" in err) {
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
278
406
|
// Ignore unparseable lines.
|
|
279
407
|
}
|
|
280
408
|
}
|
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"];
|
|
@@ -15,6 +15,7 @@ const migration_js_1 = require("./migration.js");
|
|
|
15
15
|
*/
|
|
16
16
|
class LocalGraph {
|
|
17
17
|
graph;
|
|
18
|
+
orderingDependenciesMerged = false;
|
|
18
19
|
constructor(graph) {
|
|
19
20
|
this.graph = graph;
|
|
20
21
|
}
|
|
@@ -54,19 +55,76 @@ class LocalGraph {
|
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* Persists the graph to `.polaris/clusters/{clusterId}/clusters.json`.
|
|
57
|
-
*
|
|
58
|
+
* Children are sorted topologically before writing so the saved cluster
|
|
59
|
+
* definition reflects dependency order.
|
|
58
60
|
*
|
|
59
61
|
* @param clusterId The cluster ID to use as the directory name (e.g., "POL-198").
|
|
60
62
|
* @param repoRoot The root directory of the repository.
|
|
61
63
|
* @returns The absolute path of the written file.
|
|
62
64
|
*/
|
|
63
65
|
async save(clusterId, repoRoot = process.cwd()) {
|
|
66
|
+
this.mergeOrderingDependencies();
|
|
67
|
+
const persistedGraph = {
|
|
68
|
+
...this.graph,
|
|
69
|
+
clusters: Object.fromEntries(Object.entries(this.graph.clusters).map(([id, cluster]) => [
|
|
70
|
+
id,
|
|
71
|
+
Array.isArray(cluster.children) && cluster.children.length > 1
|
|
72
|
+
? { ...cluster, children: this.topoSortChildren(cluster.children) }
|
|
73
|
+
: cluster,
|
|
74
|
+
])),
|
|
75
|
+
};
|
|
64
76
|
const dir = node_path_1.default.join(repoRoot, ".polaris", "clusters", clusterId);
|
|
65
77
|
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
66
78
|
const filePath = node_path_1.default.join(dir, "clusters.json");
|
|
67
|
-
await (0, promises_1.writeFile)(filePath, JSON.stringify(
|
|
79
|
+
await (0, promises_1.writeFile)(filePath, JSON.stringify(persistedGraph, null, 2), "utf-8");
|
|
68
80
|
return filePath;
|
|
69
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Sort a list of children topologically using the graph dependencies.
|
|
84
|
+
* Children with no in-cluster dependencies come first; if a cycle exists,
|
|
85
|
+
* the remaining nodes are appended in their original order.
|
|
86
|
+
*/
|
|
87
|
+
topoSortChildren(children) {
|
|
88
|
+
const childSet = new Set(children);
|
|
89
|
+
const inDegree = new Map();
|
|
90
|
+
const dependents = new Map();
|
|
91
|
+
for (const child of children) {
|
|
92
|
+
inDegree.set(child, 0);
|
|
93
|
+
dependents.set(child, []);
|
|
94
|
+
}
|
|
95
|
+
for (const child of children) {
|
|
96
|
+
for (const dep of this.getDependencies(child)) {
|
|
97
|
+
if (!childSet.has(dep))
|
|
98
|
+
continue;
|
|
99
|
+
inDegree.set(child, (inDegree.get(child) ?? 0) + 1);
|
|
100
|
+
dependents.get(dep).push(child);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const queue = [];
|
|
104
|
+
for (const [child, deg] of inDegree) {
|
|
105
|
+
if (deg === 0)
|
|
106
|
+
queue.push(child);
|
|
107
|
+
}
|
|
108
|
+
const sorted = [];
|
|
109
|
+
while (queue.length > 0) {
|
|
110
|
+
const node = queue.shift();
|
|
111
|
+
sorted.push(node);
|
|
112
|
+
for (const dependent of dependents.get(node) ?? []) {
|
|
113
|
+
const newDeg = (inDegree.get(dependent) ?? 1) - 1;
|
|
114
|
+
inDegree.set(dependent, newDeg);
|
|
115
|
+
if (newDeg === 0)
|
|
116
|
+
queue.push(dependent);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (sorted.length < children.length) {
|
|
120
|
+
const sortedSet = new Set(sorted);
|
|
121
|
+
for (const child of children) {
|
|
122
|
+
if (!sortedSet.has(child))
|
|
123
|
+
sorted.push(child);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return sorted;
|
|
127
|
+
}
|
|
70
128
|
/**
|
|
71
129
|
* Returns the full v2 execution graph.
|
|
72
130
|
*/
|
|
@@ -87,12 +145,57 @@ class LocalGraph {
|
|
|
87
145
|
return this.graph.nodes[id];
|
|
88
146
|
}
|
|
89
147
|
/**
|
|
90
|
-
* Returns the dependencies for a given node
|
|
148
|
+
* Returns the dependencies for a given node, merging any dependencies declared
|
|
149
|
+
* in the node's `## Ordering` body section.
|
|
91
150
|
* @param id The ID of the node to get dependencies for.
|
|
92
151
|
* @returns An array of node IDs that the given node is blocked by.
|
|
93
152
|
*/
|
|
94
153
|
getDependencies(id) {
|
|
154
|
+
this.mergeOrderingDependencies();
|
|
95
155
|
return this.graph.dependencies[id] ?? [];
|
|
96
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Extracts issue IDs from the `## Ordering` section of a node body.
|
|
159
|
+
*
|
|
160
|
+
* Only lines that explicitly declare ordering ("depends on" or "after" / "sequence after")
|
|
161
|
+
* contribute, and "before or after" clauses are ignored because they are not strict.
|
|
162
|
+
*/
|
|
163
|
+
extractOrderingIds(body) {
|
|
164
|
+
const sectionMatch = body.match(/##\s*Ordering\b([\s\S]*?)(?:\n##\s|\n\n(?=\n##\s)|$)/i);
|
|
165
|
+
if (!sectionMatch)
|
|
166
|
+
return [];
|
|
167
|
+
const section = sectionMatch[1];
|
|
168
|
+
const cleaned = section.replace(/before\s+or\s+after\s*\[[^\]]*\]/gi, "");
|
|
169
|
+
const ids = new Set();
|
|
170
|
+
const re = /(?:depends\s+on|after)\s*\[?\s*(\w+-\d+)\b/gi;
|
|
171
|
+
for (const line of cleaned.split("\n")) {
|
|
172
|
+
if (!/^\s*[-*]\s/.test(line))
|
|
173
|
+
continue;
|
|
174
|
+
for (const match of line.matchAll(re)) {
|
|
175
|
+
ids.add(match[1]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return [...ids];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Merges dependencies declared in node body `## Ordering` sections into the
|
|
182
|
+
* graph's explicit dependency map. This is idempotent for the loaded graph.
|
|
183
|
+
*/
|
|
184
|
+
mergeOrderingDependencies() {
|
|
185
|
+
if (this.orderingDependenciesMerged)
|
|
186
|
+
return;
|
|
187
|
+
for (const node of Object.values(this.graph.nodes)) {
|
|
188
|
+
const orderingIds = this.extractOrderingIds(node.body ?? "");
|
|
189
|
+
if (orderingIds.length === 0)
|
|
190
|
+
continue;
|
|
191
|
+
const existing = new Set(this.graph.dependencies[node.id] ?? []);
|
|
192
|
+
for (const dep of orderingIds) {
|
|
193
|
+
if (dep !== node.id)
|
|
194
|
+
existing.add(dep);
|
|
195
|
+
}
|
|
196
|
+
this.graph.dependencies[node.id] = [...existing].sort();
|
|
197
|
+
}
|
|
198
|
+
this.orderingDependenciesMerged = true;
|
|
199
|
+
}
|
|
97
200
|
}
|
|
98
201
|
exports.LocalGraph = LocalGraph;
|