@lsctech/polaris 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autoresearch/index.js +30 -1
- package/dist/autoresearch/proposal.js +5 -0
- package/dist/autoresearch/score.js +174 -2
- package/dist/autoresearch/sol-evaluation-writer.js +135 -0
- package/dist/autoresearch/sol-evidence-loader.js +40 -3
- package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
- package/dist/autoresearch/sol-recommendations.js +212 -0
- package/dist/autoresearch/sol-report-renderer.js +282 -0
- package/dist/autoresearch/sol-report.js +44 -0
- package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
- package/dist/autoresearch/sol-scorer.js +45 -1
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/config/defaults.js +1 -0
- package/dist/config/validator.js +157 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +198 -4
- package/dist/finalize/linear.js +8 -4
- 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 +189 -33
- package/dist/loop/worker-packet.js +75 -1
- package/dist/qc/artifacts.js +27 -0
- package/dist/qc/fixtures/repair-packets.js +170 -0
- package/dist/qc/index.js +2 -0
- package/dist/qc/orchestration.js +5 -2
- package/dist/qc/policy.js +30 -3
- package/dist/qc/providers/coderabbit.js +152 -17
- package/dist/qc/registry.js +36 -3
- package/dist/qc/repair-loop.js +458 -0
- package/dist/qc/repair-packets.js +420 -0
- package/dist/qc/runner.js +328 -37
- package/dist/qc/schemas.js +79 -1
- package/dist/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,420 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GOVERNANCE_SCOPE_PATTERNS = void 0;
|
|
37
|
+
exports.isHighRiskCategory = isHighRiskCategory;
|
|
38
|
+
exports.getRepairRoundDir = getRepairRoundDir;
|
|
39
|
+
exports.getRepairPacketManifestPath = getRepairPacketManifestPath;
|
|
40
|
+
exports.compileRepairPackets = compileRepairPackets;
|
|
41
|
+
exports.writeRepairPacketManifest = writeRepairPacketManifest;
|
|
42
|
+
exports.compileAndWriteRepairPackets = compileAndWriteRepairPackets;
|
|
43
|
+
exports.readRepairPacketManifest = readRepairPacketManifest;
|
|
44
|
+
const node_fs_1 = require("node:fs");
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const severity_js_1 = require("./severity.js");
|
|
47
|
+
const routing_js_1 = require("./routing.js");
|
|
48
|
+
const schemas_js_1 = require("./schemas.js");
|
|
49
|
+
/** Scope patterns that autonomous repair packets must not touch. */
|
|
50
|
+
exports.GOVERNANCE_SCOPE_PATTERNS = [
|
|
51
|
+
"**/.polaris/**",
|
|
52
|
+
"**/.taskchain_artifacts/**",
|
|
53
|
+
"**/polaris.config.*",
|
|
54
|
+
"**/package.json",
|
|
55
|
+
"**/tsconfig*.json",
|
|
56
|
+
"**/.github/**",
|
|
57
|
+
"**/policy*",
|
|
58
|
+
"**/governance*",
|
|
59
|
+
];
|
|
60
|
+
const HIGH_RISK_CATEGORY_PATTERN = /security|secret|vulnerability|vuln|auth|crypto|injection|xss|csrf|sql|data-loss|migration|governance|compliance|privacy|pii|gdpr|hipaa/i;
|
|
61
|
+
/** Determine whether a finding category is high-risk/sensitive. */
|
|
62
|
+
function isHighRiskCategory(category) {
|
|
63
|
+
if (!category)
|
|
64
|
+
return false;
|
|
65
|
+
return HIGH_RISK_CATEGORY_PATTERN.test(category);
|
|
66
|
+
}
|
|
67
|
+
function riskLevel(finding) {
|
|
68
|
+
return isHighRiskCategory(finding.category) ? "high-risk" : "normal";
|
|
69
|
+
}
|
|
70
|
+
function confidenceBand(finding) {
|
|
71
|
+
return finding.attribution.confidence === "high" || finding.attribution.confidence === "medium"
|
|
72
|
+
? "clear"
|
|
73
|
+
: "unclear";
|
|
74
|
+
}
|
|
75
|
+
function subsystemFromFilePath(filePath) {
|
|
76
|
+
if (!filePath)
|
|
77
|
+
return "unknown";
|
|
78
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
79
|
+
return lastSlash === -1 ? "root" : filePath.slice(0, lastSlash);
|
|
80
|
+
}
|
|
81
|
+
function enrichFinding(finding, config, sourceQcRunId) {
|
|
82
|
+
return {
|
|
83
|
+
...finding,
|
|
84
|
+
routingTarget: resolveRoutingTarget(finding, config),
|
|
85
|
+
risk: riskLevel(finding),
|
|
86
|
+
confidenceBand: confidenceBand(finding),
|
|
87
|
+
subsystem: subsystemFromFilePath(finding.filePath),
|
|
88
|
+
sourceQcRunId,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the routing target for a finding, applying conservative safety overrides
|
|
93
|
+
* before falling back to the pre-computed routing decision or the routing policy.
|
|
94
|
+
*/
|
|
95
|
+
function resolveRoutingTarget(finding, config) {
|
|
96
|
+
// Critical/high severity or high-risk categories are escalated to operator review.
|
|
97
|
+
if ((0, severity_js_1.compareSeverity)(finding.severity, "high") >= 0 || riskLevel(finding) === "high-risk") {
|
|
98
|
+
return "operator-review";
|
|
99
|
+
}
|
|
100
|
+
// Broad findings without a concrete file path cannot be safely dispatched to repair workers.
|
|
101
|
+
if (!finding.filePath) {
|
|
102
|
+
return (0, severity_js_1.compareSeverity)(finding.severity, "high") >= 0 ? "operator-review" : "follow-up";
|
|
103
|
+
}
|
|
104
|
+
// Pre-existing findings are deferred unless severity already escalated above.
|
|
105
|
+
if (finding.attribution.reason === "pre-existing") {
|
|
106
|
+
return "follow-up";
|
|
107
|
+
}
|
|
108
|
+
// Low/unattributed confidence is not safe for autonomous repair.
|
|
109
|
+
if (finding.attribution.confidence === "low" || finding.attribution.confidence === "unattributed") {
|
|
110
|
+
return finding.attribution.childId ? "repair-worker" : "follow-up";
|
|
111
|
+
}
|
|
112
|
+
return finding.routingDecision ?? (0, routing_js_1.decideRepairRouting)(finding, config, finding.autofixEligible);
|
|
113
|
+
}
|
|
114
|
+
function rangesOverlapOrAdjacent(a, b, adjacency = 5) {
|
|
115
|
+
if (!a || !b)
|
|
116
|
+
return false;
|
|
117
|
+
const aStart = a.startLine ?? 1;
|
|
118
|
+
const aEnd = a.endLine ?? a.startLine ?? 1;
|
|
119
|
+
const bStart = b.startLine ?? 1;
|
|
120
|
+
const bEnd = b.endLine ?? b.startLine ?? 1;
|
|
121
|
+
return !(aEnd + adjacency < bStart || bEnd + adjacency < aStart);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Conservative grouping predicate.
|
|
125
|
+
*
|
|
126
|
+
* Findings in the same file are always grouped under the same routing target
|
|
127
|
+
* and risk level to avoid parallel edit conflicts. Cross-file grouping requires
|
|
128
|
+
* an exact match on severity, attribution confidence, category, and a specific
|
|
129
|
+
* subsystem directory (at least one path segment deep) so top-level directories
|
|
130
|
+
* do not collapse unrelated files.
|
|
131
|
+
*/
|
|
132
|
+
function shouldGroup(a, b) {
|
|
133
|
+
if (a.routingTarget !== b.routingTarget)
|
|
134
|
+
return false;
|
|
135
|
+
if (a.risk !== b.risk)
|
|
136
|
+
return false;
|
|
137
|
+
const sameFile = a.filePath && b.filePath && a.filePath === b.filePath;
|
|
138
|
+
if (sameFile)
|
|
139
|
+
return true;
|
|
140
|
+
const sameCategory = a.category && b.category && a.category === b.category;
|
|
141
|
+
const sameSubsystem = a.subsystem !== "unknown" && a.subsystem === b.subsystem;
|
|
142
|
+
const specificSubsystem = sameSubsystem && a.subsystem.includes("/");
|
|
143
|
+
if (a.severity === b.severity &&
|
|
144
|
+
a.confidenceBand === b.confidenceBand &&
|
|
145
|
+
sameCategory &&
|
|
146
|
+
specificSubsystem) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
class UnionFind {
|
|
152
|
+
parent;
|
|
153
|
+
constructor(size) {
|
|
154
|
+
this.parent = Array.from({ length: size }, (_, i) => i);
|
|
155
|
+
}
|
|
156
|
+
find(x) {
|
|
157
|
+
if (this.parent[x] !== x) {
|
|
158
|
+
this.parent[x] = this.find(this.parent[x]);
|
|
159
|
+
}
|
|
160
|
+
return this.parent[x];
|
|
161
|
+
}
|
|
162
|
+
union(a, b) {
|
|
163
|
+
const ra = this.find(a);
|
|
164
|
+
const rb = this.find(b);
|
|
165
|
+
if (ra !== rb) {
|
|
166
|
+
// Deterministic: lower index becomes root.
|
|
167
|
+
this.parent[rb] = ra;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function groupFindings(findings) {
|
|
172
|
+
const sorted = [...findings].sort((a, b) => a.findingId.localeCompare(b.findingId));
|
|
173
|
+
const uf = new UnionFind(sorted.length);
|
|
174
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
175
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
176
|
+
if (shouldGroup(sorted[i], sorted[j])) {
|
|
177
|
+
uf.union(i, j);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const groups = new Map();
|
|
182
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
183
|
+
const root = uf.find(i);
|
|
184
|
+
if (!groups.has(root)) {
|
|
185
|
+
groups.set(root, []);
|
|
186
|
+
}
|
|
187
|
+
groups.get(root).push(sorted[i]);
|
|
188
|
+
}
|
|
189
|
+
return [...groups.values()].sort((a, b) => a[0].findingId.localeCompare(b[0].findingId));
|
|
190
|
+
}
|
|
191
|
+
function buildProviderFailurePacket(result, index, clusterId, round, createdAt) {
|
|
192
|
+
const packetId = `pkt-${clusterId}-r${round}-${String(index).padStart(3, "0")}`;
|
|
193
|
+
return {
|
|
194
|
+
packetId,
|
|
195
|
+
round,
|
|
196
|
+
clusterId,
|
|
197
|
+
sourceQcRunIds: [result.qcRunId],
|
|
198
|
+
findingIds: [],
|
|
199
|
+
severityFloor: "critical",
|
|
200
|
+
rootCauseHint: `provider failure: ${result.status}; operator review required`,
|
|
201
|
+
allowedScope: [],
|
|
202
|
+
prohibitedScope: ["**/*"],
|
|
203
|
+
validationCommands: [],
|
|
204
|
+
routingTarget: "operator-review",
|
|
205
|
+
parallelGroup: null,
|
|
206
|
+
conflicts: [],
|
|
207
|
+
medic: true,
|
|
208
|
+
status: "pending",
|
|
209
|
+
createdAt,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function buildPacket(group, index, clusterId, round, allRunIds, validationCommands, createdAt) {
|
|
213
|
+
const packetId = `pkt-${clusterId}-r${round}-${String(index).padStart(3, "0")}`;
|
|
214
|
+
const findingIds = group.map((f) => f.findingId).sort();
|
|
215
|
+
const sourceRunIds = [...new Set(group.map((f) => f.sourceQcRunId))].sort();
|
|
216
|
+
const severityFloor = group.reduce((acc, f) => (0, severity_js_1.maxSeverity)(acc, f.severity), "info");
|
|
217
|
+
const categories = [...new Set(group.map((f) => f.category).filter(Boolean))].sort();
|
|
218
|
+
const filePaths = group.map((f) => f.filePath).filter((f) => Boolean(f));
|
|
219
|
+
const files = [...new Set(filePaths)].sort();
|
|
220
|
+
const rootCauseHint = [
|
|
221
|
+
`categories=[${categories.join(", ") || "uncategorized"}]`,
|
|
222
|
+
`files=[${files.join(", ") || "none"}]`,
|
|
223
|
+
`confidence=${group[0]?.confidenceBand ?? "clear"}`,
|
|
224
|
+
`severity=${severityFloor}`,
|
|
225
|
+
].join("; ");
|
|
226
|
+
const hasHighRisk = group.some((f) => f.risk === "high-risk");
|
|
227
|
+
const medic = group[0].routingTarget === "operator-review" &&
|
|
228
|
+
((0, severity_js_1.compareSeverity)(severityFloor, "high") >= 0 || hasHighRisk);
|
|
229
|
+
return {
|
|
230
|
+
packetId,
|
|
231
|
+
round,
|
|
232
|
+
clusterId,
|
|
233
|
+
sourceQcRunIds: sourceRunIds.length > 0 ? sourceRunIds : allRunIds,
|
|
234
|
+
findingIds,
|
|
235
|
+
severityFloor,
|
|
236
|
+
rootCauseHint,
|
|
237
|
+
allowedScope: files,
|
|
238
|
+
prohibitedScope: hasHighRisk ? exports.GOVERNANCE_SCOPE_PATTERNS : [],
|
|
239
|
+
validationCommands,
|
|
240
|
+
routingTarget: group[0].routingTarget,
|
|
241
|
+
parallelGroup: null,
|
|
242
|
+
conflicts: [],
|
|
243
|
+
medic,
|
|
244
|
+
status: "pending",
|
|
245
|
+
createdAt,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function scopeOverlap(a, b) {
|
|
249
|
+
const aScope = new Set([...a.allowedScope, ...a.prohibitedScope]);
|
|
250
|
+
const bScope = new Set([...b.allowedScope, ...b.prohibitedScope]);
|
|
251
|
+
for (const s of aScope) {
|
|
252
|
+
if (bScope.has(s))
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
function packetsConflict(a, b) {
|
|
258
|
+
// Medic packets are never parallel-safe.
|
|
259
|
+
if (a.medic || b.medic)
|
|
260
|
+
return true;
|
|
261
|
+
return scopeOverlap(a, b);
|
|
262
|
+
}
|
|
263
|
+
function assignParallelGroups(packets) {
|
|
264
|
+
const sorted = [...packets].sort((a, b) => a.packetId.localeCompare(b.packetId));
|
|
265
|
+
const groups = [];
|
|
266
|
+
for (const packet of sorted) {
|
|
267
|
+
let assigned = false;
|
|
268
|
+
for (let i = 0; i < groups.length; i++) {
|
|
269
|
+
if (!groups[i].some((p) => packetsConflict(p, packet))) {
|
|
270
|
+
groups[i].push(packet);
|
|
271
|
+
assigned = true;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!assigned) {
|
|
276
|
+
groups.push([packet]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const packetToGroup = new Map();
|
|
280
|
+
groups.forEach((g, i) => g.forEach((p) => packetToGroup.set(p.packetId, i)));
|
|
281
|
+
return sorted.map((packet) => {
|
|
282
|
+
const groupIndex = packetToGroup.get(packet.packetId);
|
|
283
|
+
const conflicts = sorted
|
|
284
|
+
.filter((p) => p.packetId !== packet.packetId && packetsConflict(packet, p))
|
|
285
|
+
.map((p) => p.packetId)
|
|
286
|
+
.sort();
|
|
287
|
+
return {
|
|
288
|
+
...packet,
|
|
289
|
+
parallelGroup: groupIndex === undefined ? null : `g-${String(groupIndex).padStart(3, "0")}`,
|
|
290
|
+
conflicts,
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/** Returns the directory for a repair-round manifest. */
|
|
295
|
+
function getRepairRoundDir(clusterId, round, repoRoot) {
|
|
296
|
+
return path.join(repoRoot || process.cwd(), ".polaris", "clusters", clusterId, "qc", "repair-rounds", String(round));
|
|
297
|
+
}
|
|
298
|
+
/** Returns the persisted path for a repair-round manifest. */
|
|
299
|
+
function getRepairPacketManifestPath(clusterId, round, repoRoot) {
|
|
300
|
+
return path.join(getRepairRoundDir(clusterId, round, repoRoot), "repair-packets.json");
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Compile normalized QC findings into cluster-scoped repair packet manifests.
|
|
304
|
+
*
|
|
305
|
+
* The compiler is deterministic for the same inputs (grouping, conflict detection,
|
|
306
|
+
* and parallel group assignment are stable) and is conservative about merging
|
|
307
|
+
* unrelated security/auth/data-loss/migration/governance findings with normal repairs.
|
|
308
|
+
*/
|
|
309
|
+
function compileRepairPackets(input) {
|
|
310
|
+
const { clusterId, round, qcResults, config, validationCommands = [], repoRoot, compiledAt = new Date().toISOString(), } = input;
|
|
311
|
+
const sortedResults = [...qcResults].sort((a, b) => a.qcRunId.localeCompare(b.qcRunId));
|
|
312
|
+
const allRunIds = sortedResults.map((r) => r.qcRunId);
|
|
313
|
+
const packets = [];
|
|
314
|
+
// Provider failure artifacts that require operator review are surfaced as Medic packets.
|
|
315
|
+
for (const result of sortedResults) {
|
|
316
|
+
if (result.allProvidersFailed || result.status === "failed" || result.status === "blocked") {
|
|
317
|
+
packets.push(buildProviderFailurePacket(result, packets.length, clusterId, round, compiledAt));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Actionable findings from successful runs.
|
|
321
|
+
const actionableFindings = [];
|
|
322
|
+
for (const result of sortedResults) {
|
|
323
|
+
if (result.status === "failed" || result.status === "blocked") {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
for (const finding of result.findings) {
|
|
327
|
+
if (finding.status === "waived" ||
|
|
328
|
+
finding.status === "autofixed" ||
|
|
329
|
+
finding.status === "repaired") {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
actionableFindings.push(enrichFinding(finding, config, result.qcRunId));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Deduplicate by findingId; keep first occurrence in sorted run order.
|
|
336
|
+
const seenIds = new Set();
|
|
337
|
+
const uniqueFindings = [];
|
|
338
|
+
for (const finding of actionableFindings) {
|
|
339
|
+
if (seenIds.has(finding.findingId))
|
|
340
|
+
continue;
|
|
341
|
+
seenIds.add(finding.findingId);
|
|
342
|
+
uniqueFindings.push(finding);
|
|
343
|
+
}
|
|
344
|
+
const groups = groupFindings(uniqueFindings);
|
|
345
|
+
for (const group of groups) {
|
|
346
|
+
packets.push(buildPacket(group, packets.length, clusterId, round, allRunIds, validationCommands, compiledAt));
|
|
347
|
+
}
|
|
348
|
+
const finalPackets = assignParallelGroups(packets);
|
|
349
|
+
const manifest = {
|
|
350
|
+
schemaVersion: "1.0",
|
|
351
|
+
clusterId,
|
|
352
|
+
round,
|
|
353
|
+
compiledAt,
|
|
354
|
+
sourceQcRunIds: allRunIds,
|
|
355
|
+
packets: finalPackets,
|
|
356
|
+
};
|
|
357
|
+
const manifestPath = getRepairPacketManifestPath(clusterId, round, repoRoot);
|
|
358
|
+
return {
|
|
359
|
+
clusterId,
|
|
360
|
+
round,
|
|
361
|
+
packets: finalPackets,
|
|
362
|
+
manifest,
|
|
363
|
+
manifestPath,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Persist a compiled repair packet manifest as a durable cluster artifact.
|
|
368
|
+
* Writes atomically via a temp file + rename and returns the absolute path.
|
|
369
|
+
*/
|
|
370
|
+
function writeRepairPacketManifest(manifest, repoRoot) {
|
|
371
|
+
const manifestPath = getRepairPacketManifestPath(manifest.clusterId, manifest.round, repoRoot);
|
|
372
|
+
const dir = path.dirname(manifestPath);
|
|
373
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
374
|
+
const tempPath = `${manifestPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
375
|
+
try {
|
|
376
|
+
(0, node_fs_1.writeFileSync)(tempPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
377
|
+
(0, node_fs_1.renameSync)(tempPath, manifestPath);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
try {
|
|
381
|
+
(0, node_fs_1.unlinkSync)(tempPath);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Ignore cleanup failure.
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
return manifestPath;
|
|
389
|
+
}
|
|
390
|
+
/** Convenience helper that compiles and persists repair packets in one call. */
|
|
391
|
+
function compileAndWriteRepairPackets(input) {
|
|
392
|
+
const result = compileRepairPackets(input);
|
|
393
|
+
writeRepairPacketManifest(result.manifest, input.repoRoot);
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Read and validate a persisted repair packet manifest.
|
|
398
|
+
* Returns null if the file is missing or fails schema validation.
|
|
399
|
+
*/
|
|
400
|
+
function readRepairPacketManifest(clusterId, round, repoRoot) {
|
|
401
|
+
const manifestPath = getRepairPacketManifestPath(clusterId, round, repoRoot);
|
|
402
|
+
try {
|
|
403
|
+
const data = (0, node_fs_1.readFileSync)(manifestPath, "utf-8");
|
|
404
|
+
const parsed = JSON.parse(data);
|
|
405
|
+
const validation = (0, schemas_js_1.validateRepairPacketManifest)(parsed);
|
|
406
|
+
if (!validation.success) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
return validation.manifest;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
if (error.code === "ENOENT") {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
if (error instanceof SyntaxError) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|