@effect-agent/pr-review 0.1.0-beta.12 → 0.1.0-beta.13
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/README.md +16 -4
- package/dist/action.d.mts +1 -1
- package/dist/action.mjs +2 -2
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-TrA9EUCr.d.mts → fan-out-BiJTQrup.d.mts} +38 -8
- package/dist/{github-5TCFrxfX.mjs → github-CnGU7FFJ.mjs} +152 -30
- package/dist/github-CnGU7FFJ.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/{providers-DobNWMUn.mjs → providers-C6RkwIAJ.mjs} +6 -6
- package/dist/providers-C6RkwIAJ.mjs.map +1 -0
- package/dist/testing.d.mts +2 -1
- package/dist/testing.mjs +23 -15
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/internal/coverage.ts +11 -3
- package/src/internal/diff.ts +59 -0
- package/src/internal/fan-out.ts +4 -1
- package/src/internal/fingerprint.ts +1 -1
- package/src/internal/fixtures.ts +15 -4
- package/src/internal/github.ts +73 -12
- package/src/internal/render.ts +1 -1
- package/src/internal/review-agent.ts +32 -7
- package/src/internal/review-state.ts +21 -1
- package/src/internal/review-units.ts +17 -11
- package/dist/github-5TCFrxfX.mjs.map +0 -1
- package/dist/providers-DobNWMUn.mjs.map +0 -1
package/src/internal/fixtures.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
/** One fixture file: its changeset entry plus optional head content. */
|
|
29
29
|
export class FixtureFile extends Schema.Class<FixtureFile>("@effect-agent/pr-review/FixtureFile")({
|
|
30
30
|
file: ChangedFile,
|
|
31
|
+
baseContent: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(MAX_FILE_CHARS))),
|
|
31
32
|
headContent: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(MAX_FILE_CHARS))),
|
|
32
33
|
}) {}
|
|
33
34
|
|
|
@@ -57,12 +58,21 @@ const requireChanged = (
|
|
|
57
58
|
/** Deterministic `PullRequestSource` over one fixture pull request. */
|
|
58
59
|
export const fixturePullRequestSourceLayer = (
|
|
59
60
|
fixture: FixturePullRequest,
|
|
60
|
-
): Layer.Layer<PullRequestSource> =>
|
|
61
|
-
|
|
61
|
+
): Layer.Layer<PullRequestSource> => {
|
|
62
|
+
const files = fixture.files.map((entry) =>
|
|
63
|
+
entry.file.patch !== undefined
|
|
64
|
+
? entry.file
|
|
65
|
+
: ChangedFile.make({
|
|
66
|
+
...entry.file,
|
|
67
|
+
...(entry.baseContent === undefined ? {} : { reviewBaseContent: entry.baseContent }),
|
|
68
|
+
...(entry.headContent === undefined ? {} : { reviewHeadContent: entry.headContent }),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return Layer.succeed(PullRequestSource)(
|
|
62
72
|
PullRequestSource.of({
|
|
63
73
|
metadata: Effect.succeed(fixture.metadata),
|
|
64
|
-
changedFiles: Effect.succeed(
|
|
65
|
-
anchorFiles: Effect.succeed(
|
|
74
|
+
changedFiles: Effect.succeed(files),
|
|
75
|
+
anchorFiles: Effect.succeed(files),
|
|
66
76
|
readFile: (path) =>
|
|
67
77
|
Effect.gen(function* () {
|
|
68
78
|
const relative = yield* normalizeRepoRelativePath(path);
|
|
@@ -77,6 +87,7 @@ export const fixturePullRequestSourceLayer = (
|
|
|
77
87
|
}),
|
|
78
88
|
}),
|
|
79
89
|
);
|
|
90
|
+
};
|
|
80
91
|
|
|
81
92
|
/** In-memory publisher: records every plan and mints a deterministic receipt. */
|
|
82
93
|
export const collectingReviewPublisherLayer = (
|
package/src/internal/github.ts
CHANGED
|
@@ -313,21 +313,13 @@ export const gitHubPullRequestSourceLayer: Layer.Layer<
|
|
|
313
313
|
const metadata = yield* Effect.cached(
|
|
314
314
|
fetchMetadata.pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
315
315
|
);
|
|
316
|
-
const
|
|
316
|
+
const rawFiles = yield* Effect.cached(
|
|
317
317
|
fetchFiles.pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
318
318
|
);
|
|
319
319
|
|
|
320
|
-
const
|
|
320
|
+
const readRepositoryFile = (path: string, ref: string) =>
|
|
321
321
|
Effect.gen(function* () {
|
|
322
322
|
const relative = yield* normalizeRepoRelativePath(path);
|
|
323
|
-
const files = yield* changedFiles;
|
|
324
|
-
if (!files.some((file) => file.path === relative)) {
|
|
325
|
-
return yield* ReviewInputViolation.make({
|
|
326
|
-
input: relative,
|
|
327
|
-
reason: "Path is not part of this pull request's changeset.",
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
const head = yield* metadata;
|
|
331
323
|
const encodedPath = relative.split("/").map(encodeURIComponent).join("/");
|
|
332
324
|
const response = yield* executeOk(
|
|
333
325
|
"readFile",
|
|
@@ -336,12 +328,32 @@ export const gitHubPullRequestSourceLayer: Layer.Layer<
|
|
|
336
328
|
`${target.apiUrl}/repos/${target.repository}/contents/${encodedPath}`,
|
|
337
329
|
).pipe(
|
|
338
330
|
HttpClientRequest.accept("application/vnd.github.raw+json"),
|
|
339
|
-
HttpClientRequest.setUrlParams({ ref
|
|
331
|
+
HttpClientRequest.setUrlParams({ ref }),
|
|
340
332
|
),
|
|
341
333
|
target.token,
|
|
342
334
|
),
|
|
343
335
|
).pipe(Effect.provideService(HttpClient.HttpClient, client));
|
|
344
|
-
const
|
|
336
|
+
const buffer = yield* response.arrayBuffer.pipe(Effect.mapError(failWith("readFile")));
|
|
337
|
+
if (buffer.byteLength > MAX_FILE_CHARS) {
|
|
338
|
+
return yield* ReviewInputViolation.make({
|
|
339
|
+
input: relative,
|
|
340
|
+
reason: `File is larger than the ${MAX_FILE_CHARS}-byte read bound.`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const text = yield* Effect.try({
|
|
344
|
+
try: () => new TextDecoder("utf-8", { fatal: true }).decode(buffer),
|
|
345
|
+
catch: () =>
|
|
346
|
+
ReviewInputViolation.make({
|
|
347
|
+
input: relative,
|
|
348
|
+
reason: "File is not valid UTF-8 text.",
|
|
349
|
+
}),
|
|
350
|
+
});
|
|
351
|
+
if (text.includes("\u0000")) {
|
|
352
|
+
return yield* ReviewInputViolation.make({
|
|
353
|
+
input: relative,
|
|
354
|
+
reason: "File contains binary NUL bytes.",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
345
357
|
if (text.length > MAX_FILE_CHARS) {
|
|
346
358
|
return yield* ReviewInputViolation.make({
|
|
347
359
|
input: relative,
|
|
@@ -351,6 +363,55 @@ export const gitHubPullRequestSourceLayer: Layer.Layer<
|
|
|
351
363
|
return text;
|
|
352
364
|
});
|
|
353
365
|
|
|
366
|
+
const changedFiles = yield* Effect.cached(
|
|
367
|
+
Effect.gen(function* () {
|
|
368
|
+
const [files, pullRequest] = yield* Effect.all([rawFiles, metadata]);
|
|
369
|
+
return yield* Effect.forEach(
|
|
370
|
+
files,
|
|
371
|
+
(file) => {
|
|
372
|
+
if (file.patch !== undefined) return Effect.succeed(file);
|
|
373
|
+
const basePath = file.previousPath ?? file.path;
|
|
374
|
+
const base =
|
|
375
|
+
file.status === "added"
|
|
376
|
+
? Effect.succeed(Option.none<string>())
|
|
377
|
+
: readRepositoryFile(basePath, pullRequest.baseSha ?? pullRequest.baseRef).pipe(
|
|
378
|
+
Effect.option,
|
|
379
|
+
);
|
|
380
|
+
const head =
|
|
381
|
+
file.status === "removed"
|
|
382
|
+
? Effect.succeed(Option.none<string>())
|
|
383
|
+
: readRepositoryFile(file.path, pullRequest.headSha).pipe(Effect.option);
|
|
384
|
+
return Effect.all({ base, head }).pipe(
|
|
385
|
+
Effect.map(({ base, head }) =>
|
|
386
|
+
ChangedFile.make({
|
|
387
|
+
...file,
|
|
388
|
+
...(Option.isSome(base) ? { reviewBaseContent: base.value } : {}),
|
|
389
|
+
...(Option.isSome(head) ? { reviewHeadContent: head.value } : {}),
|
|
390
|
+
}),
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
{ concurrency: 4 },
|
|
395
|
+
);
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const readFile = (path: string) =>
|
|
400
|
+
Effect.gen(function* () {
|
|
401
|
+
const relative = yield* normalizeRepoRelativePath(path);
|
|
402
|
+
const files = yield* changedFiles;
|
|
403
|
+
const file = files.find((candidate) => candidate.path === relative);
|
|
404
|
+
if (file === undefined) {
|
|
405
|
+
return yield* ReviewInputViolation.make({
|
|
406
|
+
input: relative,
|
|
407
|
+
reason: "Path is not part of this pull request's changeset.",
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
if (file.reviewHeadContent !== undefined) return file.reviewHeadContent;
|
|
411
|
+
const head = yield* metadata;
|
|
412
|
+
return yield* readRepositoryFile(relative, head.headSha);
|
|
413
|
+
});
|
|
414
|
+
|
|
354
415
|
return PullRequestSource.of({ metadata, changedFiles, anchorFiles: changedFiles, readFile });
|
|
355
416
|
}),
|
|
356
417
|
);
|
package/src/internal/render.ts
CHANGED
|
@@ -192,7 +192,7 @@ export const anchorViolation = (
|
|
|
192
192
|
): string | undefined => {
|
|
193
193
|
const file = files.find((candidate) => candidate.path === finding.path);
|
|
194
194
|
if (file === undefined) return "path is not part of the changeset";
|
|
195
|
-
if (file.patch === undefined) return "file has no textual diff";
|
|
195
|
+
if (file.patch === undefined) return "file has no anchorable textual diff";
|
|
196
196
|
if (finding.endLine < finding.startLine) return "endLine precedes startLine";
|
|
197
197
|
if (finding.endLine - finding.startLine + 1 > 100) return "range is implausibly large";
|
|
198
198
|
const anchors = commentableLines(file.patch);
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import { Agent, AgentPolicy, ToolExecutionClass } from "effect-agent";
|
|
2
|
+
import { Agent, AgentPolicy, ToolExecutionClass, ToolResultBounds } from "effect-agent";
|
|
3
3
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
annotatePatch,
|
|
7
|
+
ChangedFileStatus,
|
|
8
|
+
ChangedPath,
|
|
9
|
+
hasReviewableContent,
|
|
10
|
+
renderReviewContent,
|
|
11
|
+
} from "./diff.ts";
|
|
6
12
|
import {
|
|
7
13
|
normalizeRepoRelativePath,
|
|
8
14
|
PullRequestSource,
|
|
@@ -27,6 +33,9 @@ export const MAX_CONCERNS = 10;
|
|
|
27
33
|
/** Annotated patches larger than this are truncated with an explicit marker. */
|
|
28
34
|
const MAX_PATCH_CHARS = 60_000;
|
|
29
35
|
|
|
36
|
+
/** The encoded Tool result must retain one complete bounded content fallback. */
|
|
37
|
+
export const REVIEW_TOOL_RESULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
38
|
+
|
|
30
39
|
/** One `read_file` slice never exceeds this many lines. */
|
|
31
40
|
const MAX_SLICE_LINES = 1_000;
|
|
32
41
|
const DEFAULT_SLICE_LINES = 400;
|
|
@@ -43,6 +52,8 @@ export class ChangedFileSummary extends Schema.Class<ChangedFileSummary>(
|
|
|
43
52
|
additions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
44
53
|
deletions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
45
54
|
hasTextualDiff: Schema.Boolean,
|
|
55
|
+
/** True when a missing patch was recovered as bounded UTF-8 base/head content. */
|
|
56
|
+
hasReviewableContent: Schema.Boolean,
|
|
46
57
|
}) {}
|
|
47
58
|
|
|
48
59
|
export class ChangedFilesView extends Schema.Class<ChangedFilesView>(
|
|
@@ -82,10 +93,13 @@ export class FileDiffView extends Schema.Class<FileDiffView>(
|
|
|
82
93
|
)({
|
|
83
94
|
path: ChangedPath,
|
|
84
95
|
status: ChangedFileStatus,
|
|
96
|
+
reviewMode: Schema.Literals(["diff", "content", "unavailable"]),
|
|
85
97
|
/**
|
|
86
98
|
* The unified diff with explicit RIGHT-side line numbers: `R<n>` marks a
|
|
87
99
|
* line present in the new file version (only those may anchor findings);
|
|
88
|
-
* `-` marks removed lines.
|
|
100
|
+
* `-` marks removed lines. For content fallback, `B<n>` and `H<n>`
|
|
101
|
+
* identify base/head lines for reading only; they are never valid anchors.
|
|
102
|
+
* Empty only when neither a patch nor bounded textual content exists.
|
|
89
103
|
*/
|
|
90
104
|
annotatedPatch: Schema.String,
|
|
91
105
|
truncated: Schema.Boolean,
|
|
@@ -98,7 +112,7 @@ export class FileDiffView extends Schema.Class<FileDiffView>(
|
|
|
98
112
|
// security (the run stays bounded by AgentPolicy regardless).
|
|
99
113
|
export const ReadFileDiff = Tool.make("read_file_diff", {
|
|
100
114
|
description:
|
|
101
|
-
"Read
|
|
115
|
+
"Read one changed file's review evidence. A normal unified diff marks valid anchors as R<number>. When GitHub omitted the diff, bounded base/head content is returned with B/H line labels for review but no valid inline anchors.",
|
|
102
116
|
parameters: FileDiffQuery,
|
|
103
117
|
success: FileDiffView,
|
|
104
118
|
failure: Schema.Union([PullRequestSourceFailure, ReviewInputViolation]),
|
|
@@ -158,6 +172,7 @@ export const listChangedFilesHandler = (_query: ListChangedFilesQuery) =>
|
|
|
158
172
|
additions: file.additions,
|
|
159
173
|
deletions: file.deletions,
|
|
160
174
|
hasTextualDiff: file.patch !== undefined,
|
|
175
|
+
hasReviewableContent: hasReviewableContent(file),
|
|
161
176
|
}),
|
|
162
177
|
),
|
|
163
178
|
});
|
|
@@ -179,11 +194,20 @@ export const readFileDiffHandler = (query: FileDiffQuery) =>
|
|
|
179
194
|
reason: "Path is not part of this pull request's changeset.",
|
|
180
195
|
});
|
|
181
196
|
}
|
|
182
|
-
const
|
|
183
|
-
const
|
|
197
|
+
const contentEvidence = renderReviewContent(file);
|
|
198
|
+
const reviewMode =
|
|
199
|
+
file.patch !== undefined
|
|
200
|
+
? ("diff" as const)
|
|
201
|
+
: contentEvidence !== undefined
|
|
202
|
+
? ("content" as const)
|
|
203
|
+
: ("unavailable" as const);
|
|
204
|
+
const annotated =
|
|
205
|
+
file.patch === undefined ? (contentEvidence ?? "") : annotatePatch(file.patch);
|
|
206
|
+
const truncated = reviewMode === "diff" && annotated.length > MAX_PATCH_CHARS;
|
|
184
207
|
return FileDiffView.make({
|
|
185
208
|
path: file.path,
|
|
186
209
|
status: file.status,
|
|
210
|
+
reviewMode,
|
|
187
211
|
annotatedPatch: truncated
|
|
188
212
|
? `${annotated.slice(0, MAX_PATCH_CHARS)}\n[diff truncated]`
|
|
189
213
|
: annotated,
|
|
@@ -337,7 +361,7 @@ export const makeReviewInstructions =
|
|
|
337
361
|
...resolveGuidance(options.guidance, mission),
|
|
338
362
|
"Work in this order:",
|
|
339
363
|
"1. Call list_changed_files once to see the changeset.",
|
|
340
|
-
"2. Call read_file_diff for every file you review.
|
|
364
|
+
"2. Call read_file_diff for every file you review. A normal diff marks new-version anchors as R<number>; only those numbers are valid startLine/endLine values. When GitHub omitted a diff, the tool may return bounded base/head content marked B/H instead. Review that content, but report its defects as non-anchored concerns because B/H lines cannot anchor GitHub comments. Never anchor a finding to a removed (-), B, or H line.",
|
|
341
365
|
"3. Call read_file when you need surrounding context the diff does not show. ONLY files in the changeset are readable: a request for any other path (an import, a neighbor, a config) returns a failed result — do not retry it; reason from the diff instead and note the gap honestly in your summary when it matters.",
|
|
342
366
|
"4. Review for real defects first: correctness, security, concurrency, resource leaks, error handling, API misuse. Style nits are least important. Do not praise; do not restate the diff.",
|
|
343
367
|
"When the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. The usual tell is a loose assertion standing where an exact one belongs — >= or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed.",
|
|
@@ -363,6 +387,7 @@ export const defaultReviewPolicy = AgentPolicy.make({
|
|
|
363
387
|
// Keep enough output/summary headroom for the 200k-class provider window;
|
|
364
388
|
// tool-heavy histories prune before the engine spends a summarization call.
|
|
365
389
|
contextTokenLimit: 150_000,
|
|
390
|
+
toolResultBounds: ToolResultBounds.make({ maxBytes: REVIEW_TOOL_RESULT_MAX_BYTES }),
|
|
366
391
|
// Budget soft landing (RUN-018): an exhausted reviewer returns its partial
|
|
367
392
|
// review on one final tool-free turn instead of failing the whole run.
|
|
368
393
|
onExhaustion: "final-answer",
|
|
@@ -443,9 +443,29 @@ export const selectedPullRequestSourceLayer = (
|
|
|
443
443
|
Effect.gen(function* () {
|
|
444
444
|
const source = yield* PullRequestSource;
|
|
445
445
|
const selectedPaths = new Set(selection.files.map((file) => file.path));
|
|
446
|
+
const selectedFiles = source.changedFiles.pipe(
|
|
447
|
+
Effect.map((fullFiles) => {
|
|
448
|
+
const fullByPath = new Map(fullFiles.map((file) => [file.path, file] as const));
|
|
449
|
+
return selection.files.map((file) => {
|
|
450
|
+
if (file.patch !== undefined) return file;
|
|
451
|
+
const full = fullByPath.get(file.path);
|
|
452
|
+
return full === undefined
|
|
453
|
+
? file
|
|
454
|
+
: ChangedFile.make({
|
|
455
|
+
...file,
|
|
456
|
+
...(full.reviewBaseContent === undefined
|
|
457
|
+
? {}
|
|
458
|
+
: { reviewBaseContent: full.reviewBaseContent }),
|
|
459
|
+
...(full.reviewHeadContent === undefined
|
|
460
|
+
? {}
|
|
461
|
+
: { reviewHeadContent: full.reviewHeadContent }),
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
446
466
|
return PullRequestSource.of({
|
|
447
467
|
metadata: source.metadata,
|
|
448
|
-
changedFiles:
|
|
468
|
+
changedFiles: selectedFiles,
|
|
449
469
|
anchorFiles: source.anchorFiles,
|
|
450
470
|
readFile: (path) =>
|
|
451
471
|
selectedPaths.has(path)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
3
|
import type { ChangedFile } from "./diff.ts";
|
|
4
|
-
import { ChangedPath } from "./diff.ts";
|
|
4
|
+
import { ChangedPath, isReviewableFile } from "./diff.ts";
|
|
5
5
|
import type { FindingSeverity } from "./review-agent.ts";
|
|
6
6
|
import { ReviewFinding } from "./review-agent.ts";
|
|
7
7
|
|
|
@@ -48,7 +48,7 @@ export class ReviewUnitPlan extends Schema.Class<ReviewUnitPlan>(
|
|
|
48
48
|
/** True when the source returned fewer files than the pull request has. */
|
|
49
49
|
truncated: Schema.Boolean,
|
|
50
50
|
units: Schema.Array(ReviewUnit).check(Schema.isMaxLength(MAX_REVIEW_UNITS)),
|
|
51
|
-
/** Changed files
|
|
51
|
+
/** Changed files with neither a textual diff nor bounded base/head text. */
|
|
52
52
|
undiffablePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
|
|
53
53
|
/**
|
|
54
54
|
* Diffable files beyond the fan-out capacity (MAX_REVIEW_UNITS units of
|
|
@@ -58,8 +58,12 @@ export class ReviewUnitPlan extends Schema.Class<ReviewUnitPlan>(
|
|
|
58
58
|
unassignedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
|
|
59
59
|
}) {}
|
|
60
60
|
|
|
61
|
-
const fileCost = (file: ChangedFile): number =>
|
|
62
|
-
|
|
61
|
+
const fileCost = (file: ChangedFile): number => {
|
|
62
|
+
const contentChars =
|
|
63
|
+
(file.reviewBaseContent?.length ?? 0) + (file.reviewHeadContent?.length ?? 0);
|
|
64
|
+
const contentWeight = Math.ceil(contentChars / 200);
|
|
65
|
+
return file.additions + file.deletions + contentWeight + FILE_OVERHEAD_LINES;
|
|
66
|
+
};
|
|
63
67
|
|
|
64
68
|
const unitOf = (index: number, files: ReadonlyArray<ChangedFile>): ReviewUnit =>
|
|
65
69
|
ReviewUnit.make({
|
|
@@ -76,10 +80,12 @@ const unitOf = (index: number, files: ReadonlyArray<ChangedFile>): ReviewUnit =>
|
|
|
76
80
|
* then packed greedily in that order under the soft changed-line budget and
|
|
77
81
|
* the hard per-unit file bound. Capacity is finite and explicit:
|
|
78
82
|
*
|
|
79
|
-
* - files without a textual diff are
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* -
|
|
83
|
+
* - files without a textual diff are still delegated when the source
|
|
84
|
+
* recovered complete bounded UTF-8 base/head content. Findings from that
|
|
85
|
+
* evidence cannot anchor inline and are reported as concerns;
|
|
86
|
+
* - files with neither form of textual evidence surface in
|
|
87
|
+
* `undiffablePaths` instead of laundering missing coverage;
|
|
88
|
+
* - reviewable files beyond `MAX_REVIEW_UNITS` full units surface in
|
|
83
89
|
* `unassignedPaths` so the review can report them as unreviewed, never
|
|
84
90
|
* silently truncated.
|
|
85
91
|
*/
|
|
@@ -88,14 +94,14 @@ export const planReviewUnits = (
|
|
|
88
94
|
options: { readonly totalChangedFiles: number },
|
|
89
95
|
): ReviewUnitPlan => {
|
|
90
96
|
const ordered = [...files].sort((left, right) => (left.path < right.path ? -1 : 1));
|
|
91
|
-
const
|
|
92
|
-
const undiffable = ordered.filter((file) => file
|
|
97
|
+
const reviewable = ordered.filter(isReviewableFile);
|
|
98
|
+
const undiffable = ordered.filter((file) => !isReviewableFile(file));
|
|
93
99
|
|
|
94
100
|
const groups: Array<Array<ChangedFile>> = [];
|
|
95
101
|
const unassigned: Array<ChangedFile> = [];
|
|
96
102
|
let current: Array<ChangedFile> = [];
|
|
97
103
|
let currentCost = 0;
|
|
98
|
-
for (const file of
|
|
104
|
+
for (const file of reviewable) {
|
|
99
105
|
const cost = fileCost(file);
|
|
100
106
|
const wouldOverflow =
|
|
101
107
|
current.length >= MAX_UNIT_FILES ||
|