@lsctech/polaris 0.5.6 → 0.5.8
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/index.js +30 -1
- package/dist/autoresearch/proposal.js +5 -0
- package/dist/autoresearch/score.js +174 -2
- package/dist/autoresearch/sol-evaluation-writer.js +135 -0
- package/dist/autoresearch/sol-evidence-loader.js +40 -3
- package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
- package/dist/autoresearch/sol-recommendations.js +212 -0
- package/dist/autoresearch/sol-report-renderer.js +282 -0
- package/dist/autoresearch/sol-report.js +44 -0
- package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
- package/dist/autoresearch/sol-scorer.js +45 -1
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/config/defaults.js +1 -0
- package/dist/config/validator.js +157 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +198 -4
- package/dist/finalize/linear.js +8 -4
- package/dist/finalize/steps/08-create-pr.js +2 -2
- package/dist/finalize/steps/11-update-linear.js +2 -2
- package/dist/loop/parent.js +189 -33
- package/dist/loop/worker-packet.js +75 -1
- package/dist/qc/artifacts.js +27 -0
- package/dist/qc/fixtures/repair-packets.js +170 -0
- package/dist/qc/index.js +2 -0
- package/dist/qc/orchestration.js +5 -2
- package/dist/qc/policy.js +30 -3
- package/dist/qc/providers/coderabbit.js +152 -17
- package/dist/qc/registry.js +36 -3
- package/dist/qc/repair-loop.js +458 -0
- package/dist/qc/repair-packets.js +420 -0
- package/dist/qc/runner.js +328 -37
- package/dist/qc/schemas.js +79 -1
- package/dist/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* accept BootstrapPacket transparently accept WorkerPacket.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.STARTUP_RETURN_CONTRACT = exports.PREFLIGHT_RETURN_CONTRACT = exports.WORKER_PROHIBITED_WRITE_PATHS = exports.FINALIZE_RETURN_CONTRACT = exports.IMPL_RETURN_CONTRACT = void 0;
|
|
14
|
+
exports.REPAIR_RETURN_CONTRACT = exports.STARTUP_RETURN_CONTRACT = exports.PREFLIGHT_RETURN_CONTRACT = exports.WORKER_PROHIBITED_WRITE_PATHS = exports.FINALIZE_RETURN_CONTRACT = exports.IMPL_RETURN_CONTRACT = void 0;
|
|
15
15
|
exports.roleContextForWorkerRole = roleContextForWorkerRole;
|
|
16
16
|
exports.isWorkerPacket = isWorkerPacket;
|
|
17
17
|
exports.getWorkerCommitPolicy = getWorkerCommitPolicy;
|
|
@@ -19,6 +19,7 @@ exports.compileStartupPacket = compileStartupPacket;
|
|
|
19
19
|
exports.compileImplPacket = compileImplPacket;
|
|
20
20
|
exports.compileFinalizePacket = compileFinalizePacket;
|
|
21
21
|
exports.compilePreflightPacket = compilePreflightPacket;
|
|
22
|
+
exports.compileRepairWorkerPacket = compileRepairWorkerPacket;
|
|
22
23
|
const worker_prompt_js_1 = require("./worker-prompt.js");
|
|
23
24
|
const body_parser_js_1 = require("./body-parser.js");
|
|
24
25
|
const WORKER_ROLE_CONTEXT = {
|
|
@@ -421,3 +422,76 @@ function compilePreflightPacket(input) {
|
|
|
421
422
|
},
|
|
422
423
|
};
|
|
423
424
|
}
|
|
425
|
+
/** Return contract for repair workers (same shape as impl). */
|
|
426
|
+
exports.REPAIR_RETURN_CONTRACT = [
|
|
427
|
+
'child_id',
|
|
428
|
+
'status',
|
|
429
|
+
'commit',
|
|
430
|
+
'validation',
|
|
431
|
+
'next_recommended_action',
|
|
432
|
+
];
|
|
433
|
+
/**
|
|
434
|
+
* Build a compiled repair worker packet.
|
|
435
|
+
*
|
|
436
|
+
* Repair workers use `worker_role: "repair"` and the same sealed packet/result
|
|
437
|
+
* contracts as impl workers, but with tightly scoped allowed/prohibited paths
|
|
438
|
+
* derived from the compiled repair packet manifest.
|
|
439
|
+
*
|
|
440
|
+
* The parent/orchestrator MUST use this function and MUST NOT implement
|
|
441
|
+
* repair work inline.
|
|
442
|
+
*/
|
|
443
|
+
function compileRepairWorkerPacket(input) {
|
|
444
|
+
const steps = [
|
|
445
|
+
`Confirm QC repair scope: allowed=${JSON.stringify(input.allowedScope)}, prohibited=${JSON.stringify(input.prohibitedScope)}.`,
|
|
446
|
+
`Review root-cause hint: ${input.rootCauseHint}`,
|
|
447
|
+
`Apply minimal repair to files within allowed scope only. Do NOT touch prohibited paths.`,
|
|
448
|
+
`Run validation commands and confirm all pass.`,
|
|
449
|
+
`Create exactly ONE git commit: [REPAIR] ${input.clusterId} round-${input.round} packet-${input.packetId}.`,
|
|
450
|
+
`Write compact return JSON to stdout (fields: ${exports.REPAIR_RETURN_CONTRACT.join(', ')}). next_recommended_action MUST be "continue" on success, "stop" on unresolvable blocker.`,
|
|
451
|
+
`TERMINATE SESSION IMMEDIATELY.`,
|
|
452
|
+
];
|
|
453
|
+
return {
|
|
454
|
+
schema_version: '2.1',
|
|
455
|
+
worker_role: 'repair',
|
|
456
|
+
run_id: input.runId,
|
|
457
|
+
cluster_id: input.clusterId,
|
|
458
|
+
active_child: input.packetId,
|
|
459
|
+
state_file: input.stateFile,
|
|
460
|
+
telemetry_file: input.telemetryFile,
|
|
461
|
+
instructions: {
|
|
462
|
+
primary_goal: `QC repair worker for cluster ${input.clusterId}, round ${input.round}, packet ${input.packetId}. ` +
|
|
463
|
+
`Apply minimal targeted repairs to findings within the allowed scope. Root cause: ${input.rootCauseHint}`,
|
|
464
|
+
steps,
|
|
465
|
+
allowed_scope: input.allowedScope,
|
|
466
|
+
validation_commands: input.validationCommands,
|
|
467
|
+
},
|
|
468
|
+
lifecycle: defaultLifecycle(input.maxConcurrentWorkers ?? 1, 'commit-and-exit'),
|
|
469
|
+
return_contract: exports.REPAIR_RETURN_CONTRACT,
|
|
470
|
+
prompt_mode: 'full',
|
|
471
|
+
prompt_metrics: { mode: 'full', char_count: 0, estimated_tokens: 0 },
|
|
472
|
+
role_context: roleContextForWorkerRole('repair'),
|
|
473
|
+
routing_context: {
|
|
474
|
+
task_type: "repair",
|
|
475
|
+
required_capabilities: ["repair"],
|
|
476
|
+
},
|
|
477
|
+
prohibited_write_paths: [
|
|
478
|
+
...exports.WORKER_PROHIBITED_WRITE_PATHS,
|
|
479
|
+
...input.prohibitedScope,
|
|
480
|
+
],
|
|
481
|
+
result_file_contract: {
|
|
482
|
+
result_file: input.resultFile,
|
|
483
|
+
result_required_fields: Object.fromEntries([
|
|
484
|
+
['run_id', input.runId],
|
|
485
|
+
['cluster_id', input.clusterId],
|
|
486
|
+
['child_id', input.packetId],
|
|
487
|
+
...exports.REPAIR_RETURN_CONTRACT.filter((key) => key !== 'child_id').map((key) => [key, `<${key}>`]),
|
|
488
|
+
]),
|
|
489
|
+
},
|
|
490
|
+
context: {
|
|
491
|
+
branch: input.branch,
|
|
492
|
+
worker_role: 'repair',
|
|
493
|
+
repair_round: input.round,
|
|
494
|
+
repair_packet_id: input.packetId,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
package/dist/qc/artifacts.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.getQcArtifactDir = getQcArtifactDir;
|
|
|
37
37
|
exports.writeQcArtifact = writeQcArtifact;
|
|
38
38
|
exports.readQcArtifact = readQcArtifact;
|
|
39
39
|
exports.listQcArtifactIds = listQcArtifactIds;
|
|
40
|
+
exports.validateQcArtifactPointers = validateQcArtifactPointers;
|
|
40
41
|
const node_fs_1 = require("node:fs");
|
|
41
42
|
const path = __importStar(require("node:path"));
|
|
42
43
|
const schemas_js_1 = require("./schemas.js");
|
|
@@ -115,3 +116,29 @@ function listQcArtifactIds(clusterId, repoRoot) {
|
|
|
115
116
|
throw error;
|
|
116
117
|
}
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Validate that the artifacts referenced by QC run pointers still exist.
|
|
121
|
+
* Returns a list of missing primary artifacts and unavailable raw audit
|
|
122
|
+
* artifacts so callers can warn, prune, or mark pointers consistently.
|
|
123
|
+
*/
|
|
124
|
+
function validateQcArtifactPointers(qcRuns) {
|
|
125
|
+
const missing = [];
|
|
126
|
+
const unavailable = [];
|
|
127
|
+
if (!qcRuns) {
|
|
128
|
+
return { ok: true, missing, unavailable };
|
|
129
|
+
}
|
|
130
|
+
for (const pointer of Object.values(qcRuns)) {
|
|
131
|
+
if (!(0, node_fs_1.existsSync)(pointer.artifact_path)) {
|
|
132
|
+
missing.push(pointer.artifact_path);
|
|
133
|
+
}
|
|
134
|
+
for (const rawPath of pointer.raw_artifact_paths ?? []) {
|
|
135
|
+
if (!(0, node_fs_1.existsSync)(rawPath)) {
|
|
136
|
+
unavailable.push(rawPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (pointer.provider_attempt_artifact_path && !(0, node_fs_1.existsSync)(pointer.provider_attempt_artifact_path)) {
|
|
140
|
+
unavailable.push(pointer.provider_attempt_artifact_path);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { ok: missing.length === 0 && unavailable.length === 0, missing, unavailable };
|
|
144
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TEST_CONFIG = void 0;
|
|
4
|
+
exports.makeFinding = makeFinding;
|
|
5
|
+
exports.makeResult = makeResult;
|
|
6
|
+
exports.makeSameFileFindings = makeSameFileFindings;
|
|
7
|
+
exports.makeCrossFileSubsystemFindings = makeCrossFileSubsystemFindings;
|
|
8
|
+
exports.makeOverlappingScopeFindings = makeOverlappingScopeFindings;
|
|
9
|
+
exports.makeHighRiskFindings = makeHighRiskFindings;
|
|
10
|
+
exports.makeLowConfidenceBroadFindings = makeLowConfidenceBroadFindings;
|
|
11
|
+
exports.DEFAULT_TEST_CONFIG = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
severityThresholds: { block: "high", repair: "medium", followUp: "low" },
|
|
14
|
+
};
|
|
15
|
+
function makeFinding(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
findingId: `f-${Math.random().toString(36).slice(2, 8)}`,
|
|
18
|
+
severity: "medium",
|
|
19
|
+
category: "style",
|
|
20
|
+
title: "Test finding",
|
|
21
|
+
fixAvailable: true,
|
|
22
|
+
autofixEligible: false,
|
|
23
|
+
attribution: { confidence: "high", reason: "changed-file-owner", childId: "POL-472" },
|
|
24
|
+
status: "open",
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function makeResult(overrides = {}) {
|
|
29
|
+
const now = new Date().toISOString();
|
|
30
|
+
return {
|
|
31
|
+
schemaVersion: "1.0",
|
|
32
|
+
qcRunId: `qc-run-${Math.random().toString(36).slice(2, 8)}`,
|
|
33
|
+
runId: "run-1",
|
|
34
|
+
clusterId: "POL-TEST",
|
|
35
|
+
trigger: "completed-cluster",
|
|
36
|
+
provider: "coderabbit",
|
|
37
|
+
providerMode: "local",
|
|
38
|
+
startedAt: now,
|
|
39
|
+
completedAt: now,
|
|
40
|
+
status: "findings",
|
|
41
|
+
findings: [],
|
|
42
|
+
rawArtifactPaths: [],
|
|
43
|
+
parserVersion: "coderabbit-1.0",
|
|
44
|
+
policyDecision: {
|
|
45
|
+
blocksDelivery: false,
|
|
46
|
+
requiresOperatorReview: false,
|
|
47
|
+
routedToRepair: false,
|
|
48
|
+
summary: "ok",
|
|
49
|
+
},
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function makeSameFileFindings() {
|
|
54
|
+
return [
|
|
55
|
+
makeFinding({
|
|
56
|
+
findingId: "f-same-1",
|
|
57
|
+
filePath: "src/auth/login.ts",
|
|
58
|
+
category: "style",
|
|
59
|
+
severity: "medium",
|
|
60
|
+
range: { startLine: 10, endLine: 15 },
|
|
61
|
+
}),
|
|
62
|
+
makeFinding({
|
|
63
|
+
findingId: "f-same-2",
|
|
64
|
+
filePath: "src/auth/login.ts",
|
|
65
|
+
category: "type-safety",
|
|
66
|
+
severity: "medium",
|
|
67
|
+
range: { startLine: 30, endLine: 35 },
|
|
68
|
+
}),
|
|
69
|
+
makeFinding({
|
|
70
|
+
findingId: "f-same-3",
|
|
71
|
+
filePath: "src/auth/login.ts",
|
|
72
|
+
category: "style",
|
|
73
|
+
severity: "medium",
|
|
74
|
+
range: { startLine: 12, endLine: 16 },
|
|
75
|
+
}),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function makeCrossFileSubsystemFindings() {
|
|
79
|
+
return [
|
|
80
|
+
makeFinding({
|
|
81
|
+
findingId: "f-sub-a",
|
|
82
|
+
filePath: "src/qc/compiler.ts",
|
|
83
|
+
category: "error-handling",
|
|
84
|
+
severity: "medium",
|
|
85
|
+
range: { startLine: 5, endLine: 8 },
|
|
86
|
+
}),
|
|
87
|
+
makeFinding({
|
|
88
|
+
findingId: "f-sub-b",
|
|
89
|
+
filePath: "src/qc/runner.ts",
|
|
90
|
+
category: "error-handling",
|
|
91
|
+
severity: "medium",
|
|
92
|
+
range: { startLine: 20, endLine: 24 },
|
|
93
|
+
}),
|
|
94
|
+
makeFinding({
|
|
95
|
+
findingId: "f-sub-c",
|
|
96
|
+
filePath: "src/loop/worker.ts",
|
|
97
|
+
category: "error-handling",
|
|
98
|
+
severity: "medium",
|
|
99
|
+
range: { startLine: 7, endLine: 11 },
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
function makeOverlappingScopeFindings() {
|
|
104
|
+
return [
|
|
105
|
+
makeFinding({
|
|
106
|
+
findingId: "f-overlap-1",
|
|
107
|
+
filePath: "src/core/state.ts",
|
|
108
|
+
category: "race-condition",
|
|
109
|
+
severity: "high",
|
|
110
|
+
range: { startLine: 40, endLine: 45 },
|
|
111
|
+
}),
|
|
112
|
+
makeFinding({
|
|
113
|
+
findingId: "f-overlap-2",
|
|
114
|
+
filePath: "src/core/state.ts",
|
|
115
|
+
category: "race-condition",
|
|
116
|
+
severity: "high",
|
|
117
|
+
range: { startLine: 42, endLine: 48 },
|
|
118
|
+
}),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
function makeHighRiskFindings() {
|
|
122
|
+
return [
|
|
123
|
+
makeFinding({
|
|
124
|
+
findingId: "f-security-1",
|
|
125
|
+
filePath: "src/auth/token.ts",
|
|
126
|
+
category: "security",
|
|
127
|
+
severity: "high",
|
|
128
|
+
range: { startLine: 1, endLine: 5 },
|
|
129
|
+
}),
|
|
130
|
+
makeFinding({
|
|
131
|
+
findingId: "f-security-2",
|
|
132
|
+
filePath: "src/auth/token.ts",
|
|
133
|
+
category: "auth",
|
|
134
|
+
severity: "medium",
|
|
135
|
+
range: { startLine: 10, endLine: 14 },
|
|
136
|
+
}),
|
|
137
|
+
makeFinding({
|
|
138
|
+
findingId: "f-migration-1",
|
|
139
|
+
filePath: "src/db/migrate.ts",
|
|
140
|
+
category: "migration",
|
|
141
|
+
severity: "medium",
|
|
142
|
+
range: { startLine: 3, endLine: 7 },
|
|
143
|
+
}),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
function makeLowConfidenceBroadFindings() {
|
|
147
|
+
return [
|
|
148
|
+
makeFinding({
|
|
149
|
+
findingId: "f-broad-1",
|
|
150
|
+
filePath: undefined,
|
|
151
|
+
category: "architecture",
|
|
152
|
+
severity: "medium",
|
|
153
|
+
attribution: { confidence: "low", reason: "provider-uncertain" },
|
|
154
|
+
}),
|
|
155
|
+
makeFinding({
|
|
156
|
+
findingId: "f-broad-2",
|
|
157
|
+
filePath: undefined,
|
|
158
|
+
category: "architecture",
|
|
159
|
+
severity: "low",
|
|
160
|
+
attribution: { confidence: "unattributed", reason: "unattributed" },
|
|
161
|
+
}),
|
|
162
|
+
makeFinding({
|
|
163
|
+
findingId: "f-preexisting-1",
|
|
164
|
+
filePath: "src/legacy/old.ts",
|
|
165
|
+
category: "tech-debt",
|
|
166
|
+
severity: "low",
|
|
167
|
+
attribution: { confidence: "medium", reason: "pre-existing" },
|
|
168
|
+
}),
|
|
169
|
+
];
|
|
170
|
+
}
|
package/dist/qc/index.js
CHANGED
|
@@ -28,3 +28,5 @@ __exportStar(require("./registry.js"), exports);
|
|
|
28
28
|
__exportStar(require("./attribution.js"), exports);
|
|
29
29
|
__exportStar(require("./autofix.js"), exports);
|
|
30
30
|
__exportStar(require("./routing.js"), exports);
|
|
31
|
+
__exportStar(require("./repair-packets.js"), exports);
|
|
32
|
+
__exportStar(require("./repair-loop.js"), exports);
|
package/dist/qc/orchestration.js
CHANGED
|
@@ -20,7 +20,7 @@ const attribution_js_1 = require("./attribution.js");
|
|
|
20
20
|
const autofix_js_1 = require("./autofix.js");
|
|
21
21
|
const routing_js_1 = require("./routing.js");
|
|
22
22
|
function buildAttributionContext(options) {
|
|
23
|
-
const { repoRoot, clusterId, branch, state } = options;
|
|
23
|
+
const { repoRoot, clusterId, branch, baseRef, state } = options;
|
|
24
24
|
const dispatchRecords = {};
|
|
25
25
|
if (state?.open_children_meta) {
|
|
26
26
|
for (const [childId, meta] of Object.entries(state.open_children_meta)) {
|
|
@@ -31,7 +31,7 @@ function buildAttributionContext(options) {
|
|
|
31
31
|
}
|
|
32
32
|
return {
|
|
33
33
|
repoRoot,
|
|
34
|
-
baseBranch: branch ?? state?.branch ?? "main",
|
|
34
|
+
baseBranch: baseRef ?? branch ?? state?.branch ?? "main",
|
|
35
35
|
completedResults: state?.completed_children_results,
|
|
36
36
|
dispatchRecords,
|
|
37
37
|
clusterState: repoRoot ? ((0, store_js_1.readClusterStateSync)(clusterId, repoRoot) ?? undefined) : undefined,
|
|
@@ -99,6 +99,7 @@ async function runQcAtTrigger(options) {
|
|
|
99
99
|
clusterId,
|
|
100
100
|
runId,
|
|
101
101
|
...(trigger === "pr" ? { prUrl } : { branch }),
|
|
102
|
+
...(options.baseRef ? { baseRef: options.baseRef } : {}),
|
|
102
103
|
};
|
|
103
104
|
try {
|
|
104
105
|
const rawResult = await (0, runner_js_1.executeQcProvider)(provider, scope, {
|
|
@@ -108,6 +109,8 @@ async function runQcAtTrigger(options) {
|
|
|
108
109
|
branch,
|
|
109
110
|
telemetryFile,
|
|
110
111
|
timeoutMs,
|
|
112
|
+
config,
|
|
113
|
+
registry,
|
|
111
114
|
});
|
|
112
115
|
const result = applyAttributionAndRouting(rawResult, config, attributionContext, ownership, routeName);
|
|
113
116
|
try {
|
package/dist/qc/policy.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.decideQcAction = decideQcAction;
|
|
12
|
+
exports.decideProviderFailureAction = decideProviderFailureAction;
|
|
12
13
|
exports.computeQcPolicyDecision = computeQcPolicyDecision;
|
|
13
14
|
const severity_js_1 = require("./severity.js");
|
|
14
15
|
/**
|
|
@@ -16,6 +17,7 @@ const severity_js_1 = require("./severity.js");
|
|
|
16
17
|
*
|
|
17
18
|
* Rules:
|
|
18
19
|
* - passed / skipped -> pass (no effect on delivery)
|
|
20
|
+
* - allProvidersFailed -> block
|
|
19
21
|
* - repairRouting: "block" -> block finalize/delivery
|
|
20
22
|
* - repairRouting: "log" -> pass (passive report only)
|
|
21
23
|
* - repairRouting: "route" / "follow-up" -> create follow-up work, but high/critical
|
|
@@ -27,6 +29,9 @@ function decideQcAction(result, config) {
|
|
|
27
29
|
if (result.status === "passed" || result.status === "skipped") {
|
|
28
30
|
return "pass";
|
|
29
31
|
}
|
|
32
|
+
if (result.allProvidersFailed) {
|
|
33
|
+
return "block";
|
|
34
|
+
}
|
|
30
35
|
const routing = config.repairRouting ?? "route";
|
|
31
36
|
if (routing === "block") {
|
|
32
37
|
return "block";
|
|
@@ -41,19 +46,41 @@ function decideQcAction(result, config) {
|
|
|
41
46
|
}
|
|
42
47
|
return "follow-up";
|
|
43
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Decide what the runtime should do with a classified provider failure.
|
|
51
|
+
*
|
|
52
|
+
* Maps a failure reason to the per-provider failure policy from configuration.
|
|
53
|
+
* Reasons without a dedicated policy key default to "fail" so behavior is
|
|
54
|
+
* deterministic even for newly-classified failures.
|
|
55
|
+
*/
|
|
56
|
+
function decideProviderFailureAction(reason, policy) {
|
|
57
|
+
switch (reason) {
|
|
58
|
+
case "timeout":
|
|
59
|
+
return policy?.timeout ?? "fail";
|
|
60
|
+
case "parse-failed":
|
|
61
|
+
return policy?.parseFailure ?? "fail";
|
|
62
|
+
default:
|
|
63
|
+
return "fail";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
44
66
|
/**
|
|
45
67
|
* Compute the detailed policy decision for a QC result after attribution and
|
|
46
68
|
* repair routing have been applied.
|
|
47
69
|
*/
|
|
48
70
|
function computeQcPolicyDecision(result, config) {
|
|
49
71
|
const blockThreshold = config.severityThresholds?.block ?? "high";
|
|
50
|
-
const blocksDelivery = result.
|
|
51
|
-
(f
|
|
52
|
-
|
|
72
|
+
const blocksDelivery = result.allProvidersFailed ||
|
|
73
|
+
result.findings.some((f) => (0, severity_js_1.compareSeverity)(f.severity, blockThreshold) >= 0 &&
|
|
74
|
+
(f.routingDecision === "operator-review" || f.routingDecision === undefined));
|
|
75
|
+
const requiresOperatorReview = result.allProvidersFailed ||
|
|
76
|
+
result.status === "failed" ||
|
|
53
77
|
result.status === "blocked" ||
|
|
54
78
|
result.findings.some((f) => f.routingDecision === "operator-review");
|
|
55
79
|
const routedToRepair = result.findings.some((f) => f.routingDecision === "original-worker" || f.routingDecision === "repair-worker");
|
|
56
80
|
const summaryParts = [];
|
|
81
|
+
if (result.allProvidersFailed) {
|
|
82
|
+
summaryParts.push("all providers failed");
|
|
83
|
+
}
|
|
57
84
|
summaryParts.push(`${result.findings.length} findings`);
|
|
58
85
|
if (requiresOperatorReview) {
|
|
59
86
|
summaryParts.push("operator review required");
|
|
@@ -16,6 +16,79 @@ function pickString(...candidates) {
|
|
|
16
16
|
}
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
|
+
const FINDING_LOCATION_KEYS = ["file", "filePath", "path"];
|
|
20
|
+
const FINDING_REVIEW_KEYS = [
|
|
21
|
+
"severity",
|
|
22
|
+
"message",
|
|
23
|
+
"title",
|
|
24
|
+
"summary",
|
|
25
|
+
"description",
|
|
26
|
+
"body",
|
|
27
|
+
"rule",
|
|
28
|
+
"category",
|
|
29
|
+
"suggestion",
|
|
30
|
+
"suggestedAction",
|
|
31
|
+
"fix",
|
|
32
|
+
"providerFindingId",
|
|
33
|
+
"id",
|
|
34
|
+
"findingId",
|
|
35
|
+
];
|
|
36
|
+
const PROGRESS_SHAPE_KEYS = new Set(["event", "progress", "heartbeat", "complete", "review_context"]);
|
|
37
|
+
const PROGRESS_TYPE_VALUES = new Set([
|
|
38
|
+
"progress",
|
|
39
|
+
"status",
|
|
40
|
+
"heartbeat",
|
|
41
|
+
"complete",
|
|
42
|
+
"review_context",
|
|
43
|
+
"reviewcontext",
|
|
44
|
+
]);
|
|
45
|
+
const PROGRESS_STATUS_VALUES = new Set([
|
|
46
|
+
"in_progress",
|
|
47
|
+
"running",
|
|
48
|
+
"pending",
|
|
49
|
+
"complete",
|
|
50
|
+
"completed",
|
|
51
|
+
"done",
|
|
52
|
+
"heartbeat",
|
|
53
|
+
"ok",
|
|
54
|
+
"success",
|
|
55
|
+
]);
|
|
56
|
+
function hasFindingLocation(record) {
|
|
57
|
+
return FINDING_LOCATION_KEYS.some((key) => record[key] !== undefined);
|
|
58
|
+
}
|
|
59
|
+
function hasFindingReviewContent(record) {
|
|
60
|
+
return FINDING_REVIEW_KEYS.some((key) => record[key] !== undefined);
|
|
61
|
+
}
|
|
62
|
+
function isProgressRecord(record) {
|
|
63
|
+
// Check progress/status indicators FIRST before the generic finding-content guard
|
|
64
|
+
const keys = Object.keys(record);
|
|
65
|
+
if (keys.length === 0)
|
|
66
|
+
return false;
|
|
67
|
+
if (keys.some((key) => PROGRESS_SHAPE_KEYS.has(key)))
|
|
68
|
+
return true;
|
|
69
|
+
if (typeof record.type === "string" && PROGRESS_TYPE_VALUES.has(record.type.toLowerCase()))
|
|
70
|
+
return true;
|
|
71
|
+
if (typeof record.status === "string" && PROGRESS_STATUS_VALUES.has(record.status.toLowerCase()))
|
|
72
|
+
return true;
|
|
73
|
+
// Status-only records with category="status" are progress records even if they have message/title fields
|
|
74
|
+
if (typeof record.category === "string" && record.category.toLowerCase() === "status")
|
|
75
|
+
return true;
|
|
76
|
+
// Only reject as progress if it has both location AND review content (true finding shape)
|
|
77
|
+
if (hasFindingLocation(record) && hasFindingReviewContent(record)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function isActionableFinding(record) {
|
|
83
|
+
if (isProgressRecord(record))
|
|
84
|
+
return false;
|
|
85
|
+
return hasFindingLocation(record) || hasFindingReviewContent(record);
|
|
86
|
+
}
|
|
87
|
+
function makeUnusableOutputError(message) {
|
|
88
|
+
const err = new Error(message);
|
|
89
|
+
err.qcFailureReason = "unusable-output";
|
|
90
|
+
return err;
|
|
91
|
+
}
|
|
19
92
|
function buildRange(raw) {
|
|
20
93
|
const startLine = coerceNumber(raw.startLine ?? raw.line) ?? 1;
|
|
21
94
|
const endLine = coerceNumber(raw.endLine);
|
|
@@ -37,23 +110,27 @@ function parseFindingsFromPayload(payload) {
|
|
|
37
110
|
}
|
|
38
111
|
const record = payload;
|
|
39
112
|
if (Array.isArray(record.findings)) {
|
|
40
|
-
return record.findings;
|
|
113
|
+
return record.findings.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
41
114
|
}
|
|
42
115
|
if (Array.isArray(record.issues)) {
|
|
43
|
-
return record.issues;
|
|
116
|
+
return record.issues.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
44
117
|
}
|
|
45
118
|
if (Array.isArray(record.results)) {
|
|
46
|
-
return record.results;
|
|
119
|
+
return record.results.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
47
120
|
}
|
|
48
121
|
// Single finding wrapped in an object
|
|
49
|
-
if (record
|
|
50
|
-
record.message !== undefined ||
|
|
51
|
-
record.title !== undefined) {
|
|
122
|
+
if (isActionableFinding(record)) {
|
|
52
123
|
return [record];
|
|
53
124
|
}
|
|
54
125
|
return [];
|
|
55
126
|
}
|
|
56
|
-
function parseReport(output) {
|
|
127
|
+
function parseReport(output, format, parser) {
|
|
128
|
+
if (parser && parser !== "coderabbit") {
|
|
129
|
+
throw new Error(`Unsupported parser for CodeRabbit provider: ${parser}`);
|
|
130
|
+
}
|
|
131
|
+
if (format === "sarif") {
|
|
132
|
+
throw new Error("SARIF output format is not supported by the CodeRabbit provider");
|
|
133
|
+
}
|
|
57
134
|
const text = "stdout" in output && typeof output.stdout === "string" ? output.stdout : "";
|
|
58
135
|
const data = "data" in output ? output.data : undefined;
|
|
59
136
|
if (data !== undefined && typeof data === "object" && data !== null) {
|
|
@@ -64,30 +141,61 @@ function parseReport(output) {
|
|
|
64
141
|
...(typeof record.prUrl === "string" ? { prUrl: record.prUrl } : {}),
|
|
65
142
|
};
|
|
66
143
|
}
|
|
67
|
-
if (text.trim().
|
|
144
|
+
if (text.trim().length === 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (format === "json" || text.trim().startsWith("{") || text.trim().startsWith("[")) {
|
|
68
148
|
try {
|
|
69
149
|
const parsed = JSON.parse(text);
|
|
70
150
|
if (Array.isArray(parsed)) {
|
|
71
|
-
|
|
151
|
+
const findings = parsed.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
152
|
+
if (findings.length === 0 && parsed.length > 0) {
|
|
153
|
+
const progressCount = parsed.filter((item) => typeof item === "object" && item !== null && isProgressRecord(item)).length;
|
|
154
|
+
if (progressCount > 0) {
|
|
155
|
+
throw makeUnusableOutputError(`CodeRabbit output contained only progress/status/heartbeat records (${progressCount} items)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { findings };
|
|
159
|
+
}
|
|
160
|
+
const findings = parseFindingsFromPayload(parsed);
|
|
161
|
+
if (findings.length === 0 && isProgressRecord(parsed)) {
|
|
162
|
+
throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
|
|
72
163
|
}
|
|
73
164
|
return {
|
|
74
|
-
findings
|
|
165
|
+
findings,
|
|
75
166
|
...(typeof parsed?.prUrl === "string"
|
|
76
167
|
? { prUrl: parsed.prUrl }
|
|
77
168
|
: {}),
|
|
78
169
|
};
|
|
79
170
|
}
|
|
80
|
-
catch {
|
|
81
|
-
//
|
|
171
|
+
catch (jsonError) {
|
|
172
|
+
// When the format is explicitly JSON, a parse error is a real failure.
|
|
173
|
+
// Otherwise, treat the text as JSONL and fall through to line scanning.
|
|
174
|
+
if (format === "json") {
|
|
175
|
+
throw jsonError;
|
|
176
|
+
}
|
|
82
177
|
}
|
|
83
178
|
}
|
|
84
|
-
//
|
|
179
|
+
// JSONL or generic line scanning: one finding per line
|
|
85
180
|
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
86
181
|
const lineFindings = [];
|
|
182
|
+
let progressLineCount = 0;
|
|
183
|
+
let parsedLineCount = 0;
|
|
87
184
|
for (const line of lines) {
|
|
88
185
|
try {
|
|
89
186
|
const parsed = JSON.parse(line);
|
|
90
|
-
|
|
187
|
+
parsedLineCount++;
|
|
188
|
+
if (!parsed || typeof parsed !== "object") {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const record = parsed;
|
|
192
|
+
if (isProgressRecord(record)) {
|
|
193
|
+
progressLineCount++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (isActionableFinding(record)) {
|
|
197
|
+
lineFindings.push(parsed);
|
|
198
|
+
}
|
|
91
199
|
}
|
|
92
200
|
catch {
|
|
93
201
|
// Ignore unparseable lines.
|
|
@@ -96,7 +204,13 @@ function parseReport(output) {
|
|
|
96
204
|
if (lineFindings.length > 0) {
|
|
97
205
|
return { findings: lineFindings };
|
|
98
206
|
}
|
|
99
|
-
|
|
207
|
+
if (progressLineCount > 0) {
|
|
208
|
+
throw makeUnusableOutputError(`CodeRabbit output contained only progress/status/heartbeat records (${progressLineCount} lines)`);
|
|
209
|
+
}
|
|
210
|
+
if (parsedLineCount > 0) {
|
|
211
|
+
throw new Error("CodeRabbit output contained no actionable findings");
|
|
212
|
+
}
|
|
213
|
+
throw new Error("CodeRabbit output could not be parsed as JSON, JSONL, or metrics payload");
|
|
100
214
|
}
|
|
101
215
|
function normalizeFinding(raw, index) {
|
|
102
216
|
const severityLabel = pickString(raw.severity, raw.level) ?? "info";
|
|
@@ -179,6 +293,7 @@ function buildResultFromReport(report, output, mode, providerFailed = false) {
|
|
|
179
293
|
* No external network calls are made here.
|
|
180
294
|
*/
|
|
181
295
|
class CodeRabbitQcProvider {
|
|
296
|
+
config;
|
|
182
297
|
name = "coderabbit";
|
|
183
298
|
supportedModes = ["local", "pr", "metrics-import"];
|
|
184
299
|
capabilities = [
|
|
@@ -188,22 +303,42 @@ class CodeRabbitQcProvider {
|
|
|
188
303
|
"auto-fix",
|
|
189
304
|
"metrics-import",
|
|
190
305
|
];
|
|
306
|
+
constructor(config) {
|
|
307
|
+
this.config = config;
|
|
308
|
+
}
|
|
191
309
|
canReview(scope) {
|
|
192
310
|
if (scope.prUrl)
|
|
193
311
|
return true;
|
|
194
312
|
return Boolean(scope.branch);
|
|
195
313
|
}
|
|
196
314
|
buildReviewCommand(scope) {
|
|
315
|
+
const execution = this.config?.execution;
|
|
316
|
+
if (execution) {
|
|
317
|
+
const args = execution.args ? [...execution.args] : [];
|
|
318
|
+
if (execution.configPath) {
|
|
319
|
+
args.push("--config", execution.configPath);
|
|
320
|
+
}
|
|
321
|
+
const baseRef = scope.baseRef ?? scope.branch ?? "main";
|
|
322
|
+
if (scope.prUrl) {
|
|
323
|
+
args.push("--pr-url", scope.prUrl);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
args.push("--base", baseRef);
|
|
327
|
+
}
|
|
328
|
+
return { command: execution.command, args };
|
|
329
|
+
}
|
|
197
330
|
if (scope.prUrl) {
|
|
198
331
|
return { command: "coderabbit", args: ["review", "--agent", "--pr-url", scope.prUrl] };
|
|
199
332
|
}
|
|
333
|
+
const baseRef = scope.baseRef ?? scope.branch ?? "main";
|
|
200
334
|
return {
|
|
201
335
|
command: "coderabbit",
|
|
202
|
-
args: ["review", "--agent", "--
|
|
336
|
+
args: ["review", "--agent", "--base", baseRef],
|
|
203
337
|
};
|
|
204
338
|
}
|
|
205
339
|
parse(output) {
|
|
206
|
-
const
|
|
340
|
+
const outputConfig = this.config?.execution?.output;
|
|
341
|
+
const report = parseReport(output, outputConfig?.format, outputConfig?.parser);
|
|
207
342
|
return buildResultFromReport(report, output, "local", output.exitCode !== 0);
|
|
208
343
|
}
|
|
209
344
|
importMetrics(payload) {
|