@lsctech/polaris 0.5.10 → 0.5.12
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 +61 -0
- package/dist/autoresearch/score.js +124 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/qc.js +97 -0
- package/dist/config/validator.js +16 -6
- package/dist/finalize/artifact-policy.js +35 -3
- package/dist/finalize/index.js +23 -4
- package/dist/finalize/run-report.js +189 -19
- package/dist/finalize/steps/05-generate-report.js +5 -1
- package/dist/finalize/steps/06-commit.js +2 -1
- package/dist/finalize/steps/12-archive.js +6 -0
- package/dist/loop/adapters/terminal-cli.js +131 -25
- package/dist/loop/body-parser.js +69 -6
- package/dist/loop/continue.js +19 -1
- package/dist/loop/dispatch.js +102 -67
- package/dist/loop/parent.js +22 -3
- package/dist/loop/router/engine.js +1 -0
- package/dist/loop/run-preflight.js +4 -1
- package/dist/loop/worker-packet.js +81 -2
- package/dist/medic/routing-signals.js +60 -0
- package/dist/qc/policy.js +2 -0
- package/dist/qc/providers/coderabbit.js +153 -14
- package/dist/qc/repair-loop.js +147 -1
- package/dist/qc/repair-packets.js +16 -3
- package/dist/qc/routing.js +6 -0
- package/dist/qc/types.js +3 -0
- package/dist/tracker/local-graph.js +106 -3
- package/package.json +1 -1
|
@@ -86,6 +86,28 @@ const ROUTER_FAILURE_FIX_ZONE_MAP = {
|
|
|
86
86
|
hint: "Worker role is repeatedly disabled by policy. Update providerPolicy.worker and role routing defaults.",
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
|
+
const ROUTER_ANOMALY_FIX_ZONE_MAP = {
|
|
90
|
+
"provider-monopoly": {
|
|
91
|
+
artifact_type: "provider-role-recommendation",
|
|
92
|
+
hint: "Repeated same-provider selection when policy evidence shows multiple providers were eligible. Review provider role assignment and policy ordering to avoid routing concentration.",
|
|
93
|
+
},
|
|
94
|
+
"missing-evidence": {
|
|
95
|
+
artifact_type: "runtime-config",
|
|
96
|
+
hint: "Routing evidence gaps detected (missing exhausted reason, router candidates, or child completion). Improve telemetry and dispatch evidence capture.",
|
|
97
|
+
},
|
|
98
|
+
"missing-sealed-result": {
|
|
99
|
+
artifact_type: "medic-template",
|
|
100
|
+
hint: "Missing sealed result file indicates a state-repair review signal. Investigate worker result writing and dispatch boundary.",
|
|
101
|
+
},
|
|
102
|
+
"stale-dispatch-abort": {
|
|
103
|
+
artifact_type: "medic-template",
|
|
104
|
+
hint: "Stale dispatch abort indicates a state-repair review signal. Investigate dispatch lifecycle and orphan recovery.",
|
|
105
|
+
},
|
|
106
|
+
"invalid-inline-attempt": {
|
|
107
|
+
artifact_type: "medic-template",
|
|
108
|
+
hint: "Invalid inline attempt indicates a state-repair review signal. Investigate dispatch boundary enforcement.",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
89
111
|
// ──────────────────────────────────────────────
|
|
90
112
|
// Build proposals from a DiagnosisReport
|
|
91
113
|
// ──────────────────────────────────────────────
|
|
@@ -202,6 +224,33 @@ function buildProposals(report) {
|
|
|
202
224
|
fix_zone: `${entry.artifact_type}/router-failure-${failure.reason}`,
|
|
203
225
|
});
|
|
204
226
|
}
|
|
227
|
+
// Routing anomaly signals (provider monopoly, evidence gaps, state-repair).
|
|
228
|
+
const routerOutcomes = report.router_outcomes;
|
|
229
|
+
const anomalySignals = [
|
|
230
|
+
...(routerOutcomes?.provider_monopoly_signals ?? []),
|
|
231
|
+
...(routerOutcomes?.evidence_gap_signals ?? []),
|
|
232
|
+
...(routerOutcomes?.state_repair_signals ?? []),
|
|
233
|
+
];
|
|
234
|
+
for (const signal of anomalySignals) {
|
|
235
|
+
// Evidence gaps and provider monopoly must be recurring to be worth a proposal.
|
|
236
|
+
if (signal.signal === "missing-evidence" && signal.occurrences < 2)
|
|
237
|
+
continue;
|
|
238
|
+
if (signal.signal === "provider-monopoly" && signal.occurrences < 2)
|
|
239
|
+
continue;
|
|
240
|
+
const entry = ROUTER_ANOMALY_FIX_ZONE_MAP[signal.signal] ?? {
|
|
241
|
+
artifact_type: "runtime-config",
|
|
242
|
+
hint: "Recurring routing anomaly detected. Review dispatch telemetry and routing policy.",
|
|
243
|
+
};
|
|
244
|
+
proposals.push({
|
|
245
|
+
gate_id: `router-anomaly:${signal.signal}:${signal.reason}`,
|
|
246
|
+
artifact_type: entry.artifact_type,
|
|
247
|
+
hint: `${entry.hint} Observed ${signal.occurrences} times across children: ${signal.child_ids.join(", ") || "unknown"}.`,
|
|
248
|
+
run_id: report.run_id,
|
|
249
|
+
evidence_run_ids: [report.run_id],
|
|
250
|
+
confidence: report.score,
|
|
251
|
+
fix_zone: `${entry.artifact_type}/router-anomaly-${signal.signal}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
205
254
|
return proposals;
|
|
206
255
|
}
|
|
207
256
|
// ──────────────────────────────────────────────
|
|
@@ -291,6 +340,15 @@ function validateDiagnosisReport(raw) {
|
|
|
291
340
|
recurring_failures: Array.isArray(routerOutcomesRaw["recurring_failures"])
|
|
292
341
|
? routerOutcomesRaw["recurring_failures"]
|
|
293
342
|
: [],
|
|
343
|
+
provider_monopoly_signals: Array.isArray(routerOutcomesRaw["provider_monopoly_signals"])
|
|
344
|
+
? routerOutcomesRaw["provider_monopoly_signals"]
|
|
345
|
+
: [],
|
|
346
|
+
evidence_gap_signals: Array.isArray(routerOutcomesRaw["evidence_gap_signals"])
|
|
347
|
+
? routerOutcomesRaw["evidence_gap_signals"]
|
|
348
|
+
: [],
|
|
349
|
+
state_repair_signals: Array.isArray(routerOutcomesRaw["state_repair_signals"])
|
|
350
|
+
? routerOutcomesRaw["state_repair_signals"]
|
|
351
|
+
: [],
|
|
294
352
|
}
|
|
295
353
|
: {
|
|
296
354
|
total_decisions: 0,
|
|
@@ -298,6 +356,9 @@ function validateDiagnosisReport(raw) {
|
|
|
298
356
|
fallback_attempts: 0,
|
|
299
357
|
successful_fallbacks: 0,
|
|
300
358
|
recurring_failures: [],
|
|
359
|
+
provider_monopoly_signals: [],
|
|
360
|
+
evidence_gap_signals: [],
|
|
361
|
+
state_repair_signals: [],
|
|
301
362
|
};
|
|
302
363
|
const normalizedQcSummary = normalizeQcScoreSummary(r["qc_summary"]);
|
|
303
364
|
return {
|
|
@@ -24,6 +24,7 @@ exports.summarizeRouterOutcomes = summarizeRouterOutcomes;
|
|
|
24
24
|
exports.scoreRun = scoreRun;
|
|
25
25
|
const node_fs_1 = require("node:fs");
|
|
26
26
|
const node_path_1 = require("node:path");
|
|
27
|
+
const routing_signals_js_1 = require("../medic/routing-signals.js");
|
|
27
28
|
const artifacts_js_1 = require("../qc/artifacts.js");
|
|
28
29
|
const store_js_1 = require("../cluster-state/store.js");
|
|
29
30
|
const gates_js_1 = require("./gates.js");
|
|
@@ -639,12 +640,135 @@ function summarizeRouterOutcomes(artifacts) {
|
|
|
639
640
|
child_ids: Array.from(value.childIds).sort(),
|
|
640
641
|
}))
|
|
641
642
|
.sort((a, b) => b.occurrences - a.occurrences || a.reason.localeCompare(b.reason));
|
|
643
|
+
// ── Provider monopoly detection ─────────────────────────────────────────────
|
|
644
|
+
// Repeated same-provider selection when policy evidence shows multiple
|
|
645
|
+
// providers were eligible or configured.
|
|
646
|
+
const hasMultiProviderEvidence = (event) => {
|
|
647
|
+
const candidates = Array.isArray(event["router_candidates"]) ? event["router_candidates"] : [];
|
|
648
|
+
const eligibleCount = candidates.filter((candidate) => asRecord(candidate)?.["eligible"] === true).length;
|
|
649
|
+
const routingSummary = asRecord(event["routing_summary"]);
|
|
650
|
+
const effectivePolicyOrder = asStringArray(routingSummary?.["effective_policy_order"]);
|
|
651
|
+
const providersTried = asStringArray(event["providers_tried"]);
|
|
652
|
+
const fallbackAttempts = Array.isArray(event["fallback_attempts"]) ? event["fallback_attempts"] : [];
|
|
653
|
+
return (eligibleCount > 1 ||
|
|
654
|
+
effectivePolicyOrder.length > 1 ||
|
|
655
|
+
providersTried.length > 1 ||
|
|
656
|
+
fallbackAttempts.length > 0 ||
|
|
657
|
+
routingSummary?.["fallback_eligible"] === true);
|
|
658
|
+
};
|
|
659
|
+
const providerSelections = new Map();
|
|
660
|
+
for (const event of selectedEvents) {
|
|
661
|
+
const selectedProvider = asString(event["selected_provider"]);
|
|
662
|
+
if (!selectedProvider || !hasMultiProviderEvidence(event))
|
|
663
|
+
continue;
|
|
664
|
+
const childId = asString(event["child_id"]) ?? undefined;
|
|
665
|
+
const existing = providerSelections.get(selectedProvider);
|
|
666
|
+
if (existing) {
|
|
667
|
+
existing.occurrences += 1;
|
|
668
|
+
if (childId)
|
|
669
|
+
existing.child_ids.push(childId);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
providerSelections.set(selectedProvider, {
|
|
673
|
+
signal: "provider-monopoly",
|
|
674
|
+
reason: selectedProvider,
|
|
675
|
+
occurrences: 1,
|
|
676
|
+
child_ids: childId ? [childId] : [],
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const providerMonopolySignals = Array.from(providerSelections.values())
|
|
681
|
+
.filter((signal) => signal.occurrences >= 2)
|
|
682
|
+
.map((signal) => ({
|
|
683
|
+
...signal,
|
|
684
|
+
child_ids: [...new Set(signal.child_ids)].sort(),
|
|
685
|
+
}))
|
|
686
|
+
.sort((a, b) => b.occurrences - a.occurrences || a.reason.localeCompare(b.reason));
|
|
687
|
+
// ── Evidence gap detection ──────────────────────────────────────────────────
|
|
688
|
+
// Missing evidence that prevents distinguishing true provider failures from
|
|
689
|
+
// routing/telemetry gaps.
|
|
690
|
+
const evidenceGapMap = new Map();
|
|
691
|
+
const countEvidenceGap = (reason, childId) => {
|
|
692
|
+
const existing = evidenceGapMap.get(reason);
|
|
693
|
+
if (existing) {
|
|
694
|
+
existing.occurrences += 1;
|
|
695
|
+
if (childId)
|
|
696
|
+
existing.child_ids.push(childId);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
evidenceGapMap.set(reason, {
|
|
700
|
+
signal: "missing-evidence",
|
|
701
|
+
reason,
|
|
702
|
+
occurrences: 1,
|
|
703
|
+
child_ids: childId ? [childId] : [],
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
for (const event of selectedEvents) {
|
|
708
|
+
const selectedProvider = asString(event["selected_provider"]);
|
|
709
|
+
const childId = asString(event["child_id"]) ?? undefined;
|
|
710
|
+
const candidates = Array.isArray(event["router_candidates"]) ? event["router_candidates"] : [];
|
|
711
|
+
const routingSummary = asRecord(event["routing_summary"]);
|
|
712
|
+
const fallbackAttempts = Array.isArray(event["fallback_attempts"]) ? event["fallback_attempts"] : [];
|
|
713
|
+
if (selectedProvider === null) {
|
|
714
|
+
const exhaustedReason = asString(event["router_exhausted_reason"]);
|
|
715
|
+
if (!exhaustedReason && candidates.length === 0) {
|
|
716
|
+
countEvidenceGap("missing-exhausted-reason", childId);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else if (selectedProvider !== null) {
|
|
720
|
+
const registryPresent = routingSummary?.["registry_present"] === true;
|
|
721
|
+
if (registryPresent && candidates.length === 0) {
|
|
722
|
+
countEvidenceGap("missing-router-candidates", childId);
|
|
723
|
+
}
|
|
724
|
+
if (fallbackAttempts.length > 0 && childId && !childCompletionStatus.has(childId)) {
|
|
725
|
+
countEvidenceGap("missing-child-completion", childId);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
for (const event of exhaustedEvents) {
|
|
730
|
+
const childId = asString(event["child_id"]) ?? undefined;
|
|
731
|
+
const reason = asString(event["reason"]);
|
|
732
|
+
if (!reason && childId) {
|
|
733
|
+
countEvidenceGap("missing-exhausted-reason", childId);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const evidenceGapSignals = Array.from(evidenceGapMap.values())
|
|
737
|
+
.map((signal) => ({
|
|
738
|
+
...signal,
|
|
739
|
+
child_ids: [...new Set(signal.child_ids)].sort(),
|
|
740
|
+
}))
|
|
741
|
+
.sort((a, b) => b.occurrences - a.occurrences || a.reason.localeCompare(b.reason));
|
|
742
|
+
// ── State-repair / Medic review signal classification ───────────────────────
|
|
743
|
+
const stateRepairMap = new Map();
|
|
744
|
+
for (const event of telemetry) {
|
|
745
|
+
const classified = (0, routing_signals_js_1.classifyRoutingTelemetryEvent)(event);
|
|
746
|
+
if (!classified)
|
|
747
|
+
continue;
|
|
748
|
+
const existing = stateRepairMap.get(classified.signal);
|
|
749
|
+
if (existing) {
|
|
750
|
+
existing.occurrences += classified.occurrences;
|
|
751
|
+
existing.child_ids.push(...classified.child_ids);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
stateRepairMap.set(classified.signal, { ...classified });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const stateRepairSignals = Array.from(stateRepairMap.values())
|
|
758
|
+
.map((signal) => ({
|
|
759
|
+
...signal,
|
|
760
|
+
child_ids: [...new Set(signal.child_ids)].sort(),
|
|
761
|
+
}))
|
|
762
|
+
.sort((a, b) => b.occurrences - a.occurrences || a.signal.localeCompare(b.signal));
|
|
642
763
|
return {
|
|
643
764
|
total_decisions: selectedEvents.length,
|
|
644
765
|
exhausted_decisions: exhaustedEvents.length,
|
|
645
766
|
fallback_attempts: fallbackEvents.length,
|
|
646
767
|
successful_fallbacks: successfulFallbacks,
|
|
647
768
|
recurring_failures: recurringFailures,
|
|
769
|
+
provider_monopoly_signals: providerMonopolySignals,
|
|
770
|
+
evidence_gap_signals: evidenceGapSignals,
|
|
771
|
+
state_repair_signals: stateRepairSignals,
|
|
648
772
|
};
|
|
649
773
|
}
|
|
650
774
|
// ──────────────────────────────────────────────
|
package/dist/cli/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const graph_js_1 = require("./graph.js");
|
|
|
25
25
|
const index_js_7 = require("../skill-packet/index.js");
|
|
26
26
|
const librarian_js_1 = require("./librarian.js");
|
|
27
27
|
const medic_js_1 = require("./medic.js");
|
|
28
|
+
const qc_js_1 = require("./qc.js");
|
|
28
29
|
const welfare_js_1 = require("../map/welfare.js");
|
|
29
30
|
const adopt_command_js_1 = require("./adopt-command.js");
|
|
30
31
|
const upgrade_command_js_1 = require("./upgrade-command.js");
|
|
@@ -127,6 +128,9 @@ function createPolarisCommand(options = {}) {
|
|
|
127
128
|
program.addCommand((0, medic_js_1.createMedicCommand)({
|
|
128
129
|
repoRoot,
|
|
129
130
|
}));
|
|
131
|
+
program.addCommand((0, qc_js_1.createQcCommand)({
|
|
132
|
+
repoRoot,
|
|
133
|
+
}));
|
|
130
134
|
program.addCommand((0, adopt_command_js_1.createAdoptCommand)({ repoRoot }));
|
|
131
135
|
program.addCommand((0, upgrade_command_js_1.createUpgradeCommand)({ repoRoot }));
|
|
132
136
|
program.addCommand((0, autoresearch_js_1.createSolCommand)({ repoRoot }));
|
package/dist/cli/qc.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createQcCommand = createQcCommand;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const types_js_1 = require("../qc/types.js");
|
|
8
|
+
const repair_loop_js_1 = require("../qc/repair-loop.js");
|
|
9
|
+
const repair_packets_js_1 = require("../qc/repair-packets.js");
|
|
10
|
+
function createQcCommand(options = {}) {
|
|
11
|
+
const repoRootDefault = options.repoRoot ?? (0, node_path_1.resolve)(process.cwd());
|
|
12
|
+
const qc = new commander_1.Command("qc")
|
|
13
|
+
.description("QC role tools")
|
|
14
|
+
.showHelpAfterError()
|
|
15
|
+
.showSuggestionAfterError();
|
|
16
|
+
qc
|
|
17
|
+
.command("resolve")
|
|
18
|
+
.description("Record a formal operator resolution for a QC repair-loop terminal state")
|
|
19
|
+
.option("-r, --repo-root <path>", "Repository root", repoRootDefault)
|
|
20
|
+
.requiredOption("--cluster-id <id>", "Cluster ID to resolve")
|
|
21
|
+
.requiredOption("--outcome <outcome>", "Resolved outcome (pass or no-repairable)")
|
|
22
|
+
.requiredOption("--reason <text>", "Reason for the resolution")
|
|
23
|
+
.option("--findings <ids>", "Comma-separated finding IDs covered by this resolution (defaults to all in the round manifest)")
|
|
24
|
+
.action((cmdOptions) => {
|
|
25
|
+
try {
|
|
26
|
+
resolveQcRepairLoop({
|
|
27
|
+
repoRoot: cmdOptions.repoRoot,
|
|
28
|
+
clusterId: cmdOptions.clusterId,
|
|
29
|
+
outcome: cmdOptions.outcome,
|
|
30
|
+
reason: cmdOptions.reason,
|
|
31
|
+
findings: cmdOptions.findings,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
process.stderr.write(`qc resolve error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return qc;
|
|
40
|
+
}
|
|
41
|
+
function resolveQcRepairLoop(options) {
|
|
42
|
+
const { repoRoot, clusterId, outcome, reason, findings } = options;
|
|
43
|
+
const allowedOutcomes = new Set(types_js_1.QC_RESOLUTION_OUTCOMES);
|
|
44
|
+
if (!allowedOutcomes.has(outcome)) {
|
|
45
|
+
throw new Error(`Invalid outcome "${outcome}". Must be one of: ${types_js_1.QC_RESOLUTION_OUTCOMES.join(", ")}.`);
|
|
46
|
+
}
|
|
47
|
+
const resolvedReason = reason.trim();
|
|
48
|
+
if (resolvedReason === "") {
|
|
49
|
+
throw new Error("A non-empty --reason is required to record a resolution.");
|
|
50
|
+
}
|
|
51
|
+
const round = findCurrentRepairRound(repoRoot, clusterId);
|
|
52
|
+
if (round === null) {
|
|
53
|
+
throw new Error(`No repair-round manifest found for cluster ${clusterId}. Run the QC repair loop first.`);
|
|
54
|
+
}
|
|
55
|
+
const manifest = (0, repair_packets_js_1.readRepairPacketManifest)(clusterId, round, repoRoot);
|
|
56
|
+
if (!manifest) {
|
|
57
|
+
throw new Error(`Repair packet manifest for cluster ${clusterId} round ${round} is missing or invalid.`);
|
|
58
|
+
}
|
|
59
|
+
const explicitFindings = findings
|
|
60
|
+
? findings
|
|
61
|
+
.split(",")
|
|
62
|
+
.map((id) => id.trim())
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
: undefined;
|
|
65
|
+
const resolvedFindings = (0, repair_loop_js_1.resolveQcResolutionFindings)(manifest, explicitFindings);
|
|
66
|
+
const resolver = (0, repair_loop_js_1.getResolverIdentity)(repoRoot);
|
|
67
|
+
const artifactPath = (0, repair_loop_js_1.writeQcResolutionArtifact)({
|
|
68
|
+
clusterId,
|
|
69
|
+
round,
|
|
70
|
+
resolver,
|
|
71
|
+
resolvedOutcome: outcome,
|
|
72
|
+
reason: resolvedReason,
|
|
73
|
+
findings: resolvedFindings,
|
|
74
|
+
repoRoot,
|
|
75
|
+
});
|
|
76
|
+
process.stdout.write(`Created resolution artifact: ${artifactPath}\n` +
|
|
77
|
+
`Resolved outcome: ${outcome}\n` +
|
|
78
|
+
`Resolved findings: ${resolvedFindings.join(", ") || "none"}\n`);
|
|
79
|
+
}
|
|
80
|
+
function findCurrentRepairRound(repoRoot, clusterId) {
|
|
81
|
+
const roundsDir = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds");
|
|
82
|
+
if (!(0, node_fs_1.existsSync)(roundsDir)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const entries = (0, node_fs_1.readdirSync)(roundsDir, { withFileTypes: true });
|
|
86
|
+
const rounds = entries
|
|
87
|
+
.filter((e) => e.isDirectory() && /^\d+$/.test(e.name))
|
|
88
|
+
.map((e) => Number(e.name))
|
|
89
|
+
.sort((a, b) => b - a);
|
|
90
|
+
for (const round of rounds) {
|
|
91
|
+
const manifestPath = (0, node_path_1.join)(roundsDir, String(round), "repair-packets.json");
|
|
92
|
+
if ((0, node_fs_1.existsSync)(manifestPath)) {
|
|
93
|
+
return round;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
package/dist/config/validator.js
CHANGED
|
@@ -364,13 +364,23 @@ function validateConfig(config) {
|
|
|
364
364
|
result.valid = false;
|
|
365
365
|
result.errors.push(`execution.providerPolicy.${roleName}.providers must be an array of strings`);
|
|
366
366
|
}
|
|
367
|
-
else
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
367
|
+
else {
|
|
368
|
+
if (providerKeys) {
|
|
369
|
+
for (const providerName of rolePolicy.providers) {
|
|
370
|
+
if (!providerKeys.has(providerName)) {
|
|
371
|
+
result.valid = false;
|
|
372
|
+
result.errors.push(`execution.providerPolicy.${roleName}.providers contains unknown provider: ${providerName}`);
|
|
373
|
+
}
|
|
372
374
|
}
|
|
373
375
|
}
|
|
376
|
+
const routerPolicyObj = isPlainObject(config.execution.routerPolicy)
|
|
377
|
+
? config.execution.routerPolicy
|
|
378
|
+
: undefined;
|
|
379
|
+
const providerRegistry = routerPolicyObj?.providerRegistry;
|
|
380
|
+
const hasRegistry = isPlainObject(providerRegistry) && Object.keys(providerRegistry).length > 0;
|
|
381
|
+
if (!hasRegistry && rolePolicy.providers.length > 1) {
|
|
382
|
+
result.warnings.push(`execution.providerPolicy.${roleName}.providers lists multiple providers but execution.routerPolicy.providerRegistry is missing or empty; dispatch will use compatibility mode and only the selected provider will appear in providers_tried`);
|
|
383
|
+
}
|
|
374
384
|
}
|
|
375
385
|
if ("allowNativeSubagent" in rolePolicy && rolePolicy.allowNativeSubagent !== undefined && !isBoolean(rolePolicy.allowNativeSubagent)) {
|
|
376
386
|
result.valid = false;
|
|
@@ -993,7 +1003,7 @@ function validateConfig(config) {
|
|
|
993
1003
|
result.errors.push(`qc.providers.${providerName}.failurePolicy must be a plain object`);
|
|
994
1004
|
}
|
|
995
1005
|
else {
|
|
996
|
-
for (const key of ["timeout", "parseFailure", "allProvidersFailed"]) {
|
|
1006
|
+
for (const key of ["timeout", "parseFailure", "rateLimited", "allProvidersFailed"]) {
|
|
997
1007
|
const value = providerConfig.failurePolicy[key];
|
|
998
1008
|
if (value !== undefined && (!isString(value) || !SUPPORTED_QC_FAILURE_ACTIONS.includes(value))) {
|
|
999
1009
|
result.valid = false;
|
|
@@ -20,8 +20,27 @@ const PROMOTED_MAP_PREFIX = ".polaris/map/";
|
|
|
20
20
|
const WORKSPACE_SCRATCH_PREFIX = ".taskchain_artifacts/";
|
|
21
21
|
const LEGACY_RUN_ARTIFACTS = new Set([
|
|
22
22
|
".polaris/runs/mutation-queue.json",
|
|
23
|
+
".polaris/runs/current-state.json",
|
|
24
|
+
".polaris/runs/run-report.md",
|
|
23
25
|
".polaris/runs/current-state.pre-pol-198.json",
|
|
24
26
|
]);
|
|
27
|
+
function isPromotedRunArchivePath(relativePath) {
|
|
28
|
+
if (!relativePath.startsWith(".polaris/runs/")) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const rest = relativePath.slice(".polaris/runs/".length);
|
|
32
|
+
if (rest === "" || rest === "evo-run-archive") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const slashIndex = rest.indexOf("/");
|
|
36
|
+
if (slashIndex === -1) {
|
|
37
|
+
// The path is a directory-style run archive path (e.g. `.polaris/runs/<run-id>`),
|
|
38
|
+
// not a top-level file like `ledger.jsonl`.
|
|
39
|
+
return !rest.includes(".") && !rest.startsWith(".");
|
|
40
|
+
}
|
|
41
|
+
const firstSegment = rest.slice(0, slashIndex);
|
|
42
|
+
return firstSegment !== "evo-run-archive";
|
|
43
|
+
}
|
|
25
44
|
function normalizeArtifactPath(filePath) {
|
|
26
45
|
const normalized = node_path_1.default.posix.normalize(filePath.replace(/\\/g, "/"));
|
|
27
46
|
return normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
@@ -59,6 +78,9 @@ function classifyArtifactPath(filePath, activeClusterId) {
|
|
|
59
78
|
if (relativePath === PROMOTED_RUN_LEDGER) {
|
|
60
79
|
return "promoted-run-ledger";
|
|
61
80
|
}
|
|
81
|
+
if (isPromotedRunArchivePath(relativePath)) {
|
|
82
|
+
return "promoted-run-archive";
|
|
83
|
+
}
|
|
62
84
|
if (relativePath.startsWith(PROMOTED_COGNITION_ARCHIVE_PREFIX)) {
|
|
63
85
|
return "promoted-cognition-archive";
|
|
64
86
|
}
|
|
@@ -80,6 +102,7 @@ function isPromotedArtifactPath(filePath, activeClusterId) {
|
|
|
80
102
|
const classification = classifyArtifactPath(filePath, activeClusterId);
|
|
81
103
|
return (classification === "promoted-cluster-artifact"
|
|
82
104
|
|| classification === "promoted-run-ledger"
|
|
105
|
+
|| classification === "promoted-run-archive"
|
|
83
106
|
|| classification === "promoted-cognition-archive"
|
|
84
107
|
|| classification === "promoted-map-artifact");
|
|
85
108
|
}
|
|
@@ -91,6 +114,8 @@ function explainArtifactPolicy(filePath, activeClusterId) {
|
|
|
91
114
|
return "active cluster evidence is eligible for promotion into finalize commits";
|
|
92
115
|
case "promoted-run-ledger":
|
|
93
116
|
return "the run ledger is durable audit evidence and stays commit-eligible";
|
|
117
|
+
case "promoted-run-archive":
|
|
118
|
+
return "archived run snapshots under .polaris/runs/<run-id>/ are durable audit evidence and stay commit-eligible";
|
|
94
119
|
case "promoted-cognition-archive":
|
|
95
120
|
return "archived cognition reconciliation notes are durable provenance and stay commit-eligible";
|
|
96
121
|
case "promoted-map-artifact":
|
|
@@ -114,6 +139,7 @@ function findArtifactPromotionViolations(stagedPaths, activeClusterId) {
|
|
|
114
139
|
if (classification === "non-artifact"
|
|
115
140
|
|| classification === "promoted-cluster-artifact"
|
|
116
141
|
|| classification === "promoted-run-ledger"
|
|
142
|
+
|| classification === "promoted-run-archive"
|
|
117
143
|
|| classification === "promoted-cognition-archive"
|
|
118
144
|
|| classification === "promoted-map-artifact") {
|
|
119
145
|
continue;
|
|
@@ -143,9 +169,11 @@ function getArtifactPromotionPolicy(activeClusterId) {
|
|
|
143
169
|
blocked: [
|
|
144
170
|
`${WORKSPACE_SCRATCH_PREFIX}**`,
|
|
145
171
|
"*.bak",
|
|
146
|
-
|
|
147
|
-
".polaris/runs/current-state.pre-pol-198.json",
|
|
172
|
+
...LEGACY_RUN_ARTIFACTS,
|
|
148
173
|
".polaris/runs/evo-run-archive/**",
|
|
174
|
+
".polaris/bootstrap/**",
|
|
175
|
+
".polaris/session-type",
|
|
176
|
+
".polaris/tmp/**",
|
|
149
177
|
".polaris/clusters/<other-cluster>/**",
|
|
150
178
|
],
|
|
151
179
|
};
|
|
@@ -163,10 +191,13 @@ function getGitignorePatterns() {
|
|
|
163
191
|
".taskchain_artifacts/**",
|
|
164
192
|
"*.bak",
|
|
165
193
|
".polaris/runs/mutation-queue.json",
|
|
194
|
+
".polaris/runs/current-state.json",
|
|
195
|
+
".polaris/runs/run-report.md",
|
|
166
196
|
".polaris/runs/current-state.pre-pol-198.json",
|
|
167
197
|
".polaris/runs/evo-run-archive/**",
|
|
168
198
|
".polaris/bootstrap/**",
|
|
169
199
|
".polaris/session-type",
|
|
200
|
+
".polaris/tmp/**",
|
|
170
201
|
"# Cognition staging — ephemeral, not committed",
|
|
171
202
|
".polaris/cognition/pending/**",
|
|
172
203
|
];
|
|
@@ -200,7 +231,8 @@ function isPathBlockedFromStaging(filePath) {
|
|
|
200
231
|
if (relativePath === ".polaris/runs/mutation-queue.json" ||
|
|
201
232
|
relativePath === ".polaris/runs/current-state.pre-pol-198.json" ||
|
|
202
233
|
relativePath.startsWith(".polaris/bootstrap/") ||
|
|
203
|
-
relativePath === ".polaris/session-type"
|
|
234
|
+
relativePath === ".polaris/session-type" ||
|
|
235
|
+
relativePath.startsWith(".polaris/tmp/")) {
|
|
204
236
|
return true;
|
|
205
237
|
}
|
|
206
238
|
// Block cognition pending staging
|
package/dist/finalize/index.js
CHANGED
|
@@ -259,8 +259,12 @@ const TRUSTED_QC_REPAIR_OUTCOMES = new Set(["pass", "qc-disabled", "no-repairabl
|
|
|
259
259
|
* Validate QC repair-loop terminal state when QC is enabled and repair routing
|
|
260
260
|
* is active. Returns null when finalize may proceed; returns a human-readable
|
|
261
261
|
* blocker string otherwise.
|
|
262
|
+
*
|
|
263
|
+
* A valid operator resolution artifact for the current repair round is accepted
|
|
264
|
+
* as equivalent to a trusted terminal_outcome; finalize does not mutate the
|
|
265
|
+
* loop state.
|
|
262
266
|
*/
|
|
263
|
-
function validateQcRepairLoopGate(state, config) {
|
|
267
|
+
function validateQcRepairLoopGate(state, config, repoRoot) {
|
|
264
268
|
// Gate only applies when QC is enabled
|
|
265
269
|
if (!config.enabled)
|
|
266
270
|
return null;
|
|
@@ -283,9 +287,24 @@ function validateQcRepairLoopGate(state, config) {
|
|
|
283
287
|
}
|
|
284
288
|
if (TRUSTED_QC_REPAIR_OUTCOMES.has(outcome))
|
|
285
289
|
return null;
|
|
290
|
+
// A valid operator resolution artifact for the current round overrides the
|
|
291
|
+
// untrusted terminal_outcome without mutating state.qc_repair_loop.
|
|
292
|
+
if (repoRoot &&
|
|
293
|
+
repairLoop.current_round &&
|
|
294
|
+
repairLoop.current_round > 0) {
|
|
295
|
+
const resolution = (0, index_js_2.readQcResolutionArtifact)(state.cluster_id, repairLoop.current_round, repoRoot);
|
|
296
|
+
if (resolution &&
|
|
297
|
+
TRUSTED_QC_REPAIR_OUTCOMES.has(resolution.resolvedOutcome)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const resolutionHint = outcome === "operator-review" || outcome === "medic-referral"
|
|
302
|
+
? ` Resolve with: polaris qc resolve --cluster-id ${state.cluster_id} --outcome <pass|no-repairable> --reason "<text>" [--findings <id1,id2,...>].`
|
|
303
|
+
: "";
|
|
286
304
|
return (`QC repair loop terminated with untrusted outcome: "${outcome}". ` +
|
|
287
305
|
`Only ${Array.from(TRUSTED_QC_REPAIR_OUTCOMES).join(", ")} outcomes allow finalize to proceed. ` +
|
|
288
|
-
`
|
|
306
|
+
`A valid resolution artifact for the current round is also accepted.` +
|
|
307
|
+
resolutionHint);
|
|
289
308
|
}
|
|
290
309
|
// ── Authoritative completed-child state cross-check ───────────────────────────
|
|
291
310
|
function warnOnMissingQcArtifacts(clusterState, repoRoot) {
|
|
@@ -546,7 +565,7 @@ async function runCompletedClusterQcWithRepair(options) {
|
|
|
546
565
|
repoRoot,
|
|
547
566
|
});
|
|
548
567
|
}
|
|
549
|
-
const repairLoopBlocker = validateQcRepairLoopGate(nextState, config.qc);
|
|
568
|
+
const repairLoopBlocker = validateQcRepairLoopGate(nextState, config.qc, repoRoot);
|
|
550
569
|
if (repairLoopBlocker) {
|
|
551
570
|
process.stderr.write(`${stepLabel} QC completed-cluster blocked finalize: ${qcResult.summary}\n` +
|
|
552
571
|
`${repairLoopBlocker}\n`);
|
|
@@ -754,7 +773,7 @@ async function runFinalize(options) {
|
|
|
754
773
|
// When QC is enabled and repair routing is active, require a trusted
|
|
755
774
|
// terminal outcome from the QC repair loop before allowing PR creation.
|
|
756
775
|
{
|
|
757
|
-
const repairLoopBlocker = validateQcRepairLoopGate(state, config.qc);
|
|
776
|
+
const repairLoopBlocker = validateQcRepairLoopGate(state, config.qc, repoRoot);
|
|
758
777
|
if (repairLoopBlocker) {
|
|
759
778
|
process.stderr.write(`finalize aborted: QC repair-loop gate failed.\n${repairLoopBlocker}\n`);
|
|
760
779
|
process.exit(1);
|