@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.
- 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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC trigger orchestration.
|
|
4
|
+
*
|
|
5
|
+
* Wires QC providers into the Polaris lifecycle at:
|
|
6
|
+
* - completed-cluster (after all children done, before final delivery)
|
|
7
|
+
* - pr (after a PR is created, for providers that require a PR URL)
|
|
8
|
+
* - child (selected at dispatch time for high-risk scopes)
|
|
9
|
+
*
|
|
10
|
+
* The orchestrator is passive/dry-run by default; only configured providers
|
|
11
|
+
* are invoked, and only blocking policy stops the loop/finalize flow.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.runQcAtTrigger = runQcAtTrigger;
|
|
15
|
+
const store_js_1 = require("../cluster-state/store.js");
|
|
16
|
+
const policy_js_1 = require("./policy.js");
|
|
17
|
+
const runner_js_1 = require("./runner.js");
|
|
18
|
+
const triggers_js_1 = require("./triggers.js");
|
|
19
|
+
const attribution_js_1 = require("./attribution.js");
|
|
20
|
+
const autofix_js_1 = require("./autofix.js");
|
|
21
|
+
const routing_js_1 = require("./routing.js");
|
|
22
|
+
function buildAttributionContext(options) {
|
|
23
|
+
const { repoRoot, clusterId, branch, state } = options;
|
|
24
|
+
const dispatchRecords = {};
|
|
25
|
+
if (state?.open_children_meta) {
|
|
26
|
+
for (const [childId, meta] of Object.entries(state.open_children_meta)) {
|
|
27
|
+
if (meta?.dispatch_record) {
|
|
28
|
+
dispatchRecords[childId] = meta.dispatch_record;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
repoRoot,
|
|
34
|
+
baseBranch: branch ?? state?.branch ?? "main",
|
|
35
|
+
completedResults: state?.completed_children_results,
|
|
36
|
+
dispatchRecords,
|
|
37
|
+
clusterState: repoRoot ? ((0, store_js_1.readClusterStateSync)(clusterId, repoRoot) ?? undefined) : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function applyAttributionAndRouting(result, config, context, ownership, routeName) {
|
|
41
|
+
const updatedFindings = result.findings.map((finding) => {
|
|
42
|
+
const attribution = (0, attribution_js_1.resolveAttributionWithOwnership)(finding, context, ownership);
|
|
43
|
+
const autofix = (0, autofix_js_1.isAutofixEligible)(finding, config, {
|
|
44
|
+
provider: result.provider,
|
|
45
|
+
routeName,
|
|
46
|
+
});
|
|
47
|
+
const routing = (0, routing_js_1.decideRepairRouting)({ ...finding, attribution, autofixEligible: autofix.eligible }, config, autofix.eligible, { routeName });
|
|
48
|
+
return {
|
|
49
|
+
...finding,
|
|
50
|
+
attribution,
|
|
51
|
+
autofixEligible: autofix.eligible,
|
|
52
|
+
routingDecision: routing,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
const updatedResult = {
|
|
56
|
+
...result,
|
|
57
|
+
findings: updatedFindings,
|
|
58
|
+
policyDecision: (0, policy_js_1.computeQcPolicyDecision)({ ...result, findings: updatedFindings }, config),
|
|
59
|
+
};
|
|
60
|
+
return updatedResult;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Run QC for the given lifecycle trigger when configured.
|
|
64
|
+
*
|
|
65
|
+
* Returns a no-op "pass" result when QC is disabled, no providers are configured
|
|
66
|
+
* for the trigger, or all providers run successfully without findings that
|
|
67
|
+
* match a blocking/follow-up policy.
|
|
68
|
+
*/
|
|
69
|
+
async function runQcAtTrigger(options) {
|
|
70
|
+
const { config, registry, trigger, prUrl, repoRoot, runId, clusterId, branch, telemetryFile, timeoutMs, state, routeName, } = options;
|
|
71
|
+
if (!config.enabled) {
|
|
72
|
+
return {
|
|
73
|
+
trigger,
|
|
74
|
+
results: [],
|
|
75
|
+
action: "pass",
|
|
76
|
+
summary: "QC disabled by configuration",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const providers = (0, triggers_js_1.activeProvidersForTrigger)(config, trigger);
|
|
80
|
+
if (providers.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
trigger,
|
|
83
|
+
results: [],
|
|
84
|
+
action: "pass",
|
|
85
|
+
summary: `No QC providers configured for trigger "${trigger}"`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const attributionContext = buildAttributionContext(options);
|
|
89
|
+
const ownership = (0, attribution_js_1.buildChangedFileOwnership)(attributionContext);
|
|
90
|
+
const results = [];
|
|
91
|
+
const errors = [];
|
|
92
|
+
for (const [name, providerConfig] of providers) {
|
|
93
|
+
const provider = registry.get(name);
|
|
94
|
+
if (!provider) {
|
|
95
|
+
errors.push(`Unknown QC provider "${name}"`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const scope = {
|
|
99
|
+
clusterId,
|
|
100
|
+
runId,
|
|
101
|
+
...(trigger === "pr" ? { prUrl } : { branch }),
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
const rawResult = await (0, runner_js_1.executeQcProvider)(provider, scope, {
|
|
105
|
+
repoRoot,
|
|
106
|
+
runId,
|
|
107
|
+
clusterId,
|
|
108
|
+
branch,
|
|
109
|
+
telemetryFile,
|
|
110
|
+
timeoutMs,
|
|
111
|
+
});
|
|
112
|
+
const result = applyAttributionAndRouting(rawResult, config, attributionContext, ownership, routeName);
|
|
113
|
+
try {
|
|
114
|
+
await (0, store_js_1.recordQcRun)(clusterId, result, repoRoot);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
errors.push(`Failed to record QC run ${result.qcRunId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
118
|
+
}
|
|
119
|
+
results.push(result);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
errors.push(`QC provider "${name}" execution error: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let action = "pass";
|
|
126
|
+
for (const result of results) {
|
|
127
|
+
const a = (0, policy_js_1.decideQcAction)(result, config);
|
|
128
|
+
if (a === "block") {
|
|
129
|
+
action = "block";
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (a === "follow-up" && action === "pass") {
|
|
133
|
+
action = "follow-up";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const successfulRuns = results.filter((result) => result.status !== "failed");
|
|
137
|
+
if (action === "pass" && successfulRuns.length === 0 && (results.length > 0 || errors.length > 0)) {
|
|
138
|
+
action = "block";
|
|
139
|
+
}
|
|
140
|
+
const summaryParts = results.map((r) => `${r.provider}: ${r.status} (${r.findings.length} findings)`);
|
|
141
|
+
if (errors.length > 0) {
|
|
142
|
+
summaryParts.push(`errors: ${errors.join(", ")}`);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
trigger,
|
|
146
|
+
results,
|
|
147
|
+
action,
|
|
148
|
+
summary: summaryParts.join("; ") || "No QC providers produced results",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC policy decision logic.
|
|
4
|
+
*
|
|
5
|
+
* Translates a normalized QC result and the Polaris QC configuration into an
|
|
6
|
+
* explicit action for the loop/finalize lifecycle. Findings with routing
|
|
7
|
+
* decisions applied are used to determine whether delivery should block,
|
|
8
|
+
* require operator review, or be routed to repair.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.decideQcAction = decideQcAction;
|
|
12
|
+
exports.computeQcPolicyDecision = computeQcPolicyDecision;
|
|
13
|
+
const severity_js_1 = require("./severity.js");
|
|
14
|
+
/**
|
|
15
|
+
* Decide what the runtime should do with a QC result.
|
|
16
|
+
*
|
|
17
|
+
* Rules:
|
|
18
|
+
* - passed / skipped -> pass (no effect on delivery)
|
|
19
|
+
* - repairRouting: "block" -> block finalize/delivery
|
|
20
|
+
* - repairRouting: "log" -> pass (passive report only)
|
|
21
|
+
* - repairRouting: "route" / "follow-up" -> create follow-up work, but high/critical
|
|
22
|
+
* findings still block according to severity thresholds.
|
|
23
|
+
*
|
|
24
|
+
* Timeouts are surfaced as failed results and follow the same routing policy.
|
|
25
|
+
*/
|
|
26
|
+
function decideQcAction(result, config) {
|
|
27
|
+
if (result.status === "passed" || result.status === "skipped") {
|
|
28
|
+
return "pass";
|
|
29
|
+
}
|
|
30
|
+
const routing = config.repairRouting ?? "route";
|
|
31
|
+
if (routing === "block") {
|
|
32
|
+
return "block";
|
|
33
|
+
}
|
|
34
|
+
if (routing === "log") {
|
|
35
|
+
return "pass";
|
|
36
|
+
}
|
|
37
|
+
const blockThreshold = config.severityThresholds?.block ?? "high";
|
|
38
|
+
const hasBlockingFinding = result.findings.some((f) => (0, severity_js_1.compareSeverity)(f.severity, blockThreshold) >= 0);
|
|
39
|
+
if (hasBlockingFinding) {
|
|
40
|
+
return "block";
|
|
41
|
+
}
|
|
42
|
+
return "follow-up";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compute the detailed policy decision for a QC result after attribution and
|
|
46
|
+
* repair routing have been applied.
|
|
47
|
+
*/
|
|
48
|
+
function computeQcPolicyDecision(result, config) {
|
|
49
|
+
const blockThreshold = config.severityThresholds?.block ?? "high";
|
|
50
|
+
const blocksDelivery = result.findings.some((f) => (0, severity_js_1.compareSeverity)(f.severity, blockThreshold) >= 0 &&
|
|
51
|
+
(f.routingDecision === "operator-review" || f.routingDecision === undefined));
|
|
52
|
+
const requiresOperatorReview = result.status === "failed" ||
|
|
53
|
+
result.status === "blocked" ||
|
|
54
|
+
result.findings.some((f) => f.routingDecision === "operator-review");
|
|
55
|
+
const routedToRepair = result.findings.some((f) => f.routingDecision === "original-worker" || f.routingDecision === "repair-worker");
|
|
56
|
+
const summaryParts = [];
|
|
57
|
+
summaryParts.push(`${result.findings.length} findings`);
|
|
58
|
+
if (requiresOperatorReview) {
|
|
59
|
+
summaryParts.push("operator review required");
|
|
60
|
+
}
|
|
61
|
+
if (routedToRepair) {
|
|
62
|
+
summaryParts.push("routed to repair");
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
blocksDelivery,
|
|
66
|
+
requiresOperatorReview,
|
|
67
|
+
routedToRepair,
|
|
68
|
+
summary: summaryParts.join("; "),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QcProviderRegistry = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Registry of configured QC providers. The registry is keyed by provider name.
|
|
6
|
+
*/
|
|
7
|
+
class QcProviderRegistry {
|
|
8
|
+
providers = new Map();
|
|
9
|
+
register(provider) {
|
|
10
|
+
this.providers.set(provider.name, provider);
|
|
11
|
+
}
|
|
12
|
+
get(name) {
|
|
13
|
+
return this.providers.get(name);
|
|
14
|
+
}
|
|
15
|
+
has(name) {
|
|
16
|
+
return this.providers.has(name);
|
|
17
|
+
}
|
|
18
|
+
list() {
|
|
19
|
+
return Array.from(this.providers.values());
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Return providers that can review the given scope, ordered by registration.
|
|
23
|
+
*/
|
|
24
|
+
candidatesFor(scope) {
|
|
25
|
+
return this.list().filter((provider) => provider.canReview(scope));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.QcProviderRegistry = QcProviderRegistry;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodeRabbitQcProvider = void 0;
|
|
4
|
+
const severity_js_1 = require("../severity.js");
|
|
5
|
+
function coerceNumber(value) {
|
|
6
|
+
if (value === null || value === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
const n = Number(value);
|
|
9
|
+
return Number.isFinite(n) ? n : undefined;
|
|
10
|
+
}
|
|
11
|
+
function pickString(...candidates) {
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
14
|
+
return candidate.trim();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
function buildRange(raw) {
|
|
20
|
+
const startLine = coerceNumber(raw.startLine ?? raw.line) ?? 1;
|
|
21
|
+
const endLine = coerceNumber(raw.endLine);
|
|
22
|
+
const startColumn = coerceNumber(raw.startColumn ?? raw.column);
|
|
23
|
+
const endColumn = coerceNumber(raw.endColumn);
|
|
24
|
+
if (!endLine && startColumn === undefined && endColumn === undefined) {
|
|
25
|
+
return { startLine };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
startLine,
|
|
29
|
+
...(startColumn !== undefined ? { startColumn } : {}),
|
|
30
|
+
...(endLine !== undefined ? { endLine } : {}),
|
|
31
|
+
...(endColumn !== undefined ? { endColumn } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function parseFindingsFromPayload(payload) {
|
|
35
|
+
if (!payload || typeof payload !== "object") {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const record = payload;
|
|
39
|
+
if (Array.isArray(record.findings)) {
|
|
40
|
+
return record.findings;
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(record.issues)) {
|
|
43
|
+
return record.issues;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(record.results)) {
|
|
46
|
+
return record.results;
|
|
47
|
+
}
|
|
48
|
+
// Single finding wrapped in an object
|
|
49
|
+
if (record.severity !== undefined ||
|
|
50
|
+
record.message !== undefined ||
|
|
51
|
+
record.title !== undefined) {
|
|
52
|
+
return [record];
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
function parseReport(output) {
|
|
57
|
+
const text = "stdout" in output && typeof output.stdout === "string" ? output.stdout : "";
|
|
58
|
+
const data = "data" in output ? output.data : undefined;
|
|
59
|
+
if (data !== undefined && typeof data === "object" && data !== null) {
|
|
60
|
+
const findings = parseFindingsFromPayload(data);
|
|
61
|
+
const record = data;
|
|
62
|
+
return {
|
|
63
|
+
findings,
|
|
64
|
+
...(typeof record.prUrl === "string" ? { prUrl: record.prUrl } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (text.trim().startsWith("{") || text.trim().startsWith("[")) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(text);
|
|
70
|
+
if (Array.isArray(parsed)) {
|
|
71
|
+
return { findings: parsed };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
findings: parseFindingsFromPayload(parsed),
|
|
75
|
+
...(typeof parsed?.prUrl === "string"
|
|
76
|
+
? { prUrl: parsed.prUrl }
|
|
77
|
+
: {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Fall through to line scanning.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Try JSONL: one finding per line
|
|
85
|
+
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
86
|
+
const lineFindings = [];
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(line);
|
|
90
|
+
lineFindings.push(parsed);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Ignore unparseable lines.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (lineFindings.length > 0) {
|
|
97
|
+
return { findings: lineFindings };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function normalizeFinding(raw, index) {
|
|
102
|
+
const severityLabel = pickString(raw.severity, raw.level) ?? "info";
|
|
103
|
+
const severity = (0, severity_js_1.normalizeSeverity)(severityLabel);
|
|
104
|
+
const title = pickString(raw.title, raw.summary, raw.rule, raw.type, raw.category) ?? `Finding #${index + 1}`;
|
|
105
|
+
const message = pickString(raw.message, raw.description, raw.body);
|
|
106
|
+
const filePath = pickString(raw.file, raw.filePath, raw.path);
|
|
107
|
+
const suggestedAction = pickString(raw.suggestion, raw.suggestedAction, raw.fix);
|
|
108
|
+
const providerFindingId = pickString(raw.providerFindingId, raw.id, raw.findingId);
|
|
109
|
+
const confidence = coerceNumber(raw.confidence);
|
|
110
|
+
const attribution = {
|
|
111
|
+
confidence: "unattributed",
|
|
112
|
+
reason: "provider-uncertain",
|
|
113
|
+
filePath,
|
|
114
|
+
};
|
|
115
|
+
const fixAvailable = raw.fixAvailable === true || raw.autofixEligible === true || Boolean(raw.fix);
|
|
116
|
+
return {
|
|
117
|
+
findingId: `coderabbit-${index + 1}-${Date.now()}`,
|
|
118
|
+
...(providerFindingId ? { providerFindingId } : {}),
|
|
119
|
+
severity,
|
|
120
|
+
...(pickString(raw.category, raw.type, raw.rule) ? { category: pickString(raw.category, raw.type, raw.rule) } : {}),
|
|
121
|
+
title,
|
|
122
|
+
...(message ? { message } : {}),
|
|
123
|
+
...(filePath ? { filePath } : {}),
|
|
124
|
+
range: filePath ? buildRange(raw) : undefined,
|
|
125
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
126
|
+
...(suggestedAction ? { suggestedAction } : {}),
|
|
127
|
+
fixAvailable,
|
|
128
|
+
autofixEligible: raw.autofixEligible === true || fixAvailable,
|
|
129
|
+
attribution,
|
|
130
|
+
status: "open",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function computeResultStatus(findings, providerFailed) {
|
|
134
|
+
if (findings.length === 0) {
|
|
135
|
+
return providerFailed ? "failed" : "passed";
|
|
136
|
+
}
|
|
137
|
+
const highestSeverity = findings.reduce((max, finding) => (0, severity_js_1.maxSeverity)(max, finding.severity), findings[0].severity);
|
|
138
|
+
if (highestSeverity === "critical") {
|
|
139
|
+
return "blocked";
|
|
140
|
+
}
|
|
141
|
+
if (highestSeverity === "info") {
|
|
142
|
+
return "passed";
|
|
143
|
+
}
|
|
144
|
+
return "findings";
|
|
145
|
+
}
|
|
146
|
+
function buildResultFromReport(report, output, mode, providerFailed = false) {
|
|
147
|
+
const now = new Date().toISOString();
|
|
148
|
+
const rawFindings = report?.findings ?? [];
|
|
149
|
+
const findings = rawFindings.map(normalizeFinding);
|
|
150
|
+
return {
|
|
151
|
+
schemaVersion: "1.0",
|
|
152
|
+
qcRunId: `${output.provider}-${Date.now()}`,
|
|
153
|
+
runId: "unknown",
|
|
154
|
+
clusterId: "unknown",
|
|
155
|
+
trigger: "completed-cluster",
|
|
156
|
+
provider: output.provider,
|
|
157
|
+
providerMode: mode,
|
|
158
|
+
prUrl: report?.prUrl,
|
|
159
|
+
startedAt: now,
|
|
160
|
+
completedAt: now,
|
|
161
|
+
status: computeResultStatus(findings, providerFailed),
|
|
162
|
+
findings,
|
|
163
|
+
rawArtifactPaths: "artifactPath" in output && output.artifactPath ? [output.artifactPath] : [],
|
|
164
|
+
parserVersion: "coderabbit-1.0",
|
|
165
|
+
policyDecision: {
|
|
166
|
+
blocksDelivery: findings.some((f) => f.severity === "critical"),
|
|
167
|
+
requiresOperatorReview: findings.some((f) => f.severity === "high" || f.severity === "critical"),
|
|
168
|
+
routedToRepair: findings.some((f) => f.severity === "medium" || f.severity === "high" || f.severity === "critical"),
|
|
169
|
+
summary: findings.length === 0
|
|
170
|
+
? "CodeRabbit review returned no findings."
|
|
171
|
+
: `CodeRabbit review returned ${findings.length} finding(s).`,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* CodeRabbit-style QC adapter.
|
|
177
|
+
*
|
|
178
|
+
* Parses JSON, JSONL, and metrics payloads into normalized Polaris findings.
|
|
179
|
+
* No external network calls are made here.
|
|
180
|
+
*/
|
|
181
|
+
class CodeRabbitQcProvider {
|
|
182
|
+
name = "coderabbit";
|
|
183
|
+
supportedModes = ["local", "pr", "metrics-import"];
|
|
184
|
+
capabilities = [
|
|
185
|
+
"diff-review",
|
|
186
|
+
"pr-review",
|
|
187
|
+
"result-parsing",
|
|
188
|
+
"auto-fix",
|
|
189
|
+
"metrics-import",
|
|
190
|
+
];
|
|
191
|
+
canReview(scope) {
|
|
192
|
+
if (scope.prUrl)
|
|
193
|
+
return true;
|
|
194
|
+
return Boolean(scope.branch);
|
|
195
|
+
}
|
|
196
|
+
buildReviewCommand(scope) {
|
|
197
|
+
if (scope.prUrl) {
|
|
198
|
+
return { command: "coderabbit", args: ["review", "--agent", "--pr-url", scope.prUrl] };
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
command: "coderabbit",
|
|
202
|
+
args: ["review", "--agent", "--branch", scope.branch ?? "HEAD"],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
parse(output) {
|
|
206
|
+
const report = parseReport(output);
|
|
207
|
+
return buildResultFromReport(report, output, "local", output.exitCode !== 0);
|
|
208
|
+
}
|
|
209
|
+
importMetrics(payload) {
|
|
210
|
+
const report = parseReport(payload);
|
|
211
|
+
return buildResultFromReport(report, payload, "metrics-import");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.CodeRabbitQcProvider = CodeRabbitQcProvider;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodeRabbitQcProvider = void 0;
|
|
4
|
+
var coderabbit_js_1 = require("./coderabbit.js");
|
|
5
|
+
Object.defineProperty(exports, "CodeRabbitQcProvider", { enumerable: true, get: function () { return coderabbit_js_1.CodeRabbitQcProvider; } });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Default QC provider registry.
|
|
4
|
+
*
|
|
5
|
+
* Registers all built-in QC adapters. In the future this can be extended to
|
|
6
|
+
* discover providers from configuration or installed plugins.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createDefaultQcRegistry = createDefaultQcRegistry;
|
|
10
|
+
const provider_js_1 = require("./provider.js");
|
|
11
|
+
const coderabbit_js_1 = require("./providers/coderabbit.js");
|
|
12
|
+
function createDefaultQcRegistry() {
|
|
13
|
+
const registry = new provider_js_1.QcProviderRegistry();
|
|
14
|
+
registry.register(new coderabbit_js_1.CodeRabbitQcProvider());
|
|
15
|
+
return registry;
|
|
16
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC repair routing.
|
|
4
|
+
*
|
|
5
|
+
* Maps an attributed finding to one of four repair paths:
|
|
6
|
+
* - original-worker: hand back to the child that owns the change.
|
|
7
|
+
* - repair-worker: hand to a Medic-style repair worker (shared/unclear ownership).
|
|
8
|
+
* - follow-up: create a tracker follow-up issue (low severity, non-security).
|
|
9
|
+
* - operator-review: escalate to a human (high/critical, security, blocked auto-fix).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.decideRepairRouting = decideRepairRouting;
|
|
13
|
+
const security_category_js_1 = require("./security-category.js");
|
|
14
|
+
const severity_js_1 = require("./severity.js");
|
|
15
|
+
function getBlockThreshold(config, context) {
|
|
16
|
+
const route = context.routeName ? config.routes?.[context.routeName] : undefined;
|
|
17
|
+
return route?.blockThreshold ?? config.severityThresholds?.block ?? "high";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Decide the repair path for a finding.
|
|
21
|
+
*
|
|
22
|
+
* Rules (in order):
|
|
23
|
+
* 1. High/critical findings or security-sensitive findings -> operator-review.
|
|
24
|
+
* 2. Auto-fix eligible findings with clear attribution -> original-worker.
|
|
25
|
+
* 3. Low severity -> follow-up issue.
|
|
26
|
+
* 4. Shared ownership -> repair-worker.
|
|
27
|
+
* 5. Unclear/weak attribution -> operator-review for medium+, follow-up for low/info.
|
|
28
|
+
*/
|
|
29
|
+
function decideRepairRouting(finding, config, autofixEligible, context = {}) {
|
|
30
|
+
const blockThreshold = getBlockThreshold(config, context);
|
|
31
|
+
if ((0, severity_js_1.compareSeverity)(finding.severity, blockThreshold) >= 0) {
|
|
32
|
+
return "operator-review";
|
|
33
|
+
}
|
|
34
|
+
if ((0, security_category_js_1.isSecurityCategory)(finding.category)) {
|
|
35
|
+
return "operator-review";
|
|
36
|
+
}
|
|
37
|
+
const attribution = finding.attribution;
|
|
38
|
+
const clearAttribution = attribution.confidence === "high" || attribution.confidence === "medium";
|
|
39
|
+
if (autofixEligible && clearAttribution && attribution.childId) {
|
|
40
|
+
return "original-worker";
|
|
41
|
+
}
|
|
42
|
+
if ((0, severity_js_1.compareSeverity)(finding.severity, "low") <= 0) {
|
|
43
|
+
return "follow-up";
|
|
44
|
+
}
|
|
45
|
+
if (attribution.reason === "shared-file") {
|
|
46
|
+
return "repair-worker";
|
|
47
|
+
}
|
|
48
|
+
if (attribution.confidence === "low" && attribution.reason === "provider-uncertain") {
|
|
49
|
+
return attribution.childId ? "repair-worker" : "operator-review";
|
|
50
|
+
}
|
|
51
|
+
if (attribution.confidence === "unattributed") {
|
|
52
|
+
return "operator-review";
|
|
53
|
+
}
|
|
54
|
+
return "repair-worker";
|
|
55
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC provider execution runner.
|
|
4
|
+
*
|
|
5
|
+
* Runs a provider-specific review command, handles timeouts, and normalizes
|
|
6
|
+
* the raw output into a Polaris QcResult with runtime fields populated.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.executeQcProvider = executeQcProvider;
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
function makeSyntheticResult(provider, scope, startedAt, status, summary) {
|
|
14
|
+
const completedAt = new Date().toISOString();
|
|
15
|
+
return {
|
|
16
|
+
schemaVersion: "1.0",
|
|
17
|
+
qcRunId: `${provider.name}-${Date.now()}`,
|
|
18
|
+
runId: scope.runId,
|
|
19
|
+
clusterId: scope.clusterId,
|
|
20
|
+
trigger: scope.prUrl ? "pr" : "completed-cluster",
|
|
21
|
+
provider: provider.name,
|
|
22
|
+
providerMode: scope.prUrl ? "pr" : "local",
|
|
23
|
+
prUrl: scope.prUrl,
|
|
24
|
+
startedAt,
|
|
25
|
+
completedAt,
|
|
26
|
+
status,
|
|
27
|
+
findings: [],
|
|
28
|
+
rawArtifactPaths: [],
|
|
29
|
+
parserVersion: `${provider.name}-synthetic`,
|
|
30
|
+
policyDecision: {
|
|
31
|
+
blocksDelivery: false,
|
|
32
|
+
requiresOperatorReview: status !== "passed",
|
|
33
|
+
routedToRepair: false,
|
|
34
|
+
summary,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function appendTelemetry(telemetryFile, event) {
|
|
39
|
+
if (!telemetryFile)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
|
|
43
|
+
(0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Telemetry is best-effort.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function resolveExecutionFailure(provider, scope, options, startedAt, output, reason) {
|
|
50
|
+
const result = makeSyntheticResult(provider, scope, startedAt, "failed", reason);
|
|
51
|
+
appendTelemetry(options.telemetryFile, {
|
|
52
|
+
event: "qc-provider-execution-failed",
|
|
53
|
+
run_id: scope.runId,
|
|
54
|
+
cluster_id: scope.clusterId,
|
|
55
|
+
provider: provider.name,
|
|
56
|
+
exit_code: output.exitCode,
|
|
57
|
+
reason,
|
|
58
|
+
timestamp: result.completedAt,
|
|
59
|
+
});
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Execute a provider review command for the given scope.
|
|
64
|
+
*
|
|
65
|
+
* Timeouts produce a synthetic "failed" result so that policy logic downstream
|
|
66
|
+
* can decide whether to block, follow-up, or report passively.
|
|
67
|
+
*/
|
|
68
|
+
async function executeQcProvider(provider, scope, options) {
|
|
69
|
+
const startedAt = new Date().toISOString();
|
|
70
|
+
const command = provider.buildReviewCommand(scope);
|
|
71
|
+
const execFn = options.execFileImpl ?? node_child_process_1.execFile;
|
|
72
|
+
const timeoutMs = options.timeoutMs ?? 300_000; // 5 minutes default
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
let stdout = "";
|
|
75
|
+
let stderr = "";
|
|
76
|
+
const child = execFn(command.command, command.args, {
|
|
77
|
+
cwd: options.repoRoot,
|
|
78
|
+
timeout: timeoutMs,
|
|
79
|
+
encoding: "utf-8",
|
|
80
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
81
|
+
}, (error, out, err) => {
|
|
82
|
+
stdout = out ?? "";
|
|
83
|
+
stderr = err ?? "";
|
|
84
|
+
// Detect timeout: execFile kills with SIGTERM on timeout.
|
|
85
|
+
if (error && error.killed && error.signal === "SIGTERM") {
|
|
86
|
+
const result = makeSyntheticResult(provider, scope, startedAt, "failed", `QC provider ${provider.name} timed out after ${timeoutMs}ms`);
|
|
87
|
+
appendTelemetry(options.telemetryFile, {
|
|
88
|
+
event: "qc-provider-timeout",
|
|
89
|
+
run_id: scope.runId,
|
|
90
|
+
cluster_id: scope.clusterId,
|
|
91
|
+
provider: provider.name,
|
|
92
|
+
timeout_ms: timeoutMs,
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
resolve(result);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const output = {
|
|
99
|
+
provider: provider.name,
|
|
100
|
+
stdout,
|
|
101
|
+
stderr,
|
|
102
|
+
exitCode: error ? (typeof error.code === "number" ? error.code : 1) : 0,
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
const parsed = provider.parse(output);
|
|
106
|
+
const completedAt = new Date().toISOString();
|
|
107
|
+
const result = {
|
|
108
|
+
...parsed,
|
|
109
|
+
qcRunId: `${provider.name}-${Date.now()}`,
|
|
110
|
+
runId: scope.runId,
|
|
111
|
+
clusterId: scope.clusterId,
|
|
112
|
+
trigger: scope.prUrl ? "pr" : scope.branch ? "completed-cluster" : "child",
|
|
113
|
+
provider: provider.name,
|
|
114
|
+
providerMode: scope.prUrl ? "pr" : "local",
|
|
115
|
+
prUrl: scope.prUrl,
|
|
116
|
+
startedAt,
|
|
117
|
+
completedAt,
|
|
118
|
+
};
|
|
119
|
+
appendTelemetry(options.telemetryFile, {
|
|
120
|
+
event: "qc-provider-executed",
|
|
121
|
+
run_id: scope.runId,
|
|
122
|
+
cluster_id: scope.clusterId,
|
|
123
|
+
provider: provider.name,
|
|
124
|
+
trigger: result.trigger,
|
|
125
|
+
status: result.status,
|
|
126
|
+
findings_count: result.findings.length,
|
|
127
|
+
exit_code: output.exitCode,
|
|
128
|
+
timestamp: completedAt,
|
|
129
|
+
});
|
|
130
|
+
resolve(result);
|
|
131
|
+
}
|
|
132
|
+
catch (parseError) {
|
|
133
|
+
resolve(resolveExecutionFailure(provider, scope, options, startedAt, output, `QC provider ${provider.name} parse failed: ${parseError instanceof Error ? parseError.message : String(parseError)}`));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|