@lsctech/polaris 0.5.0 → 0.5.3

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.
@@ -23,6 +23,7 @@ exports.gateValidationFailed = gateValidationFailed;
23
23
  exports.gateWorkerWentOutOfScope = gateWorkerWentOutOfScope;
24
24
  exports.gateForemanTokenBurnOverBudget = gateForemanTokenBurnOverBudget;
25
25
  exports.gateStateRepairRequired = gateStateRepairRequired;
26
+ exports.gateQcBlockingFindings = gateQcBlockingFindings;
26
27
  exports.readJsonLines = readJsonLines;
27
28
  const node_fs_1 = require("node:fs");
28
29
  const node_path_1 = require("node:path");
@@ -362,6 +363,51 @@ function safeReaddir(dir) {
362
363
  return [];
363
364
  }
364
365
  }
366
+ /**
367
+ * qc-blocking-findings: Are there unresolved critical/high QC findings
368
+ * attributed to this cluster with high or medium confidence?
369
+ *
370
+ * Evidence: qcResults loaded from .polaris/clusters/<cluster-id>/qc/.
371
+ * Skipped when no QC artifacts exist.
372
+ *
373
+ * Only high/medium attribution-confidence findings are counted — provider noise
374
+ * (low/unattributed confidence) is excluded so workers are not penalized for
375
+ * external reviewer uncertainty.
376
+ */
377
+ function gateQcBlockingFindings(artifacts) {
378
+ if (artifacts.qcResults.length === 0) {
379
+ return { gate: "qc-blocking-findings", outcome: "skipped", detail: "no QC artifacts found" };
380
+ }
381
+ let blockingCount = 0;
382
+ let weightedOpenScore = 0;
383
+ const severityWeights = { critical: 10, high: 5, medium: 2, low: 1, info: 0.5 };
384
+ const confidenceWeights = { high: 1.0, medium: 0.7, low: 0.3, unattributed: 0 };
385
+ for (const result of artifacts.qcResults) {
386
+ for (const finding of result.findings) {
387
+ const conf = finding.attribution.confidence;
388
+ const isOpenUnresolved = finding.status === "open" || finding.status === "follow-up";
389
+ if (isOpenUnresolved && conf !== "low" && conf !== "unattributed") {
390
+ weightedOpenScore += severityWeights[finding.severity] * confidenceWeights[conf];
391
+ }
392
+ if (conf === "low" || conf === "unattributed")
393
+ continue;
394
+ if (finding.status === "autofixed" || finding.status === "repaired" || finding.status === "waived")
395
+ continue;
396
+ if ((finding.severity === "critical" || finding.severity === "high") &&
397
+ (finding.status === "open" || finding.status === "follow-up")) {
398
+ blockingCount++;
399
+ }
400
+ }
401
+ }
402
+ if (blockingCount > 0) {
403
+ return {
404
+ gate: "qc-blocking-findings",
405
+ outcome: "failed",
406
+ detail: `${blockingCount} unresolved critical/high QC finding${blockingCount === 1 ? "" : "s"} with attributed confidence (weighted open score ${weightedOpenScore.toFixed(2)})`,
407
+ };
408
+ }
409
+ return { gate: "qc-blocking-findings", outcome: "passed" };
410
+ }
365
411
  exports.ALL_GATES = [
366
412
  gateUserIntervened,
367
413
  gateForemanResentPacket,
@@ -371,4 +417,5 @@ exports.ALL_GATES = [
371
417
  gateWorkerWentOutOfScope,
372
418
  gateForemanTokenBurnOverBudget,
373
419
  gateStateRepairRequired,
420
+ gateQcBlockingFindings,
374
421
  ];
@@ -10,6 +10,7 @@
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FIX_ZONE_MAP = void 0;
13
+ exports.buildQcProposals = buildQcProposals;
13
14
  exports.buildProposals = buildProposals;
14
15
  exports.loadDiagnosisReport = loadDiagnosisReport;
15
16
  exports.validateDiagnosisReport = validateDiagnosisReport;
@@ -50,6 +51,10 @@ exports.FIX_ZONE_MAP = {
50
51
  artifact_type: "medic-template",
51
52
  hint: "Medic artifacts detected — state repair was required. Review medic template heuristics and state-repair triggers.",
52
53
  },
54
+ "qc-blocking-findings": {
55
+ artifact_type: "scoring-rule",
56
+ hint: "Unresolved critical/high QC findings with attributed confidence. Review QC artifacts, triage open findings, and route repairs before delivery.",
57
+ },
53
58
  };
54
59
  const ROUTER_FAILURE_FIX_ZONE_MAP = {
55
60
  "quota-exhausted": {
@@ -88,6 +93,80 @@ const ROUTER_FAILURE_FIX_ZONE_MAP = {
88
93
  * Maps a DiagnosisReport's failed gates to AutresearchProposal objects.
89
94
  * Gates without a fix zone entry are skipped.
90
95
  */
96
+ const QC_FIX_ZONE_MAP = {
97
+ "qc-recurring-provider": {
98
+ artifact_type: "provider-role-recommendation",
99
+ hint: "A QC provider is producing recurring attributed findings. Review provider capability mapping, role assignment, and confidence thresholds.",
100
+ },
101
+ "qc-recurring-child": {
102
+ artifact_type: "worker-template",
103
+ hint: "A child/worker route accumulates recurring QC findings. Tighten packet scope, validation commands, or acceptance criteria for this route.",
104
+ },
105
+ "qc-unvalidated-noise": {
106
+ artifact_type: "runtime-config",
107
+ hint: "A large share of QC findings are low/unattributed provider noise. Raise providerConfidenceThreshold or tighten QC provider selection.",
108
+ },
109
+ "qc-recurring-validation": {
110
+ artifact_type: "scoring-rule",
111
+ hint: "Unresolved QC findings indicate validation gaps. Strengthen scoring rules and repair routing before delivery.",
112
+ },
113
+ "qc-recurring-docs": {
114
+ artifact_type: "skill-prompt",
115
+ hint: "Recurring QC findings relate to documentation. Update skill prompts and worker instructions that produce docs artifacts.",
116
+ },
117
+ };
118
+ /**
119
+ * Builds QC-derived improvement proposals from the diagnosis report.
120
+ *
121
+ * These recommendations target config, provider, packet-scope, validation,
122
+ * and docs artifacts for recurring QC failure patterns.
123
+ */
124
+ function buildQcProposals(report) {
125
+ const qc = report.qc_summary;
126
+ if (!qc)
127
+ return [];
128
+ const proposals = [];
129
+ const confidence = Math.max(0, Math.min(1, 1 - qc.qc_penalty));
130
+ const addProposal = (gateId, artifactType, hint) => {
131
+ proposals.push({
132
+ gate_id: gateId,
133
+ artifact_type: artifactType,
134
+ hint,
135
+ run_id: report.run_id,
136
+ evidence_run_ids: [report.run_id],
137
+ confidence,
138
+ fix_zone: `${artifactType}/${gateId}`,
139
+ });
140
+ };
141
+ // Recurring provider signal: multiple blocking findings from the same provider.
142
+ for (const [provider, summary] of Object.entries(qc.provider_breakdown)) {
143
+ if (summary.total >= 2 && summary.blocking > 0) {
144
+ addProposal(`qc-recurring-provider:${provider}`, "provider-role-recommendation", `Provider '${provider}' produced ${summary.blocking} blocking QC findings across ${summary.total} total findings. Review provider capability mapping, role assignment, and confidence thresholds.`);
145
+ }
146
+ }
147
+ // Recurring child/worker route signal: same child owns multiple findings.
148
+ for (const signal of qc.recurring_child_signals) {
149
+ if (signal.finding_count >= 2) {
150
+ addProposal(`qc-recurring-child:${signal.child_id}`, "worker-template", `Child '${signal.child_id}' accumulated ${signal.finding_count} attributed QC findings (weighted score ${signal.weighted_score.toFixed(2)}). Tighten packet scope, validation commands, or acceptance criteria for this worker route.`);
151
+ }
152
+ }
153
+ // High share of unvalidated/provider-noise findings.
154
+ const noiseRatio = qc.total_findings > 0 ? qc.unvalidated_findings / qc.total_findings : 0;
155
+ if (qc.unvalidated_findings >= 3 && noiseRatio > 0.3) {
156
+ addProposal("qc-unvalidated-noise", "runtime-config", `${qc.unvalidated_findings} of ${qc.total_findings} QC findings are low/unattributed provider noise. Raise providerConfidenceThreshold or tighten QC provider selection in runtime config.`);
157
+ }
158
+ // Unresolved blocking findings indicate a validation/routing gap.
159
+ if (qc.blocking_findings > 0 && qc.weighted_open_score > 0) {
160
+ addProposal("qc-recurring-validation", "scoring-rule", `${qc.blocking_findings} blocking QC findings remain unresolved (weighted score ${qc.weighted_open_score.toFixed(2)}). Strengthen validation gates and repair routing before delivery.`);
161
+ }
162
+ // Recurring docs-category findings.
163
+ for (const [category, summary] of Object.entries(qc.category_breakdown)) {
164
+ if (category === "docs" && summary.total >= 2) {
165
+ addProposal("qc-recurring-docs", "skill-prompt", `${summary.total} QC findings relate to documentation. Update skill prompts and worker instructions that produce docs artifacts.`);
166
+ }
167
+ }
168
+ return proposals;
169
+ }
91
170
  function buildProposals(report) {
92
171
  const proposals = [];
93
172
  for (const gateId of report.failed_gates) {
@@ -104,6 +183,7 @@ function buildProposals(report) {
104
183
  fix_zone: `${entry.artifact_type}/${gateId}`,
105
184
  });
106
185
  }
186
+ proposals.push(...buildQcProposals(report));
107
187
  const recurringRouterFailures = report.router_outcomes?.recurring_failures ?? [];
108
188
  for (const failure of recurringRouterFailures) {
109
189
  if (failure.occurrences < 2)
@@ -144,6 +224,37 @@ function loadDiagnosisReport(filePath) {
144
224
  }
145
225
  return validateDiagnosisReport(raw);
146
226
  }
227
+ function defaultRoutingBreakdown() {
228
+ return { original_worker: 0, repair_worker: 0, follow_up: 0, operator_review: 0, unset: 0 };
229
+ }
230
+ function normalizeQcScoreSummary(raw) {
231
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
232
+ return null;
233
+ const q = raw;
234
+ const zeroSeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
235
+ const rawOpen = q.open_by_severity && typeof q.open_by_severity === "object" && !Array.isArray(q.open_by_severity)
236
+ ? q.open_by_severity
237
+ : undefined;
238
+ const openBySeverity = rawOpen ? { ...zeroSeverity, ...rawOpen } : zeroSeverity;
239
+ return {
240
+ total_findings: q.total_findings ?? 0,
241
+ blocking_findings: q.blocking_findings ?? 0,
242
+ autofixed_findings: q.autofixed_findings ?? 0,
243
+ repaired_findings: q.repaired_findings ?? 0,
244
+ waived_findings: q.waived_findings ?? 0,
245
+ unvalidated_findings: q.unvalidated_findings ?? 0,
246
+ open_by_severity: openBySeverity,
247
+ weighted_open_score: q.weighted_open_score ?? 0,
248
+ qc_penalty: q.qc_penalty ?? 0,
249
+ blocks_delivery: q.blocks_delivery ?? false,
250
+ qc_run_count: q.qc_run_count ?? 0,
251
+ provider_breakdown: q.provider_breakdown ?? {},
252
+ routing_breakdown: q.routing_breakdown ?? defaultRoutingBreakdown(),
253
+ category_breakdown: q.category_breakdown ?? {},
254
+ recurring_child_signals: Array.isArray(q.recurring_child_signals) ? q.recurring_child_signals : [],
255
+ recurring_provider_signals: Array.isArray(q.recurring_provider_signals) ? q.recurring_provider_signals : [],
256
+ };
257
+ }
147
258
  /**
148
259
  * Validates a parsed value against the DiagnosisReport schema.
149
260
  * Throws a descriptive error if required fields are missing or have wrong types.
@@ -183,8 +294,10 @@ function validateDiagnosisReport(raw) {
183
294
  successful_fallbacks: 0,
184
295
  recurring_failures: [],
185
296
  };
297
+ const normalizedQcSummary = normalizeQcScoreSummary(r["qc_summary"]);
186
298
  return {
187
299
  ...raw,
188
300
  router_outcomes: normalizedRouterOutcomes,
301
+ qc_summary: normalizedQcSummary,
189
302
  };
190
303
  }
@@ -19,10 +19,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.loadRunArtifacts = loadRunArtifacts;
20
20
  exports.computeScore = computeScore;
21
21
  exports.buildDiagnosisHints = buildDiagnosisHints;
22
+ exports.computeQcSummary = computeQcSummary;
22
23
  exports.summarizeRouterOutcomes = summarizeRouterOutcomes;
23
24
  exports.scoreRun = scoreRun;
24
25
  const node_fs_1 = require("node:fs");
25
26
  const node_path_1 = require("node:path");
27
+ const artifacts_js_1 = require("../qc/artifacts.js");
26
28
  const gates_js_1 = require("./gates.js");
27
29
  // ──────────────────────────────────────────────
28
30
  // Diagnosis hints table
@@ -60,6 +62,10 @@ const HINTS = {
60
62
  fix_zone: "medic / cluster-state",
61
63
  hint: "Medic artifacts detected — state repair was required during this run. Review the medic chart and root cause.",
62
64
  },
65
+ "qc-blocking-findings": {
66
+ fix_zone: "qc / findings",
67
+ hint: "QC produced unresolved critical/high findings attributed to this run. Review QC artifacts, triage findings, and route repairs before delivery.",
68
+ },
63
69
  };
64
70
  // ──────────────────────────────────────────────
65
71
  // Artifact loader
@@ -96,6 +102,18 @@ function safeReaddir(dir) {
96
102
  }
97
103
  }
98
104
  const NON_WORKER_PREFIXES = ["librarian-", "CHART-", "medic-result-"];
105
+ function loadQcArtifacts(clusterId, repoRoot) {
106
+ if (!clusterId)
107
+ return [];
108
+ const ids = (0, artifacts_js_1.listQcArtifactIds)(clusterId, repoRoot);
109
+ const results = [];
110
+ for (const id of ids) {
111
+ const result = (0, artifacts_js_1.readQcArtifact)(clusterId, id, repoRoot);
112
+ if (result)
113
+ results.push(result);
114
+ }
115
+ return results;
116
+ }
99
117
  function readResultPackets(clusterDir) {
100
118
  if (!clusterDir)
101
119
  return [];
@@ -177,6 +195,7 @@ function loadRunArtifacts(repoRoot, runId) {
177
195
  // Legacy fallback: extract from result packet files
178
196
  workerResultContracts = extractWorkerResultContracts(resultPackets);
179
197
  }
198
+ const qcResults = loadQcArtifacts(clusterId, repoRoot);
180
199
  return {
181
200
  runId,
182
201
  runDir,
@@ -186,6 +205,7 @@ function loadRunArtifacts(repoRoot, runId) {
186
205
  resultPackets,
187
206
  workerResultContracts,
188
207
  telemetryEvents,
208
+ qcResults,
189
209
  };
190
210
  }
191
211
  // ──────────────────────────────────────────────
@@ -204,6 +224,163 @@ function buildDiagnosisHints(failedGateNames) {
204
224
  return { gate, ...h };
205
225
  });
206
226
  }
227
+ const SEVERITY_WEIGHTS = {
228
+ critical: 10,
229
+ high: 5,
230
+ medium: 2,
231
+ low: 1,
232
+ info: 0.5,
233
+ };
234
+ const CONFIDENCE_WEIGHTS = {
235
+ high: 1.0,
236
+ medium: 0.7,
237
+ low: 0.3,
238
+ unattributed: 0,
239
+ };
240
+ function zeroCounts() {
241
+ return { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
242
+ }
243
+ function zeroRouting() {
244
+ return { original_worker: 0, repair_worker: 0, follow_up: 0, operator_review: 0, unset: 0 };
245
+ }
246
+ function computeQcPenalty(weightedOpenScore) {
247
+ // Soft penalty that grows with weighted severity but does not dominate the score.
248
+ return weightedOpenScore === 0 ? 0 : weightedOpenScore / (weightedOpenScore + 20);
249
+ }
250
+ /**
251
+ * Computes a QcScoreSummary from the QC results loaded for this run.
252
+ * Returns null when no QC results are present.
253
+ *
254
+ * A finding is "blocking" if:
255
+ * - severity is critical or high
256
+ * - status is "open" or "follow-up" (not autofixed, repaired, or waived)
257
+ * - attribution confidence is "high" or "medium" (not unvalidated provider noise)
258
+ *
259
+ * Worker scoring intentionally does not penalize for "unvalidated" or
260
+ * "low" attribution-confidence findings — these are treated as provider noise.
261
+ *
262
+ * The weighted open score multiplies severity by attribution confidence so that
263
+ * a critical/high-confidence finding hurts SOL scoring more than a low-severity
264
+ * or uncertain finding.
265
+ */
266
+ function computeQcSummary(qcResults) {
267
+ if (qcResults.length === 0)
268
+ return null;
269
+ let total = 0;
270
+ let blocking = 0;
271
+ let autofixed = 0;
272
+ let repaired = 0;
273
+ let waived = 0;
274
+ let unvalidated = 0;
275
+ const openBySeverity = zeroCounts();
276
+ let weightedOpenScore = 0;
277
+ let blocksDelivery = false;
278
+ const providerBreakdown = new Map();
279
+ const categoryBreakdown = new Map();
280
+ const routingBreakdown = zeroRouting();
281
+ const childScores = new Map();
282
+ const providerScores = new Map();
283
+ for (const result of qcResults) {
284
+ if (result.policyDecision.blocksDelivery)
285
+ blocksDelivery = true;
286
+ for (const finding of result.findings) {
287
+ total++;
288
+ const provider = result.provider;
289
+ const providerEntry = providerBreakdown.get(provider) ?? { total: 0, blocking: 0, unvalidated: 0 };
290
+ providerEntry.total += 1;
291
+ providerBreakdown.set(provider, providerEntry);
292
+ const category = (finding.category ?? "uncategorized").toLowerCase();
293
+ const categoryEntry = categoryBreakdown.get(category) ?? { total: 0, blocking: 0 };
294
+ categoryEntry.total += 1;
295
+ const routing = finding.routingDecision ?? "unset";
296
+ if (routing === "original-worker")
297
+ routingBreakdown.original_worker += 1;
298
+ else if (routing === "repair-worker")
299
+ routingBreakdown.repair_worker += 1;
300
+ else if (routing === "follow-up")
301
+ routingBreakdown.follow_up += 1;
302
+ else if (routing === "operator-review")
303
+ routingBreakdown.operator_review += 1;
304
+ else
305
+ routingBreakdown.unset += 1;
306
+ const conf = finding.attribution.confidence;
307
+ const isUnvalidated = conf === "low" || conf === "unattributed";
308
+ if (isUnvalidated) {
309
+ unvalidated++;
310
+ providerEntry.unvalidated += 1;
311
+ categoryBreakdown.set(category, categoryEntry);
312
+ continue; // do not count provider noise toward any negative bucket
313
+ }
314
+ if (finding.status === "autofixed") {
315
+ autofixed++;
316
+ categoryBreakdown.set(category, categoryEntry);
317
+ continue;
318
+ }
319
+ if (finding.status === "repaired") {
320
+ repaired++;
321
+ categoryBreakdown.set(category, categoryEntry);
322
+ continue;
323
+ }
324
+ if (finding.status === "waived") {
325
+ waived++;
326
+ categoryBreakdown.set(category, categoryEntry);
327
+ continue;
328
+ }
329
+ // "open" or "follow-up" — count toward open_by_severity and weighted score
330
+ const sev = finding.severity;
331
+ if (sev in openBySeverity)
332
+ openBySeverity[sev]++;
333
+ const isBlocking = (sev === "critical" || sev === "high") &&
334
+ (finding.status === "open" || finding.status === "follow-up");
335
+ if (isBlocking) {
336
+ blocking++;
337
+ categoryEntry.blocking += 1;
338
+ providerEntry.blocking += 1;
339
+ }
340
+ const findingWeight = SEVERITY_WEIGHTS[sev] * CONFIDENCE_WEIGHTS[conf];
341
+ weightedOpenScore += findingWeight;
342
+ const childId = finding.attribution.childId;
343
+ if (childId) {
344
+ const childEntry = childScores.get(childId) ?? { weighted_score: 0, finding_count: 0 };
345
+ childEntry.weighted_score += findingWeight;
346
+ childEntry.finding_count += 1;
347
+ childScores.set(childId, childEntry);
348
+ }
349
+ const providerScoreEntry = providerScores.get(provider) ?? { weighted_score: 0, finding_count: 0 };
350
+ providerScoreEntry.weighted_score += findingWeight;
351
+ providerScoreEntry.finding_count += 1;
352
+ providerScores.set(provider, providerScoreEntry);
353
+ categoryBreakdown.set(category, categoryEntry);
354
+ providerBreakdown.set(provider, providerEntry);
355
+ }
356
+ }
357
+ const recurringChildSignals = Array.from(childScores.entries())
358
+ .map(([child_id, value]) => ({ child_id, ...value }))
359
+ .filter((s) => s.weighted_score > 0)
360
+ .sort((a, b) => b.weighted_score - a.weighted_score || a.child_id.localeCompare(b.child_id));
361
+ const recurringProviderSignals = Array.from(providerScores.entries())
362
+ .map(([provider, value]) => ({ provider, ...value }))
363
+ .filter((s) => s.weighted_score > 0)
364
+ .sort((a, b) => b.weighted_score - a.weighted_score || a.provider.localeCompare(b.provider));
365
+ return {
366
+ total_findings: total,
367
+ blocking_findings: blocking,
368
+ autofixed_findings: autofixed,
369
+ repaired_findings: repaired,
370
+ waived_findings: waived,
371
+ unvalidated_findings: unvalidated,
372
+ open_by_severity: openBySeverity,
373
+ weighted_open_score: Number(weightedOpenScore.toFixed(3)),
374
+ qc_penalty: Number(computeQcPenalty(weightedOpenScore).toFixed(4)),
375
+ blocks_delivery: blocksDelivery,
376
+ qc_run_count: qcResults.length,
377
+ provider_breakdown: Object.fromEntries(providerBreakdown),
378
+ routing_breakdown: routingBreakdown,
379
+ category_breakdown: Object.fromEntries(categoryBreakdown),
380
+ recurring_child_signals: recurringChildSignals,
381
+ recurring_provider_signals: recurringProviderSignals,
382
+ };
383
+ }
207
384
  function asRecord(value) {
208
385
  return value && typeof value === "object" && !Array.isArray(value)
209
386
  ? value
@@ -308,6 +485,7 @@ function scoreRun(repoRoot, runId) {
308
485
  const score = computeScore(gateResults);
309
486
  const diagnosisHints = buildDiagnosisHints(failedGates);
310
487
  const routerOutcomes = summarizeRouterOutcomes(artifacts);
488
+ const qcSummary = computeQcSummary(artifacts.qcResults);
311
489
  const clusterId = artifacts.currentState &&
312
490
  typeof artifacts.currentState === "object" &&
313
491
  !Array.isArray(artifacts.currentState)
@@ -322,5 +500,6 @@ function scoreRun(repoRoot, runId) {
322
500
  score,
323
501
  diagnosis_hints: diagnosisHints,
324
502
  router_outcomes: routerOutcomes,
503
+ qc_summary: qcSummary,
325
504
  };
326
505
  }
@@ -33,17 +33,19 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.initializeClusterState = exports.writeClusterStateSync = exports.writeClusterState = exports.readClusterStateSync = exports.readClusterState = void 0;
36
+ exports.recordQcRun = exports.initializeClusterState = exports.writeClusterStateSync = exports.writeClusterState = exports.readClusterStateSync = exports.readClusterState = void 0;
37
37
  exports.pruneExpiredClaims = pruneExpiredClaims;
38
38
  const fs_1 = require("fs");
39
39
  const path = __importStar(require("path"));
40
40
  const local_graph_1 = require("../tracker/local-graph");
41
+ const artifacts_js_1 = require("../qc/artifacts.js");
41
42
  const getClusterStatePath = (clusterId, repoRoot) => {
42
43
  return path.join(repoRoot || process.cwd(), '.polaris', 'clusters', clusterId, 'cluster-state.json');
43
44
  };
44
45
  const normalizeClusterState = (state) => ({
45
46
  ...state,
46
47
  tracker_mutations: state.tracker_mutations ?? {},
48
+ qc_runs: state.qc_runs ?? {},
47
49
  });
48
50
  function pruneExpiredClaims(state, now = new Date()) {
49
51
  const nowMs = now.getTime();
@@ -330,8 +332,40 @@ const initializeClusterState = async (clusterId, repoRoot) => {
330
332
  commits: {},
331
333
  tracker_mutations: {},
332
334
  blockers: [],
335
+ qc_runs: {},
333
336
  };
334
337
  await (0, exports.writeClusterState)(clusterId, initialState, repoRoot);
335
338
  return initialState;
336
339
  };
337
340
  exports.initializeClusterState = initializeClusterState;
341
+ /**
342
+ * Persist a QC result artifact under the active cluster's evidence surface and
343
+ * record a pointer in the cluster state. This is the only supported way to
344
+ * durably store QC runs; callers must not write QC artifacts directly outside
345
+ * `.polaris/clusters/<cluster-id>/qc/`.
346
+ */
347
+ const recordQcRun = async (clusterId, result, repoRoot) => {
348
+ const artifactPath = (0, artifacts_js_1.writeQcArtifact)(clusterId, result, repoRoot);
349
+ const currentState = await (0, exports.readClusterState)(clusterId, repoRoot);
350
+ if (!currentState) {
351
+ throw new Error(`Cluster ${clusterId} state not found; cannot record QC run ${result.qcRunId}.`);
352
+ }
353
+ const pointer = {
354
+ artifact_path: artifactPath,
355
+ status: result.status,
356
+ provider: result.provider,
357
+ started_at: result.startedAt,
358
+ completed_at: result.completedAt,
359
+ };
360
+ const nextState = {
361
+ ...currentState,
362
+ state_generation: currentState.state_generation + 1,
363
+ qc_runs: {
364
+ ...currentState.qc_runs,
365
+ [result.qcRunId]: pointer,
366
+ },
367
+ };
368
+ await (0, exports.writeClusterState)(clusterId, nextState, repoRoot);
369
+ return { artifactPath, state: nextState };
370
+ };
371
+ exports.recordQcRun = recordQcRun;
@@ -103,4 +103,21 @@ exports.DEFAULT_CONFIG = {
103
103
  auto_deep_analysis: false,
104
104
  allow_cross_provider_delegation: false,
105
105
  },
106
+ qc: {
107
+ enabled: false,
108
+ defaultTrigger: "completed-cluster",
109
+ providers: {},
110
+ severityThresholds: {
111
+ block: "high",
112
+ repair: "medium",
113
+ followUp: "low",
114
+ },
115
+ autoFix: "disabled",
116
+ repairRouting: "route",
117
+ artifactRetention: {
118
+ retainRawOutput: false,
119
+ maxRuns: 10,
120
+ },
121
+ routes: {},
122
+ },
106
123
  };