@lsctech/polaris 0.5.11 → 0.5.13
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 +61 -0
- package/dist/autoresearch/score.js +124 -0
- package/dist/cli/adopt-canon.js +22 -8
- package/dist/cli/index.js +4 -0
- package/dist/cli/qc.js +97 -0
- package/dist/config/validator.js +16 -6
- package/dist/finalize/artifact-policy.js +12 -3
- package/dist/finalize/index.js +36 -21
- package/dist/finalize/run-report.js +189 -19
- package/dist/finalize/steps/05-generate-report.js +5 -1
- package/dist/finalize/steps/12-archive.js +6 -0
- package/dist/loop/adapters/terminal-cli.js +159 -25
- package/dist/loop/body-parser.js +73 -6
- package/dist/loop/continue.js +22 -4
- package/dist/loop/dispatch.js +103 -68
- package/dist/loop/orphan-recovery.js +2 -1
- package/dist/loop/parent.js +50 -15
- package/dist/loop/router/engine.js +1 -0
- package/dist/loop/run-bootstrap.js +3 -1
- package/dist/loop/run-preflight.js +4 -1
- package/dist/loop/worker-packet.js +81 -2
- package/dist/map/inference.js +4 -1
- package/dist/medic/routing-signals.js +60 -0
- package/dist/medic/run-health-consult.js +5 -4
- package/dist/medic/treatment-packets.js +5 -1
- package/dist/qc/policy.js +2 -0
- package/dist/qc/providers/coderabbit.js +140 -10
- package/dist/qc/repair-loop.js +147 -1
- package/dist/qc/repair-packets.js +16 -3
- package/dist/qc/routing.js +6 -0
- package/dist/qc/types.js +3 -0
- package/dist/run-health/index.js +12 -0
- package/dist/skill-packet/generator.js +234 -3
- package/dist/smartdocs-engine/canon-check.js +5 -5
- package/dist/smartdocs-engine/index.js +54 -0
- package/dist/smartdocs-engine/seed-instructions.js +159 -4
- package/dist/smartdocs-engine/validate-instructions.js +46 -4
- package/dist/tracker/local-graph.js +106 -3
- package/dist/workspace/POLARIS.md +4 -0
- package/dist/workspace/SUMMARY.md +56 -0
- package/package.json +1 -1
|
@@ -10,6 +10,39 @@
|
|
|
10
10
|
* WorkerPacket extends BootstrapPacket so all existing adapters that
|
|
11
11
|
* accept BootstrapPacket transparently accept WorkerPacket.
|
|
12
12
|
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
13
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
47
|
exports.REPAIR_RETURN_CONTRACT = exports.STARTUP_RETURN_CONTRACT = exports.PREFLIGHT_RETURN_CONTRACT = exports.WORKER_SYMPTOM_CATEGORIES = exports.WORKER_PROHIBITED_WRITE_PATHS = exports.FINALIZE_RETURN_CONTRACT = exports.IMPL_RETURN_CONTRACT = void 0;
|
|
15
48
|
exports.roleContextForWorkerRole = roleContextForWorkerRole;
|
|
@@ -20,6 +53,7 @@ exports.compileImplPacket = compileImplPacket;
|
|
|
20
53
|
exports.compileFinalizePacket = compileFinalizePacket;
|
|
21
54
|
exports.compilePreflightPacket = compilePreflightPacket;
|
|
22
55
|
exports.compileRepairWorkerPacket = compileRepairWorkerPacket;
|
|
56
|
+
const path = __importStar(require("node:path"));
|
|
23
57
|
const worker_prompt_js_1 = require("./worker-prompt.js");
|
|
24
58
|
const body_parser_js_1 = require("./body-parser.js");
|
|
25
59
|
const WORKER_ROLE_CONTEXT = {
|
|
@@ -172,6 +206,49 @@ function defaultLifecycle(maxConcurrent, cleanupOnExit) {
|
|
|
172
206
|
cleanup_on_exit: cleanupOnExit,
|
|
173
207
|
};
|
|
174
208
|
}
|
|
209
|
+
/** Derives the adjacent test file path(s) for a single source TypeScript file. */
|
|
210
|
+
function deriveTestPaths(sourcePath) {
|
|
211
|
+
if (!sourcePath.endsWith('.ts') || sourcePath.endsWith('.test.ts') || sourcePath.endsWith('.d.ts')) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
if (sourcePath.includes('*') || sourcePath.includes('?')) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const candidates = [sourcePath.replace(/\.ts$/, '.test.ts')];
|
|
218
|
+
if (sourcePath.endsWith('/index.ts')) {
|
|
219
|
+
const dir = sourcePath.slice(0, -'/index.ts'.length);
|
|
220
|
+
const dirName = path.basename(dir);
|
|
221
|
+
if (dirName) {
|
|
222
|
+
candidates.push(`${dir}/${dirName}.test.ts`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return candidates;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Expands an allowed scope list with adjacent test paths when the validation
|
|
229
|
+
* commands include a vitest run. This lets workers touch test files for source
|
|
230
|
+
* files they are modifying, matching the `*.test.ts` validation commands.
|
|
231
|
+
*/
|
|
232
|
+
function expandScopeWithTestPaths(scope, validationCommands) {
|
|
233
|
+
const hasVitest = validationCommands.some((cmd) => cmd.includes('vitest'));
|
|
234
|
+
if (!hasVitest || scope.length === 0)
|
|
235
|
+
return scope;
|
|
236
|
+
const testPaths = new Set();
|
|
237
|
+
for (const item of scope) {
|
|
238
|
+
for (const testPath of deriveTestPaths(item)) {
|
|
239
|
+
testPaths.add(testPath);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (testPaths.size === 0)
|
|
243
|
+
return scope;
|
|
244
|
+
const expanded = [...scope];
|
|
245
|
+
for (const testPath of testPaths) {
|
|
246
|
+
if (!expanded.includes(testPath)) {
|
|
247
|
+
expanded.push(testPath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return expanded;
|
|
251
|
+
}
|
|
175
252
|
/**
|
|
176
253
|
* Build a compiled startup worker packet.
|
|
177
254
|
* Startup workers prepare tracker/graph/config/run-state evidence; the parent
|
|
@@ -257,6 +334,8 @@ function compileImplPacket(input) {
|
|
|
257
334
|
const resolvedRequirements = input.issueContext?.key_requirements.length
|
|
258
335
|
? input.issueContext.key_requirements
|
|
259
336
|
: bodyParsed?.requirements ?? [];
|
|
337
|
+
// Include adjacent test files when the validation commands indicate a vitest run.
|
|
338
|
+
const expandedScope = expandScopeWithTestPaths(resolvedScope, resolvedValidation);
|
|
260
339
|
const requirementLines = resolvedRequirements.map((r, i) => ` ${i + 1}. ${r}`);
|
|
261
340
|
const bodyLines = input.issueContext?.body
|
|
262
341
|
? [`Issue description:\n${input.issueContext.body}`]
|
|
@@ -285,7 +364,7 @@ function compileImplPacket(input) {
|
|
|
285
364
|
stateFile: input.stateFile,
|
|
286
365
|
telemetryFile: input.telemetryFile,
|
|
287
366
|
issueContext: input.issueContext,
|
|
288
|
-
allowedScope:
|
|
367
|
+
allowedScope: expandedScope,
|
|
289
368
|
validationCommands: resolvedValidation,
|
|
290
369
|
mode: promptMode,
|
|
291
370
|
simplicityMode: input.simplicityMode,
|
|
@@ -301,7 +380,7 @@ function compileImplPacket(input) {
|
|
|
301
380
|
instructions: {
|
|
302
381
|
primary_goal: promptResult.prompt,
|
|
303
382
|
steps,
|
|
304
|
-
allowed_scope:
|
|
383
|
+
allowed_scope: expandedScope,
|
|
305
384
|
validation_commands: resolvedValidation,
|
|
306
385
|
issue_context: input.issueContext,
|
|
307
386
|
},
|
package/dist/map/inference.js
CHANGED
|
@@ -17,7 +17,10 @@ function inferRoute(filePath, repoRoot, config, knownRoutes, branchName) {
|
|
|
17
17
|
const subdir = rest.split("/")[0];
|
|
18
18
|
if (subdir && rest.includes("/")) {
|
|
19
19
|
domain = subdir;
|
|
20
|
-
|
|
20
|
+
// Route identity follows the file's own containing directory, not just the
|
|
21
|
+
// top-level domain subdir — otherwise nested folders (e.g. src/runtime/continuation)
|
|
22
|
+
// incorrectly report the parent domain's route (e.g. src/runtime).
|
|
23
|
+
route = (0, node_path_1.dirname)(filePath).replace(/\\/g, "/");
|
|
21
24
|
taskchain = `polaris-${subdir}`;
|
|
22
25
|
confidence += 0.85;
|
|
23
26
|
tags.push(subdir);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Polaris routing anomaly → Medic/state-repair signal classification.
|
|
4
|
+
*
|
|
5
|
+
* These signals are emitted by the loop/dispatch/runtime boundary and are
|
|
6
|
+
* reviewed by Medic, not used to mutate routing.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.classifyRoutingTelemetryEvent = classifyRoutingTelemetryEvent;
|
|
10
|
+
/**
|
|
11
|
+
* Map a raw telemetry event to a state-repair review signal.
|
|
12
|
+
*
|
|
13
|
+
* Only loop/dispatch boundary events that indicate runtime state-repair work
|
|
14
|
+
* are classified. The returned signal has occurrences=1 and a single child_id;
|
|
15
|
+
* consumers (such as SOL scoring) should aggregate by signal name.
|
|
16
|
+
*/
|
|
17
|
+
function classifyRoutingTelemetryEvent(event) {
|
|
18
|
+
const eventName = event["event"];
|
|
19
|
+
if (typeof eventName !== "string")
|
|
20
|
+
return undefined;
|
|
21
|
+
const childId = typeof event["child_id"] === "string" ? event["child_id"] : undefined;
|
|
22
|
+
const childIds = childId ? [childId] : [];
|
|
23
|
+
switch (eventName) {
|
|
24
|
+
case "sealed-result-read-error":
|
|
25
|
+
return {
|
|
26
|
+
signal: "missing-sealed-result",
|
|
27
|
+
reason: "missing-sealed-result",
|
|
28
|
+
occurrences: 1,
|
|
29
|
+
child_ids: childIds,
|
|
30
|
+
};
|
|
31
|
+
case "stale-dispatch-aborted":
|
|
32
|
+
return {
|
|
33
|
+
signal: "stale-dispatch-abort",
|
|
34
|
+
reason: "stale-dispatch-abort",
|
|
35
|
+
occurrences: 1,
|
|
36
|
+
child_ids: childIds,
|
|
37
|
+
};
|
|
38
|
+
case "invalid-inline-attempt":
|
|
39
|
+
return {
|
|
40
|
+
signal: "invalid-inline-attempt",
|
|
41
|
+
reason: "invalid-inline-attempt",
|
|
42
|
+
occurrences: 1,
|
|
43
|
+
child_ids: childIds,
|
|
44
|
+
};
|
|
45
|
+
case "child-recovery-initiated": {
|
|
46
|
+
const recoveryReason = event["recovery_reason"];
|
|
47
|
+
if (recoveryReason === "stale-dispatch") {
|
|
48
|
+
return {
|
|
49
|
+
signal: "stale-dispatch-abort",
|
|
50
|
+
reason: "stale-dispatch",
|
|
51
|
+
occurrences: 1,
|
|
52
|
+
child_ids: childIds,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -114,11 +114,12 @@ function markInProgress(report, chartRef, repoRoot) {
|
|
|
114
114
|
(0, index_js_1.markMedicDecision)(report.run_id, { status: "in-progress", chartRefs: [chartRef] }, repoRoot);
|
|
115
115
|
}
|
|
116
116
|
function markResolved(report, chartRef, treatmentRefs, outcome, repoRoot) {
|
|
117
|
+
const success = outcome === "treatment-success" || outcome === "no-treatment-needed";
|
|
117
118
|
(0, index_js_1.markMedicDecision)(report.run_id, {
|
|
118
|
-
status: "resolved",
|
|
119
|
+
status: success ? "resolved" : "in-progress",
|
|
119
120
|
chartRefs: [chartRef],
|
|
120
121
|
treatmentPacketRefs: treatmentRefs,
|
|
121
|
-
resolvedAt: new Date().toISOString(),
|
|
122
|
+
resolvedAt: success ? new Date().toISOString() : undefined,
|
|
122
123
|
resolutionNotes: `Terminal outcome: ${outcome}`,
|
|
123
124
|
}, repoRoot);
|
|
124
125
|
}
|
|
@@ -355,7 +356,7 @@ async function runMedicRunHealthConsult(input) {
|
|
|
355
356
|
run_id: packet.run_id,
|
|
356
357
|
cluster_id: packet.cluster_id,
|
|
357
358
|
dispatch_id: packet.dispatch_id,
|
|
358
|
-
status: "
|
|
359
|
+
status: "blocked",
|
|
359
360
|
chart_id: chart.chart_id,
|
|
360
361
|
decision: "terminal",
|
|
361
362
|
treatment_packet_refs: allTreatmentRefs,
|
|
@@ -371,7 +372,7 @@ async function runMedicRunHealthConsult(input) {
|
|
|
371
372
|
run_id: packet.run_id,
|
|
372
373
|
cluster_id: packet.cluster_id,
|
|
373
374
|
dispatch_id: packet.dispatch_id,
|
|
374
|
-
status: "resolved",
|
|
375
|
+
status: remainingSymptoms.length === 0 ? "resolved" : "blocked",
|
|
375
376
|
chart_id: chart.chart_id,
|
|
376
377
|
decision: "terminal",
|
|
377
378
|
treatment_packet_refs: allTreatmentRefs,
|
|
@@ -59,7 +59,7 @@ function buildTreatmentPackets(input) {
|
|
|
59
59
|
return treated.map((symptom) => {
|
|
60
60
|
const packetId = buildTreatmentPacketId(report.run_id, round, symptom.id);
|
|
61
61
|
const dispatchId = (0, node_crypto_1.randomUUID)();
|
|
62
|
-
const resultFile = (0, node_path_1.join)(getMedicDir(report.cluster_id, repoRoot), `${packetId}-result.json`);
|
|
62
|
+
const resultFile = (0, node_path_1.relative)(repoRoot, (0, node_path_1.join)(getMedicDir(report.cluster_id, repoRoot), `${packetId}-result.json`));
|
|
63
63
|
return {
|
|
64
64
|
packet_id: packetId,
|
|
65
65
|
run_id: report.run_id,
|
|
@@ -154,12 +154,16 @@ async function dispatchTreatmentWorker(input) {
|
|
|
154
154
|
const result = await dispatch(workerPacket);
|
|
155
155
|
const summary = parseTreatmentWorkerSummary(result.summary);
|
|
156
156
|
if (result.exit_code === 0 && summary?.status === "done") {
|
|
157
|
+
const completedTreatment = { ...treatment, status: "completed" };
|
|
158
|
+
writeTreatmentPacket({ treatment: completedTreatment, repoRoot });
|
|
157
159
|
return {
|
|
158
160
|
packet_id: treatment.packet_id,
|
|
159
161
|
status: "success",
|
|
160
162
|
commit_sha: summary.commit,
|
|
161
163
|
};
|
|
162
164
|
}
|
|
165
|
+
const failedTreatment = { ...treatment, status: "failed" };
|
|
166
|
+
writeTreatmentPacket({ treatment: failedTreatment, repoRoot });
|
|
163
167
|
return {
|
|
164
168
|
packet_id: treatment.packet_id,
|
|
165
169
|
status: "failure",
|
package/dist/qc/policy.js
CHANGED
|
@@ -37,6 +37,8 @@ const FINDING_BOOKKEEPING_KEYS = [
|
|
|
37
37
|
"findingId",
|
|
38
38
|
];
|
|
39
39
|
const PROGRESS_SHAPE_KEYS = new Set(["event", "progress", "heartbeat", "complete", "review_context"]);
|
|
40
|
+
const ERROR_TYPE_RATE_LIMIT = new Set(["rate_limit", "rate-limit", "ratelimit", "rate limit"]);
|
|
41
|
+
const ERROR_TYPE_AUTH = new Set(["auth", "authentication", "unauthorized", "unauthorised", "forbidden"]);
|
|
40
42
|
const PROGRESS_TYPE_VALUES = new Set([
|
|
41
43
|
"progress",
|
|
42
44
|
"status",
|
|
@@ -56,6 +58,14 @@ const PROGRESS_STATUS_VALUES = new Set([
|
|
|
56
58
|
"ok",
|
|
57
59
|
"success",
|
|
58
60
|
]);
|
|
61
|
+
const TERMINAL_COMPLETE_STATUSES = new Set([
|
|
62
|
+
"review_completed",
|
|
63
|
+
"review_skipped",
|
|
64
|
+
"completed",
|
|
65
|
+
"complete",
|
|
66
|
+
"done",
|
|
67
|
+
"success",
|
|
68
|
+
]);
|
|
59
69
|
function hasFindingLocation(record) {
|
|
60
70
|
return FINDING_LOCATION_KEYS.some((key) => record[key] !== undefined);
|
|
61
71
|
}
|
|
@@ -112,6 +122,8 @@ function isProgressRecord(record) {
|
|
|
112
122
|
return false;
|
|
113
123
|
}
|
|
114
124
|
function isActionableFinding(record) {
|
|
125
|
+
if (isErrorRecord(record))
|
|
126
|
+
return false;
|
|
115
127
|
if (isProgressRecord(record))
|
|
116
128
|
return false;
|
|
117
129
|
return hasFindingLocation(record) || hasFindingContent(record);
|
|
@@ -124,6 +136,87 @@ function makeUnusableOutputError(message) {
|
|
|
124
136
|
err.qcFailureReason = "unusable-output";
|
|
125
137
|
return err;
|
|
126
138
|
}
|
|
139
|
+
function makeQcFailureError(reason, message) {
|
|
140
|
+
const err = new Error(message);
|
|
141
|
+
err.qcFailureReason = reason;
|
|
142
|
+
return err;
|
|
143
|
+
}
|
|
144
|
+
function isErrorRecord(record) {
|
|
145
|
+
if (record.type === "error")
|
|
146
|
+
return true;
|
|
147
|
+
if (typeof record.errorType === "string")
|
|
148
|
+
return true;
|
|
149
|
+
if (record.error === true || record.error === "true")
|
|
150
|
+
return true;
|
|
151
|
+
if (typeof record.status === "string" && record.status.toLowerCase() === "error")
|
|
152
|
+
return true;
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
function errorTypeFromRecord(record) {
|
|
156
|
+
const fromType = typeof record.errorType === "string" ? record.errorType : undefined;
|
|
157
|
+
const fromMessage = typeof record.message === "string" ? record.message : undefined;
|
|
158
|
+
return fromType ?? fromMessage;
|
|
159
|
+
}
|
|
160
|
+
function classifyErrorType(record) {
|
|
161
|
+
const raw = errorTypeFromRecord(record);
|
|
162
|
+
const errorType = (raw ?? "").toString().toLowerCase().trim();
|
|
163
|
+
if (errorType === "")
|
|
164
|
+
return "unavailable-provider";
|
|
165
|
+
if (ERROR_TYPE_RATE_LIMIT.has(errorType))
|
|
166
|
+
return "rate-limited";
|
|
167
|
+
if (ERROR_TYPE_AUTH.has(errorType))
|
|
168
|
+
return "auth-failure";
|
|
169
|
+
if (errorType.includes("rate limit"))
|
|
170
|
+
return "rate-limited";
|
|
171
|
+
if (errorType.includes("auth"))
|
|
172
|
+
return "auth-failure";
|
|
173
|
+
if (errorType.includes("unauthorized"))
|
|
174
|
+
return "auth-failure";
|
|
175
|
+
if (errorType.includes("forbidden"))
|
|
176
|
+
return "auth-failure";
|
|
177
|
+
if (["timeout", "timed out"].includes(errorType))
|
|
178
|
+
return "timeout";
|
|
179
|
+
if (errorType.includes("not found") || errorType.includes("not_found"))
|
|
180
|
+
return "command-not-found";
|
|
181
|
+
if (errorType.includes("unavailable") || errorType.includes("api_error") || errorType.includes("apierror") || errorType.includes("server") || errorType.includes("network") || errorType.includes("connection") || errorType.includes("internal")) {
|
|
182
|
+
return "unavailable-provider";
|
|
183
|
+
}
|
|
184
|
+
return "unavailable-provider";
|
|
185
|
+
}
|
|
186
|
+
function getTerminalCompleteFindingsCount(record) {
|
|
187
|
+
if (record.findings !== undefined) {
|
|
188
|
+
if (Array.isArray(record.findings)) {
|
|
189
|
+
return record.findings.length;
|
|
190
|
+
}
|
|
191
|
+
const count = typeof record.findings === "number" ? record.findings : Number(record.findings);
|
|
192
|
+
if (Number.isFinite(count))
|
|
193
|
+
return count;
|
|
194
|
+
}
|
|
195
|
+
const summary = record.summary;
|
|
196
|
+
if (summary && typeof summary === "object") {
|
|
197
|
+
const summaryRecord = summary;
|
|
198
|
+
if (summaryRecord.total !== undefined) {
|
|
199
|
+
const count = Number(summaryRecord.total);
|
|
200
|
+
if (Number.isFinite(count))
|
|
201
|
+
return count;
|
|
202
|
+
}
|
|
203
|
+
if (summaryRecord.issues !== undefined) {
|
|
204
|
+
const count = Number(summaryRecord.issues);
|
|
205
|
+
if (Number.isFinite(count))
|
|
206
|
+
return count;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
function isTerminalCompleteNoFindings(record) {
|
|
212
|
+
if (record.type !== "complete")
|
|
213
|
+
return false;
|
|
214
|
+
const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
|
|
215
|
+
if (!TERMINAL_COMPLETE_STATUSES.has(status))
|
|
216
|
+
return false;
|
|
217
|
+
const count = getTerminalCompleteFindingsCount(record);
|
|
218
|
+
return count === undefined || count === 0;
|
|
219
|
+
}
|
|
127
220
|
function buildRange(raw) {
|
|
128
221
|
const startLine = coerceNumber(raw.startLine ?? raw.line) ?? 1;
|
|
129
222
|
const endLine = coerceNumber(raw.endLine);
|
|
@@ -144,14 +237,27 @@ function parseFindingsFromPayload(payload) {
|
|
|
144
237
|
return [];
|
|
145
238
|
}
|
|
146
239
|
const record = payload;
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
if (Array.isArray(record.issues)) {
|
|
151
|
-
return record.issues.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
|
|
240
|
+
if (isErrorRecord(record)) {
|
|
241
|
+
throw makeQcFailureError(classifyErrorType(record), `CodeRabbit provider returned an error: ${record.message ?? record.errorType ?? "unknown"}`);
|
|
152
242
|
}
|
|
153
|
-
|
|
154
|
-
|
|
243
|
+
const arrays = [record.findings, record.issues, record.results];
|
|
244
|
+
for (const arr of arrays) {
|
|
245
|
+
if (Array.isArray(arr)) {
|
|
246
|
+
const findings = [];
|
|
247
|
+
for (const item of arr) {
|
|
248
|
+
if (typeof item !== "object" || item === null) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const itemRecord = item;
|
|
252
|
+
if (isErrorRecord(itemRecord)) {
|
|
253
|
+
throw makeQcFailureError(classifyErrorType(itemRecord), `CodeRabbit provider returned an error in findings array: ${itemRecord.message ?? itemRecord.errorType ?? "unknown"}`);
|
|
254
|
+
}
|
|
255
|
+
if (isActionableFinding(itemRecord)) {
|
|
256
|
+
findings.push(item);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return findings;
|
|
260
|
+
}
|
|
155
261
|
}
|
|
156
262
|
// Single finding wrapped in an object
|
|
157
263
|
if (isActionableFinding(record)) {
|
|
@@ -172,6 +278,9 @@ function parseReport(output, format, parser) {
|
|
|
172
278
|
const findings = parseFindingsFromPayload(data);
|
|
173
279
|
const record = data;
|
|
174
280
|
if (findings.length === 0) {
|
|
281
|
+
if (isTerminalCompleteNoFindings(record)) {
|
|
282
|
+
return { findings: [] };
|
|
283
|
+
}
|
|
175
284
|
if (isProgressRecord(record)) {
|
|
176
285
|
throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
|
|
177
286
|
}
|
|
@@ -198,7 +307,19 @@ function parseReport(output, format, parser) {
|
|
|
198
307
|
try {
|
|
199
308
|
const parsed = JSON.parse(text);
|
|
200
309
|
if (Array.isArray(parsed)) {
|
|
201
|
-
const findings =
|
|
310
|
+
const findings = [];
|
|
311
|
+
for (const item of parsed) {
|
|
312
|
+
if (typeof item !== "object" || item === null) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const itemRecord = item;
|
|
316
|
+
if (isErrorRecord(itemRecord)) {
|
|
317
|
+
throw makeQcFailureError(classifyErrorType(itemRecord), `CodeRabbit provider returned an error in JSON array: ${itemRecord.message ?? itemRecord.errorType ?? "unknown"}`);
|
|
318
|
+
}
|
|
319
|
+
if (isActionableFinding(itemRecord)) {
|
|
320
|
+
findings.push(item);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
202
323
|
if (findings.length === 0 && parsed.length > 0) {
|
|
203
324
|
const unusableCount = parsed.filter((item) => typeof item === "object" && item !== null && isUnusableFindingRecord(item)).length;
|
|
204
325
|
if (unusableCount > 0) {
|
|
@@ -210,6 +331,9 @@ function parseReport(output, format, parser) {
|
|
|
210
331
|
const findings = parseFindingsFromPayload(parsed);
|
|
211
332
|
if (findings.length === 0) {
|
|
212
333
|
const parsedRecord = parsed;
|
|
334
|
+
if (isTerminalCompleteNoFindings(parsedRecord)) {
|
|
335
|
+
return { findings: [] };
|
|
336
|
+
}
|
|
213
337
|
if (isProgressRecord(parsedRecord)) {
|
|
214
338
|
throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
|
|
215
339
|
}
|
|
@@ -260,9 +384,12 @@ function parseReport(output, format, parser) {
|
|
|
260
384
|
continue;
|
|
261
385
|
}
|
|
262
386
|
const record = parsed;
|
|
263
|
-
if (record
|
|
387
|
+
if (isTerminalCompleteNoFindings(record)) {
|
|
264
388
|
sawExplicitSkip = true;
|
|
265
389
|
}
|
|
390
|
+
if (isErrorRecord(record)) {
|
|
391
|
+
throw makeQcFailureError(classifyErrorType(record), `CodeRabbit provider returned an error in JSONL: ${record.message ?? record.errorType ?? "unknown"}`);
|
|
392
|
+
}
|
|
266
393
|
if (isProgressRecord(record)) {
|
|
267
394
|
progressLineCount++;
|
|
268
395
|
continue;
|
|
@@ -274,7 +401,10 @@ function parseReport(output, format, parser) {
|
|
|
274
401
|
unusableLineCount++;
|
|
275
402
|
}
|
|
276
403
|
}
|
|
277
|
-
catch {
|
|
404
|
+
catch (err) {
|
|
405
|
+
if (typeof err === "object" && err !== null && "qcFailureReason" in err) {
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
278
408
|
// Ignore unparseable lines.
|
|
279
409
|
}
|
|
280
410
|
}
|
package/dist/qc/repair-loop.js
CHANGED
|
@@ -19,10 +19,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
exports.DEFAULT_REPAIR_DISPATCH_TIMEOUT_MS = exports.DEFAULT_MAX_REPAIR_ROUNDS = void 0;
|
|
20
20
|
exports.partitionRepairPackets = partitionRepairPackets;
|
|
21
21
|
exports.initRepairLoopState = initRepairLoopState;
|
|
22
|
+
exports.getQcResolutionArtifactPath = getQcResolutionArtifactPath;
|
|
23
|
+
exports.resolveQcResolutionFindings = resolveQcResolutionFindings;
|
|
24
|
+
exports.getResolverIdentity = getResolverIdentity;
|
|
25
|
+
exports.isValidQcResolutionArtifact = isValidQcResolutionArtifact;
|
|
26
|
+
exports.readQcResolutionArtifact = readQcResolutionArtifact;
|
|
27
|
+
exports.writeQcResolutionArtifact = writeQcResolutionArtifact;
|
|
22
28
|
exports.runQcRepairLoop = runQcRepairLoop;
|
|
23
29
|
const node_fs_1 = require("node:fs");
|
|
30
|
+
const node_child_process_1 = require("node:child_process");
|
|
24
31
|
const node_path_1 = require("node:path");
|
|
25
32
|
const repair_packets_js_1 = require("./repair-packets.js");
|
|
33
|
+
const types_js_1 = require("./types.js");
|
|
26
34
|
const orchestration_js_1 = require("./orchestration.js");
|
|
27
35
|
const store_js_1 = require("../cluster-state/store.js");
|
|
28
36
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
@@ -189,6 +197,127 @@ function initRepairLoopState(opts) {
|
|
|
189
197
|
updated_at: now,
|
|
190
198
|
};
|
|
191
199
|
}
|
|
200
|
+
// ── Operator resolution artifact helpers ──────────────────────────────────────
|
|
201
|
+
/** Path for the operator-written resolution artifact for a repair round. */
|
|
202
|
+
function getQcResolutionArtifactPath(clusterId, round, repoRoot) {
|
|
203
|
+
return (0, node_path_1.join)((0, repair_packets_js_1.getRepairRoundDir)(clusterId, round, repoRoot), "resolution.json");
|
|
204
|
+
}
|
|
205
|
+
/** Resolve the finding IDs covered by a resolution artifact. */
|
|
206
|
+
function resolveQcResolutionFindings(manifest, explicitFindings) {
|
|
207
|
+
const manifestFindings = [
|
|
208
|
+
...new Set(manifest.packets.flatMap((p) => p.findingIds)),
|
|
209
|
+
].sort();
|
|
210
|
+
if (!explicitFindings || explicitFindings.length === 0) {
|
|
211
|
+
return manifestFindings;
|
|
212
|
+
}
|
|
213
|
+
const allowed = new Set(manifestFindings);
|
|
214
|
+
const unknown = explicitFindings.filter((id) => !allowed.has(id));
|
|
215
|
+
if (unknown.length > 0) {
|
|
216
|
+
throw new Error(`Unknown finding IDs: ${unknown.join(", ")}. Expected one of: ${manifestFindings.join(", ") || "none"}`);
|
|
217
|
+
}
|
|
218
|
+
return [...new Set(explicitFindings)].sort();
|
|
219
|
+
}
|
|
220
|
+
/** Determine the resolver identity from git config or the default operator. */
|
|
221
|
+
function getResolverIdentity(repoRoot) {
|
|
222
|
+
try {
|
|
223
|
+
const name = (0, node_child_process_1.execFileSync)("git", ["config", "user.name"], {
|
|
224
|
+
cwd: repoRoot || process.cwd(),
|
|
225
|
+
encoding: "utf-8",
|
|
226
|
+
}).trim();
|
|
227
|
+
return name || "lsctech";
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return "lsctech";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const QC_RESOLUTION_OUTCOME_SET = new Set(types_js_1.QC_RESOLUTION_OUTCOMES);
|
|
234
|
+
/** Validate a resolution artifact object. */
|
|
235
|
+
function isValidQcResolutionArtifact(value) {
|
|
236
|
+
if (!value || typeof value !== "object")
|
|
237
|
+
return false;
|
|
238
|
+
const v = value;
|
|
239
|
+
if (typeof v.schemaVersion !== "string")
|
|
240
|
+
return false;
|
|
241
|
+
if (typeof v.clusterId !== "string")
|
|
242
|
+
return false;
|
|
243
|
+
if (typeof v.round !== "number")
|
|
244
|
+
return false;
|
|
245
|
+
if (typeof v.resolvedAt !== "string")
|
|
246
|
+
return false;
|
|
247
|
+
if (typeof v.resolver !== "string" || v.resolver === "")
|
|
248
|
+
return false;
|
|
249
|
+
if (typeof v.resolvedOutcome !== "string" ||
|
|
250
|
+
!QC_RESOLUTION_OUTCOME_SET.has(v.resolvedOutcome)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (typeof v.reason !== "string" || v.reason.trim() === "")
|
|
254
|
+
return false;
|
|
255
|
+
if (!Array.isArray(v.findings) ||
|
|
256
|
+
v.findings.length === 0 ||
|
|
257
|
+
!v.findings.every((f) => typeof f === "string")) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
/** Read and validate a resolution artifact, if it exists. */
|
|
263
|
+
function readQcResolutionArtifact(clusterId, round, repoRoot) {
|
|
264
|
+
const artifactPath = getQcResolutionArtifactPath(clusterId, round, repoRoot);
|
|
265
|
+
try {
|
|
266
|
+
const data = (0, node_fs_1.readFileSync)(artifactPath, "utf-8");
|
|
267
|
+
const parsed = JSON.parse(data);
|
|
268
|
+
if (isValidQcResolutionArtifact(parsed)) {
|
|
269
|
+
// Verify identity matches expected clusterId and round
|
|
270
|
+
if (parsed.clusterId !== clusterId || parsed.round !== round) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return parsed;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error.code === "ENOENT") {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
if (error instanceof SyntaxError) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Write a resolution artifact atomically and return its absolute path. */
|
|
288
|
+
function writeQcResolutionArtifact(options) {
|
|
289
|
+
const { clusterId, round, resolver, resolvedOutcome, reason, findings, repoRoot, } = options;
|
|
290
|
+
const artifactPath = getQcResolutionArtifactPath(clusterId, round, repoRoot);
|
|
291
|
+
const dir = (0, node_path_1.dirname)(artifactPath);
|
|
292
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
293
|
+
const artifact = {
|
|
294
|
+
schemaVersion: "1.0",
|
|
295
|
+
clusterId,
|
|
296
|
+
round,
|
|
297
|
+
resolvedAt: new Date().toISOString(),
|
|
298
|
+
resolver,
|
|
299
|
+
resolvedOutcome,
|
|
300
|
+
reason,
|
|
301
|
+
findings,
|
|
302
|
+
};
|
|
303
|
+
const tempPath = `${artifactPath}.tmp.${process.pid}.${Date.now()}.${Math.random()
|
|
304
|
+
.toString(36)
|
|
305
|
+
.slice(2, 8)}`;
|
|
306
|
+
try {
|
|
307
|
+
(0, node_fs_1.writeFileSync)(tempPath, JSON.stringify(artifact, null, 2), "utf-8");
|
|
308
|
+
(0, node_fs_1.renameSync)(tempPath, artifactPath);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
try {
|
|
312
|
+
(0, node_fs_1.unlinkSync)(tempPath);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Ignore cleanup failure.
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
return artifactPath;
|
|
320
|
+
}
|
|
192
321
|
// ── Main repair loop ───────────────────────────────────────────────────────────
|
|
193
322
|
/**
|
|
194
323
|
* Run the bounded QC repair loop.
|
|
@@ -293,10 +422,21 @@ async function runQcRepairLoop(options) {
|
|
|
293
422
|
else {
|
|
294
423
|
const existingManifestPath = loopState.manifest_path ??
|
|
295
424
|
(0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds", String(round), "repair-packets.json");
|
|
425
|
+
// Synchronize manifest packet statuses with the persisted loop state.
|
|
426
|
+
const completedSet = new Set(loopState.completed_packet_ids);
|
|
427
|
+
for (const packet of manifest.packets) {
|
|
428
|
+
if (completedSet.has(packet.packetId)) {
|
|
429
|
+
packet.status = "completed";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
(0, repair_packets_js_1.writeRepairPacketManifest)(manifest, repoRoot);
|
|
296
433
|
loopState = {
|
|
297
434
|
...loopState,
|
|
298
435
|
current_round: round,
|
|
299
436
|
manifest_path: existingManifestPath,
|
|
437
|
+
pending_packet_ids: manifest.packets
|
|
438
|
+
.filter((p) => p.status === "pending" && !completedSet.has(p.packetId))
|
|
439
|
+
.map((p) => p.packetId),
|
|
300
440
|
updated_at: new Date().toISOString(),
|
|
301
441
|
};
|
|
302
442
|
onStateUpdate?.(loopState);
|
|
@@ -367,10 +507,16 @@ async function runQcRepairLoop(options) {
|
|
|
367
507
|
const result = await dispatchRepairWorkerWithTimeout(dispatchRepairWorker, pkt, round, manifest, repairDispatchTimeoutMs, telemetryFile, runId, clusterId);
|
|
368
508
|
allWorkerResults.push(result);
|
|
369
509
|
}
|
|
370
|
-
// Record completed packet IDs.
|
|
510
|
+
// Record completed packet IDs and update manifest statuses.
|
|
371
511
|
const completedIds = allWorkerResults
|
|
372
512
|
.filter((r) => r.status !== "skipped")
|
|
373
513
|
.map((r) => r.packetId);
|
|
514
|
+
for (const packet of manifest.packets) {
|
|
515
|
+
if (completedIds.includes(packet.packetId)) {
|
|
516
|
+
packet.status = "completed";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
(0, repair_packets_js_1.writeRepairPacketManifest)(manifest, repoRoot);
|
|
374
520
|
loopState = {
|
|
375
521
|
...loopState,
|
|
376
522
|
completed_packet_ids: [...loopState.completed_packet_ids, ...completedIds],
|