@effect-agent/pr-review 0.1.0-beta.20 → 0.1.0-beta.22
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 +82 -26
- package/dist/action.d.mts +12 -6
- package/dist/action.mjs +17 -27
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-CQFA-o0v.d.mts → fan-out-n-00ppWr.d.mts} +308 -223
- package/dist/{github-DnenG3be.mjs → github-BgtP7Rdv.mjs} +652 -196
- package/dist/github-BgtP7Rdv.mjs.map +1 -0
- package/dist/index.d.mts +27 -54
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BE83_Tfo.mjs → providers-BguZK4B_.mjs} +408 -134
- package/dist/providers-BguZK4B_.mjs.map +1 -0
- package/dist/testing.d.mts +10 -47
- package/dist/testing.mjs +29 -55
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +40 -39
- package/src/index.ts +1 -0
- package/src/internal/anchors.ts +20 -0
- package/src/internal/coverage.ts +628 -134
- package/src/internal/factory.ts +12 -9
- package/src/internal/fan-out-scripted.ts +61 -94
- package/src/internal/fan-out.ts +560 -255
- package/src/internal/profiles.ts +12 -0
- package/src/internal/progress.ts +1 -1
- package/src/internal/render.ts +51 -25
- package/src/internal/review-agent.ts +74 -25
- package/src/internal/review-state.ts +7 -5
- package/src/internal/review-units.ts +326 -32
- package/src/internal/run.ts +48 -41
- package/dist/github-DnenG3be.mjs.map +0 -1
- package/dist/providers-BE83_Tfo.mjs.map +0 -1
|
@@ -2,8 +2,12 @@ import { Schema } from "effect";
|
|
|
2
2
|
|
|
3
3
|
import type { ChangedFile } from "./diff.ts";
|
|
4
4
|
import { ChangedPath, isReviewableFile } from "./diff.ts";
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
fileReviewEvidenceChunks,
|
|
7
|
+
type FindingSeverity,
|
|
8
|
+
MAX_PATCH_CHARS,
|
|
9
|
+
type ReviewFinding,
|
|
10
|
+
} from "./review-agent.ts";
|
|
7
11
|
|
|
8
12
|
// ---------------------------------------------------------------------------
|
|
9
13
|
// Pure, deterministic planning for the fan-out reviewer: group the changeset
|
|
@@ -19,25 +23,101 @@ export const MAX_REVIEW_UNITS = 8;
|
|
|
19
23
|
/** A unit never carries more files than this, regardless of their size. */
|
|
20
24
|
export const MAX_UNIT_FILES = 12;
|
|
21
25
|
|
|
22
|
-
/**
|
|
26
|
+
/** Compatibility export; complete evidence chars now own unit packing. */
|
|
23
27
|
export const UNIT_CHANGED_LINE_BUDGET = 800;
|
|
24
28
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Bound the complete model-visible evidence assigned to one child. This is a
|
|
31
|
+
* character bound rather than a token estimate because it is deterministic,
|
|
32
|
+
* provider-independent, and enforced before any model call.
|
|
33
|
+
*/
|
|
34
|
+
export const UNIT_EVIDENCE_CHAR_BUDGET = 240_000;
|
|
35
|
+
|
|
36
|
+
/** Maximum complete evidence shards placed in one child brief. */
|
|
37
|
+
export const MAX_UNIT_EVIDENCE_SHARDS = 12;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Keep overflow diagnostics bounded to one plan's total assignment capacity.
|
|
41
|
+
* The plan separately records the exact overflow count and every affected
|
|
42
|
+
* path, so identifiers are a deterministic diagnostic sample rather than the
|
|
43
|
+
* authority for whether input coverage is complete.
|
|
44
|
+
*/
|
|
45
|
+
export const MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS = MAX_REVIEW_UNITS * MAX_UNIT_EVIDENCE_SHARDS;
|
|
46
|
+
|
|
47
|
+
/** @deprecated Use `MAX_PATCH_CHARS`; this is now the per-shard bound. */
|
|
48
|
+
export const MAX_FILE_EVIDENCE_CHARS = MAX_PATCH_CHARS;
|
|
27
49
|
|
|
28
50
|
/** The merged review never exceeds the `CodeReview` findings bound. */
|
|
29
51
|
export const MAX_MERGED_FINDINGS = 20;
|
|
30
52
|
|
|
31
53
|
export const ReviewUnitId = Schema.NonEmptyString.check(Schema.isMaxLength(32));
|
|
32
54
|
|
|
55
|
+
/** High-risk surfaces that receive an explicit specialist focus label. */
|
|
56
|
+
export const ReviewRiskCategory = Schema.Literals([
|
|
57
|
+
"authentication-authorization",
|
|
58
|
+
"security-boundary",
|
|
59
|
+
"persistence-durability",
|
|
60
|
+
"concurrency",
|
|
61
|
+
"credential-handling",
|
|
62
|
+
"external-side-effects",
|
|
63
|
+
]);
|
|
64
|
+
export type ReviewRiskCategory = typeof ReviewRiskCategory.Type;
|
|
65
|
+
|
|
66
|
+
export const ReviewDiscoveryPerspective = Schema.Literals(["general", "risk-specialist"]);
|
|
67
|
+
export type ReviewDiscoveryPerspective = typeof ReviewDiscoveryPerspective.Type;
|
|
68
|
+
|
|
69
|
+
export const ReviewPassId = Schema.NonEmptyString.check(Schema.isMaxLength(64));
|
|
70
|
+
export const ReviewEvidenceShardId = Schema.NonEmptyString.check(Schema.isMaxLength(32));
|
|
71
|
+
|
|
72
|
+
/** One complete bounded slice of a changed path's model-visible evidence. */
|
|
73
|
+
export class ReviewEvidenceShard extends Schema.Class<ReviewEvidenceShard>(
|
|
74
|
+
"@effect-agent/pr-review/ReviewEvidenceShard",
|
|
75
|
+
)({
|
|
76
|
+
shardId: ReviewEvidenceShardId,
|
|
77
|
+
path: ChangedPath,
|
|
78
|
+
ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
79
|
+
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
80
|
+
evidenceChars: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).check(
|
|
81
|
+
Schema.isLessThanOrEqualTo(MAX_PATCH_CHARS),
|
|
82
|
+
),
|
|
83
|
+
}) {}
|
|
84
|
+
|
|
85
|
+
const EvidenceShardIds = Schema.Array(ReviewEvidenceShardId)
|
|
86
|
+
.check(Schema.isMinLength(1))
|
|
87
|
+
.check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS));
|
|
88
|
+
|
|
89
|
+
/** One required, independently scoped discovery attempt. */
|
|
90
|
+
export class ReviewDiscoveryPass extends Schema.Class<ReviewDiscoveryPass>(
|
|
91
|
+
"@effect-agent/pr-review/ReviewDiscoveryPass",
|
|
92
|
+
)({
|
|
93
|
+
passId: ReviewPassId,
|
|
94
|
+
unitId: ReviewUnitId,
|
|
95
|
+
paths: Schema.Array(ChangedPath)
|
|
96
|
+
.check(Schema.isMinLength(1))
|
|
97
|
+
.check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
98
|
+
evidenceShardIds: EvidenceShardIds,
|
|
99
|
+
perspective: ReviewDiscoveryPerspective,
|
|
100
|
+
/** Empty for the general pass; explicit deterministic focus for specialists. */
|
|
101
|
+
riskCategories: Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6)),
|
|
102
|
+
}) {}
|
|
103
|
+
|
|
33
104
|
/** One bounded slice of the changeset delegated to one child reviewer. */
|
|
34
105
|
export class ReviewUnit extends Schema.Class<ReviewUnit>("@effect-agent/pr-review/ReviewUnit")({
|
|
35
106
|
unitId: ReviewUnitId,
|
|
36
107
|
paths: Schema.Array(ChangedPath)
|
|
37
108
|
.check(Schema.isMinLength(1))
|
|
38
109
|
.check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
110
|
+
evidenceShards: Schema.Array(ReviewEvidenceShard)
|
|
111
|
+
.check(Schema.isMinLength(1))
|
|
112
|
+
.check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS)),
|
|
39
113
|
/** additions + deletions across the unit's files, for honest sizing. */
|
|
40
114
|
changedLines: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
115
|
+
/** Complete model-visible diff/content evidence assigned to each child. */
|
|
116
|
+
evidenceChars: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).check(
|
|
117
|
+
Schema.isLessThanOrEqualTo(UNIT_EVIDENCE_CHAR_BUDGET),
|
|
118
|
+
),
|
|
119
|
+
/** Host-classified focus labels for the unit's redundant specialist pass. */
|
|
120
|
+
riskCategories: Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6)),
|
|
41
121
|
}) {}
|
|
42
122
|
|
|
43
123
|
/** The complete deterministic fan-out plan over one changeset. */
|
|
@@ -48,8 +128,20 @@ export class ReviewUnitPlan extends Schema.Class<ReviewUnitPlan>(
|
|
|
48
128
|
/** True when the source returned fewer files than the pull request has. */
|
|
49
129
|
truncated: Schema.Boolean,
|
|
50
130
|
units: Schema.Array(ReviewUnit).check(Schema.isMaxLength(MAX_REVIEW_UNITS)),
|
|
131
|
+
/** Exact discovery calls the coordinator must make. */
|
|
132
|
+
discoveryPasses: Schema.Array(ReviewDiscoveryPass).check(
|
|
133
|
+
Schema.isMaxLength(MAX_REVIEW_UNITS * 2),
|
|
134
|
+
),
|
|
51
135
|
/** Changed files with neither a textual diff nor bounded base/head text. */
|
|
52
136
|
undiffablePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
|
|
137
|
+
/** Assigned paths with one or more evidence shards beyond plan capacity. */
|
|
138
|
+
partialEvidencePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
|
|
139
|
+
/** Exact number of shards beyond the bounded unit capacity. */
|
|
140
|
+
unassignedEvidenceShardCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
141
|
+
/** Bounded deterministic prefix of the unassigned shard identifiers. */
|
|
142
|
+
unassignedEvidenceShardIds: Schema.Array(ReviewEvidenceShardId).check(
|
|
143
|
+
Schema.isMaxLength(MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS),
|
|
144
|
+
),
|
|
53
145
|
/**
|
|
54
146
|
* Diffable files beyond the fan-out capacity (MAX_REVIEW_UNITS units of
|
|
55
147
|
* MAX_UNIT_FILES files). Never silently dropped: the coordinator must name
|
|
@@ -58,36 +150,217 @@ export class ReviewUnitPlan extends Schema.Class<ReviewUnitPlan>(
|
|
|
58
150
|
unassignedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
|
|
59
151
|
}) {}
|
|
60
152
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
153
|
+
const riskRules: ReadonlyArray<{
|
|
154
|
+
readonly category: ReviewRiskCategory;
|
|
155
|
+
readonly patterns: ReadonlyArray<RegExp>;
|
|
156
|
+
}> = [
|
|
157
|
+
{
|
|
158
|
+
category: "authentication-authorization",
|
|
159
|
+
patterns: [/auth/, /authoriz/, /permission/, /principal/, /access[-_ ]?control/, /role\b/],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
category: "security-boundary",
|
|
163
|
+
patterns: [
|
|
164
|
+
/security/,
|
|
165
|
+
/sandbox/,
|
|
166
|
+
/untrusted/,
|
|
167
|
+
/schema\.decode/,
|
|
168
|
+
/validation/,
|
|
169
|
+
/injection/,
|
|
170
|
+
/csrf/,
|
|
171
|
+
/xss/,
|
|
172
|
+
/path traversal/,
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
category: "persistence-durability",
|
|
177
|
+
patterns: [
|
|
178
|
+
/durab/,
|
|
179
|
+
/persist/,
|
|
180
|
+
/storage/,
|
|
181
|
+
/database/,
|
|
182
|
+
/\bsql\b/,
|
|
183
|
+
/journal/,
|
|
184
|
+
/ledger/,
|
|
185
|
+
/checkpoint/,
|
|
186
|
+
/migration/,
|
|
187
|
+
/transaction/,
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
category: "concurrency",
|
|
192
|
+
patterns: [
|
|
193
|
+
/concurr/,
|
|
194
|
+
/semaphore/,
|
|
195
|
+
/\bfiber/,
|
|
196
|
+
/race/,
|
|
197
|
+
/mutex/,
|
|
198
|
+
/\block\b/,
|
|
199
|
+
/queue/,
|
|
200
|
+
/parallel/,
|
|
201
|
+
/interrupt/,
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
category: "credential-handling",
|
|
206
|
+
patterns: [/credential/, /secret/, /password/, /api[-_ ]?key/, /bearer/, /hmac/, /signature/],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
category: "external-side-effects",
|
|
210
|
+
patterns: [
|
|
211
|
+
/publish/,
|
|
212
|
+
/webhook/,
|
|
213
|
+
/github/,
|
|
214
|
+
/fetch\(/,
|
|
215
|
+
/http/,
|
|
216
|
+
/send[-_ ]?(email|message)/,
|
|
217
|
+
/write[-_ ]?(file|record)/,
|
|
218
|
+
/delete/,
|
|
219
|
+
/mutation/,
|
|
220
|
+
/side[-_ ]?effect/,
|
|
221
|
+
/spawn/,
|
|
222
|
+
/exec/,
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Deterministic host policy for specialist assignment. It intentionally
|
|
229
|
+
* favors false positives: an extra bounded pass costs work, while a missed
|
|
230
|
+
* high-risk classification removes redundancy. This is not a claim that the
|
|
231
|
+
* keyword policy recognizes every semantically risky change.
|
|
232
|
+
*/
|
|
233
|
+
export const classifyReviewRisks = (file: ChangedFile): ReadonlyArray<ReviewRiskCategory> => {
|
|
234
|
+
const text = [
|
|
235
|
+
file.path,
|
|
236
|
+
file.previousPath ?? "",
|
|
237
|
+
file.patch ?? "",
|
|
238
|
+
file.reviewBaseContent ?? "",
|
|
239
|
+
file.reviewHeadContent ?? "",
|
|
240
|
+
]
|
|
241
|
+
.join("\n")
|
|
242
|
+
.toLowerCase();
|
|
243
|
+
return riskRules
|
|
244
|
+
.filter((rule) => rule.patterns.some((pattern) => pattern.test(text)))
|
|
245
|
+
.map((rule) => rule.category);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Whether every claimed finding anchor was present in the exact bounded
|
|
250
|
+
* evidence shards assigned to one unit. This is stricter than checking the
|
|
251
|
+
* full pull-request diff when an oversized path spans multiple units.
|
|
252
|
+
*/
|
|
253
|
+
export const findingAnchorInUnitEvidence = (
|
|
254
|
+
finding: ReviewFinding,
|
|
255
|
+
unit: ReviewUnit,
|
|
256
|
+
files: ReadonlyArray<ChangedFile>,
|
|
257
|
+
): boolean => {
|
|
258
|
+
const file = files.find((candidate) => candidate.path === finding.path);
|
|
259
|
+
if (file?.patch === undefined || finding.endLine < finding.startLine) return false;
|
|
260
|
+
const assignedOrdinals = new Set(
|
|
261
|
+
unit.evidenceShards
|
|
262
|
+
.filter((shard) => shard.path === finding.path)
|
|
263
|
+
.map((shard) => shard.ordinal),
|
|
264
|
+
);
|
|
265
|
+
const visibleLines = new Set<number>();
|
|
266
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
267
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
268
|
+
if (!assignedOrdinals.has(index + 1)) continue;
|
|
269
|
+
for (const line of chunks[index]?.annotatedPatch.split("\n") ?? []) {
|
|
270
|
+
const match = /^R(\d+) /.exec(line);
|
|
271
|
+
if (match?.[1] !== undefined) visibleLines.add(Number(match[1]));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (let line = finding.startLine; line <= finding.endLine; line += 1) {
|
|
275
|
+
if (!visibleLines.has(line)) return false;
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
interface PlannedEvidenceShard {
|
|
281
|
+
readonly shard: ReviewEvidenceShard;
|
|
282
|
+
readonly file: ChangedFile;
|
|
283
|
+
readonly changedLines: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const uniquePaths = (shards: ReadonlyArray<PlannedEvidenceShard>): ReadonlyArray<string> => [
|
|
287
|
+
...new Set(shards.map(({ shard }) => shard.path)),
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const plannedEvidenceShards = (
|
|
291
|
+
files: ReadonlyArray<ChangedFile>,
|
|
292
|
+
): ReadonlyArray<PlannedEvidenceShard> => {
|
|
293
|
+
const planned: Array<PlannedEvidenceShard> = [];
|
|
294
|
+
let shardIndex = 0;
|
|
295
|
+
for (const file of files) {
|
|
296
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
297
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
298
|
+
const chunk = chunks[index];
|
|
299
|
+
if (chunk === undefined) continue;
|
|
300
|
+
shardIndex += 1;
|
|
301
|
+
planned.push({
|
|
302
|
+
shard: ReviewEvidenceShard.make({
|
|
303
|
+
shardId: `shard-${String(shardIndex).padStart(4, "0")}`,
|
|
304
|
+
path: file.path,
|
|
305
|
+
ordinal: index + 1,
|
|
306
|
+
total: chunks.length,
|
|
307
|
+
evidenceChars: chunk.annotatedPatch.length,
|
|
308
|
+
}),
|
|
309
|
+
file,
|
|
310
|
+
changedLines: index === 0 ? file.additions + file.deletions : 0,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return planned;
|
|
66
315
|
};
|
|
67
316
|
|
|
68
|
-
const unitOf = (index: number,
|
|
317
|
+
const unitOf = (index: number, shards: ReadonlyArray<PlannedEvidenceShard>): ReviewUnit =>
|
|
69
318
|
ReviewUnit.make({
|
|
70
319
|
unitId: `unit-${String(index + 1).padStart(3, "0")}`,
|
|
71
|
-
paths:
|
|
72
|
-
|
|
320
|
+
paths: uniquePaths(shards),
|
|
321
|
+
evidenceShards: shards.map(({ shard }) => shard),
|
|
322
|
+
changedLines: shards.reduce((total, shard) => total + shard.changedLines, 0),
|
|
323
|
+
evidenceChars: shards.reduce((total, { shard }) => total + shard.evidenceChars, 0),
|
|
324
|
+
riskCategories: [...new Set(shards.flatMap(({ file }) => classifyReviewRisks(file)))],
|
|
73
325
|
});
|
|
74
326
|
|
|
327
|
+
const discoveryPassesFor = (units: ReadonlyArray<ReviewUnit>): ReadonlyArray<ReviewDiscoveryPass> =>
|
|
328
|
+
units.flatMap((unit) => [
|
|
329
|
+
ReviewDiscoveryPass.make({
|
|
330
|
+
passId: `${unit.unitId}-general`,
|
|
331
|
+
unitId: unit.unitId,
|
|
332
|
+
paths: unit.paths,
|
|
333
|
+
evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
|
|
334
|
+
perspective: "general",
|
|
335
|
+
riskCategories: [],
|
|
336
|
+
}),
|
|
337
|
+
ReviewDiscoveryPass.make({
|
|
338
|
+
passId: `${unit.unitId}-specialist`,
|
|
339
|
+
unitId: unit.unitId,
|
|
340
|
+
paths: unit.paths,
|
|
341
|
+
evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
|
|
342
|
+
perspective: "risk-specialist",
|
|
343
|
+
riskCategories: unit.riskCategories,
|
|
344
|
+
}),
|
|
345
|
+
]);
|
|
346
|
+
|
|
75
347
|
/**
|
|
76
348
|
* Group the changeset into at most `MAX_REVIEW_UNITS` review units.
|
|
77
349
|
*
|
|
78
350
|
* Deterministic by construction: files are ordered by path (so files sharing
|
|
79
351
|
* a directory become neighbors — directory affinity without a heuristic),
|
|
80
|
-
* then
|
|
81
|
-
* the hard per-unit
|
|
352
|
+
* then split into complete line-bounded evidence shards and packed greedily
|
|
353
|
+
* under the hard evidence and per-unit shard bounds. Capacity is finite and
|
|
354
|
+
* explicit:
|
|
82
355
|
*
|
|
83
356
|
* - files without a textual diff are still delegated when the source
|
|
84
357
|
* recovered complete bounded UTF-8 base/head content. Findings from that
|
|
85
358
|
* evidence cannot anchor inline and are reported as concerns;
|
|
86
359
|
* - files with neither form of textual evidence surface in
|
|
87
360
|
* `undiffablePaths` instead of laundering missing coverage;
|
|
88
|
-
* -
|
|
89
|
-
*
|
|
90
|
-
*
|
|
361
|
+
* - an oversized path spans as many deterministic shards and units as needed;
|
|
362
|
+
* - shards beyond `MAX_REVIEW_UNITS` full units surface explicitly, so a path
|
|
363
|
+
* is partial only when finite plan capacity is genuinely exhausted.
|
|
91
364
|
*/
|
|
92
365
|
export const planReviewUnits = (
|
|
93
366
|
files: ReadonlyArray<ChangedFile>,
|
|
@@ -97,37 +370,58 @@ export const planReviewUnits = (
|
|
|
97
370
|
const reviewable = ordered.filter(isReviewableFile);
|
|
98
371
|
const undiffable = ordered.filter((file) => !isReviewableFile(file));
|
|
99
372
|
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
373
|
+
const shards = plannedEvidenceShards(reviewable);
|
|
374
|
+
const groups: Array<Array<PlannedEvidenceShard>> = [];
|
|
375
|
+
const unassigned: Array<PlannedEvidenceShard> = [];
|
|
376
|
+
let current: Array<PlannedEvidenceShard> = [];
|
|
377
|
+
let currentEvidenceChars = 0;
|
|
378
|
+
for (const shard of shards) {
|
|
379
|
+
const nextPaths = new Set([...uniquePaths(current), shard.shard.path]);
|
|
106
380
|
const wouldOverflow =
|
|
107
|
-
current.length >=
|
|
108
|
-
|
|
381
|
+
current.length >= MAX_UNIT_EVIDENCE_SHARDS ||
|
|
382
|
+
nextPaths.size > MAX_UNIT_FILES ||
|
|
383
|
+
(current.length > 0 &&
|
|
384
|
+
currentEvidenceChars + shard.shard.evidenceChars > UNIT_EVIDENCE_CHAR_BUDGET);
|
|
109
385
|
if (wouldOverflow) {
|
|
110
386
|
groups.push(current);
|
|
111
387
|
current = [];
|
|
112
|
-
|
|
388
|
+
currentEvidenceChars = 0;
|
|
113
389
|
}
|
|
114
390
|
if (groups.length >= MAX_REVIEW_UNITS) {
|
|
115
|
-
unassigned.push(
|
|
391
|
+
unassigned.push(shard);
|
|
116
392
|
continue;
|
|
117
393
|
}
|
|
118
|
-
current.push(
|
|
119
|
-
|
|
394
|
+
current.push(shard);
|
|
395
|
+
currentEvidenceChars += shard.shard.evidenceChars;
|
|
120
396
|
}
|
|
121
397
|
if (current.length > 0 && groups.length < MAX_REVIEW_UNITS) {
|
|
122
398
|
groups.push(current);
|
|
123
399
|
}
|
|
124
400
|
|
|
401
|
+
const units = groups.map((group, index) => unitOf(index, group));
|
|
402
|
+
const assignedShardIds = new Set(
|
|
403
|
+
units.flatMap((unit) => unit.evidenceShards.map((shard) => shard.shardId)),
|
|
404
|
+
);
|
|
405
|
+
const assignedPaths = new Set(
|
|
406
|
+
shards
|
|
407
|
+
.filter(({ shard }) => assignedShardIds.has(shard.shardId))
|
|
408
|
+
.map(({ shard }) => shard.path),
|
|
409
|
+
);
|
|
410
|
+
const unassignedPathsWithEvidence = new Set(unassigned.map(({ shard }) => shard.path));
|
|
125
411
|
return ReviewUnitPlan.make({
|
|
126
412
|
totalFiles: files.length,
|
|
127
413
|
truncated: files.length < options.totalChangedFiles,
|
|
128
|
-
units
|
|
414
|
+
units,
|
|
415
|
+
discoveryPasses: discoveryPassesFor(units),
|
|
129
416
|
undiffablePaths: undiffable.map((file) => file.path),
|
|
130
|
-
|
|
417
|
+
partialEvidencePaths: [...unassignedPathsWithEvidence].filter((path) =>
|
|
418
|
+
assignedPaths.has(path),
|
|
419
|
+
),
|
|
420
|
+
unassignedEvidenceShardCount: unassigned.length,
|
|
421
|
+
unassignedEvidenceShardIds: unassigned
|
|
422
|
+
.slice(0, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS)
|
|
423
|
+
.map(({ shard }) => shard.shardId),
|
|
424
|
+
unassignedPaths: [...unassignedPathsWithEvidence].filter((path) => !assignedPaths.has(path)),
|
|
131
425
|
});
|
|
132
426
|
};
|
|
133
427
|
|
package/src/internal/run.ts
CHANGED
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
import { type Tool } from "effect/unstable/ai";
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
assessReviewPipeline,
|
|
14
|
+
ReviewAssurance,
|
|
15
15
|
ReviewCoverage,
|
|
16
|
+
ReviewInputCoverage,
|
|
16
17
|
type ReviewShape,
|
|
17
18
|
} from "./coverage.ts";
|
|
18
19
|
import type { ChangedFile } from "./diff.ts";
|
|
@@ -66,11 +67,11 @@ export const reviewBudgetLimits = UsageBudgetLimits.make({
|
|
|
66
67
|
* while bounded children run.
|
|
67
68
|
*/
|
|
68
69
|
export const fanOutReviewBudgetLimits = UsageBudgetLimits.make({
|
|
69
|
-
maxInputTokens:
|
|
70
|
-
maxOutputTokens:
|
|
71
|
-
maxToolCalls:
|
|
70
|
+
maxInputTokens: 600_000,
|
|
71
|
+
maxOutputTokens: 32_000,
|
|
72
|
+
maxToolCalls: 32,
|
|
72
73
|
maxCostMicrousd: 2_000_000,
|
|
73
|
-
maxDurationMillis:
|
|
74
|
+
maxDurationMillis: 1_200_000,
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
/** Everything one review run produced, publication receipt included. */
|
|
@@ -84,6 +85,10 @@ export class ReviewRunOutcome extends Schema.Class<ReviewRunOutcome>(
|
|
|
84
85
|
activeConcerns: Schema.Array(ReviewConcern).check(Schema.isMaxLength(10)),
|
|
85
86
|
/** Host-owned structural coverage used by the Actions check conclusion. */
|
|
86
87
|
coverage: ReviewCoverage,
|
|
88
|
+
/** Exact path/evidence assignment, distinct from semantic review work. */
|
|
89
|
+
inputCoverage: ReviewInputCoverage,
|
|
90
|
+
/** Settlement of configured discovery, specialist, and verification work. */
|
|
91
|
+
assurance: ReviewAssurance,
|
|
87
92
|
plan: ReviewPublicationPlan,
|
|
88
93
|
published: Schema.optionalKey(PublishedReview),
|
|
89
94
|
turns: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
@@ -245,31 +250,30 @@ export const executeReview = <
|
|
|
245
250
|
// The engine validated the terminal JSON against the output schema; this
|
|
246
251
|
// decode recovers the typed value on this side of the generic boundary.
|
|
247
252
|
const decoded = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
+
const reviewTotalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
|
|
254
|
+
const pipeline = assessReviewPipeline({
|
|
255
|
+
shape: options.reviewShape ?? "flat",
|
|
256
|
+
files,
|
|
257
|
+
totalFiles: reviewTotalFiles,
|
|
258
|
+
anchorFiles,
|
|
259
|
+
totalAnchorFiles: metadata.totalChangedFiles,
|
|
260
|
+
events,
|
|
261
|
+
});
|
|
262
|
+
// The coordinator owns prose only. Fan-out findings and concerns are
|
|
263
|
+
// reconstructed from exact verifier-confirmed discovery candidates; an
|
|
264
|
+
// unsupported or coordinator-invented candidate cannot reach publication.
|
|
253
265
|
const verifiedReview =
|
|
254
|
-
options.reviewShape !== "fan-out"
|
|
266
|
+
options.reviewShape !== "fan-out"
|
|
255
267
|
? decoded
|
|
256
|
-
: (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return CodeReview.make({
|
|
266
|
-
summary: decoded.summary,
|
|
267
|
-
verdict: decoded.verdict,
|
|
268
|
-
findings: decoded.findings,
|
|
269
|
-
...(decoded.concerns !== undefined ? { concerns: decoded.concerns } : {}),
|
|
270
|
-
...(walkthrough.length > 0 ? { walkthrough } : {}),
|
|
271
|
-
});
|
|
272
|
-
})();
|
|
268
|
+
: CodeReview.make({
|
|
269
|
+
summary: decoded.summary,
|
|
270
|
+
verdict: decoded.verdict,
|
|
271
|
+
findings: rankAndDedupeFindings(pipeline.confirmedFindings),
|
|
272
|
+
...(pipeline.confirmedConcerns.length === 0
|
|
273
|
+
? {}
|
|
274
|
+
: { concerns: rankAndDedupeConcerns(pipeline.confirmedConcerns) }),
|
|
275
|
+
...(pipeline.walkthrough.length === 0 ? {} : { walkthrough: pipeline.walkthrough }),
|
|
276
|
+
});
|
|
273
277
|
const review = enforceFindingsBound(verifiedReview, clampMaxFindings(options.maxFindings));
|
|
274
278
|
const usage = yield* budget.snapshot;
|
|
275
279
|
const affectedPaths = new Set(
|
|
@@ -311,18 +315,11 @@ export const executeReview = <
|
|
|
311
315
|
const key = `${concern.title}\u0000${concern.body}`;
|
|
312
316
|
return activeConcernKeys.has(key) && !currentConcernKeys.has(key);
|
|
313
317
|
});
|
|
314
|
-
const
|
|
315
|
-
const coverage = assessReviewCoverage({
|
|
316
|
-
shape: options.reviewShape ?? "flat",
|
|
317
|
-
files,
|
|
318
|
-
totalFiles: reviewTotalFiles,
|
|
319
|
-
anchorFiles,
|
|
320
|
-
totalAnchorFiles: metadata.totalChangedFiles,
|
|
321
|
-
events,
|
|
322
|
-
});
|
|
318
|
+
const { assurance, coverage, inputCoverage } = pipeline;
|
|
323
319
|
const stateCandidate =
|
|
324
320
|
executionContext !== undefined &&
|
|
325
|
-
|
|
321
|
+
inputCoverage.status === "complete" &&
|
|
322
|
+
assurance.status === "settled" &&
|
|
326
323
|
fingerprint !== undefined &&
|
|
327
324
|
metadata.baseSha !== undefined &&
|
|
328
325
|
executionContext.stateAuthenticator?.status === "available"
|
|
@@ -349,7 +346,8 @@ export const executeReview = <
|
|
|
349
346
|
marker: undefined,
|
|
350
347
|
notice:
|
|
351
348
|
executionContext?.stateAuthenticator?.status === "unavailable" &&
|
|
352
|
-
|
|
349
|
+
inputCoverage.status === "complete" &&
|
|
350
|
+
assurance.status === "settled"
|
|
353
351
|
? (executionContext.stateAuthenticator.unavailableReason ??
|
|
354
352
|
"authenticated continuity state is unavailable")
|
|
355
353
|
: undefined,
|
|
@@ -377,8 +375,13 @@ export const executeReview = <
|
|
|
377
375
|
runUrl: options.runUrl,
|
|
378
376
|
usage,
|
|
379
377
|
usageScope: options.usageScope,
|
|
380
|
-
fingerprint:
|
|
378
|
+
fingerprint:
|
|
379
|
+
inputCoverage.status === "complete" && assurance.status === "settled"
|
|
380
|
+
? fingerprint
|
|
381
|
+
: undefined,
|
|
381
382
|
coverage,
|
|
383
|
+
inputCoverage,
|
|
384
|
+
assurance,
|
|
382
385
|
carriedFindings,
|
|
383
386
|
carriedConcerns,
|
|
384
387
|
reviewMode: executionContext?.mode,
|
|
@@ -398,6 +401,8 @@ export const executeReview = <
|
|
|
398
401
|
activeFindings,
|
|
399
402
|
activeConcerns,
|
|
400
403
|
coverage,
|
|
404
|
+
inputCoverage,
|
|
405
|
+
assurance,
|
|
401
406
|
plan,
|
|
402
407
|
turns: result.turns,
|
|
403
408
|
usage,
|
|
@@ -415,6 +420,8 @@ export const executeReview = <
|
|
|
415
420
|
activeFindings,
|
|
416
421
|
activeConcerns,
|
|
417
422
|
coverage,
|
|
423
|
+
inputCoverage,
|
|
424
|
+
assurance,
|
|
418
425
|
plan,
|
|
419
426
|
published,
|
|
420
427
|
turns: result.turns,
|