@claudexor/review 3.1.2 → 3.3.7-rc.0
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/findings.d.ts +11 -0
- package/dist/findings.d.ts.map +1 -1
- package/dist/findings.js +48 -5
- package/dist/findings.js.map +1 -1
- package/dist/readiness.d.ts.map +1 -1
- package/dist/revalidate.js.map +1 -1
- package/dist/reviewEngine.d.ts +2 -4
- package/dist/reviewEngine.d.ts.map +1 -1
- package/dist/reviewEngine.js +168 -350
- package/dist/reviewEngine.js.map +1 -1
- package/dist/reviewPrompt.d.ts +122 -1
- package/dist/reviewPrompt.d.ts.map +1 -1
- package/dist/reviewPrompt.js +102 -17
- package/dist/reviewPrompt.js.map +1 -1
- package/dist/reviewRuntimeTypes.d.ts +1 -0
- package/dist/reviewRuntimeTypes.d.ts.map +1 -1
- package/dist/reviewRuntimeTypes.js.map +1 -1
- package/dist/reviewerCostKnowledge.d.ts.map +1 -1
- package/dist/reviewerSpendAccumulator.d.ts.map +1 -1
- package/dist/reviewerWorkspace.d.ts +24 -0
- package/dist/reviewerWorkspace.d.ts.map +1 -0
- package/dist/reviewerWorkspace.js +360 -0
- package/dist/reviewerWorkspace.js.map +1 -0
- package/dist/route.js.map +1 -1
- package/dist/sealedReviewEnvelope.d.ts +20 -0
- package/dist/sealedReviewEnvelope.d.ts.map +1 -0
- package/dist/sealedReviewEnvelope.js +176 -0
- package/dist/sealedReviewEnvelope.js.map +1 -0
- package/package.json +6 -6
package/dist/reviewEngine.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { parseUnifiedDiff, runCapture } from "@claudexor/core";
|
|
2
1
|
import { preflightEvidence, writeDiffEvidence } from "@claudexor/context";
|
|
3
2
|
import { HarnessRunSpec, ReviewFinding as ReviewFindingSchema } from "@claudexor/schema";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { appendLine, containsSecretLikeToken, ensureDir, newId, nowIso, readTextSafe, redactSecrets,
|
|
9
|
-
import { dedupeFindings, extractJsonBlocks, parseFindingsDetailed, } from "./findings.js";
|
|
10
|
-
import { buildReviewPrompt } from "./reviewPrompt.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
5
|
+
import { rm } from "node:fs/promises";
|
|
6
|
+
import { join, relative, resolve } from "node:path";
|
|
7
|
+
import { appendLine, containsSecretLikeToken, engineBuildIdentity, ensureDir, newId, nowIso, readTextSafe, redactSecrets, sha256, writeJson, writeText, } from "@claudexor/util";
|
|
8
|
+
import { dedupeFindings, extractJsonBlocks, parseFindingsDetailed, parseSealedReviewEnvelopeDetailed, sealedReviewTranscriptChunk, } from "./findings.js";
|
|
9
|
+
import { buildReviewPrompt, SEALED_REVIEW_OUTPUT_SCHEMA } from "./reviewPrompt.js";
|
|
10
|
+
import { sealedReviewTranscriptFromEvents } from "./sealedReviewEnvelope.js";
|
|
11
|
+
import { buildReviewerCandidateInventory, cleanupTemporaryReviewerWorkspaceBaseDir, copyReviewEvidencePacket, extractDiffPostimagePaths, isSameOrInside, prepareReviewerWorkspace, selectReviewerWorkspaceBaseDir, } from "./reviewerWorkspace.js";
|
|
12
|
+
export { extractDiffPostimagePaths as __testExtractDiffPostimagePaths } from "./reviewerWorkspace.js";
|
|
11
13
|
import { buildRouteProof, classifyDiversity } from "./route.js";
|
|
12
14
|
import { reviewerAuthMode, reviewerAuthSwitchFromEvent } from "./reviewRuntimeTypes.js";
|
|
13
15
|
import { ReviewerCostKnowledge } from "./reviewerCostKnowledge.js";
|
|
@@ -18,8 +20,6 @@ const DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY = {
|
|
|
18
20
|
initialDelayMs: 1_000,
|
|
19
21
|
maxDelayMs: 10_000,
|
|
20
22
|
};
|
|
21
|
-
const BLOCKED_REVIEWER_RUNTIME_ROOTS = new Set("auth cache daemon home homes logs runs secrets state tmp workspaces".split(" "));
|
|
22
|
-
const TEXT_EVIDENCE_SUFFIXES = [".md", ".txt", ".json", ".yaml", ".yml", ".patch"];
|
|
23
23
|
const REVIEW_WAVE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
24
24
|
function readExistingDiffEvidence(dir, diff) {
|
|
25
25
|
const diffText = diff.endsWith("\n") ? diff : `${diff}\n`;
|
|
@@ -55,6 +55,26 @@ function reviewerInfo(reviewer, routeProofStatus, observedModel = null) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
export async function reviewCandidate(input) {
|
|
58
|
+
const sourceRoot = resolve(input.cwd);
|
|
59
|
+
const sourceExists = existsSync(sourceRoot);
|
|
60
|
+
const canonicalSourceRoot = sourceExists ? realpathSync(sourceRoot) : sourceRoot;
|
|
61
|
+
const sourceEvidencePath = resolve(input.evidenceDir);
|
|
62
|
+
const sourceEvidenceRoot = existsSync(input.evidenceDir)
|
|
63
|
+
? realpathSync(input.evidenceDir)
|
|
64
|
+
: sourceEvidencePath;
|
|
65
|
+
if (sourceExists && isSameOrInside(sourceEvidenceRoot, canonicalSourceRoot)) {
|
|
66
|
+
throw new Error("review evidence directory must not contain the candidate root");
|
|
67
|
+
}
|
|
68
|
+
const evidenceInsideLexicalSource = sourceExists && isSameOrInside(sourceRoot, sourceEvidencePath);
|
|
69
|
+
const evidenceInsideCanonicalSource = sourceExists && isSameOrInside(canonicalSourceRoot, sourceEvidenceRoot);
|
|
70
|
+
const evidencePathInSourceNamespace = evidenceInsideCanonicalSource
|
|
71
|
+
? resolve(sourceRoot, relative(canonicalSourceRoot, sourceEvidenceRoot))
|
|
72
|
+
: null;
|
|
73
|
+
const candidateEvidenceExcludeRoots = evidenceInsideLexicalSource || evidenceInsideCanonicalSource
|
|
74
|
+
? [
|
|
75
|
+
...new Set([sourceEvidencePath, sourceEvidenceRoot, evidencePathInSourceNamespace].filter((path) => path !== null)),
|
|
76
|
+
]
|
|
77
|
+
: [];
|
|
58
78
|
const findingsByReviewer = input.reviewers.map(() => []);
|
|
59
79
|
const reviewerFamilies = input.reviewers.map((reviewer) => reviewer.providerFamily);
|
|
60
80
|
const routeProofs = input.reviewers.map((reviewer, index) => reviewerRouteProof(reviewer, null, "unavailable", reviewerFamilies.filter((_, otherIndex) => otherIndex !== index)));
|
|
@@ -72,12 +92,26 @@ export async function reviewCandidate(input) {
|
|
|
72
92
|
throw new Error("sealed release review requires CLAUDEXOR_REVIEW_WAVE_ID UUID");
|
|
73
93
|
}
|
|
74
94
|
const frozenMetadata = input.frozenIdentity
|
|
75
|
-
? {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
? (() => {
|
|
96
|
+
const runtime = engineBuildIdentity();
|
|
97
|
+
const runtimeEntry = realpathSync(runtime.entry);
|
|
98
|
+
const runtimeEntryStat = lstatSync(runtimeEntry);
|
|
99
|
+
if (!runtimeEntryStat.isFile() || runtimeEntryStat.isSymbolicLink()) {
|
|
100
|
+
throw new Error("sealed release review runtime entry is not a regular file");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
candidate_sha: input.frozenIdentity.candidateSha,
|
|
104
|
+
candidate_tree: input.frozenIdentity.candidateTree,
|
|
105
|
+
packet_manifest_sha256: input.frozenIdentity.packetManifestSha256,
|
|
106
|
+
review_runtime_version: runtime.version,
|
|
107
|
+
review_runtime_build_sha: runtime.sha,
|
|
108
|
+
review_runtime_entry: runtimeEntry,
|
|
109
|
+
review_runtime_entry_sha256: createHash("sha256")
|
|
110
|
+
.update(readFileSync(runtimeEntry))
|
|
111
|
+
.digest("hex"),
|
|
112
|
+
...(reviewWaveId ? { review_wave_id: reviewWaveId } : {}),
|
|
113
|
+
};
|
|
114
|
+
})()
|
|
81
115
|
: {};
|
|
82
116
|
if (containsSecretLikeToken(input.diff || "(empty diff)\n")) {
|
|
83
117
|
throw new Error("diff evidence contains a secret-like token; refusing to persist raw DIFF.patch");
|
|
@@ -100,7 +134,15 @@ export async function reviewCandidate(input) {
|
|
|
100
134
|
].filter(Boolean);
|
|
101
135
|
throw new Error(`mandatory evidence preflight failed (${parts.join("; ")})`);
|
|
102
136
|
}
|
|
137
|
+
const postimagePaths = extractDiffPostimagePaths(input.diff);
|
|
138
|
+
const candidateInventory = await buildReviewerCandidateInventory(input.cwd, postimagePaths, input.evidenceReadOnly === true);
|
|
103
139
|
const artifactsBaseDir = input.artifactsDir ?? join(input.evidenceDir, "reviewer-artifacts");
|
|
140
|
+
if (input.evidenceReadOnly &&
|
|
141
|
+
input.frozenIdentity &&
|
|
142
|
+
existsSync(artifactsBaseDir) &&
|
|
143
|
+
readdirSync(artifactsBaseDir).length > 0) {
|
|
144
|
+
throw new Error("sealed release review requires a fresh empty artifacts directory");
|
|
145
|
+
}
|
|
104
146
|
ensureDir(artifactsBaseDir);
|
|
105
147
|
const persistentEvidenceDir = join(artifactsBaseDir, "evidence");
|
|
106
148
|
await copyReviewEvidencePacket(input.evidenceDir, persistentEvidenceDir, input.evidenceReadOnly === true);
|
|
@@ -116,11 +158,11 @@ export async function reviewCandidate(input) {
|
|
|
116
158
|
diff_path: persistentPatch.diffPath,
|
|
117
159
|
summary_path: persistentPatch.summaryPath,
|
|
118
160
|
diff_sha256: persistentPatch.diffSha256,
|
|
119
|
-
|
|
161
|
+
candidate_inventory_mode: candidateInventory.mode,
|
|
162
|
+
candidate_inventory_reason: candidateInventory.reason,
|
|
120
163
|
...frozenMetadata,
|
|
121
164
|
});
|
|
122
165
|
const artifacts = input.reviewers.map(() => undefined);
|
|
123
|
-
const preservePaths = extractDiffTouchedPaths(input.diff);
|
|
124
166
|
const reviewerWorkspaceBaseDir = selectReviewerWorkspaceBaseDir(input.cwd, artifactsBaseDir, input.evidenceDir);
|
|
125
167
|
const runReviewer = async (reviewer, index) => {
|
|
126
168
|
if (input.signal?.aborted)
|
|
@@ -135,8 +177,9 @@ export async function reviewCandidate(input) {
|
|
|
135
177
|
sourceEvidenceDir: persistentEvidenceDir,
|
|
136
178
|
workspaceBaseDir: reviewerWorkspaceBaseDir,
|
|
137
179
|
reviewerDirName: `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`,
|
|
138
|
-
excludeRoots: [artifactsBaseDir],
|
|
139
|
-
|
|
180
|
+
excludeRoots: [artifactsBaseDir, ...candidateEvidenceExcludeRoots],
|
|
181
|
+
postimagePaths,
|
|
182
|
+
candidateCopyPaths: candidateInventory.copyPaths,
|
|
140
183
|
preserveEvidenceBytes: input.evidenceReadOnly === true,
|
|
141
184
|
});
|
|
142
185
|
const reviewerPatch = input.evidenceReadOnly
|
|
@@ -152,34 +195,42 @@ export async function reviewCandidate(input) {
|
|
|
152
195
|
persistent_diff_path: persistentPatch.diffPath,
|
|
153
196
|
persistent_summary_path: persistentPatch.summaryPath,
|
|
154
197
|
diff_sha256: persistentPatch.diffSha256,
|
|
155
|
-
|
|
198
|
+
candidate_inventory_mode: candidateInventory.mode,
|
|
199
|
+
candidate_inventory_reason: candidateInventory.reason,
|
|
156
200
|
...frozenMetadata,
|
|
157
201
|
});
|
|
158
|
-
const runtimePrompt = buildReviewPrompt(input.candidateLabel, reviewerWorkspace.root, reviewerWorkspace.evidenceDir, reviewerPatch,
|
|
202
|
+
const runtimePrompt = buildReviewPrompt(input.candidateLabel, reviewerWorkspace.root, reviewerWorkspace.evidenceDir, reviewerPatch, {
|
|
203
|
+
sealed: input.evidenceReadOnly === true,
|
|
204
|
+
candidateInventoryMode: candidateInventory.mode,
|
|
205
|
+
});
|
|
159
206
|
spec = HarnessRunSpec.parse({
|
|
160
207
|
session_id: newId("rev"),
|
|
161
208
|
intent: "review",
|
|
162
209
|
prompt: runtimePrompt,
|
|
163
210
|
cwd: reviewerWorkspace.root,
|
|
164
211
|
access: "readonly",
|
|
212
|
+
...(input.evidenceReadOnly && input.frozenIdentity
|
|
213
|
+
? {
|
|
214
|
+
external_context_policy: "live",
|
|
215
|
+
tool_permission_policy: { web: "live", allow: [], deny: [] },
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
165
218
|
model_hint: reviewer.requestedModel ?? null,
|
|
166
219
|
effort_hint: reviewer.requestedEffort ?? null,
|
|
167
220
|
auth_preference: reviewer.authPreference ?? "auto",
|
|
168
221
|
env_inheritance: input.envInheritance ?? "mirror_native",
|
|
222
|
+
...(input.evidenceReadOnly && input.frozenIdentity
|
|
223
|
+
? { output_schema: SEALED_REVIEW_OUTPUT_SCHEMA }
|
|
224
|
+
: {}),
|
|
169
225
|
...(input.env ? { env: input.env } : {}),
|
|
170
226
|
});
|
|
171
|
-
writeText(artifact.promptPath,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
Runtime prompt used during review follows. Its candidate-tree paths may be transient after orchestrator cleanup; use the durable replay paths above for audit/replay.
|
|
180
|
-
|
|
181
|
-
${runtimePrompt}
|
|
182
|
-
`));
|
|
227
|
+
writeText(artifact.promptPath, spec.prompt);
|
|
228
|
+
updateReviewerMetadata(artifact, {
|
|
229
|
+
session_id: spec.session_id,
|
|
230
|
+
external_context_policy: spec.external_context_policy,
|
|
231
|
+
tool_web_policy: spec.tool_permission_policy.web,
|
|
232
|
+
submitted_prompt_sha256: createHash("sha256").update(spec.prompt).digest("hex"),
|
|
233
|
+
});
|
|
183
234
|
}
|
|
184
235
|
catch (err) {
|
|
185
236
|
const failedAt = nowIso();
|
|
@@ -210,9 +261,13 @@ ${runtimePrompt}
|
|
|
210
261
|
let routeModel;
|
|
211
262
|
let routeSource = "unavailable";
|
|
212
263
|
let reviewerError = null;
|
|
264
|
+
let sealedProjectionError = null;
|
|
213
265
|
try {
|
|
214
|
-
const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.
|
|
266
|
+
const out = await collectReviewerOutput(reviewer, spec, reviewerTimeoutMs, input.evidenceReadOnly && input.frozenIdentity
|
|
267
|
+
? { maxRetries: 0, initialDelayMs: 0, maxDelayMs: 0 }
|
|
268
|
+
: (input.transientRetryPolicy ?? DEFAULT_REVIEWER_TRANSIENT_RETRY_POLICY), artifact, input.onReviewerEvent, input.signal, input.evidenceReadOnly === true);
|
|
215
269
|
text = out.text;
|
|
270
|
+
sealedProjectionError = out.sealedProjectionError ?? null;
|
|
216
271
|
streamObservedModel = out.observedModel;
|
|
217
272
|
routeModel = out.observedModel;
|
|
218
273
|
routeSource = out.observedSource;
|
|
@@ -228,6 +283,9 @@ ${runtimePrompt}
|
|
|
228
283
|
if (typeof partial?.partialText === "string" && partial.partialText.trim() !== "") {
|
|
229
284
|
text = partial.partialText;
|
|
230
285
|
}
|
|
286
|
+
if (typeof partial?.partialSealedProjectionError === "string") {
|
|
287
|
+
sealedProjectionError = partial.partialSealedProjectionError;
|
|
288
|
+
}
|
|
231
289
|
reviewerSpend.recordPartial(index, partial);
|
|
232
290
|
if (partial?.partialObservedModel) {
|
|
233
291
|
streamObservedModel = partial.partialObservedModel;
|
|
@@ -242,8 +300,24 @@ ${runtimePrompt}
|
|
|
242
300
|
const proof = reviewerRouteProof(reviewer, routeModel ?? null, routeSource, reviewerFamilies.filter((_, i) => i !== index));
|
|
243
301
|
routeProofs[index] = proof;
|
|
244
302
|
const info = reviewerInfo(reviewer, proof.status, streamObservedModel ?? null);
|
|
245
|
-
const
|
|
303
|
+
const sealedParse = input.evidenceReadOnly
|
|
304
|
+
? parseSealedReviewEnvelopeDetailed(text, info)
|
|
305
|
+
: null;
|
|
306
|
+
const jsonBlocks = sealedParse?.blocks ?? extractJsonBlocks(text);
|
|
246
307
|
writeJson(artifact.parsedPath, redactValue(jsonBlocks));
|
|
308
|
+
if (sealedParse && (sealedProjectionError || sealedParse.error)) {
|
|
309
|
+
const detail = sealedProjectionError ?? sealedParse.error ?? "invalid sealed review output";
|
|
310
|
+
writeParseError(artifact, {
|
|
311
|
+
error: "invalid_sealed_review_envelope",
|
|
312
|
+
detail,
|
|
313
|
+
malformed: sealedParse.malformed,
|
|
314
|
+
text_sha256: sha256(text),
|
|
315
|
+
...(reviewerError ? { reviewer_error: reviewerError } : {}),
|
|
316
|
+
});
|
|
317
|
+
findingsByReviewer[index]?.push(...sealedParse.findings);
|
|
318
|
+
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Invalid sealed review envelope: ${detail}.`));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
247
321
|
if (reviewerError && (text.trim() === "" || jsonBlocks.length === 0)) {
|
|
248
322
|
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, `Reviewer failed: ${reviewerError}`));
|
|
249
323
|
return;
|
|
@@ -253,7 +327,7 @@ ${runtimePrompt}
|
|
|
253
327
|
findingsByReviewer[index]?.push(insufficientEvidenceFinding(info, "Reviewer produced no parseable JSON findings."));
|
|
254
328
|
return;
|
|
255
329
|
}
|
|
256
|
-
const parsed = parseFindingsDetailed(text, info);
|
|
330
|
+
const parsed = sealedParse ?? parseFindingsDetailed(text, info);
|
|
257
331
|
const parseError = {};
|
|
258
332
|
let parsedFindingsRecorded = false;
|
|
259
333
|
const recordParsedFindings = () => {
|
|
@@ -262,7 +336,7 @@ ${runtimePrompt}
|
|
|
262
336
|
findingsByReviewer[index]?.push(...parsed.findings);
|
|
263
337
|
parsedFindingsRecorded = true;
|
|
264
338
|
};
|
|
265
|
-
if (parsed.malformed > 0) {
|
|
339
|
+
if (parsed.malformed > 0 && !sealedParse?.error) {
|
|
266
340
|
Object.assign(parseError, {
|
|
267
341
|
error: "malformed_findings",
|
|
268
342
|
malformed: parsed.malformed,
|
|
@@ -342,7 +416,7 @@ ${runtimePrompt}
|
|
|
342
416
|
await cleanupTemporaryReviewerWorkspaceBaseDir(reviewerWorkspaceBaseDir, artifactsBaseDir);
|
|
343
417
|
}
|
|
344
418
|
}
|
|
345
|
-
async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPolicy, artifact, onReviewerEvent, signal) {
|
|
419
|
+
async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPolicy, artifact, onReviewerEvent, signal, sealed = false) {
|
|
346
420
|
const controller = new AbortController();
|
|
347
421
|
spec.extra["abortSignal"] = controller.signal;
|
|
348
422
|
const startMs = Date.now();
|
|
@@ -371,6 +445,8 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
371
445
|
let observedSource = "unavailable";
|
|
372
446
|
let observedAuthMode = null;
|
|
373
447
|
let currentAuthMode = null;
|
|
448
|
+
const observedAuthModes = new Set();
|
|
449
|
+
const ignoredSettings = new Set();
|
|
374
450
|
let costUsd = 0;
|
|
375
451
|
let costEstimated = false;
|
|
376
452
|
let cashUsd = 0;
|
|
@@ -390,9 +466,20 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
390
466
|
let lastError = null;
|
|
391
467
|
let attemptObservedModel;
|
|
392
468
|
let attemptObservedSource = "unavailable";
|
|
469
|
+
const sealedMessageEvents = [];
|
|
393
470
|
for await (const ev of iter) {
|
|
394
471
|
const eventTime = nowIso();
|
|
395
|
-
|
|
472
|
+
const persistedEvent = redactValue(ev);
|
|
473
|
+
appendLine(artifact.eventsPath, JSON.stringify(persistedEvent));
|
|
474
|
+
const ignored = ev.payload?.["ignored_settings"];
|
|
475
|
+
if (Array.isArray(ignored)) {
|
|
476
|
+
for (const item of ignored)
|
|
477
|
+
if (typeof item === "string")
|
|
478
|
+
ignoredSettings.add(item);
|
|
479
|
+
if (ignoredSettings.size > 0) {
|
|
480
|
+
updateReviewerMetadata(artifact, { ignored_settings: [...ignoredSettings] });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
396
483
|
if (ev.transient)
|
|
397
484
|
sawTransient = true;
|
|
398
485
|
if (ev.type === "error") {
|
|
@@ -413,8 +500,11 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
413
500
|
});
|
|
414
501
|
}
|
|
415
502
|
const disclosedAuthMode = reviewerAuthMode(ev.credential_route);
|
|
416
|
-
if (disclosedAuthMode)
|
|
503
|
+
if (disclosedAuthMode) {
|
|
417
504
|
currentAuthMode = disclosedAuthMode;
|
|
505
|
+
observedAuthModes.add(disclosedAuthMode);
|
|
506
|
+
updateReviewerMetadata(artifact, { auth_modes: [...observedAuthModes] });
|
|
507
|
+
}
|
|
418
508
|
costKnowledge.observeEvent(currentAuthMode);
|
|
419
509
|
if (!observedAuthMode) {
|
|
420
510
|
observedAuthMode = disclosedAuthMode;
|
|
@@ -454,11 +544,24 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
454
544
|
unknown_usd: unknownUsd,
|
|
455
545
|
});
|
|
456
546
|
}
|
|
547
|
+
if (sealed && ev.type === "message" && ev.final === true) {
|
|
548
|
+
sealedMessageEvents.push(persistedEvent);
|
|
549
|
+
}
|
|
457
550
|
if (ev.type === "message" && ev.text && ev.payload?.["auth_switched"] !== true) {
|
|
458
|
-
const safeText =
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
551
|
+
const safeText = sealedReviewTranscriptChunk(persistedEvent);
|
|
552
|
+
if (safeText !== null) {
|
|
553
|
+
if (sealed) {
|
|
554
|
+
// Keep the long-running transcript visibly active for monitors. A
|
|
555
|
+
// clean completion replaces these progress bytes with the exact
|
|
556
|
+
// typed-final projection used by the sealed parser and sealer.
|
|
557
|
+
appendLine(artifact.transcriptPath, safeText);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
text += safeText;
|
|
561
|
+
partialText += safeText;
|
|
562
|
+
appendLine(artifact.transcriptPath, safeText);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
462
565
|
}
|
|
463
566
|
if (ev.observed_model) {
|
|
464
567
|
observedModel = ev.observed_model;
|
|
@@ -478,6 +581,17 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
478
581
|
if (isCancelled()) {
|
|
479
582
|
throw new Error("Reviewer cancelled");
|
|
480
583
|
}
|
|
584
|
+
let sealedProjectionError;
|
|
585
|
+
if (sealed) {
|
|
586
|
+
try {
|
|
587
|
+
text = sealedReviewTranscriptFromEvents(sealedMessageEvents);
|
|
588
|
+
partialText = text;
|
|
589
|
+
writeText(artifact.transcriptPath, text);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
sealedProjectionError = error instanceof Error ? error.message : String(error);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
481
595
|
if (sawTransient &&
|
|
482
596
|
text.trim() === "" &&
|
|
483
597
|
nativeTry < transientRetryPolicy.maxRetries &&
|
|
@@ -511,7 +625,9 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
511
625
|
return consumeOnce(nativeTry + 1);
|
|
512
626
|
}
|
|
513
627
|
if (sawError && !timedOut) {
|
|
514
|
-
throw new Error(`Reviewer emitted error event: ${lastError ?? "unknown error"}`)
|
|
628
|
+
throw Object.assign(new Error(`Reviewer emitted error event: ${lastError ?? "unknown error"}`), {
|
|
629
|
+
...(sealedProjectionError ? { partialSealedProjectionError: sealedProjectionError } : {}),
|
|
630
|
+
});
|
|
515
631
|
}
|
|
516
632
|
if (!timedOut && !isCancelled()) {
|
|
517
633
|
const completedTime = nowIso();
|
|
@@ -537,6 +653,7 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
537
653
|
costKnowledge.finishAttempt();
|
|
538
654
|
return {
|
|
539
655
|
text,
|
|
656
|
+
...(sealedProjectionError ? { sealedProjectionError } : {}),
|
|
540
657
|
observedModel: attemptObservedModel,
|
|
541
658
|
observedSource: attemptObservedSource,
|
|
542
659
|
costUsd,
|
|
@@ -670,126 +787,6 @@ async function collectReviewerOutput(reviewer, spec, timeoutMs, transientRetryPo
|
|
|
670
787
|
});
|
|
671
788
|
}
|
|
672
789
|
}
|
|
673
|
-
function selectReviewerWorkspaceBaseDir(sourceRoot, artifactsBaseDir, sourceEvidenceDir) {
|
|
674
|
-
const durableBase = join(artifactsBaseDir, "workspaces");
|
|
675
|
-
if (!isSameOrInside(sourceRoot, durableBase) && !isSameOrInside(sourceEvidenceDir, durableBase)) {
|
|
676
|
-
return durableBase;
|
|
677
|
-
}
|
|
678
|
-
return join(tmpdir(), `claudexor-review-workspaces-${newId("ws")}`);
|
|
679
|
-
}
|
|
680
|
-
function isTemporaryReviewerWorkspaceBaseDir(baseDir) {
|
|
681
|
-
const resolved = resolve(baseDir);
|
|
682
|
-
const rel = relative(tmpdir(), resolved);
|
|
683
|
-
return (isSameOrInside(tmpdir(), resolved) &&
|
|
684
|
-
rel.split(/[\\/]+/)[0]?.startsWith("claudexor-review-workspaces-") === true);
|
|
685
|
-
}
|
|
686
|
-
async function prepareReviewerWorkspace(input) {
|
|
687
|
-
const sourceRoot = resolve(input.sourceRoot);
|
|
688
|
-
const workspaceBaseDir = resolve(input.workspaceBaseDir);
|
|
689
|
-
const root = join(workspaceBaseDir, input.reviewerDirName);
|
|
690
|
-
if (!existsSync(sourceRoot)) {
|
|
691
|
-
throw new Error(`candidate root does not exist: ${sourceRoot}`);
|
|
692
|
-
}
|
|
693
|
-
if (isSameOrInside(sourceRoot, root)) {
|
|
694
|
-
throw new Error(`reviewer workspace must be outside candidate root: ${root}`);
|
|
695
|
-
}
|
|
696
|
-
try {
|
|
697
|
-
await rm(root, { recursive: true, force: true });
|
|
698
|
-
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
699
|
-
const excludeRoots = input.excludeRoots.map((p) => resolve(p));
|
|
700
|
-
const resolvedSourceRoot = realpathSync(sourceRoot);
|
|
701
|
-
await cp(sourceRoot, root, {
|
|
702
|
-
recursive: true,
|
|
703
|
-
dereference: false,
|
|
704
|
-
filter: (sourcePath) => shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, input.preservePaths),
|
|
705
|
-
});
|
|
706
|
-
const sourceEvidenceDir = resolve(input.sourceEvidenceDir);
|
|
707
|
-
const evidenceDir = join(root, ".claudexor-review-evidence");
|
|
708
|
-
if (existsSync(sourceEvidenceDir)) {
|
|
709
|
-
const resolvedSourceEvidenceDir = realpathSync(sourceEvidenceDir);
|
|
710
|
-
const evidenceExcludeRoots = excludeRoots.filter((root) => !isSameOrInside(root, sourceEvidenceDir));
|
|
711
|
-
await rm(evidenceDir, { recursive: true, force: true });
|
|
712
|
-
await cp(sourceEvidenceDir, evidenceDir, input.preserveEvidenceBytes
|
|
713
|
-
? { recursive: true, dereference: false }
|
|
714
|
-
: {
|
|
715
|
-
recursive: true,
|
|
716
|
-
dereference: false,
|
|
717
|
-
filter: (sourcePath) => shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, evidenceExcludeRoots),
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
await mkdir(evidenceDir, { recursive: true, mode: 0o700 });
|
|
721
|
-
await initializeReviewerWorkspaceGit(root);
|
|
722
|
-
return { root, evidenceDir };
|
|
723
|
-
}
|
|
724
|
-
catch (err) {
|
|
725
|
-
await rm(root, { recursive: true, force: true });
|
|
726
|
-
throw err;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
async function copyReviewEvidencePacket(sourceEvidenceDir, persistentEvidenceDir, preserveBytes = false) {
|
|
730
|
-
const source = resolve(sourceEvidenceDir);
|
|
731
|
-
const target = resolve(persistentEvidenceDir);
|
|
732
|
-
await rm(target, { recursive: true, force: true });
|
|
733
|
-
if (!existsSync(source)) {
|
|
734
|
-
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (preserveBytes) {
|
|
738
|
-
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
739
|
-
await cp(source, target, { recursive: true, dereference: false });
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
743
|
-
const resolvedSource = realpathSync(source);
|
|
744
|
-
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
745
|
-
const sourcePath = join(source, entry.name);
|
|
746
|
-
if (!shouldCopyEvidencePacketPath(source, resolvedSource, sourcePath, target)) {
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
await copyReviewEvidenceEntry(source, resolvedSource, sourcePath, join(target, entry.name), target);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
async function copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, targetPath, targetEvidenceDir) {
|
|
753
|
-
const stat = lstatSync(sourcePath);
|
|
754
|
-
if (stat.isDirectory()) {
|
|
755
|
-
await mkdir(targetPath, { recursive: true, mode: 0o700 });
|
|
756
|
-
for (const entry of await readdir(sourcePath, { withFileTypes: true })) {
|
|
757
|
-
const childSource = join(sourcePath, entry.name);
|
|
758
|
-
if (!shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceDir, childSource, targetEvidenceDir)) {
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
await copyReviewEvidenceEntry(sourceEvidenceDir, resolvedSourceEvidenceDir, childSource, join(targetPath, entry.name), targetEvidenceDir);
|
|
762
|
-
}
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
if (stat.isFile() && shouldTextSanitizeEvidenceFile(sourcePath)) {
|
|
766
|
-
const raw = readTextSafe(sourcePath);
|
|
767
|
-
if (raw === null)
|
|
768
|
-
throw new Error(`could not read review evidence file: ${sourcePath}`);
|
|
769
|
-
const text = shouldFailClosedEvidenceFile(sourcePath) ? raw : redactSecrets(raw);
|
|
770
|
-
if (containsSecretLikeToken(text)) {
|
|
771
|
-
throw new Error(`review evidence file contains a secret-like token: ${relative(sourceEvidenceDir, sourcePath)}`);
|
|
772
|
-
}
|
|
773
|
-
writeText(targetPath, text);
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
777
|
-
await cp(sourcePath, targetPath, { recursive: false, dereference: false });
|
|
778
|
-
}
|
|
779
|
-
function shouldTextSanitizeEvidenceFile(path) {
|
|
780
|
-
return TEXT_EVIDENCE_SUFFIXES.some((extension) => path.toLowerCase().endsWith(extension));
|
|
781
|
-
}
|
|
782
|
-
function shouldFailClosedEvidenceFile(path) {
|
|
783
|
-
return path.toLowerCase().endsWith(".patch");
|
|
784
|
-
}
|
|
785
|
-
function shouldCopyEvidencePacketPath(sourceEvidenceDir, resolvedSourceEvidenceDir, sourcePath, targetEvidenceDir) {
|
|
786
|
-
const resolvedSourcePath = resolve(sourcePath);
|
|
787
|
-
if (isSameOrInside(resolvedSourcePath, targetEvidenceDir))
|
|
788
|
-
return false;
|
|
789
|
-
if (isSameOrInside(targetEvidenceDir, resolvedSourcePath))
|
|
790
|
-
return false;
|
|
791
|
-
return shouldCopyReviewerPath(sourceEvidenceDir, resolvedSourceEvidenceDir, resolvedSourcePath, [targetEvidenceDir], new Set(), false);
|
|
792
|
-
}
|
|
793
790
|
async function cleanupReviewerWorkspace(workspace, artifact) {
|
|
794
791
|
try {
|
|
795
792
|
await rm(workspace.root, { recursive: true, force: true });
|
|
@@ -802,188 +799,6 @@ async function cleanupReviewerWorkspace(workspace, artifact) {
|
|
|
802
799
|
});
|
|
803
800
|
}
|
|
804
801
|
}
|
|
805
|
-
async function cleanupTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir, artifactsBaseDir) {
|
|
806
|
-
if (!isTemporaryReviewerWorkspaceBaseDir(workspaceBaseDir))
|
|
807
|
-
return;
|
|
808
|
-
try {
|
|
809
|
-
await rm(workspaceBaseDir, { recursive: true, force: true });
|
|
810
|
-
}
|
|
811
|
-
catch (err) {
|
|
812
|
-
try {
|
|
813
|
-
writeJson(join(artifactsBaseDir, "reviewer-workspace-base-cleanup-error.json"), {
|
|
814
|
-
reviewer_workspace_base_cleanup: "failed",
|
|
815
|
-
workspace_base_dir: workspaceBaseDir,
|
|
816
|
-
error: redactSecrets(err instanceof Error ? err.message : String(err)),
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
catch {
|
|
820
|
-
// Do not let cleanup telemetry hide the review result or the original error.
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
function shouldCopyReviewerPath(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots, preservePaths = new Set(), enforceContentPolicy = true) {
|
|
825
|
-
const resolvedSourcePath = resolve(sourcePath);
|
|
826
|
-
if (!isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, resolvedSourcePath, excludeRoots)) {
|
|
827
|
-
return false;
|
|
828
|
-
}
|
|
829
|
-
if (excludeRoots.some((root) => isSameOrInside(root, resolvedSourcePath)))
|
|
830
|
-
return false;
|
|
831
|
-
const rel = relative(sourceRoot, resolvedSourcePath);
|
|
832
|
-
if (!rel)
|
|
833
|
-
return true;
|
|
834
|
-
const parts = rel.split(/[\\/]+/);
|
|
835
|
-
if (sensitiveResourcePolicy.classifyPath(rel).sensitive) {
|
|
836
|
-
return false;
|
|
837
|
-
}
|
|
838
|
-
if (parts[0] === ".claudexor") {
|
|
839
|
-
return (isCopyableReviewerClaudexorPath(rel, parts, preservePaths) &&
|
|
840
|
-
(!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
|
|
841
|
-
}
|
|
842
|
-
if (parts.some((part) => [".git", ".adversarial-review", ".turbo", "node_modules"].includes(part))) {
|
|
843
|
-
return false;
|
|
844
|
-
}
|
|
845
|
-
if (parts.some((part) => [".next", ".cache", "coverage", "dist"].includes(part)) &&
|
|
846
|
-
!isPreservedReviewerPath(rel, preservePaths)) {
|
|
847
|
-
return false;
|
|
848
|
-
}
|
|
849
|
-
return (!rel.endsWith(".tsbuildinfo") &&
|
|
850
|
-
(!enforceContentPolicy || reviewerFileContentAllowed(resolvedSourcePath)));
|
|
851
|
-
}
|
|
852
|
-
function reviewerFileContentAllowed(path) {
|
|
853
|
-
let targetStat;
|
|
854
|
-
try {
|
|
855
|
-
targetStat = statSync(path);
|
|
856
|
-
}
|
|
857
|
-
catch {
|
|
858
|
-
return false;
|
|
859
|
-
}
|
|
860
|
-
if (!targetStat.isFile())
|
|
861
|
-
return true;
|
|
862
|
-
const content = readTextSafe(path);
|
|
863
|
-
return content !== null && !sensitiveResourcePolicy.containsSensitiveContent(content);
|
|
864
|
-
}
|
|
865
|
-
function isCopyableReviewerClaudexorPath(rel, parts, preservePaths) {
|
|
866
|
-
if (parts.length === 1) {
|
|
867
|
-
return true;
|
|
868
|
-
}
|
|
869
|
-
if (parts.length === 2 && parts[1] === "config.yaml") {
|
|
870
|
-
return true;
|
|
871
|
-
}
|
|
872
|
-
const runtimeRoot = parts[1]?.toLowerCase();
|
|
873
|
-
if (runtimeRoot && BLOCKED_REVIEWER_RUNTIME_ROOTS.has(runtimeRoot))
|
|
874
|
-
return false;
|
|
875
|
-
return isPreservedReviewerPath(rel, preservePaths);
|
|
876
|
-
}
|
|
877
|
-
function isPreservedReviewerPath(rel, preservePaths) {
|
|
878
|
-
const normalized = normalizeReviewerRelativePath(rel);
|
|
879
|
-
if (!normalized)
|
|
880
|
-
return false;
|
|
881
|
-
if (preservePaths.has(normalized))
|
|
882
|
-
return true;
|
|
883
|
-
const prefix = `${normalized}/`;
|
|
884
|
-
for (const preserved of preservePaths) {
|
|
885
|
-
if (preserved.startsWith(prefix))
|
|
886
|
-
return true;
|
|
887
|
-
}
|
|
888
|
-
return false;
|
|
889
|
-
}
|
|
890
|
-
export function __testExtractDiffTouchedPaths(diff) {
|
|
891
|
-
return extractDiffTouchedPaths(diff);
|
|
892
|
-
}
|
|
893
|
-
function extractDiffTouchedPaths(diff) {
|
|
894
|
-
const paths = new Set();
|
|
895
|
-
for (const file of parseUnifiedDiff(diff).files) {
|
|
896
|
-
if (file.oldPath)
|
|
897
|
-
addReviewerPreservePath(paths, file.oldPath);
|
|
898
|
-
if (file.newPath)
|
|
899
|
-
addReviewerPreservePath(paths, file.newPath);
|
|
900
|
-
}
|
|
901
|
-
return paths;
|
|
902
|
-
}
|
|
903
|
-
function addReviewerPreservePath(paths, value) {
|
|
904
|
-
const normalized = normalizeReviewerRelativePath(value);
|
|
905
|
-
if (normalized)
|
|
906
|
-
paths.add(normalized);
|
|
907
|
-
}
|
|
908
|
-
function normalizeReviewerRelativePath(value) {
|
|
909
|
-
if (!value || value === "/dev/null" || isAbsolute(value))
|
|
910
|
-
return null;
|
|
911
|
-
const normalized = normalize(value).replace(/\\/g, "/");
|
|
912
|
-
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
return normalized;
|
|
916
|
-
}
|
|
917
|
-
function isCopyableReviewerSymlink(sourceRoot, resolvedSourceRoot, sourcePath, excludeRoots) {
|
|
918
|
-
let stat;
|
|
919
|
-
try {
|
|
920
|
-
stat = lstatSync(sourcePath);
|
|
921
|
-
}
|
|
922
|
-
catch {
|
|
923
|
-
return false;
|
|
924
|
-
}
|
|
925
|
-
if (!stat.isSymbolicLink())
|
|
926
|
-
return true;
|
|
927
|
-
let linkTarget = "";
|
|
928
|
-
let resolvedTarget = "";
|
|
929
|
-
let targetKind = "other";
|
|
930
|
-
try {
|
|
931
|
-
linkTarget = readlinkSync(sourcePath);
|
|
932
|
-
resolvedTarget = realpathSync(sourcePath);
|
|
933
|
-
const targetStat = statSync(sourcePath);
|
|
934
|
-
targetKind = targetStat.isDirectory() ? "directory" : targetStat.isFile() ? "file" : "other";
|
|
935
|
-
}
|
|
936
|
-
catch {
|
|
937
|
-
return false;
|
|
938
|
-
}
|
|
939
|
-
return sensitiveResourcePolicy.assessSymlink({
|
|
940
|
-
sourceRoot,
|
|
941
|
-
canonicalSourceRoot: resolvedSourceRoot,
|
|
942
|
-
sourcePath,
|
|
943
|
-
linkTarget,
|
|
944
|
-
resolvedTargetPath: resolvedTarget,
|
|
945
|
-
targetKind,
|
|
946
|
-
allowedTargetKinds: ["file", "directory"],
|
|
947
|
-
excludedRoots: excludeRoots,
|
|
948
|
-
relocationRoot: sourceRoot,
|
|
949
|
-
}).allowed;
|
|
950
|
-
}
|
|
951
|
-
async function initializeReviewerWorkspaceGit(root) {
|
|
952
|
-
const noHooks = ["-c", "core.hooksPath=/dev/null"];
|
|
953
|
-
await runGitOrThrow("init", root, ["-c", "init.templateDir=", ...noHooks, "init"]);
|
|
954
|
-
for (const [key, value] of [
|
|
955
|
-
["user.email", "claudexor-review@example.invalid"],
|
|
956
|
-
["user.name", "Claudexor Review"],
|
|
957
|
-
]) {
|
|
958
|
-
await runGitOrThrow(`config ${key}`, root, [...noHooks, "config", key, value]);
|
|
959
|
-
}
|
|
960
|
-
await runGitOrThrow("add", root, [...noHooks, "add", "-A", "--force"]);
|
|
961
|
-
await runGitOrThrow("commit", root, [
|
|
962
|
-
...noHooks,
|
|
963
|
-
"commit",
|
|
964
|
-
"--allow-empty",
|
|
965
|
-
"--no-verify",
|
|
966
|
-
"--no-gpg-sign",
|
|
967
|
-
"-m",
|
|
968
|
-
"review baseline",
|
|
969
|
-
]);
|
|
970
|
-
}
|
|
971
|
-
async function runGitOrThrow(label, cwd, args) {
|
|
972
|
-
const gitEnv = Object.fromEntries(Object.keys(process.env)
|
|
973
|
-
.filter((key) => key.startsWith("GIT_"))
|
|
974
|
-
.map((key) => [key, null]));
|
|
975
|
-
gitEnv.GIT_CONFIG_NOSYSTEM = "1";
|
|
976
|
-
const result = await runCapture("git", args, { cwd, env: gitEnv, timeoutMs: 60_000 });
|
|
977
|
-
if (result.code === 0)
|
|
978
|
-
return;
|
|
979
|
-
const detail = redactSecrets((result.stderr || result.stdout || `exit ${result.code}`).trim());
|
|
980
|
-
throw new Error(`failed to prepare reviewer workspace (${label}): ${detail}`);
|
|
981
|
-
}
|
|
982
|
-
function isSameOrInside(parent, target) {
|
|
983
|
-
const rel = relative(resolve(parent), resolve(target));
|
|
984
|
-
const firstPart = rel.split(/[\\/]+/)[0];
|
|
985
|
-
return rel === "" || (!!rel && firstPart !== ".." && !isAbsolute(rel));
|
|
986
|
-
}
|
|
987
802
|
function createReviewerArtifactContext(baseDir, index, reviewer) {
|
|
988
803
|
const dir = join(baseDir, `${String(index + 1).padStart(2, "0")}-${safeFilePart(reviewer.adapter.id)}`);
|
|
989
804
|
ensureDir(dir);
|
|
@@ -1012,8 +827,11 @@ function createReviewerArtifactContext(baseDir, index, reviewer) {
|
|
|
1012
827
|
return ctx;
|
|
1013
828
|
}
|
|
1014
829
|
function safeFilePart(value) {
|
|
1015
|
-
const safe = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(
|
|
1016
|
-
|
|
830
|
+
const safe = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+/, "");
|
|
831
|
+
let end = safe.length;
|
|
832
|
+
while (end > 0 && safe[end - 1] === "-")
|
|
833
|
+
end -= 1;
|
|
834
|
+
return safe.slice(0, end) || "reviewer";
|
|
1017
835
|
}
|
|
1018
836
|
function emitReviewerProgress(artifact, reviewer, onReviewerEvent, patch) {
|
|
1019
837
|
const event = {
|