@lsctech/polaris 0.4.9 → 0.5.2

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 (42) hide show
  1. package/dist/autoresearch/gates.js +47 -0
  2. package/dist/autoresearch/proposal.js +183 -1
  3. package/dist/autoresearch/score.js +276 -0
  4. package/dist/cli/init.js +17 -0
  5. package/dist/cluster-state/store.js +70 -1
  6. package/dist/config/defaults.js +25 -0
  7. package/dist/config/validator.js +390 -0
  8. package/dist/finalize/artifact-policy.js +3 -1
  9. package/dist/finalize/index.js +73 -17
  10. package/dist/finalize/linear.js +45 -0
  11. package/dist/finalize/run-report.js +68 -12
  12. package/dist/loop/adapters/agent-subtask.js +19 -0
  13. package/dist/loop/adapters/cli-subtask-bridge.js +0 -1
  14. package/dist/loop/adapters/terminal-cli.js +194 -14
  15. package/dist/loop/checkpoint.js +40 -0
  16. package/dist/loop/dispatch-boundary.js +29 -0
  17. package/dist/loop/dispatch.js +193 -22
  18. package/dist/loop/orphan-recovery.js +56 -0
  19. package/dist/loop/parent.js +197 -22
  20. package/dist/loop/router/engine.js +229 -0
  21. package/dist/loop/router/index.js +5 -0
  22. package/dist/loop/router/types.js +2 -0
  23. package/dist/loop/worker-packet.js +16 -0
  24. package/dist/qc/artifacts.js +117 -0
  25. package/dist/qc/attribution.js +266 -0
  26. package/dist/qc/autofix.js +77 -0
  27. package/dist/qc/index.js +30 -0
  28. package/dist/qc/orchestration.js +150 -0
  29. package/dist/qc/policy.js +70 -0
  30. package/dist/qc/provider.js +28 -0
  31. package/dist/qc/providers/coderabbit.js +214 -0
  32. package/dist/qc/providers/index.js +5 -0
  33. package/dist/qc/registry.js +16 -0
  34. package/dist/qc/routing.js +55 -0
  35. package/dist/qc/runner.js +137 -0
  36. package/dist/qc/schemas.js +110 -0
  37. package/dist/qc/security-category.js +11 -0
  38. package/dist/qc/severity.js +78 -0
  39. package/dist/qc/triggers.js +92 -0
  40. package/dist/qc/types.js +9 -0
  41. package/dist/runtime/scheduling/child-selector.js +81 -0
  42. package/package.json +1 -1
@@ -23,6 +23,7 @@ exports.gateValidationFailed = gateValidationFailed;
23
23
  exports.gateWorkerWentOutOfScope = gateWorkerWentOutOfScope;
24
24
  exports.gateForemanTokenBurnOverBudget = gateForemanTokenBurnOverBudget;
25
25
  exports.gateStateRepairRequired = gateStateRepairRequired;
26
+ exports.gateQcBlockingFindings = gateQcBlockingFindings;
26
27
  exports.readJsonLines = readJsonLines;
27
28
  const node_fs_1 = require("node:fs");
28
29
  const node_path_1 = require("node:path");
@@ -362,6 +363,51 @@ function safeReaddir(dir) {
362
363
  return [];
363
364
  }
364
365
  }
366
+ /**
367
+ * qc-blocking-findings: Are there unresolved critical/high QC findings
368
+ * attributed to this cluster with high or medium confidence?
369
+ *
370
+ * Evidence: qcResults loaded from .polaris/clusters/<cluster-id>/qc/.
371
+ * Skipped when no QC artifacts exist.
372
+ *
373
+ * Only high/medium attribution-confidence findings are counted — provider noise
374
+ * (low/unattributed confidence) is excluded so workers are not penalized for
375
+ * external reviewer uncertainty.
376
+ */
377
+ function gateQcBlockingFindings(artifacts) {
378
+ if (artifacts.qcResults.length === 0) {
379
+ return { gate: "qc-blocking-findings", outcome: "skipped", detail: "no QC artifacts found" };
380
+ }
381
+ let blockingCount = 0;
382
+ let weightedOpenScore = 0;
383
+ const severityWeights = { critical: 10, high: 5, medium: 2, low: 1, info: 0.5 };
384
+ const confidenceWeights = { high: 1.0, medium: 0.7, low: 0.3, unattributed: 0 };
385
+ for (const result of artifacts.qcResults) {
386
+ for (const finding of result.findings) {
387
+ const conf = finding.attribution.confidence;
388
+ const isOpenUnresolved = finding.status === "open" || finding.status === "follow-up";
389
+ if (isOpenUnresolved && conf !== "low" && conf !== "unattributed") {
390
+ weightedOpenScore += severityWeights[finding.severity] * confidenceWeights[conf];
391
+ }
392
+ if (conf === "low" || conf === "unattributed")
393
+ continue;
394
+ if (finding.status === "autofixed" || finding.status === "repaired" || finding.status === "waived")
395
+ continue;
396
+ if ((finding.severity === "critical" || finding.severity === "high") &&
397
+ (finding.status === "open" || finding.status === "follow-up")) {
398
+ blockingCount++;
399
+ }
400
+ }
401
+ }
402
+ if (blockingCount > 0) {
403
+ return {
404
+ gate: "qc-blocking-findings",
405
+ outcome: "failed",
406
+ detail: `${blockingCount} unresolved critical/high QC finding${blockingCount === 1 ? "" : "s"} with attributed confidence (weighted open score ${weightedOpenScore.toFixed(2)})`,
407
+ };
408
+ }
409
+ return { gate: "qc-blocking-findings", outcome: "passed" };
410
+ }
365
411
  exports.ALL_GATES = [
366
412
  gateUserIntervened,
367
413
  gateForemanResentPacket,
@@ -371,4 +417,5 @@ exports.ALL_GATES = [
371
417
  gateWorkerWentOutOfScope,
372
418
  gateForemanTokenBurnOverBudget,
373
419
  gateStateRepairRequired,
420
+ gateQcBlockingFindings,
374
421
  ];
@@ -10,6 +10,7 @@
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FIX_ZONE_MAP = void 0;
13
+ exports.buildQcProposals = buildQcProposals;
13
14
  exports.buildProposals = buildProposals;
14
15
  exports.loadDiagnosisReport = loadDiagnosisReport;
15
16
  exports.validateDiagnosisReport = validateDiagnosisReport;
@@ -50,6 +51,40 @@ exports.FIX_ZONE_MAP = {
50
51
  artifact_type: "medic-template",
51
52
  hint: "Medic artifacts detected — state repair was required. Review medic template heuristics and state-repair triggers.",
52
53
  },
54
+ "qc-blocking-findings": {
55
+ artifact_type: "scoring-rule",
56
+ hint: "Unresolved critical/high QC findings with attributed confidence. Review QC artifacts, triage open findings, and route repairs before delivery.",
57
+ },
58
+ };
59
+ const ROUTER_FAILURE_FIX_ZONE_MAP = {
60
+ "quota-exhausted": {
61
+ artifact_type: "runtime-config",
62
+ hint: "Recurring provider quota exhaustion detected. Adjust quota policy and fallback order in execution.routerPolicy, and document expected quota behavior for operators.",
63
+ },
64
+ "capability-mismatch": {
65
+ artifact_type: "provider-role-recommendation",
66
+ hint: "Recurring capability mismatch detected. Update provider capability metadata and role task-type mapping, and document role expectations.",
67
+ },
68
+ "trust-too-low": {
69
+ artifact_type: "provider-role-recommendation",
70
+ hint: "Recurring trust-tier mismatch detected. Update trust policy thresholds or provider trust metadata, and document trust requirements for worker routing.",
71
+ },
72
+ "no-slot": {
73
+ artifact_type: "runtime-config",
74
+ hint: "Recurring slot exhaustion detected. Tune worker pool slot limits and provider slot caps in router policy.",
75
+ },
76
+ "cost-policy": {
77
+ artifact_type: "runtime-config",
78
+ hint: "Recurring cost policy rejection detected. Revisit max cost tier and quota policies in router constraints.",
79
+ },
80
+ "not-in-policy": {
81
+ artifact_type: "provider-role-recommendation",
82
+ hint: "Recurring provider policy mismatch detected. Align rotation/providers with role policy allowlists and update routing docs.",
83
+ },
84
+ "role-disabled": {
85
+ artifact_type: "runtime-config",
86
+ hint: "Worker role is repeatedly disabled by policy. Update providerPolicy.worker and role routing defaults.",
87
+ },
53
88
  };
54
89
  // ──────────────────────────────────────────────
55
90
  // Build proposals from a DiagnosisReport
@@ -58,6 +93,80 @@ exports.FIX_ZONE_MAP = {
58
93
  * Maps a DiagnosisReport's failed gates to AutresearchProposal objects.
59
94
  * Gates without a fix zone entry are skipped.
60
95
  */
96
+ const QC_FIX_ZONE_MAP = {
97
+ "qc-recurring-provider": {
98
+ artifact_type: "provider-role-recommendation",
99
+ hint: "A QC provider is producing recurring attributed findings. Review provider capability mapping, role assignment, and confidence thresholds.",
100
+ },
101
+ "qc-recurring-child": {
102
+ artifact_type: "worker-template",
103
+ hint: "A child/worker route accumulates recurring QC findings. Tighten packet scope, validation commands, or acceptance criteria for this route.",
104
+ },
105
+ "qc-unvalidated-noise": {
106
+ artifact_type: "runtime-config",
107
+ hint: "A large share of QC findings are low/unattributed provider noise. Raise providerConfidenceThreshold or tighten QC provider selection.",
108
+ },
109
+ "qc-recurring-validation": {
110
+ artifact_type: "scoring-rule",
111
+ hint: "Unresolved QC findings indicate validation gaps. Strengthen scoring rules and repair routing before delivery.",
112
+ },
113
+ "qc-recurring-docs": {
114
+ artifact_type: "skill-prompt",
115
+ hint: "Recurring QC findings relate to documentation. Update skill prompts and worker instructions that produce docs artifacts.",
116
+ },
117
+ };
118
+ /**
119
+ * Builds QC-derived improvement proposals from the diagnosis report.
120
+ *
121
+ * These recommendations target config, provider, packet-scope, validation,
122
+ * and docs artifacts for recurring QC failure patterns.
123
+ */
124
+ function buildQcProposals(report) {
125
+ const qc = report.qc_summary;
126
+ if (!qc)
127
+ return [];
128
+ const proposals = [];
129
+ const confidence = Math.max(0, Math.min(1, 1 - qc.qc_penalty));
130
+ const addProposal = (gateId, artifactType, hint) => {
131
+ proposals.push({
132
+ gate_id: gateId,
133
+ artifact_type: artifactType,
134
+ hint,
135
+ run_id: report.run_id,
136
+ evidence_run_ids: [report.run_id],
137
+ confidence,
138
+ fix_zone: `${artifactType}/${gateId}`,
139
+ });
140
+ };
141
+ // Recurring provider signal: multiple blocking findings from the same provider.
142
+ for (const [provider, summary] of Object.entries(qc.provider_breakdown)) {
143
+ if (summary.total >= 2 && summary.blocking > 0) {
144
+ addProposal(`qc-recurring-provider:${provider}`, "provider-role-recommendation", `Provider '${provider}' produced ${summary.blocking} blocking QC findings across ${summary.total} total findings. Review provider capability mapping, role assignment, and confidence thresholds.`);
145
+ }
146
+ }
147
+ // Recurring child/worker route signal: same child owns multiple findings.
148
+ for (const signal of qc.recurring_child_signals) {
149
+ if (signal.finding_count >= 2) {
150
+ addProposal(`qc-recurring-child:${signal.child_id}`, "worker-template", `Child '${signal.child_id}' accumulated ${signal.finding_count} attributed QC findings (weighted score ${signal.weighted_score.toFixed(2)}). Tighten packet scope, validation commands, or acceptance criteria for this worker route.`);
151
+ }
152
+ }
153
+ // High share of unvalidated/provider-noise findings.
154
+ const noiseRatio = qc.total_findings > 0 ? qc.unvalidated_findings / qc.total_findings : 0;
155
+ if (qc.unvalidated_findings >= 3 && noiseRatio > 0.3) {
156
+ addProposal("qc-unvalidated-noise", "runtime-config", `${qc.unvalidated_findings} of ${qc.total_findings} QC findings are low/unattributed provider noise. Raise providerConfidenceThreshold or tighten QC provider selection in runtime config.`);
157
+ }
158
+ // Unresolved blocking findings indicate a validation/routing gap.
159
+ if (qc.blocking_findings > 0 && qc.weighted_open_score > 0) {
160
+ addProposal("qc-recurring-validation", "scoring-rule", `${qc.blocking_findings} blocking QC findings remain unresolved (weighted score ${qc.weighted_open_score.toFixed(2)}). Strengthen validation gates and repair routing before delivery.`);
161
+ }
162
+ // Recurring docs-category findings.
163
+ for (const [category, summary] of Object.entries(qc.category_breakdown)) {
164
+ if (category === "docs" && summary.total >= 2) {
165
+ addProposal("qc-recurring-docs", "skill-prompt", `${summary.total} QC findings relate to documentation. Update skill prompts and worker instructions that produce docs artifacts.`);
166
+ }
167
+ }
168
+ return proposals;
169
+ }
61
170
  function buildProposals(report) {
62
171
  const proposals = [];
63
172
  for (const gateId of report.failed_gates) {
@@ -74,6 +183,25 @@ function buildProposals(report) {
74
183
  fix_zone: `${entry.artifact_type}/${gateId}`,
75
184
  });
76
185
  }
186
+ proposals.push(...buildQcProposals(report));
187
+ const recurringRouterFailures = report.router_outcomes?.recurring_failures ?? [];
188
+ for (const failure of recurringRouterFailures) {
189
+ if (failure.occurrences < 2)
190
+ continue;
191
+ const entry = ROUTER_FAILURE_FIX_ZONE_MAP[failure.reason] ?? {
192
+ artifact_type: "runtime-config",
193
+ hint: "Recurring router failure detected. Review router policy configuration and fallback behavior.",
194
+ };
195
+ proposals.push({
196
+ gate_id: `router-failure:${failure.reason}`,
197
+ artifact_type: entry.artifact_type,
198
+ hint: `${entry.hint} Observed ${failure.occurrences} times across children: ${failure.child_ids.join(", ") || "unknown"}.`,
199
+ run_id: report.run_id,
200
+ evidence_run_ids: [report.run_id],
201
+ confidence: report.score,
202
+ fix_zone: `${entry.artifact_type}/router-failure-${failure.reason}`,
203
+ });
204
+ }
77
205
  return proposals;
78
206
  }
79
207
  // ──────────────────────────────────────────────
@@ -96,6 +224,37 @@ function loadDiagnosisReport(filePath) {
96
224
  }
97
225
  return validateDiagnosisReport(raw);
98
226
  }
227
+ function defaultRoutingBreakdown() {
228
+ return { original_worker: 0, repair_worker: 0, follow_up: 0, operator_review: 0, unset: 0 };
229
+ }
230
+ function normalizeQcScoreSummary(raw) {
231
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
232
+ return null;
233
+ const q = raw;
234
+ const zeroSeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
235
+ const rawOpen = q.open_by_severity && typeof q.open_by_severity === "object" && !Array.isArray(q.open_by_severity)
236
+ ? q.open_by_severity
237
+ : undefined;
238
+ const openBySeverity = rawOpen ? { ...zeroSeverity, ...rawOpen } : zeroSeverity;
239
+ return {
240
+ total_findings: q.total_findings ?? 0,
241
+ blocking_findings: q.blocking_findings ?? 0,
242
+ autofixed_findings: q.autofixed_findings ?? 0,
243
+ repaired_findings: q.repaired_findings ?? 0,
244
+ waived_findings: q.waived_findings ?? 0,
245
+ unvalidated_findings: q.unvalidated_findings ?? 0,
246
+ open_by_severity: openBySeverity,
247
+ weighted_open_score: q.weighted_open_score ?? 0,
248
+ qc_penalty: q.qc_penalty ?? 0,
249
+ blocks_delivery: q.blocks_delivery ?? false,
250
+ qc_run_count: q.qc_run_count ?? 0,
251
+ provider_breakdown: q.provider_breakdown ?? {},
252
+ routing_breakdown: q.routing_breakdown ?? defaultRoutingBreakdown(),
253
+ category_breakdown: q.category_breakdown ?? {},
254
+ recurring_child_signals: Array.isArray(q.recurring_child_signals) ? q.recurring_child_signals : [],
255
+ recurring_provider_signals: Array.isArray(q.recurring_provider_signals) ? q.recurring_provider_signals : [],
256
+ };
257
+ }
99
258
  /**
100
259
  * Validates a parsed value against the DiagnosisReport schema.
101
260
  * Throws a descriptive error if required fields are missing or have wrong types.
@@ -117,5 +276,28 @@ function validateDiagnosisReport(raw) {
117
276
  throw new Error("Diagnosis report missing required number field: score");
118
277
  if (!Array.isArray(r["diagnosis_hints"]))
119
278
  throw new Error("Diagnosis report missing required array field: diagnosis_hints");
120
- return raw;
279
+ const routerOutcomesRaw = r["router_outcomes"];
280
+ const normalizedRouterOutcomes = routerOutcomesRaw && typeof routerOutcomesRaw === "object"
281
+ ? {
282
+ total_decisions: typeof routerOutcomesRaw["total_decisions"] === "number" ? routerOutcomesRaw["total_decisions"] : 0,
283
+ exhausted_decisions: typeof routerOutcomesRaw["exhausted_decisions"] === "number" ? routerOutcomesRaw["exhausted_decisions"] : 0,
284
+ fallback_attempts: typeof routerOutcomesRaw["fallback_attempts"] === "number" ? routerOutcomesRaw["fallback_attempts"] : 0,
285
+ successful_fallbacks: typeof routerOutcomesRaw["successful_fallbacks"] === "number" ? routerOutcomesRaw["successful_fallbacks"] : 0,
286
+ recurring_failures: Array.isArray(routerOutcomesRaw["recurring_failures"])
287
+ ? routerOutcomesRaw["recurring_failures"]
288
+ : [],
289
+ }
290
+ : {
291
+ total_decisions: 0,
292
+ exhausted_decisions: 0,
293
+ fallback_attempts: 0,
294
+ successful_fallbacks: 0,
295
+ recurring_failures: [],
296
+ };
297
+ const normalizedQcSummary = normalizeQcScoreSummary(r["qc_summary"]);
298
+ return {
299
+ ...raw,
300
+ router_outcomes: normalizedRouterOutcomes,
301
+ qc_summary: normalizedQcSummary,
302
+ };
121
303
  }
@@ -19,9 +19,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.loadRunArtifacts = loadRunArtifacts;
20
20
  exports.computeScore = computeScore;
21
21
  exports.buildDiagnosisHints = buildDiagnosisHints;
22
+ exports.computeQcSummary = computeQcSummary;
23
+ exports.summarizeRouterOutcomes = summarizeRouterOutcomes;
22
24
  exports.scoreRun = scoreRun;
23
25
  const node_fs_1 = require("node:fs");
24
26
  const node_path_1 = require("node:path");
27
+ const artifacts_js_1 = require("../qc/artifacts.js");
25
28
  const gates_js_1 = require("./gates.js");
26
29
  // ──────────────────────────────────────────────
27
30
  // Diagnosis hints table
@@ -59,6 +62,10 @@ const HINTS = {
59
62
  fix_zone: "medic / cluster-state",
60
63
  hint: "Medic artifacts detected — state repair was required during this run. Review the medic chart and root cause.",
61
64
  },
65
+ "qc-blocking-findings": {
66
+ fix_zone: "qc / findings",
67
+ hint: "QC produced unresolved critical/high findings attributed to this run. Review QC artifacts, triage findings, and route repairs before delivery.",
68
+ },
62
69
  };
63
70
  // ──────────────────────────────────────────────
64
71
  // Artifact loader
@@ -95,6 +102,18 @@ function safeReaddir(dir) {
95
102
  }
96
103
  }
97
104
  const NON_WORKER_PREFIXES = ["librarian-", "CHART-", "medic-result-"];
105
+ function loadQcArtifacts(clusterId, repoRoot) {
106
+ if (!clusterId)
107
+ return [];
108
+ const ids = (0, artifacts_js_1.listQcArtifactIds)(clusterId, repoRoot);
109
+ const results = [];
110
+ for (const id of ids) {
111
+ const result = (0, artifacts_js_1.readQcArtifact)(clusterId, id, repoRoot);
112
+ if (result)
113
+ results.push(result);
114
+ }
115
+ return results;
116
+ }
98
117
  function readResultPackets(clusterDir) {
99
118
  if (!clusterDir)
100
119
  return [];
@@ -176,6 +195,7 @@ function loadRunArtifacts(repoRoot, runId) {
176
195
  // Legacy fallback: extract from result packet files
177
196
  workerResultContracts = extractWorkerResultContracts(resultPackets);
178
197
  }
198
+ const qcResults = loadQcArtifacts(clusterId, repoRoot);
179
199
  return {
180
200
  runId,
181
201
  runDir,
@@ -185,6 +205,7 @@ function loadRunArtifacts(repoRoot, runId) {
185
205
  resultPackets,
186
206
  workerResultContracts,
187
207
  telemetryEvents,
208
+ qcResults,
188
209
  };
189
210
  }
190
211
  // ──────────────────────────────────────────────
@@ -203,6 +224,257 @@ function buildDiagnosisHints(failedGateNames) {
203
224
  return { gate, ...h };
204
225
  });
205
226
  }
227
+ const SEVERITY_WEIGHTS = {
228
+ critical: 10,
229
+ high: 5,
230
+ medium: 2,
231
+ low: 1,
232
+ info: 0.5,
233
+ };
234
+ const CONFIDENCE_WEIGHTS = {
235
+ high: 1.0,
236
+ medium: 0.7,
237
+ low: 0.3,
238
+ unattributed: 0,
239
+ };
240
+ function zeroCounts() {
241
+ return { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
242
+ }
243
+ function zeroRouting() {
244
+ return { original_worker: 0, repair_worker: 0, follow_up: 0, operator_review: 0, unset: 0 };
245
+ }
246
+ function computeQcPenalty(weightedOpenScore) {
247
+ // Soft penalty that grows with weighted severity but does not dominate the score.
248
+ return weightedOpenScore === 0 ? 0 : weightedOpenScore / (weightedOpenScore + 20);
249
+ }
250
+ /**
251
+ * Computes a QcScoreSummary from the QC results loaded for this run.
252
+ * Returns null when no QC results are present.
253
+ *
254
+ * A finding is "blocking" if:
255
+ * - severity is critical or high
256
+ * - status is "open" or "follow-up" (not autofixed, repaired, or waived)
257
+ * - attribution confidence is "high" or "medium" (not unvalidated provider noise)
258
+ *
259
+ * Worker scoring intentionally does not penalize for "unvalidated" or
260
+ * "low" attribution-confidence findings — these are treated as provider noise.
261
+ *
262
+ * The weighted open score multiplies severity by attribution confidence so that
263
+ * a critical/high-confidence finding hurts SOL scoring more than a low-severity
264
+ * or uncertain finding.
265
+ */
266
+ function computeQcSummary(qcResults) {
267
+ if (qcResults.length === 0)
268
+ return null;
269
+ let total = 0;
270
+ let blocking = 0;
271
+ let autofixed = 0;
272
+ let repaired = 0;
273
+ let waived = 0;
274
+ let unvalidated = 0;
275
+ const openBySeverity = zeroCounts();
276
+ let weightedOpenScore = 0;
277
+ let blocksDelivery = false;
278
+ const providerBreakdown = new Map();
279
+ const categoryBreakdown = new Map();
280
+ const routingBreakdown = zeroRouting();
281
+ const childScores = new Map();
282
+ const providerScores = new Map();
283
+ for (const result of qcResults) {
284
+ if (result.policyDecision.blocksDelivery)
285
+ blocksDelivery = true;
286
+ for (const finding of result.findings) {
287
+ total++;
288
+ const provider = result.provider;
289
+ const providerEntry = providerBreakdown.get(provider) ?? { total: 0, blocking: 0, unvalidated: 0 };
290
+ providerEntry.total += 1;
291
+ providerBreakdown.set(provider, providerEntry);
292
+ const category = (finding.category ?? "uncategorized").toLowerCase();
293
+ const categoryEntry = categoryBreakdown.get(category) ?? { total: 0, blocking: 0 };
294
+ categoryEntry.total += 1;
295
+ const routing = finding.routingDecision ?? "unset";
296
+ if (routing === "original-worker")
297
+ routingBreakdown.original_worker += 1;
298
+ else if (routing === "repair-worker")
299
+ routingBreakdown.repair_worker += 1;
300
+ else if (routing === "follow-up")
301
+ routingBreakdown.follow_up += 1;
302
+ else if (routing === "operator-review")
303
+ routingBreakdown.operator_review += 1;
304
+ else
305
+ routingBreakdown.unset += 1;
306
+ const conf = finding.attribution.confidence;
307
+ const isUnvalidated = conf === "low" || conf === "unattributed";
308
+ if (isUnvalidated) {
309
+ unvalidated++;
310
+ providerEntry.unvalidated += 1;
311
+ categoryBreakdown.set(category, categoryEntry);
312
+ continue; // do not count provider noise toward any negative bucket
313
+ }
314
+ if (finding.status === "autofixed") {
315
+ autofixed++;
316
+ categoryBreakdown.set(category, categoryEntry);
317
+ continue;
318
+ }
319
+ if (finding.status === "repaired") {
320
+ repaired++;
321
+ categoryBreakdown.set(category, categoryEntry);
322
+ continue;
323
+ }
324
+ if (finding.status === "waived") {
325
+ waived++;
326
+ categoryBreakdown.set(category, categoryEntry);
327
+ continue;
328
+ }
329
+ // "open" or "follow-up" — count toward open_by_severity and weighted score
330
+ const sev = finding.severity;
331
+ if (sev in openBySeverity)
332
+ openBySeverity[sev]++;
333
+ const isBlocking = (sev === "critical" || sev === "high") &&
334
+ (finding.status === "open" || finding.status === "follow-up");
335
+ if (isBlocking) {
336
+ blocking++;
337
+ categoryEntry.blocking += 1;
338
+ providerEntry.blocking += 1;
339
+ }
340
+ const findingWeight = SEVERITY_WEIGHTS[sev] * CONFIDENCE_WEIGHTS[conf];
341
+ weightedOpenScore += findingWeight;
342
+ const childId = finding.attribution.childId;
343
+ if (childId) {
344
+ const childEntry = childScores.get(childId) ?? { weighted_score: 0, finding_count: 0 };
345
+ childEntry.weighted_score += findingWeight;
346
+ childEntry.finding_count += 1;
347
+ childScores.set(childId, childEntry);
348
+ }
349
+ const providerScoreEntry = providerScores.get(provider) ?? { weighted_score: 0, finding_count: 0 };
350
+ providerScoreEntry.weighted_score += findingWeight;
351
+ providerScoreEntry.finding_count += 1;
352
+ providerScores.set(provider, providerScoreEntry);
353
+ categoryBreakdown.set(category, categoryEntry);
354
+ providerBreakdown.set(provider, providerEntry);
355
+ }
356
+ }
357
+ const recurringChildSignals = Array.from(childScores.entries())
358
+ .map(([child_id, value]) => ({ child_id, ...value }))
359
+ .filter((s) => s.weighted_score > 0)
360
+ .sort((a, b) => b.weighted_score - a.weighted_score || a.child_id.localeCompare(b.child_id));
361
+ const recurringProviderSignals = Array.from(providerScores.entries())
362
+ .map(([provider, value]) => ({ provider, ...value }))
363
+ .filter((s) => s.weighted_score > 0)
364
+ .sort((a, b) => b.weighted_score - a.weighted_score || a.provider.localeCompare(b.provider));
365
+ return {
366
+ total_findings: total,
367
+ blocking_findings: blocking,
368
+ autofixed_findings: autofixed,
369
+ repaired_findings: repaired,
370
+ waived_findings: waived,
371
+ unvalidated_findings: unvalidated,
372
+ open_by_severity: openBySeverity,
373
+ weighted_open_score: Number(weightedOpenScore.toFixed(3)),
374
+ qc_penalty: Number(computeQcPenalty(weightedOpenScore).toFixed(4)),
375
+ blocks_delivery: blocksDelivery,
376
+ qc_run_count: qcResults.length,
377
+ provider_breakdown: Object.fromEntries(providerBreakdown),
378
+ routing_breakdown: routingBreakdown,
379
+ category_breakdown: Object.fromEntries(categoryBreakdown),
380
+ recurring_child_signals: recurringChildSignals,
381
+ recurring_provider_signals: recurringProviderSignals,
382
+ };
383
+ }
384
+ function asRecord(value) {
385
+ return value && typeof value === "object" && !Array.isArray(value)
386
+ ? value
387
+ : undefined;
388
+ }
389
+ function asStringArray(value) {
390
+ if (!Array.isArray(value))
391
+ return [];
392
+ return value.filter((entry) => typeof entry === "string");
393
+ }
394
+ function summarizeRouterOutcomes(artifacts) {
395
+ const telemetry = artifacts.telemetryEvents
396
+ .map((event) => asRecord(event))
397
+ .filter((event) => event !== undefined);
398
+ const childCompletionStatus = new Map();
399
+ for (const event of telemetry) {
400
+ if (event["event"] !== "child-complete" || typeof event["child_id"] !== "string")
401
+ continue;
402
+ const completionStatus = typeof event["completion_status"] === "string"
403
+ ? event["completion_status"]
404
+ : "done";
405
+ childCompletionStatus.set(event["child_id"], completionStatus);
406
+ }
407
+ const selectedEvents = telemetry.filter((event) => event["event"] === "provider-selected");
408
+ const exhaustedEvents = telemetry.filter((event) => event["event"] === "provider-exhausted");
409
+ const fallbackEvents = telemetry.filter((event) => event["event"] === "provider-fallback-attempted");
410
+ const reasonCounts = new Map();
411
+ const countReason = (reason, childId) => {
412
+ const current = reasonCounts.get(reason) ?? { count: 0, childIds: new Set() };
413
+ current.count += 1;
414
+ if (childId)
415
+ current.childIds.add(childId);
416
+ reasonCounts.set(reason, current);
417
+ };
418
+ for (const event of exhaustedEvents) {
419
+ const reason = typeof event["reason"] === "string" ? event["reason"] : "no-provider-selected";
420
+ const childId = typeof event["child_id"] === "string" ? event["child_id"] : undefined;
421
+ countReason(reason, childId);
422
+ }
423
+ for (const event of selectedEvents) {
424
+ const childId = typeof event["child_id"] === "string" ? event["child_id"] : undefined;
425
+ const selectedProvider = typeof event["selected_provider"] === "string" ? event["selected_provider"] : null;
426
+ if (!selectedProvider) {
427
+ // Use per-candidate rejection reasons when available; fall back to the
428
+ // overall exhausted reason. Counting both for the same event would
429
+ // double-count shared reason strings (e.g. "no-slot").
430
+ const candidates = Array.isArray(event["router_candidates"]) ? event["router_candidates"] : [];
431
+ if (candidates.length > 0) {
432
+ for (const candidateRaw of candidates) {
433
+ const candidate = asRecord(candidateRaw);
434
+ if (!candidate)
435
+ continue;
436
+ const rejectionReasons = asStringArray(candidate["rejection_reasons"]);
437
+ for (const reason of rejectionReasons) {
438
+ countReason(reason, childId);
439
+ }
440
+ }
441
+ }
442
+ else {
443
+ const exhaustedReason = typeof event["router_exhausted_reason"] === "string"
444
+ ? event["router_exhausted_reason"]
445
+ : "no-provider-selected";
446
+ countReason(exhaustedReason, childId);
447
+ }
448
+ }
449
+ }
450
+ let successfulFallbacks = 0;
451
+ for (const event of selectedEvents) {
452
+ const selectedProvider = typeof event["selected_provider"] === "string" ? event["selected_provider"] : null;
453
+ const providersTried = asStringArray(event["providers_tried"]);
454
+ const usedFallback = selectedProvider !== null && providersTried.length > 1;
455
+ if (!usedFallback)
456
+ continue;
457
+ const childId = typeof event["child_id"] === "string" ? event["child_id"] : undefined;
458
+ const completionStatus = childId ? childCompletionStatus.get(childId) : undefined;
459
+ if (completionStatus && completionStatus !== "blocked" && completionStatus !== "error") {
460
+ successfulFallbacks += 1;
461
+ }
462
+ }
463
+ const recurringFailures = Array.from(reasonCounts.entries())
464
+ .map(([reason, value]) => ({
465
+ reason,
466
+ occurrences: value.count,
467
+ child_ids: Array.from(value.childIds).sort(),
468
+ }))
469
+ .sort((a, b) => b.occurrences - a.occurrences || a.reason.localeCompare(b.reason));
470
+ return {
471
+ total_decisions: selectedEvents.length,
472
+ exhausted_decisions: exhaustedEvents.length,
473
+ fallback_attempts: fallbackEvents.length,
474
+ successful_fallbacks: successfulFallbacks,
475
+ recurring_failures: recurringFailures,
476
+ };
477
+ }
206
478
  // ──────────────────────────────────────────────
207
479
  // Main entry point
208
480
  // ──────────────────────────────────────────────
@@ -212,6 +484,8 @@ function scoreRun(repoRoot, runId) {
212
484
  const failedGates = gateResults.filter((g) => g.outcome === "failed").map((g) => g.gate);
213
485
  const score = computeScore(gateResults);
214
486
  const diagnosisHints = buildDiagnosisHints(failedGates);
487
+ const routerOutcomes = summarizeRouterOutcomes(artifacts);
488
+ const qcSummary = computeQcSummary(artifacts.qcResults);
215
489
  const clusterId = artifacts.currentState &&
216
490
  typeof artifacts.currentState === "object" &&
217
491
  !Array.isArray(artifacts.currentState)
@@ -225,5 +499,7 @@ function scoreRun(repoRoot, runId) {
225
499
  failed_gates: failedGates,
226
500
  score,
227
501
  diagnosis_hints: diagnosisHints,
502
+ router_outcomes: routerOutcomes,
503
+ qc_summary: qcSummary,
228
504
  };
229
505
  }
package/dist/cli/init.js CHANGED
@@ -36,6 +36,13 @@ const PLAN_COMPLETE_STATUSES = new Set(["completed", "skipped"]);
36
36
  const ADOPTION_LOCKED_EXECUTION = {
37
37
  rotation: [],
38
38
  allowCrossAgentFallback: false,
39
+ routerPolicy: {
40
+ defaultWorkerPool: {
41
+ maxActiveWorkers: 1,
42
+ maxActiveSlots: 1,
43
+ },
44
+ allowCrossProviderFallback: false,
45
+ },
39
46
  adapter: "terminal-cli",
40
47
  };
41
48
  const ADOPTION_LOCKED_ORCHESTRATION = {
@@ -65,9 +72,19 @@ function asRecord(value) {
65
72
  function isAdoptionConfigLocked(existing) {
66
73
  const execution = asRecord(existing.execution);
67
74
  const orchestration = asRecord(existing.orchestration);
75
+ const routerPolicy = asRecord(execution.routerPolicy);
76
+ const defaultWorkerPool = asRecord(routerPolicy.defaultWorkerPool);
77
+ const routerPolicyMatches = Object.keys(routerPolicy).length === 0 ||
78
+ (routerPolicy.allowCrossProviderFallback ===
79
+ ADOPTION_LOCKED_EXECUTION.routerPolicy.allowCrossProviderFallback &&
80
+ defaultWorkerPool.maxActiveWorkers ===
81
+ ADOPTION_LOCKED_EXECUTION.routerPolicy.defaultWorkerPool.maxActiveWorkers &&
82
+ defaultWorkerPool.maxActiveSlots ===
83
+ ADOPTION_LOCKED_EXECUTION.routerPolicy.defaultWorkerPool.maxActiveSlots);
68
84
  return (Array.isArray(execution.rotation) &&
69
85
  execution.rotation.length === 0 &&
70
86
  execution.allowCrossAgentFallback === false &&
87
+ routerPolicyMatches &&
71
88
  execution.adapter === ADOPTION_LOCKED_EXECUTION.adapter &&
72
89
  orchestration.mode === ADOPTION_LOCKED_ORCHESTRATION.mode);
73
90
  }