@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/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);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* accept BootstrapPacket transparently accept WorkerPacket.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.STARTUP_RETURN_CONTRACT = exports.PREFLIGHT_RETURN_CONTRACT = exports.WORKER_PROHIBITED_WRITE_PATHS = exports.FINALIZE_RETURN_CONTRACT = exports.IMPL_RETURN_CONTRACT = void 0;
|
|
14
|
+
exports.REPAIR_RETURN_CONTRACT = exports.STARTUP_RETURN_CONTRACT = exports.PREFLIGHT_RETURN_CONTRACT = exports.WORKER_PROHIBITED_WRITE_PATHS = exports.FINALIZE_RETURN_CONTRACT = exports.IMPL_RETURN_CONTRACT = void 0;
|
|
15
15
|
exports.roleContextForWorkerRole = roleContextForWorkerRole;
|
|
16
16
|
exports.isWorkerPacket = isWorkerPacket;
|
|
17
17
|
exports.getWorkerCommitPolicy = getWorkerCommitPolicy;
|
|
@@ -19,6 +19,7 @@ exports.compileStartupPacket = compileStartupPacket;
|
|
|
19
19
|
exports.compileImplPacket = compileImplPacket;
|
|
20
20
|
exports.compileFinalizePacket = compileFinalizePacket;
|
|
21
21
|
exports.compilePreflightPacket = compilePreflightPacket;
|
|
22
|
+
exports.compileRepairWorkerPacket = compileRepairWorkerPacket;
|
|
22
23
|
const worker_prompt_js_1 = require("./worker-prompt.js");
|
|
23
24
|
const body_parser_js_1 = require("./body-parser.js");
|
|
24
25
|
const WORKER_ROLE_CONTEXT = {
|
|
@@ -421,3 +422,76 @@ function compilePreflightPacket(input) {
|
|
|
421
422
|
},
|
|
422
423
|
};
|
|
423
424
|
}
|
|
425
|
+
/** Return contract for repair workers (same shape as impl). */
|
|
426
|
+
exports.REPAIR_RETURN_CONTRACT = [
|
|
427
|
+
'child_id',
|
|
428
|
+
'status',
|
|
429
|
+
'commit',
|
|
430
|
+
'validation',
|
|
431
|
+
'next_recommended_action',
|
|
432
|
+
];
|
|
433
|
+
/**
|
|
434
|
+
* Build a compiled repair worker packet.
|
|
435
|
+
*
|
|
436
|
+
* Repair workers use `worker_role: "repair"` and the same sealed packet/result
|
|
437
|
+
* contracts as impl workers, but with tightly scoped allowed/prohibited paths
|
|
438
|
+
* derived from the compiled repair packet manifest.
|
|
439
|
+
*
|
|
440
|
+
* The parent/orchestrator MUST use this function and MUST NOT implement
|
|
441
|
+
* repair work inline.
|
|
442
|
+
*/
|
|
443
|
+
function compileRepairWorkerPacket(input) {
|
|
444
|
+
const steps = [
|
|
445
|
+
`Confirm QC repair scope: allowed=${JSON.stringify(input.allowedScope)}, prohibited=${JSON.stringify(input.prohibitedScope)}.`,
|
|
446
|
+
`Review root-cause hint: ${input.rootCauseHint}`,
|
|
447
|
+
`Apply minimal repair to files within allowed scope only. Do NOT touch prohibited paths.`,
|
|
448
|
+
`Run validation commands and confirm all pass.`,
|
|
449
|
+
`Create exactly ONE git commit: [REPAIR] ${input.clusterId} round-${input.round} packet-${input.packetId}.`,
|
|
450
|
+
`Write compact return JSON to stdout (fields: ${exports.REPAIR_RETURN_CONTRACT.join(', ')}). next_recommended_action MUST be "continue" on success, "stop" on unresolvable blocker.`,
|
|
451
|
+
`TERMINATE SESSION IMMEDIATELY.`,
|
|
452
|
+
];
|
|
453
|
+
return {
|
|
454
|
+
schema_version: '2.1',
|
|
455
|
+
worker_role: 'repair',
|
|
456
|
+
run_id: input.runId,
|
|
457
|
+
cluster_id: input.clusterId,
|
|
458
|
+
active_child: input.packetId,
|
|
459
|
+
state_file: input.stateFile,
|
|
460
|
+
telemetry_file: input.telemetryFile,
|
|
461
|
+
instructions: {
|
|
462
|
+
primary_goal: `QC repair worker for cluster ${input.clusterId}, round ${input.round}, packet ${input.packetId}. ` +
|
|
463
|
+
`Apply minimal targeted repairs to findings within the allowed scope. Root cause: ${input.rootCauseHint}`,
|
|
464
|
+
steps,
|
|
465
|
+
allowed_scope: input.allowedScope,
|
|
466
|
+
validation_commands: input.validationCommands,
|
|
467
|
+
},
|
|
468
|
+
lifecycle: defaultLifecycle(input.maxConcurrentWorkers ?? 1, 'commit-and-exit'),
|
|
469
|
+
return_contract: exports.REPAIR_RETURN_CONTRACT,
|
|
470
|
+
prompt_mode: 'full',
|
|
471
|
+
prompt_metrics: { mode: 'full', char_count: 0, estimated_tokens: 0 },
|
|
472
|
+
role_context: roleContextForWorkerRole('repair'),
|
|
473
|
+
routing_context: {
|
|
474
|
+
task_type: "repair",
|
|
475
|
+
required_capabilities: ["repair"],
|
|
476
|
+
},
|
|
477
|
+
prohibited_write_paths: [
|
|
478
|
+
...exports.WORKER_PROHIBITED_WRITE_PATHS,
|
|
479
|
+
...input.prohibitedScope,
|
|
480
|
+
],
|
|
481
|
+
result_file_contract: {
|
|
482
|
+
result_file: input.resultFile,
|
|
483
|
+
result_required_fields: Object.fromEntries([
|
|
484
|
+
['run_id', input.runId],
|
|
485
|
+
['cluster_id', input.clusterId],
|
|
486
|
+
['child_id', input.packetId],
|
|
487
|
+
...exports.REPAIR_RETURN_CONTRACT.filter((key) => key !== 'child_id').map((key) => [key, `<${key}>`]),
|
|
488
|
+
]),
|
|
489
|
+
},
|
|
490
|
+
context: {
|
|
491
|
+
branch: input.branch,
|
|
492
|
+
worker_role: 'repair',
|
|
493
|
+
repair_round: input.round,
|
|
494
|
+
repair_packet_id: input.packetId,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TEST_CONFIG = void 0;
|
|
4
|
+
exports.makeFinding = makeFinding;
|
|
5
|
+
exports.makeResult = makeResult;
|
|
6
|
+
exports.makeSameFileFindings = makeSameFileFindings;
|
|
7
|
+
exports.makeCrossFileSubsystemFindings = makeCrossFileSubsystemFindings;
|
|
8
|
+
exports.makeOverlappingScopeFindings = makeOverlappingScopeFindings;
|
|
9
|
+
exports.makeHighRiskFindings = makeHighRiskFindings;
|
|
10
|
+
exports.makeLowConfidenceBroadFindings = makeLowConfidenceBroadFindings;
|
|
11
|
+
exports.DEFAULT_TEST_CONFIG = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
severityThresholds: { block: "high", repair: "medium", followUp: "low" },
|
|
14
|
+
};
|
|
15
|
+
function makeFinding(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
findingId: `f-${Math.random().toString(36).slice(2, 8)}`,
|
|
18
|
+
severity: "medium",
|
|
19
|
+
category: "style",
|
|
20
|
+
title: "Test finding",
|
|
21
|
+
fixAvailable: true,
|
|
22
|
+
autofixEligible: false,
|
|
23
|
+
attribution: { confidence: "high", reason: "changed-file-owner", childId: "POL-472" },
|
|
24
|
+
status: "open",
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function makeResult(overrides = {}) {
|
|
29
|
+
const now = new Date().toISOString();
|
|
30
|
+
return {
|
|
31
|
+
schemaVersion: "1.0",
|
|
32
|
+
qcRunId: `qc-run-${Math.random().toString(36).slice(2, 8)}`,
|
|
33
|
+
runId: "run-1",
|
|
34
|
+
clusterId: "POL-TEST",
|
|
35
|
+
trigger: "completed-cluster",
|
|
36
|
+
provider: "coderabbit",
|
|
37
|
+
providerMode: "local",
|
|
38
|
+
startedAt: now,
|
|
39
|
+
completedAt: now,
|
|
40
|
+
status: "findings",
|
|
41
|
+
findings: [],
|
|
42
|
+
rawArtifactPaths: [],
|
|
43
|
+
parserVersion: "coderabbit-1.0",
|
|
44
|
+
policyDecision: {
|
|
45
|
+
blocksDelivery: false,
|
|
46
|
+
requiresOperatorReview: false,
|
|
47
|
+
routedToRepair: false,
|
|
48
|
+
summary: "ok",
|
|
49
|
+
},
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function makeSameFileFindings() {
|
|
54
|
+
return [
|
|
55
|
+
makeFinding({
|
|
56
|
+
findingId: "f-same-1",
|
|
57
|
+
filePath: "src/auth/login.ts",
|
|
58
|
+
category: "style",
|
|
59
|
+
severity: "medium",
|
|
60
|
+
range: { startLine: 10, endLine: 15 },
|
|
61
|
+
}),
|
|
62
|
+
makeFinding({
|
|
63
|
+
findingId: "f-same-2",
|
|
64
|
+
filePath: "src/auth/login.ts",
|
|
65
|
+
category: "type-safety",
|
|
66
|
+
severity: "medium",
|
|
67
|
+
range: { startLine: 30, endLine: 35 },
|
|
68
|
+
}),
|
|
69
|
+
makeFinding({
|
|
70
|
+
findingId: "f-same-3",
|
|
71
|
+
filePath: "src/auth/login.ts",
|
|
72
|
+
category: "style",
|
|
73
|
+
severity: "medium",
|
|
74
|
+
range: { startLine: 12, endLine: 16 },
|
|
75
|
+
}),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function makeCrossFileSubsystemFindings() {
|
|
79
|
+
return [
|
|
80
|
+
makeFinding({
|
|
81
|
+
findingId: "f-sub-a",
|
|
82
|
+
filePath: "src/qc/compiler.ts",
|
|
83
|
+
category: "error-handling",
|
|
84
|
+
severity: "medium",
|
|
85
|
+
range: { startLine: 5, endLine: 8 },
|
|
86
|
+
}),
|
|
87
|
+
makeFinding({
|
|
88
|
+
findingId: "f-sub-b",
|
|
89
|
+
filePath: "src/qc/runner.ts",
|
|
90
|
+
category: "error-handling",
|
|
91
|
+
severity: "medium",
|
|
92
|
+
range: { startLine: 20, endLine: 24 },
|
|
93
|
+
}),
|
|
94
|
+
makeFinding({
|
|
95
|
+
findingId: "f-sub-c",
|
|
96
|
+
filePath: "src/loop/worker.ts",
|
|
97
|
+
category: "error-handling",
|
|
98
|
+
severity: "medium",
|
|
99
|
+
range: { startLine: 7, endLine: 11 },
|
|
100
|
+
}),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
function makeOverlappingScopeFindings() {
|
|
104
|
+
return [
|
|
105
|
+
makeFinding({
|
|
106
|
+
findingId: "f-overlap-1",
|
|
107
|
+
filePath: "src/core/state.ts",
|
|
108
|
+
category: "race-condition",
|
|
109
|
+
severity: "high",
|
|
110
|
+
range: { startLine: 40, endLine: 45 },
|
|
111
|
+
}),
|
|
112
|
+
makeFinding({
|
|
113
|
+
findingId: "f-overlap-2",
|
|
114
|
+
filePath: "src/core/state.ts",
|
|
115
|
+
category: "race-condition",
|
|
116
|
+
severity: "high",
|
|
117
|
+
range: { startLine: 42, endLine: 48 },
|
|
118
|
+
}),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
function makeHighRiskFindings() {
|
|
122
|
+
return [
|
|
123
|
+
makeFinding({
|
|
124
|
+
findingId: "f-security-1",
|
|
125
|
+
filePath: "src/auth/token.ts",
|
|
126
|
+
category: "security",
|
|
127
|
+
severity: "high",
|
|
128
|
+
range: { startLine: 1, endLine: 5 },
|
|
129
|
+
}),
|
|
130
|
+
makeFinding({
|
|
131
|
+
findingId: "f-security-2",
|
|
132
|
+
filePath: "src/auth/token.ts",
|
|
133
|
+
category: "auth",
|
|
134
|
+
severity: "medium",
|
|
135
|
+
range: { startLine: 10, endLine: 14 },
|
|
136
|
+
}),
|
|
137
|
+
makeFinding({
|
|
138
|
+
findingId: "f-migration-1",
|
|
139
|
+
filePath: "src/db/migrate.ts",
|
|
140
|
+
category: "migration",
|
|
141
|
+
severity: "medium",
|
|
142
|
+
range: { startLine: 3, endLine: 7 },
|
|
143
|
+
}),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
function makeLowConfidenceBroadFindings() {
|
|
147
|
+
return [
|
|
148
|
+
makeFinding({
|
|
149
|
+
findingId: "f-broad-1",
|
|
150
|
+
filePath: undefined,
|
|
151
|
+
category: "architecture",
|
|
152
|
+
severity: "medium",
|
|
153
|
+
attribution: { confidence: "low", reason: "provider-uncertain" },
|
|
154
|
+
}),
|
|
155
|
+
makeFinding({
|
|
156
|
+
findingId: "f-broad-2",
|
|
157
|
+
filePath: undefined,
|
|
158
|
+
category: "architecture",
|
|
159
|
+
severity: "low",
|
|
160
|
+
attribution: { confidence: "unattributed", reason: "unattributed" },
|
|
161
|
+
}),
|
|
162
|
+
makeFinding({
|
|
163
|
+
findingId: "f-preexisting-1",
|
|
164
|
+
filePath: "src/legacy/old.ts",
|
|
165
|
+
category: "tech-debt",
|
|
166
|
+
severity: "low",
|
|
167
|
+
attribution: { confidence: "medium", reason: "pre-existing" },
|
|
168
|
+
}),
|
|
169
|
+
];
|
|
170
|
+
}
|
package/dist/qc/index.js
CHANGED
|
@@ -28,3 +28,5 @@ __exportStar(require("./registry.js"), exports);
|
|
|
28
28
|
__exportStar(require("./attribution.js"), exports);
|
|
29
29
|
__exportStar(require("./autofix.js"), exports);
|
|
30
30
|
__exportStar(require("./routing.js"), exports);
|
|
31
|
+
__exportStar(require("./repair-packets.js"), exports);
|
|
32
|
+
__exportStar(require("./repair-loop.js"), exports);
|
package/dist/qc/orchestration.js
CHANGED