@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.
@@ -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
  }
@@ -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.createDefaultQcRegistry)();
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,
@@ -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 index_js_1 = require("./router/index.js");
46
+ const index_js_2 = require("./router/index.js");
44
47
  const child_selector_js_1 = require("../runtime/scheduling/child-selector.js");
45
- const index_js_2 = require("../tracker/index.js");
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, index_js_2.loadTrackerGraph)(config, state.cluster_id);
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, index_js_1.decideWorkerRoute)({
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, index_js_2.loadTrackerAdapter)(config);
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);
@@ -108,6 +108,8 @@ async function runQcAtTrigger(options) {
108
108
  branch,
109
109
  telemetryFile,
110
110
  timeoutMs,
111
+ config,
112
+ registry,
111
113
  });
112
114
  const result = applyAttributionAndRouting(rawResult, config, attributionContext, ownership, routeName);
113
115
  try {