@lsctech/polaris 0.5.0 → 0.5.2
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/gates.js +47 -0
- package/dist/autoresearch/proposal.js +113 -0
- package/dist/autoresearch/score.js +179 -0
- package/dist/cluster-state/store.js +35 -1
- package/dist/config/defaults.js +17 -0
- package/dist/config/validator.js +244 -0
- package/dist/finalize/artifact-policy.js +3 -1
- package/dist/finalize/index.js +73 -17
- package/dist/finalize/linear.js +45 -0
- package/dist/finalize/run-report.js +68 -12
- package/dist/loop/adapters/cli-subtask-bridge.js +0 -1
- package/dist/loop/adapters/terminal-cli.js +0 -3
- package/dist/loop/dispatch-boundary.js +29 -0
- package/dist/loop/parent.js +50 -2
- package/dist/qc/artifacts.js +117 -0
- package/dist/qc/attribution.js +266 -0
- package/dist/qc/autofix.js +77 -0
- package/dist/qc/index.js +30 -0
- package/dist/qc/orchestration.js +150 -0
- package/dist/qc/policy.js +70 -0
- package/dist/qc/provider.js +28 -0
- package/dist/qc/providers/coderabbit.js +214 -0
- package/dist/qc/providers/index.js +5 -0
- package/dist/qc/registry.js +16 -0
- package/dist/qc/routing.js +55 -0
- package/dist/qc/runner.js +137 -0
- package/dist/qc/schemas.js +110 -0
- package/dist/qc/security-category.js +11 -0
- package/dist/qc/severity.js +78 -0
- package/dist/qc/triggers.js +92 -0
- package/dist/qc/types.js +9 -0
- package/package.json +1 -1
|
@@ -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;
|
package/dist/config/defaults.js
CHANGED
|
@@ -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
|
};
|