@lsctech/polaris 0.5.7 → 0.5.9
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/index.js +30 -1
- package/dist/autoresearch/sol-evaluation-writer.js +135 -0
- package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
- package/dist/autoresearch/sol-recommendations.js +130 -0
- package/dist/autoresearch/sol-report-renderer.js +282 -0
- package/dist/autoresearch/sol-run-health-bridge.js +246 -0
- package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cli/medic.js +65 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/config/defaults.js +3 -0
- package/dist/config/validator.js +141 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +231 -5
- package/dist/finalize/linear.js +8 -4
- package/dist/finalize/medic-gate.js +42 -0
- package/dist/finalize/steps/08-create-pr.js +2 -2
- package/dist/finalize/steps/11-update-linear.js +2 -2
- package/dist/loop/parent.js +234 -28
- package/dist/loop/worker-packet.js +19 -1
- package/dist/medic/run-health-consult.js +381 -0
- package/dist/medic/treatment-packets.js +169 -0
- package/dist/qc/artifacts.js +27 -0
- package/dist/qc/orchestration.js +3 -2
- package/dist/qc/providers/coderabbit.js +114 -11
- package/dist/qc/repair-loop.js +49 -14
- package/dist/qc/runner.js +10 -2
- package/dist/qc/schemas.js +1 -0
- package/dist/run-health/foreman-symptoms.js +100 -0
- package/dist/run-health/index.js +301 -0
- package/dist/run-health/qc-escalation.js +155 -0
- package/dist/run-health/schema.js +123 -0
- package/dist/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.getRunHealthDir = getRunHealthDir;
|
|
18
|
+
exports.getRunHealthReportPath = getRunHealthReportPath;
|
|
19
|
+
exports.getRunHealthMarkdownPath = getRunHealthMarkdownPath;
|
|
20
|
+
exports.readRunHealthReport = readRunHealthReport;
|
|
21
|
+
exports.createRunHealthReport = createRunHealthReport;
|
|
22
|
+
exports.appendSymptom = appendSymptom;
|
|
23
|
+
exports.markBypassed = markBypassed;
|
|
24
|
+
exports.markMedicDecision = markMedicDecision;
|
|
25
|
+
exports.upsertWorkerSymptoms = upsertWorkerSymptoms;
|
|
26
|
+
exports.isMedicGateSatisfied = isMedicGateSatisfied;
|
|
27
|
+
const node_fs_1 = require("node:fs");
|
|
28
|
+
const node_path_1 = require("node:path");
|
|
29
|
+
const schema_js_1 = require("./schema.js");
|
|
30
|
+
// ──────────────────────────────────────────────
|
|
31
|
+
// Path resolution
|
|
32
|
+
// ──────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Returns the directory that contains the run-health report for a given run.
|
|
35
|
+
* Active reports live under .polaris/runs/<run-id>/
|
|
36
|
+
*/
|
|
37
|
+
function getRunHealthDir(runId, repoRoot) {
|
|
38
|
+
return (0, node_path_1.join)(repoRoot ?? process.cwd(), ".polaris", "runs", runId);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the absolute path to the run-health JSON report.
|
|
42
|
+
* The file is created on demand; its absence means no symptoms occurred.
|
|
43
|
+
*/
|
|
44
|
+
function getRunHealthReportPath(runId, repoRoot) {
|
|
45
|
+
return (0, node_path_1.join)(getRunHealthDir(runId, repoRoot), "run-health-report.json");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns the optional Markdown sibling path for operator review.
|
|
49
|
+
* The .md file is never required for machine consumption.
|
|
50
|
+
*/
|
|
51
|
+
function getRunHealthMarkdownPath(runId, repoRoot) {
|
|
52
|
+
return (0, node_path_1.join)(getRunHealthDir(runId, repoRoot), "run-health-report.md");
|
|
53
|
+
}
|
|
54
|
+
// ──────────────────────────────────────────────
|
|
55
|
+
// Atomic write helper
|
|
56
|
+
// ──────────────────────────────────────────────
|
|
57
|
+
function writeJsonAtomic(filePath, data) {
|
|
58
|
+
const tempPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
59
|
+
try {
|
|
60
|
+
(0, node_fs_1.writeFileSync)(tempPath, JSON.stringify(data, null, 2), "utf-8");
|
|
61
|
+
(0, node_fs_1.renameSync)(tempPath, filePath);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
try {
|
|
65
|
+
(0, node_fs_1.unlinkSync)(tempPath);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore cleanup failure.
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ──────────────────────────────────────────────
|
|
74
|
+
// Read
|
|
75
|
+
// ──────────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Read the run-health report for a given run.
|
|
78
|
+
* Returns null when no symptoms have been recorded (file absent or unreadable).
|
|
79
|
+
* Throws if the file exists but fails schema validation — this indicates
|
|
80
|
+
* a corrupt artifact that must not be silently ignored.
|
|
81
|
+
*/
|
|
82
|
+
function readRunHealthReport(runId, repoRoot) {
|
|
83
|
+
const filePath = getRunHealthReportPath(runId, repoRoot);
|
|
84
|
+
if (!(0, node_fs_1.existsSync)(filePath))
|
|
85
|
+
return null;
|
|
86
|
+
let raw;
|
|
87
|
+
try {
|
|
88
|
+
raw = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (err.code === "ENOENT")
|
|
92
|
+
return null;
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
const validation = (0, schema_js_1.validateRunHealthReport)(parsed);
|
|
97
|
+
if (!validation.valid) {
|
|
98
|
+
throw new Error(`run-health report for run "${runId}" failed schema validation:\n` +
|
|
99
|
+
validation.errors.join("\n"));
|
|
100
|
+
}
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Create a new run-health report with the first symptom.
|
|
105
|
+
* Throws if a report already exists for this run — use appendSymptom instead.
|
|
106
|
+
* Returns the created report (immutable copy).
|
|
107
|
+
*/
|
|
108
|
+
function createRunHealthReport(params) {
|
|
109
|
+
const { runId, clusterId, firstSymptom, evidenceRefs, sourceActor, repoRoot } = params;
|
|
110
|
+
const filePath = getRunHealthReportPath(runId, repoRoot);
|
|
111
|
+
if ((0, node_fs_1.existsSync)(filePath)) {
|
|
112
|
+
throw new Error(`run-health report already exists for run "${runId}" — use appendSymptom to add symptoms`);
|
|
113
|
+
}
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
const report = {
|
|
116
|
+
schema_version: schema_js_1.SCHEMA_VERSION,
|
|
117
|
+
run_id: runId,
|
|
118
|
+
cluster_id: clusterId,
|
|
119
|
+
symptoms: [firstSymptom],
|
|
120
|
+
evidence_refs: evidenceRefs ?? [],
|
|
121
|
+
created_at: now,
|
|
122
|
+
updated_at: now,
|
|
123
|
+
source_actor: sourceActor,
|
|
124
|
+
};
|
|
125
|
+
const validation = (0, schema_js_1.validateRunHealthReport)(report);
|
|
126
|
+
if (!validation.valid) {
|
|
127
|
+
throw new Error(`Invalid run-health report:\n${validation.errors.join("\n")}`);
|
|
128
|
+
}
|
|
129
|
+
(0, node_fs_1.mkdirSync)(getRunHealthDir(runId, repoRoot), { recursive: true });
|
|
130
|
+
writeJsonAtomic(filePath, report);
|
|
131
|
+
return Object.freeze({ ...report });
|
|
132
|
+
}
|
|
133
|
+
// ──────────────────────────────────────────────
|
|
134
|
+
// Append
|
|
135
|
+
// ──────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Append a symptom to an existing run-health report.
|
|
138
|
+
* Creates the report if it does not exist (convenience overload — callers
|
|
139
|
+
* may use createRunHealthReport for explicit first-write semantics).
|
|
140
|
+
* Returns the updated report (immutable copy).
|
|
141
|
+
*/
|
|
142
|
+
function appendSymptom(runId, symptom, repoRoot) {
|
|
143
|
+
const filePath = getRunHealthReportPath(runId, repoRoot);
|
|
144
|
+
const existing = readRunHealthReport(runId, repoRoot);
|
|
145
|
+
if (!existing) {
|
|
146
|
+
throw new Error(`No run-health report found for run "${runId}" — use createRunHealthReport first`);
|
|
147
|
+
}
|
|
148
|
+
const updated = {
|
|
149
|
+
...existing,
|
|
150
|
+
symptoms: [...existing.symptoms, symptom],
|
|
151
|
+
updated_at: new Date().toISOString(),
|
|
152
|
+
};
|
|
153
|
+
const validation = (0, schema_js_1.validateRunHealthReport)(updated);
|
|
154
|
+
if (!validation.valid) {
|
|
155
|
+
throw new Error(`Appended symptom produces invalid report:\n${validation.errors.join("\n")}`);
|
|
156
|
+
}
|
|
157
|
+
writeJsonAtomic(filePath, updated);
|
|
158
|
+
return Object.freeze({ ...updated });
|
|
159
|
+
}
|
|
160
|
+
// ──────────────────────────────────────────────
|
|
161
|
+
// Mark bypassed
|
|
162
|
+
// ──────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Record that policy gates based on this report have been explicitly bypassed.
|
|
165
|
+
* Idempotent: re-calling with the same reason overwrites the bypass metadata.
|
|
166
|
+
* Returns the updated report (immutable copy).
|
|
167
|
+
*/
|
|
168
|
+
function markBypassed(runId, bypass, repoRoot) {
|
|
169
|
+
const filePath = getRunHealthReportPath(runId, repoRoot);
|
|
170
|
+
const existing = readRunHealthReport(runId, repoRoot);
|
|
171
|
+
if (!existing) {
|
|
172
|
+
throw new Error(`No run-health report found for run "${runId}"`);
|
|
173
|
+
}
|
|
174
|
+
const updated = {
|
|
175
|
+
...existing,
|
|
176
|
+
policy_bypass: bypass,
|
|
177
|
+
updated_at: new Date().toISOString(),
|
|
178
|
+
};
|
|
179
|
+
const validation = (0, schema_js_1.validateRunHealthReport)(updated);
|
|
180
|
+
if (!validation.valid) {
|
|
181
|
+
throw new Error(`Policy bypass produces invalid report:\n${validation.errors.join("\n")}`);
|
|
182
|
+
}
|
|
183
|
+
writeJsonAtomic(filePath, updated);
|
|
184
|
+
return Object.freeze({ ...updated });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Record the Medic consult status and associated artifact references.
|
|
188
|
+
* Merges with any existing consult metadata (chart/treatment refs are additive).
|
|
189
|
+
* Returns the updated report (immutable copy).
|
|
190
|
+
*/
|
|
191
|
+
function markMedicDecision(runId, decision, repoRoot) {
|
|
192
|
+
const filePath = getRunHealthReportPath(runId, repoRoot);
|
|
193
|
+
const existing = readRunHealthReport(runId, repoRoot);
|
|
194
|
+
if (!existing) {
|
|
195
|
+
throw new Error(`No run-health report found for run "${runId}"`);
|
|
196
|
+
}
|
|
197
|
+
const prevConsult = existing.medic_consult;
|
|
198
|
+
const mergedConsult = {
|
|
199
|
+
status: decision.status,
|
|
200
|
+
chart_refs: [
|
|
201
|
+
...(prevConsult?.chart_refs ?? []),
|
|
202
|
+
...(decision.chartRefs ?? []),
|
|
203
|
+
],
|
|
204
|
+
treatment_packet_refs: [
|
|
205
|
+
...(prevConsult?.treatment_packet_refs ?? []),
|
|
206
|
+
...(decision.treatmentPacketRefs ?? []),
|
|
207
|
+
],
|
|
208
|
+
resolved_at: decision.resolvedAt ?? prevConsult?.resolved_at,
|
|
209
|
+
resolution_notes: decision.resolutionNotes ?? prevConsult?.resolution_notes,
|
|
210
|
+
};
|
|
211
|
+
const updated = {
|
|
212
|
+
...existing,
|
|
213
|
+
medic_consult: mergedConsult,
|
|
214
|
+
updated_at: new Date().toISOString(),
|
|
215
|
+
};
|
|
216
|
+
const validation = (0, schema_js_1.validateRunHealthReport)(updated);
|
|
217
|
+
if (!validation.valid) {
|
|
218
|
+
throw new Error(`Medic decision produces invalid report:\n${validation.errors.join("\n")}`);
|
|
219
|
+
}
|
|
220
|
+
writeJsonAtomic(filePath, updated);
|
|
221
|
+
return Object.freeze({ ...updated });
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Ingest worker-reported symptoms into the run-health report.
|
|
225
|
+
*
|
|
226
|
+
* Creates the report when one does not yet exist; appends to it when one does.
|
|
227
|
+
* No-ops when `symptoms` is empty — no report is created or modified.
|
|
228
|
+
* Returns the updated report, or null when no symptoms were provided.
|
|
229
|
+
*/
|
|
230
|
+
function upsertWorkerSymptoms(params) {
|
|
231
|
+
const { runId, clusterId, childId, workerId, provider, symptoms, repoRoot } = params;
|
|
232
|
+
if (!symptoms || symptoms.length === 0)
|
|
233
|
+
return null;
|
|
234
|
+
const sourceActor = {
|
|
235
|
+
role: "worker",
|
|
236
|
+
child_id: childId,
|
|
237
|
+
worker_id: workerId,
|
|
238
|
+
provider,
|
|
239
|
+
};
|
|
240
|
+
const existing = readRunHealthReport(runId, repoRoot);
|
|
241
|
+
const offset = existing?.symptoms.length ?? 0;
|
|
242
|
+
const toRunHealthSymptom = (s, index) => ({
|
|
243
|
+
// Stable id: child + category + (offset + index) so repeated calls don't collide
|
|
244
|
+
id: `${childId}:${s.category}:${offset + index}`,
|
|
245
|
+
severity: mapCategoryToSeverity(s.category),
|
|
246
|
+
code: s.category,
|
|
247
|
+
message: s.message,
|
|
248
|
+
source_actor: sourceActor,
|
|
249
|
+
evidence_refs: s.evidence_refs ?? [],
|
|
250
|
+
occurred_at: s.occurred_at,
|
|
251
|
+
});
|
|
252
|
+
if (!existing) {
|
|
253
|
+
const [first, ...rest] = symptoms.map(toRunHealthSymptom);
|
|
254
|
+
const report = createRunHealthReport({
|
|
255
|
+
runId,
|
|
256
|
+
clusterId,
|
|
257
|
+
firstSymptom: first,
|
|
258
|
+
sourceActor,
|
|
259
|
+
repoRoot,
|
|
260
|
+
});
|
|
261
|
+
return rest.reduce((acc, sym) => appendSymptom(runId, sym, repoRoot), report);
|
|
262
|
+
}
|
|
263
|
+
return symptoms
|
|
264
|
+
.map(toRunHealthSymptom)
|
|
265
|
+
.reduce((acc, sym) => appendSymptom(runId, sym, repoRoot), existing);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Maps a worker symptom category to a run-health severity level.
|
|
269
|
+
* Category-to-severity is intentionally opinionated but not final —
|
|
270
|
+
* Medic may re-classify during triage.
|
|
271
|
+
*/
|
|
272
|
+
function mapCategoryToSeverity(category) {
|
|
273
|
+
switch (category) {
|
|
274
|
+
case 'worker-blocked': return 'high';
|
|
275
|
+
case 'validation-failed': return 'high';
|
|
276
|
+
case 'repeated-rework': return 'medium';
|
|
277
|
+
case 'unclear-requirements': return 'medium';
|
|
278
|
+
case 'unusual-assumption': return 'low';
|
|
279
|
+
default: return 'medium';
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ──────────────────────────────────────────────
|
|
283
|
+
// Re-export schema types and validation for consumers
|
|
284
|
+
// ──────────────────────────────────────────────
|
|
285
|
+
/**
|
|
286
|
+
* Returns true when a run-health report has a satisfied Medic gate:
|
|
287
|
+
* either a resolved/bypassed decision, or an explicit policy bypass.
|
|
288
|
+
* Used by both loop/parent.ts and finalize/medic-gate.ts to ensure
|
|
289
|
+
* consistent gate evaluation.
|
|
290
|
+
*/
|
|
291
|
+
function isMedicGateSatisfied(report) {
|
|
292
|
+
const status = report.medic_consult?.status;
|
|
293
|
+
if (status === "resolved" || status === "bypassed")
|
|
294
|
+
return true;
|
|
295
|
+
if (report.policy_bypass)
|
|
296
|
+
return true;
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
__exportStar(require("./schema.js"), exports);
|
|
300
|
+
__exportStar(require("./foreman-symptoms.js"), exports);
|
|
301
|
+
__exportStar(require("./qc-escalation.js"), exports);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QC escalation criteria → run-health evidence refs.
|
|
4
|
+
*
|
|
5
|
+
* QC providers are artifact producers only; they never write to the
|
|
6
|
+
* run-health report directly. This module is called by Foreman/orchestration
|
|
7
|
+
* code after QC results are available, and it determines which results meet
|
|
8
|
+
* escalation criteria before appending evidence-referenced symptoms to the
|
|
9
|
+
* run-health report.
|
|
10
|
+
*
|
|
11
|
+
* Escalation criteria:
|
|
12
|
+
* - blocking findings (policyDecision.blocksDelivery)
|
|
13
|
+
* - repeated findings after repair (findings present in a post-repair rerun)
|
|
14
|
+
* - unusable output (failureReason: "unusable-output")
|
|
15
|
+
* - parse failure (parserResult: "failed")
|
|
16
|
+
* - all providers failed (allProvidersFailed flag)
|
|
17
|
+
* - noisy provider output (large findings count with no blocking finding)
|
|
18
|
+
* - max repair rounds exhausted
|
|
19
|
+
* - repair-loop dispatch failures (medic-referral outcome)
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.appendQcEscalationSymptoms = appendQcEscalationSymptoms;
|
|
23
|
+
exports.appendRepairLoopOutcomeSymptom = appendRepairLoopOutcomeSymptom;
|
|
24
|
+
const node_crypto_1 = require("node:crypto");
|
|
25
|
+
const index_js_1 = require("./index.js");
|
|
26
|
+
/**
|
|
27
|
+
* Threshold for "noisy output": more than this many findings without a
|
|
28
|
+
* blocking finding is classified as noisy.
|
|
29
|
+
*/
|
|
30
|
+
const NOISY_FINDINGS_THRESHOLD = 20;
|
|
31
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
32
|
+
function makeQcSourceActor() {
|
|
33
|
+
return { role: "foreman" };
|
|
34
|
+
}
|
|
35
|
+
function makeQcSymptom(code, message, evidenceRefs) {
|
|
36
|
+
return {
|
|
37
|
+
id: `qc:${code}:${(0, node_crypto_1.randomUUID)()}`,
|
|
38
|
+
severity: qcEscalationSeverity(code),
|
|
39
|
+
code,
|
|
40
|
+
message,
|
|
41
|
+
source_actor: makeQcSourceActor(),
|
|
42
|
+
evidence_refs: evidenceRefs,
|
|
43
|
+
occurred_at: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function qcEscalationSeverity(code) {
|
|
47
|
+
switch (code) {
|
|
48
|
+
case "qc-blocking-findings":
|
|
49
|
+
case "qc-all-providers-failed":
|
|
50
|
+
case "qc-repair-dispatch-failure":
|
|
51
|
+
return "high";
|
|
52
|
+
case "qc-repeated-findings":
|
|
53
|
+
case "qc-parse-failure":
|
|
54
|
+
case "qc-unusable-output":
|
|
55
|
+
case "qc-max-repair-rounds":
|
|
56
|
+
return "medium";
|
|
57
|
+
case "qc-noisy-output":
|
|
58
|
+
return "low";
|
|
59
|
+
default:
|
|
60
|
+
return "medium";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function upsertQcSymptom(runId, clusterId, symptom, repoRoot) {
|
|
64
|
+
try {
|
|
65
|
+
const existing = (0, index_js_1.readRunHealthReport)(runId, repoRoot);
|
|
66
|
+
if (!existing) {
|
|
67
|
+
(0, index_js_1.createRunHealthReport)({
|
|
68
|
+
runId,
|
|
69
|
+
clusterId,
|
|
70
|
+
firstSymptom: symptom,
|
|
71
|
+
sourceActor: makeQcSourceActor(),
|
|
72
|
+
repoRoot,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
(0, index_js_1.appendSymptom)(runId, symptom, repoRoot);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// QC escalation must never block the run.
|
|
81
|
+
// ponytail: surface to telemetry in a future pass
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Evaluate QC results against escalation criteria and append any matching
|
|
86
|
+
* symptoms to the run-health report.
|
|
87
|
+
*
|
|
88
|
+
* No-ops when no escalation criteria are met.
|
|
89
|
+
* Never throws.
|
|
90
|
+
*/
|
|
91
|
+
function appendQcEscalationSymptoms(params) {
|
|
92
|
+
const { runId, clusterId, qcResults, afterRepair = false, repoRoot } = params;
|
|
93
|
+
// Collect artifact paths as evidence refs for each result.
|
|
94
|
+
const allEvidenceRefs = qcResults.flatMap((r) => r.rawArtifactPaths ?? []);
|
|
95
|
+
// ── All providers failed ───────────────────────────────────────────────────
|
|
96
|
+
const allFailed = qcResults.length > 0 && qcResults.every((r) => r.allProvidersFailed || r.status === "failed");
|
|
97
|
+
if (allFailed) {
|
|
98
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-all-providers-failed", `All QC providers failed (${qcResults.length} provider(s)); no review was performed.`, allEvidenceRefs), repoRoot);
|
|
99
|
+
return; // If all providers failed, other criteria cannot be evaluated reliably.
|
|
100
|
+
}
|
|
101
|
+
for (const result of qcResults) {
|
|
102
|
+
const evidenceRefs = result.rawArtifactPaths ?? [];
|
|
103
|
+
// ── Parse failure ──────────────────────────────────────────────────────
|
|
104
|
+
if (result.providerAttempt?.parserResult === "failed") {
|
|
105
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-parse-failure", `QC provider "${result.provider}" output could not be parsed (qcRunId: ${result.qcRunId}).`, evidenceRefs), repoRoot);
|
|
106
|
+
continue; // No findings to evaluate from a failed parse.
|
|
107
|
+
}
|
|
108
|
+
// ── Unusable output ────────────────────────────────────────────────────
|
|
109
|
+
if (result.providerAttempt?.failureReason === "unusable-output" ||
|
|
110
|
+
result.providerAttempt?.failureReason === "empty-output") {
|
|
111
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-unusable-output", `QC provider "${result.provider}" produced unusable output (reason: ${result.providerAttempt.failureReason}).`, evidenceRefs), repoRoot);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// ── Blocking findings ──────────────────────────────────────────────────
|
|
115
|
+
if (result.policyDecision?.blocksDelivery) {
|
|
116
|
+
const blockingCount = result.findings.filter((f) => f.status === "open" || f.status === "follow-up").length;
|
|
117
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-blocking-findings", `QC provider "${result.provider}" has ${blockingCount} blocking finding(s) that prevent delivery (qcRunId: ${result.qcRunId}).`, evidenceRefs), repoRoot);
|
|
118
|
+
}
|
|
119
|
+
// ── Repeated findings after repair ─────────────────────────────────────
|
|
120
|
+
if (afterRepair && result.findings.some((f) => f.status === "open")) {
|
|
121
|
+
const repeatedCount = result.findings.filter((f) => f.status === "open").length;
|
|
122
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-repeated-findings", `QC provider "${result.provider}" still shows ${repeatedCount} open finding(s) after repair (qcRunId: ${result.qcRunId}).`, evidenceRefs), repoRoot);
|
|
123
|
+
}
|
|
124
|
+
// ── Noisy provider output ──────────────────────────────────────────────
|
|
125
|
+
if (result.findings.length > NOISY_FINDINGS_THRESHOLD &&
|
|
126
|
+
!result.policyDecision?.blocksDelivery) {
|
|
127
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-noisy-output", `QC provider "${result.provider}" produced ${result.findings.length} findings (>${NOISY_FINDINGS_THRESHOLD} threshold) without a blocking policy decision — possible noise.`, evidenceRefs), repoRoot);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Append run-health symptoms for non-passing QC repair loop outcomes.
|
|
133
|
+
*
|
|
134
|
+
* Handles: max-rounds, medic-referral (repair dispatch failure), all-providers-failed.
|
|
135
|
+
* The pass/no-repairable/qc-disabled/operator-review outcomes produce no symptoms.
|
|
136
|
+
* Never throws.
|
|
137
|
+
*/
|
|
138
|
+
function appendRepairLoopOutcomeSymptom(params) {
|
|
139
|
+
const { runId, clusterId, repairResult, repoRoot } = params;
|
|
140
|
+
const evidenceRefs = repairResult.final_qc_results.flatMap((r) => r.rawArtifactPaths ?? []);
|
|
141
|
+
switch (repairResult.outcome) {
|
|
142
|
+
case "max-rounds":
|
|
143
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-max-repair-rounds", `QC repair loop exhausted max rounds (${repairResult.rounds_completed}) without passing: ${repairResult.summary}`, evidenceRefs), repoRoot);
|
|
144
|
+
break;
|
|
145
|
+
case "medic-referral":
|
|
146
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-repair-dispatch-failure", `QC repair loop terminated with Medic referral after ${repairResult.rounds_completed} round(s) — repair worker(s) failed: ${repairResult.summary}`, evidenceRefs), repoRoot);
|
|
147
|
+
break;
|
|
148
|
+
case "all-providers-failed":
|
|
149
|
+
upsertQcSymptom(runId, clusterId, makeQcSymptom("qc-all-providers-failed", `All QC providers failed during repair loop (rounds: ${repairResult.rounds_completed}): ${repairResult.summary}`, evidenceRefs), repoRoot);
|
|
150
|
+
break;
|
|
151
|
+
// pass, no-repairable, qc-disabled, operator-review — no symptom needed
|
|
152
|
+
default:
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RunHealthReport = exports.MedicConsult = exports.MedicConsultStatus = exports.PolicyBypassMetadata = exports.RunHealthSymptom = exports.SourceActor = exports.SymptomSeverity = exports.SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.validateRunHealthReport = validateRunHealthReport;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
/**
|
|
7
|
+
* Run-health report schema — v1.
|
|
8
|
+
*
|
|
9
|
+
* The run-health report is the canonical artifact for recording symptoms that
|
|
10
|
+
* occur during a Polaris run. It is the single source of truth used by workers,
|
|
11
|
+
* Foreman, closeout, SOL, and Medic.
|
|
12
|
+
*
|
|
13
|
+
* Design rules:
|
|
14
|
+
* - Report is ONLY created when at least one symptom occurs (YAGNI).
|
|
15
|
+
* - JSON is the machine-readable source of truth; optional .md sibling for
|
|
16
|
+
* operator review.
|
|
17
|
+
* - QC artifacts are referenced by path/id only — QC never writes symptoms.
|
|
18
|
+
* - All mutations use atomic temp-file + rename to avoid partial writes.
|
|
19
|
+
*/
|
|
20
|
+
exports.SCHEMA_VERSION = "1";
|
|
21
|
+
// ──────────────────────────────────────────────
|
|
22
|
+
// Sub-schemas
|
|
23
|
+
// ──────────────────────────────────────────────
|
|
24
|
+
exports.SymptomSeverity = zod_1.z.enum(["critical", "high", "medium", "low"]);
|
|
25
|
+
exports.SourceActor = zod_1.z.object({
|
|
26
|
+
/** Polaris role: worker, foreman, medic, etc. */
|
|
27
|
+
role: zod_1.z.string().min(1),
|
|
28
|
+
/** Child task id when emitted from a worker or child session. */
|
|
29
|
+
child_id: zod_1.z.string().optional(),
|
|
30
|
+
/** Worker instance id from dispatch record. */
|
|
31
|
+
worker_id: zod_1.z.string().optional(),
|
|
32
|
+
/** Provider name (e.g. "devin", "claude"). */
|
|
33
|
+
provider: zod_1.z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
exports.RunHealthSymptom = zod_1.z.object({
|
|
36
|
+
/** Unique symptom id within this report (opaque, stable across appends). */
|
|
37
|
+
id: zod_1.z.string().min(1),
|
|
38
|
+
severity: exports.SymptomSeverity,
|
|
39
|
+
/**
|
|
40
|
+
* Short machine-readable code (e.g. "build-failure", "test-regression").
|
|
41
|
+
* Consumers use this for routing; keep it stable between runs.
|
|
42
|
+
*/
|
|
43
|
+
code: zod_1.z.string().min(1),
|
|
44
|
+
/** Human-readable description for operator review. */
|
|
45
|
+
message: zod_1.z.string().min(1),
|
|
46
|
+
/** Actor that observed and recorded this symptom. */
|
|
47
|
+
source_actor: exports.SourceActor,
|
|
48
|
+
/**
|
|
49
|
+
* Paths or ids of QC/evidence artifacts this symptom is based on.
|
|
50
|
+
* QC artifacts live at .polaris/clusters/<cluster-id>/qc/<qcRunId>.json.
|
|
51
|
+
* Symptom producers reference them by path; QC itself never writes here.
|
|
52
|
+
*/
|
|
53
|
+
evidence_refs: zod_1.z.array(zod_1.z.string()).default([]),
|
|
54
|
+
occurred_at: zod_1.z.string().datetime(),
|
|
55
|
+
});
|
|
56
|
+
exports.PolicyBypassMetadata = zod_1.z.object({
|
|
57
|
+
reason: zod_1.z.string().min(1),
|
|
58
|
+
/** Actor that approved the bypass. */
|
|
59
|
+
bypassed_by: zod_1.z.string().min(1),
|
|
60
|
+
bypassed_at: zod_1.z.string().datetime(),
|
|
61
|
+
});
|
|
62
|
+
exports.MedicConsultStatus = zod_1.z.enum([
|
|
63
|
+
"pending",
|
|
64
|
+
"in-progress",
|
|
65
|
+
"resolved",
|
|
66
|
+
"bypassed",
|
|
67
|
+
]);
|
|
68
|
+
exports.MedicConsult = zod_1.z.object({
|
|
69
|
+
status: exports.MedicConsultStatus,
|
|
70
|
+
/**
|
|
71
|
+
* References to Medic chart files (CHART-YYYY-MM-DD-NNN format).
|
|
72
|
+
* Charts live under .polaris/charts/. Referenced by id/path, never embedded.
|
|
73
|
+
*/
|
|
74
|
+
chart_refs: zod_1.z.array(zod_1.z.string()).default([]),
|
|
75
|
+
/**
|
|
76
|
+
* References to Medic treatment packet files.
|
|
77
|
+
* Treatment packets live under .polaris/clusters/<id>/medic/.
|
|
78
|
+
* Referenced by path, never embedded.
|
|
79
|
+
*/
|
|
80
|
+
treatment_packet_refs: zod_1.z.array(zod_1.z.string()).default([]),
|
|
81
|
+
resolved_at: zod_1.z.string().datetime().optional(),
|
|
82
|
+
resolution_notes: zod_1.z.string().optional(),
|
|
83
|
+
});
|
|
84
|
+
// ──────────────────────────────────────────────
|
|
85
|
+
// Root schema
|
|
86
|
+
// ──────────────────────────────────────────────
|
|
87
|
+
exports.RunHealthReport = zod_1.z.object({
|
|
88
|
+
schema_version: zod_1.z.literal(exports.SCHEMA_VERSION),
|
|
89
|
+
run_id: zod_1.z.string().min(1),
|
|
90
|
+
cluster_id: zod_1.z.string().min(1),
|
|
91
|
+
/** Ordered list of symptoms; newest appended at end. */
|
|
92
|
+
symptoms: zod_1.z.array(exports.RunHealthSymptom),
|
|
93
|
+
/**
|
|
94
|
+
* Run-level evidence refs (e.g. telemetry path, state snapshot).
|
|
95
|
+
* Individual symptoms carry their own evidence_refs; this field holds
|
|
96
|
+
* refs that apply to the report as a whole.
|
|
97
|
+
*/
|
|
98
|
+
evidence_refs: zod_1.z.array(zod_1.z.string()).default([]),
|
|
99
|
+
/**
|
|
100
|
+
* Present only when an operator or SOL has explicitly bypassed policy
|
|
101
|
+
* gates based on this report. Absence means no bypass has been granted.
|
|
102
|
+
*/
|
|
103
|
+
policy_bypass: exports.PolicyBypassMetadata.optional(),
|
|
104
|
+
/**
|
|
105
|
+
* Present only when Medic has been consulted. Absence means no Medic
|
|
106
|
+
* consult has occurred — closeout must not infer severity from absence.
|
|
107
|
+
*/
|
|
108
|
+
medic_consult: exports.MedicConsult.optional(),
|
|
109
|
+
created_at: zod_1.z.string().datetime(),
|
|
110
|
+
updated_at: zod_1.z.string().datetime(),
|
|
111
|
+
/** Actor that created the report (first producer). */
|
|
112
|
+
source_actor: exports.SourceActor,
|
|
113
|
+
});
|
|
114
|
+
function validateRunHealthReport(data) {
|
|
115
|
+
const result = exports.RunHealthReport.safeParse(data);
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
return {
|
|
118
|
+
valid: false,
|
|
119
|
+
errors: result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { valid: true, errors: [] };
|
|
123
|
+
}
|