@lsctech/polaris 0.5.5 → 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/index.js +20 -1
- package/dist/autoresearch/proposal.js +5 -0
- package/dist/autoresearch/score.js +174 -2
- package/dist/autoresearch/sol-evidence-loader.js +516 -0
- package/dist/autoresearch/sol-history.js +90 -0
- package/dist/autoresearch/sol-recommendations.js +382 -0
- package/dist/autoresearch/sol-report.js +197 -0
- package/dist/autoresearch/sol-scorer.js +524 -0
- package/dist/cli/autoresearch.js +185 -11
- package/dist/cli/index.js +1 -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/dist/types/sol-evidence.js +18 -0
- package/dist/types/sol-score.js +18 -0
- package/package.json +1 -1
package/dist/cli/autoresearch.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createAutoresearchCommand =
|
|
3
|
+
exports.createAutoresearchCommand = void 0;
|
|
4
|
+
exports.createSolCommand = createSolCommand;
|
|
4
5
|
const node_path_1 = require("node:path");
|
|
5
6
|
const commander_1 = require("commander");
|
|
6
7
|
const dev_gate_js_1 = require("../autoresearch/dev-gate.js");
|
|
7
8
|
const score_js_1 = require("../autoresearch/score.js");
|
|
8
9
|
const proposal_js_1 = require("../autoresearch/proposal.js");
|
|
9
10
|
const routing_js_1 = require("../autoresearch/routing.js");
|
|
10
|
-
|
|
11
|
+
const sol_evidence_loader_js_1 = require("../autoresearch/sol-evidence-loader.js");
|
|
12
|
+
const sol_scorer_js_1 = require("../autoresearch/sol-scorer.js");
|
|
13
|
+
const sol_history_js_1 = require("../autoresearch/sol-history.js");
|
|
14
|
+
const sol_report_js_1 = require("../autoresearch/sol-report.js");
|
|
15
|
+
const sol_recommendations_js_1 = require("../autoresearch/sol-recommendations.js");
|
|
16
|
+
function createSolCommand(options) {
|
|
11
17
|
const repoRoot = options.repoRoot;
|
|
12
|
-
const
|
|
13
|
-
.
|
|
18
|
+
const sol = new commander_1.Command("sol")
|
|
19
|
+
.alias("autoresearch")
|
|
20
|
+
.description("Self-Optimization Loop (SOL) tools — autoresearch compatibility alias (dev-gated — Polaris development context only)")
|
|
14
21
|
.showHelpAfterError()
|
|
15
22
|
.showSuggestionAfterError();
|
|
16
|
-
|
|
23
|
+
sol
|
|
17
24
|
.command("score <run-id>")
|
|
18
25
|
.description("Score a completed Polaris run against the binary gate scorecard and output a diagnosis report")
|
|
19
26
|
.option("-r, --repo-root <path>", "Repository root", repoRoot)
|
|
@@ -35,11 +42,39 @@ function createAutoresearchCommand(options) {
|
|
|
35
42
|
process.stdout.write(`${output}\n`);
|
|
36
43
|
}
|
|
37
44
|
catch (err) {
|
|
38
|
-
process.stderr.write(`
|
|
45
|
+
process.stderr.write(`sol score error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
39
46
|
process.exit(1);
|
|
40
47
|
}
|
|
41
48
|
});
|
|
42
|
-
|
|
49
|
+
sol
|
|
50
|
+
.command("score-report <run-id>")
|
|
51
|
+
.description("Compute a SOL score report with diagnostic sub-scores for Foreman and Workers")
|
|
52
|
+
.option("-r, --repo-root <path>", "Repository root", repoRoot)
|
|
53
|
+
.option("--json", "Output raw JSON (default: pretty-printed)")
|
|
54
|
+
.action((runId, cmdOptions) => {
|
|
55
|
+
const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
|
|
56
|
+
try {
|
|
57
|
+
(0, dev_gate_js_1.assertPolarisDevContext)(root);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const artifacts = (0, score_js_1.loadRunArtifacts)(root, runId);
|
|
65
|
+
const evidence = (0, sol_evidence_loader_js_1.aggregateSolEvidence)(artifacts);
|
|
66
|
+
const report = (0, sol_scorer_js_1.computeSolScoreReport)(evidence);
|
|
67
|
+
const output = cmdOptions.json
|
|
68
|
+
? JSON.stringify(report)
|
|
69
|
+
: JSON.stringify(report, null, 2);
|
|
70
|
+
process.stdout.write(`${output}\n`);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
process.stderr.write(`sol score-report error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
sol
|
|
43
78
|
.command("propose <diagnosis-file>")
|
|
44
79
|
.description("File Linear improvement proposals from a diagnosis report (dev-gated — never auto-applied)")
|
|
45
80
|
.option("-r, --repo-root <path>", "Repository root", repoRoot)
|
|
@@ -60,7 +95,7 @@ function createAutoresearchCommand(options) {
|
|
|
60
95
|
report = (0, proposal_js_1.loadDiagnosisReport)((0, node_path_1.resolve)(diagnosisFile));
|
|
61
96
|
}
|
|
62
97
|
catch (err) {
|
|
63
|
-
process.stderr.write(`
|
|
98
|
+
process.stderr.write(`sol propose: invalid diagnosis file: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
64
99
|
process.exit(1);
|
|
65
100
|
}
|
|
66
101
|
const proposals = (0, proposal_js_1.buildProposals)(report);
|
|
@@ -70,7 +105,7 @@ function createAutoresearchCommand(options) {
|
|
|
70
105
|
}
|
|
71
106
|
const apiKey = process.env["LINEAR_API_KEY"];
|
|
72
107
|
if (!apiKey && !cmdOptions.dryRun) {
|
|
73
|
-
process.stderr.write("
|
|
108
|
+
process.stderr.write("sol propose: LINEAR_API_KEY environment variable is required.\n");
|
|
74
109
|
process.exit(1);
|
|
75
110
|
}
|
|
76
111
|
try {
|
|
@@ -88,9 +123,148 @@ function createAutoresearchCommand(options) {
|
|
|
88
123
|
}
|
|
89
124
|
}
|
|
90
125
|
catch (err) {
|
|
91
|
-
process.stderr.write(`
|
|
126
|
+
process.stderr.write(`sol propose error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
sol
|
|
131
|
+
.command("recommend")
|
|
132
|
+
.description("Generate SOL routing recommendations from historical snapshots (advisory by default; filing is Polaris-dev only)")
|
|
133
|
+
.option("-r, --repo-root <path>", "Repository root", repoRoot)
|
|
134
|
+
.option("--history-path <path>", "Custom history directory (relative to repo root)")
|
|
135
|
+
.option("--group-by <dims>", "Comma-separated grouping dimensions (provider,model,role,route,task_type,repo,risk,worker_id,run_id,time_window)", "provider,model,role,route,task_type")
|
|
136
|
+
.option("--threshold <n>", "Mean composite threshold below which a recommendation is triggered", "0.7")
|
|
137
|
+
.option("--min-samples <n>", "Minimum snapshots per group before recommending", "2")
|
|
138
|
+
.option("--file", "File review-gated tracker proposals (requires Polaris dev context)")
|
|
139
|
+
.option("--team <team>", "Tracker team name or ID", "Polaris")
|
|
140
|
+
.option("--dry-run", "Show tracker mutations without writing to the tracker")
|
|
141
|
+
.option("--json", "Output raw JSON (default: human-readable)")
|
|
142
|
+
.action(async (cmdOptions) => {
|
|
143
|
+
const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
|
|
144
|
+
let snapshots;
|
|
145
|
+
try {
|
|
146
|
+
snapshots = (0, sol_history_js_1.loadSnapshots)(root, cmdOptions.historyPath);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
process.stderr.write(`sol recommend error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const groupBy = cmdOptions.groupBy.split(",").filter(Boolean);
|
|
153
|
+
const threshold = parseFloat(cmdOptions.threshold) || 0.7;
|
|
154
|
+
const minSamples = parseInt(cmdOptions.minSamples, 10) || 2;
|
|
155
|
+
const report = (0, sol_recommendations_js_1.generateRecommendations)(snapshots, { groupBy, threshold, minSamples });
|
|
156
|
+
// Advisory mode: never touches the tracker or filesystem.
|
|
157
|
+
if (!cmdOptions.file) {
|
|
158
|
+
if (cmdOptions.json) {
|
|
159
|
+
process.stdout.write(JSON.stringify(report) + "\n");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
process.stdout.write((0, sol_recommendations_js_1.formatRecommendationsCli)(report));
|
|
163
|
+
}
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
// Filing mode: Polaris dev context only.
|
|
167
|
+
try {
|
|
168
|
+
(0, dev_gate_js_1.assertPolarisDevContext)(root);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
if (report.recommendations.length === 0) {
|
|
175
|
+
process.stdout.write("No underperforming groups — nothing to file.\n");
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
const apiKey = process.env["LINEAR_API_KEY"];
|
|
179
|
+
if (!apiKey && !cmdOptions.dryRun) {
|
|
180
|
+
process.stderr.write("sol recommend: LINEAR_API_KEY environment variable is required.\n");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const proposals = (0, sol_recommendations_js_1.recommendationsToProposals)(report.recommendations);
|
|
184
|
+
try {
|
|
185
|
+
const result = await (0, routing_js_1.routeProposals)(proposals, {
|
|
186
|
+
apiKey: apiKey ?? "",
|
|
187
|
+
teamKey: cmdOptions.team,
|
|
188
|
+
dryRun: cmdOptions.dryRun,
|
|
189
|
+
});
|
|
190
|
+
const output = cmdOptions.json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
|
|
191
|
+
process.stdout.write(`${output}\n`);
|
|
192
|
+
if (result.total_errors > 0) {
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
process.stderr.write(`sol recommend error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// ── history subcommand group ──
|
|
202
|
+
const history = new commander_1.Command("history")
|
|
203
|
+
.description("SOL historical performance storage and reports");
|
|
204
|
+
history
|
|
205
|
+
.command("save <run-id>")
|
|
206
|
+
.description("Score a run and persist the SOL score snapshot to local history")
|
|
207
|
+
.option("-r, --repo-root <path>", "Repository root", repoRoot)
|
|
208
|
+
.option("--history-path <path>", "Custom history directory (relative to repo root)")
|
|
209
|
+
.action((runId, cmdOptions) => {
|
|
210
|
+
const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
|
|
211
|
+
try {
|
|
212
|
+
(0, dev_gate_js_1.assertPolarisDevContext)(root);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const artifacts = (0, score_js_1.loadRunArtifacts)(root, runId);
|
|
220
|
+
const evidence = (0, sol_evidence_loader_js_1.aggregateSolEvidence)(artifacts);
|
|
221
|
+
const report = (0, sol_scorer_js_1.computeSolScoreReport)(evidence);
|
|
222
|
+
const workerIds = evidence.children.map((c) => c.worker_id);
|
|
223
|
+
const snapshot = (0, sol_history_js_1.buildSnapshot)(report, evidence.grouping_keys, workerIds);
|
|
224
|
+
const path = (0, sol_history_js_1.appendSnapshot)(root, snapshot, cmdOptions.historyPath);
|
|
225
|
+
process.stdout.write(`Snapshot saved to ${path}\n`);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
process.stderr.write(`sol history save error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
history
|
|
233
|
+
.command("report")
|
|
234
|
+
.description("Generate a report from SOL historical snapshots")
|
|
235
|
+
.option("-r, --repo-root <path>", "Repository root", repoRoot)
|
|
236
|
+
.option("--history-path <path>", "Custom history directory (relative to repo root)")
|
|
237
|
+
.option("--group-by <dims>", "Comma-separated grouping dimensions (repo,route,task_type,role,risk,provider,model,worker_id,run_id,time_window)", "run_id")
|
|
238
|
+
.option("--window-days <days>", "Time window size in days for time_window grouping", "7")
|
|
239
|
+
.option("--json", "Output raw JSON (default: human-readable table)")
|
|
240
|
+
.action((cmdOptions) => {
|
|
241
|
+
const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
|
|
242
|
+
try {
|
|
243
|
+
(0, dev_gate_js_1.assertPolarisDevContext)(root);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const snapshots = (0, sol_history_js_1.loadSnapshots)(root, cmdOptions.historyPath);
|
|
251
|
+
const groupBy = cmdOptions.groupBy.split(",").filter(Boolean);
|
|
252
|
+
const windowDays = parseInt(cmdOptions.windowDays, 10) || 7;
|
|
253
|
+
const report = (0, sol_report_js_1.generateReport)(snapshots, { groupBy, windowDays });
|
|
254
|
+
if (cmdOptions.json) {
|
|
255
|
+
process.stdout.write(JSON.stringify(report) + "\n");
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
process.stdout.write((0, sol_report_js_1.formatReportCli)(report));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
process.stderr.write(`sol history report error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
92
263
|
process.exit(1);
|
|
93
264
|
}
|
|
94
265
|
});
|
|
95
|
-
|
|
266
|
+
sol.addCommand(history);
|
|
267
|
+
return sol;
|
|
96
268
|
}
|
|
269
|
+
/** @deprecated Use {@link createSolCommand}. */
|
|
270
|
+
exports.createAutoresearchCommand = createSolCommand;
|
package/dist/cli/index.js
CHANGED
|
@@ -129,7 +129,7 @@ function createPolarisCommand(options = {}) {
|
|
|
129
129
|
}));
|
|
130
130
|
program.addCommand((0, adopt_command_js_1.createAdoptCommand)({ repoRoot }));
|
|
131
131
|
program.addCommand((0, upgrade_command_js_1.createUpgradeCommand)({ repoRoot }));
|
|
132
|
-
program.addCommand((0, autoresearch_js_1.
|
|
132
|
+
program.addCommand((0, autoresearch_js_1.createSolCommand)({ repoRoot }));
|
|
133
133
|
program
|
|
134
134
|
.command("simplicity")
|
|
135
135
|
.description("View or override the simplicity discipline mode for the active run")
|
package/dist/config/defaults.js
CHANGED
package/dist/config/validator.js
CHANGED
|
@@ -10,6 +10,9 @@ function isNumber(value) {
|
|
|
10
10
|
function isPositiveInteger(value) {
|
|
11
11
|
return Number.isInteger(value) && Number(value) > 0;
|
|
12
12
|
}
|
|
13
|
+
function isNonNegativeInteger(value) {
|
|
14
|
+
return Number.isInteger(value) && Number(value) >= 0;
|
|
15
|
+
}
|
|
13
16
|
function isString(value) {
|
|
14
17
|
return typeof value === "string";
|
|
15
18
|
}
|
|
@@ -91,6 +94,8 @@ const SUPPORTED_QC_PROVIDER_CAPABILITIES = [
|
|
|
91
94
|
"auto-fix",
|
|
92
95
|
"metrics-import",
|
|
93
96
|
];
|
|
97
|
+
const SUPPORTED_QC_OUTPUT_FORMATS = ["json", "jsonl", "sarif", "generic"];
|
|
98
|
+
const SUPPORTED_QC_FAILURE_ACTIONS = ["fail", "fallback", "ignore", "block"];
|
|
94
99
|
const SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
95
100
|
function severityIndex(severity) {
|
|
96
101
|
return SEVERITY_ORDER.indexOf(severity);
|
|
@@ -908,6 +913,158 @@ function validateConfig(config) {
|
|
|
908
913
|
}
|
|
909
914
|
}
|
|
910
915
|
}
|
|
916
|
+
if ("enabled" in providerConfig && providerConfig.enabled !== undefined && !isBoolean(providerConfig.enabled)) {
|
|
917
|
+
result.valid = false;
|
|
918
|
+
result.errors.push(`qc.providers.${providerName}.enabled must be a boolean`);
|
|
919
|
+
}
|
|
920
|
+
if ("execution" in providerConfig && providerConfig.execution !== undefined) {
|
|
921
|
+
if (!isPlainObject(providerConfig.execution)) {
|
|
922
|
+
result.valid = false;
|
|
923
|
+
result.errors.push(`qc.providers.${providerName}.execution must be a plain object`);
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
const execution = providerConfig.execution;
|
|
927
|
+
if ("command" in execution && execution.command !== undefined && !isString(execution.command)) {
|
|
928
|
+
result.valid = false;
|
|
929
|
+
result.errors.push(`qc.providers.${providerName}.execution.command must be a string`);
|
|
930
|
+
}
|
|
931
|
+
if ("args" in execution && execution.args !== undefined && !isStringArray(execution.args)) {
|
|
932
|
+
result.valid = false;
|
|
933
|
+
result.errors.push(`qc.providers.${providerName}.execution.args must be an array of strings`);
|
|
934
|
+
}
|
|
935
|
+
if ("output" in execution && execution.output !== undefined) {
|
|
936
|
+
if (!isPlainObject(execution.output)) {
|
|
937
|
+
result.valid = false;
|
|
938
|
+
result.errors.push(`qc.providers.${providerName}.execution.output must be a plain object`);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
if ("format" in execution.output &&
|
|
942
|
+
execution.output.format !== undefined &&
|
|
943
|
+
(!isString(execution.output.format) ||
|
|
944
|
+
!SUPPORTED_QC_OUTPUT_FORMATS.includes(execution.output.format))) {
|
|
945
|
+
result.valid = false;
|
|
946
|
+
result.errors.push(`qc.providers.${providerName}.execution.output.format must be one of json, jsonl, sarif, generic`);
|
|
947
|
+
}
|
|
948
|
+
if ("parser" in execution.output &&
|
|
949
|
+
execution.output.parser !== undefined &&
|
|
950
|
+
!isString(execution.output.parser)) {
|
|
951
|
+
result.valid = false;
|
|
952
|
+
result.errors.push(`qc.providers.${providerName}.execution.output.parser must be a string`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if ("configPath" in execution &&
|
|
957
|
+
execution.configPath !== undefined &&
|
|
958
|
+
!isString(execution.configPath)) {
|
|
959
|
+
result.valid = false;
|
|
960
|
+
result.errors.push(`qc.providers.${providerName}.execution.configPath must be a string`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if ("timeoutMs" in providerConfig && providerConfig.timeoutMs !== undefined && !isPositiveInteger(providerConfig.timeoutMs)) {
|
|
965
|
+
result.valid = false;
|
|
966
|
+
result.errors.push(`qc.providers.${providerName}.timeoutMs must be a positive integer`);
|
|
967
|
+
}
|
|
968
|
+
if ("primary" in providerConfig && providerConfig.primary !== undefined && !isBoolean(providerConfig.primary)) {
|
|
969
|
+
result.valid = false;
|
|
970
|
+
result.errors.push(`qc.providers.${providerName}.primary must be a boolean`);
|
|
971
|
+
}
|
|
972
|
+
if ("fallback" in providerConfig && providerConfig.fallback !== undefined && !isStringArray(providerConfig.fallback)) {
|
|
973
|
+
result.valid = false;
|
|
974
|
+
result.errors.push(`qc.providers.${providerName}.fallback must be an array of strings`);
|
|
975
|
+
}
|
|
976
|
+
if ("failurePolicy" in providerConfig && providerConfig.failurePolicy !== undefined) {
|
|
977
|
+
if (!isPlainObject(providerConfig.failurePolicy)) {
|
|
978
|
+
result.valid = false;
|
|
979
|
+
result.errors.push(`qc.providers.${providerName}.failurePolicy must be a plain object`);
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
for (const key of ["timeout", "parseFailure", "allProvidersFailed"]) {
|
|
983
|
+
const value = providerConfig.failurePolicy[key];
|
|
984
|
+
if (value !== undefined && (!isString(value) || !SUPPORTED_QC_FAILURE_ACTIONS.includes(value))) {
|
|
985
|
+
result.valid = false;
|
|
986
|
+
result.errors.push(`qc.providers.${providerName}.failurePolicy.${key} must be one of fail, fallback, ignore, block`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if ("rateLimit" in providerConfig && providerConfig.rateLimit !== undefined) {
|
|
992
|
+
if (!isPlainObject(providerConfig.rateLimit)) {
|
|
993
|
+
result.valid = false;
|
|
994
|
+
result.errors.push(`qc.providers.${providerName}.rateLimit must be a plain object`);
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
if ("requestsPerMinute" in providerConfig.rateLimit &&
|
|
998
|
+
providerConfig.rateLimit.requestsPerMinute !== undefined &&
|
|
999
|
+
!isPositiveInteger(providerConfig.rateLimit.requestsPerMinute)) {
|
|
1000
|
+
result.valid = false;
|
|
1001
|
+
result.errors.push(`qc.providers.${providerName}.rateLimit.requestsPerMinute must be a positive integer`);
|
|
1002
|
+
}
|
|
1003
|
+
if ("maxConcurrent" in providerConfig.rateLimit &&
|
|
1004
|
+
providerConfig.rateLimit.maxConcurrent !== undefined &&
|
|
1005
|
+
!isPositiveInteger(providerConfig.rateLimit.maxConcurrent)) {
|
|
1006
|
+
result.valid = false;
|
|
1007
|
+
result.errors.push(`qc.providers.${providerName}.rateLimit.maxConcurrent must be a positive integer`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if ("retry" in providerConfig && providerConfig.retry !== undefined) {
|
|
1012
|
+
if (!isPlainObject(providerConfig.retry)) {
|
|
1013
|
+
result.valid = false;
|
|
1014
|
+
result.errors.push(`qc.providers.${providerName}.retry must be a plain object`);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
if ("maxRetries" in providerConfig.retry &&
|
|
1018
|
+
providerConfig.retry.maxRetries !== undefined &&
|
|
1019
|
+
!isNonNegativeInteger(providerConfig.retry.maxRetries)) {
|
|
1020
|
+
result.valid = false;
|
|
1021
|
+
result.errors.push(`qc.providers.${providerName}.retry.maxRetries must be a non-negative integer`);
|
|
1022
|
+
}
|
|
1023
|
+
if ("backoffMs" in providerConfig.retry &&
|
|
1024
|
+
providerConfig.retry.backoffMs !== undefined &&
|
|
1025
|
+
!isNonNegativeInteger(providerConfig.retry.backoffMs)) {
|
|
1026
|
+
result.valid = false;
|
|
1027
|
+
result.errors.push(`qc.providers.${providerName}.retry.backoffMs must be a non-negative integer`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if ("artifactPolicy" in providerConfig && providerConfig.artifactPolicy !== undefined) {
|
|
1032
|
+
if (!isPlainObject(providerConfig.artifactPolicy)) {
|
|
1033
|
+
result.valid = false;
|
|
1034
|
+
result.errors.push(`qc.providers.${providerName}.artifactPolicy must be a plain object`);
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
if ("retainRawOutput" in providerConfig.artifactPolicy &&
|
|
1038
|
+
providerConfig.artifactPolicy.retainRawOutput !== undefined &&
|
|
1039
|
+
!isBoolean(providerConfig.artifactPolicy.retainRawOutput)) {
|
|
1040
|
+
result.valid = false;
|
|
1041
|
+
result.errors.push(`qc.providers.${providerName}.artifactPolicy.retainRawOutput must be a boolean`);
|
|
1042
|
+
}
|
|
1043
|
+
if ("outputDir" in providerConfig.artifactPolicy &&
|
|
1044
|
+
providerConfig.artifactPolicy.outputDir !== undefined &&
|
|
1045
|
+
!isString(providerConfig.artifactPolicy.outputDir)) {
|
|
1046
|
+
result.valid = false;
|
|
1047
|
+
result.errors.push(`qc.providers.${providerName}.artifactPolicy.outputDir must be a string`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// Validate fallback references after all provider keys are known.
|
|
1053
|
+
if (providers) {
|
|
1054
|
+
const providerKeys = new Set(Object.keys(config.qc.providers));
|
|
1055
|
+
for (const [providerName, providerConfig] of Object.entries(config.qc.providers)) {
|
|
1056
|
+
if (!isPlainObject(providerConfig))
|
|
1057
|
+
continue;
|
|
1058
|
+
const fallback = providerConfig.fallback;
|
|
1059
|
+
if (Array.isArray(fallback)) {
|
|
1060
|
+
for (const fallbackName of fallback) {
|
|
1061
|
+
if (isString(fallbackName) && !providerKeys.has(fallbackName)) {
|
|
1062
|
+
result.valid = false;
|
|
1063
|
+
result.errors.push(`qc.providers.${providerName}.fallback contains unknown provider: ${fallbackName}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
911
1068
|
}
|
|
912
1069
|
}
|
|
913
1070
|
}
|
package/dist/finalize/index.js
CHANGED
|
@@ -185,7 +185,7 @@ function resolveQcTelemetryFile(state, repoRoot) {
|
|
|
185
185
|
}
|
|
186
186
|
async function runQcGate(options) {
|
|
187
187
|
const { config, state, repoRoot, branch, trigger, prUrl, stepLabel } = options;
|
|
188
|
-
const registry = (0, index_js_2.
|
|
188
|
+
const registry = (0, index_js_2.createQcRegistry)(config);
|
|
189
189
|
const result = await (0, index_js_2.runQcAtTrigger)({
|
|
190
190
|
config,
|
|
191
191
|
registry,
|
package/dist/loop/parent.js
CHANGED
|
@@ -37,12 +37,15 @@ const node_child_process_1 = require("node:child_process");
|
|
|
37
37
|
const artifact_policy_js_1 = require("../finalize/artifact-policy.js");
|
|
38
38
|
const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
|
|
39
39
|
const triggers_js_1 = require("../qc/triggers.js");
|
|
40
|
+
const index_js_1 = require("../qc/index.js");
|
|
41
|
+
const repair_loop_js_1 = require("../qc/repair-loop.js");
|
|
42
|
+
const worker_packet_js_2 = require("./worker-packet.js");
|
|
40
43
|
const run_bootstrap_js_1 = require("./run-bootstrap.js");
|
|
41
44
|
const ledger_js_1 = require("./ledger.js");
|
|
42
45
|
const dispatch_js_1 = require("./dispatch.js");
|
|
43
|
-
const
|
|
46
|
+
const index_js_2 = require("./router/index.js");
|
|
44
47
|
const child_selector_js_1 = require("../runtime/scheduling/child-selector.js");
|
|
45
|
-
const
|
|
48
|
+
const index_js_3 = require("../tracker/index.js");
|
|
46
49
|
const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
|
|
47
50
|
const local_graph_js_1 = require("../tracker/local-graph.js");
|
|
48
51
|
const CLAIM_TTL_MS = 30 * 60 * 1000;
|
|
@@ -813,7 +816,7 @@ async function runParentLoop(options) {
|
|
|
813
816
|
if (openChildrenNeedingScope.length > 0) {
|
|
814
817
|
process.stderr.write(`[polaris] ${openChildrenNeedingScope.length} children missing scope — attempting tracker sync-in...\n`);
|
|
815
818
|
try {
|
|
816
|
-
await (0,
|
|
819
|
+
await (0, index_js_3.loadTrackerGraph)(config, state.cluster_id);
|
|
817
820
|
process.stderr.write(`[polaris] sync-in complete.\n`);
|
|
818
821
|
}
|
|
819
822
|
catch (syncErr) {
|
|
@@ -844,7 +847,7 @@ async function runParentLoop(options) {
|
|
|
844
847
|
max_concurrent: maxConcurrentWorkers,
|
|
845
848
|
claim_ttl_ms: CLAIM_TTL_MS,
|
|
846
849
|
get_dependencies: (childId) => localGraph?.getDependencies(childId) ?? [],
|
|
847
|
-
decide_route: ({ activeSlotsByProvider }) => (0,
|
|
850
|
+
decide_route: ({ activeSlotsByProvider }) => (0, index_js_2.decideWorkerRoute)({
|
|
848
851
|
role: "worker",
|
|
849
852
|
taskType: "impl",
|
|
850
853
|
adapter: adapterName,
|
|
@@ -884,6 +887,154 @@ async function runParentLoop(options) {
|
|
|
884
887
|
};
|
|
885
888
|
}
|
|
886
889
|
const autoFinalizeRequested = orchestrationMode === "auto" && config.orchestration?.auto_finalize === true;
|
|
890
|
+
// ── QC repair loop (post-completion gate) ─────────────────────────────
|
|
891
|
+
// When QC is enabled, run the completed-cluster QC trigger and, if
|
|
892
|
+
// findings are produced, run the bounded repair loop before halting.
|
|
893
|
+
// The repair loop is Foreman-owned: it dispatches repair workers via the
|
|
894
|
+
// same adapter and NEVER implements repairs inline.
|
|
895
|
+
if (!dryRun && config.qc?.enabled) {
|
|
896
|
+
const qcRegistry = (0, index_js_1.createQcRegistry)(config.qc);
|
|
897
|
+
let initialQcResult;
|
|
898
|
+
try {
|
|
899
|
+
initialQcResult = await (0, index_js_1.runQcAtTrigger)({
|
|
900
|
+
config: config.qc,
|
|
901
|
+
registry: qcRegistry,
|
|
902
|
+
trigger: "completed-cluster",
|
|
903
|
+
repoRoot,
|
|
904
|
+
runId: state.run_id,
|
|
905
|
+
clusterId: state.cluster_id,
|
|
906
|
+
branch: state.branch ?? getCurrentBranch(repoRoot),
|
|
907
|
+
telemetryFile,
|
|
908
|
+
state,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
913
|
+
appendTelemetry(telemetryFile, {
|
|
914
|
+
event: "qc-repair-loop-qc-run-error",
|
|
915
|
+
run_id: state.run_id,
|
|
916
|
+
error: msg,
|
|
917
|
+
timestamp: new Date().toISOString(),
|
|
918
|
+
});
|
|
919
|
+
// Non-fatal: proceed to cluster-complete without repair loop.
|
|
920
|
+
initialQcResult = null;
|
|
921
|
+
}
|
|
922
|
+
const hasFindings = initialQcResult !== null &&
|
|
923
|
+
initialQcResult.results.some((r) => r.findings.length > 0 && r.status !== "passed" && r.status !== "skipped");
|
|
924
|
+
if (initialQcResult !== null && hasFindings) {
|
|
925
|
+
const maxRepairRounds = config.qc.maxRepairRounds ?? repair_loop_js_1.DEFAULT_MAX_REPAIR_ROUNDS;
|
|
926
|
+
const priorLoopState = state.qc_repair_loop ?? null;
|
|
927
|
+
// Build the repair worker dispatcher using the existing adapter + provider.
|
|
928
|
+
const repairDispatcher = async (packet, round, manifest) => {
|
|
929
|
+
const repairPacketId = packet.packetId;
|
|
930
|
+
const dispatchId = (0, node_crypto_1.randomUUID)();
|
|
931
|
+
const repairWorkerId = `${state.run_id}:repair-${repairPacketId}:${Date.now()}`;
|
|
932
|
+
const repairResultPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "results", `repair-${repairPacketId}-${dispatchId}.json`);
|
|
933
|
+
const workerPacket = (0, worker_packet_js_2.compileRepairWorkerPacket)({
|
|
934
|
+
runId: state.run_id,
|
|
935
|
+
clusterId: state.cluster_id,
|
|
936
|
+
packetId: repairPacketId,
|
|
937
|
+
branch: state.branch ?? getCurrentBranch(repoRoot),
|
|
938
|
+
stateFile,
|
|
939
|
+
telemetryFile,
|
|
940
|
+
round,
|
|
941
|
+
allowedScope: packet.allowedScope,
|
|
942
|
+
prohibitedScope: packet.prohibitedScope,
|
|
943
|
+
validationCommands: packet.validationCommands,
|
|
944
|
+
rootCauseHint: packet.rootCauseHint,
|
|
945
|
+
resultFile: repairResultPath,
|
|
946
|
+
maxConcurrentWorkers,
|
|
947
|
+
});
|
|
948
|
+
appendTelemetry(telemetryFile, {
|
|
949
|
+
event: "repair-worker-dispatched",
|
|
950
|
+
run_id: state.run_id,
|
|
951
|
+
cluster_id: state.cluster_id,
|
|
952
|
+
packet_id: repairPacketId,
|
|
953
|
+
worker_id: repairWorkerId,
|
|
954
|
+
round,
|
|
955
|
+
timestamp: new Date().toISOString(),
|
|
956
|
+
});
|
|
957
|
+
try {
|
|
958
|
+
const dispatchResult = await adapter.dispatch(workerPacket, { provider: providerName, dryRun });
|
|
959
|
+
const workerSummary = parseWorkerSummary(dispatchResult.summary);
|
|
960
|
+
const success = workerSummary?.status === "done";
|
|
961
|
+
appendTelemetry(telemetryFile, {
|
|
962
|
+
event: "repair-worker-completed",
|
|
963
|
+
run_id: state.run_id,
|
|
964
|
+
packet_id: repairPacketId,
|
|
965
|
+
worker_id: repairWorkerId,
|
|
966
|
+
round,
|
|
967
|
+
status: success ? "success" : "failure",
|
|
968
|
+
timestamp: new Date().toISOString(),
|
|
969
|
+
});
|
|
970
|
+
return {
|
|
971
|
+
packetId: repairPacketId,
|
|
972
|
+
status: success ? "success" : "failure",
|
|
973
|
+
commitSha: workerSummary?.commit,
|
|
974
|
+
errorMessage: success ? undefined : String(workerSummary?.error_message ?? "repair worker failed"),
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
catch (err) {
|
|
978
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
979
|
+
appendTelemetry(telemetryFile, {
|
|
980
|
+
event: "repair-worker-error",
|
|
981
|
+
run_id: state.run_id,
|
|
982
|
+
packet_id: repairPacketId,
|
|
983
|
+
worker_id: repairWorkerId,
|
|
984
|
+
round,
|
|
985
|
+
error: msg,
|
|
986
|
+
timestamp: new Date().toISOString(),
|
|
987
|
+
});
|
|
988
|
+
return {
|
|
989
|
+
packetId: repairPacketId,
|
|
990
|
+
status: "failure",
|
|
991
|
+
errorMessage: msg,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
try {
|
|
996
|
+
const repairLoopResult = await (0, repair_loop_js_1.runQcRepairLoop)({
|
|
997
|
+
clusterId: state.cluster_id,
|
|
998
|
+
runId: state.run_id,
|
|
999
|
+
branch: state.branch ?? getCurrentBranch(repoRoot),
|
|
1000
|
+
repoRoot,
|
|
1001
|
+
telemetryFile,
|
|
1002
|
+
config: config.qc,
|
|
1003
|
+
registry: qcRegistry,
|
|
1004
|
+
initialQcResults: initialQcResult.results,
|
|
1005
|
+
dispatchRepairWorker: repairDispatcher,
|
|
1006
|
+
maxRounds: maxRepairRounds,
|
|
1007
|
+
priorLoopState,
|
|
1008
|
+
onStateUpdate: (loopState) => {
|
|
1009
|
+
// Persist loop state to the state file on each mutation.
|
|
1010
|
+
const stateWithLoop = { ...state, qc_repair_loop: loopState };
|
|
1011
|
+
(0, checkpoint_js_1.writeStateAtomic)(stateFile, stateWithLoop);
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
state = { ...state, qc_repair_loop: repairLoopResult.loop_state };
|
|
1015
|
+
(0, checkpoint_js_1.writeStateAtomic)(stateFile, state);
|
|
1016
|
+
appendTelemetry(telemetryFile, {
|
|
1017
|
+
event: "qc-repair-loop-finished",
|
|
1018
|
+
run_id: state.run_id,
|
|
1019
|
+
outcome: repairLoopResult.outcome,
|
|
1020
|
+
rounds_completed: repairLoopResult.rounds_completed,
|
|
1021
|
+
summary: repairLoopResult.summary,
|
|
1022
|
+
timestamp: new Date().toISOString(),
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
catch (err) {
|
|
1026
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1027
|
+
appendTelemetry(telemetryFile, {
|
|
1028
|
+
event: "qc-repair-loop-error",
|
|
1029
|
+
run_id: state.run_id,
|
|
1030
|
+
error: msg,
|
|
1031
|
+
timestamp: new Date().toISOString(),
|
|
1032
|
+
});
|
|
1033
|
+
// Non-fatal: proceed to cluster-complete.
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// ── End QC repair loop ─────────────────────────────────────────────────
|
|
887
1038
|
// All children completed — write final state and halt
|
|
888
1039
|
if (!dryRun) {
|
|
889
1040
|
logStatus(notificationFormat, "COMPLETE");
|
|
@@ -1427,7 +1578,7 @@ async function runParentLoop(options) {
|
|
|
1427
1578
|
// Errors are logged to telemetry but do not fail the halt.
|
|
1428
1579
|
let adapter;
|
|
1429
1580
|
try {
|
|
1430
|
-
adapter = (0,
|
|
1581
|
+
adapter = (0, index_js_3.loadTrackerAdapter)(config);
|
|
1431
1582
|
}
|
|
1432
1583
|
catch (err) {
|
|
1433
1584
|
const errorMsg = err instanceof Error ? err.message : String(err);
|