@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.
@@ -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 (Array.isArray(record.findings)) {
147
- return record.findings.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
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
- if (Array.isArray(record.results)) {
153
- return record.results.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
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 = parsed.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
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 || raw.autofixEligible === true || Boolean(raw.fix);
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 } : {}),
@@ -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: sourceRunIds.length > 0 ? sourceRunIds : allRunIds,
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);
@@ -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"];