@lsctech/polaris 0.5.6 → 0.5.8

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.
Files changed (37) hide show
  1. package/dist/autoresearch/index.js +30 -1
  2. package/dist/autoresearch/proposal.js +5 -0
  3. package/dist/autoresearch/score.js +174 -2
  4. package/dist/autoresearch/sol-evaluation-writer.js +135 -0
  5. package/dist/autoresearch/sol-evidence-loader.js +40 -3
  6. package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
  7. package/dist/autoresearch/sol-recommendations.js +212 -0
  8. package/dist/autoresearch/sol-report-renderer.js +282 -0
  9. package/dist/autoresearch/sol-report.js +44 -0
  10. package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
  11. package/dist/autoresearch/sol-scorer.js +45 -1
  12. package/dist/cli/autoresearch.js +77 -0
  13. package/dist/cluster-state/store.js +56 -0
  14. package/dist/config/defaults.js +1 -0
  15. package/dist/config/validator.js +157 -0
  16. package/dist/finalize/artifact-policy.js +2 -0
  17. package/dist/finalize/github.js +13 -9
  18. package/dist/finalize/index.js +198 -4
  19. package/dist/finalize/linear.js +8 -4
  20. package/dist/finalize/steps/08-create-pr.js +2 -2
  21. package/dist/finalize/steps/11-update-linear.js +2 -2
  22. package/dist/loop/parent.js +189 -33
  23. package/dist/loop/worker-packet.js +75 -1
  24. package/dist/qc/artifacts.js +27 -0
  25. package/dist/qc/fixtures/repair-packets.js +170 -0
  26. package/dist/qc/index.js +2 -0
  27. package/dist/qc/orchestration.js +5 -2
  28. package/dist/qc/policy.js +30 -3
  29. package/dist/qc/providers/coderabbit.js +152 -17
  30. package/dist/qc/registry.js +36 -3
  31. package/dist/qc/repair-loop.js +458 -0
  32. package/dist/qc/repair-packets.js +420 -0
  33. package/dist/qc/runner.js +328 -37
  34. package/dist/qc/schemas.js +79 -1
  35. package/dist/types/sol-metrics.js +108 -0
  36. package/dist/types/sol-scorecard.js +148 -0
  37. package/package.json +1 -1
@@ -207,6 +207,48 @@ function scoreForemanCompletion(ev) {
207
207
  detail: `succeeded=${succeeded}/${total}`,
208
208
  });
209
209
  }
210
+ /**
211
+ * qc_repair_loop: Observe QC repair loop outcomes.
212
+ *
213
+ * Scores repair loop terminal states without treating provider findings as
214
+ * ground truth. Best outcome = loop passed or no repairable findings.
215
+ * Worst = all providers failed, operator review required, or Medic referral.
216
+ * Skipped when QC is not configured or no repair loop ran.
217
+ */
218
+ function scoreForemanQcRepairLoop(ev) {
219
+ if (ev.qc.availability === "future" || ev.qc.availability === "unavailable") {
220
+ return skipped("qc_repair_loop", `QC evidence availability=${ev.qc.availability}`);
221
+ }
222
+ const loop = ev.qc.repair_loop;
223
+ if (!loop) {
224
+ return skipped("qc_repair_loop", "no QC repair loop data available");
225
+ }
226
+ const { status, rounds_completed, max_rounds, packets_compiled, packets_failed, rerun_outcome } = loop;
227
+ switch (status) {
228
+ case "passed":
229
+ case "repaired":
230
+ case "no-repairable":
231
+ return dim("qc_repair_loop", 1.0, "high", {
232
+ detail: `status=${status}, rounds=${rounds_completed}/${max_rounds}, packets=${packets_compiled}, rerun=${rerun_outcome ?? "n/a"}`,
233
+ });
234
+ case "max-rounds":
235
+ return dim("qc_repair_loop", 0.5, "high", {
236
+ detail: `status=${status}, rounds=${rounds_completed}/${max_rounds}, packets=${packets_compiled}`,
237
+ });
238
+ case "all-providers-failed":
239
+ case "operator-review":
240
+ case "medic-referral":
241
+ return dim("qc_repair_loop", 0.0, "high", {
242
+ detail: `status=${status}, failed_packets=${packets_failed}, rerun=${rerun_outcome ?? "n/a"}`,
243
+ });
244
+ case "not-run":
245
+ case "not-configured":
246
+ case "in-progress":
247
+ case "unknown":
248
+ default:
249
+ return skipped("qc_repair_loop", `QC repair loop status=${status}`);
250
+ }
251
+ }
210
252
  /**
211
253
  * recovery: State repair detection.
212
254
  * Score = 1.0 when no medic/repair artifacts detected; 0.0 when repair required.
@@ -388,10 +430,11 @@ function computeForemanScore(ev) {
388
430
  const scopeDim = scoreForemanScope(ev);
389
431
  const completionDim = scoreForemanCompletion(ev);
390
432
  const recoveryDim = scoreForemanRecovery(ev);
433
+ const qcRepairLoopDim = scoreForemanQcRepairLoop(ev);
391
434
  const dims = [
392
435
  tokenDim, durationDim, interventionDim, preAnalysisDim,
393
436
  dependencyDim, dispatchDim, evidenceValidationDim, scopeDim,
394
- completionDim, recoveryDim,
437
+ completionDim, recoveryDim, qcRepairLoopDim,
395
438
  ];
396
439
  const { score: composite_score, confidence: composite_confidence } = composite(dims);
397
440
  return {
@@ -407,6 +450,7 @@ function computeForemanScore(ev) {
407
450
  scope: scopeDim,
408
451
  completion: completionDim,
409
452
  recovery: recoveryDim,
453
+ qc_repair_loop: qcRepairLoopDim,
410
454
  };
411
455
  }
412
456
  /**
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createAutoresearchCommand = void 0;
4
4
  exports.createSolCommand = createSolCommand;
5
5
  const node_path_1 = require("node:path");
6
+ const node_fs_1 = require("node:fs");
6
7
  const commander_1 = require("commander");
7
8
  const dev_gate_js_1 = require("../autoresearch/dev-gate.js");
8
9
  const score_js_1 = require("../autoresearch/score.js");
@@ -12,6 +13,9 @@ const sol_evidence_loader_js_1 = require("../autoresearch/sol-evidence-loader.js
12
13
  const sol_scorer_js_1 = require("../autoresearch/sol-scorer.js");
13
14
  const sol_history_js_1 = require("../autoresearch/sol-history.js");
14
15
  const sol_report_js_1 = require("../autoresearch/sol-report.js");
16
+ const sol_scorecard_calculator_js_1 = require("../autoresearch/sol-scorecard-calculator.js");
17
+ const sol_evaluation_writer_js_1 = require("../autoresearch/sol-evaluation-writer.js");
18
+ const sol_report_renderer_js_1 = require("../autoresearch/sol-report-renderer.js");
15
19
  const sol_recommendations_js_1 = require("../autoresearch/sol-recommendations.js");
16
20
  function createSolCommand(options) {
17
21
  const repoRoot = options.repoRoot;
@@ -74,6 +78,79 @@ function createSolCommand(options) {
74
78
  process.exit(1);
75
79
  }
76
80
  });
81
+ sol
82
+ .command("report <run-id>")
83
+ .description("Generate SOL evaluation artifacts and a human-readable SmartDocs report for a run")
84
+ .option("-r, --repo-root <path>", "Repository root", repoRoot)
85
+ .option("--format <mode>", "Output format: markdown or json", "markdown")
86
+ .option("--json", "Output raw JSON (same as --format json)")
87
+ .option("--no-write", "Skip writing artifact files")
88
+ .action((runId, cmdOptions) => {
89
+ const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
90
+ if (cmdOptions.format !== "markdown" && cmdOptions.format !== "json") {
91
+ process.stderr.write(`sol report error: invalid --format "${cmdOptions.format}" (expected "markdown" or "json")\n`);
92
+ process.exit(1);
93
+ }
94
+ try {
95
+ (0, dev_gate_js_1.assertPolarisDevContext)(root);
96
+ }
97
+ catch (err) {
98
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
99
+ process.exit(1);
100
+ }
101
+ let artifacts;
102
+ let evidence;
103
+ try {
104
+ artifacts = (0, score_js_1.loadRunArtifacts)(root, runId);
105
+ evidence = (0, sol_evidence_loader_js_1.aggregateSolEvidence)(artifacts);
106
+ }
107
+ catch (err) {
108
+ process.stderr.write(`sol report error: cannot load run artifacts: ${err instanceof Error ? err.message : String(err)}\n`);
109
+ process.exit(1);
110
+ }
111
+ try {
112
+ const report = (0, sol_scorer_js_1.computeSolScoreReport)(evidence);
113
+ const scorecards = (0, sol_scorecard_calculator_js_1.computeAllScorecards)(evidence);
114
+ const qcRecommendations = (0, sol_recommendations_js_1.generateQcRecommendations)(evidence);
115
+ let evaluationPath;
116
+ let scorecardPaths;
117
+ let markdownPath;
118
+ const evaluationRecord = (0, sol_evaluation_writer_js_1.buildEvaluationRecord)(report);
119
+ const rendered = (0, sol_report_renderer_js_1.renderSolMarkdown)(evaluationRecord, scorecards, qcRecommendations);
120
+ if (cmdOptions.write) {
121
+ // Persist the same evaluationRecord object that was rendered, rather than rebuilding internally
122
+ const evaluationDir = (0, sol_evaluation_writer_js_1.getSolEvaluationsDir)(root);
123
+ if (!(0, node_fs_1.existsSync)(evaluationDir))
124
+ (0, node_fs_1.mkdirSync)(evaluationDir, { recursive: true });
125
+ evaluationPath = (0, sol_evaluation_writer_js_1.getEvaluationRecordPath)(root, report.run_id);
126
+ (0, node_fs_1.writeFileSync)(evaluationPath, JSON.stringify(evaluationRecord, null, 2) + "\n", "utf-8");
127
+ scorecardPaths = (0, sol_evaluation_writer_js_1.writeScorecardSet)(root, scorecards);
128
+ markdownPath = (0, sol_evaluation_writer_js_1.writeSolMarkdownReport)(root, runId, rendered.markdown);
129
+ }
130
+ const outputFormat = cmdOptions.json || cmdOptions.format === "json" ? "json" : "markdown";
131
+ if (outputFormat === "json") {
132
+ const output = JSON.stringify({
133
+ run_id: runId,
134
+ evaluation: evaluationRecord,
135
+ scorecards,
136
+ qc_recommendations: qcRecommendations,
137
+ artifacts: {
138
+ evaluation: evaluationPath,
139
+ scorecards: scorecardPaths,
140
+ markdown: markdownPath,
141
+ },
142
+ }, null, 2);
143
+ process.stdout.write(`${output}\n`);
144
+ }
145
+ else {
146
+ process.stdout.write(rendered.markdown);
147
+ }
148
+ }
149
+ catch (err) {
150
+ process.stderr.write(`sol report error: ${err instanceof Error ? err.message : String(err)}\n`);
151
+ process.exit(1);
152
+ }
153
+ });
77
154
  sol
78
155
  .command("propose <diagnosis-file>")
79
156
  .description("File Linear improvement proposals from a diagnosis report (dev-gated — never auto-applied)")
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.recordQcRun = exports.initializeClusterState = exports.writeClusterStateSync = exports.writeClusterState = exports.readClusterStateSync = exports.readClusterState = void 0;
37
37
  exports.pruneExpiredClaims = pruneExpiredClaims;
38
+ exports.pruneMissingQcRunPointers = pruneMissingQcRunPointers;
38
39
  const fs_1 = require("fs");
39
40
  const path = __importStar(require("path"));
40
41
  const local_graph_1 = require("../tracker/local-graph");
@@ -350,12 +351,25 @@ const recordQcRun = async (clusterId, result, repoRoot) => {
350
351
  if (!currentState) {
351
352
  throw new Error(`Cluster ${clusterId} state not found; cannot record QC run ${result.qcRunId}.`);
352
353
  }
354
+ const rawArtifactPaths = result.rawArtifactPaths ?? [];
355
+ const providerAttemptPath = result.providerAttempt?.rawOutputArtifactPath;
356
+ const auditArtifactPaths = providerAttemptPath
357
+ ? [...rawArtifactPaths, providerAttemptPath]
358
+ : rawArtifactPaths;
359
+ const failedRun = result.status === "failed" || result.status === "blocked" || result.allProvidersFailed === true;
360
+ let availability = "available";
361
+ if (failedRun && auditArtifactPaths.some((p) => !(0, fs_1.existsSync)(p))) {
362
+ availability = "unavailable";
363
+ }
353
364
  const pointer = {
354
365
  artifact_path: artifactPath,
355
366
  status: result.status,
356
367
  provider: result.provider,
357
368
  started_at: result.startedAt,
358
369
  completed_at: result.completedAt,
370
+ availability,
371
+ raw_artifact_paths: rawArtifactPaths.length > 0 ? rawArtifactPaths : undefined,
372
+ provider_attempt_artifact_path: providerAttemptPath,
359
373
  };
360
374
  const nextState = {
361
375
  ...currentState,
@@ -369,3 +383,45 @@ const recordQcRun = async (clusterId, result, repoRoot) => {
369
383
  return { artifactPath, state: nextState };
370
384
  };
371
385
  exports.recordQcRun = recordQcRun;
386
+ /**
387
+ * Remove QC run pointers whose primary artifacts are missing and mark
388
+ * pointers with missing raw audit artifacts as unavailable. Returns a new
389
+ * state object; callers must persist the result if they want the cleanup to
390
+ * be durable.
391
+ */
392
+ function pruneMissingQcRunPointers(state) {
393
+ const warnings = [];
394
+ const pruned = [];
395
+ if (!state.qc_runs) {
396
+ return { state, pruned, warnings };
397
+ }
398
+ const validation = (0, artifacts_js_1.validateQcArtifactPointers)(state.qc_runs);
399
+ if (validation.ok && validation.unavailable.length === 0) {
400
+ return { state, pruned, warnings };
401
+ }
402
+ const nextQcRuns = {};
403
+ for (const [runId, pointer] of Object.entries(state.qc_runs)) {
404
+ if (validation.missing.includes(pointer.artifact_path)) {
405
+ pruned.push(runId);
406
+ warnings.push(`QC run ${runId} pointer pruned: artifact missing ${pointer.artifact_path}`);
407
+ continue;
408
+ }
409
+ const updated = { ...pointer };
410
+ const hasMissingRaw = (pointer.raw_artifact_paths ?? []).some((p) => validation.unavailable.includes(p)) ||
411
+ (pointer.provider_attempt_artifact_path &&
412
+ validation.unavailable.includes(pointer.provider_attempt_artifact_path));
413
+ if (hasMissingRaw && updated.availability === "available") {
414
+ updated.availability = "unavailable";
415
+ warnings.push(`QC run ${runId} raw audit artifacts missing; marked unavailable`);
416
+ }
417
+ nextQcRuns[runId] = updated;
418
+ }
419
+ return {
420
+ state: {
421
+ ...state,
422
+ qc_runs: nextQcRuns,
423
+ },
424
+ pruned,
425
+ warnings,
426
+ };
427
+ }
@@ -119,5 +119,6 @@ exports.DEFAULT_CONFIG = {
119
119
  maxRuns: 10,
120
120
  },
121
121
  routes: {},
122
+ maxRepairRounds: 2,
122
123
  },
123
124
  };
@@ -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
  }
@@ -37,6 +37,7 @@ function isPromotedClusterArtifact(relativePath, activeClusterId) {
37
37
  const suffix = relativePath.slice(activeClusterPrefix.length);
38
38
  return (suffix === "clusters.json"
39
39
  || suffix === "cluster-state.json"
40
+ || suffix === "state.json"
40
41
  || suffix.startsWith("packets/")
41
42
  || suffix.startsWith("results/")
42
43
  || suffix.startsWith("qc/"));
@@ -131,6 +132,7 @@ function getArtifactPromotionPolicy(activeClusterId) {
131
132
  promoted: [
132
133
  `${activeClusterPrefix}clusters.json`,
133
134
  `${activeClusterPrefix}cluster-state.json`,
135
+ `${activeClusterPrefix}state.json`,
134
136
  `${activeClusterPrefix}packets/**`,
135
137
  `${activeClusterPrefix}results/**`,
136
138
  `${activeClusterPrefix}qc/**`,
@@ -1,9 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildPrBody = buildPrBody;
3
4
  exports.createDraftPr = createDraftPr;
4
5
  const node_child_process_1 = require("node:child_process");
6
+ function buildPrBody(state, branch, authoritativeChildCount) {
7
+ return [
8
+ `**Cluster ID:** ${state.cluster_id}`,
9
+ `**Run ID:** ${state.run_id}`,
10
+ `**Branch:** ${branch}`,
11
+ `**Children completed:** ${authoritativeChildCount ?? state.completed_children.length}`,
12
+ ``,
13
+ `_Generated by polaris finalize_`,
14
+ ].join("\n");
15
+ }
5
16
  function createDraftPr(options) {
6
- const { repoRoot, branch, state, draft } = options;
17
+ const { repoRoot, branch, state, draft, authoritativeChildCount } = options;
7
18
  if (!state.cluster_id) {
8
19
  throw new Error(`createDraftPr: state.cluster_id is empty — cannot create PR without a cluster identifier`);
9
20
  }
@@ -16,14 +27,7 @@ function createDraftPr(options) {
16
27
  `branch/cluster mismatch, aborting PR creation`);
17
28
  }
18
29
  const title = `polaris finalize: ${state.cluster_id} (${state.run_id})`;
19
- const body = [
20
- `**Cluster ID:** ${state.cluster_id}`,
21
- `**Run ID:** ${state.run_id}`,
22
- `**Branch:** ${branch}`,
23
- `**Children completed:** ${state.completed_children.length}`,
24
- ``,
25
- `_Generated by polaris finalize_`,
26
- ].join("\n");
30
+ const body = buildPrBody(state, branch, authoritativeChildCount);
27
31
  const args = [
28
32
  "pr", "create",
29
33
  "--title", title,