@lsctech/polaris 0.5.6 → 0.5.7
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/proposal.js +5 -0
- package/dist/autoresearch/score.js +174 -2
- package/dist/autoresearch/sol-evidence-loader.js +40 -3
- package/dist/autoresearch/sol-recommendations.js +82 -0
- package/dist/autoresearch/sol-report.js +44 -0
- package/dist/autoresearch/sol-scorer.js +45 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/validator.js +157 -0
- package/dist/finalize/index.js +1 -1
- package/dist/loop/parent.js +156 -5
- package/dist/loop/worker-packet.js +75 -1
- package/dist/qc/fixtures/repair-packets.js +170 -0
- package/dist/qc/index.js +2 -0
- package/dist/qc/orchestration.js +2 -0
- package/dist/qc/policy.js +30 -3
- package/dist/qc/providers/coderabbit.js +39 -7
- package/dist/qc/registry.js +36 -3
- package/dist/qc/repair-loop.js +423 -0
- package/dist/qc/repair-packets.js +420 -0
- package/dist/qc/runner.js +320 -37
- package/dist/qc/schemas.js +78 -1
- package/package.json +1 -1
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");
|
|
@@ -53,7 +53,13 @@ function parseFindingsFromPayload(payload) {
|
|
|
53
53
|
}
|
|
54
54
|
return [];
|
|
55
55
|
}
|
|
56
|
-
function parseReport(output) {
|
|
56
|
+
function parseReport(output, format, parser) {
|
|
57
|
+
if (parser && parser !== "coderabbit") {
|
|
58
|
+
throw new Error(`Unsupported parser for CodeRabbit provider: ${parser}`);
|
|
59
|
+
}
|
|
60
|
+
if (format === "sarif") {
|
|
61
|
+
throw new Error("SARIF output format is not supported by the CodeRabbit provider");
|
|
62
|
+
}
|
|
57
63
|
const text = "stdout" in output && typeof output.stdout === "string" ? output.stdout : "";
|
|
58
64
|
const data = "data" in output ? output.data : undefined;
|
|
59
65
|
if (data !== undefined && typeof data === "object" && data !== null) {
|
|
@@ -64,7 +70,10 @@ function parseReport(output) {
|
|
|
64
70
|
...(typeof record.prUrl === "string" ? { prUrl: record.prUrl } : {}),
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
|
-
if (text.trim().
|
|
73
|
+
if (text.trim().length === 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (format === "json" || text.trim().startsWith("{") || text.trim().startsWith("[")) {
|
|
68
77
|
try {
|
|
69
78
|
const parsed = JSON.parse(text);
|
|
70
79
|
if (Array.isArray(parsed)) {
|
|
@@ -77,11 +86,15 @@ function parseReport(output) {
|
|
|
77
86
|
: {}),
|
|
78
87
|
};
|
|
79
88
|
}
|
|
80
|
-
catch {
|
|
81
|
-
//
|
|
89
|
+
catch (jsonError) {
|
|
90
|
+
// When the format is explicitly JSON, a parse error is a real failure.
|
|
91
|
+
// Otherwise, treat the text as JSONL and fall through to line scanning.
|
|
92
|
+
if (format === "json") {
|
|
93
|
+
throw jsonError;
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
|
-
//
|
|
97
|
+
// JSONL or generic line scanning: one finding per line
|
|
85
98
|
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
86
99
|
const lineFindings = [];
|
|
87
100
|
for (const line of lines) {
|
|
@@ -96,7 +109,7 @@ function parseReport(output) {
|
|
|
96
109
|
if (lineFindings.length > 0) {
|
|
97
110
|
return { findings: lineFindings };
|
|
98
111
|
}
|
|
99
|
-
|
|
112
|
+
throw new Error("CodeRabbit output could not be parsed as JSON, JSONL, or metrics payload");
|
|
100
113
|
}
|
|
101
114
|
function normalizeFinding(raw, index) {
|
|
102
115
|
const severityLabel = pickString(raw.severity, raw.level) ?? "info";
|
|
@@ -179,6 +192,7 @@ function buildResultFromReport(report, output, mode, providerFailed = false) {
|
|
|
179
192
|
* No external network calls are made here.
|
|
180
193
|
*/
|
|
181
194
|
class CodeRabbitQcProvider {
|
|
195
|
+
config;
|
|
182
196
|
name = "coderabbit";
|
|
183
197
|
supportedModes = ["local", "pr", "metrics-import"];
|
|
184
198
|
capabilities = [
|
|
@@ -188,12 +202,29 @@ class CodeRabbitQcProvider {
|
|
|
188
202
|
"auto-fix",
|
|
189
203
|
"metrics-import",
|
|
190
204
|
];
|
|
205
|
+
constructor(config) {
|
|
206
|
+
this.config = config;
|
|
207
|
+
}
|
|
191
208
|
canReview(scope) {
|
|
192
209
|
if (scope.prUrl)
|
|
193
210
|
return true;
|
|
194
211
|
return Boolean(scope.branch);
|
|
195
212
|
}
|
|
196
213
|
buildReviewCommand(scope) {
|
|
214
|
+
const execution = this.config?.execution;
|
|
215
|
+
if (execution) {
|
|
216
|
+
const args = execution.args ? [...execution.args] : [];
|
|
217
|
+
if (execution.configPath) {
|
|
218
|
+
args.push("--config", execution.configPath);
|
|
219
|
+
}
|
|
220
|
+
if (scope.prUrl) {
|
|
221
|
+
args.push("--pr-url", scope.prUrl);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
args.push("--branch", scope.branch ?? "HEAD");
|
|
225
|
+
}
|
|
226
|
+
return { command: execution.command, args };
|
|
227
|
+
}
|
|
197
228
|
if (scope.prUrl) {
|
|
198
229
|
return { command: "coderabbit", args: ["review", "--agent", "--pr-url", scope.prUrl] };
|
|
199
230
|
}
|
|
@@ -203,7 +234,8 @@ class CodeRabbitQcProvider {
|
|
|
203
234
|
};
|
|
204
235
|
}
|
|
205
236
|
parse(output) {
|
|
206
|
-
const
|
|
237
|
+
const outputConfig = this.config?.execution?.output;
|
|
238
|
+
const report = parseReport(output, outputConfig?.format, outputConfig?.parser);
|
|
207
239
|
return buildResultFromReport(report, output, "local", output.exitCode !== 0);
|
|
208
240
|
}
|
|
209
241
|
importMetrics(payload) {
|
package/dist/qc/registry.js
CHANGED
|
@@ -1,16 +1,49 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* QC provider registry builders.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* `createDefaultQcRegistry` returns the static built-in registry for backward
|
|
6
|
+
* compatibility. `createQcRegistry` builds a registry from polaris.config.json
|
|
7
|
+
* qc.providers, wiring provider-agnostic command/output config into the
|
|
8
|
+
* matching built-in adapters.
|
|
7
9
|
*/
|
|
8
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
11
|
exports.createDefaultQcRegistry = createDefaultQcRegistry;
|
|
12
|
+
exports.createQcRegistry = createQcRegistry;
|
|
10
13
|
const provider_js_1 = require("./provider.js");
|
|
11
14
|
const coderabbit_js_1 = require("./providers/coderabbit.js");
|
|
15
|
+
function createProvider(name, config) {
|
|
16
|
+
// Only CodeRabbit is supported as a concrete provider in this child.
|
|
17
|
+
// Future providers can be registered here by name.
|
|
18
|
+
if (name === "coderabbit") {
|
|
19
|
+
return new coderabbit_js_1.CodeRabbitQcProvider(config);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
12
23
|
function createDefaultQcRegistry() {
|
|
13
24
|
const registry = new provider_js_1.QcProviderRegistry();
|
|
14
25
|
registry.register(new coderabbit_js_1.CodeRabbitQcProvider());
|
|
15
26
|
return registry;
|
|
16
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a registry from configured QC providers.
|
|
30
|
+
*
|
|
31
|
+
* Disabled providers (enabled: false) are skipped. Unknown provider names are
|
|
32
|
+
* skipped; callers should validate config before dispatch and may report
|
|
33
|
+
* unknown providers as blockers at runtime.
|
|
34
|
+
*/
|
|
35
|
+
function createQcRegistry(config) {
|
|
36
|
+
const registry = new provider_js_1.QcProviderRegistry();
|
|
37
|
+
if (!config?.enabled) {
|
|
38
|
+
return registry;
|
|
39
|
+
}
|
|
40
|
+
for (const [name, providerConfig] of Object.entries(config.providers ?? {})) {
|
|
41
|
+
if (providerConfig.enabled === false)
|
|
42
|
+
continue;
|
|
43
|
+
const provider = createProvider(name, providerConfig);
|
|
44
|
+
if (provider) {
|
|
45
|
+
registry.register(provider);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return registry;
|
|
49
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC repair loop orchestration.
|
|
4
|
+
*
|
|
5
|
+
* The Foreman calls `runQcRepairLoop` after a completed-cluster QC run produces
|
|
6
|
+
* findings. This module:
|
|
7
|
+
*
|
|
8
|
+
* 1. Discovers a compiled repair packet manifest for the current round.
|
|
9
|
+
* 2. Converts eligible repair packets into Foreman-dispatched repair workers.
|
|
10
|
+
* 3. Awaits repair worker completion (via sealed result files / adapter).
|
|
11
|
+
* 4. Reruns QC after all repair workers in a round have completed.
|
|
12
|
+
* 5. Loops until: pass, no repairable packets, max rounds reached,
|
|
13
|
+
* all providers failed, operator review required, or Medic referral.
|
|
14
|
+
*
|
|
15
|
+
* Dispatch boundary: the parent/orchestrator NEVER implements repair code.
|
|
16
|
+
* Each repair packet becomes a sealed WorkerPacket with worker_role: "repair".
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.DEFAULT_MAX_REPAIR_ROUNDS = void 0;
|
|
20
|
+
exports.partitionRepairPackets = partitionRepairPackets;
|
|
21
|
+
exports.initRepairLoopState = initRepairLoopState;
|
|
22
|
+
exports.runQcRepairLoop = runQcRepairLoop;
|
|
23
|
+
const node_fs_1 = require("node:fs");
|
|
24
|
+
const node_path_1 = require("node:path");
|
|
25
|
+
const repair_packets_js_1 = require("./repair-packets.js");
|
|
26
|
+
const orchestration_js_1 = require("./orchestration.js");
|
|
27
|
+
const store_js_1 = require("../cluster-state/store.js");
|
|
28
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
29
|
+
/** Default maximum repair rounds when not configured. */
|
|
30
|
+
exports.DEFAULT_MAX_REPAIR_ROUNDS = 2;
|
|
31
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
32
|
+
function appendTelemetry(telemetryFile, event) {
|
|
33
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
|
|
34
|
+
(0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
|
|
35
|
+
}
|
|
36
|
+
/** Returns true when any finding in the results routes to repair-worker. */
|
|
37
|
+
function hasRepairableFindings(results) {
|
|
38
|
+
return results.some((r) => r.findings.some((f) => (f.routingDecision === "repair-worker" || f.routingDecision === "original-worker") &&
|
|
39
|
+
f.status !== "waived" &&
|
|
40
|
+
f.status !== "autofixed" &&
|
|
41
|
+
f.status !== "repaired"));
|
|
42
|
+
}
|
|
43
|
+
/** Returns true when all QC providers failed in every result. */
|
|
44
|
+
function allProvidersFailed(results) {
|
|
45
|
+
return (results.length > 0 &&
|
|
46
|
+
results.every((r) => r.allProvidersFailed || r.status === "failed"));
|
|
47
|
+
}
|
|
48
|
+
/** Returns true when any finding requires operator review. */
|
|
49
|
+
function requiresOperatorReview(results) {
|
|
50
|
+
return results.some((r) => r.findings.some((f) => f.routingDecision === "operator-review" &&
|
|
51
|
+
f.status !== "waived" &&
|
|
52
|
+
f.status !== "repaired"));
|
|
53
|
+
}
|
|
54
|
+
/** Returns true when the QC rerun passed (no open/blocking findings). */
|
|
55
|
+
function rerunPassed(results) {
|
|
56
|
+
if (results.length === 0)
|
|
57
|
+
return false;
|
|
58
|
+
return results.every((r) => r.status === "passed" || r.status === "skipped");
|
|
59
|
+
}
|
|
60
|
+
/** Partition packets into safe-to-parallel and must-serialize groups. */
|
|
61
|
+
function partitionRepairPackets(packets) {
|
|
62
|
+
// Medic (operator-review) packets are always serialized.
|
|
63
|
+
const serialized = packets.filter((p) => p.medic || p.routingTarget === "operator-review");
|
|
64
|
+
const parallelizable = packets.filter((p) => !p.medic && p.routingTarget !== "operator-review");
|
|
65
|
+
// Group by parallelGroup assignment from the compiler.
|
|
66
|
+
const groups = new Map();
|
|
67
|
+
for (const pkt of parallelizable) {
|
|
68
|
+
const key = pkt.parallelGroup ?? `solo-${pkt.packetId}`;
|
|
69
|
+
if (!groups.has(key))
|
|
70
|
+
groups.set(key, []);
|
|
71
|
+
groups.get(key).push(pkt);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
parallelGroups: Array.from(groups.values()),
|
|
75
|
+
serialized,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Initialize a fresh QcRepairLoopState. */
|
|
79
|
+
function initRepairLoopState(opts) {
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
return {
|
|
82
|
+
current_round: 0,
|
|
83
|
+
max_rounds: opts.maxRounds,
|
|
84
|
+
source_qc_run_ids: opts.sourceQcRunIds,
|
|
85
|
+
manifest_path: null,
|
|
86
|
+
pending_packet_ids: [],
|
|
87
|
+
completed_packet_ids: [],
|
|
88
|
+
rerun_requested: false,
|
|
89
|
+
rerun_qc_run_ids: {},
|
|
90
|
+
terminal_outcome: null,
|
|
91
|
+
initiated_at: now,
|
|
92
|
+
updated_at: now,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ── Main repair loop ───────────────────────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Run the bounded QC repair loop.
|
|
98
|
+
*
|
|
99
|
+
* The loop is Foreman-owned: it coordinates dispatch, never implements repairs.
|
|
100
|
+
* Returns deterministically on pass, no-repairable, max-rounds, all-providers-failed,
|
|
101
|
+
* operator-review, or medic-referral.
|
|
102
|
+
*/
|
|
103
|
+
async function runQcRepairLoop(options) {
|
|
104
|
+
const { clusterId, runId, branch, repoRoot, telemetryFile, config, registry, initialQcResults, dispatchRepairWorker, validationCommands = [], timeoutMs, maxRounds = exports.DEFAULT_MAX_REPAIR_ROUNDS, priorLoopState, onStateUpdate, } = options;
|
|
105
|
+
if (!config.enabled) {
|
|
106
|
+
const state = initRepairLoopState({
|
|
107
|
+
maxRounds,
|
|
108
|
+
sourceQcRunIds: initialQcResults.map((r) => r.qcRunId),
|
|
109
|
+
});
|
|
110
|
+
state.terminal_outcome = "qc-disabled";
|
|
111
|
+
state.updated_at = new Date().toISOString();
|
|
112
|
+
return {
|
|
113
|
+
outcome: "qc-disabled",
|
|
114
|
+
rounds_completed: 0,
|
|
115
|
+
final_qc_results: initialQcResults,
|
|
116
|
+
loop_state: state,
|
|
117
|
+
summary: "QC repair loop skipped — QC disabled by configuration",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const sourceRunIds = initialQcResults.map((r) => r.qcRunId);
|
|
121
|
+
let loopState = priorLoopState ?? initRepairLoopState({ maxRounds, sourceQcRunIds: sourceRunIds });
|
|
122
|
+
let currentResults = initialQcResults;
|
|
123
|
+
let roundsCompleted = loopState.current_round;
|
|
124
|
+
appendTelemetry(telemetryFile, {
|
|
125
|
+
event: "qc-repair-loop-started",
|
|
126
|
+
run_id: runId,
|
|
127
|
+
cluster_id: clusterId,
|
|
128
|
+
max_rounds: maxRounds,
|
|
129
|
+
source_qc_run_ids: sourceRunIds,
|
|
130
|
+
timestamp: new Date().toISOString(),
|
|
131
|
+
});
|
|
132
|
+
// ── Main loop ──────────────────────────────────────────────────────────────
|
|
133
|
+
while (roundsCompleted < maxRounds) {
|
|
134
|
+
const round = roundsCompleted + 1;
|
|
135
|
+
// Check exit conditions at the top of each round.
|
|
136
|
+
if (rerunPassed(currentResults) && round > 1) {
|
|
137
|
+
// A rerun already passed in this call frame — exit.
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (allProvidersFailed(currentResults)) {
|
|
141
|
+
loopState = { ...loopState, terminal_outcome: "all-providers-failed", updated_at: new Date().toISOString() };
|
|
142
|
+
onStateUpdate?.(loopState);
|
|
143
|
+
appendTelemetry(telemetryFile, {
|
|
144
|
+
event: "qc-repair-loop-terminal",
|
|
145
|
+
run_id: runId,
|
|
146
|
+
outcome: "all-providers-failed",
|
|
147
|
+
round,
|
|
148
|
+
timestamp: new Date().toISOString(),
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
outcome: "all-providers-failed",
|
|
152
|
+
rounds_completed: roundsCompleted,
|
|
153
|
+
final_qc_results: currentResults,
|
|
154
|
+
loop_state: loopState,
|
|
155
|
+
summary: `QC repair loop halted: all QC providers failed at round ${round}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (requiresOperatorReview(currentResults)) {
|
|
159
|
+
loopState = { ...loopState, terminal_outcome: "operator-review", updated_at: new Date().toISOString() };
|
|
160
|
+
onStateUpdate?.(loopState);
|
|
161
|
+
appendTelemetry(telemetryFile, {
|
|
162
|
+
event: "qc-repair-loop-terminal",
|
|
163
|
+
run_id: runId,
|
|
164
|
+
outcome: "operator-review",
|
|
165
|
+
round,
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
outcome: "operator-review",
|
|
170
|
+
rounds_completed: roundsCompleted,
|
|
171
|
+
final_qc_results: currentResults,
|
|
172
|
+
loop_state: loopState,
|
|
173
|
+
summary: `QC repair loop halted: unresolved operator-review findings at round ${round}`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// ── Compile / discover repair packets ──────────────────────────────────
|
|
177
|
+
let manifest = null;
|
|
178
|
+
// Try to read an existing manifest for this round first (idempotent re-entry).
|
|
179
|
+
manifest = (0, repair_packets_js_1.readRepairPacketManifest)(clusterId, round, repoRoot);
|
|
180
|
+
if (!manifest) {
|
|
181
|
+
// Compile fresh repair packets from current QC results.
|
|
182
|
+
const compiled = (0, repair_packets_js_1.compileAndWriteRepairPackets)({
|
|
183
|
+
clusterId,
|
|
184
|
+
round,
|
|
185
|
+
qcResults: currentResults,
|
|
186
|
+
config,
|
|
187
|
+
validationCommands,
|
|
188
|
+
repoRoot,
|
|
189
|
+
});
|
|
190
|
+
manifest = compiled.manifest;
|
|
191
|
+
// Update cluster state with manifest path.
|
|
192
|
+
try {
|
|
193
|
+
const clusterState = (0, store_js_1.readClusterStateSync)(clusterId, repoRoot);
|
|
194
|
+
if (clusterState) {
|
|
195
|
+
const repairManifests = {
|
|
196
|
+
...(clusterState.qc_repair_manifests ?? {}),
|
|
197
|
+
[round]: compiled.manifestPath,
|
|
198
|
+
};
|
|
199
|
+
await (0, store_js_1.writeClusterState)(clusterId, { ...clusterState, state_generation: clusterState.state_generation + 1, qc_repair_manifests: repairManifests }, repoRoot);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Non-fatal: cluster state update is best-effort.
|
|
204
|
+
}
|
|
205
|
+
loopState = {
|
|
206
|
+
...loopState,
|
|
207
|
+
current_round: round,
|
|
208
|
+
manifest_path: compiled.manifestPath,
|
|
209
|
+
pending_packet_ids: compiled.packets.map((p) => p.packetId),
|
|
210
|
+
updated_at: new Date().toISOString(),
|
|
211
|
+
};
|
|
212
|
+
onStateUpdate?.(loopState);
|
|
213
|
+
appendTelemetry(telemetryFile, {
|
|
214
|
+
event: "qc-repair-manifest-compiled",
|
|
215
|
+
run_id: runId,
|
|
216
|
+
cluster_id: clusterId,
|
|
217
|
+
round,
|
|
218
|
+
packet_count: compiled.packets.length,
|
|
219
|
+
manifest_path: compiled.manifestPath,
|
|
220
|
+
timestamp: new Date().toISOString(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
loopState = {
|
|
225
|
+
...loopState,
|
|
226
|
+
current_round: round,
|
|
227
|
+
manifest_path: loopState.manifest_path ?? (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds", String(round), "repair-packets.json"),
|
|
228
|
+
updated_at: new Date().toISOString(),
|
|
229
|
+
};
|
|
230
|
+
onStateUpdate?.(loopState);
|
|
231
|
+
}
|
|
232
|
+
// ── Check for repairable packets ────────────────────────────────────────
|
|
233
|
+
const repairablePackets = manifest.packets.filter((p) => p.routingTarget === "repair-worker" &&
|
|
234
|
+
p.status === "pending" &&
|
|
235
|
+
!loopState.completed_packet_ids.includes(p.packetId));
|
|
236
|
+
if (repairablePackets.length === 0 && !hasRepairableFindings(currentResults)) {
|
|
237
|
+
loopState = { ...loopState, terminal_outcome: "no-repairable", updated_at: new Date().toISOString() };
|
|
238
|
+
onStateUpdate?.(loopState);
|
|
239
|
+
appendTelemetry(telemetryFile, {
|
|
240
|
+
event: "qc-repair-loop-terminal",
|
|
241
|
+
run_id: runId,
|
|
242
|
+
outcome: "no-repairable",
|
|
243
|
+
round,
|
|
244
|
+
timestamp: new Date().toISOString(),
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
outcome: "no-repairable",
|
|
248
|
+
rounds_completed: roundsCompleted,
|
|
249
|
+
final_qc_results: currentResults,
|
|
250
|
+
loop_state: loopState,
|
|
251
|
+
summary: `QC repair loop: no repairable packets at round ${round}`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// ── Dispatch repair workers (parallel groups, then serialized) ──────────
|
|
255
|
+
const { parallelGroups, serialized } = partitionRepairPackets(repairablePackets);
|
|
256
|
+
const allWorkerResults = [];
|
|
257
|
+
let hasMedicReferral = false;
|
|
258
|
+
// Dispatch parallel groups.
|
|
259
|
+
for (const group of parallelGroups) {
|
|
260
|
+
appendTelemetry(telemetryFile, {
|
|
261
|
+
event: "qc-repair-worker-group-start",
|
|
262
|
+
run_id: runId,
|
|
263
|
+
round,
|
|
264
|
+
parallel_group: group[0]?.parallelGroup ?? null,
|
|
265
|
+
packet_ids: group.map((p) => p.packetId),
|
|
266
|
+
timestamp: new Date().toISOString(),
|
|
267
|
+
});
|
|
268
|
+
// Within a group, packets are non-conflicting and can run concurrently.
|
|
269
|
+
const groupResults = await Promise.all(group.map((pkt) => dispatchRepairWorker(pkt, round, manifest)));
|
|
270
|
+
allWorkerResults.push(...groupResults);
|
|
271
|
+
}
|
|
272
|
+
// Dispatch serialized (medic/operator-review) packets sequentially.
|
|
273
|
+
for (const pkt of serialized) {
|
|
274
|
+
const result = await dispatchRepairWorker(pkt, round, manifest);
|
|
275
|
+
allWorkerResults.push(result);
|
|
276
|
+
}
|
|
277
|
+
// Record completed packet IDs.
|
|
278
|
+
const completedIds = allWorkerResults
|
|
279
|
+
.filter((r) => r.status !== "skipped")
|
|
280
|
+
.map((r) => r.packetId);
|
|
281
|
+
loopState = {
|
|
282
|
+
...loopState,
|
|
283
|
+
completed_packet_ids: [...loopState.completed_packet_ids, ...completedIds],
|
|
284
|
+
pending_packet_ids: loopState.pending_packet_ids.filter((id) => !completedIds.includes(id)),
|
|
285
|
+
updated_at: new Date().toISOString(),
|
|
286
|
+
};
|
|
287
|
+
onStateUpdate?.(loopState);
|
|
288
|
+
// Check for failed repair workers → Medic referral.
|
|
289
|
+
const failedWorkers = allWorkerResults.filter((r) => r.status === "failure");
|
|
290
|
+
if (failedWorkers.length > 0) {
|
|
291
|
+
hasMedicReferral = true;
|
|
292
|
+
appendTelemetry(telemetryFile, {
|
|
293
|
+
event: "qc-repair-worker-failures",
|
|
294
|
+
run_id: runId,
|
|
295
|
+
round,
|
|
296
|
+
failed_packet_ids: failedWorkers.map((r) => r.packetId),
|
|
297
|
+
errors: failedWorkers.map((r) => r.errorMessage ?? "unknown"),
|
|
298
|
+
timestamp: new Date().toISOString(),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (hasMedicReferral) {
|
|
302
|
+
loopState = { ...loopState, terminal_outcome: "medic-referral", updated_at: new Date().toISOString() };
|
|
303
|
+
onStateUpdate?.(loopState);
|
|
304
|
+
appendTelemetry(telemetryFile, {
|
|
305
|
+
event: "qc-repair-loop-terminal",
|
|
306
|
+
run_id: runId,
|
|
307
|
+
outcome: "medic-referral",
|
|
308
|
+
round,
|
|
309
|
+
failed_count: failedWorkers.length,
|
|
310
|
+
timestamp: new Date().toISOString(),
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
outcome: "medic-referral",
|
|
314
|
+
rounds_completed: roundsCompleted + 1,
|
|
315
|
+
final_qc_results: currentResults,
|
|
316
|
+
loop_state: loopState,
|
|
317
|
+
summary: `QC repair loop: ${failedWorkers.length} repair worker(s) failed at round ${round} — Medic referral triggered`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
roundsCompleted += 1;
|
|
321
|
+
// ── QC rerun after successful repairs ───────────────────────────────────
|
|
322
|
+
loopState = { ...loopState, rerun_requested: true, updated_at: new Date().toISOString() };
|
|
323
|
+
onStateUpdate?.(loopState);
|
|
324
|
+
appendTelemetry(telemetryFile, {
|
|
325
|
+
event: "qc-repair-rerun-start",
|
|
326
|
+
run_id: runId,
|
|
327
|
+
cluster_id: clusterId,
|
|
328
|
+
round,
|
|
329
|
+
timestamp: new Date().toISOString(),
|
|
330
|
+
});
|
|
331
|
+
const rerunOptions = {
|
|
332
|
+
config,
|
|
333
|
+
registry,
|
|
334
|
+
trigger: "completed-cluster",
|
|
335
|
+
repoRoot,
|
|
336
|
+
runId,
|
|
337
|
+
clusterId,
|
|
338
|
+
branch,
|
|
339
|
+
telemetryFile,
|
|
340
|
+
timeoutMs,
|
|
341
|
+
};
|
|
342
|
+
let rerunResult;
|
|
343
|
+
try {
|
|
344
|
+
rerunResult = await (0, orchestration_js_1.runQcAtTrigger)(rerunOptions);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
348
|
+
appendTelemetry(telemetryFile, {
|
|
349
|
+
event: "qc-repair-rerun-error",
|
|
350
|
+
run_id: runId,
|
|
351
|
+
round,
|
|
352
|
+
error: msg,
|
|
353
|
+
timestamp: new Date().toISOString(),
|
|
354
|
+
});
|
|
355
|
+
// Treat rerun error as all-providers-failed.
|
|
356
|
+
loopState = { ...loopState, terminal_outcome: "all-providers-failed", updated_at: new Date().toISOString() };
|
|
357
|
+
onStateUpdate?.(loopState);
|
|
358
|
+
return {
|
|
359
|
+
outcome: "all-providers-failed",
|
|
360
|
+
rounds_completed: roundsCompleted,
|
|
361
|
+
final_qc_results: currentResults,
|
|
362
|
+
loop_state: loopState,
|
|
363
|
+
summary: `QC repair loop: rerun at round ${round} threw an error: ${msg}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// Record rerun QC run IDs.
|
|
367
|
+
const rerunIds = rerunResult.results.map((r) => r.qcRunId).filter(Boolean);
|
|
368
|
+
loopState = {
|
|
369
|
+
...loopState,
|
|
370
|
+
rerun_requested: false,
|
|
371
|
+
rerun_qc_run_ids: { ...loopState.rerun_qc_run_ids, [round]: rerunIds },
|
|
372
|
+
updated_at: new Date().toISOString(),
|
|
373
|
+
};
|
|
374
|
+
onStateUpdate?.(loopState);
|
|
375
|
+
appendTelemetry(telemetryFile, {
|
|
376
|
+
event: "qc-repair-rerun-complete",
|
|
377
|
+
run_id: runId,
|
|
378
|
+
cluster_id: clusterId,
|
|
379
|
+
round,
|
|
380
|
+
action: rerunResult.action,
|
|
381
|
+
summary: rerunResult.summary,
|
|
382
|
+
rerun_qc_run_ids: rerunIds,
|
|
383
|
+
timestamp: new Date().toISOString(),
|
|
384
|
+
});
|
|
385
|
+
currentResults = rerunResult.results;
|
|
386
|
+
// Pass condition: rerun returned no blocking findings.
|
|
387
|
+
if (rerunResult.action === "pass") {
|
|
388
|
+
loopState = { ...loopState, terminal_outcome: "pass", updated_at: new Date().toISOString() };
|
|
389
|
+
onStateUpdate?.(loopState);
|
|
390
|
+
appendTelemetry(telemetryFile, {
|
|
391
|
+
event: "qc-repair-loop-terminal",
|
|
392
|
+
run_id: runId,
|
|
393
|
+
outcome: "pass",
|
|
394
|
+
round,
|
|
395
|
+
timestamp: new Date().toISOString(),
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
outcome: "pass",
|
|
399
|
+
rounds_completed: roundsCompleted,
|
|
400
|
+
final_qc_results: currentResults,
|
|
401
|
+
loop_state: loopState,
|
|
402
|
+
summary: `QC repair loop passed after ${roundsCompleted} round(s)`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// ── Max rounds exhausted ──────────────────────────────────────────────────
|
|
407
|
+
loopState = { ...loopState, terminal_outcome: "max-rounds", updated_at: new Date().toISOString() };
|
|
408
|
+
onStateUpdate?.(loopState);
|
|
409
|
+
appendTelemetry(telemetryFile, {
|
|
410
|
+
event: "qc-repair-loop-terminal",
|
|
411
|
+
run_id: runId,
|
|
412
|
+
outcome: "max-rounds",
|
|
413
|
+
rounds_completed: roundsCompleted,
|
|
414
|
+
timestamp: new Date().toISOString(),
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
outcome: "max-rounds",
|
|
418
|
+
rounds_completed: roundsCompleted,
|
|
419
|
+
final_qc_results: currentResults,
|
|
420
|
+
loop_state: loopState,
|
|
421
|
+
summary: `QC repair loop exhausted max rounds (${maxRounds}) without passing`,
|
|
422
|
+
};
|
|
423
|
+
}
|